wrap extra client-side ListRule filter with existence check and replaced the map with a slice to minimize test flakiness

This commit is contained in:
Gani Georgiev
2026-03-01 00:09:05 +02:00
parent faf96896e7
commit d695e9d180
3 changed files with 42 additions and 17 deletions

View File

@@ -25,6 +25,11 @@ const (
changedModifier string = "changed"
)
type ruleJoin struct {
collection *Collection
tableAlias string
}
// ensure that `search.FieldResolver` interface is implemented
var _ search.FieldResolver = (*RecordFieldResolver)(nil)
@@ -51,8 +56,8 @@ type RecordFieldResolver struct {
joins []*search.Join
allowHiddenFields bool
// ---
listRuleJoins map[string]*Collection // tableAlias->collection
joinAliasSuffix string // used for uniqueness in the flatten collection list rule join
listRuleJoins []ruleJoin
joinAliasSuffix string // used for uniqueness in the flatten collection list rule join
baseCollectionAlias string
}
@@ -141,8 +146,8 @@ func (r *RecordFieldResolver) UpdateQuery(query *dbx.SelectQuery) error {
// note: for now the joins are not applied for multi-match conditions to avoid excessive checks
if len(r.listRuleJoins) > 0 {
for alias, c := range r.listRuleJoins {
err := r.updateQueryWithCollectionListRule(c, alias, query)
for _, join := range r.listRuleJoins {
err := r.updateQueryWithCollectionListRule(join.collection, join.tableAlias, query)
if err != nil {
return err
}
@@ -164,9 +169,19 @@ func (r *RecordFieldResolver) updateQueryWithCollectionListRule(c *Collection, t
cloneR.allowHiddenFields = true
cloneR.joinAliasSuffix = security.PseudorandomString(8)
expr, err := search.FilterData(*c.ListRule).BuildExpr(&cloneR)
// The extra "id='' || (\nRULE\n)" concatenated part on its own
// doesn't make much sense because all records are required to have an id,
// but it is necessary to properly resolve client-side filters when
// referencing missing relations (the "\n" is for leading and trailing comments).
//
// Consider the client-side filter: "a.name != '' || b.name != ''",
// where both "a" and "b" ref collections have non-empty ListRule.
// Without the empty check the query will always evaluate to FALSE
// when one of the "a" or "b" relation fields are empty,
// even if for example "a.name != ''" is true.
expr, err := search.FilterData("id='' || (\n" + *c.ListRule + "\n)").BuildExpr(&cloneR)
if err != nil {
return fmt.Errorf("to buld %q list rule join subquery filter expression: %w", c.Name, err)
return fmt.Errorf("failed to build %q ListRule join subquery filter expression: %w", c.Name, err)
}
query.AndWhere(expr)
@@ -326,7 +341,7 @@ func (r *RecordFieldResolver) resolveStaticRequestField(path ...string) (*search
// no further processing is needed...
default:
// non-plain value
// try casting to string (in case for exampe fmt.Stringer is implemented)
// try casting to string (in case for example fmt.Stringer is implemented)
val, castErr := cast.ToStringE(v)
// if that doesn't work, try encoding it
@@ -393,10 +408,7 @@ func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string,
return fmt.Errorf("%q fields can be accessed only when allowHiddenFields is enabled or by superusers", c.Name)
}
if r.listRuleJoins == nil {
r.listRuleJoins = map[string]*Collection{}
}
r.listRuleJoins[newJoin.TableAlias] = c
r.registerRuleJoin(c, newJoin.TableAlias)
}
}
@@ -413,6 +425,19 @@ func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string,
return nil
}
func (r *RecordFieldResolver) registerRuleJoin(collection *Collection, tableAlias string) {
// replace existing
for i, j := range r.listRuleJoins {
if j.tableAlias == tableAlias {
r.listRuleJoins[i].collection = collection
return
}
}
// register new
r.listRuleJoins = append(r.listRuleJoins, ruleJoin{collection, tableAlias})
}
type mapExtractor interface {
AsMap() map[string]any
}