1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2025-03-19 22:19:23 +02:00
pocketbase/core/record_field_resolver.go

404 lines
10 KiB
Go
Raw Permalink Normal View History

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"
"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"
"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,
// &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
staticRequestInfo map[string]any
allowedFields []string
joins []*join
allowHiddenFields bool
2022-07-07 00:19:05 +03: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 {
r := &RecordFieldResolver{
2024-09-29 19:23:19 +03:00
app: app,
2022-07-07 00:19:05 +03:00
baseCollection: baseCollection,
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+$`,
`^\@collection\.\w+(\:\w+)?\.[\w\.\:]*\w+$`,
2022-07-07 00:19:05 +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
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
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()
}
}
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
// screen.project_via_prototype.name
// @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
}
// 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
}
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,
}
// replace existing join
for i, j := range r.joins {
2023-01-07 22:25:56 +02:00
if j.tableAlias == join.tableAlias {
r.joins[i] = join
return
}
2022-07-07 00:19:05 +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...)
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...)
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) {
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
}