flatten relation joins

This commit is contained in:
Gani Georgiev
2025-11-03 14:21:01 +02:00
parent 153ad12e64
commit 593721dcea
8 changed files with 230 additions and 133 deletions

View File

@@ -48,6 +48,10 @@ type RecordFieldResolver struct {
allowedFields []string
joins []*join
allowHiddenFields bool
// ---
listRuleJoins map[string]*Collection // tableAlias->collection
joinAliasSuffix string // used for uniqueness in the flatten collection list rule join
baseCollectionAlias string
}
// AllowedFields returns a copy of the resolver's allowed fields.
@@ -115,7 +119,6 @@ func NewRecordFieldResolver(
return r
}
// @todo consider removing error return type OR update the existin calls to check the error
// @todo think of a better a way how to call it automatically after BuildExpr
//
// UpdateQuery implements `search.FieldResolver` interface.
@@ -134,6 +137,49 @@ 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)
if err != nil {
return err
}
}
}
return nil
}
func (r *RecordFieldResolver) updateQueryWithCollectionListRule(c *Collection, tableAlias string, query *dbx.SelectQuery) error {
if r.allowHiddenFields || c == nil || c.ListRule == nil || *c.ListRule == "" {
return nil
}
cloneR := *r
cloneR.joins = []*join{}
cloneR.baseCollection = c
cloneR.baseCollectionAlias = tableAlias
cloneR.allowHiddenFields = true
cloneR.joinAliasSuffix = security.PseudorandomString(6)
expr, err := search.FilterData(*c.ListRule).BuildExpr(&cloneR)
if err != nil {
return fmt.Errorf("to buld %q list rule join subquery filter expression: %w", c.Name, err)
}
query.AndWhere(expr)
if len(cloneR.joins) > 0 {
query.Distinct(true)
for _, j := range cloneR.joins {
query.LeftJoin(
(j.tableName + " " + j.tableAlias),
j.on,
)
}
}
return nil
}
@@ -242,80 +288,42 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*Collec
return getCollectionByModelOrIdentifier(r.app, collectionNameOrId)
}
func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, on dbx.Expression) {
func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, on dbx.Expression) error {
newJoin := &join{
tableName: tableName,
tableAlias: tableAlias,
on: on,
}
// (see updateQueryWithCollectionListRule)
if !r.allowHiddenFields {
c, _ := r.loadCollection(tableName)
r.updateCollectionJoinWithListRuleSubquery(c, newJoin)
// ignore non-collections since the table name could be an expression (e.g. json) or some other subquery
if c != nil {
// treat all fields as if they are hidden
if c.ListRule == nil {
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
}
}
// replace existing join
for i, j := range r.joins {
if j.tableAlias == newJoin.tableAlias {
r.joins[i] = newJoin
return
return nil
}
}
// register new join
r.joins = append(r.joins, newJoin)
}
func (r *RecordFieldResolver) updateCollectionJoinWithListRuleSubquery(c *Collection, j *join) {
if c == nil || r.allowHiddenFields {
return
}
// resolve to empty set for superusers only collections
// (treat all collection fields as "hidden")
if c.ListRule == nil {
j.on = dbx.NewExp("1=2")
return
}
if *c.ListRule == "" {
return
}
primaryCol := "_rowid_"
if c.IsView() {
primaryCol = "id"
}
subquery := r.app.DB().Select("(1)").From(c.Name)
subquery.AndWhere(dbx.NewExp("[[" + j.tableAlias + "." + primaryCol + "]]=[[" + c.Name + "." + primaryCol + "]]"))
cloneR := *r
cloneR.joins = []*join{}
cloneR.baseCollection = c
cloneR.allowHiddenFields = true
expr, err := search.FilterData(*c.ListRule).BuildExpr(&cloneR)
if err != nil {
// just log for now and resolve to empty set to minimize breaking changes
r.app.Logger().Warn("Failed to buld collection join list rule subquery filter expression", "error", err)
j.on = dbx.NewExp("1=2")
return
}
subquery.AndWhere(expr)
err = cloneR.UpdateQuery(subquery)
if err != nil {
// just log for now and resolve to empty set to minimize breaking changes
r.app.Logger().Warn("Failed to update collection join with list rule subquery", "error", err)
j.on = dbx.NewExp("1=2")
return
}
sb := subquery.Build()
j.on = dbx.And(j.on, dbx.NewExp("EXISTS ("+sb.SQL()+")", sb.Params()))
return nil
}
type mapExtractor interface {