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

View File

@@ -3,18 +3,23 @@ package resolvers
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/pocketbase/dbx"
"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/search"
"github.com/pocketbase/pocketbase/tools/security"
)
// maxNestedRels defines the max allowed nested relations depth.
const maxNestedRels = 6
// parseAndRun starts a new one-off RecordFieldResolver.Resolve execution.
func parseAndRun(fieldName string, resolver *RecordFieldResolver) (*search.ResolverResult, error) {
r := &runner{
@@ -334,6 +339,8 @@ func (r *runner) processRequestInfoRelationField(dataField *schema.SchemaField)
return r.processActiveProps()
}
var viaRegex = regexp.MustCompile(`^(\w+)_via_(\w+)$`)
func (r *runner) processActiveProps() (*search.ResolverResult, error) {
totalProps := len(r.activeProps)
@@ -393,12 +400,12 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
jePair := r.activeTableAlias + "." + cleanFieldName
result := &search.ResolverResult{
Identifier: jsonArrayLength(jePair),
Identifier: dbutils.JsonArrayLength(jePair),
}
if r.withMultiMatch {
jePair2 := r.multiMatchActiveTableAlias + "." + cleanFieldName
r.multiMatch.valueIdentifier = jsonArrayLength(jePair2)
r.multiMatch.valueIdentifier = dbutils.JsonArrayLength(jePair2)
result.MultiMatchSubQuery = r.multiMatch
}
@@ -410,7 +417,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
if field.Type == schema.FieldTypeSelect && modifier == eachModifier {
jePair := r.activeTableAlias + "." + cleanFieldName
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
r.resolver.registerJoin(jsonEach(jePair), jeAlias, nil)
r.resolver.registerJoin(dbutils.JsonEach(jePair), jeAlias, nil)
result := &search.ResolverResult{
Identifier: fmt.Sprintf("[[%s.value]]", jeAlias),
@@ -431,7 +438,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName + "_je"
r.multiMatch.joins = append(r.multiMatch.joins, &join{
tableName: jsonEach(jePair2),
tableName: dbutils.JsonEach(jePair2),
tableAlias: jeAlias2,
})
r.multiMatch.valueIdentifier = fmt.Sprintf("[[%s.value]]", jeAlias2)
@@ -458,9 +465,9 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
// (https://github.com/pocketbase/pocketbase/issues/4068)
if field.Type == schema.FieldTypeJson {
result.NoCoalesce = true
result.Identifier = jsonExtract(r.activeTableAlias+"."+cleanFieldName, "")
result.Identifier = dbutils.JsonExtract(r.activeTableAlias+"."+cleanFieldName, "")
if r.withMultiMatch {
r.multiMatch.valueIdentifier = jsonExtract(r.multiMatchActiveTableAlias+"."+cleanFieldName, "")
r.multiMatch.valueIdentifier = dbutils.JsonExtract(r.multiMatchActiveTableAlias+"."+cleanFieldName, "")
}
}
@@ -468,23 +475,19 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
}
field := collection.Schema.GetFieldByName(prop)
if field == nil {
if r.nullifyMisingField {
return &search.ResolverResult{Identifier: "NULL"}, nil
}
return nil, fmt.Errorf("unknown field %q", prop)
}
// check if it is a json field
if field.Type == schema.FieldTypeJson {
// json field -> treat the rest of the props as json path
if field != nil && field.Type == schema.FieldTypeJson {
var jsonPath strings.Builder
for _, p := range r.activeProps[i+1:] {
for j, p := range r.activeProps[i+1:] {
if _, err := strconv.Atoi(p); err == nil {
jsonPath.WriteString("[")
jsonPath.WriteString(inflector.Columnify(p))
jsonPath.WriteString("]")
} else {
jsonPath.WriteString(".")
if j > 0 {
jsonPath.WriteString(".")
}
jsonPath.WriteString(inflector.Columnify(p))
}
}
@@ -492,18 +495,130 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
result := &search.ResolverResult{
NoCoalesce: true,
Identifier: jsonExtract(r.activeTableAlias+"."+inflector.Columnify(prop), jsonPathStr),
Identifier: dbutils.JsonExtract(r.activeTableAlias+"."+inflector.Columnify(prop), jsonPathStr),
}
if r.withMultiMatch {
r.multiMatch.valueIdentifier = jsonExtract(r.multiMatchActiveTableAlias+"."+inflector.Columnify(prop), jsonPathStr)
r.multiMatch.valueIdentifier = dbutils.JsonExtract(r.multiMatchActiveTableAlias+"."+inflector.Columnify(prop), jsonPathStr)
result.MultiMatchSubQuery = r.multiMatch
}
return result, nil
}
// check if it is a relation field
if i >= maxNestedRels {
return nil, fmt.Errorf("max nested relations reached for field %q", prop)
}
// check for back relation (eg. yourCollection_via_yourRelField)
// -----------------------------------------------------------
if field == nil {
parts := viaRegex.FindStringSubmatch(prop)
if len(parts) != 3 {
if r.nullifyMisingField {
return &search.ResolverResult{Identifier: "NULL"}, nil
}
return nil, fmt.Errorf("unknown field %q", prop)
}
backCollection, err := r.resolver.loadCollection(parts[1])
if err != nil {
return nil, fmt.Errorf("failed to resolve field %q", prop)
}
backField := backCollection.Schema.GetFieldByName(parts[2])
if backField == nil || backField.Type != schema.FieldTypeRelation {
return nil, fmt.Errorf("invalid or missing back relation field %q", parts[2])
}
backField.InitOptions()
backFieldOptions, ok := backField.Options.(*schema.RelationOptions)
if !ok {
return nil, fmt.Errorf("failed to initialize back relation field %q options", backField.Name)
}
if backFieldOptions.CollectionId != collection.Id {
return nil, fmt.Errorf("invalid back relation field %q collection reference", backField.Name)
}
// join the back relation to the main query
// ---
cleanProp := inflector.Columnify(prop)
cleanBackFieldName := inflector.Columnify(backField.Name)
newTableAlias := r.activeTableAlias + "_" + cleanProp
newCollectionName := inflector.Columnify(backCollection.Name)
isBackRelMultiple := backFieldOptions.IsMultiple()
if !isBackRelMultiple {
// additionally check if the rel field has a single column unique index
isBackRelMultiple = !dbutils.HasSingleColumnUniqueIndex(backField.Name, backCollection.Indexes)
}
if !isBackRelMultiple {
r.resolver.registerJoin(
newCollectionName,
newTableAlias,
dbx.NewExp(fmt.Sprintf("[[%s.%s]] = [[%s.id]]", newTableAlias, cleanBackFieldName, r.activeTableAlias)),
)
} else {
jeAlias := r.activeTableAlias + "_" + cleanProp + "_je"
r.resolver.registerJoin(
newCollectionName,
newTableAlias,
dbx.NewExp(fmt.Sprintf(
"[[%s.id]] IN (SELECT [[%s.value]] FROM %s {{%s}})",
r.activeTableAlias,
jeAlias,
dbutils.JsonEach(newTableAlias+"."+cleanBackFieldName),
jeAlias,
)),
)
}
r.activeCollectionName = newCollectionName
r.activeTableAlias = newTableAlias
// ---
// join the back relation to the multi-match subquery
// ---
if isBackRelMultiple {
r.withMultiMatch = true // enable multimatch if not already
}
newTableAlias2 := r.multiMatchActiveTableAlias + "_" + cleanProp
if !isBackRelMultiple {
r.multiMatch.joins = append(
r.multiMatch.joins,
&join{
tableName: newCollectionName,
tableAlias: newTableAlias2,
on: dbx.NewExp(fmt.Sprintf("[[%s.%s]] = [[%s.id]]", newTableAlias2, cleanBackFieldName, r.multiMatchActiveTableAlias)),
},
)
} else {
jeAlias2 := r.multiMatchActiveTableAlias + "_" + cleanProp + "_je"
r.multiMatch.joins = append(
r.multiMatch.joins,
&join{
tableName: newCollectionName,
tableAlias: newTableAlias2,
on: dbx.NewExp(fmt.Sprintf(
"[[%s.id]] IN (SELECT [[%s.value]] FROM %s {{%s}})",
r.multiMatchActiveTableAlias,
jeAlias2,
dbutils.JsonEach(newTableAlias2+"."+cleanBackFieldName),
jeAlias2,
)),
},
)
}
r.multiMatchActiveTableAlias = newTableAlias2
// ---
continue
}
// -----------------------------------------------------------
// check for direct relation
if field.Type != schema.FieldTypeRelation {
return nil, fmt.Errorf("field %q is not a valid relation", prop)
}
@@ -534,7 +649,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
)
} else {
jeAlias := r.activeTableAlias + "_" + cleanFieldName + "_je"
r.resolver.registerJoin(jsonEach(prefixedFieldName), jeAlias, nil)
r.resolver.registerJoin(dbutils.JsonEach(prefixedFieldName), jeAlias, nil)
r.resolver.registerJoin(
inflector.Columnify(newCollectionName),
newTableAlias,
@@ -549,7 +664,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
// join the relation to the multi-match subquery
// ---
if options.IsMultiple() {
r.withMultiMatch = true
r.withMultiMatch = true // enable multimatch if not already
}
newTableAlias2 := r.multiMatchActiveTableAlias + "_" + cleanFieldName
@@ -569,7 +684,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
r.multiMatch.joins = append(
r.multiMatch.joins,
&join{
tableName: jsonEach(prefixedFieldName2),
tableName: dbutils.JsonEach(prefixedFieldName2),
tableAlias: jeAlias2,
},
&join{
@@ -587,34 +702,6 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
return nil, fmt.Errorf("failed to resolve field %q", r.fieldName)
}
func jsonArrayLength(tableColumnPair string) string {
return fmt.Sprintf(
// note: the case is used to normalize value access for single and multiple relations.
`json_array_length(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE (CASE WHEN [[%s]] = '' OR [[%s]] IS NULL THEN json_array() ELSE json_array([[%s]]) END) END)`,
tableColumnPair, tableColumnPair, tableColumnPair, tableColumnPair, tableColumnPair,
)
}
func jsonEach(tableColumnPair string) string {
return fmt.Sprintf(
// note: the case is used to normalize value access for single and multiple relations.
`json_each(CASE WHEN json_valid([[%s]]) THEN [[%s]] ELSE json_array([[%s]]) END)`,
tableColumnPair, tableColumnPair, tableColumnPair,
)
}
func jsonExtract(tableColumnPair string, path string) string {
return fmt.Sprintf(
// note: the extra object wrapping is needed to workaround the cases where a json_extract is used with non-json columns.
"(CASE WHEN json_valid([[%s]]) THEN JSON_EXTRACT([[%s]], '$%s') ELSE JSON_EXTRACT(json_object('pb', [[%s]]), '$.pb%s') END)",
tableColumnPair,
tableColumnPair,
path,
tableColumnPair,
path,
)
}
func resolvableSystemFieldNames(collection *models.Collection) []string {
result := schema.BaseModelFieldNames()