Skip to content

Commit a35f7a6

Browse files
feat(spec)!: delete checkPageComponentViewData; the component-props rule is the one judge of a page component's retired api data binding (#23784)
Fixes #23447 Clause-②: yes Ruling `6161173492` on #23388, item 6, verbatim: > 6. #23357's narrow judge proceeds as triaged and does not wait. Once this PR lands, that judge overlaps the general rule and is deleted (one operation, two implementations: the governed side wins), in #23357's PR if it is still open, else in a one-line follow-up. PR #23418 (`d0c09e47c1`) put `validateComponentProps` on the runtime save door for `page` writes. This PR deletes the page-parse judge that duplicated it, rewrites the sentences that became false, and re-pins the save door on the props rule. ## What changes 1. **The narrow judge is deleted** (`packages/spec/src/ui/page.zod.ts`): `checkPageComponentViewData`, `VIEW_DATA_SOURCE_COMPONENT_TYPES`, their docblocks, the `.superRefine` on `PageSchema`, and the now-unused `ViewDataSchema` import. The published export leaves `@objectstack/spec/ui`; `api-surface/ui.json` and `export-origins/ui.json` were regenerated with `check:generated --fix` (one line each). `page-component-view-data.test.ts` is deleted, since its subject is gone. Its reach and control cases move to the save-door pins below, which run the real rule. In `object-refinement-check-exports.test.ts` the judge's fixtures and rows are removed, and absence pins (0 declarations, 0 attachments, not in the barrel) follow the `checkListViewPageMount` precedent. 2. **Sentences rewritten:** - the `refused-shapes.ts` docblocks and `COMPONENT_API_BINDING_FATE`, with their tests; - `18.view-data-api-provider-retired.ts` `acceptanceCriteria`; - `18.ui-page-action-row-declarative-update-rules.ts` `reason`; - the `validate-component-props.ts` header ("does not run this rule" at the runtime publish door); - the `view.zod.ts` comment; - the `PageSchema` trailing comment ("five checks" is now four). 3. **The stored-page fate was measured before it was rewritten** (table below). The new fate text says what was measured: the page loads and is served with no read diagnostic, and its next save is refused by the props rule. 4. **Pins** (`packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts`, new describe after the #23388 block). They run the real `saveMetaItem` / `loadMetaFromDb` / `getMetaItem` path: - each of `object-grid`, `object-map`, `object-gantt` and `object-tree` → `422 INVALID_METADATA`, `rulesRun` contains `validateComponentProps`, one `component-props-invalid` issue carrying the prescription at `pages[0].regions[0].components[0].properties.data.provider`, nothing stored; - reach: a `page:tabs` item, a component nested in `flex` › `page:card`, and a slot; - control: `provider: 'object'` and `provider: 'value'` save, one row each; - stored page: `loadMetaFromDb` → `loaded: 1, errors: 0, invalid: 0`, the page is registered, no `metadata_spec_invalid` warning, `getMetaItem` serves it as stored with `_diagnostics.valid: true`, its next save is refused by the props rule with the row unchanged, and the fixed page saves. - The old `#23357` block in `protocol.save-union-issues.test.ts` (a spec-parse `custom` issue, plus a read-path `_diagnostics.valid: false` pin, which is now false) is removed. ## Each door, before and after (measured) | door | before (`29c7157af4`) | after | | --- | --- | --- | | `PageSchema.safeParse` / `definePage` / `defineStack` | refused: `custom` at `regions.0.components.0.properties.data.provider` (and at each nested and slot position) | **accepted**: these doors run no lint rule. This is the ruling's expected consequence. No equivalent check was added to spec. | | `os validate` (built CLI, `defineStack` config) | exit 1: `defineStack validation failed` with the prescription | exit 1: `component-props-invalid at pages[0].regions[0].components[0].properties.data.provider` with the prescription; the control config (`provider: 'object'`) passes, exit 0 | | `os build` | exit 2: refused inside `defineStack` | exit 1: the same `component-props-invalid` finding; control exit 0 | | save door `saveMetaItem` | 422 `INVALID_METADATA`, `failed spec validation`, issue `code: custom`, no `rulesRun` | 422 `INVALID_METADATA`, `failed author-time validation`, rule `component-props-invalid`, `rulesRun` contains `validateComponentProps`, nothing stored | | boot `loadMetaFromDb` | `loaded: 1, errors: 0, invalid: 1`, `[metadata_spec_invalid]` warning | `loaded: 1, errors: 0, invalid: 0`, registered, no warning | | read `getMetaItem` | served, `_diagnostics.valid: false` with the refusal at the component | served as stored, `_diagnostics.valid: true` | Rule-level reach, the same before and after (the probe ran `validateComponentProps` on 10 page bodies, with identical output on both trees): one finding per binding on all four types, the bare `{ provider }`, a `page:tabs` item, `flex` › `page:card` › component, `slots.details` and `slots.header[0]`. No finding on the `object` and `value` controls or on a custom type. An `object-kanban` carrying the binding draws `component-props-invalid` at `properties.data` (`expected array`), which is its own row's verdict, as before. ## Merge with main `origin/main` `43567a8f` (PR #23708, PR #23800 included) was merged in at `4087b452`, through `scripts/pm/os-regen-merge.sh`: - `packages/spec/src/ui/page.zod.ts`: Conflict at the PageSchema superRefine chain. Kept main's checkPageDefaultPlacement and checkPageRegionPlacement (PR #23708, #23610 ruling A), dropped the deleted checkPageComponentViewData. The trailing comment reads 'the six checks above', and the props-bag sentence stays. - `packages/spec/src/ui/object-refinement-check-exports.test.ts`: 5 hunks: main's side in each, minus every checkPageComponentViewData line, i.e. the import, the view-data fixture block (main's #23610 fixtures kept), the MIRRORED row and the barrel row. The leg-3 'declares all six exports' loop names the 4 old checks plus the 2 new ones. This PR's 0-declaration, 0-attachment and barrel-absence pins are kept. - `packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json`: os-regen driver, not text-merged. Step 2 took main's side, the merge commit was the deferral, then rebuild + gen:api-surface + gen:export-origins on the merged tree, committed as 392224e. The deferral was discharged. Delta vs origin/main: exactly one removed line in each (checkPageComponentViewData). main's 9 new ui exports are present. - `packages/spec/src/migrations/entries/semantic/18.ui-page-action-row-declarative-update-rules.ts`: No conflict: PR #23800 left this line to this PR. Aligned with #23800's wording precedent: '... the load path runs no such gate, so a stored page still loads and its next save meets the component-props gate at the metadata save door.' ## DELIBERATE CORRECTION File: `.changeset/pre/23357-page-component-view-data-api-refused.md`, commit `7c25be0a`. `18.0.0-next.2` consumed the note, and the 18.0.0 GA entry is compiled from `.changeset/pre/` again (`release.yml`: "On `pre exit` the GA version consumes them all again"). This PR makes the following sentences false, and each is rewritten in place, measured against this PR's head: 1. Title: "the page parse refuses …" → "a page component's `data` prop that names the retired view-level `api` data source is refused at the Studio, REST and MCP save door as `os validate` refuses it". 2. BREAKING paragraph: "is now refused by `PageSchema`" and "the component-props gate … runs at the CLI doors only" → the props gate (`validateComponentProps`, rule `component-props-invalid`) refuses it at `os validate`, `os build`, `os lint` and the metadata save door. The CLI-only fact now reads as history: "before 18". 3. Judge paragraph: "raised by the page parse unchanged (… `code` is `custom` …)" → the props row parses `data` with `ViewDataSchema`, and the finding carries that schema's prescription. The row-array types are refused for their shape, not with this prescription. The `checkPageComponentViewData` … re-attach sentence is deleted. 4. FROM → TO header: "(accepted by the page parse until this change)" → "(saved by the Studio, REST and MCP door before 18)". 5. "`definePage()`, `defineStack({ pages })` … refuse the page" → `os validate` / `os build` / `os lint` and `PUT /api/v1/meta/page/:name` refuse it; `definePage()` and `defineStack({ pages })` run no authoring rule and accept it. 6. Stored page: "registered under `[metadata_spec_invalid]` with the refusal in the `_diagnostics` of every read" → served as stored, with no read diagnostic for the binding; the next save is refused with 422 by the props gate. objectui#12325 is now in the console pin, so the sentence changes from "until then a stored page keeps sending it" to "so a stored page does not send the request". 7. ADR-0087 marker reason: "makes the page parse refuse …" → "the component-props gate refuses what that entry retired at the CLI doors and the save door"; the disposition token itself is unchanged. Authority: the maintainer's 「同意」, recorded in `6210183493`, as applied by `6210987382` (which supersedes `6210952492`), with PR #23418's `6203578381` as the precedent. `packages/spec/CHANGELOG.md` is untouched. `Check Changeset` reds by design on this edit (`check-empty-changeset`: DELIBERATE CORRECTION class); it is not a required context and runs only on `pull_request`. ## Verification - **Final head `7c25be0a`** (rounds after the merge). `dispatch-gates --commands`: 90 derived, 89 run, 1 NOT-MEASURED (`check:dual-build-cjs-loads`, left to CI). 88 exit 0, plus `check-empty-changeset` exit 1, by design, for the DELIBERATE CORRECTION class. `check-adr-0087-registration` and `check-changeset-no-major` exit 0. On `39977e95`: spec 682 files / 20873 tests, lint 140 files / 6781, metadata-protocol pin files 170/170, typechecks exit 0. On `40624b32`: `refused-shapes.test.ts` + `object-refinement-check-exports.test.ts` 284/284, `check:generated` and `check:doc-authoring` exit 0. `7c25be0a` adds only the changeset text. - **Round 1** (before the merge): - Ablation of the general rule's runtime face: `validateComponentProps`'s entry was set to `surfaces: ['cli']` with `runtimeTypes` removed (through `scripts/ablation-replace.mjs`). `@objectstack/lint` was rebuilt, and `ablation-dist-preflight` found the marker in 4 built files. Result: the 6 refusal pins went red and the control stayed green. On restore, the blob matched the HEAD hash, the rebuild passed `--absent` with a clean tree, and the whole file is green at 145/145. - `@objectstack/metadata-protocol`: `pnpm test`, 236 files and 20580 tests passed; `pnpm typecheck` exit 0. - `@objectstack/spec`: full `pnpm test` showed one failure (a missed `#23357` half in `refused-shapes.test.ts`), which is fixed. Re-run: `refused-shapes.test.ts` and `object-refinement-check-exports.test.ts` 250/250, and `pnpm typecheck` exit 0. - `@objectstack/lint`: `pnpm test`, 139 files and 6771 tests passed; `pnpm typecheck` exit 0. - `check:generated`: 2 artifacts were stale (`api-surface`, `export-origins`) and were regenerated; the other 12 were up to date. - `check:adr-0087-registration`: `[BREAKING+bang+clause-②-narrowing] not-required (already-registered)`. - The derived gate families (`dispatch-gates --commands`, 90 commands) were run locally; their results are in the report comment on #23447. ## Acceptance notes - **The rest of the family is closed.** PR #23418 made the stale save-half sentences false. PR #23800 (#23789) rewrote them and landed before this PR merged `main`. The one line it left for this PR, in `18.ui-page-action-row-declarative-update-rules.ts`, now follows #23800's wording. - **The 23357 note** had been consumed by `18.0.0-next.2` into `.changeset/pre/`, and the 18.0.0 GA entry is compiled from there again. It is corrected in place: see `## DELIBERATE CORRECTION`. The published `packages/spec/CHANGELOG.md` entry is untouched. - **`@objectstack/lint: patch` was dropped** from this PR's changeset. The lint diff is the file-top comment of `validate-component-props.ts`, and no changeset gate asks for it. - **`COMPONENT_API_BINDING_FATE`** (`refused-shapes.ts`) now says the console no longer reads the retired arm. objectui#12325 has been in the console pin since PR #23709 (`53e3638e`). - **The `_diagnostics` field is not absent** after the change: it reads `valid: true`, and the pin asserts that. --- _Generated by [Claude Code](https://claude.ai/code/session_016QpnMQNQ3TQ8mvWiHJwN2j)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 34b740f commit a35f7a6

