From 1afde54ffd7fd5c07874e8a718ac26d5daa60041 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 07:50:33 +0000 Subject: [PATCH 1/2] docs(skills): objectstack-query teaches the served route for a related record's column, not the refused nested form The published query skill taught `{ relation: { field: value } }` under a lookup as a working filter. The engine refuses that form on every driver (`INVALID_FILTER` / 400) and names the route that works: filter the related object first, then `$in` its ids (`$contains` per id on a multi-valued lookup). The six sites that taught or pointed at the form now state the refusal and the route; the rule section's rewrite is paid for inside `rules/filters.md` by deleting three examples whose rule already lives in `SKILL.md`. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg --- skills/objectstack-query/SKILL.md | 20 ++++--- skills/objectstack-query/rules/filters.md | 65 +++++------------------ 2 files changed, 23 insertions(+), 62 deletions(-) diff --git a/skills/objectstack-query/SKILL.md b/skills/objectstack-query/SKILL.md index e9139347c19..62f9efe2466 100644 --- a/skills/objectstack-query/SKILL.md +++ b/skills/objectstack-query/SKILL.md @@ -73,7 +73,7 @@ both are given. Writes take only the trailing argument. | Removed key | Live replacement | |:--|:--| | `query.cursor` | keyset paging — `where` on the sort key + `orderBy` + `limit` | -| `query.joins` | `expand`, or a nested relation filter | +| `query.joins` | `expand` (display), or filter the related object and `$in` its ids | | `query.distinct` | `groupBy` the fields — each unique combination is one row | | `query.windowFunctions` | report/dashboard metadata (**objectstack-ui**), or rank / accumulate in app code | | aggregation `distinct: true` | `count_distinct` | @@ -85,7 +85,7 @@ procedure and the full tombstone register are **objectstack-upgrade**. ## Quick Reference — Detailed Rules -- **[Filters](./rules/filters.md)** — all operators, logical combinations, nested relations, date macros and session tokens +- **[Filters](./rules/filters.md)** — all operators, logical combinations, filtering by a related record, date macros and session tokens - **[Aggregation](./rules/aggregation.md)** — groupBy, date bucketing, functions, `having`, per-measure `filter` - **[Pagination](./rules/pagination.md)** — offset vs keyset, best practices, performance @@ -184,14 +184,11 @@ Combine conditions with `$and`, `$or`, and `$not`: { where: { $not: { status: 'closed' } } } ``` -### Nested Relation Filters +### Filtering by a related record -Filter through relationships without an explicit join: - -```typescript -// Accounts whose related contact has a verified profile -{ object: 'account', where: { contact: { profile: { verified: true } } } } -``` +`{ customer: { country: 'US' } }` beneath a lookup is refused, `INVALID_FILTER` / +400: filter the related object first, then `$in` its ids — +**[filter rules → Relation Filters](./rules/filters.md)**. ### Cross-field comparisons @@ -331,7 +328,7 @@ that set, never widen it: over the REST/protocol ingress a name outside it is Mirror the related record's title into a **stored** field on the queried object and search that; the field, the write hooks and the lint wording are **objectstack-data → Search Fields (`searchableFields`)**. To *filter* by a -related record's column use a nested relation filter; to *display* it, `expand`. +related record's column, `$in` ids from its own query; to *display* it, `expand`. ## Common Patterns @@ -340,7 +337,8 @@ related record's column use a nested relation filter; to *display* it, `expand`. | Scenario | Use | |:---------|:----| | Load lookup fields for display | `expand` | -| Filter parent by child conditions | Nested relation filter | +| Filter rows by their lookup target's column | Query the target object, then `{ lookup: { $in: ids } }` — `$contains` per id when `multiple` | +| Filter parent by child conditions | Query the child with `fields: [lookup]`, then `{ id: { $in: those ids } }` on the parent | | **Keyword-search by a related record's title** | **Mirror the title into a stored field on this object and search that** — `search` never traverses | | Paginate/sort a parent's related records | Query the related object directly | | Analytical queries across objects | Report/dashboard metadata, or separate queries combined in app code | diff --git a/skills/objectstack-query/rules/filters.md b/skills/objectstack-query/rules/filters.md index af50457826a..a2dd2abe794 100644 --- a/skills/objectstack-query/rules/filters.md +++ b/skills/objectstack-query/rules/filters.md @@ -37,18 +37,6 @@ allowlist that omits them and refuse — `Unsupported filter operator`, `INVALID_FILTER` / 400 — rather than approximating. A pattern ending in a lone unpaired backslash is refused by every face. -## Implicit Equality (Shorthand) - -The most common filter — equality — has a shorthand: - -```typescript -// ✅ Implicit equality (preferred for simple cases) -where: { status: 'active' } - -// ✅ Explicit equality (same result) -where: { status: { $eq: 'active' } } -``` - ## Logical Operators ### AND (implicit) @@ -90,28 +78,6 @@ where: { } ``` -### NOT - -```typescript -// ✅ Exclude deleted records -where: { - $not: { status: 'deleted' } -} -``` - -### Combining Logical Operators - -```typescript -// ✅ Active users who are admin OR have high score -where: { - status: 'active', // AND - $or: [ - { role: 'admin' }, - { score: { $gte: 90 } } - ] -} -``` - ## Field References > ✅ **Enforced.** `{ $field: '...' }` compares two columns of the same row. @@ -129,28 +95,25 @@ Legal in a **comparison** position only. As an `$in` / `$nin` member or a `$between` endpoint it is refused at parse — no evaluation path resolves a reference there. -## Nested Relation Filters +## Relation Filters -Filter by a related object's fields: +A plain object with no `$` operator beneath a relation field (`lookup`, +`master_detail`, `user`, `tree`) is refused, `INVALID_FILTER` / 400 on every +driver: the column stores the related record's id, and no driver follows it into +the related object. Filter the related object first, then `$in` its ids: ```typescript -// ✅ Find orders where the customer is in the US -where: { - customer: { - country: 'US' - } -} - -// ✅ Deeper nesting -where: { - customer: { - organization: { - industry: 'Technology' - } - } -} +// ❌ Refused: where: { customer: { country: 'US' } } +// ✅ Orders whose customer is in the US +const us = await engine.find('customer', { where: { country: 'US' }, fields: ['id'] }); +where: { customer: { $in: us.map((c) => c.id) } } ``` +On a `multiple: true` lookup match each id with `$contains` (an `$or` of those +for several); the SQL driver refuses `$in` there. A parent by its children's +fields is the same two steps reversed: query the child with the condition and +`fields: [the lookup]`, then `{ id: { $in: … } }` on the parent. + ## Common Mistakes ### ❌ Wrong: expecting sibling keys to be an OR From b893787bfd1323878680c47487e47c8ef6b47130 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 07:55:04 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(gates):=20ratchet=20the=20role-word?= =?UTF-8?q?=20baseline=20down=20for=20rules/filters.md=20(8=20=E2=86=92=20?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deleted "Combining Logical Operators" example carried one `role: 'admin'` literal; `check:role-word` prescribes the ratchet-down and this is its `--update` output, one row. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg --- scripts/role-word-baseline.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/role-word-baseline.json b/scripts/role-word-baseline.json index 2b9bef2686d..6d8a1a13ddb 100644 --- a/scripts/role-word-baseline.json +++ b/scripts/role-word-baseline.json @@ -39,7 +39,7 @@ "skills/objectstack-data/references/data-hooks.md": 3, "skills/objectstack-data/rules/relationships.md": 1, "skills/objectstack-platform/SKILL.md": 2, - "skills/objectstack-query/rules/filters.md": 8, + "skills/objectstack-query/rules/filters.md": 7, "skills/objectstack-ui/rules/list-views.md": 1, "skills/objectstack-ui/rules/navigation.md": 1, "skills/objectstack-upgrade/references/examples-upgrade.md": 1