added back relation filter reference support
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user