add extra subquery check for client-side relation filtering
This commit is contained in:
parent
d5dcd01551
commit
67ee431585
|
|
@ -382,10 +382,32 @@ func TestRecordCrudList(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
Name: "multi-match - at least one of",
|
||||
Name: "multi-match - at least one of (guest - non-satisfied relation filter API rule)",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length?=2"),
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
`"perPage":30`,
|
||||
`"totalPages":0`,
|
||||
`"totalItems":0`,
|
||||
`"items":[]`,
|
||||
},
|
||||
ExpectedEvents: map[string]int{
|
||||
"*": 0,
|
||||
"OnRecordsListRequest": 1,
|
||||
"OnRecordEnrich": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "multi-match - at least one of (clients)",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length?=2"),
|
||||
Headers: map[string]string{
|
||||
// clients, test@example.com
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
`"perPage":30`,
|
||||
|
|
@ -401,9 +423,13 @@ func TestRecordCrudList(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
Name: "multi-match - all",
|
||||
Name: "multi-match - all (clients)",
|
||||
Method: http.MethodGet,
|
||||
URL: "/api/collections/demo4/records?filter=" + url.QueryEscape("rel_many_no_cascade_required.files:length=2"),
|
||||
Headers: map[string]string{
|
||||
// clients, test@example.com
|
||||
"Authorization": "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6ImdrMzkwcWVnczR5NDd3biIsInR5cGUiOiJhdXRoIiwiY29sbGVjdGlvbklkIjoidjg1MXE0cjc5MHJoa25sIiwiZXhwIjoyNTI0NjA0NDYxLCJyZWZyZXNoYWJsZSI6dHJ1ZX0.0ONnm_BsvPRZyDNT31GN1CKUB6uQRxvVvQ-Wc9AZfG0",
|
||||
},
|
||||
ExpectedStatus: 200,
|
||||
ExpectedContent: []string{
|
||||
`"page":1`,
|
||||
|
|
|
|||
|
|
@ -1195,7 +1195,7 @@ type App interface {
|
|||
// ---------------------------------------------------------------
|
||||
|
||||
// OnMailerSend hook is triggered every time when a new email is
|
||||
// being send using the [App.NewMailClient()] instance.
|
||||
// being sent using the [App.NewMailClient()] instance.
|
||||
//
|
||||
// It allows intercepting the email message or to use a custom mailer client.
|
||||
OnMailerSend() *hook.Hook[*MailerEvent]
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func DefaultDBConnect(dbPath string) (*dbx.DB, error) {
|
|||
// Note: the busy_timeout pragma must be first because
|
||||
// the connection needs to be set to block on busy before WAL mode
|
||||
// is set in case it hasn't been already set by another connection.
|
||||
pragmas := "?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)&_pragma=temp_store(MEMORY)&_pragma=cache_size(-16000)"
|
||||
pragmas := "?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=journal_size_limit(200000000)&_pragma=synchronous(NORMAL)&_pragma=foreign_keys(ON)&_pragma=temp_store(MEMORY)&_pragma=cache_size(-64000)"
|
||||
|
||||
db, err := dbx.Open("sqlite", dbPath+pragmas)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -115,6 +115,9 @@ 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.
|
||||
//
|
||||
// Conditionally updates the provided search query based on the
|
||||
|
|
@ -240,22 +243,80 @@ func (r *RecordFieldResolver) loadCollection(collectionNameOrId string) (*Collec
|
|||
}
|
||||
|
||||
func (r *RecordFieldResolver) registerJoin(tableName string, tableAlias string, on dbx.Expression) {
|
||||
join := &join{
|
||||
newJoin := &join{
|
||||
tableName: tableName,
|
||||
tableAlias: tableAlias,
|
||||
on: on,
|
||||
}
|
||||
|
||||
if !r.allowHiddenFields {
|
||||
c, _ := r.loadCollection(tableName)
|
||||
r.updateCollectionJoinWithListRuleSubquery(c, newJoin)
|
||||
}
|
||||
|
||||
// replace existing join
|
||||
for i, j := range r.joins {
|
||||
if j.tableAlias == join.tableAlias {
|
||||
r.joins[i] = join
|
||||
if j.tableAlias == newJoin.tableAlias {
|
||||
r.joins[i] = newJoin
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// register new join
|
||||
r.joins = append(r.joins, join)
|
||||
r.joins = append(r.joins, newJoin)
|
||||
}
|
||||
|
||||
func (r *RecordFieldResolver) updateCollectionJoinWithListRuleSubquery(c *Collection, j *join) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// resolve to empty set for superusers only collections
|
||||
// (treat all collection fields as "hidden")
|
||||
if c.ListRule == nil {
|
||||
if !r.allowHiddenFields {
|
||||
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
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
type mapExtractor interface {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ type runner struct {
|
|||
activeProps []string // holds the active props that remains to be processed
|
||||
activeCollectionName string // the last used collection name
|
||||
activeTableAlias string // the last used table alias
|
||||
allowHiddenFields bool // indicates whether hidden fields (eg. email) should be allowed without extra conditions
|
||||
nullifyMisingField bool // indicating whether to return null on missing field or return an error
|
||||
withMultiMatch bool // indicates whether to attach a multiMatchSubquery condition to the ResolverResult
|
||||
multiMatchActiveTableAlias string // the last used multi-match table alias
|
||||
|
|
@ -135,12 +134,6 @@ func (r *runner) prepare() {
|
|||
r.activeCollectionName = r.resolver.baseCollection.Name
|
||||
r.activeTableAlias = inflector.Columnify(r.activeCollectionName)
|
||||
|
||||
r.allowHiddenFields = r.resolver.allowHiddenFields
|
||||
// always allow hidden fields since the @.* filter is a system one
|
||||
if r.activeProps[0] == "@collection" || r.activeProps[0] == "@request" {
|
||||
r.allowHiddenFields = true
|
||||
}
|
||||
|
||||
// enable the ignore flag for missing @request.* fields for backward
|
||||
// compatibility and consistency with all @request.* filter fields and types
|
||||
r.nullifyMisingField = r.activeProps[0] == "@request"
|
||||
|
|
@ -416,19 +409,13 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
|||
|
||||
field := collection.Fields.GetByName(prop)
|
||||
|
||||
if field != nil && field.GetHidden() && !r.allowHiddenFields {
|
||||
if field != nil && field.GetHidden() && !r.resolver.allowHiddenFields {
|
||||
return nil, fmt.Errorf("non-filterable field %q", prop)
|
||||
}
|
||||
|
||||
// @todo consider moving to the finalizer and converting to "JSONExtractable" interface with optional extra validation for the remaining props?
|
||||
// json or geoPoint field -> treat the rest of the props as json path
|
||||
if field != nil && (field.Type() == FieldTypeJSON || field.Type() == FieldTypeGeoPoint) {
|
||||
// consider List/Search superusers-only collections as if all their fields are hidden
|
||||
// (apply only for the last nested filter field for now to minimize breaking changes)
|
||||
if i > 0 && collection.ListRule == nil && !r.allowHiddenFields {
|
||||
return nil, fmt.Errorf("collection %q is not allowed to be filtered", collection.Name)
|
||||
}
|
||||
|
||||
var jsonPath strings.Builder
|
||||
for j, p := range r.activeProps[i+1:] {
|
||||
if _, err := strconv.Atoi(p); err == nil {
|
||||
|
|
@ -495,7 +482,7 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
|||
return nil, fmt.Errorf("invalid back relation field %q", parts[2])
|
||||
}
|
||||
|
||||
if backField.GetHidden() && !r.allowHiddenFields {
|
||||
if backField.GetHidden() && !r.resolver.allowHiddenFields {
|
||||
return nil, fmt.Errorf("non-filterable back relation field %q", backField.GetName())
|
||||
}
|
||||
|
||||
|
|
@ -684,12 +671,6 @@ func (r *runner) processActiveProps() (*search.ResolverResult, error) {
|
|||
}
|
||||
|
||||
func (r *runner) finalizeActivePropsProcessing(collection *Collection, prop string, propDepth int) (*search.ResolverResult, error) {
|
||||
// consider List/Search superusers-only collections as if all their fields are hidden
|
||||
// (apply only for the last nested filter field for now to minimize breaking changes)
|
||||
if propDepth > 0 && collection.ListRule == nil && !r.allowHiddenFields {
|
||||
return nil, fmt.Errorf("collection %q is not allowed to be filtered", collection.Name)
|
||||
}
|
||||
|
||||
name, modifier, err := splitModifier(prop)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -703,7 +684,7 @@ func (r *runner) finalizeActivePropsProcessing(collection *Collection, prop stri
|
|||
return nil, fmt.Errorf("unknown field %q", name)
|
||||
}
|
||||
|
||||
if field.GetHidden() && !r.allowHiddenFields {
|
||||
if field.GetHidden() && !r.resolver.allowHiddenFields {
|
||||
return nil, fmt.Errorf("non-filterable field %q", name)
|
||||
}
|
||||
|
||||
|
|
@ -772,7 +753,7 @@ func (r *runner) finalizeActivePropsProcessing(collection *Collection, prop stri
|
|||
}
|
||||
|
||||
// allow querying only auth records with emails marked as public
|
||||
if field.GetName() == FieldNameEmail && !r.allowHiddenFields && collection.IsAuth() {
|
||||
if field.GetName() == FieldNameEmail && !r.resolver.allowHiddenFields && collection.IsAuth() {
|
||||
result.AfterBuild = func(expr dbx.Expression) dbx.Expression {
|
||||
return dbx.Enclose(dbx.And(expr, dbx.NewExp(fmt.Sprintf(
|
||||
"[[%s.%s]] = TRUE",
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -327,7 +327,6 @@ func (s *Provider) Exec(items any) (*Result, error) {
|
|||
if !s.skipTotal {
|
||||
// execute the 2 queries concurrently
|
||||
errg := new(errgroup.Group)
|
||||
errg.SetLimit(2)
|
||||
errg.Go(countExec)
|
||||
errg.Go(modelsExec)
|
||||
if err := errg.Wait(); err != nil {
|
||||
|
|
|
|||
Loading…
Reference in New Issue