added :changed request body modifier

This commit is contained in:
Gani Georgiev committed 2025-11-17 13:58:43 +02:00
1 parent 2525f29c1c
commit 6e739fd33d
37 files changed
+170 -67

No files matched your search

+20 -6
View File
@@ -17,10 +17,11 @@ import (
// filter modifiers
const (
eachModifier string = "each"
issetModifier string = "isset"
lengthModifier string = "length"
lowerModifier string = "lower"
eachModifier string = "each"
issetModifier string = "isset"
lengthModifier string = "length"
lowerModifier string = "lower"
changedModifier string = "changed"
)
// ensure that `search.FieldResolver` interface is implemented
@@ -235,7 +236,7 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search
switch v := resultVal.(type) {
case nil:
return &search.ResolverResult{Identifier: "NULL"}, nil
// no further processing is needed...
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
@@ -265,8 +266,20 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search
resultVal = val
}
// unsupported modifier
// @todo consider deprecating with the introduction of filter functions
if modifier != "" && modifier != lowerModifier {
return nil, fmt.Errorf("invalid modifier sequence %s:%s", lastProp, modifier)
}
// no need to wrap as placeholder if we already know that it is null
if resultVal == nil {
return &search.ResolverResult{Identifier: "NULL"}, nil
}
placeholder := "f" + security.PseudorandomString(8)
// @todo consider deprecating with the introduction of filter functions
if modifier == lowerModifier {
return &search.ResolverResult{
Identifier: "LOWER({:" + placeholder + "})",
@@ -463,7 +476,8 @@ func splitModifier(combined string) (string, string, error) {
case issetModifier,
eachModifier,
lengthModifier,
lowerModifier:
lowerModifier,
changedModifier:
return parts[0], parts[1], nil
}
@@ -0,0 +1,31 @@
package core
import (
"strings"
"github.com/pocketbase/dbx"
)
var _ dbx.Expression = (*replaceWithExpression)(nil)
// replaceWithExpression defines a custom expression that will replace
// a placeholder identifier found in "old" with the result of "new".
type replaceWithExpression struct {
placeholder string
old dbx.Expression
new dbx.Expression
}
// Build converts the expression into a SQL fragment.
//
// Implements [dbx.Expression] interface.
func (e *replaceWithExpression) Build(db *dbx.DB, params dbx.Params) string {
if e.placeholder == "" || e.old == nil || e.new == nil {
return "0=1"
}
oldResult := e.old.Build(db, params)
newResult := e.new.Build(db, params)
return strings.ReplaceAll(oldResult, e.placeholder, newResult)
}
+47 -20
View File
@@ -88,7 +88,9 @@ func (r *runner) run() (*search.ResolverResult, error) {
return r.processRequestAuthField()
}
if strings.HasPrefix(r.fieldName, "@request.body.") && len(r.activeProps) > 2 {
totalProps := len(r.activeProps)
if strings.HasPrefix(r.fieldName, "@request.body.") && totalProps > 2 {
name, modifier, err := splitModifier(r.activeProps[2])
if err != nil {
return nil, err
@@ -100,23 +102,21 @@ func (r *runner) run() (*search.ResolverResult, error) {
}
// check for body relation field
if bodyField.Type() == FieldTypeRelation && len(r.activeProps) > 3 {
return r.processRequestInfoRelationField(bodyField)
if bodyField.Type() == FieldTypeRelation && totalProps > 3 {
return r.processRequestBodyRelationField(bodyField)
}
// check for body arrayble fields ":each" modifier
if modifier == eachModifier && len(r.activeProps) == 3 {
return r.processRequestInfoEachModifier(bodyField)
}
// check for body arrayble fields ":length" modifier
if modifier == lengthModifier && len(r.activeProps) == 3 {
return r.processRequestInfoLengthModifier(bodyField)
}
// check for body arrayble fields ":lower" modifier
if modifier == lowerModifier && len(r.activeProps) == 3 {
return r.processRequestInfoLowerModifier(bodyField)
if totalProps == 3 { // aka. last prop
switch modifier {
case eachModifier:
return r.processRequestBodyEachModifier(bodyField)
case lengthModifier:
return r.processRequestBodyLengthModifier(bodyField)
case lowerModifier:
return r.processRequestBodyLowerModifier(bodyField)
case changedModifier:
return r.processRequestBodyChangedModifier(bodyField)
}
}
}
@@ -271,7 +271,34 @@ func toSlice(value any) []any {
return result
}
func (r *runner) processRequestInfoLowerModifier(bodyField Field) (*search.ResolverResult, error) {
func (r *runner) processRequestBodyChangedModifier(bodyField Field) (*search.ResolverResult, error) {
name := bodyField.GetName()
alias := search.FilterData(fmt.Sprintf("@request.body.%s:isset = true && @request.body.%s != %s", name, name, name))
aliasExpr, err := alias.BuildExpr(r.resolver)
if err != nil {
return nil, err
}
placeholder := "@changed@" + name + security.PseudorandomString(6)
result := &search.ResolverResult{
Identifier: placeholder,
NoCoalesce: true,
AfterBuild: func(expr dbx.Expression) dbx.Expression {
return &replaceWithExpression{
placeholder: placeholder,
old: expr,
new: aliasExpr,
}
},
}
return result, nil
}
func (r *runner) processRequestBodyLowerModifier(bodyField Field) (*search.ResolverResult, error) {
rawValue := cast.ToString(r.resolver.requestInfo.Body[bodyField.GetName()])
placeholder := "infoLower" + bodyField.GetName() + security.PseudorandomString(6)
@@ -284,7 +311,7 @@ func (r *runner) processRequestInfoLowerModifier(bodyField Field) (*search.Resol
return result, nil
}
func (r *runner) processRequestInfoLengthModifier(bodyField Field) (*search.ResolverResult, error) {
func (r *runner) processRequestBodyLengthModifier(bodyField Field) (*search.ResolverResult, error) {
if _, ok := bodyField.(MultiValuer); !ok {
return nil, fmt.Errorf("field %q doesn't support multivalue operations", bodyField.GetName())
}
@@ -298,7 +325,7 @@ func (r *runner) processRequestInfoLengthModifier(bodyField Field) (*search.Reso
return result, nil
}
func (r *runner) processRequestInfoEachModifier(bodyField Field) (*search.ResolverResult, error) {
func (r *runner) processRequestBodyEachModifier(bodyField Field) (*search.ResolverResult, error) {
multiValuer, ok := bodyField.(MultiValuer)
if !ok {
return nil, fmt.Errorf("field %q doesn't support multivalue operations", bodyField.GetName())
@@ -347,7 +374,7 @@ func (r *runner) processRequestInfoEachModifier(bodyField Field) (*search.Resolv
return result, nil
}
func (r *runner) processRequestInfoRelationField(bodyField Field) (*search.ResolverResult, error) {
func (r *runner) processRequestBodyRelationField(bodyField Field) (*search.ResolverResult, error) {
relField, ok := bodyField.(*RelationField)
if !ok {
return nil, fmt.Errorf("failed to initialize data relation field %q", bodyField.GetName())
+16
View File
@@ -600,6 +600,22 @@ func TestRecordFieldResolverUpdateQuery(t *testing.T) {
true,
"SELECT DISTINCT `demo4`.* FROM `demo4` LEFT JOIN `demo4` `__data_demo4_self_rel_one` ON [[__data_demo4_self_rel_one.id]]={:p0} LEFT JOIN `demo3` `__data_demo3_rel_many_cascade` ON [[__data_demo3_rel_many_cascade.id]] IN ({:p1}, {:p2}) LEFT JOIN `demo3` `__data_demo3_rel_one_cascade` ON [[__data_demo3_rel_one_cascade.id]]={:p3} LEFT JOIN `demo4` `demo4_self_rel_one` ON [[demo4_self_rel_one.id]] = [[demo4.self_rel_one]] LEFT JOIN json_each(CASE WHEN iif(json_valid([[demo4_self_rel_one.rel_many_cascade]]), json_type([[demo4_self_rel_one.rel_many_cascade]])='array', FALSE) THEN [[demo4_self_rel_one.rel_many_cascade]] ELSE json_array([[demo4_self_rel_one.rel_many_cascade]]) END) `__je_demo4_self_rel_one_rel_many_cascade` LEFT JOIN `demo3` `demo4_self_rel_one_rel_many_cascade` ON [[demo4_self_rel_one_rel_many_cascade.id]] = [[__je_demo4_self_rel_one_rel_many_cascade.value]] WHERE (json_array_length(CASE WHEN iif(json_valid([[__data_demo4_self_rel_one.self_rel_many]]), json_type([[__data_demo4_self_rel_one.self_rel_many]])='array', FALSE) THEN [[__data_demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[__data_demo4_self_rel_one.self_rel_many]] = '' OR [[__data_demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[__data_demo4_self_rel_one.self_rel_many]]) END) END) > {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[__data_demo4_self_rel_one.self_rel_many]]), json_type([[__data_demo4_self_rel_one.self_rel_many]])='array', FALSE) THEN [[__data_demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[__data_demo4_self_rel_one.self_rel_many]] = '' OR [[__data_demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[__data_demo4_self_rel_one.self_rel_many]]) END) END) > {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[__data_demo3_rel_many_cascade.files]]), json_type([[__data_demo3_rel_many_cascade.files]])='array', FALSE) THEN [[__data_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_many_cascade.files]] = '' OR [[__data_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_many_cascade.files]]) END) END) < {:TEST} AND ((json_array_length(CASE WHEN iif(json_valid([[__data_demo3_rel_many_cascade.files]]), json_type([[__data_demo3_rel_many_cascade.files]])='array', FALSE) THEN [[__data_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_many_cascade.files]] = '' OR [[__data_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_many_cascade.files]]) END) END) < {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(CASE WHEN iif(json_valid([[__mm___data_demo3_rel_many_cascade.files]]), json_type([[__mm___data_demo3_rel_many_cascade.files]])='array', FALSE) THEN [[__mm___data_demo3_rel_many_cascade.files]] ELSE (CASE WHEN [[__mm___data_demo3_rel_many_cascade.files]] = '' OR [[__mm___data_demo3_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__mm___data_demo3_rel_many_cascade.files]]) END) END) as [[multiMatchValue]] FROM `demo4` `__mm_demo4` LEFT JOIN `demo3` `__mm___data_demo3_rel_many_cascade` ON [[__mm___data_demo3_rel_many_cascade.id]] IN ({:p8}, {:p9}) WHERE `__mm_demo4`.`id` = `demo4`.`id`) {{__smTEST}} WHERE NOT ([[__smTEST.multiMatchValue]] < {:TEST})))) AND json_array_length(CASE WHEN iif(json_valid([[__data_demo3_rel_one_cascade.files]]), json_type([[__data_demo3_rel_one_cascade.files]])='array', FALSE) THEN [[__data_demo3_rel_one_cascade.files]] ELSE (CASE WHEN [[__data_demo3_rel_one_cascade.files]] = '' OR [[__data_demo3_rel_one_cascade.files]] IS NULL THEN json_array() ELSE json_array([[__data_demo3_rel_one_cascade.files]]) END) END) < {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[demo4_self_rel_one.self_rel_many]]), json_type([[demo4_self_rel_one.self_rel_many]])='array', FALSE) THEN [[demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[demo4_self_rel_one.self_rel_many]] = '' OR [[demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) END) = {:TEST} AND json_array_length(CASE WHEN iif(json_valid([[demo4_self_rel_one.self_rel_many]]), json_type([[demo4_self_rel_one.self_rel_many]])='array', FALSE) THEN [[demo4_self_rel_one.self_rel_many]] ELSE (CASE WHEN [[demo4_self_rel_one.self_rel_many]] = '' OR [[demo4_self_rel_one.self_rel_many]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one.self_rel_many]]) END) END) = {:TEST} AND ((json_array_length(CASE WHEN iif(json_valid([[demo4_self_rel_one_rel_many_cascade.files]]), json_type([[demo4_self_rel_one_rel_many_cascade.files]])='array', FALSE) THEN [[demo4_self_rel_one_rel_many_cascade.files]] ELSE (CASE WHEN [[demo4_self_rel_one_rel_many_cascade.files]] = '' OR [[demo4_self_rel_one_rel_many_cascade.files]] IS NULL THEN json_array() ELSE json_array([[demo4_self_rel_one_rel_many_cascade.files]]) END) END) IS NOT {:TEST}) AND (NOT EXISTS (SELECT 1 FROM (SELECT json_array_length(Line truncated
},
{
"request body :changed modifier with non-existing collection field",
"demo1",
"@request.body.a:changed > 1",
true,
"",
},
{
"regular body :changed modifier",
"demo1",
"@request.body.number:changed = false &&" +
"@request.body.email:changed = true &&" +
"@request.body.number:changed = @request.body.select_many:changed",
true,
"SELECT `demo1`.* FROM `demo1` WHERE ((TRUE = 1 AND {:TEST} IS NOT [[demo1.number]]) IS 0 AND (FALSE = 1 AND ('' IS NOT [[demo1.email]] AND [[demo1.email]] IS NOT NULL)) IS 1 AND (TRUE = 1 AND {:TEST} IS NOT [[demo1.number]]) IS (TRUE = 1 AND {:TEST} IS NOT [[demo1.select_many]]))",
},
{
"json_extract and json_array_length COALESCE equal normalizations",
"demo4",