Skip to content

Commit 62ce45d

Browse files
feat(spec): a page element dataSource can name an API endpoint (provider: 'api') (#23079)
Fixes #23016 Clause-②: yes (widening: a new accepted shape) ## What A page element's `dataSource` gains a second shape beside the object binding, per the maintainer's ruling in the card body: the **API binding** `{ provider: 'api', read: { url, method? }, path? }`, exported as `ElementApiDataSourceSchema` (type `ElementApiDataSource`, pinned isomorphic). `PageComponentSchema.dataSource` is now `z.union([ElementDataSourceSchema, ElementApiDataSourceSchema])`. - **Same-origin only, refused at parse, every class named.** `read.url` must open with a single `/`. Refused at `dataSource.read.url` (`invalid_format`), with a message that names the class, the rule and the fix, and never echoes the value: `scheme` (an absolute URL of any scheme: `https:`, `http:`, `HTTPS:`, `javascript:`, `data:`, `mailto:`, `localhost:3000/…`), `protocol-relative` (`//host`), `backslash` (a slash then a backslash, or a leading backslash: the URL parser reads both as `//host`), `whitespace-or-control` (tab, LF, CR anywhere; a space at either end, which the parser strips, so a space before `//host` is `//host`; a mid-path space) and `relative` (`./home`, `../home`, `api/v1/x`, `?q`, `#f`, the empty string). The first four are the browser-equivalent spellings objectui's `classifyNavigationTarget` refuses for a navigation. Accepted: `/api/v1/cloud/home`, with a query string, and `/`. - **The rule is a `.regex()`, not a refinement**, so it projects into the published JSON Schema as `pattern`. A refinement would be dropped from `json-schema/` (`dropped-refinements.baseline.json`), and a validator would accept a URL the parse refuses. A pin holds the published `pattern` to the same verdict as the parse on all 23 refused values and 3 accepted ones. - **No credentials in metadata.** The read block declares `url` and `method` and nothing else (the view-level `HttpRequestSchema` carries `headers` / `params` / `body`; this one does not). `headers`, `credentials`, `auth`, `authorization`, `token` and `apiKey` are refused with a prescription that names the reuse of the console's signed-in fetch; `params` and `body` are refused with their own fix. - **`method` admits `GET` alone**, the only method the renderer sends for a read (measured below). - **`path`** is a dotted path into the `{ success, data }` envelope (`data.environments`): segments of letters, digits, `_`, `-`, joined by single dots; a JSONPath (`$.data`), brackets, slashes or empty segments are refused at `dataSource.path`. - The API arm is closed (`strictObject`); `object`, `write` and a top-level `url` / `method` on it are refused with guidance. ## The other halves of the acceptance list (not this repository) - **Acceptance 1, `objectui validate` accepts a page using the `api` shape:** objectui's half. objectui's block schemas import `ElementDataSourceSchema` for their object-bound `dataSource` (`packages/types/src/zod/objectql.zod.ts`). That is why the object arm keeps that name and meaning. The generic blocks that read the API binding, and their validation, are objectstack-ai/objectui#12197, re-scoped by the same ruling. - **Acceptance 3, a cloud page binds `GET /api/v1/cloud/home` with no cloud-specific React:** cloud's half, objectstack-ai/cloud#2777 part B. - **Acceptance 2, the docs:** here. The generated page reference (`content/docs/references/ui/page.mdx`) now lists `ElementApiDataSource` beside `ElementDataSource`, and so does `PageComponent.dataSource[option 2]`. The hand-written `content/docs/ui/pages.mdx` gains an "Element data sources" section with both shapes in one type-checked (`os:check`) example and the API binding's rules. ## Measured calls (the dispatch's mechanism assumptions) 1. **A plain `z.union`; no `provider: 'object'` on the object arm.** zod 4.6.5's `$ZodDiscriminatedUnion` constructor throws `Invalid discriminated union option` for an option whose shape lacks the discriminator key (read in `zod/v4/core/schemas.js`). So a discriminated union would have needed a `provider: 'object'` key on the object binding: a second spelling of the same binding, and a change to the def objectui imports. With the union, the object arm's accept set and published def are byte-identical (`authorable-surface/` and `json-schema.manifest/` diffs are additions only). **The cost, measured:** a refused object binding parsed through `PageComponentSchema` now arrives as one `invalid_union` at `dataSource`, whose object branch carries the old issue. `formatZodIssue` and `zodIssuesToFields` add one `dataSource: Invalid input` entry, then render the branch at the old path (`dataSource.filter`, `dataSource.object`). An API-binding refusal arrives directly at its path (`dataSource.read.url`). Two `dropped-refinements.baseline.json` sites moved path (`in.dataSource.filter.element` became `in.dataSource.options[0].filter.element`, on `ui/PageComponent` and `ui/PageRegion`); no new gap. 2. **`./relative` is refused.** Measured at objectui `7578323a544e`: the `apiBase` promotion of a `url` action (`ActionRunner.navigateTo`) prefixes only root-relative `/(api|_auth|_account)/…` paths. A `./x` read is a `fetch` resolved against the console route the page is mounted on, on the SPA host. `classifyNavigationTarget` admits `./` for a navigation, which is a different question. 3. **`method`: `GET` only.** Measured at objectui `7578323a544e`: `ApiDataSource.find`, `findOne` and `aggregate` each pass `method: 'GET'` explicitly, which overrides `read.method` (`options.method ?? base.method`), so no other value would ever be sent. 4. **No renderer reads the API binding yet.** Measured at objectui `7578323a544e`: `useElementDataSource` takes `schema.dataSource` only through `isElementDataSourceConfig` (a plain object with a non-empty string `object`), so an API binding reaches no block. `SchemaRenderer` strips `dataSource` from the props spread, so the binding cannot shadow an injected adapter either. The reader is objectstack-ai/objectui#12197. The schema's describe carries `[PLANNED — not yet rendered]`. 5. **Liveness: no ledger row is possible.** The page ledger's walk stops at `page/regions` and `page/slots`, recorded undrilled containers. No component-level key has a row, and `childShape()` does not descend a `z.union`. The planned record therefore went into `liveness/page.json`'s `_note`, with carrier objectstack-ai/objectui#12197, and `check:liveness` is green. The checkable records are the `ui/ElementApiDataSource:*` authorable-surface rows and the describe marker. ## Readers of the schema: each one updated or unaffected - `PageComponentSchema.dataSource`: **updated** (the union). - Types: `ElementDataSource` / `ElementDataSourceParsed` are **unaffected** (object arm unchanged). `ElementApiDataSource` is new. `PageComponent['dataSource']` is now a union, so a typed read of `.object` must narrow. The one in-tree typed reader (`page.test.ts`) was updated. Reverse check against the rebuilt `dist/ui/index.d.mts`: `read: { method: 'POST' }` fails with `TS2322: Type '"POST"' is not assignable to type '"GET"'`, the valid object and API components compile, and removing the bad line returns exit 0. - `visibleWhen`'s describe (the `data` binding): **unaffected**. It already records `data` as renderer behaviour, not contract. The API arm's own describe states what an API-bound element reads as `data`, marked planned. - `@objectstack/lint`: `page-walk.ts`, `validate-component-props.ts` (`DATA_SOURCE_BOUND_ELEMENT_TYPES` owe `dataSource.object`), `validate-action-name-refs.ts`, `validate-preset-comparands.ts` and `validate-page-field-bindings.ts` are **unaffected**. All read untyped (`isRec` / `strName`). An API binding has no `object`, so each falls back exactly as for no binding. For `element:record_picker` / `element:number` / `element:repeater` the existing warning stays correct, because those elements read only the object binding. - `@objectstack/cli` `i18n-extract.ts`: **unaffected** (through lint's page walk, same fallback). - `packages/spec/scripts/check-yaml-examples.ts`: **unaffected** (the same rule as `validate-component-props`). - Conversions `element-flat-data-binding-to-data-source` and `page-component-filter-record-to-rule-array`: **unaffected**. The first moves only tombstoned flat keys, which the parse refuses (and a v17 row cannot carry an API binding). The second rewrites `dataSource.filter`, which the API arm has none of. - `@objectstack/metadata-protocol`: **unaffected**. No `src` reader; its tests assert conversion TODO paths, not zod paths. - `@objectstack/sdui-parser`: **unaffected**, with zero `dataSource` references. - `@objectstack/objectql` (its test reads the stored item untyped) and `examples/app-showcase` (authors object bindings; a test reads `as any`): **unaffected**, tests run below. ## Verification Readings at head `b6c1d0790` (this branch with `origin/main` `466380132` merged in, then the references index regenerated on the merged tree) unless marked otherwise. Commands under `scripts/pm/os-verify-lock.sh`, exit codes captured before any pipe. - **Gate families.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 115 families, the same list at the pre-merge head and at `b6c1d0790`. All 115 were run at `b6c1d0790`: 114 exit 0 and 1 exit 1. `--ran` reconciliation: `115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN`. - The red one is `pnpm check:platform-checklist`: 8 `ABSENT SYMBOL` / floor problems in `docs/qa/platform-checklist/areas/{access-security,attachments-storage,search}.json`, which cite symbols in `metadata-protocol/src/protocol.ts` and `service-storage`. This diff touches none of its inputs. - Control leg: the same command on a clean checkout at the base `df7a137923` exits 1 with the same 8 problems. - `checklist-status.yml` keeps this gate out of per-PR CI. - **Spec build and generated artifacts.** `pnpm --filter @objectstack/spec build` exits 0. `pnpm --filter @objectstack/spec check:generated` exits 0, with all 14 artifacts current, `check:test-typecheck` among them. - **Spec tests at `b6c1d0790`.** 7 files, 315 passed in the `local` project: `page-element-api-data-source`, `page`, `type-alias-convention.pin`, `alias-integrity`, `union-author-message-pins`, `page-component-filter-record-to-rule-array` and `filter-rule-array-guidance`. `element-flat-binding-retirement` passed 56 in the `repo` project. - **Before the merge, at `22930dae3` (the same diff, older base):** - `pnpm --filter @objectstack/spec test`: 655 files, 19699 passed. - `@objectstack/lint` test: 137 files, 6569 passed. - `@objectstack/metadata-protocol` test: 227 files passed, 3 skipped; 28190 tests passed. - `@objectstack/cli` `vitest run --project unit`: 282 of 283 files passed. The 2 failures were 5 s timeouts in `test/hook-timeout-override-refusal.test.ts`, which spawns vitest, while the gate battery shared the cores. Run alone afterwards: 4/4 passed. - `@objectstack/example-showcase` test: 33 files, 409 passed. - `@objectstack/objectql` `src/registry-cross-package-item-classes.test.ts`: 14 passed. - The consumer suites were not re-run after the merge. The incoming commits touch none of the reader files listed above and not `page.zod.ts`; their one `dataSource` mention is a code comment. - **Reverse type check against the rebuilt declarations.** A scratch program compiled against `packages/spec/dist/ui/index.d.mts`. The bad line, `read: { method: 'POST' }`, gives exit 2 with `TS2322: Type '"POST"' is not assignable to type '"GET"'`. Removing it gives exit 0, with an object-bound and an API-bound `PageComponent` still compiling. - **Control bytes.** `grep -naP` for raw control bytes over every changed file finds none: the regexes spell `\x00`-`\x20` and `\x7F` as escapes. `pnpm check:nul-bytes` exits 0. ## Acceptance notes Observations recorded here and not filed (no public-door measurement, or zero producers): - **`@objectstack/lint` `page-walk.ts` inherits the page's `object` for an API-bound node.** An API binding has no `object`, so the walk falls through to `properties.object` and then the page object. Every reader of `objectName` (field bindings, preset comparands, i18n section keys) would then judge an API-bound block's field-shaped props against the page's object. No API-bound block exists today. The first field-bearing generic block (objectstack-ai/objectui#12197) is where this needs an answer. Carrier: none in this repository yet. - **objectui's `url`-action `apiBase` promotion hard-codes `/(api|_auth|_account)/`, while the REST `basePath` is configurable (default `/api`).** A renderer for this binding that reuses the rule would not promote a deployment with another base path. Carrier: objectstack-ai/objectui#12197. - **The view-level `ViewDataSchema` `api` arm (`HttpRequestSchema`) still accepts a cross-origin `url` and a `headers` map.** Its `method` admits `POST`/`PUT`/`PATCH`/`DELETE`, which `ApiDataSource`'s reads never send. The ruling scoped same-origin and no-credentials to the page-element binding only. This is a read-only inference: the producers measured in this repository (`content/docs/ui/views.mdx`, `skills/objectstack-ui/rules/list-views.md`) write `GET` and a same-origin path. Carrier: none. - **When objectstack-ai/objectui#12197 lands,** the `[PLANNED — not yet rendered]` marker leaves `ElementApiDataSourceSchema`'s describe, the `liveness/page.json` `_note` sentence is rewritten, and the "Not rendered yet" bullet in `content/docs/ui/pages.mdx` goes. - `skills/objectstack-ui/references/_index.md` changed as a pure `gen:skill-refs` regeneration. `check-governed-merges.mjs --test` over the final file list answers NOT governed after the generated-artifact lift. --- _Generated by [Claude Code](https://claude.ai/code/session_01GZSPSQ5ArPjSJdxxuqwGoZ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7f2f081 commit 62ce45d

17 files changed

Lines changed: 634 additions & 31 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): a page element's `dataSource` can name an API endpoint — `{ provider: 'api', read: { url }, path? }`
6+
7+
Clause-②: yes (widening: a new accepted shape)
8+
9+
- **What is new.** `PageComponentSchema.dataSource` takes a second shape beside the object binding: the API binding, exported as `ElementApiDataSourceSchema` (type `ElementApiDataSource`). It reads one endpoint on the deployment's own origin and binds its answer, so a product page composes from a summary that is not an object — `{ provider: 'api', read: { url: '/api/v1/cloud/home' }, path: 'data.environments' }`. `path` is an optional dotted path into the `{ success, data }` response; omitted, the element binds the whole response.
10+
- **Same-origin only, refused at parse.** `read.url` must open with a single `/`. Five classes are refused with the reason and the fix, at `dataSource.read.url`: an absolute URL of any scheme (`https:`, `http:`, `mailto:`, `javascript:`, `data:`, …), a protocol-relative `//host`, a backslash the browser reads as a slash (`/` then a backslash, or a leading backslash), a space or ASCII control character (a tab, line feed or carriage return anywhere is deleted by the URL parser, so `/`, a tab, `/host` is `//host`), and a relative path such as `./home` or `api/v1/x`, which resolves against the page, never the API. The rule is a `pattern` in the published JSON Schema too, so a validator reading `json-schema/` refuses what the parse refuses.
11+
- **No credentials in metadata.** The console reads the endpoint with its own signed-in fetch. The read block declares `url` and `method` only; `headers`, `credentials`, `auth`, `authorization`, `token` and `apiKey` are refused with that prescription, and so are `params` and `body`.
12+
- **Reads only.** `read.method` is optional and accepts `GET` alone, the method the console's API adapter sends for every read. There is no `write` block.
13+
- **The object binding is unchanged.** `ElementDataSourceSchema` accepts exactly what it did and gains no `provider` key; the two shapes meet as a `z.union` at `PageComponentSchema.dataSource`. One visible difference: a refused object binding parsed through `PageComponentSchema` now arrives as one `invalid_union` issue at `dataSource`, whose object branch carries the same issue as before. `os validate` and the API error envelope print one `Invalid input` line for the union at `dataSource`, then the object branch's issue at the same path as before (for example `dataSource.filter`).
14+
- **Not rendered yet.** No console block reads an API binding today: the generic stat, usage-bar and checklist blocks that will are a console release still to come. Until then the binding parses and no block reads it; its schema description says so (`[PLANNED — not yet rendered]`).

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1523 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1524 schemas across 14 protocol modules
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -33,8 +33,8 @@ counts are sums of the rows they head. Regenerate with
3333
| [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
3434
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3535
| [System Protocol](/docs/references/system) | 35 | 277 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
36-
| [UI Protocol](/docs/references/ui) | 16 | 166 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37-
| **Total** | **196** | **1523** | 14 protocol modules |
36+
| [UI Protocol](/docs/references/ui) | 16 | 167 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37+
| **Total** | **196** | **1524** | 14 protocol modules |
3838

3939
---
4040

@@ -363,7 +363,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
363363

364364
## UI Protocol
365365

366-
**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 166 schemas**
366+
**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 167 schemas**
367367

368368
Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer.
369369

@@ -380,7 +380,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay
380380
| [`expression-bindable-text-keys.zod.ts`](/docs/references/ui/expression-bindable-text-keys) | `ExpressionBindableTextKey` |
381381
| [`i18n.zod.ts`](/docs/references/ui/i18n) | `AriaProps`, `I18nLabel`, `InlineLocaleMap` |
382382
| [`notification.zod.ts`](/docs/references/ui/notification) | `NotificationPosition`, `NotificationSeverity`, `NotificationType` |
383-
| [`page.zod.ts`](/docs/references/ui/page) | `ElementDataSource`, `InterfacePageConfig`, `Page`, `PageComponent`, `PageComponentType`, `PagePrint`, `PageRegion`, `PageType`, `PageVariable` |
383+
| [`page.zod.ts`](/docs/references/ui/page) | `ElementApiDataSource`, `ElementDataSource`, `InterfacePageConfig`, `Page`, `PageComponent`, `PageComponentType`, `PagePrint`, `PageRegion`, `PageType`, `PageVariable` |
384384
| [`report.zod.ts`](/docs/references/ui/report) | `JoinedReportBlock`, `Report`, `ReportChart`, `ReportSort`, `ReportType` |
385385
| [`responsive.zod.ts`](/docs/references/ui/responsive) | `ResponsiveStyles`, `StyleMap` |
386386
| [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` |

‎content/docs/references/ui/page.mdx‎

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Page schema — UI Protocol property reference
33
navTitle: Page
4-
description: "Page schemas of the ObjectStack UI Protocol: ElementDataSource, InterfacePageConfig and 7 more — each property with its type, default and a TypeScript example."
4+
description: "Page schemas of the ObjectStack UI Protocol: ElementApiDataSource, ElementDataSource and 8 more — each property with its type, default and a TypeScript example."
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -13,13 +13,35 @@ description: "Page schemas of the ObjectStack UI Protocol: ElementDataSource, In
1313
## TypeScript Usage
1414

1515
```typescript
16-
import { ElementDataSourceSchema, InterfacePageConfigSchema, PageSchema, PageComponentSchema, PageComponentType, PagePrintSchema, PageRegionSchema, PageTypeSchema, PageVariableSchema } from '@objectstack/spec/ui';
17-
import type { ElementDataSource, InterfacePageConfig, Page, PageComponent, PageComponentType, PagePrint, PageRegion, PageType, PageVariable } from '@objectstack/spec/ui';
16+
import { ElementApiDataSourceSchema, ElementDataSourceSchema, InterfacePageConfigSchema, PageSchema, PageComponentSchema, PageComponentType, PagePrintSchema, PageRegionSchema, PageTypeSchema, PageVariableSchema } from '@objectstack/spec/ui';
17+
import type { ElementApiDataSource, ElementDataSource, InterfacePageConfig, Page, PageComponent, PageComponentType, PagePrint, PageRegion, PageType, PageVariable } from '@objectstack/spec/ui';
1818

1919
// Validate data
20-
const result = ElementDataSourceSchema.parse(data);
20+
const result = ElementApiDataSourceSchema.parse(data);
2121
```
2222

23+
---
24+
25+
## ElementApiDataSource
26+
27+
[PLANNED — not yet rendered] API binding of a page element: `{ provider: 'api', read: { url }, path? }` reads one same-origin endpoint with the console's signed-in fetch and binds the selected value, which the element reads as `data` in its expression holes and its `visibleWhen`. The console's generic stat, usage-bar and checklist blocks are the planned readers; until they ship, the binding parses and no block reads it.
28+
29+
### Properties
30+
31+
| Property | Type | Required | Description |
32+
| :--- | :--- | :--- | :--- |
33+
| **provider** | `'api'` | ✅ | Binds the element to one same-origin API endpoint instead of an object |
34+
| **read** | `{ url: string; method?: 'GET' }` | ✅ | The read request — the same-origin endpoint the element binds |
35+
| **path** | `string` | optional | Dotted path selecting the sub-tree of the response the element binds, e.g. `data.environments` in a `{ success, data }` envelope; omitted, the element binds the whole response |
36+
37+
### Nested Shape: `ElementApiDataSource.read`
38+
39+
| Property | Type | Required | Description |
40+
| :--- | :--- | :--- | :--- |
41+
| **url** | `string` | ✅ | Same-origin API path the element reads — it opens with a single `/`, e.g. `/api/v1/cloud/home`. An absolute URL of any scheme, a protocol-relative `//host`, a backslash read as a slash, a space or control character, and a relative path are refused at parse. No credential is written here: the console reads the endpoint with its own signed-in fetch. |
42+
| **method** | `'GET'` | optional | HTTP method of the read — `GET`, the only method a page element's read is sent with |
43+
44+
2345
---
2446

2547
## ElementDataSource
@@ -270,7 +292,7 @@ View filter rule
270292
| **responsiveStyles** | `{ large?: Record<string, string \| number>; medium?: Record<string, string \| number>; small?: Record<string, string \| number>; xsmall?: Record<string, string \| number> }` | optional | Per-breakpoint scoped style maps (ADR-0065) |
271293
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features`, `os.user` and binds `data` to the data-source ADAPTER here — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). ⚠️ `data` is surface-dependent: on a `page:tabs` item `visibleWhen` it is the record ROW instead. e.g. "page.selectedProjectId != ''" |
272294
| **visibility** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. |
273-
| **dataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … }` | optional | Per-element data binding for multi-object pages |
295+
| **dataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … } \| { provider: 'api'; read: object; path?: string }` | optional | Per-element data binding: an object binding `{ object, view?, filter?, sort?, limit? }` that overrides the page object, or an API binding `{ provider: 'api', read: { url }, path? }` that reads one same-origin endpoint |
274296
| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
275297
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
276298

@@ -283,7 +305,7 @@ View filter rule
283305
| **small** | `Record<string, string \| number>` | optional | Applied at ≤ small breakpoint |
284306
| **xsmall** | `Record<string, string \| number>` | optional | Applied at ≤ xsmall breakpoint |
285307

286-
### Nested Shape: `PageComponent.dataSource`
308+
### Nested Shape: `PageComponent.dataSource[option 1]`
287309

288310
| Property | Type | Required | Description |
289311
| :--- | :--- | :--- | :--- |
@@ -293,6 +315,16 @@ View filter rule
293315
| **sort** | `{ field: string; order: Enum<'asc' \| 'desc'> }[]` | optional | Sort order |
294316
| **limit** | `integer` | optional | Max records to display |
295317

318+
### Nested Shape: `PageComponent.dataSource[option 2]`
319+
320+
[PLANNED — not yet rendered] API binding of a page element: `{ provider: 'api', read: { url }, path? }` reads one same-origin endpoint with the console's signed-in fetch and binds the selected value, which the element reads as `data` in its expression holes and its `visibleWhen`. The console's generic stat, usage-bar and checklist blocks are the planned readers; until they ship, the binding parses and no block reads it.
321+
322+
| Property | Type | Required | Description |
323+
| :--- | :--- | :--- | :--- |
324+
| **provider** | `'api'` | ✅ | Binds the element to one same-origin API endpoint instead of an object |
325+
| **read** | `{ url: string; method?: 'GET' }` | ✅ | The read request — the same-origin endpoint the element binds |
326+
| **path** | `string` | optional | Dotted path selecting the sub-tree of the response the element binds, e.g. `data.environments` in a `{ success, data }` envelope; omitted, the element binds the whole response |
327+
296328
### Nested Shape: `PageComponent.aria`
297329

298330
| Property | Type | Required | Description |
@@ -399,7 +431,7 @@ Print declaration: paper size, orientation, margins, the running header and foot
399431
| **responsiveStyles** | `{ large?: Record<string, string \| number>; medium?: Record<string, string \| number>; small?: Record<string, string \| number>; xsmall?: Record<string, string \| number> }` | optional | Per-breakpoint scoped style maps (ADR-0065) |
400432
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features`, `os.user` and binds `data` to the data-source ADAPTER here — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). ⚠️ `data` is surface-dependent: on a `page:tabs` item `visibleWhen` it is the record ROW instead. e.g. "page.selectedProjectId != ''" |
401433
| **visibility** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. |
402-
| **dataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … }` | optional | Per-element data binding for multi-object pages |
434+
| **dataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … } \| { provider: 'api'; read: object; path?: string }` | optional | Per-element data binding: an object binding `{ object, view?, filter?, sort?, limit? }` that overrides the page object, or an API binding `{ provider: 'api', read: { url }, path? }` that reads one same-origin endpoint |
403435
| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
404436
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
405437

