Skip to content

Commit bd718b6

Browse files
committed
docs(query-syntax): Filtering Across Relationships states the served nested-relation form
1 parent 33b6e8b commit bd718b6

1 file changed

Lines changed: 54 additions & 11 deletions

File tree

‎content/docs/protocol/objectql/query-syntax.mdx‎

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -600,29 +600,72 @@ have kept a `null`-valued field.
600600

601601
### Filtering Across Relationships
602602

603-
<Callout type="warn">
604-
**Relation traversal inside `where` is not supported.** Neither the nested form
605-
(`where: { account: { industry: 'tech' } }`) nor a dotted path
606-
(`where: { 'account.industry': 'tech' }`) is resolved. `SqlDriver.applyFilters()` only
607-
recognises a nested object as an operator map when its keys start with `$`; anything
608-
else is compiled as a comparison against a single column of the queried table, and a
609-
dotted key is emitted verbatim, so Knex renders it as `"account"."industry"` against a
610-
table that was never joined.
611-
</Callout>
603+
Filter on a related record's fields by nesting the condition beneath the relation
604+
field in `where`:
605+
606+
```typescript
607+
// Opportunities whose account is in the tech industry
608+
const opportunities = await engine.find('opportunity', {
609+
where: { account: { industry: 'tech' } },
610+
});
611+
```
612612

613-
Filter on the local foreign key, or run two queries:
613+
A condition on a related record's fields beneath a relation field (`lookup`,
614+
`master_detail`, `user`, `tree`) is served in `where`: the engine reads the
615+
related object with it **as the caller**, then matches the field against the
616+
ids it returns (`$in`; any member when `multiple: true`).
617+
618+
Limits — one level: every key a field the related object declares, no relation
619+
or dotted key inside; forward only: never a parent by its children; `where`
620+
only: an aggregation's `filter` and `having` refuse it, `INVALID_FILTER` / 400;
621+
at most 1000 related ids, refused past that, `INVALID_FILTER` / 400, never
622+
truncated; as the caller: the related object's row scope and field permissions
623+
apply, so a field the caller cannot read is refused, `PERMISSION_DENIED` / 403,
624+
never an empty result.
625+
626+
- **Every verb that takes a `where`** serves it — `find`, `findOne`, `count`,
627+
`aggregate`, `update` and `delete` — and so do the REST `GET /data/:object` and
628+
`POST /data/:object/query` doors, which read through the same engine calls. A
629+
single-valued relation matches with `$in` on the related ids; a multi-valued one
630+
(`multiple: true`) matches a record when **any** member qualifies, written as one
631+
`$contains` per id under an `$or`.
632+
- **A second level is refused.** `{ account: { owner: { region: 'NA' } } }` and
633+
`{ account: { 'owner.region': 'NA' } }` answer `INVALID_FILTER` / 400.
634+
- **The reverse direction is not served.** A parent filtered by its children is not
635+
written as `{ opportunities: { stage: 'won' } }`: the parent has no such field, so the
636+
REST doors answer `INVALID_FIELD` / 400.
637+
- **A dotted path is still refused.** `where: { 'account.industry': 'tech' }` answers
638+
`INVALID_FIELD` / 400 at the engine and at the REST doors; the message names the
639+
nested spelling to write instead.
640+
641+
Past the 1000-id cap, and for the reverse direction, run the two steps yourself —
642+
filter the related object, then `$in` its ids:
614643

615644
```typescript
645+
// Past the cap: read the related ids, then match the local key
616646
const techAccounts = await engine.find('account', {
617647
where: { industry: 'tech', annual_revenue: { $gt: 1000000 } },
618648
fields: ['id'],
619649
});
620650

621651
const opportunities = await engine.find('opportunity', {
622-
where: { account_id: { $in: techAccounts.map((a) => a.id) } },
652+
where: { account: { $in: techAccounts.map((a) => a.id) } },
653+
});
654+
655+
// Reverse: accounts that have a won opportunity
656+
const won = await engine.find('opportunity', {
657+
where: { stage: 'won' },
658+
fields: ['account'],
659+
});
660+
661+
const accounts = await engine.find('account', {
662+
where: { id: { $in: won.map((o) => o.account) } },
623663
});
624664
```
625665

666+
On a `multiple: true` relation, match the ids with one `$contains` per id under an
667+
`$or` instead of `$in`.
668+
626669
### Filtering on a `formula` field
627670

628671
<Callout type="warn">

0 commit comments

Comments
 (0)