Skip to content

Commit 93027d8

Browse files
committed
Merge origin/main into claude/issue-23050-s13d-meta-item-history
Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
2 parents 56a51a8 + 1e001c7 commit 93027d8

21 files changed

Lines changed: 1176 additions & 9 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/client": minor
3+
---
4+
5+
feat(client): `packages.publishDrafts(id, { backfill })` sends the opt-in backfill of new fields' defaults
6+
7+
Clause-②: yes (widening)
8+
9+
- `opts.backfill?: Array<{ object, field }>` is sent in the publish body; the response's `backfillApplied` reports the rows written per field. A field the publish does not add, or one without a literal `defaultValue`, refuses the whole request with a `400` before anything is published.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
---
4+
5+
feat(metadata-protocol): `publishPackageDrafts` takes an opt-in `backfill` that writes the literal `defaultValue` of fields the publish adds into the object's existing rows, as the caller
6+
7+
Clause-②: yes (widening)
8+
9+
- **New request keys.** `backfill?: Array<{ object, field }>` names fields this publish adds; `context` is the caller's execution context the writes run as. Absent or `[]`, no row is touched.
10+
- **Judged before anything is published.** Every entry must name an object drafted in the package, a field its draft declares, a field the published object does not have yet, and a literal `defaultValue` (a runtime token such as `NOW()` or `current_user`, or an expression, is resolved per new record and is refused). A malformed entry, a duplicate, or a backfill without a caller context is refused too. All problems come back in one `400 VALIDATION_ERROR`, one `issues[]` entry per problem with its `backfill.N` path, and nothing is promoted or written.
11+
- **Written after the publish, as the caller.** Once the drafts are live and the new column exists, rows whose value is absent are read and written 200 at a time through `updateManyData` with the caller's context, so permissions, RLS/FLS, validation and record hooks run as on that caller's own edit. A value already stored is never overwritten.
12+
- **New response key `backfillApplied`:** `{ success, fields: [{ object, field, updated, error?, code? }] }`, one element per requested field. `updated` counts the rows this publish wrote. A field stops at its first failing row and reports the rows written so far with the error; the publish is not undone. On the SQL driver the new column's DEFAULT already fills existing rows, so `updated` is 0 there.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/runtime": minor
3+
---
4+
5+
feat(runtime): `POST /packages/:id/publish-drafts` accepts a `backfill` body key and passes it to the protocol with the caller's execution context
6+
7+
Clause-②: yes (widening)
8+
9+
- The body's `backfill` (`[{ object, field }]`) reaches `publishPackageDrafts` as sent, together with the request's own execution context, so the backfill writes run as the signed-in caller, never as the system. A body without the key sends neither.
10+
- The protocol's request refusal answers `400 VALIDATION_ERROR` with its `issues[]`, before anything is published, flipped or announced. The `backfillApplied` receipt rides the `200` body.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `PublishPackageDraftsResponseSchema` declares `backfillApplied`, the per-field receipt of the publish door's opt-in backfill
6+
7+
Clause-②: yes (widening)
8+
9+
- `backfillApplied?: { success, fields: [{ object, field, updated, error?, code? }] }`, present only when the request named a `backfill` and the batch published. Declared so the door's response parse carries it instead of stripping it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): the bound record blocks `record:details`, `record:highlights` and `record:quick_actions` accept an authored `emptyState`, drawn only when the block's `dataSource` binding answers no row
6+
7+
Clause-②: yes (widening)
8+
9+
- **The key.** `RecordDetailsProps`, `RecordHighlightsProps` and `RecordQuickActionsProps` each gain an optional `emptyState`: the list view's own `EmptyStateSchema` by reference (`{ title, message, icon, link }`), the same shape `object-grid.emptyState` takes. Until now all three rows refused the key, so a page whose binding can legitimately answer no row (for example an environment block on an organisation with no production environment) could only show the block's own placeholder: "record not found", nothing, or an empty action bar.
10+
- **When it is drawn.** Only when the block carries a node-level `dataSource` binding and that read answers no row. A block with no binding reads the record page's record and never draws it, a loading read draws the loading state, and a failed read draws its error. On `record:quick_actions`, a bound record whose bar resolves no action keeps the existing empty placeholder.
11+
- **Planned, not yet rendered.** No renderer reads the key yet; each describe opens with `[PLANNED — not yet rendered]`, and the renderer half is objectstack-ai/objectui#12932, which ships once a spec release carrying the key is installable. Until then an authored `emptyState` validates and is not drawn.
12+
- **No other row moves.** Every other `ComponentPropsMap` row still refuses `emptyState` (`record:related_list`, whose `parentDataSource` has its own declared answer for no row, included), so `os validate` keeps reporting it there as `component-props-unknown-key`. Nothing narrows.

