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
23 changes: 23 additions & 0 deletions .changeset/strict-insert-contract-tsdoc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
"@objectstack/spec": patch
---

docs(spec): `WriteObservabilityOptions.strictReadonlyWrites` no longer claims INSERT ignores it (#7064)

The contract's closing paragraph still said "INSERT ignores it … insert is
exempt from both strips, so there is nothing to refuse" — true when #5126
shipped the option, false since #5503 wired `engine.insert` to REFUSE a
payload carrying a runtime-owned value (`RUNTIME_OWNED_FIELD_TYPES`, today
`autonumber`) under `strictReadonlyWrites: true`, throwing
`ReadonlyFieldRejectedError` (`ERR_READONLY_FIELD_REJECTED`,
`operation: 'insert'`) and writing nothing.

The TSDoc now states, measured against the engine: insert stays exempt from
the two author-declared strips at this seam (#3413 — an in-process create may
seed a `readonly: true` field's initial value; `readonlyWhen` cannot lock a
create), while the runtime-owned strip runs on insert and is exactly what
strict refuses; the exempt writers are the ones the error message names
(`isSystem`, and `preserveAudit` for a #3493 historical import), explicitly
scoped to this in-process seam so the DataProtocol ingress policy
(#3043/#6640, `FieldSchema.readonly`) stays a distinct layer. Prose only — no
key, type, or behaviour changes.
34 changes: 30 additions & 4 deletions packages/spec/src/contracts/data-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import type { IDataDriver } from './data-driver.js';
*
* `onFieldsDropped` is invoked by the engine when caller-supplied write fields
* are LEGALLY stripped from the payload before the driver write — static
* `readonly` (#2948) or a TRUE `readonlyWhen` predicate (#3042). The write
* `readonly` (#2948), a TRUE `readonlyWhen` predicate (#3042), or an
* implicitly-readonly runtime-owned type (#5503; `RUNTIME_OWNED_FIELD_TYPES`,
* today `autonumber` — the one strip that also runs on INSERT). The write
* still succeeds; the listener exists so callers that report per-field success
* (e.g. a flow's `update_record` step) can surface a warning instead of a
* silent success (#3356's masked stage write-backs).
Expand Down Expand Up @@ -48,7 +50,9 @@ export interface WriteObservabilityOptions {
* It covers every drop `onFieldsDropped` reports, i.e. both
* `DroppedFieldsEvent['reason']` arms: static `readonly: true` (#2948, which
* only runs for non-system callers) and a TRUE `readonlyWhen` predicate
* (#3042, which runs for every caller, `isSystem` included). Covering only
* (#3042, which runs for every caller, `isSystem` included) — plus, since
* #5503, the implicitly-readonly runtime-owned strip, which reports under
* the same `'readonly'` arm (see the INSERT section below). Covering only
* the static arm would leave a trusted caller — the very caller this option
* exists for, one that already passes `{ context: { isSystem: true } }` and
* is therefore exempt from the static strip — still losing `readonlyWhen`
Expand Down Expand Up @@ -81,8 +85,30 @@ export interface WriteObservabilityOptions {
* client toggle write-refusal on a security-adjacent path. Widening strict to
* the wire is a SEPARATE decision, not a side effect of this one.
*
* INSERT ignores it, for the same reason `onFieldsDropped` never fires there:
* insert is exempt from both strips, so there is nothing to refuse.
* ## INSERT — refuses runtime-owned values (since #5503)
*
* Until #5503 this paragraph declared the option inert on insert — true
* when written (#5126 predates the runtime-owned strip), false since. At
* this seam insert remains deliberately exempt from the two AUTHOR-DECLARED
* strips (#3413: an in-process create may seed a `readonly: true` field's
* initial value, and `readonlyWhen` cannot lock anything on a create at
* all), but the implicitly-readonly runtime-owned strip #5503 added runs on
* insert too — and it is exactly the one strict refuses. An insert whose
* payload carries a runtime-owned value (`RUNTIME_OWNED_FIELD_TYPES`, today
* `autonumber` — a caller-supplied record number) behaves like update at
* this seam: with this option `true` it throws `ReadonlyFieldRejectedError`
* (`operation: 'insert'`) and nothing is written; without it the value is
* stripped, the write completes, and `onFieldsDropped` fires with
* `reason: 'readonly'`. The engine-level writers exempt from that strip —
* and therefore never refused — are the two the error message itself names:
* `isSystem`, and the `preserveAudit` historical import reinstating legacy
* record numbers (#3493). Layer note: that exemption pair is THIS
* in-process seam's. The DataProtocol ingress enforces its own
* author-declared `readonly` policy on create (#3043), where
* `preserveAudit` is UPDATE-only (#6640) — see `FieldSchema.readonly`;
* nothing here widens or narrows it. `ReadonlyFieldRejectedError`'s own doc
* records the same contract from the error's side: "Thrown by
* `engine.update` — and, since #5503, by `engine.insert`".
*/
strictReadonlyWrites?: boolean;
}
Expand Down
Loading