15 files changed

Lines changed: 239 additions & 474 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: `checkPageComponentViewData` is deleted; a page component's `data` prop naming the retired view-level `api` source is refused by the component-props rule alone, at the CLI doors and the metadata save door (#23447)
6+
7+
**BREAKING** — `checkPageComponentViewData` is no longer exported from `@objectstack/spec/ui`, and `PageSchema` no longer carries it. Since the component-props rule (`validateComponentProps`, `@objectstack/lint`) runs on a `page` write at the metadata save door, it and the page-parse check judged the same binding at the same door: one operation with two implementations. The governed side stays. The props rule parses the component's `properties` against the type's `ComponentPropsMap` row, and that row's `data` is `ViewDataSchema`, so it raises the retirement's own prescription on the same four types (`object-grid`, `object-map`, `object-gantt`, `object-tree`), at the same positions: regions, `page:tabs` items, nested containers and slots.
8+
9+
Clause-②: yes (narrowing)
10+
11+
What narrows: the published export leaves `@objectstack/spec/ui`. What `PageSchema` accepts widens by this one shape, a component `data` naming the retired provider, which the props rule then refuses at every door that runs it.
12+
13+
## FROM → TO
14+
15+
| you wrote | write instead |
16+
| --- | --- |
17+
| `import { checkPageComponentViewData } from '@objectstack/spec/ui'`, and `.superRefine(checkPageComponentViewData)` on a mirror built from `PageSchema.shape` | nothing on the mirror: a page component's `properties` is judged by `validateComponentProps` from `@objectstack/lint` (or `runAuthoringRules`), against the stack that carries the page |
18+
| a page component's `properties.data` with `provider: 'api'` | unchanged from the step-18 retirement: `provider: 'object'` with `object`, or `provider: 'value'` with `items`; an endpoint read moves onto the component's `dataSource: { provider: 'api', read: { url: '/api/...' } }` |
19+
20+
**The one-line fix:** drop the import and the re-attachment; run the props rule where you need the verdict.
21+
22+
**Which doors refuse the binding now, measured:**
23+
24+
| door | before | after |
25+
| --- | --- | --- |
26+
| `definePage()`, `defineStack({ pages })`, `PageSchema.safeParse` | refused, `custom` at `…properties.data.provider` | accepted (these doors run no lint rule) |
27+
| `os validate`, `os build` | refused inside `defineStack` | refused by the props rule: `component-props-invalid` at `pages[0].regions[0].components[0].properties.data.provider`, with the prescription |
28+
| metadata save door (`PUT /api/v1/meta/page/:name`, the Studio, MCP) | `422 INVALID_METADATA`, spec-parse issue `custom` | `422 INVALID_METADATA`, rule `component-props-invalid` at the same component, with the prescription; nothing stored |
29+
| boot (`loadMetaFromDb`) | `invalid: 1`, registered under `[metadata_spec_invalid]` | `invalid: 0`, registered, no warning |
30+
| read (`getMetaItem`) | served, `_diagnostics.valid: false` with the refusal | served as stored, `_diagnostics.valid: true` |
31+
32+
**A page already stored with such a component** still loads and is served as stored, and no longer carries a read diagnostic for the binding; its next save is refused with 422 by the props rule. `os migrate meta --stored` still lists it under the step-18 entry `view-data-api-provider-retired`, and that entry's fate text now says so.
33+
34+
<!-- adr-0087: not-required (already-registered view-data-api-provider-retired) the retirement this check enforced is that step-18 entry; this change deletes the second implementation of its page-component half, and the entry's acceptance criteria now name the component-props gate at the CLI doors and the save door as the one judge -->

