diff --git a/.changeset/7297-record-id-filter-token.md b/.changeset/7297-record-id-filter-token.md index d6416e7f6a..3fa1ab10b1 100644 --- a/.changeset/7297-record-id-filter-token.md +++ b/.changeset/7297-record-id-filter-token.md @@ -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. diff --git a/.changeset/8945-element-datasource-filter-note.md b/.changeset/8945-element-datasource-filter-note.md new file mode 100644 index 0000000000..4e521a54a9 --- /dev/null +++ b/.changeset/8945-element-datasource-filter-note.md @@ -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. diff --git a/packages/core/src/data-scope/element-data-source.ts b/packages/core/src/data-scope/element-data-source.ts index ddfcaaeba3..1d9f9b6c2b 100644 --- a/packages/core/src/data-scope/element-data-source.ts +++ b/packages/core/src/data-scope/element-data-source.ts @@ -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 @@ -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;