‎content/docs/references/api/protocol.mdx‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2524,6 +2524,7 @@ Installed package with runtime lifecycle state
25242524
| **materializeApplied** | `{ success: boolean; inserted: integer; updated: integer; failures: object[] }` | optional | ADR-0086 P2 — aggregate result of publish-time materializers across the batch (e.g. `flow`, whose draft credentials become the live ones), including side-effect failures surfaced by the per-item effects loop. Present ONLY when at least one published item had a registered materializer or a side-effect failure. Best-effort, same contract as `seedApplied`. |
25252525
| **probes** | `any` | optional | ADR-0038 L3 post-publish runtime probe report — one real read per published artifact (seeded objects have rows, views are readable, widget dataset selections execute). DELIBERATELY OPAQUE in this contract: the key is declared and carried through verbatim, but its inner shape is intentionally not modeled until a consumer needs a field of it. Present only when something was publishable; probes never fail the publish. |
25262526
| **commitId** | `string` | optional | ADR-0067 — id of the commit this publish recorded. Absent when nothing published. |
2527+
| **backfillApplied** | `{ success: boolean; fields: object[] }` | optional | The opt-in `backfill` request key's receipt: the literal `defaultValue` of each named field this publish added, written into the object's existing rows whose value is absent, page by page, as the caller (permissions, RLS/FLS and record hooks apply as on their own edit). Present ONLY when the request named at least one field and the batch published; absent means nothing was asked. A request that names a field this publish does not add, or one without a literal default, is refused with a 400 before anything is published, so this key never reports a refusal. |
25272528
| **unhiddenApps** | `string[]` | optional | ADR-0045 §3 — names of the apps whose `_unpublished` gate this publish cleared (publish = live AND visible; a materialized additive build has no drafts, only this flip). Attached by the REST door, not the protocol helper. Present ONLY when at least one app flipped — on a mid-loop failure it names the apps that DID persist, beside `unhideError` ('s split report). Spelling is a permanent wire contract (see the door's note). |
25282529
| **unhideError** | `string` | optional | Present when the ADR-0045 visibility flip failed (wholly or partway): the drafts ARE published, but apps still stored `_unpublished: true` stay externally unobservable. Client-facing text only — undeclared driver text is withheld per ADR-0112; the full cause is in the server log. The route is idempotent: re-run it once the cause is resolved. |
25292530
| **rebindError** | `string` | optional | Present when the post-publish `metadata:reloaded` announce failed: everything is published and stored, but boot-cached consumers keep the pre-publish view until re-run or restart (a newly published record-triggered flow does not bind its trigger). Client-facing text only, same ADR-0112 withhold as `unhideError`. |
@@ -2567,6 +2568,13 @@ Installed package with runtime lifecycle state
25672568
| **updated** | `integer` | ✅ | Data-plane rows updated across the batch. |
25682569
| **failures** | `{ type: string; name: string; error: string }[]` | ✅ | Each item whose data-plane projection did NOT land — named per-item, unlike the single-item door's scalar `error`, because one aggregate boolean over N items would hide WHICH ones never went live. |
25692570

2571+
### Nested Shape: `PublishPackageDraftsResponse.backfillApplied`
2572+
2573+
| Property | Type | Required | Description |
2574+
| :--- | :--- | :--- | :--- |
2575+
| **success** | `boolean` | ✅ | False when any field stopped short (its entry carries `error`). The publish itself still succeeded and is not undone. |
2576+
| **fields** | `{ object: string; field: string; updated: integer; error?: string; … }[]` | ✅ | One element per requested field, in request order. |
2577+
25702578

