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