Skip to content

Commit e13345b

Browse files
docs(skills): objectstack-query states the served nested-relation filter, its limits, and the two-step route past them (WIP)
Claude-Session: https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg Co-authored-by: Claude <noreply@anthropic.com>
1 parent 00a92e1 commit e13345b

2 files changed

Lines changed: 25 additions & 53 deletions

File tree

‎skills/objectstack-query/SKILL.md‎

Lines changed: 7 additions & 6 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` (display), or filter the related object and `$in` its ids |
76+
| `query.joins` | `expand` (display), or `{ relation: { field: value } }` in `where` (filter) |
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` |
@@ -186,9 +186,10 @@ Combine conditions with `$and`, `$or`, and `$not`:
186186

187187
### Filtering by a related record
188188

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)**.
189+
`{ customer: { country: 'US' } }` beneath a lookup is served in `where`: the
190+
engine reads the related object as the caller and matches its ids. The limits
191+
(one level, forward only, `where` only, 1000 ids, the caller's permissions) and
192+
the two-step route past them: **[filter rules → Relation Filters](./rules/filters.md)**.
192193

193194
### Cross-field comparisons
194195

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

333334
## Common Patterns
334335

@@ -337,7 +338,7 @@ related record's column, `$in` ids from its own query; to *display* it, `expand`
337338
| Scenario | Use |
338339
|:---------|:----|
339340
| Load lookup fields for display | `expand` |
340-
| Filter rows by their lookup target's column | Query the target object, then `{ lookup: { $in: ids } }` — `$contains` per id when `multiple` |
341+
| Filter rows by their lookup target's column | `{ lookup: { column: value } }` in `where`, up to 1000 related ids; past the cap query the target, then `{ lookup: { $in: ids } }` — `$contains` per id when `multiple` |
341342
| Filter parent by child conditions | Query the child with `fields: [lookup]`, then `{ id: { $in: those ids } }` on the parent |
342343
| **Keyword-search by a related record's title** | **Mirror the title into a stored field on this object and search that** — `search` never traverses |
343344
| Paginate/sort a parent's related records | Query the related object directly |

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

Lines changed: 18 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -37,47 +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-
## Logical Operators
41-
42-
### AND (implicit)
43-
44-
All top-level conditions are AND-combined by default:
45-
46-
```typescript
47-
// ✅ Implicit AND — all conditions must match
48-
where: {
49-
status: 'active',
50-
role: 'admin',
51-
age: { $gte: 18 }
52-
}
53-
54-
// ✅ Explicit $and — same result
55-
where: {
56-
$and: [
57-
{ status: 'active' },
58-
{ role: 'admin' },
59-
{ age: { $gte: 18 } }
60-
]
61-
}
62-
```
63-
64-
### OR
65-
66-
```typescript
67-
// ✅ Find admins OR managers
68-
where: {
69-
$or: [
70-
{ role: 'admin' },
71-
{ role: 'manager' }
72-
]
73-
}
74-
75-
// ✅ Equivalent using $in
76-
where: {
77-
role: { $in: ['admin', 'manager'] }
78-
}
79-
```
80-
8140
## Field References
8241

8342
> ✅ **Enforced.** `{ $field: '...' }` compares two columns of the same row.
@@ -97,14 +56,26 @@ reference there.
9756

9857
## Relation Filters
9958

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:
59+
A condition on a related record's fields beneath a relation field (`lookup`,
60+
`master_detail`, `user`, `tree`) is served in `where`: the engine reads the
61+
related object with it **as the caller**, then matches the field against the
62+
ids it returns (`$in`; any member when `multiple: true`).
10463

10564
```typescript
106-
// ❌ Refused: where: { customer: { country: 'US' } }
10765
// ✅ Orders whose customer is in the US
66+
where: { customer: { country: 'US' } }
67+
```
68+
69+
Limits — one level: every key a field the related object declares, no relation
70+
or dotted key inside; forward only: never a parent by its children; `where`
71+
only: an aggregation's `filter` and `having` refuse it, `INVALID_FILTER` / 400;
72+
at most 1000 related ids, refused past that, `INVALID_FILTER` / 400, never
73+
truncated; as the caller: the related object's row scope and field permissions
74+
apply, so a field the caller cannot read is refused, `PERMISSION_DENIED` / 403,
75+
never an empty result. Past the cap, run the two steps yourself — filter the
76+
related object, then `$in` its ids:
77+
78+
```typescript
10879
const us = await engine.find('customer', { where: { country: 'US' }, fields: ['id'] });
10980
where: { customer: { $in: us.map((c) => c.id) } }
11081
```
@@ -119,7 +90,7 @@ fields is the same two steps reversed: query the child with the condition and
11990
### ❌ Wrong: expecting sibling keys to be an OR
12091

12192
`where: { role: 'admin', status: 'active' }` is an AND — sibling keys always
122-
are. For OR, wrap them in a `$or` array (see **Logical Operators** above).
93+
are. For OR, wrap them in a `$or` array (SKILL.md, **Logical Operators**).
12394

12495
### ❌ Wrong: Using string operators on non-string fields
12596

0 commit comments

Comments
 (0)