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: 2 additions & 0 deletions .changeset/7297-record-id-filter-token.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ A filter on a record page can now be scoped to the record the page shows (object
The id comes from the record page's mounted record context, never from a URL parameter or a page variable. `@object-ui/react`'s `useFilterScope()` adds it to the scope it returns (as the new optional `recordId` member of `@object-ui/core`'s `FilterTokenScope`) whenever a `RecordContextProvider` is mounted above the component; `FilterScopeProvider` is unchanged and still carries only the session values. The held filters in `useResolvedFilter`, `object-grid` and `object-view` resolve again when the record changes, so moving to another record queries again without a remount.

Anywhere with no record in context (a list view, a dashboard, a report, or a page that is not a record page) `{record_id}` is refused by name: `resolveContextTokens` reports it through `onUnresolved` (a console warning by default), the way an unresolved `{current_user_id}` is reported, and leaves it as written. It never becomes `null` and its condition is never dropped, so a number can never silently count every record; the ObjectStack server refuses the leftover token by name (`FILTER_TOKEN_UNRESOLVED`). Like the session tokens, `{record_id}` only scopes what a component shows. It is not access control: which rows a user may read is still decided by the server's row-level security.

**Correction, 2026-09-30 (objectui#8945).** The example in the first paragraph, `{ "assignee": "{record_id}" }`, is the retired MongoDB-style record form. `@objectstack/spec` 17.5.0 refuses it at an `element:number` `filter`, because that key takes an array and a record is refused by kind. Write the same condition as a `ViewFilterRule` array, which the spec accepts: `[{ "field": "assignee", "operator": "equals", "value": "{record_id}" }]`. A component-level `dataSource.filter` takes the same array. The `{record_id}` token itself, and the rest of this entry, are unaffected.
38 changes: 38 additions & 0 deletions .changeset/8945-element-datasource-filter-note.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@object-ui/core': patch
---

docs(core): the `ElementDataSourceConfig.filter` note now separates what an author may write from what a renderer may still receive (objectui#8945)

The note said "three shapes legitimately reach a renderer here" — a MongoDB-style
record, an ObjectQL AST tuple array and a spec `ViewFilterRule` array — and typed
`filter` as `unknown` "rather than the spec's `FilterCondition`". Since the spec's
`filter` doors converged on the rule array (objectui#6206; migration
`element-data-source-and-object-block-filter-rule-array`), that sentence mixes two
populations the convergence split apart:

- **What an author may write** is the `ViewFilterRule` array alone,
`[{ field, operator, value }, …]`. `ElementDataSourceSchema` refuses the record
form by kind and refuses an AST tuple array because each member must be a rule
object.
- **What a renderer may still receive** is all three. The D2 conversion
`page-component-filter-record-to-rule-array` rewrites a stored `filter` only
where the rule array spells it losslessly (a flat record, an operator object
whose operators the rule vocabulary spells, several such keys, or a
single-level AST tuple array), on every ObjectStack stored-row read and under
`os migrate meta --stored`. It leaves exactly as stored a filter carrying
`$and` / `$or` / `$not`, any filter with a part that has no lossless rule
spelling (a `null` value, `$null` / `$exists` or an AST `like`, an array or
object comparand in equality position, an AST `and` / `or` group), and every
filter of a component whose rows are inline. This renderer is backend-agnostic
and replays no conversion itself; only ObjectStack's own data-at-rest seams do.
So all three shapes may still arrive, and `mergeFilterNodes` still lowers each
of them.

The note now states both, names the spec's type as `ViewFilterRule[]` rather than
`FilterCondition`, and keeps `filter` typed `unknown` for the reason it always
had: this interface carries stored values, and narrowing it would only move the
cast. The module's example binding writes `"filter": [ … ]` instead of the record
form it used to show.

Comments only. No type, export, runtime behaviour or accepted set changes.
39 changes: 33 additions & 6 deletions packages/core/src/data-scope/element-data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* than the page's own context:
*
* ```json
* { "object": "account", "view": "hot", "filter": { … }, "sort": [ … ], "limit": 20 }
* { "object": "account", "view": "hot", "filter": [ … ], "sort": [ … ], "limit": 20 }
* ```
*
* This module owns the two pure halves of consuming it — telling the METADATA
Expand Down Expand Up @@ -76,11 +76,38 @@ export interface ElementDataSourceSort {
/**
* Element-level data source — matches `@objectstack/spec` `ElementDataSourceSchema`.
*
* `filter` is typed `unknown` rather than the spec's `FilterCondition` because
* three shapes legitimately reach a renderer here (a MongoDB-style condition
* object, an ObjectQL AST node array, a spec `ViewFilterRule[]`) and
* {@link mergeFilterNodes} is the single sink that lowers all three. Narrowing
* the type here would only move the cast, not remove it.
* `filter` has two populations, and since the upstream convergence they differ:
*
* - **What an author may write** is the spec's `ViewFilterRule` array alone —
* `[{ field, operator, value }, …]`. The spec's `filter` doors converged on it
* (objectui#6206; migration `element-data-source-and-object-block-filter-rule-array`),
* and `ElementDataSourceSchema` refuses the other two shapes at the door: the
* MongoDB-style record form (`{ status: 'open' }`) by kind, and an ObjectQL
* AST tuple array (`[['status', '=', 'open']]`) because each member must be a
* rule object.
* - **What a renderer may still receive** is all three. The D2 conversion that
* migration entry records, `page-component-filter-record-to-rule-array`,
* rewrites a stored `filter` only where the rule array spells it losslessly —
* a flat record, an operator object whose operators the rule vocabulary
* spells, several such keys, or a single-level AST tuple array — on every
* ObjectStack stored-row read and under `os migrate meta --stored`. It leaves
* exactly as stored a filter carrying `$and` / `$or` / `$not`; any filter with
* a part that has no lossless rule spelling (a `null` value, an operator such
* as `$null` / `$exists` or an AST `like`, an array or object comparand in
* equality position, an AST `and` / `or` group); and every filter, the
* binding's included, of a component whose rows are inline
* (`data: { provider: 'value' }`, a `data` array, or `staticData`). And this
* renderer is backend-agnostic: it replays no conversion itself, and only
* ObjectStack's own data-at-rest seams do, so a page that reaches it any other
* way arrives as it was written. Any of the three shapes may therefore still
* arrive here, beside the rule array.
*
* `filter` is typed `unknown` rather than the spec's `ViewFilterRule[]` because
* this interface carries the second population, not the first, and
* {@link mergeFilterNodes} is the single sink that lowers all three shapes.
* Narrowing the type here would only move the cast, not remove it, and it would
* not stop a stored record-form filter from arriving. Refusing the retired
* shapes is the spec schema's job at the authoring door, not this type's.
*/
export interface ElementDataSourceConfig {
object: string;
Expand Down
Loading