25712579
---
25722580

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,6 +1685,7 @@ View filter rule
16851685
| **enforceFieldSecurity** | `boolean` | optional | Fold this block's field list through the caller's FIELD-read permissions before rendering, so a field the permission set denies leaves no empty row behind (renderer default: off). Presentation only: it re-applies the same field-read answer the server already enforced (ADR-0066 D3) and never widens access — with it off a denied field still arrives masked or stripped, and with it on the server still decides every value. |
16861686
| **redactFields** | `string[]` | optional | Field names this block never renders, whatever the permission answer (renderer default: render everything authored). Presentation only, evaluated in the browser after the record is fetched — the values are still in the page, so this is NOT a data-access control and NOT the object's `publicSharing.redactFields`, which removes them server-side. To keep a value from the caller, gate the field itself (`requiredPermissions` / `maskingRule`, ADR-0066 D3) or the permission set. Neighbours `hideFields`, which is the dedupe channel the renderer also writes to. |
16871687
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities the user must ALL hold — names that permission sets grant through `systemPermissions`, not object actions: `read` or `update` here is an ordinary capability name, not the object's read or edit permission. When the client has resolved the user's capabilities and any of these is missing, this block does not render its content; wherever it would otherwise render, an insufficient-permissions notice takes its place. Presentation only: it authorises nothing, and the data API still serves the same data to the same user. A client that cannot resolve the user's capabilities (no permission provider, or one that does not report `systemPermissions`) renders this block as if they were held — it fails open. |
1688+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | [PLANNED — not yet rendered] What this block draws when its `dataSource` binding answers no row: `{ title, message, icon, link }`, the list view's own empty-state shape. Drawn then and only then — never on a block with no `dataSource` (it reads the record page's record instead), never while the bound read is loading, and never in place of the error a failed read shows. Omitted, a binding with no row keeps the block's own placeholder. |
16881689
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
16891690

16901691
### Nested Shape: `RecordDetailsProps.sections[number]`
@@ -1705,6 +1706,15 @@ View filter rule
17051706
| **headerColor** | `Enum<'muted' \| 'muted/50' \| 'accent' \| 'primary/10' \| 'secondary/10' \| 'destructive/10'>` | optional | Section-header background tint, from the closed six-token vocabulary rendered by objectui's `record:details` header (`muted` \| `muted/50` \| `accent` \| `primary/10` \| `secondary/10` \| `destructive/10`). A value outside the enum is refused at authoring time rather than silently not painting. Omit for an untinted header. |
17061707
| **count** | `{ field: string }` | optional | Show how many values a multi-value field holds on the current record in this section's heading: `{ field: 'tags' }`, with `label` writing the `{{ count }}` hole where the number goes (`'{{ count }} tags'`). The field must be a multi-value field the bound object declares — `multiselect`, `checkboxes`, `tags`, or a `select` / `lookup` / `user` / `file` / `image` field with `multiple: true` — and an absent or empty value counts 0. Every locale of an inline label map, and a translated `objects.<object>._sections.<name>.label`, writes the same `{{ count }}` hole. Not allowed beside `group`. The record detail page fills every hole in the heading after translating it. |
17071708

1709+
### Nested Shape: `RecordDetailsProps.emptyState`
1710+
1711+
| Property | Type | Required | Description |
1712+
| :--- | :--- | :--- | :--- |
1713+
| **title** | `string \| Record<string, string>` | optional | Heading of the empty state |
1714+
| **message** | `string \| Record<string, string>` | optional | Line of text below the heading |
1715+
| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph |
1716+
| **link** | `{ label: string \| Record<string, string>; url: string }` | optional | The one next step an empty list names: a link the user follows, `{ label, url }` — for example to the how-to page that explains what belongs here when nobody can add a row. It opens a page or an address and runs no action. The console draws it in a list's empty state (the `grid` and `gallery` visualizations) and in an `object-grid` block's, beside the add-record action where one is offered. |
1717+
17081718
### Nested Shape: `RecordDetailsProps.aria`
17091719

17101720
| Property | Type | Required | Description |
@@ -1757,6 +1767,7 @@ Type: `string`
17571767
| **enforceFieldSecurity** | `boolean` | optional | Fold this block's highlight chips through the caller's FIELD-read permissions before rendering, so a field the permission set denies leaves no empty chip behind (renderer default: off). Presentation only: it re-applies the same field-read answer the server already enforced (ADR-0066 D3) and never widens access — the record is fetched either way and the server still decides every value. |
17581768
| **redactFields** | `string[]` | optional | Field names this block never renders as a chip, whatever the permission answer (renderer default: render every field authored). Presentation only, evaluated in the browser after the record is fetched — the values are still in the page, so this is NOT a data-access control and NOT the object's `publicSharing.redactFields`, which removes them server-side. To keep a value from the caller, gate the field itself (`requiredPermissions` / `maskingRule`, ADR-0066 D3) or the permission set. |
17591769
| **requiredPermissions** | `string[]` | optional | [ADR-0066] Capabilities the user must ALL hold — names that permission sets grant through `systemPermissions`, not object actions: `read` or `update` here is an ordinary capability name, not the object's read or edit permission. When the client has resolved the user's capabilities and any of these is missing, this block does not render its content; wherever it would otherwise render, an insufficient-permissions notice takes its place. Presentation only: it authorises nothing, and the data API still serves the same data to the same user. A client that cannot resolve the user's capabilities (no permission provider, or one that does not report `systemPermissions`) renders this block as if they were held — it fails open. |
1770+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | [PLANNED — not yet rendered] What this block draws when its `dataSource` binding answers no row: `{ title, message, icon, link }`, the list view's own empty-state shape. Drawn then and only then — never on a block with no `dataSource` (it reads the record page's record instead), never while the bound read is loading, and never in place of the error a failed read shows. Omitted, a binding with no row keeps the block's own placeholder. |
17601771
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
17611772