‎content/docs/ui/pages.mdx‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,45 @@ The `type` field is a union of the standard `PageComponentType` enum and any cus
186186
- **AI:** `ai:suggestion` — `ai:chat_window` was retired in v17.x: AI chat is not a page element, and the floating chat overlay the console mounts on every page is the supported entry point (set the app's `defaultAgent` to choose which platform agent answers there). No renderer for it ever shipped, and an authored `ai:chat_window` node is refused at the schema door — `definePage()`, `os validate`, `os build` — with that prescription at the node's path instead of drawing an unknown-type panel in front of a user
187187
- **Elements:** `element:text`, `element:number`, `element:image`, `element:divider`, `element:button`, `element:record_picker`, `element:text_input` (`element:filter` and `element:form` were retired in v17.x — no renderer ever shipped for either, and an authored node of either type is refused at the schema door — `definePage()`, `os validate`, `os build` — with that prescription at the node's path, bare node included. List surfaces own their filtering via a view's `userFilters` quick-filter bar or the list toolbar's filter builder; for forms use the object-bound `object-form` block, which is rendered and designer-publishable)
188188

189-
Components may also carry `dataSource` (per-element object binding for multi-object pages), `responsiveStyles` (per-breakpoint scoped CSS, ADR-0065), and `aria` configuration. Custom string types are also accepted for project-specific widgets. (The former `responsive` layout block was retired in v17.x — no renderer ever applied it; see the upgrade guide.)
189+
Components may also carry `dataSource` (a per-element data binding — see below), `responsiveStyles` (per-breakpoint scoped CSS, ADR-0065), and `aria` configuration. Custom string types are also accepted for project-specific widgets. (The former `responsive` layout block was retired in v17.x — no renderer ever applied it; see the upgrade guide.)
190+
191+
### Element data sources
192+
193+
A component's `dataSource` takes one of two shapes. The **object** binding queries an object, overriding the page's own `object`, so one page can show several objects. The **API** binding reads one endpoint on the deployment's own origin and binds its answer, so a page can compose from a summary that is not an object:
194+
195+
{/* os:check */}
196+
```typescript
197+
import type { PageComponent } from '@objectstack/spec/ui';
198+
199+
// Object binding: `object` is required; `view`, `filter`, `sort` and `limit` narrow it.
200+
const openDeals: PageComponent = {
201+
type: 'list-view',
202+
dataSource: {
203+
object: 'opportunity',
204+
view: 'open_deals',
205+
filter: [{ field: 'amount', operator: 'greater_than', value: 10000 }],
206+
limit: 10,
207+
},
208+
};
209+
210+
// API binding: `read.url` is a same-origin API path; `path` selects the part
211+
// of the `{ success, data }` response the element binds.
212+
const environments: PageComponent = {
213+
type: 'stat',
214+
dataSource: {
215+
provider: 'api',
216+
read: { url: '/api/v1/cloud/home' },
217+
path: 'data.environments',
218+
},
219+
};
220+
```
221+
222+
The API binding's rules:
223+
224+
- **Same-origin only.** `read.url` must open with a single `/`. An absolute URL of any scheme (`https:`, `mailto:`, `javascript:`, …), a protocol-relative `//host`, a backslash the browser reads as a slash, a space or control character, and a relative path such as `./home` are all refused when the page is parsed (`definePage()`, `os validate`, `os build`), with the reason.
225+
- **No credentials in metadata.** The console reads the endpoint with its own signed-in fetch, so the read block has no `headers`, and a credential key such as `headers`, `credentials` or `token` is refused.
226+
- **Reads only.** `read.method` is optional and accepts `GET` alone; there is no `write` block.
227+
- **Not rendered yet.** The console's generic blocks that read an API binding (a stat, a usage bar, a checklist) have not shipped: until they do, the binding parses and no block reads it.
190228

191229
## Variables
192230

0 commit comments

Comments
 (0)