You've already forked pocketbase
mirror of
https://github.com/pocketbase/pocketbase.git
synced 2025-12-13 11:58:27 +02:00
initial v0.8 pre-release
This commit is contained in:
@@ -3,6 +3,7 @@ package resolvers
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
@@ -19,6 +20,20 @@ import (
|
||||
// ensure that `search.FieldResolver` interface is implemented
|
||||
var _ search.FieldResolver = (*RecordFieldResolver)(nil)
|
||||
|
||||
// list of auth filter fields that don't require join with the auth
|
||||
// collection or any other extra checks to be resolved
|
||||
var plainRequestAuthFields = []string{
|
||||
"@request.auth." + schema.FieldNameId,
|
||||
"@request.auth." + schema.FieldNameCollectionId,
|
||||
"@request.auth." + schema.FieldNameCollectionName,
|
||||
"@request.auth." + schema.FieldNameUsername,
|
||||
"@request.auth." + schema.FieldNameEmail,
|
||||
"@request.auth." + schema.FieldNameEmailVisibility,
|
||||
"@request.auth." + schema.FieldNameVerified,
|
||||
"@request.auth." + schema.FieldNameCreated,
|
||||
"@request.auth." + schema.FieldNameUpdated,
|
||||
}
|
||||
|
||||
type join struct {
|
||||
id string
|
||||
table string
|
||||
@@ -35,28 +50,37 @@ type join struct {
|
||||
type RecordFieldResolver struct {
|
||||
dao *daos.Dao
|
||||
baseCollection *models.Collection
|
||||
allowHiddenFields bool
|
||||
allowedFields []string
|
||||
requestData map[string]any
|
||||
joins []join // we cannot use a map because the insertion order is not preserved
|
||||
loadedCollections []*models.Collection
|
||||
joins []join // we cannot use a map because the insertion order is not preserved
|
||||
exprs []dbx.Expression
|
||||
}
|
||||
|
||||
// NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`.
|
||||
//
|
||||
// @todo consider changing in v0.8+:
|
||||
// - requestData to a typed struct when introducing the "IN" operator
|
||||
// - allowHiddenFields -> isSystemAdmin
|
||||
func NewRecordFieldResolver(
|
||||
dao *daos.Dao,
|
||||
baseCollection *models.Collection,
|
||||
requestData map[string]any,
|
||||
allowHiddenFields bool,
|
||||
) *RecordFieldResolver {
|
||||
return &RecordFieldResolver{
|
||||
dao: dao,
|
||||
baseCollection: baseCollection,
|
||||
requestData: requestData,
|
||||
allowHiddenFields: allowHiddenFields,
|
||||
joins: []join{},
|
||||
exprs: []dbx.Expression{},
|
||||
loadedCollections: []*models.Collection{baseCollection},
|
||||
allowedFields: []string{
|
||||
`^\w+[\w\.]*$`,
|
||||
`^\@request\.method$`,
|
||||
`^\@request\.user\.\w+[\w\.]*$`,
|
||||
`^\@request\.auth\.\w+[\w\.]*$`,
|
||||
`^\@request\.data\.\w+[\w\.]*$`,
|
||||
`^\@request\.query\.\w+[\w\.]*$`,
|
||||
`^\@collection\.\w+\.\w+[\w\.]*$`,
|
||||
@@ -77,6 +101,12 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||
}
|
||||
}
|
||||
|
||||
for _, expr := range r.exprs {
|
||||
if expr != nil {
|
||||
query.AndWhere(expr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -86,7 +116,7 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
||||
// id
|
||||
// project.screen.status
|
||||
// @request.status
|
||||
// @request.user.profile.someRelation.name
|
||||
// @request.auth.someRelation.name
|
||||
// @collection.product.name
|
||||
func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, placeholderParams dbx.Params, err error) {
|
||||
if len(r.allowedFields) > 0 && !list.ExistInSliceWithRegex(fieldName, r.allowedFields) {
|
||||
@@ -98,6 +128,11 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
currentCollectionName := r.baseCollection.Name
|
||||
currentTableAlias := inflector.Columnify(currentCollectionName)
|
||||
|
||||
// flag indicating whether to return null on missing field or return on an error
|
||||
nullifyMisingField := false
|
||||
|
||||
allowHiddenFields := r.allowHiddenFields
|
||||
|
||||
// check for @collection field (aka. non-relational join)
|
||||
// must be in the format "@collection.COLLECTION_NAME.FIELD[.FIELD2....]"
|
||||
if props[0] == "@collection" {
|
||||
@@ -113,55 +148,70 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
|
||||
}
|
||||
|
||||
r.addJoin(inflector.Columnify(collection.Name), currentTableAlias, nil)
|
||||
// always allow hidden fields since the @collection.* filter is a system one
|
||||
allowHiddenFields = true
|
||||
|
||||
r.registerJoin(inflector.Columnify(collection.Name), currentTableAlias, nil)
|
||||
|
||||
props = props[2:] // leave only the collection fields
|
||||
} else if props[0] == "@request" {
|
||||
// check for @request field
|
||||
if len(props) == 1 {
|
||||
return "", nil, fmt.Errorf("Invalid @request data field path in %q.", fieldName)
|
||||
}
|
||||
|
||||
// not a profile relational field
|
||||
if !strings.HasPrefix(fieldName, "@request.user.profile.") {
|
||||
// plain @request.* field
|
||||
if !strings.HasPrefix(fieldName, "@request.auth.") || list.ExistInSlice(fieldName, plainRequestAuthFields) {
|
||||
return r.resolveStaticRequestField(props[1:]...)
|
||||
}
|
||||
|
||||
// resolve the profile collection fields
|
||||
currentCollectionName = models.ProfileCollectionName
|
||||
currentTableAlias = inflector.Columnify("__user_" + currentCollectionName)
|
||||
// always allow hidden fields since the @request.* filter is a system one
|
||||
allowHiddenFields = true
|
||||
|
||||
collection, err := r.loadCollection(currentCollectionName)
|
||||
// enable the ignore flag for missing @request.auth.* fields
|
||||
// for consistency with @request.data.* and @request.query.*
|
||||
nullifyMisingField = true
|
||||
|
||||
// resolve the auth collection fields
|
||||
// ---
|
||||
rawAuthRecordId, _ := extractNestedMapVal(r.requestData, "auth", "id")
|
||||
authRecordId := cast.ToString(rawAuthRecordId)
|
||||
if authRecordId == "" {
|
||||
return "NULL", nil, nil
|
||||
}
|
||||
|
||||
rawAuthCollectionId, _ := extractNestedMapVal(r.requestData, "auth", schema.FieldNameCollectionId)
|
||||
authCollectionId := cast.ToString(rawAuthCollectionId)
|
||||
if authCollectionId == "" {
|
||||
return "NULL", nil, nil
|
||||
}
|
||||
|
||||
collection, err := r.loadCollection(authCollectionId)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Failed to load collection %q from field path %q.", currentCollectionName, fieldName)
|
||||
}
|
||||
|
||||
profileIdPlaceholder, profileIdPlaceholderParam, err := r.resolveStaticRequestField("user", "profile", "id")
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Failed to resolve @request.user.profile.id path in %q.", fieldName)
|
||||
}
|
||||
if strings.ToLower(profileIdPlaceholder) == "null" {
|
||||
// the user doesn't have an associated profile
|
||||
return "NULL", nil, nil
|
||||
}
|
||||
currentCollectionName = collection.Name
|
||||
currentTableAlias = "__auth_" + inflector.Columnify(currentCollectionName)
|
||||
|
||||
// join the profile collection
|
||||
r.addJoin(
|
||||
authIdParamKey := "auth" + security.RandomString(5)
|
||||
authIdParams := dbx.Params{authIdParamKey: authRecordId}
|
||||
// ---
|
||||
|
||||
// join the auth collection
|
||||
r.registerJoin(
|
||||
inflector.Columnify(collection.Name),
|
||||
currentTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf(
|
||||
// aka. profiles.id = profileId
|
||||
"[[%s.id]] = %s",
|
||||
currentTableAlias,
|
||||
profileIdPlaceholder,
|
||||
), profileIdPlaceholderParam),
|
||||
// aka. __auth_users.id = :userId
|
||||
"[[%s.id]] = {:%s}",
|
||||
inflector.Columnify(currentTableAlias),
|
||||
authIdParamKey,
|
||||
), authIdParams),
|
||||
)
|
||||
|
||||
props = props[3:] // leave only the profile fields
|
||||
props = props[2:] // leave only the auth relation fields
|
||||
}
|
||||
|
||||
baseModelFields := schema.ReservedFieldNames()
|
||||
|
||||
totalProps := len(props)
|
||||
|
||||
for i, prop := range props {
|
||||
@@ -170,13 +220,37 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
return "", nil, fmt.Errorf("Failed to resolve field %q.", prop)
|
||||
}
|
||||
|
||||
// base model prop (always available but not part of the collection schema)
|
||||
if list.ExistInSlice(prop, baseModelFields) {
|
||||
systemFieldNames := schema.BaseModelFieldNames()
|
||||
if collection.IsAuth() {
|
||||
systemFieldNames = append(
|
||||
systemFieldNames,
|
||||
schema.FieldNameUsername,
|
||||
schema.FieldNameVerified,
|
||||
schema.FieldNameEmailVisibility,
|
||||
schema.FieldNameEmail,
|
||||
)
|
||||
}
|
||||
|
||||
// internal model prop (always available but not part of the collection schema)
|
||||
if list.ExistInSlice(prop, systemFieldNames) {
|
||||
// allow querying only auth records with emails marked as public
|
||||
if prop == schema.FieldNameEmail && !allowHiddenFields {
|
||||
r.registerExpr(dbx.NewExp(fmt.Sprintf(
|
||||
"[[%s.%s]] = TRUE",
|
||||
currentTableAlias,
|
||||
inflector.Columnify(schema.FieldNameEmailVisibility),
|
||||
)))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
|
||||
}
|
||||
|
||||
field := collection.Schema.GetFieldByName(prop)
|
||||
if field == nil {
|
||||
if nullifyMisingField {
|
||||
return "NULL", nil, nil
|
||||
}
|
||||
|
||||
return "", nil, fmt.Errorf("Unrecognized field %q.", prop)
|
||||
}
|
||||
|
||||
@@ -185,6 +259,28 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
return fmt.Sprintf("[[%s.%s]]", currentTableAlias, inflector.Columnify(prop)), nil, nil
|
||||
}
|
||||
|
||||
// check if it is a json field
|
||||
if field.Type == schema.FieldTypeJson {
|
||||
var jsonPath strings.Builder
|
||||
jsonPath.WriteString("$")
|
||||
for _, p := range props[i+1:] {
|
||||
if _, err := strconv.Atoi(p); err == nil {
|
||||
jsonPath.WriteString("[")
|
||||
jsonPath.WriteString(inflector.Columnify(p))
|
||||
jsonPath.WriteString("]")
|
||||
} else {
|
||||
jsonPath.WriteString(".")
|
||||
jsonPath.WriteString(inflector.Columnify(p))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"JSON_EXTRACT([[%s.%s]], '%s')",
|
||||
currentTableAlias,
|
||||
inflector.Columnify(prop),
|
||||
jsonPath.String(),
|
||||
), nil, nil
|
||||
}
|
||||
|
||||
// check if it is a relation field
|
||||
if field.Type != schema.FieldTypeRelation {
|
||||
return "", nil, fmt.Errorf("Field %q is not a valid relation.", prop)
|
||||
@@ -210,7 +306,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
jeTable := currentTableAlias + "_" + cleanFieldName + "_je"
|
||||
jePair := currentTableAlias + "." + cleanFieldName
|
||||
|
||||
r.addJoin(
|
||||
r.registerJoin(
|
||||
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)`,
|
||||
@@ -219,7 +315,7 @@ func (r *RecordFieldResolver) Resolve(fieldName string) (resultName string, plac
|
||||
jeTable,
|
||||
nil,
|
||||
)
|
||||
r.addJoin(
|
||||
r.registerJoin(
|
||||
inflector.Columnify(newCollectionName),
|
||||
newTableAlias,
|
||||
dbx.NewExp(fmt.Sprintf("[[%s.id]] = [[%s.value]]", newTableAlias, jeTable)),
|
||||
@@ -306,7 +402,7 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*models
|
||||
return collection, nil
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) addJoin(tableName string, tableAlias string, on dbx.Expression) {
|
||||
func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, on dbx.Expression) {
|
||||
tableExpr := fmt.Sprintf("%s %s", tableName, tableAlias)
|
||||
|
||||
join := join{
|
||||
@@ -326,3 +422,7 @@ func (r *RecordFieldResolver) addJoin(tableName string, tableAlias string, on db
|
||||
// register new join
|
||||
r.joins = append(r.joins, join)
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) registerExpr(expr dbx.Expression) {
|
||||
r.exprs = append(r.exprs, expr)
|
||||
}
|
||||
|
||||
@@ -14,111 +14,155 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
|
||||
app, _ := tests.NewTestApp()
|
||||
defer app.Cleanup()
|
||||
|
||||
collection, err := app.Dao().FindCollectionByNameOrId("demo4")
|
||||
authRecord, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
requestData := map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c",
|
||||
"profile": map[string]any{
|
||||
"id": "d13f60a4-5765-48c7-9e1d-3e782340f833",
|
||||
"name": "test",
|
||||
},
|
||||
},
|
||||
"auth": authRecord.PublicExport(),
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
fields []string
|
||||
expectQuery string
|
||||
name string
|
||||
collectionIdOrName string
|
||||
fields []string
|
||||
allowHiddenFields bool
|
||||
expectQuery string
|
||||
}{
|
||||
{
|
||||
"missing field",
|
||||
"demo4",
|
||||
[]string{""},
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
},
|
||||
{
|
||||
"non relation field",
|
||||
"demo4",
|
||||
[]string{"title"},
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
},
|
||||
{
|
||||
"incomplete rel",
|
||||
[]string{"onerel"},
|
||||
"demo4",
|
||||
[]string{"self_rel_one"},
|
||||
false,
|
||||
"SELECT `demo4`.* FROM `demo4`",
|
||||
},
|
||||
{
|
||||
"single rel",
|
||||
[]string{"onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
|
||||
"single rel (self rel)",
|
||||
"demo4",
|
||||
[]string{"self_rel_one.title"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]]",
|
||||
},
|
||||
{
|
||||
"single rel (other collection)",
|
||||
"demo4",
|
||||
[]string{"rel_one_cascade.title"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.rel_one_cascade]]) THEN [[demo4.rel_one_cascade]] ELSE json_array([[demo4.rel_one_cascade]]) END) `demo4_rel_one_cascade_je` LEFT JOIN `demo3` `demo4_rel_one_cascade` ON [[demo4_rel_one_cascade.id]] = [[demo4_rel_one_cascade_je.value]]",
|
||||
},
|
||||
{
|
||||
"non-relation field + single rel",
|
||||
[]string{"title", "onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
|
||||
"demo4",
|
||||
[]string{"title", "self_rel_one.title"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]]",
|
||||
},
|
||||
{
|
||||
"nested incomplete rels",
|
||||
[]string{"manyrels.onerel"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]]",
|
||||
"demo4",
|
||||
[]string{"self_rel_many.self_rel_one"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]]",
|
||||
},
|
||||
{
|
||||
"nested complete rels",
|
||||
[]string{"manyrels.onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels.onerel]]) THEN [[demo4_manyrels.onerel]] ELSE json_array([[demo4_manyrels.onerel]]) END) `demo4_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels_onerel.id]] = [[demo4_manyrels_onerel_je.value]]",
|
||||
"demo4",
|
||||
[]string{"self_rel_many.self_rel_one.title"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_je.value]]",
|
||||
},
|
||||
{
|
||||
"repeated nested rels",
|
||||
[]string{"manyrels.onerel.manyrels.onerel.title"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels.onerel]]) THEN [[demo4_manyrels.onerel]] ELSE json_array([[demo4_manyrels.onerel]]) END) `demo4_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel` ON [[demo4_manyrels_onerel.id]] = [[demo4_manyrels_onerel_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels_onerel.manyrels]]) THEN [[demo4_manyrels_onerel.manyrels]] ELSE json_array([[demo4_manyrels_onerel.manyrels]]) END) `demo4_manyrels_onerel_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels` ON [[demo4_manyrels_onerel_manyrels.id]] = [[demo4_manyrels_onerel_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_manyrels_onerel_manyrels.onerel]]) THEN [[demo4_manyrels_onerel_manyrels.onerel]] ELSE json_array([[demo4_manyrels_onerel_manyrels.onerel]]) END) `demo4_manyrels_onerel_manyrels_onerel_je` LEFT JOIN `demo4` `demo4_manyrels_onerel_manyrels_onerel` ON [[demo4_manyrels_onerel_manyrels_onerel.id]] = [[demo4_manyrels_onerel_manyrels_onerel_je.value]]",
|
||||
"demo4",
|
||||
[]string{"self_rel_many.self_rel_one.self_rel_many.self_rel_one.title"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one.self_rel_many]]) THEN [[demo4_self_rel_many_self_rel_one.self_rel_many]] ELSE json_array([[demo4_self_rel_many_self_rel_one.self_rel_many]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many` ON [[demo4_self_rel_many_self_rel_one_self_rel_many.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) THEN [[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]] ELSE json_array([[demo4_self_rel_many_self_rel_one_self_rel_many.self_rel_one]]) END) `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one` ON [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.id]] = [[demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one_je.value]]",
|
||||
},
|
||||
{
|
||||
"multiple rels",
|
||||
[]string{"manyrels.title", "onerel.onefile"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.manyrels]]) THEN [[demo4.manyrels]] ELSE json_array([[demo4.manyrels]]) END) `demo4_manyrels_je` LEFT JOIN `demo4` `demo4_manyrels` ON [[demo4_manyrels.id]] = [[demo4_manyrels_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4.onerel]]) THEN [[demo4.onerel]] ELSE json_array([[demo4.onerel]]) END) `demo4_onerel_je` LEFT JOIN `demo4` `demo4_onerel` ON [[demo4_onerel.id]] = [[demo4_onerel_je.value]]",
|
||||
"demo4",
|
||||
[]string{"self_rel_many.title", "self_rel_one.onefile"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_many]]) THEN [[demo4.self_rel_many]] ELSE json_array([[demo4.self_rel_many]]) END) `demo4_self_rel_many_je` LEFT JOIN `demo4` `demo4_self_rel_many` ON [[demo4_self_rel_many.id]] = [[demo4_self_rel_many_je.value]] LEFT JOIN json_each(CASE WHEN json_valid([[demo4.self_rel_one]]) THEN [[demo4.self_rel_one]] ELSE json_array([[demo4.self_rel_one]]) END) `demo4_self_rel_one_je` LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4_self_rel_one_je.value]]",
|
||||
},
|
||||
{
|
||||
"@collection join",
|
||||
[]string{"@collection.demo.title", "@collection.demo2.text", "@collection.demo.file"},
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo` `__collection_demo` LEFT JOIN `demo2` `__collection_demo2`",
|
||||
"demo4",
|
||||
[]string{"@collection.demo1.text", "@collection.demo2.active", "@collection.demo1.file_one"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo1` `__collection_demo1` LEFT JOIN `demo2` `__collection_demo2`",
|
||||
},
|
||||
{
|
||||
"static @request.user.profile fields",
|
||||
[]string{"@request.user.id", "@request.user.profile.id", "@request.data.demo"},
|
||||
"@request.auth fields",
|
||||
"demo4",
|
||||
[]string{"@request.auth.id", "@request.auth.username", "@request.auth.rel.title", "@request.data.demo"},
|
||||
false,
|
||||
"^" +
|
||||
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `profiles` `__user_profiles` ON [[__user_profiles.id]] =") +
|
||||
" {:.*}$",
|
||||
},
|
||||
{
|
||||
"relational @request.user.profile fields",
|
||||
[]string{"@request.user.profile.rel.id", "@request.user.profile.rel.name"},
|
||||
"^" +
|
||||
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `profiles` `__user_profiles` ON [[__user_profiles.id]] =") +
|
||||
regexp.QuoteMeta("SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__auth_users` ON [[__auth_users.id]] =") +
|
||||
" {:.*} " +
|
||||
regexp.QuoteMeta("LEFT JOIN json_each(CASE WHEN json_valid([[__user_profiles.rel]]) THEN [[__user_profiles.rel]] ELSE json_array([[__user_profiles.rel]]) END) `__user_profiles_rel_je` LEFT JOIN `profiles` `__user_profiles_rel` ON [[__user_profiles_rel.id]] = [[__user_profiles_rel_je.value]]") +
|
||||
regexp.QuoteMeta("LEFT JOIN json_each(CASE WHEN json_valid([[__auth_users.rel]]) THEN [[__auth_users.rel]] ELSE json_array([[__auth_users.rel]]) END) `__auth_users_rel_je` LEFT JOIN `demo2` `__auth_users_rel` ON [[__auth_users_rel.id]] = [[__auth_users_rel_je.value]]") +
|
||||
"$",
|
||||
},
|
||||
{
|
||||
"hidden field with system filters (ignore emailVisibility)",
|
||||
"demo4",
|
||||
[]string{"@collection.users.email", "@request.auth.email"},
|
||||
false,
|
||||
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `users` `__collection_users`",
|
||||
},
|
||||
{
|
||||
"hidden field (add emailVisibility)",
|
||||
"users",
|
||||
[]string{"email"},
|
||||
false,
|
||||
"SELECT `users`.* FROM `users` WHERE [[users.emailVisibility]] = TRUE",
|
||||
},
|
||||
{
|
||||
"hidden field (force ignore emailVisibility)",
|
||||
"users",
|
||||
[]string{"email"},
|
||||
true,
|
||||
"SELECT `users`.* FROM `users`",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
collection, err := app.Dao().FindCollectionByNameOrId(s.collectionIdOrName)
|
||||
if err != nil {
|
||||
t.Errorf("[%s] Failed to load collection %s: %v", s.name, s.collectionIdOrName, err)
|
||||
}
|
||||
|
||||
query := app.Dao().RecordQuery(collection)
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, s.allowHiddenFields)
|
||||
for _, field := range s.fields {
|
||||
r.Resolve(field)
|
||||
}
|
||||
|
||||
if err := r.UpdateQuery(query); err != nil {
|
||||
t.Errorf("(%s) UpdateQuery failed with error %v", s.name, err)
|
||||
t.Errorf("[%s] UpdateQuery failed with error %v", s.name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
rawQuery := query.Build().SQL()
|
||||
|
||||
if !list.ExistInSliceWithRegex(rawQuery, []string{s.expectQuery}) {
|
||||
t.Errorf("(%s) Expected query\n %v \ngot:\n %v", s.name, s.expectQuery, rawQuery)
|
||||
t.Errorf("[%s] Expected query\n %v \ngot:\n %v", s.name, s.expectQuery, rawQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,16 +176,16 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
requestData := map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c",
|
||||
"profile": map[string]any{
|
||||
"id": "d13f60a4-5765-48c7-9e1d-3e782340f833",
|
||||
},
|
||||
},
|
||||
authRecord, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
|
||||
requestData := map[string]any{
|
||||
"auth": authRecord.PublicExport(),
|
||||
}
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true)
|
||||
|
||||
scenarios := []struct {
|
||||
fieldName string
|
||||
@@ -157,28 +201,31 @@ func TestRecordFieldResolverResolveSchemaFields(t *testing.T) {
|
||||
{"updated", false, "[[demo4.updated]]"},
|
||||
{"title", false, "[[demo4.title]]"},
|
||||
{"title.test", true, ""},
|
||||
{"manyrels", false, "[[demo4.manyrels]]"},
|
||||
{"manyrels.", true, ""},
|
||||
{"manyrels.unknown", true, ""},
|
||||
{"manyrels.title", false, "[[demo4_manyrels.title]]"},
|
||||
{"manyrels.onerel.manyrels.onefile", false, "[[demo4_manyrels_onerel_manyrels.onefile]]"},
|
||||
// @request.user.profile relation join:
|
||||
{"@request.user.profile.rel", false, "[[__user_profiles.rel]]"},
|
||||
{"@request.user.profile.rel.name", false, "[[__user_profiles_rel.name]]"},
|
||||
{"self_rel_many", false, "[[demo4.self_rel_many]]"},
|
||||
{"self_rel_many.", true, ""},
|
||||
{"self_rel_many.unknown", true, ""},
|
||||
{"self_rel_many.title", false, "[[demo4_self_rel_many.title]]"},
|
||||
{"self_rel_many.self_rel_one.self_rel_many.title", false, "[[demo4_self_rel_many_self_rel_one_self_rel_many.title]]"},
|
||||
// json_extract
|
||||
{"json_array.0", false, "JSON_EXTRACT([[demo4.json_array]], '$[0]')"},
|
||||
{"json_object.a.b.c", false, "JSON_EXTRACT([[demo4.json_object]], '$.a.b.c')"},
|
||||
// @request.auth relation join:
|
||||
{"@request.auth.rel", false, "[[__auth_users.rel]]"},
|
||||
{"@request.auth.rel.title", false, "[[__auth_users_rel.title]]"},
|
||||
// @collection fieds:
|
||||
{"@collect", true, ""},
|
||||
{"collection.demo4.title", true, ""},
|
||||
{"@collection", true, ""},
|
||||
{"@collection.unknown", true, ""},
|
||||
{"@collection.demo", true, ""},
|
||||
{"@collection.demo.", true, ""},
|
||||
{"@collection.demo.title", false, "[[__collection_demo.title]]"},
|
||||
{"@collection.demo2", true, ""},
|
||||
{"@collection.demo2.", true, ""},
|
||||
{"@collection.demo2.title", false, "[[__collection_demo2.title]]"},
|
||||
{"@collection.demo4.title", false, "[[__collection_demo4.title]]"},
|
||||
{"@collection.demo4.id", false, "[[__collection_demo4.id]]"},
|
||||
{"@collection.demo4.created", false, "[[__collection_demo4.created]]"},
|
||||
{"@collection.demo4.updated", false, "[[__collection_demo4.updated]]"},
|
||||
{"@collection.demo4.manyrels.missing", true, ""},
|
||||
{"@collection.demo4.manyrels.onerel.manyrels.onerel.onefile", false, "[[__collection_demo4_manyrels_onerel_manyrels_onerel.onefile]]"},
|
||||
{"@collection.demo4.self_rel_many.missing", true, ""},
|
||||
{"@collection.demo4.self_rel_many.self_rel_one.self_rel_many.self_rel_one.title", false, "[[__collection_demo4_self_rel_many_self_rel_one_self_rel_many_self_rel_one.title]]"},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
@@ -210,6 +257,11 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
authRecord, err := app.Dao().FindRecordById("users", "4q1xlclmfloku33")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
requestData := map[string]any{
|
||||
"method": "get",
|
||||
"query": map[string]any{
|
||||
@@ -219,16 +271,10 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
"b": 456,
|
||||
"c": map[string]int{"sub": 1},
|
||||
},
|
||||
"user": map[string]any{
|
||||
"id": "4d0197cc-2b4a-3f83-a26b-d77bc8423d3c",
|
||||
"profile": map[string]any{
|
||||
"id": "d13f60a4-5765-48c7-9e1d-3e782340f833",
|
||||
"name": "test",
|
||||
},
|
||||
},
|
||||
"user": authRecord.PublicExport(),
|
||||
}
|
||||
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData)
|
||||
r := resolvers.NewRecordFieldResolver(app.Dao(), collection, requestData, true)
|
||||
|
||||
scenarios := []struct {
|
||||
fieldName string
|
||||
@@ -247,9 +293,9 @@ func TestRecordFieldResolverResolveStaticRequestDataFields(t *testing.T) {
|
||||
{"@request.data.b", false, `456`},
|
||||
{"@request.data.b.missing", false, ``},
|
||||
{"@request.data.c", false, `"{\"sub\":1}"`},
|
||||
{"@request.user", true, ""},
|
||||
{"@request.user.id", false, `"4d0197cc-2b4a-3f83-a26b-d77bc8423d3c"`},
|
||||
{"@request.user.profile", false, `"{\"id\":\"d13f60a4-5765-48c7-9e1d-3e782340f833\",\"name\":\"test\"}"`},
|
||||
{"@request.auth", true, ""},
|
||||
{"@request.auth.id", false, `"4q1xlclmfloku33"`},
|
||||
{"@request.auth.file", false, `"[]"`},
|
||||
}
|
||||
|
||||
for i, s := range scenarios {
|
||||
|
||||
Reference in New Issue
Block a user