17621773
### Nested Shape: `RecordHighlightsProps.fields[number]`
@@ -1768,6 +1779,15 @@ Type: `string`
17681779
| **type** | `string` | optional | Override cell renderer type (rare) |
17691780
| **readonly** | `boolean` | optional | Render this chip read-only — suppresses inline editing on the highlight card. Use for hook/automation-maintained columns that must not be hand-edited from the record header. |
17701781

1782+
### Nested Shape: `RecordHighlightsProps.emptyState`
1783+
1784+
| Property | Type | Required | Description |
1785+
| :--- | :--- | :--- | :--- |
1786+
| **title** | `string \| Record<string, string>` | optional | Heading of the empty state |
1787+
| **message** | `string \| Record<string, string>` | optional | Line of text below the heading |
1788+
| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph |
1789+
| **link** | `{ label: string \| Record<string, string>; url: string }` | optional | The one next step an empty list names: a link the user follows, `{ label, url }` — for example to the how-to page that explains what belongs here when nobody can add a row. It opens a page or an address and runs no action. The console draws it in a list's empty state (the `grid` and `gallery` visualizations) and in an `object-grid` block's, beside the add-record action where one is offered. |
1790+
17711791
### Nested Shape: `RecordHighlightsProps.aria`
17721792

17731793
| Property | Type | Required | Description |
@@ -1903,6 +1923,16 @@ Sort field and direction pair
19031923
| **inline** | `boolean` | optional | Render in the flow instead of pulling up into the record-header band. The page header sets this itself when it hosts the bar in its own action slot. |
19041924
| **variant** | `Enum<'default' \| 'destructive' \| 'outline' \| 'secondary' \| 'ghost' \| 'link'>` | optional | Button variant for every action — the Button primitive's own vocabulary; a per-action `variant` on the resolved def wins (renderer default: `default`). |
19051925
| **size** | `Enum<'default' \| 'sm' \| 'lg' \| 'icon'>` | optional | Button size for every action — the Button primitive's own vocabulary; a per-action `size` wins (renderer default: `sm`). |
1926+
| **emptyState** | `{ title?: string \| Record<string, string>; message?: string \| Record<string, string>; icon?: string; link?: object }` | optional | [PLANNED — not yet rendered] What this block draws when its `dataSource` binding answers no row: `{ title, message, icon, link }`, the list view's own empty-state shape. Drawn then and only then — never on a block with no `dataSource` (it reads the record page's record instead), never while the bound read is loading, and never in place of the error a failed read shows. Omitted, a binding with no row keeps the block's own placeholder. |
1927+
1928+
### Nested Shape: `RecordQuickActionsProps.emptyState`
1929+
1930+
| Property | Type | Required | Description |
1931+
| :--- | :--- | :--- | :--- |
1932+
| **title** | `string \| Record<string, string>` | optional | Heading of the empty state |
1933+
| **message** | `string \| Record<string, string>` | optional | Line of text below the heading |
1934+
| **icon** | `string` | optional | Icon name drawn above the heading; a name that resolves to no icon draws the default empty-state glyph |
1935+
| **link** | `{ label: string \| Record<string, string>; url: string }` | optional | The one next step an empty list names: a link the user follows, `{ label, url }` — for example to the how-to page that explains what belongs here when nobody can add a row. It opens a page or an address and runs no action. The console draws it in a list's empty state (the `grid` and `gallery` visualizations) and in an `object-grid` block's, beside the add-record action where one is offered. |
19061936

19071937

19081938
---

0 commit comments

Comments
 (0)