From 4837c74e6e3430cf2ba3e4d959e258f931f325e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 22:58:41 +0000 Subject: [PATCH 1/3] docs(core): re-state the ElementDataSourceConfig.filter note for the two populations The note said three filter shapes "legitimately reach a renderer here" and typed filter as unknown "rather than the spec's FilterCondition". Since the spec's filter doors converged on the ViewFilterRule array (objectui#6206), that mixes what an author may write (the rule array alone; the record form and AST tuple arrays are refused at ElementDataSourceSchema) with what a renderer may still receive (all three, because metadata at rest is not rewritten). The note now states both, names the spec type as ViewFilterRule[], and keeps the unknown type and its reason. The module example binding writes a rule array instead of the record form. Comments and a patch changeset only. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .../8945-element-datasource-filter-note.md | 28 +++++++++++++++++++ .../src/data-scope/element-data-source.ts | 27 ++++++++++++++---- 2 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 .changeset/8945-element-datasource-filter-note.md diff --git a/.changeset/8945-element-datasource-filter-note.md b/.changeset/8945-element-datasource-filter-note.md new file mode 100644 index 0000000000..2a3abfed22 --- /dev/null +++ b/.changeset/8945-element-datasource-filter-note.md @@ -0,0 +1,28 @@ +--- +'@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 convergence does not + rewrite metadata at rest, so a stored page carrying the record form or a tuple + array keeps arriving, 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..39d441ce6a 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,26 @@ 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 convergence + * deliberately does not rewrite metadata at rest, so a stored page carrying + * the record form or an AST tuple array keeps arriving 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; From 0b4445d5a174f6eff45d5a6a935ad46c8189dff4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 23:19:28 +0000 Subject: [PATCH 2/3] docs(changeset): note that the 7297 record-form filter example is refused on 17.5.0 The pending objectui#7297 changeset teaches { "assignee": "{record_id}" } as an element:number filter. @objectstack/spec 17.5.0 refuses that record form at the filter key (invalid_type, expected array) and accepts the ViewFilterRule array spelling. Append a dated correction giving the accepted spelling; frontmatter and existing sentences are unchanged. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .changeset/7297-record-id-filter-token.md | 2 ++ 1 file changed, 2 insertions(+) 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. From a9dd51ca03d2dfdc90d9da4f137abd6165d344a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 23:36:03 +0000 Subject: [PATCH 3/3] docs(core): give the upstream at-rest disposition as the reason renderers still see all three filter shapes The re-stated ElementDataSourceConfig note and its changeset said the convergence does not rewrite metadata at rest. The migration entry they cite says the opposite: the D2 conversion page-component-filter-record-to-rule-array rewrites the losslessly mappable record forms and single-level AST tuple arrays on every ObjectStack stored-row read and under os migrate meta --stored, and leaves combinator records, parts with no lossless rule spelling and inline-row components' filters as stored. The reason now says that, plus that this backend-agnostic renderer replays no conversion itself. The conclusion, the unknown type and its cast argument are unchanged. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .../8945-element-datasource-filter-note.md | 16 ++++++++++++--- .../src/data-scope/element-data-source.ts | 20 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.changeset/8945-element-datasource-filter-note.md b/.changeset/8945-element-datasource-filter-note.md index 2a3abfed22..4e521a54a9 100644 --- a/.changeset/8945-element-datasource-filter-note.md +++ b/.changeset/8945-element-datasource-filter-note.md @@ -15,9 +15,19 @@ populations the convergence split apart: `[{ 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 convergence does not - rewrite metadata at rest, so a stored page carrying the record form or a tuple - array keeps arriving, and `mergeFilterNodes` still lowers each of them. +- **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 diff --git a/packages/core/src/data-scope/element-data-source.ts b/packages/core/src/data-scope/element-data-source.ts index 39d441ce6a..1d9f9b6c2b 100644 --- a/packages/core/src/data-scope/element-data-source.ts +++ b/packages/core/src/data-scope/element-data-source.ts @@ -85,10 +85,22 @@ export interface ElementDataSourceSort { * 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 convergence - * deliberately does not rewrite metadata at rest, so a stored page carrying - * the record form or an AST tuple array keeps arriving here, beside the rule - * array. + * - **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