1
0
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:
Gani Georgiev
2022-10-30 10:28:14 +02:00
parent 9cbb2e750e
commit 90dba45d7c
388 changed files with 21580 additions and 13603 deletions

View File

@@ -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)
}

View File

@@ -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 {