feat(data-access): compound AND filter and orderBy column for postgrest queries - #1874
Conversation
|
This PR will trigger a minor release when merged. |
There was a problem hiding this comment.
Hey @bottadobe,
Verdict: Request changes - one documentation update needed before merge.
Complexity: MEDIUM - recursive expression evaluator refactor plus new query option.
Changes: Adds compound AND filter (op.and) and explicit orderBy column option for PostgREST queries in the data-access layer (4 files).
Must fix before merge
- [Important] Stale operator list in CLAUDE.md after adding
and-packages/spacecat-shared-data-access/CLAUDE.md(details inline)
Non-blocking (4): minor issues and suggestions
- nit:
applyExprrecurses without a depth cap for nestedand- not exploitable (callers are trusted internal services) but a one-line guard prevents accidental infinite recursion from a future bug -src/util/postgrest.utils.js:163 - suggestion: add tests for malformed
orderBy(e.g.{ direction: 'desc' }with noattribute) and for the default-direction path ({ attribute: 'updatedAt' }with nodirection) to cover thehasExplicitOrderByguard branches explicitly -test/unit/models/site-ims-org-access/site-ims-org-access.collection.test.js - suggestion: the back-compat ordering test asserts only that
updated_atis NOT the order column - also asserting thatquery.orderwas called (proving the fallback ran) and verifying theascendingflag would make it more robust -test/unit/models/site-ims-org-access/site-ims-org-access.collection.test.js - suggestion: consider documenting the new
orderBy: { attribute, direction }option in the data-access CLAUDE.md Common Patterns section (author notes this is planned for the consumer PR)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 4m 11s | Cost: $4.53 | Commit: 43c80bbf7c2552d2b55140af6b0b7820ad952e13
If this code review was useful, please react with 👍. Otherwise, react with 👎.
| like: (field, value) => ({ type: 'like', field, value }), | ||
| ilike: (field, value) => ({ type: 'ilike', field, value }), | ||
| contains: (field, value) => ({ type: 'contains', field, value }), | ||
| and: (...conditions) => ({ type: 'and', conditions: conditions.filter(Boolean) }), |
There was a problem hiding this comment.
issue (blocking): The data-access CLAUDE.md documents the available WHERE operators as:
PostgREST WHERE operators:
eq,ne,gt,gte,lt,lte,is,in,contains,like,ilike
This list is now stale - it omits the new and combinator added here. A developer (or AI assistant) reading that documentation would conclude compound queries are not possible at this layer and try to work around the limitation.
Fix: Add and to the operator list in packages/spacecat-shared-data-access/CLAUDE.md (the "PostgREST WHERE operators" line). If the root CLAUDE.md has the same list, update it there too.
dzehnder
left a comment
There was a problem hiding this comment.
Hey @bottadobe,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repo's docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Request changes - clean, well-tested additive change, but the new orderBy option ships untyped and forwards unvalidated caller input as a raw ORDER BY column.
Complexity: MEDIUM - small diff, but it touches the data-access query layer shared by every service.
Changes: adds a compound op.and(...) filter combinator and an orderBy: { attribute, direction } option to the PostgREST query layer of spacecat-shared-data-access (4 files).
Must fix before merge
- [Important]
orderBy.attributeis not validated against the field map - unknown attributes are silentlycamelToSnaked into a raw.order()column -packages/spacecat-shared-data-access/src/models/base/base.collection.js:433(details inline) - [Important]
orderBy.directionis exact-string'desc', so'DESC'/ invalid values silently sort ascending, and the new ascending branch is untested -packages/spacecat-shared-data-access/src/models/base/base.collection.js:436(details inline) - [Important] The new public
orderByoption is untyped and undocumented, and cannot be deferred to the consumer PR -packages/spacecat-shared-data-access/src/models/base/base.collection.js:432(details inline)
Non-blocking (5): minor issues and suggestions
- nit: empty or all-falsy
op.and(...)returns the query unchanged, so awherebuilt from an empty condition list matches every row. This is intentional and tested, but document the footgun onop.and-packages/spacecat-shared-data-access/src/util/postgrest.utils.js:155 - nit:
(expr.conditions || [])- the|| []is unreachable (theandbuilder always setsconditionsto an array) and adds a dead branch against the 97% branch-coverage gate; drop it toexpr.conditions.reduce(...)-packages/spacecat-shared-data-access/src/util/postgrest.utils.js:166 - suggestion:
order(legacy string) and the neworderBy(object) now coexist on the base collection; when both are passed,orderBysilently wins. Document the precedence, or expressorderas sugar overorderByso there is one sort path -packages/spacecat-shared-data-access/src/models/base/base.collection.js:432 - suggestion: strengthen the tests - make the back-compat assertion positive (
order('id', { ascending: true })) rather than negative-only, and add cases for a missing/uppercase direction (ascending branch), an unknownorderBy.attribute, and an unsupported operator nested insideop.and(...) - nit: the
packages/spacecat-shared-data-access/CLAUDE.md"PostgREST WHERE operators" list does not include the newandoperator; append it (the ordering example in the same doc also predates this option - see must-fix item 3)
Out of scope, worth tracking: ordering by a non-indexed column pushes the sort server-side, and #queryPage sorts the full filtered set before .range() paginates. For a large table (e.g. sites sorted by updatedAt) this needs a DB index in mysticat-data-service plus a sortable-column allowlist in the spacecat-api-service GET /sites consumer before it ships to a large tenant.
| const ascending = options.order === 'asc'; | ||
| const hasExplicitOrderBy = isObject(options.orderBy) && hasText(options.orderBy.attribute); | ||
| const orderFields = hasExplicitOrderBy | ||
| ? [this.#toDbField(options.orderBy.attribute)] |
There was a problem hiding this comment.
issue (blocking): orderBy.attribute is passed straight to #toDbField, which resolves as map[field] || camelToSnake(field) (postgrest.utils.js:81). An attribute not in the field map does not throw - it is camelToSnaked and forwarded as the raw .order() column. Because camelToSnake only rewrites case boundaries, punctuation like , . ( ) passes through, so an attribute such as id,created_at becomes the token order=id,created_at.asc. PostgREST rejects unknown identifiers (HTTP 400, so this is not SQL injection), but a caller can still order by any real column of the table, including ones never meant to be sortable - an ordering oracle - and typos surface only as an opaque Failed to query error.
This is a shared foundation library; relying on the GET /sites consumer to whitelist columns pushes a security-relevant invariant onto every current and future caller, when the field map to enforce it already lives here. Validate the attribute before ordering, e.g.:
if (hasExplicitOrderBy
&& !Object.prototype.hasOwnProperty.call(this.fieldMaps.toDbMap, options.orderBy.attribute)) {
this.#logAndThrowError(`Failed to query [${this.entityName}]: unknown orderBy attribute [${options.orderBy.attribute}]`);
}
The same non-allowlisting passthrough already exists on the between / where paths; a shared #requireKnownAttribute helper would close it in one place.
| ? [this.#toDbField(options.orderBy.attribute)] | ||
| : this.#getOrderFields(indexName, keys); | ||
| const ascending = hasExplicitOrderBy | ||
| ? options.orderBy.direction !== 'desc' |
There was a problem hiding this comment.
issue (blocking): const ascending = options.orderBy.direction !== 'desc' treats any value other than the exact lowercase string 'desc' as ascending - 'DESC', 'Desc', 'descending', a typo, null, or a missing direction all silently produce the opposite of a descending request, with no error and no log. A silent wrong-order is worse than a thrown error, and this is a shared package with callers beyond the one that validates input.
Normalize or validate: String(options.orderBy.direction).toLowerCase() === 'desc', or reject anything not in { asc, desc }. The added test only exercises direction: 'desc'; the entire ascending branch of the new code is uncovered - add a test for a missing/uppercase direction.
| const orderFields = this.#getOrderFields(indexName, keys); | ||
| const ascending = options.order === 'asc'; | ||
| const hasExplicitOrderBy = isObject(options.orderBy) && hasText(options.orderBy.attribute); | ||
| const orderFields = hasExplicitOrderBy |
There was a problem hiding this comment.
issue (blocking): this adds a new public query option (options.orderBy: { attribute, direction }), consumed by all() / allByIndexKeys(), but QueryOptions in src/models/base/index.d.ts:32 still declares only order?: string - no orderBy. The repo-root CLAUDE.md ("TypeScript Declarations") requires .d.ts files for public APIs, and that type lives in this package, so the PR-body plan to fold it into the spacecat-api-service consumer PR is not possible: a different repo cannot edit spacecat-shared's type contract. Add it here (one line):
orderBy?: { attribute: string; direction?: 'asc' | 'desc' };
While there, the already-missing where / between options could join it. Separately, this orderBy: { attribute, ... } shape diverges from the ordering example in packages/spacecat-shared-data-access/CLAUDE.md (order: { field, direction }, which is itself already stale - the real order is a plain string). Update that example to the current contract in the same PR.
There was a problem hiding this comment.
Hey @bottadobe,
Verdict: Approve - prior blocking findings addressed; one minor doc gap remains.
Complexity: MEDIUM - recursive expression evaluator plus new query option in shared library.
Changes: Adds compound AND filter (op.and) and explicit orderBy column option for PostgREST queries, with full input validation (4 files + tests).
Previously flagged, now resolved
- orderBy.attribute now validated against the field map (throws on unknown attribute)
- orderBy.direction normalized to lowercase, validated to asc/desc only (throws on invalid)
- TypeScript type added to QueryOptions interface
- CLAUDE.md example updated to use the new orderBy shape
Non-blocking (1): minor issues and suggestions
- nit: CLAUDE.md operators list at line 198 still reads
eq,ne,gt,gte,lt,lte,is,in,contains,like,ilikewithout the newandcombinator - append it so developers discover compound query support from the docs -packages/spacecat-shared-data-access/CLAUDE.md:198
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 34s | Cost: $3.66 | Commit: 07fcfa2fc200eb4817f2a8956aa6151a1966135e
If this code review was useful, please react with 👍. Otherwise, react with 👎.
dzehnder
left a comment
There was a problem hiding this comment.
Hey @bottadobe,
⚠ Degraded review - no spec document was found for this change (searched the PR links, the touched repo's docs, the architecture/guidelines docs, and linked Jira). This review covers code-level quality but could not validate the change against an agreed design, so confidence is reduced. Add a spec link (PR template section 4) and re-request review for a full-confidence pass.
Verdict: Approve - the three blocking findings from the prior review are resolved with attribute/direction validation, a type declaration, new tests, and a doc fix.
Complexity: MEDIUM - small diff, data-access query layer shared by every service.
Changes: hardens the new orderBy: { attribute, direction } option with field-map validation, case-insensitive direction, a QueryOptions type, and tests (this update: 4 files).
Note: CI Test check is still running - confirm green before merge.
Previously flagged, now resolved
orderBy.attributeis now validated against the field map and throwsunknown orderBy attribute, closing the raw-column passthrough.orderBy.directionis normalized case-insensitively, defaults toasc, and throws on invalid values; the ascending, omitted, andDESCbranches are now tested.orderByis declared inQueryOptions(index.d.ts), and theCLAUDE.mdordering example is updated to theorderBy: { attribute, ... }shape.
Non-blocking (3): carried over from the prior review, still open
- nit: empty or all-falsy
op.and(...)still returns the query unchanged (matches every row) -packages/spacecat-shared-data-access/src/util/postgrest.utils.js:155 - nit:
(expr.conditions || [])remains an unreachable branch against the 97% coverage gate -packages/spacecat-shared-data-access/src/util/postgrest.utils.js:166 - nit: the
CLAUDE.md"PostgREST WHERE operators" list still omits the newandoperator
## [@adobe/spacecat-shared-data-access-v4.20.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.19.0...@adobe/spacecat-shared-data-access-v4.20.0) (2026-08-12) ### Features * **data-access:** compound AND filter and orderBy column for postgrest queries ([#1874](#1874)) ([620b37e](620b37e))
|
🎉 This PR is included in version @adobe/spacecat-shared-data-access-v4.20.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
What
Two additive capabilities on the PostgREST query layer of
@adobe/spacecat-shared-data-access:op.and(...conditions)in thewherebuilder (src/util/postgrest.utils.js) — combine multiple column predicates withANDin a single query.orderBy: { attribute, direction }option onall()/#queryPage(src/models/base/base.collection.js) — order results by an arbitrary model attribute (mapped to its DB column),asc/desc, with the existing id tiebreaker preserved.Why
The Experience Success Studio back-office
/sitespage currently downloads the entire sites dataset and filters/sorts it client-side — a slow initial load and an unbounded payload that grows with every site. Moving that work server-side requires the data-access layer to do two things it couldn't before:applyWherepreviously applied only a single condition, so combined server-side filters were impossible.updatedAtdesc). Ordering was previously locked to the index-derived sort key.These two primitives unblock the
spacecat-api-serviceGET /sitesfiltered/sorted endpoint that the back-office consumes.How
applyWheregains anandoperator returning{ type: 'and', conditions }; a recursiveapplyExprapplies each sub-condition to the query in sequence (PostgREST chains filters asAND). Every existing single-condition operator path is unchanged.#queryPagehonorsoptions.orderBywhenorderBy.attributeis set (attribute → DB column via the existing field map; directionasc/desc), keeping the id tiebreaker; whenorderByis absent, ordering is byte-for-byte as before.Scope & backward compatibility
whereand index-derived ordering behave identically to before.Testing
op.and(test/unit/util/postgrest.utils.test.js);orderBycolumn+direction and the no-orderByfallback exercised against the real PostgREST query path (test/unit/models/.../*.collection.test.js).Downstream / follow-ups
spacecat-api-serviceGET /sites(separate PR), which whitelists sortable columns and validates direction — so unknown-column / casing concerns are handled at the API boundary.orderBy(and the already-absentwhere/between) could be added to theQueryOptionstype insrc/models/base/index.d.ts; best folded into the consumer PR.