‎.changeset/pre/23357-page-component-view-data-api-refused.md‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,25 @@
22
'@objectstack/spec': minor
33
---
44

5-
feat(spec)!: the page parse refuses a page component's `data` prop that names the retired view-level `api` data source, so the Studio, REST and MCP save door refuses it as `os validate` does (#23357)
5+
feat(spec)!: a page component's `data` prop that names the retired view-level `api` data source is refused at the Studio, REST and MCP save door as `os validate` refuses it (#23357)
66

7-
**BREAKING** — a page whose `object-grid`, `object-map`, `object-gantt` or `object-tree` component carries `properties.data` with `provider: 'api'` is now refused by `PageSchema`, at the component's `properties.data.provider`, with the retirement's own prescription. Those four components take a view data source (`ViewDataSchema`) as `data`, so the step-18 retirement of the view-level `api` source reached them through the same schema; but the page parse leaves a component's `properties` an open record, and the component-props gate that refused the binding runs at the CLI doors only. A page saved through the Studio, REST or MCP therefore kept the binding, with its request headers, and the console kept sending it.
7+
**BREAKING** — a page whose `object-grid`, `object-map`, `object-gantt` or `object-tree` component carries `properties.data` with `provider: 'api'` is refused at the component's `properties.data.provider`, with the retirement's own prescription, by the component-props gate (`validateComponentProps`, `@objectstack/lint`, rule `component-props-invalid`) at `os validate`, `os build`, `os lint` and the metadata save door. Those four components take a view data source (`ViewDataSchema`) as `data`, so the step-18 retirement of the view-level `api` source reached them through the same schema. The page parse leaves a component's `properties` an open record, and before 18 the component-props gate ran at the CLI doors only, so a page saved through the Studio, REST or MCP kept the binding, with its request headers, and the console kept sending it.
88

