2024-09-29 19:23:19 +03:00
|
|
|
package core
|
2022-07-07 00:19:05 +03:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2024-09-29 19:23:19 +03:00
|
|
|
"errors"
|
2022-07-07 00:19:05 +03:00
|
|
|
"fmt"
|
2024-12-29 17:31:58 +02:00
|
|
|
"slices"
|
2022-10-30 10:28:14 +02:00
|
|
|
"strconv"
|
2022-07-07 00:19:05 +03:00
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/pocketbase/dbx"
|
|
|
|
"github.com/pocketbase/pocketbase/tools/search"
|
|
|
|
"github.com/pocketbase/pocketbase/tools/security"
|
2025-02-21 18:00:13 +02:00
|
|
|
"github.com/pocketbase/pocketbase/tools/types"
|
2022-07-07 00:19:05 +03:00
|
|
|
"github.com/spf13/cast"
|
|
|
|
)
|
|
|
|
|
2023-01-07 22:25:56 +02:00
|
|
|
// filter modifiers
|
|
|
|
const (
|
|
|
|
eachModifier string = "each"
|
|
|
|
issetModifier string = "isset"
|
|
|
|
lengthModifier string = "length"
|
2024-12-06 16:08:39 +02:00
|
|
|
lowerModifier string = "lower"
|
2023-01-07 22:25:56 +02:00
|
|
|
)
|
2022-07-07 00:19:05 +03:00
|
|
|
|
2023-01-07 22:25:56 +02:00
|
|
|
// ensure that `search.FieldResolver` interface is implemented
|
|
|
|
var _ search.FieldResolver = (*RecordFieldResolver)(nil)
|
2022-07-07 00:19:05 +03:00
|
|
|
|
|
|
|
// RecordFieldResolver defines a custom search resolver struct for
|
|
|
|
// managing Record model search fields.
|
|
|
|
//
|
2023-02-23 21:51:42 +02:00
|
|
|
// Usually used together with `search.Provider`.
|
|
|
|
// Example:
|
|
|
|
//
|
|
|
|
// resolver := resolvers.NewRecordFieldResolver(
|
2024-09-29 19:23:19 +03:00
|
|
|
// app,
|
2023-02-23 21:51:42 +02:00
|
|
|
// myCollection,
|
2023-07-17 23:13:39 +03:00
|
|
|
// &models.RequestInfo{...},
|
2023-02-23 21:51:42 +02:00
|
|
|
// true,
|
|
|
|
// )
|
|
|
|
// provider := search.NewProvider(resolver)
|
|
|
|
// ...
|
2022-07-07 00:19:05 +03:00
|
|
|
type RecordFieldResolver struct {
|
2024-09-29 19:23:19 +03:00
|
|
|
app App
|
|
|
|
baseCollection *Collection
|
|
|
|
requestInfo *RequestInfo
|
2023-07-17 23:13:39 +03:00
|
|
|
staticRequestInfo map[string]any
|
2023-10-23 22:46:47 +03:00
|
|
|
allowedFields []string
|
|
|
|
joins []*join
|
|
|
|
allowHiddenFields bool
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
|
|
|
|
2024-12-29 17:31:58 +02:00
|
|
|
// AllowedFields returns a copy of the resolver's allowed fields.
|
|
|
|
func (r *RecordFieldResolver) AllowedFields() []string {
|
|
|
|
return slices.Clone(r.allowedFields)
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetAllowedFields replaces the resolver's allowed fields with the new ones.
|
|
|
|
func (r *RecordFieldResolver) SetAllowedFields(newAllowedFields []string) {
|
|
|
|
r.allowedFields = slices.Clone(newAllowedFields)
|
|
|
|
}
|
|
|
|
|
|
|
|
// AllowHiddenFields returns whether the current resolver allows filtering hidden fields.
|
|
|
|
func (r *RecordFieldResolver) AllowHiddenFields() bool {
|
|
|
|
return r.allowHiddenFields
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetAllowHiddenFields enables or disables hidden fields filtering.
|
|
|
|
func (r *RecordFieldResolver) SetAllowHiddenFields(allowHiddenFields bool) {
|
|
|
|
r.allowHiddenFields = allowHiddenFields
|
|
|
|
}
|
|
|
|
|
2022-07-07 00:19:05 +03:00
|
|
|
// NewRecordFieldResolver creates and initializes a new `RecordFieldResolver`.
|
|
|
|
func NewRecordFieldResolver(
|
2024-09-29 19:23:19 +03:00
|
|
|
app App,
|
|
|
|
baseCollection *Collection,
|
|
|
|
requestInfo *RequestInfo,
|
2022-10-30 10:28:14 +02:00
|
|
|
allowHiddenFields bool,
|
2022-07-07 00:19:05 +03:00
|
|
|
) *RecordFieldResolver {
|
2022-11-17 14:17:10 +02:00
|
|
|
r := &RecordFieldResolver{
|
2024-09-29 19:23:19 +03:00
|
|
|
app: app,
|
2022-07-07 00:19:05 +03:00
|
|
|
baseCollection: baseCollection,
|
2023-07-17 23:13:39 +03:00
|
|
|
requestInfo: requestInfo,
|
2024-09-29 19:23:19 +03:00
|
|
|
allowHiddenFields: allowHiddenFields, // note: it is not based only on the requestInfo.auth since it could be used by a non-request internal method
|
2023-01-07 22:25:56 +02:00
|
|
|
joins: []*join{},
|
2022-07-07 00:19:05 +03:00
|
|
|
allowedFields: []string{
|
2023-01-07 22:25:56 +02:00
|
|
|
`^\w+[\w\.\:]*$`,
|
2024-02-17 15:01:09 +02:00
|
|
|
`^\@request\.context$`,
|
2022-07-07 00:19:05 +03:00
|
|
|
`^\@request\.method$`,
|
2023-01-07 22:25:56 +02:00
|
|
|
`^\@request\.auth\.[\w\.\:]*\w+$`,
|
2024-09-29 19:23:19 +03:00
|
|
|
`^\@request\.body\.[\w\.\:]*\w+$`,
|
2023-01-07 22:25:56 +02:00
|
|
|
`^\@request\.query\.[\w\.\:]*\w+$`,
|
2024-09-29 19:23:19 +03:00
|
|
|
`^\@request\.headers\.[\w\.\:]*\w+$`,
|
2023-12-03 10:57:49 +02:00
|
|
|
`^\@collection\.\w+(\:\w+)?\.[\w\.\:]*\w+$`,
|
2022-07-07 00:19:05 +03:00
|
|
|
},
|
|
|
|
}
|
2022-11-17 14:17:10 +02:00
|
|
|
|
2023-07-17 23:13:39 +03:00
|
|
|
r.staticRequestInfo = map[string]any{}
|
|
|
|
if r.requestInfo != nil {
|
2024-02-17 15:01:09 +02:00
|
|
|
r.staticRequestInfo["context"] = r.requestInfo.Context
|
2023-07-17 23:13:39 +03:00
|
|
|
r.staticRequestInfo["method"] = r.requestInfo.Method
|
|
|
|
r.staticRequestInfo["query"] = r.requestInfo.Query
|
|
|
|
r.staticRequestInfo["headers"] = r.requestInfo.Headers
|
2024-09-29 19:23:19 +03:00
|
|
|
r.staticRequestInfo["body"] = r.requestInfo.Body
|
2023-07-17 23:13:39 +03:00
|
|
|
r.staticRequestInfo["auth"] = nil
|
2024-09-29 19:23:19 +03:00
|
|
|
if r.requestInfo.Auth != nil {
|
|
|
|
authClone := r.requestInfo.Auth.Clone()
|
|
|
|
r.staticRequestInfo["auth"] = authClone.
|
|
|
|
Unhide(authClone.Collection().Fields.FieldNames()...).
|
|
|
|
IgnoreEmailVisibility(true).
|
|
|
|
PublicExport()
|
2022-11-17 14:17:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return r
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// UpdateQuery implements `search.FieldResolver` interface.
|
|
|
|
//
|
|
|
|
// Conditionally updates the provided search query based on the
|
|
|
|
// resolved fields (eg. dynamically joining relations).
|
|
|
|
func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
|
|
|
|
if len(r.joins) > 0 {
|
|
|
|
query.Distinct(true)
|
|
|
|
|
|
|
|
for _, join := range r.joins {
|
2023-01-07 22:25:56 +02:00
|
|
|
query.LeftJoin(
|
|
|
|
(join.tableName + " " + join.tableAlias),
|
|
|
|
join.on,
|
|
|
|
)
|
2022-10-30 10:28:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-07 00:19:05 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Resolve implements `search.FieldResolver` interface.
|
|
|
|
//
|
2023-01-07 22:25:56 +02:00
|
|
|
// Example of some resolvable fieldName formats:
|
|
|
|
//
|
2023-02-23 21:51:42 +02:00
|
|
|
// id
|
|
|
|
// someSelect.each
|
|
|
|
// project.screen.status
|
2024-02-19 23:13:04 +02:00
|
|
|
// screen.project_via_prototype.name
|
2024-02-19 16:55:34 +02:00
|
|
|
// @request.context
|
|
|
|
// @request.method
|
2023-02-23 21:51:42 +02:00
|
|
|
// @request.query.filter
|
2023-03-02 18:56:18 +02:00
|
|
|
// @request.headers.x_token
|
2023-02-23 21:51:42 +02:00
|
|
|
// @request.auth.someRelation.name
|
2024-09-29 19:23:19 +03:00
|
|
|
// @request.body.someRelation.name
|
|
|
|
// @request.body.someField
|
|
|
|
// @request.body.someSelect:each
|
|
|
|
// @request.body.someField:isset
|
2023-02-23 21:51:42 +02:00
|
|
|
// @collection.product.name
|
2023-01-07 22:25:56 +02:00
|
|
|
func (r *RecordFieldResolver) Resolve(fieldName string) (*search.ResolverResult, error) {
|
|
|
|
return parseAndRun(fieldName, r)
|
|
|
|
}
|
2022-10-30 10:28:14 +02:00
|
|
|
|
2023-01-07 22:25:56 +02:00
|
|
|
func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search.ResolverResult, error) {
|
|
|
|
if len(path) == 0 {
|
2024-09-29 19:23:19 +03:00
|
|
|
return nil, errors.New("at least one path key should be provided")
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
|
|
|
|
2023-01-07 22:25:56 +02:00
|
|
|
lastProp, modifier, err := splitModifier(path[len(path)-1])
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-10-30 10:28:14 +02:00
|
|
|
|
2023-01-07 22:25:56 +02:00
|
|
|
path[len(path)-1] = lastProp
|
2022-07-07 00:19:05 +03:00
|
|
|
|
2023-01-07 22:25:56 +02:00
|
|
|
// extract value
|
2024-09-29 19:23:19 +03:00
|
|
|
resultVal, err := extractNestedVal(r.staticRequestInfo, path...)
|
|
|
|
if err != nil {
|
|
|
|
r.app.Logger().Debug("resolveStaticRequestField graceful fallback", "error", err.Error())
|
|
|
|
}
|
2022-07-07 00:19:05 +03:00
|
|
|
|
2023-01-07 22:25:56 +02:00
|
|
|
if modifier == issetModifier {
|
|
|
|
if err != nil {
|
|
|
|
return &search.ResolverResult{Identifier: "FALSE"}, nil
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
2023-01-07 22:25:56 +02:00
|
|
|
return &search.ResolverResult{Identifier: "TRUE"}, nil
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
|
|
|
|
2023-07-17 23:13:39 +03:00
|
|
|
// note: we are ignoring the error because requestInfo is dynamic
|
2023-01-07 22:25:56 +02:00
|
|
|
// and some of the lookup keys may not be defined for the request
|
2022-07-07 00:19:05 +03:00
|
|
|
|
|
|
|
switch v := resultVal.(type) {
|
|
|
|
case nil:
|
2023-01-07 22:25:56 +02:00
|
|
|
return &search.ResolverResult{Identifier: "NULL"}, nil
|
|
|
|
case string:
|
|
|
|
// check if it is a number field and explicitly try to cast to
|
|
|
|
// float in case of a numeric string value was used
|
|
|
|
// (this usually the case when the data is from a multipart/form-data request)
|
2024-09-29 19:23:19 +03:00
|
|
|
field := r.baseCollection.Fields.GetByName(path[len(path)-1])
|
|
|
|
if field != nil && field.Type() == FieldTypeNumber {
|
2023-01-07 22:25:56 +02:00
|
|
|
if nv, err := strconv.ParseFloat(v, 64); err == nil {
|
|
|
|
resultVal = nv
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// otherwise - no further processing is needed...
|
|
|
|
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
2022-07-07 00:19:05 +03:00
|
|
|
// no further processing is needed...
|
|
|
|
default:
|
|
|
|
// non-plain value
|
|
|
|
// try casting to string (in case for exampe fmt.Stringer is implemented)
|
|
|
|
val, castErr := cast.ToStringE(v)
|
|
|
|
|
|
|
|
// if that doesn't work, try encoding it
|
|
|
|
if castErr != nil {
|
|
|
|
encoded, jsonErr := json.Marshal(v)
|
|
|
|
if jsonErr == nil {
|
|
|
|
val = string(encoded)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
resultVal = val
|
|
|
|
}
|
|
|
|
|
2025-01-01 17:19:30 +02:00
|
|
|
placeholder := "f" + security.PseudorandomString(8)
|
2022-07-07 00:19:05 +03:00
|
|
|
|
2024-12-06 16:08:39 +02:00
|
|
|
if modifier == lowerModifier {
|
|
|
|
return &search.ResolverResult{
|
|
|
|
Identifier: "LOWER({:" + placeholder + "})",
|
|
|
|
Params: dbx.Params{placeholder: resultVal},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2023-01-07 22:25:56 +02:00
|
|
|
return &search.ResolverResult{
|
|
|
|
Identifier: "{:" + placeholder + "}",
|
|
|
|
Params: dbx.Params{placeholder: resultVal},
|
|
|
|
}, nil
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*Collection, error) {
|
|
|
|
if collectionNameOrId == r.baseCollection.Name || collectionNameOrId == r.baseCollection.Id {
|
|
|
|
return r.baseCollection, nil
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
return getCollectionByModelOrIdentifier(r.app, collectionNameOrId)
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
|
|
|
|
2022-10-30 10:28:14 +02:00
|
|
|
func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, on dbx.Expression) {
|
2023-01-07 22:25:56 +02:00
|
|
|
join := &join{
|
|
|
|
tableName: tableName,
|
|
|
|
tableAlias: tableAlias,
|
|
|
|
on: on,
|
2022-07-20 22:33:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// replace existing join
|
|
|
|
for i, j := range r.joins {
|
2023-01-07 22:25:56 +02:00
|
|
|
if j.tableAlias == join.tableAlias {
|
2022-07-20 22:33:24 +03:00
|
|
|
r.joins[i] = join
|
|
|
|
return
|
|
|
|
}
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
|
|
|
|
2022-07-20 22:33:24 +03:00
|
|
|
// register new join
|
|
|
|
r.joins = append(r.joins, join)
|
2022-07-07 00:19:05 +03:00
|
|
|
}
|
2022-10-30 10:28:14 +02:00
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
type mapExtractor interface {
|
|
|
|
AsMap() map[string]any
|
|
|
|
}
|
|
|
|
|
|
|
|
func extractNestedVal(rawData any, keys ...string) (any, error) {
|
2023-01-07 22:25:56 +02:00
|
|
|
if len(keys) == 0 {
|
2024-09-29 19:23:19 +03:00
|
|
|
return nil, errors.New("at least one key should be provided")
|
2023-01-07 22:25:56 +02:00
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
switch m := rawData.(type) {
|
|
|
|
// maps
|
|
|
|
case map[string]any:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]string:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]bool:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]float32:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]float64:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]int:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]int8:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]int16:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]int32:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]int64:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]uint:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]uint8:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]uint16:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]uint32:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case map[string]uint64:
|
|
|
|
return mapVal(m, keys...)
|
|
|
|
case mapExtractor:
|
|
|
|
return mapVal(m.AsMap(), keys...)
|
2025-02-21 18:00:13 +02:00
|
|
|
case types.JSONRaw:
|
|
|
|
var raw any
|
|
|
|
err := json.Unmarshal(m, &raw)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to unmarshal raw JSON in order extract nested value from: %w", err)
|
|
|
|
}
|
|
|
|
return extractNestedVal(raw, keys...)
|
2024-09-29 19:23:19 +03:00
|
|
|
|
|
|
|
// slices
|
|
|
|
case []string:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []bool:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []float32:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []float64:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []int:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []int8:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []int16:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []int32:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []int64:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []uint:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []uint8:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []uint16:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []uint32:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []uint64:
|
|
|
|
return arrVal(m, keys...)
|
|
|
|
case []mapExtractor:
|
|
|
|
extracted := make([]any, len(m))
|
|
|
|
for i, v := range m {
|
|
|
|
extracted[i] = v.AsMap()
|
|
|
|
}
|
|
|
|
return arrVal(extracted, keys...)
|
|
|
|
case []any:
|
|
|
|
return arrVal(m, keys...)
|
2025-02-21 18:00:13 +02:00
|
|
|
case []types.JSONRaw:
|
|
|
|
return arrVal(m, keys...)
|
2024-09-29 19:23:19 +03:00
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("expected map or array, got %#v", rawData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func mapVal[T any](m map[string]T, keys ...string) (any, error) {
|
2023-03-23 19:30:35 +02:00
|
|
|
result, ok := m[keys[0]]
|
|
|
|
if !ok {
|
2023-01-07 22:25:56 +02:00
|
|
|
return nil, fmt.Errorf("invalid key path - missing key %q", keys[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
// end key reached
|
|
|
|
if len(keys) == 1 {
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
return extractNestedVal(result, keys[1:]...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func arrVal[T any](m []T, keys ...string) (any, error) {
|
|
|
|
idx, err := strconv.Atoi(keys[0])
|
|
|
|
if err != nil || idx < 0 || idx >= len(m) {
|
|
|
|
return nil, fmt.Errorf("invalid key path - invalid or missing array index %q", keys[0])
|
|
|
|
}
|
|
|
|
|
|
|
|
result := m[idx]
|
|
|
|
|
|
|
|
// end key reached
|
|
|
|
if len(keys) == 1 {
|
|
|
|
return result, nil
|
2023-01-07 22:25:56 +02:00
|
|
|
}
|
|
|
|
|
2024-09-29 19:23:19 +03:00
|
|
|
return extractNestedVal(result, keys[1:]...)
|
2023-01-07 22:25:56 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func splitModifier(combined string) (string, string, error) {
|
|
|
|
parts := strings.Split(combined, ":")
|
|
|
|
|
|
|
|
if len(parts) != 2 {
|
|
|
|
return combined, "", nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// validate modifier
|
|
|
|
switch parts[1] {
|
|
|
|
case issetModifier,
|
|
|
|
eachModifier,
|
2024-12-06 16:08:39 +02:00
|
|
|
lengthModifier,
|
|
|
|
lowerModifier:
|
2023-01-07 22:25:56 +02:00
|
|
|
return parts[0], parts[1], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", "", fmt.Errorf("unknown modifier in %q", combined)
|
2022-10-30 10:28:14 +02:00
|
|
|
}
|