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
28 changes: 28 additions & 0 deletions .changeset/26924-new-tab-url-backend-path.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@objectstack/spec': minor
---

fix(spec)!: an action's `newTabUrl` is a path on this backend and must start with `/`; an absolute URL is refused at parse

`newTabUrl` is the zero-roundtrip target of the pre-opened-tab flow (`opensInNewTab: true`): on click the console drives the pre-opened tab to the backend origin + `newTabUrl`, with `{recordId}` substituted, and posts nothing. Its JSDoc called it a path template, while its `.describe()` and the action form's help text called it a URL template, and nothing refused an absolute value. So `newTabUrl: 'https://example.com/export/{recordId}'` parsed clean and opened a tab at `https://BACKEND/https://example.com/export/…`. `ActionSchema.newTabUrl` now refuses any value that does not start with `/`, and the JSDoc, `.describe()` and the form help (all four form-translation locales) say the same thing: a path on this backend.

Clause-②: no (narrowing) — `ActionSchema.newTabUrl` (every action door: `defineAction`, the `action` metadata type, an object's `actions[]`) refuses a value that does not start with `/`. Nothing is widened.

Metadata-protocol: narrowed (an action's `newTabUrl` may no longer be an absolute URL or a relative path without a leading `/`)

<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed: newTabUrl keeps its name, shape and meaning, and now refuses the values that never opened the page they named. No backend path can be derived from an external URL, so there is no mechanical rewrite; a stored action row holding one loads unchanged, is counted invalid with a [metadata_spec_invalid] boot line and still registered, is served with _diagnostics locating newTabUrl, and its next save is refused until the author rewrites it. -->

**BREAKING** accept-set narrowing on `ActionSchema.newTabUrl` (`@objectstack/spec`).

- **Before.** `newTabUrl: 'https://example.com/export/{recordId}'` (or `sso-open/{recordId}`) parsed, and the console opened the backend origin + that text: a broken tab.
- **Now.** The parse refuses it at `newTabUrl` with: `` `newTabUrl` is a path on this backend and must start with `/` `` and names the external-page remedy.
- **Unchanged.** A path template such as `/sso-open/{recordId}` or `/api/v1/invoice/{recordId}/pdf` parses byte-identically. No renderer changes: the console already prefixes the backend origin, so the declared meaning and the behaviour now agree.
- **A row stored before this release.** Nothing converts it. It loads unchanged and is still registered, counted `invalid` with a `[metadata_spec_invalid]` boot line; the metadata read serves it with `_diagnostics` locating `newTabUrl`; it keeps opening the same broken tab; and its next save is refused until the value is a path.
- **Who sees it.** An author who wrote an absolute URL in `newTabUrl`. No example, template, fixture or platform object in this repository does.

**The remedy.** For an endpoint on this backend, write its path: `newTabUrl: '/sso-open/{recordId}'`. For an external page, drop `opensInNewTab` and `newTabUrl` and declare a `type: 'url'` action instead: `target: 'https://example.com/export/${ctx.recordId}'`, `openIn: 'new-tab'`.

Also in `@objectstack/spec`, prose only, no accept-set change:

- The `DashboardWidget` docblock and `requiresObject`'s `.describe()` say that, outside design mode, a dataset-bound widget whose query the server refuses as a read refusal (403 `PERMISSION_DENIED`) is left out of the dashboard for that viewer, and that `requiresObject` stays an install gate, never a viewer gate.
- Three shipped citations of `content/docs/concepts/north-star.mdx` "§8" / "§6.3" (sections that page never had; the page is now `concepts/architecture-overview`) are corrected: `expression.zod.ts` cites ADR-0032 ("one CEL language"), `environment-artifact.zod.ts` cites the architecture overview's "Environment Artifact" section, and `hook-body.zod.ts` drops a pointer to a heading that does not exist. The generated references follow.
8 changes: 8 additions & 0 deletions .changeset/26924-platform-objects-new-tab-url-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@objectstack/platform-objects': patch
---

fix(platform-objects): the action form's `newTabUrl` help text says it is a path on this backend starting with `/`, in all four locales (en, zh-CN, ja-JP, es-ES), and names the `type: 'url'` action for an external page — matching the spec, which now refuses an absolute `newTabUrl`

Clause-②: no
Metadata-protocol: none
8 changes: 8 additions & 0 deletions .changeset/26973-formula-north-star-citation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@objectstack/formula': patch
---

docs(formula): the README and the package docblock cite ADR-0032 ("one CEL language") instead of `content/docs/concepts/north-star.mdx` "§8 No private expression DSL", a section that page never had (the page is now `concepts/architecture-overview`, which carries no such heading)

Clause-②: no
Metadata-protocol: none
2 changes: 1 addition & 1 deletion content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ const result = ApiMethod.parse(data);
| **bodyExtra** | `Record<string, any>` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.<var>}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. |
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
| **newTabUrl** | `string` | optional | New-tab path template on this backend, starting with / (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab to the backend origin + this path immediately — no action POST. The endpoint must enforce auth itself. An absolute URL is refused; an external page is a type:'url' action with openIn: 'new-tab'. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/kernel/metadata-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ const result = MetadataBulkResultSchema.parse(data);
| **bodyExtra** | `Record<string, any>` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.<var>}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. |
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
| **newTabUrl** | `string` | optional | New-tab path template on this backend, starting with / (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab to the backend origin + this path immediately — no action POST. The endpoint must enforce auth itself. An absolute URL is refused; an external page is a type:'url' action with openIn: 'new-tab'. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/shared/expression.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ SQL fragments (analytics joins, partial indexes) are intentionally **not**
routed through this schema — they stay driver-native because their security
posture and portability story differ.

See also: content/docs/concepts/north-star.mdx §8 "No private expression DSL"
See also: docs/adr/0032-unified-expression-layer.md — "one CEL language"

<Callout type="info">
**Source:** `packages/spec/src/shared/expression.zod.ts`
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/system/environment-artifact.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ identity (`commitId`, `checksum`).
`metadata`, outside the `checksum` digest. Absent ≠ `{}` — see the
key's docblock.

See `content/docs/concepts/north-star.mdx` §6.3 for the
runtime-inputs boundary.
See `content/docs/concepts/architecture-overview.mdx` § "Environment
Artifact" for the runtime-inputs boundary.

### History (#4740)

Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/ui/action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const result = ActionSchema.parse(data);
| **bodyExtra** | `Record<string, any>` | optional | Static request-body fields for a type:"api" action, merged last (overrides user params). `{{page.<var>}}` tokens are resolved by the runtime. This — not `params` — is where a payload goes. |
| **mode** | `Enum<'create' \| 'edit' \| 'delete' \| 'custom'>` | optional | Semantic mode of the action. |
| **opensInNewTab** | `boolean` | optional | Open the action result in a new tab. The renderer pre-opens the tab synchronously on click (popup-blocker-safe) and navigates it to the handler's redirectUrl. |
| **newTabUrl** | `string` | optional | Direct new-tab URL template (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab here immediately — no action POST. The endpoint must enforce auth itself. |
| **newTabUrl** | `string` | optional | New-tab path template on this backend, starting with / (`{recordId}` placeholder). When set with opensInNewTab, the renderer navigates the pre-opened tab to the backend origin + this path immediately — no action POST. The endpoint must enforce auth itself. An absolute URL is refused; an external page is a type:'url' action with openIn: 'new-tab'. |
| **onSuccess** | `{ navigate: string; openIn?: Enum<'self' \| 'newTab'> }` | optional | Post-success navigation for type:'api', type:'script' and type:'flow' actions. `navigate` is a route/URL template interpolating $`{param.*}`, $`{ctx.*}` and $`{result.*}` (the server response; for a flow, the completed run's answer); `openIn` defaults 'self'. A flow run that pauses at a screen or ends refused performs no navigation. The handler-return convention (`{ redirectUrl }` without openIn) keeps its 17.0.0 new-tab behavior. |
| **aria** | `never` | optional | [REMOVED] `action.aria` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no action surface ever applied it: the button, icon, menu, group and bar renderers, the row and bulk action menus and the record quick-actions toolbar all take the accessible name from the action's `label` and never read this block, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied is the action's required `label` — the visible button or menu-item text, and the `aria-label` of an icon-only action — so write the name you meant there. To name the region that PLACES the actions, author `ariaLabel` / `ariaDescribedBy` / `role` in the `aria` block of the placing node: `page.components[].aria` (the component that renders the actions) or the list view `aria`. 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. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/ui/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const result = DashboardSchema.parse(data);
| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| …>` | optional (default: `"metric"`) | Visualization type |
| **chartConfig** | `{ title?: string \| Record<string, string>; subtitle?: string \| Record<string, string>; description?: string \| Record<string, string>; colors?: string[] \| Record<string, string>; … }` | optional | Chart appearance: title, subtitle, description, colors, height, legend, data labels, annotations, interaction. The bound dataset decides which series exist and which column each one reads (ADR-0021), so `type` (the widget's own `type` wins), `xAxis`, `yAxis` and `series` are refused here — select `dimensions` / `values` instead. |
| **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming |
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered |
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered — an install gate, not a viewer gate. Outside design mode, a dataset-bound widget whose query the server refuses as a read refusal (403 PERMISSION_DENIED) is left out of the dashboard for that viewer with no key. |
| **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered |
| **filter** | `any` | optional | Presentation-scope filter (runtimeFilter) |
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`) |
Expand Down Expand Up @@ -167,7 +167,7 @@ Dashboard header action
| **type** | `Enum<'bar' \| 'horizontal-bar' \| 'column' \| 'line' \| 'area' \| 'pie' \| 'donut' \| 'funnel' \| 'scatter' \| 'treemap' \| 'sankey' \| 'combo' \| 'gauge' \| 'solid-gauge' \| … +6 more>` | optional (default: `"metric"`) | Visualization type |
| **chartConfig** | `{ title?: string \| Record<string, string>; subtitle?: string \| Record<string, string>; description?: string \| Record<string, string>; colors?: string[] \| Record<string, string>; … }` | optional | Chart appearance: title, subtitle, description, colors, height, legend, data labels, annotations, interaction. The bound dataset decides which series exist and which column each one reads (ADR-0021), so `type` (the widget's own `type` wins), `xAxis`, `yAxis` and `series` are refused here — select `dimensions` / `values` instead. |
| **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming |
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered |
| **requiresObject** | `string` | optional | Hide the widget unless the named object is registered — an install gate, not a viewer gate. Outside design mode, a dataset-bound widget whose query the server refuses as a read refusal (403 PERMISSION_DENIED) is left out of the dashboard for that viewer with no key. |
| **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered |
| **filter** | `any` | optional | Presentation-scope filter (runtimeFilter) |
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`) |
Expand Down
2 changes: 1 addition & 1 deletion packages/formula/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ Canonical expression engine for ObjectStack.
- **Dialect: `js`** — sandboxed L2 hook bodies (delegated).
- **Dialect: `cron`** — job schedules (delegated).

See `content/docs/concepts/north-star.mdx` §8 "No private expression DSL" and `ROADMAP.md` M9.
See ADR-0032 (`docs/adr/0032-unified-expression-layer.md`, "one CEL language") and `ROADMAP.md` M9.
2 changes: 1 addition & 1 deletion packages/formula/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Language) is the default dialect; `js` and `cron` are dispatched to
* dedicated plugin engines.
*
* @see content/docs/concepts/north-star.mdx §8 "No private expression DSL"
* @see docs/adr/0032-unified-expression-layer.md — "one CEL language"
* @see ROADMAP.md M9 "Expression Unification"
*/

Expand Down
Loading
Loading