added back relation filter reference support

This commit is contained in:
Gani Georgiev
2024-02-19 16:55:34 +02:00
parent 4743c1ce72
commit 4937acb3e2
18 changed files with 660 additions and 169 deletions
+30 -27
View File
@@ -153,9 +153,11 @@ func TestFindCollectionReferences(t *testing.T) {
"rel_one_no_cascade",
"rel_one_no_cascade_required",
"rel_one_cascade",
"rel_one_unique",
"rel_many_no_cascade",
"rel_many_no_cascade_required",
"rel_many_cascade",
"rel_many_unique",
}
for col, fields := range result {
@@ -756,7 +758,7 @@ func TestImportCollections(t *testing.T) {
"demo1": 15,
"demo2": 2,
"demo3": 2,
"demo4": 11,
"demo4": 13,
"demo5": 6,
"new_import": 1,
}
@@ -774,37 +776,38 @@ func TestImportCollections(t *testing.T) {
},
}
for _, scenario := range scenarios {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
testApp, _ := tests.NewTestApp()
defer testApp.Cleanup()
importedCollections := []*models.Collection{}
importedCollections := []*models.Collection{}
// load data
loadErr := json.Unmarshal([]byte(scenario.jsonData), &importedCollections)
if loadErr != nil {
t.Fatalf("[%s] Failed to load data: %v", scenario.name, loadErr)
continue
}
// load data
loadErr := json.Unmarshal([]byte(s.jsonData), &importedCollections)
if loadErr != nil {
t.Fatalf("Failed to load data: %v", loadErr)
}
err := testApp.Dao().ImportCollections(importedCollections, scenario.deleteMissing, scenario.beforeRecordsSync)
err := testApp.Dao().ImportCollections(importedCollections, s.deleteMissing, s.beforeRecordsSync)
hasErr := err != nil
if hasErr != scenario.expectError {
t.Errorf("[%s] Expected hasErr to be %v, got %v (%v)", scenario.name, scenario.expectError, hasErr, err)
}
hasErr := err != nil
if hasErr != s.expectError {
t.Fatalf("Expected hasErr to be %v, got %v (%v)", s.expectError, hasErr, err)
}
// check collections count
collections := []*models.Collection{}
if err := testApp.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
if len(collections) != scenario.expectCollectionsCount {
t.Errorf("[%s] Expected %d collections, got %d", scenario.name, scenario.expectCollectionsCount, len(collections))
}
// check collections count
collections := []*models.Collection{}
if err := testApp.Dao().CollectionQuery().All(&collections); err != nil {
t.Fatal(err)
}
if len(collections) != s.expectCollectionsCount {
t.Fatalf("Expected %d collections, got %d", s.expectCollectionsCount, len(collections))
}
if scenario.afterTestFunc != nil {
scenario.afterTestFunc(testApp, collections)
}
if s.afterTestFunc != nil {
s.afterTestFunc(testApp, collections)
}
})
}
}
-2
View File
@@ -198,8 +198,6 @@ func (dao *Dao) FindRecordsByIds(
return records, nil
}
// @todo consider to depricate as it may be easier to just use dao.RecordQuery()
//
// FindRecordsByExpr finds all records by the specified db expression.
//
// Returns all collection records if no expressions are provided.
+54 -34
View File
@@ -1,7 +1,9 @@
package daos
import (
"errors"
"fmt"
"log"
"regexp"
"strings"
@@ -9,13 +11,14 @@ import (
"github.com/pocketbase/pocketbase/models"
"github.com/pocketbase/pocketbase/models/schema"
"github.com/pocketbase/pocketbase/tools/dbutils"
"github.com/pocketbase/pocketbase/tools/inflector"
"github.com/pocketbase/pocketbase/tools/list"
"github.com/pocketbase/pocketbase/tools/security"
"github.com/pocketbase/pocketbase/tools/types"
)
// MaxExpandDepth specifies the max allowed nested expand depth path.
//
// @todo Consider eventually reusing resolvers.maxNestedRels
const MaxExpandDepth = 6
// ExpandFetchFunc defines the function that is used to fetch the expanded relation records.
@@ -51,13 +54,15 @@ func (dao *Dao) ExpandRecords(records []*models.Record, expands []string, optFet
return failed
}
var indirectExpandRegex = regexp.MustCompile(`^(\w+)\((\w+)\)$`)
// Deprecated
var indirectExpandRegexOld = regexp.MustCompile(`^(\w+)\((\w+)\)$`)
var indirectExpandRegex = regexp.MustCompile(`^(\w+)_via_(\w+)$`)
// notes:
// - if fetchFunc is nil, dao.FindRecordsByIds will be used
// - all records are expected to be from the same collection
// - if MaxExpandDepth is reached, the function returns nil ignoring the remaining expand path
// - indirect expands are supported only with single relation fields
func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetchFunc ExpandFetchFunc, recursionLevel int) error {
if fetchFunc == nil {
// load a default fetchFunc
@@ -77,7 +82,22 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
var relCollection *models.Collection
parts := strings.SplitN(expandPath, ".", 2)
matches := indirectExpandRegex.FindStringSubmatch(parts[0])
var matches []string
// @todo remove the old syntax support
if strings.Contains(parts[0], "(") {
matches = indirectExpandRegexOld.FindStringSubmatch(parts[0])
if len(matches) == 3 {
log.Printf(
"%s expand format is deprecated and will be removed in the future. Consider replacing it with %s_via_%s.\n",
matches[0],
matches[1],
matches[2],
)
}
} else {
matches = indirectExpandRegex.FindStringSubmatch(parts[0])
}
if len(matches) == 3 {
indirectRel, _ := dao.FindCollectionByNameOrId(matches[1])
@@ -95,47 +115,47 @@ func (dao *Dao) expandRecords(records []*models.Record, expandPath string, fetch
if indirectRelFieldOptions == nil || indirectRelFieldOptions.CollectionId != mainCollection.Id {
return fmt.Errorf("Invalid indirect relation field path %q.", parts[0])
}
if indirectRelFieldOptions.IsMultiple() {
// for now don't allow multi-relation indirect fields expand
// due to eventual poor query performance with large data sets.
return fmt.Errorf("Multi-relation fields cannot be indirectly expanded in %q.", parts[0])
}
recordIds := make([]any, len(records))
for i, record := range records {
recordIds[i] = record.Id
}
// add the related id(s) as a dynamic relation field value to
// allow further expand checks at later stage in a more unified manner
prepErr := func() error {
q := dao.DB().Select("id").From(indirectRel.Name)
// @todo after the index optimizations consider allowing
// indirect expand for multi-relation fields
indirectRecords, err := dao.FindRecordsByExpr(
indirectRel.Id,
dbx.In(inflector.Columnify(matches[2]), recordIds...),
)
if err != nil {
return err
}
mappedIndirectRecordIds := make(map[string][]string, len(indirectRecords))
for _, indirectRecord := range indirectRecords {
recId := indirectRecord.GetString(matches[2])
if recId != "" {
mappedIndirectRecordIds[recId] = append(mappedIndirectRecordIds[recId], indirectRecord.Id)
if indirectRelFieldOptions.IsMultiple() {
q.AndWhere(dbx.Exists(dbx.NewExp(fmt.Sprintf(
"SELECT 1 FROM %s je WHERE je.value = {:id}",
dbutils.JsonEach(indirectRelField.Name),
))))
} else {
q.AndWhere(dbx.NewExp("[[" + indirectRelField.Name + "]] = {:id}"))
}
}
// add the indirect relation ids as a new relation field value
for _, record := range records {
relIds, ok := mappedIndirectRecordIds[record.Id]
if ok && len(relIds) > 0 {
record.Set(parts[0], relIds)
pq := q.Build().Prepare()
for _, record := range records {
var relIds []string
err := pq.Bind(dbx.Params{"id": record.Id}).Column(&relIds)
if err != nil {
return errors.Join(err, pq.Close())
}
if len(relIds) > 0 {
record.Set(parts[0], relIds)
}
}
return pq.Close()
}()
if prepErr != nil {
return prepErr
}
relFieldOptions = &schema.RelationOptions{
MaxSelect: nil,
CollectionId: indirectRel.Id,
}
if isRelFieldUnique(indirectRel, indirectRelField.Name) {
if dbutils.HasSingleColumnUniqueIndex(indirectRelField.Name, indirectRel.Indexes) {
relFieldOptions.MaxSelect = types.Pointer(1)
}
// indirect relation
+61 -13
View File
@@ -163,7 +163,7 @@ func TestExpandRecords(t *testing.T) {
0,
},
{
"simple indirect expand",
"simple back single relation field expand (deprecated syntax)",
"demo3",
[]string{"lcl9d87w22ml6jy"},
[]string{"demo4(rel_one_no_cascade_required)"},
@@ -174,11 +174,22 @@ func TestExpandRecords(t *testing.T) {
0,
},
{
"nested indirect expand",
"simple back expand via single relation field",
"demo3",
[]string{"lcl9d87w22ml6jy"},
[]string{"demo4_via_rel_one_no_cascade_required"},
func(c *models.Collection, ids []string) ([]*models.Record, error) {
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
},
1,
0,
},
{
"nested back expand via single relation field",
"demo3",
[]string{"lcl9d87w22ml6jy"},
[]string{
"demo4(rel_one_no_cascade_required).self_rel_many.self_rel_many.self_rel_one",
"demo4_via_rel_one_no_cascade_required.self_rel_many.self_rel_many.self_rel_one",
},
func(c *models.Collection, ids []string) ([]*models.Record, error) {
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
@@ -186,6 +197,19 @@ func TestExpandRecords(t *testing.T) {
5,
0,
},
{
"nested back expand via multiple relation field",
"demo3",
[]string{"lcl9d87w22ml6jy"},
[]string{
"demo4_via_rel_many_no_cascade_required.self_rel_many.rel_many_no_cascade_required.demo4_via_rel_many_no_cascade_required",
},
func(c *models.Collection, ids []string) ([]*models.Record, error) {
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
},
7,
0,
},
{
"expand multiple relations sharing a common path",
"demo4",
@@ -332,7 +356,7 @@ func TestExpandRecord(t *testing.T) {
0,
},
{
"simple indirect expand",
"simple indirect expand via single relation field (deprecated syntax)",
"demo3",
"lcl9d87w22ml6jy",
[]string{"demo4(rel_one_no_cascade_required)"},
@@ -343,7 +367,18 @@ func TestExpandRecord(t *testing.T) {
0,
},
{
"nested indirect expand",
"simple indirect expand via single relation field",
"demo3",
"lcl9d87w22ml6jy",
[]string{"demo4_via_rel_one_no_cascade_required"},
func(c *models.Collection, ids []string) ([]*models.Record, error) {
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
},
1,
0,
},
{
"nested indirect expand via single relation field",
"demo3",
"lcl9d87w22ml6jy",
[]string{
@@ -355,6 +390,19 @@ func TestExpandRecord(t *testing.T) {
5,
0,
},
{
"nested indirect expand via single relation field",
"demo3",
"lcl9d87w22ml6jy",
[]string{
"demo4_via_rel_many_no_cascade_required.self_rel_many.rel_many_no_cascade_required.demo4_via_rel_many_no_cascade_required",
},
func(c *models.Collection, ids []string) ([]*models.Record, error) {
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
},
7,
0,
},
}
for _, s := range scenarios {
@@ -388,21 +436,23 @@ func TestIndirectExpandSingeVsArrayResult(t *testing.T) {
// non-unique indirect expand
{
errs := app.Dao().ExpandRecord(record, []string{"demo4(rel_one_cascade)"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
errs := app.Dao().ExpandRecord(record, []string{"demo4_via_rel_one_cascade"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
})
if len(errs) > 0 {
t.Fatal(errs)
}
result, ok := record.Expand()["demo4(rel_one_cascade)"].([]*models.Record)
result, ok := record.Expand()["demo4_via_rel_one_cascade"].([]*models.Record)
if !ok {
t.Fatalf("Expected the expanded result to be a slice, got %v", result)
}
}
// mock a unique constraint for the rel_one_cascade field
// unique indirect expand
{
// mock a unique constraint for the rel_one_cascade field
// ---
demo4, err := app.Dao().FindCollectionByNameOrId("demo4")
if err != nil {
t.Fatal(err)
@@ -413,18 +463,16 @@ func TestIndirectExpandSingeVsArrayResult(t *testing.T) {
if err := app.Dao().SaveCollection(demo4); err != nil {
t.Fatalf("Failed to mock unique constraint: %v", err)
}
}
// ---
// non-unique indirect expand
{
errs := app.Dao().ExpandRecord(record, []string{"demo4(rel_one_cascade)"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
errs := app.Dao().ExpandRecord(record, []string{"demo4_via_rel_one_cascade"}, func(c *models.Collection, ids []string) ([]*models.Record, error) {
return app.Dao().FindRecordsByIds(c.Id, ids, nil)
})
if len(errs) > 0 {
t.Fatal(errs)
}
result, ok := record.Expand()["demo4(rel_one_cascade)"].(*models.Record)
result, ok := record.Expand()["demo4_via_rel_one_cascade"].(*models.Record)
if !ok {
t.Fatalf("Expected the expanded result to be a single model, got %v", result)
}