99
Clause-②: yes (narrowing)
1010

11-
The judge is the retirement's own: `ViewDataSchema` parses the component's `data`, and the prescription it raises is raised by the page parse unchanged (one text for a view and a page; the page's issue `code` is `custom`, as every object-level check on `PageSchema` raises). It reaches every component under `regions` and `slots`, at any depth, a component nested in a `page:tabs` item or a container included. It judges nothing else in the bag: another `data` provider, a component whose `data` is a row array (`object-kanban`, `object-calendar`, `object-timeline`), a custom or registered type with no props row, and a component's own `dataSource` binding are untouched. `checkPageComponentViewData` is exported from `@objectstack/spec/ui`, like the four other `PageSchema` checks, so a mirror built from `PageSchema.shape` can re-attach it.
11+
The judge is the retirement's own: the component's `ComponentPropsMap` row parses its `data` with `ViewDataSchema`, and the finding carries that schema's prescription unchanged, one text for a view and a page. It reaches every component under `regions` and `slots`, at any depth, a component nested in a `page:tabs` item or a container included. Only those four types meet this prescription: a component whose `data` is a row array (`object-kanban`, `object-calendar`, `object-timeline`) is judged by its own row and refused for its shape, a custom or registered type with no props row is not judged, and a component's own `dataSource` binding is untouched.
1212

1313
## FROM → TO
1414

15-
| you wrote (accepted by the page parse until this change) | write instead |
15+
| you wrote (saved by the Studio, REST and MCP door before 18) | write instead |
1616
| --- | --- |
1717
| `properties.data` with `provider: 'api'` (and its `read` / `write` request blocks) on an `object-grid`, `object-map`, `object-gantt` or `object-tree` page component | `properties.data` with `provider: 'object'` and `object: '<object_name>'`, or `provider: 'value'` with `items: [...]`; an endpoint the page must read is a page component's `dataSource: { provider: 'api', read: { url: '/api/...' }, path? }`, same-origin and with no credential key |
1818
| `read.headers: { Authorization: '...' }` inside that binding | nothing: a credential never lives in metadata, and the console reads an element binding with its own signed-in fetch |
1919

2020
**The one-line fix:** bind each such component to its object or to static rows, and move any endpoint read onto a page component's `dataSource`.
2121

22-
**What an author who still writes it sees.** `definePage()`, `defineStack({ pages })`, `os validate` and `PUT /api/v1/meta/page/:name` refuse the page at `regions[i].components[j].properties.data.provider` (or the nested or slot position) with the prescription. At the save door that is `422 INVALID_METADATA`, nothing persisted.
22+
**What an author who still writes it sees.** `os validate`, `os build`, `os lint` and `PUT /api/v1/meta/page/:name` refuse the page at `pages[i].regions[j].components[k].properties.data.provider` (or the nested or slot position), rule `component-props-invalid`, with the prescription. At the save door that is `422 INVALID_METADATA`, nothing persisted. `definePage()` and `defineStack({ pages })` run no authoring rule and accept the page.
2323

24-
**A page already stored with such a component.** `os migrate meta --stored` lists it by page and component (the step-18 entry `view-data-api-provider-retired`), and nothing rewrites it: deleting `data` would make the component query its object instead. Left as stored, the page still loads and is served, registered under `[metadata_spec_invalid]` with the refusal in the `_diagnostics` of every read, and its next save is refused with 422. The console stops sending the request when its own reader of the retired arm is deleted (objectstack-ai/objectui#12325); until then a stored page keeps sending it.
24+
**A page already stored with such a component.** `os migrate meta --stored` lists it by page and component (the step-18 entry `view-data-api-provider-retired`), and nothing rewrites it: deleting `data` would make the component query its object instead. Left as stored, the page still loads and is served as stored, with no read diagnostic for the binding, and its next save is refused with 422 by the component-props gate. The console no longer reads the retired arm (objectstack-ai/objectui#12325), so a stored page does not send the request.
2525

26-
<!-- adr-0087: not-required (already-registered view-data-api-provider-retired) the step-18 entry already names the data prop of these four page components in its scope; this change makes the page parse refuse what that entry retired, and its acceptance criteria now name the page parse beside the component-props gate -->
26+
<!-- adr-0087: not-required (already-registered view-data-api-provider-retired) the step-18 entry already names the data prop of these four page components in its scope; the component-props gate refuses what that entry retired at the CLI doors and the save door, and its acceptance criteria name that gate -->

‎packages/lint/src/validate-component-props.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,14 @@
109109
* object arm tombstones `publicPicker`.
110110
*
111111
* Where it refuses: the three CLI doors — `os validate`, `os build`, `os lint`
112-
* — run every gating rule. The runtime publish door (the Studio, REST and MCP
113-
* write path) does not run this rule (`surfaces` in `authoring-rules.ts`), so a
114-
* page saved there is not refused for any of these findings. That is a
115-
* rollout decision, not a missing input: a `page` write's per-write snapshot
116-
* carries the written page, which is everything this rule reads.
112+
* — and the runtime publish door (the Studio, REST and MCP write path), on a
113+
* `page` write (`surfaces` / `runtimeTypes` in `authoring-rules.ts`, the ruling
114+
* of record 6161173492, letter B). A `page` write's per-write snapshot carries
115+
* the written page, which is everything this rule reads. The boot and read
116+
* paths run no authoring rule, so a stored page with a finding still loads and
117+
* is served; its next save is refused. This rule is the one judge of a
118+
* component's `properties`: the page parse leaves the bag an open record and
119+
* carries no second judge of any prop, a retired `data` provider included.
117120
*
118121
* ## Unregistered types are SKIPPED — a required semantic, not leniency
119122
*

‎packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts‎

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3197,3 +3197,136 @@ describe('runtime authoring gate on PAGE writes — the component props and type
31973197
expect(JSON.parse(activePageRows(rows)[0]!.metadata)).toEqual(cleanPage());
31983198
});
31993199
});
3200+
3201+
// A page component's `data` prop naming the retired view-level `api` provider,
3202+
// at the runtime save door. `object-grid`, `object-map`, `object-gantt` and
3203+
// `object-tree` take a view data source (`ViewDataSchema`) as `data`, so the
3204+
// step-18 retirement of that provider reaches them through their
3205+
// `ComponentPropsMap` rows. The page parse leaves `properties` an open record;
3206+
// the rule that judges the bag is `validateComponentProps`, which runs on a
3207+
// `page` write by the ruling of record above (6161173492, letter B). One
3208+
// operation, one implementation: the refusal is that rule's
3209+
// `component-props-invalid`, carrying the retirement's own prescription.
3210+
//
3211+
// Stored pages: the boot and read paths run no authoring rule, so a stored
3212+
// page with the binding loads, is registered and is served with no read
3213+
// diagnostic for it; its next save is refused until the binding is fixed.
3214+
describe('runtime authoring gate on PAGE writes — a component `data` naming the retired view-level api source', () => {
3215+
// Spelled through a constant: the spec's tree-scoped absence pin reads
3216+
// authored literals, and these bodies are refusals, not authorings.
3217+
const RETIRED_PROVIDER = 'api' as const;
3218+
const RETIRED_BINDING = {
3219+
provider: RETIRED_PROVIDER,
3220+
read: { url: 'https://partner.example.com/cases', headers: { Authorization: 'Bearer s3cr3t' } },
3221+
};
3222+
const PRESCRIPTION = "`view.data.provider: 'api'` was removed in @objectstack/spec 18 (ADR-0049";
3223+
const block = (type: string, data: unknown, id = 'cases') => ({ id, type, properties: { objectName: 'crm_case', data } });
3224+
const page = (components: unknown[], extra: Record<string, unknown> = {}) => ({
3225+
name: 'partner_page',
3226+
label: 'Partner',
3227+
type: 'app',
3228+
regions: [{ name: 'main', components }],
3229+
...extra,
3230+
});
3231+
const savePage = (protocol: any, item: unknown) =>
3232+
protocol.saveMetaItem({ type: 'page', name: 'partner_page', item });
3233+
const activePageRows = (rows: Map<string, Row>) =>
3234+
Array.from(rows.values()).filter((r) => r.type === 'page' && r.state === 'active');
3235+
const retirementIssues = (err: any) =>
3236+
(err?.issues ?? []).filter((i: any) => i.rule === 'component-props-invalid' && String(i.message).includes(PRESCRIPTION));
3237+
3238+
let warn: ReturnType<typeof vi.spyOn>;
3239+
beforeEach(() => {
3240+
warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
3241+
delete process.env.OS_ALLOW_UNLINTED_METADATA_WRITES;
3242+
});
3243+
afterEach(() => {
3244+
warn.mockRestore();
3245+
delete process.env.OS_ALLOW_UNLINTED_METADATA_WRITES;
3246+
});
3247+
3248+
it.each(['object-grid', 'object-map', 'object-gantt', 'object-tree'])(
3249+
'%s: 422 INVALID_METADATA from the props rule at the component\'s `data.provider`, with the prescription; nothing stored',
3250+
async (type) => {
3251+
const { protocol, rows } = makeProtocol();
3252+
3253+
const err = await savePage(protocol, page([block(type, RETIRED_BINDING)])).catch((e: any) => e);
3254+
3255+
expect(err).toBeInstanceOf(Error);
3256+
expect(err.status).toBe(422);
3257+
expect(err.code).toBe('INVALID_METADATA');
3258+
expect(err.rulesRun).toContain('validateComponentProps');
3259+
expect(retirementIssues(err).map((i: any) => i.path)).toEqual([
3260+
'pages[0].regions[0].components[0].properties.data.provider',
3261+
]);
3262+
expect(activePageRows(rows)).toEqual([]);
3263+
},
3264+
);
3265+
3266+
it('reaches a component in a `page:tabs` item, one nested in containers, and one in a slot', async () => {
3267+
const { protocol, rows } = makeProtocol();
3268+
const body = page([
3269+
{ id: 'tabs', type: 'page:tabs', properties: { items: [{ label: 'Cases', children: [block('object-grid', RETIRED_BINDING, 'in_tab')] }] } },
3270+
{ id: 'row', type: 'flex', properties: { children: [{ id: 'card', type: 'page:card', properties: { children: [block('object-tree', RETIRED_BINDING, 'deep')] } }] } },
3271+
], { kind: 'slotted', type: 'record', object: 'crm_case', slots: { details: block('object-map', RETIRED_BINDING, 'slotted') } });
3272+
3273+
const err = await savePage(protocol, body).catch((e: any) => e);
3274+
3275+
expect(err?.status).toBe(422);
3276+
expect(err.code).toBe('INVALID_METADATA');
3277+
expect(retirementIssues(err).map((i: any) => i.path).sort()).toEqual([
3278+
'pages[0].regions[0].components[0].properties.items[0].children[0].properties.data.provider',
3279+
'pages[0].regions[0].components[1].properties.children[0].properties.children[0].properties.data.provider',
3280+
'pages[0].slots.details.properties.data.provider',
3281+
]);
3282+
expect(activePageRows(rows)).toEqual([]);
3283+
});
3284+
3285+
it('the control: the same page with the `object` or `value` provider saves, one row', async () => {
3286+
for (const data of [{ provider: 'object', object: 'crm_case' }, { provider: 'value', items: [{ name: 'a' }] }]) {
3287+
const { protocol, rows } = makeProtocol();
3288+
await expect(savePage(protocol, page([block('object-grid', data)]))).resolves.toMatchObject({ success: true });
3289+
expect(activePageRows(rows), JSON.stringify(data)).toHaveLength(1);
3290+
}
3291+
});
3292+
3293+
it('a stored page with the binding loads, registers and is served with no read diagnostic; its next save is refused until fixed', async () => {
3294+
const { engine, rows } = makeStubEngine();
3295+
const registered: Array<{ type: string; name: unknown }> = [];
3296+
engine.registry.registerItem = (type: string, item: { name?: unknown }) => {
3297+
registered.push({ type, name: item?.name });
3298+
};
3299+
const protocol = new ObjectStackProtocolImplementation(engine, () => new Map(), 'env_test') as any;
3300+
const stored = page([block('object-grid', RETIRED_BINDING)]);
3301+
rows.set(keyOf({ type: 'page', name: 'partner_page', organization_id: null, state: 'active' }), {
3302+
id: 'r_seed', type: 'page', name: 'partner_page', organization_id: null, state: 'active',
3303+
metadata: JSON.stringify(stored),
3304+
});
3305+
3306+
// Boot: no authoring rule runs, and the page parse no longer judges the bag.
3307+
const result = await protocol.loadMetaFromDb();
3308+
expect(result).toMatchObject({ loaded: 1, errors: 0, invalid: 0 });
3309+
expect(registered).toContainEqual({ type: 'page', name: 'partner_page' });
3310+
// Read: served as stored, with no diagnostic for the binding.
3311+
const served: any = (await protocol.getMetaItem({ type: 'page', name: 'partner_page' })).item;
3312+
expect(served?.regions?.[0]?.components?.[0]?.properties?.data?.provider).toBe(RETIRED_PROVIDER);
3313+
expect(served?._diagnostics?.valid).toBe(true);
3314+
expect(served?._diagnostics?.errors ?? []).toEqual([]);
3315+
// No `[metadata_spec_invalid]` registration either: that came from the page parse.
3316+
expect((warn.mock.calls as unknown[][]).map((c) => String(c[0])).filter((m) => m.includes('metadata_spec_invalid'))).toEqual([]);
3317+
3318+
// The next save of the same body is a write, and is judged.
3319+
const err = await savePage(protocol, stored).catch((e: any) => e);
3320+
expect(err?.status).toBe(422);
3321+
expect(err.code).toBe('INVALID_METADATA');
3322+
expect(retirementIssues(err).map((i: any) => i.path)).toEqual([
3323+
'pages[0].regions[0].components[0].properties.data.provider',
3324+
]);
3325+
expect(JSON.parse(activePageRows(rows)[0]!.metadata)).toEqual(stored);
3326+
3327+
// Fixed, then saved.
3328+
const fixed = page([block('object-grid', { provider: 'object', object: 'crm_case' })]);
3329+
await expect(savePage(protocol, fixed)).resolves.toMatchObject({ success: true });
3330+
expect(JSON.parse(activePageRows(rows)[0]!.metadata)).toEqual(fixed);
3331+
});
3332+
});

0 commit comments

Comments
 (0)