Skip to content

Commit 28d5dd6

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-24961-readonly-probe-unopenable-path
Claude-Session: https://claude.ai/code/session_01QK6pdbxKZiHdFApL8RYVh9 Co-authored-by: Claude <noreply@anthropic.com>
2 parents 7c66e98 + 4fe3af9 commit 28d5dd6

51 files changed

Lines changed: 1797 additions & 180 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/objectql': minor
4+
'@objectstack/service-automation': patch
5+
---
6+
7+
feat(objectql): a caller value that a stored field's `expression` replaced is reported through `onFieldsDropped` / `droppedFields` under the new `DroppedFieldsEvent.reason` value `stored_expression`
8+
9+
Clause-②: yes (widening)
10+
11+
The engine computes a stored (non-`formula`) field's declared `expression` on every write, so a value the caller sends for such a field never stands. Until now nothing said so. `onFieldsDropped` stayed silent, so the PATCH and `/batch` doors answered with no `droppedFields`, and a flow's `update_record` reported success over a row that holds the computed value instead of the one sent.
12+
13+
- **New `reason` value.** `DroppedFieldsEventSchema.reason` (`@objectstack/spec/data`) gains `stored_expression`. It means the field is a stored column the engine computes from its `expression`, and the caller's value was replaced by the computed one. The field IS written, so `computed` (a `formula` with no column, whose key is dropped) would be wrong for it. The four existing values are unchanged.
14+
- **When it is reported.** On insert (single, batch, and `insertMany`'s per-row `droppedFields`), on update by id, and on a predicate update, in every context, `isSystem` included. It is reported only when the caller sent the field and the value stored differs from the caller's value. A client that sends the computed value (the console's line-item grid) or no value is told nothing. A value another strip already took, such as a stored expression field that is also `readonly`, is reported once, under that strip's reason.
15+
- **`strictReadonlyWrites` is unchanged.** The maintainer's ruling 「甲」 says only the loud half ships in 18.x. A strict write whose value for such a field differs from the computed one is NOT refused: it completes with the computed value, and the replacement is reported through `onFieldsDropped` under strict as well. A refusal is runtime behaviour that no conversion can carry, so the strict refusal of a `stored_expression` replacement is deferred to 19.0. A strict write that also carries a refusable drop (`readonly`, `readonly_when`, `primary_key`, `computed`) is refused as before, for those drops only, and fires no listener.
16+
- **Flow step warning.** `@objectstack/service-automation` words the new reason. Its warning says the requested value(s) were not stored, rather than that the field was not written, and its closing sentence says the write succeeded with the computed value(s).
17+
18+
**For consumers that branch on `reason`.** A `Record<DroppedFieldsEvent['reason'], …>` table fails its typecheck until it has a `stored_expression` row. That is the intended signal: add the row. A client running behind the server should treat a reason it does not recognise as "not applied", never as one of the known reasons.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': patch
4+
---
5+
6+
feat(spec)!: a list's `addRecord` block says what renders — `enabled` is required, `position` defaults to `top`, and `mode` / `formView` are retired
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: registered add-record-mode-form-view-removed, add-record-enabled-defaulted, add-record-mode-form-view-retired, add-record-enabled-required -->
11+
12+
**BREAKING** (an authoring narrowing), shipped as `minor` under the launch-window convention for breaking changes, and in 18.x under the standing rule for breaking narrowings: an ADR-0087 conversion carries every body that parsed under 18.0, so no stored row or built artifact is refused.
13+
14+
`AddRecordConfigSchema` — the `addRecord` block on a list view, an object's or a view container's `listViews`, a list page's `interfaceConfig` and a page `list-view` node — declared defaults the renderer never applied (`enabled` `true`, `position` `bottom`, `mode` `inline`) and two keys no renderer read. The list's Add button is drawn only for an explicit `enabled: true`, an absent `position` renders at the top, and every Add opens the object's create form. The block now says exactly that:
15+
16+
- **`enabled` is REQUIRED, with no default.** `addRecord: {}` is refused at parse (`os validate`, `os build`, the save doors) with a prescription naming the key, instead of drawing nothing.
17+
- **`position` defaults to `top`**, where the renderer has always placed an absent position. Values stay `top` / `bottom` / `both`. No screen moves.
18+
- **`mode` and `formView` are `retiredKey()` tombstones** (ADR-0049 enforce-or-remove): authoring either is a `tsc` error and a parse error carrying the prescription. The create form is chosen by convention instead: the create entry opens `formViews.create` when the object's view container declares it, else `form` (or `formViews.default`). How that form opens stays the renderer's.
19+
20+
### FROM → TO
21+
22+
| before | after |
23+
| --- | --- |
24+
| `addRecord: {}` (parsed as `enabled: true`, drew nothing) | refused: write `addRecord: { enabled: true }` |
25+
| `addRecord: { position: 'bottom' }` | refused: write `addRecord: { enabled: true, position: 'bottom' }` |
26+
| `addRecord: { enabled: true }` (parsed `position: 'bottom'`) | parses as `position: 'top'`, where it always rendered |
27+
| `addRecord: { enabled: true, mode: 'form' }` (any `mode`) | `addRecord: { enabled: true }` |
28+
| `addRecord: { enabled: true, formView: 'create' }` | `addRecord: { enabled: true }`, with the form declared as `formViews.create` on the object's view container |
29+
30+
**The one-line fix:** delete `mode` and `formView`, and write `enabled: true` (or `enabled: false`) on every `addRecord` block. `os migrate meta --from 17` lists the edits for existing metadata sources; `--write` applies the ones it can prove, and you apply the rest by hand.
31+
32+
### The ADR-0087 disposition
33+
34+
- D2 `add-record-mode-form-view-removed` strips `mode` and `formView` from every `addRecord` block it reaches (`stack.views[]` in all three persisted spellings, `objects[].listViews.*`, `pages[].interfaceConfig` and every page `list-view` node), with a notice per key. Lossless in pixels: no renderer read either key.
35+
- D2 `add-record-enabled-defaulted` writes `enabled: true`, the default the 18.0 spec declared, onto a block that omits `enabled`, with a notice. Measured before choosing it: no producer in this repository (the showcase's two blocks, the lint fixture) or in objectui's Studio design surface writes `addRecord` without `enabled`, so the conversion carries the narrowing with a zero measured population. **One visible consequence, stated:** a stored block that omitted `enabled` drew no Add button, because the renderer read the raw body; converted, it draws the button its declaration always promised. To keep a list without one, write `enabled: false` or drop the block. An explicit `enabled` value is never touched.
36+
- Both are retired from the load path: a live author is refused at parse and taught; stored rows, built artifacts inside their declared-floor window and `os migrate meta --from 17` replay them.
37+
- `RETIRED_KEYS_BY_MAJOR[18]` gains `ui/AddRecordConfig:mode` and `ui/AddRecordConfig:formView`; the D3 entries are `add-record-mode-form-view-retired` and `add-record-enabled-required`; the default moves (`enabled` `true` → none, `position` `bottom` → `top`) are declared in `DEFAULT_CHANGES_BY_MAJOR[18]`.
38+
39+
### Also in this change
40+
41+
- The Studio view form and page form offer exactly `enabled` and `position` for `addRecord`.
42+
- `@objectstack/lint`: `os explain list-view-field-unknown` no longer names `addRecord.formView` as a position another rule checks.
43+
- The showcase's two `addRecord` blocks and the docs (`ui/create-vs-edit-form`, `ui/views`) drop `mode` / `formView` and state the create-form convention.

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,28 @@ Auth settings precedence:
129129

130130
---
131131

132-
## Branding (single-tenant)
132+
## Branding
133+
134+
The console's product name, logo, favicon and colours are served by `RuntimeConfigPlugin` (`@objectstack/cloud-connection`) as the `branding` block of `GET /api/v1/runtime/config`, which the console reads on every page load. A host that constructs its own `RuntimeConfigPlugin` with an option of the same name (`productName`, `logoUrl`, …) outranks the variable. The first eight variables below are read **once, when that plugin is constructed at boot**: changing one takes a restart. A value saved in **Setup → Branding** is read per request instead (see the precedence below). Mail and auth are named separately, by `OS_APP_NAME`.
133135

134136
| Variable | Type | Default | Description |
135137
|:---|:---|:---|:---|
136-
| `OS_PRODUCT_NAME` | string | `ObjectStack` | Product name shown in the UI chrome and emails. |
137-
| `OS_PRODUCT_SHORT_NAME` | string | `OS` | Short name used in compact UI surfaces (favicons, mobile titles). |
138-
| `OS_APP_NAME` | string | — | The deployment's product name under `objectstack serve`: `{{appName}}` in every email template (auth emails included), the issuer an authenticator app lists a TOTP enrollment under, and the MCP OAuth resource name. Wins over `config.email.appName`, `config.email.defaultTemplateContext.appName` and `config.appName`; with none of them set the name is `ObjectStack`. An explicitly set `branding.workspace_name` setting outranks it in auth. |
138+
| `OS_PRODUCT_NAME` | string | `ObjectOS` | The console's product name (`branding.productName`): the browser-tab title (alone, or after the open app's label), the header's product mark, the sign-in pages, the loading screen and the installed-app (PWA) manifest's `name`. It does **not** name emails, the authenticator app entry or the MCP sign-in — `OS_APP_NAME` does — so an unconfigured deployment reads `ObjectOS` in the console and `ObjectStack` in its mail. Empty reads as unset. |
139+
| `OS_PRODUCT_SHORT_NAME` | string | the product name served | The PWA manifest's `short_name` (`branding.productShortName`). Unset, it is the product name actually served, which under `single` may be a saved Branding name. |
140+
| `OS_PRODUCT_STAGE` | enum | — (no `stage` key) | `preview` \| `beta` \| `ga`, matched exactly. Drives the console's top-bar chip (`branding.stage`): "Preview" for `preview`, "Beta" for `beta`, none for `ga`. Unset sends no `stage` key and the console keeps its own `preview` default, so the chip shows. Any other value (`GA`, `general-availability`) is refused with one `warn` at boot and treated as unset. |
141+
| `OS_LOGO_URL` | url | — | The product logo, absolute or relative (`branding.logoUrl`): the sign-in pages, the loading screen and the header's product mark. Unset, the console draws its built-in mark. |
142+
| `OS_FAVICON_URL` | url | — | The favicon, absolute or relative (`branding.faviconUrl`): the browser-tab icon and the PWA manifest's icon. While an app with its own `branding.favicon` is open, that icon replaces it. No Branding setting overrides this variable, on any posture. |
143+
| `OS_BRAND_COLOR` | string | — | The brand colour as hex, e.g. `#4F46E5` (`branding.brandColor`). The console uses it as the installed-app theme colour, through `OS_PWA_THEME_COLOR`'s default; each app's own `AppBranding` colours are separate. |
144+
| `OS_PWA_DESCRIPTION` | string | `<product name> — runtime console` | The PWA manifest's `description` (`branding.pwaDescription`). Unset, it is built from the product name actually served. |
145+
| `OS_PWA_THEME_COLOR` | string | the brand colour served, else `#4f46e5` | The PWA manifest's `theme_color` (`branding.pwaThemeColor`), as hex. Unset, it is the brand colour actually served, else `#4f46e5`. |
146+
| `OS_APP_NAME` | string | — | The deployment's product name under `objectstack serve` for mail and auth: `{{appName}}` in every email template (auth emails included), the issuer an authenticator app lists a TOTP enrollment under, and the MCP OAuth resource name. Wins over `config.email.appName`, `config.email.defaultTemplateContext.appName` and `config.appName`; with none of them set the name is `ObjectStack`. Under `single`, an explicitly set `branding.workspace_name` setting outranks it in auth (the authenticator issuer takes a change only when the auth instance is rebuilt, such as at a restart); under `group` / `isolated` nothing does. It never names the console — that is `OS_PRODUCT_NAME`. |
147+
148+
Branding precedence, by tenancy posture (`OS_TENANCY_POSTURE`):
149+
150+
- **`single`** — three fields read the `branding` settings namespace on every request, so a save in **Setup → Branding** shows on the next page load with no restart: `workspace_name` → `productName`, `logo_url` → `logoUrl`, `accent_color` → `brandColor`. Each takes the first source that is set: the setting (a value saved in Setup, or the settings layer's own `OS_BRANDING_WORKSPACE_NAME` / `OS_BRANDING_LOGO_URL` / `OS_BRANDING_ACCENT_COLOR`, which outranks a saved value and locks the field), then the host's `RuntimeConfigPlugin` option, then `OS_PRODUCT_NAME` / `OS_LOGO_URL` / `OS_BRAND_COLOR`, then the built-in default. The Branding page's own default for `workspace_name` (`ObjectStack`) is never served to the console. A set `workspace_name` also names sign-in emails, the authenticator app entry and the MCP sign-in, ahead of `OS_APP_NAME`.
151+
- **`group` / `isolated`** — the `branding` namespace is not offered: it is absent from `GET /api/settings`, `GET` and `PUT /api/settings/branding` answer `404 UNKNOWN_NAMESPACE`, and Setup lists no Branding entry. The `OS_BRANDING_*` variables are not read. The console's branding is the host option, then the variables in this table, then the default, and the auth product name is the `OS_APP_NAME` chain alone. A walled deployment that set `OS_BRANDING_WORKSPACE_NAME` moves the value to `OS_APP_NAME`.
152+
153+
On every posture `productShortName`, `pwaDescription` and `pwaThemeColor` follow the name and colour actually served unless set themselves, and `OS_PRODUCT_STAGE` and `OS_FAVICON_URL` have no setting above them.
139154

140155
---
141156

0 commit comments

Comments
 (0)