Skip to content

Commit 1afde54

Browse files
committed
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg
1 parent 96e7244 commit 1afde54

2 files changed

Lines changed: 23 additions & 62 deletions

File tree

‎skills/objectstack-query/SKILL.md‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ both are given. Writes take only the trailing argument.
7373
| Removed key | Live replacement |
7474
|:--|:--|
7575
| `query.cursor` | keyset paging — `where` on the sort key + `orderBy` + `limit` |
76-
| `query.joins` | `expand`, or a nested relation filter |
76+
| `query.joins` | `expand` (display), or filter the related object and `$in` its ids |
7777
| `query.distinct` | `groupBy` the fields — each unique combination is one row |
7878
| `query.windowFunctions` | report/dashboard metadata (**objectstack-ui**), or rank / accumulate in app code |
7979
| aggregation `distinct: true` | `count_distinct` |
@@ -85,7 +85,7 @@ procedure and the full tombstone register are **objectstack-upgrade**.
8585

8686
## Quick Reference — Detailed Rules
8787

88-
- **[Filters](./rules/filters.md)** — all operators, logical combinations, nested relations, date macros and session tokens
88+
- **[Filters](./rules/filters.md)** — all operators, logical combinations, filtering by a related record, date macros and session tokens
8989
- **[Aggregation](./rules/aggregation.md)** — groupBy, date bucketing, functions, `having`, per-measure `filter`
9090
- **[Pagination](./rules/pagination.md)** — offset vs keyset, best practices, performance
9191

@@ -184,14 +184,11 @@ Combine conditions with `$and`, `$or`, and `$not`:
184184
{ where: { $not: { status: 'closed' } } }
185185
```
186186

187-
### Nested Relation Filters
187+
### Filtering by a related record
188188

189-
Filter through relationships without an explicit join:
190-
191-
```typescript
192-
// Accounts whose related contact has a verified profile
193-
{ object: 'account', where: { contact: { profile: { verified: true } } } }
194-
```
189+
`{ customer: { country: 'US' } }` beneath a lookup is refused, `INVALID_FILTER` /
190+
400: filter the related object first, then `$in` its ids —
191+
**[filter rules → Relation Filters](./rules/filters.md)**.
195192

196193
### Cross-field comparisons
197194

@@ -331,7 +328,7 @@ that set, never widen it: over the REST/protocol ingress a name outside it is
331328
Mirror the related record's title into a **stored** field on the queried object
332329
and search that; the field, the write hooks and the lint wording are
333330
**objectstack-data → Search Fields (`searchableFields`)**. To *filter* by a
334-
related record's column use a nested relation filter; to *display* it, `expand`.
331+
related record's column, `$in` ids from its own query; to *display* it, `expand`.
335332

336333
## Common Patterns
337334

@@ -340,7 +337,8 @@ related record's column use a nested relation filter; to *display* it, `expand`.
340337
| Scenario | Use |
341338
|:---------|:----|
342339
| Load lookup fields for display | `expand` |
343-
| Filter parent by child conditions | Nested relation filter |
340+
| Filter rows by their lookup target's column | Query the target object, then `{ lookup: { $in: ids } }` — `$contains` per id when `multiple` |
341+
| Filter parent by child conditions | Query the child with `fields: [lookup]`, then `{ id: { $in: those ids } }` on the parent |
344342
| **Keyword-search by a related record's title** | **Mirror the title into a stored field on this object and search that** — `search` never traverses |
345343
| Paginate/sort a parent's related records | Query the related object directly |
346344
| Analytical queries across objects | Report/dashboard metadata, or separate queries combined in app code |

‎skills/objectstack-query/rules/filters.md‎

Lines changed: 14 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,6 @@ allowlist that omits them and refuse — `Unsupported filter operator`,
3737
`INVALID_FILTER` / 400 — rather than approximating. A pattern ending in a lone
3838
unpaired backslash is refused by every face.
3939

40-
## Implicit Equality (Shorthand)
41-
42-
The most common filter — equality — has a shorthand:
43-
44-
```typescript
45-
// ✅ Implicit equality (preferred for simple cases)
46-
where: { status: 'active' }
47-
48-
// ✅ Explicit equality (same result)
49-
where: { status: { $eq: 'active' } }
50-
```
51-
5240
## Logical Operators
5341

5442
### AND (implicit)
@@ -90,28 +78,6 @@ where: {
9078
}
9179
```
9280

93-
### NOT
94-
95-
```typescript
96-
// ✅ Exclude deleted records
97-
where: {
98-
$not: { status: 'deleted' }
99-
}
100-
```
101-
102-
### Combining Logical Operators
103-
104-
```typescript
105-
// ✅ Active users who are admin OR have high score
106-
where: {
107-
status: 'active', // AND
108-
$or: [
109-
{ role: 'admin' },
110-
{ score: { $gte: 90 } }
111-
]
112-
}
113-
```
114-
11581
## Field References
11682

11783
> ✅ **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
12995
`$between` endpoint it is refused at parse — no evaluation path resolves a
13096
reference there.
13197

132-
## Nested Relation Filters
98+
## Relation Filters
13399

134-
Filter by a related object's fields:
100+
A plain object with no `$` operator beneath a relation field (`lookup`,
101+
`master_detail`, `user`, `tree`) is refused, `INVALID_FILTER` / 400 on every
102+
driver: the column stores the related record's id, and no driver follows it into
103+
the related object. Filter the related object first, then `$in` its ids:
135104

136105
```typescript
137-
// ✅ Find orders where the customer is in the US
138-
where: {
139-
customer: {
140-
country: 'US'
141-
}
142-
}
143-
144-
// ✅ Deeper nesting
145-
where: {
146-
customer: {
147-
organization: {
148-
industry: 'Technology'
149-
}
150-
}
151-
}
106+
// ❌ Refused: where: { customer: { country: 'US' } }
107+
// ✅ Orders whose customer is in the US
108+
const us = await engine.find('customer', { where: { country: 'US' }, fields: ['id'] });
109+
where: { customer: { $in: us.map((c) => c.id) } }
152110
```
153111

112+
On a `multiple: true` lookup match each id with `$contains` (an `$or` of those
113+
for several); the SQL driver refuses `$in` there. A parent by its children's
114+
fields is the same two steps reversed: query the child with the condition and
115+
`fields: [the lookup]`, then `{ id: { $in: … } }` on the parent.
116+
154117
## Common Mistakes
155118

156119
### ❌ Wrong: expecting sibling keys to be an OR

0 commit comments

Comments
 (0)