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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user