Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/role-word-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 9 additions & 11 deletions skills/objectstack-query/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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 |
Expand Down
65 changes: 14 additions & 51 deletions skills/objectstack-query/rules/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down
Loading