You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(approvals,rest): tell the truth about approval writes — lock policy, dropped fields, and the batch create ingress (#3794, #3835) (#3834)
* fix(approvals,rest): surface the node's lock policy and the batch's dropped fields (#3794)
An approval flow reported record writability wrong in both directions: what the
user could change said "locked", and what they couldn't said "updated
successfully". Both halves were missing signal, not wrong behaviour — the server
did the right thing and told nobody.
`rowFromRequest` now emits `locks_record`, read from the same `node_config_json`
snapshot the `beforeUpdate` lock hook reads, with the same default-true. A client
could previously only see "a request is pending" and had to guess whether that
locked the record; the Console guessed "locked" every time, which hides the whole
point of a `lockRecord: false` node.
`POST /batch` (cross-object transactional batch) never wired `onFieldsDropped`,
so the one write path the Console's master-detail record form takes was also the
one path with no write-observability — a `readonlyWhen` strip there was
completely silent. It now collects per-op events and returns them as a top-level
`droppedFields` list tagged with each operation's index; omitted when empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(spec): regenerate the batch reference for CrossObjectBatchDroppedFields (#3794)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(spec): refresh the API-surface snapshot for CrossObjectBatchDroppedFields (#3794)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(rest): route batch creates through the create ingress so `readonly` is enforced (#3835)
`POST /batch` called `ql.insert` directly, and the engine's INSERT path is
static-`readonly`-exempt by design (#3413) — the strip that stops a non-system
caller from seeding a read-only column lives at the protocol's create ingress
(#3043). So the same forged value was dropped on `POST /data/:object` and
written through on `/batch`: one rule, two answers.
Create ops now go through `p.createData`, the ingress itself, rather than a
second copy of the strip at the REST layer. One ingress means a future change to
its policy covers the batch for free, and the carve-outs it already encodes stay
intact — the platform-object exemption (a `sys_`/`managedBy` object's own guard
must REJECT a forged value, not silently swallow it) and the `isSystem`
exemption. `trxCtx` is passed as the context, so the insert still joins the batch
transaction and `$ref` resolution is unaffected; the ingress's `droppedFields`
fold into the batch's per-op list.
Update ops are untouched — the engine enforces both strips on its update path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
|**fields**|`string[]`| ✅ | Caller-supplied field names the engine removed from the write payload |
151
+
|**reason**|`Enum<'readonly' \| 'readonly_when'>`| ✅ | Why the fields were dropped: static readonly (#2948) or a TRUE readonlyWhen predicate (#3042) |
152
+
|**index**|`integer`| ✅ | Index of the operation in the request `operations` array |
153
+
154
+
139
155
---
140
156
141
157
## CrossObjectBatchOperation
@@ -171,6 +187,7 @@ const result = BatchConfig.parse(data);
171
187
| Property | Type | Required | Description |
172
188
| :--- | :--- | :--- | :--- |
173
189
|**results**|`any[]`| ✅ | Per-operation result, index-aligned with the request operations |
190
+
|**droppedFields**|`{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when'>; index: integer }[]`| optional | Write-observability (#3407/#3431/#3455/#3794): caller-supplied fields the engine LEGALLY stripped from an operation before it was written — static `readonly` (#2948) or a TRUE `readonlyWhen` predicate (#3042). This endpoint is the console record form's save path (master-detail writes parent + children in one transaction), so without it the ONE surface where a user edits a `readonlyWhen` field reported plain success while the value never landed. Each event carries the `index` of its operation. Present ONLY when ≥1 field was dropped; the batch still committed without them (results/success semantics unchanged). Optional — omit-when-empty keeps the shape backward-compatible. |
0 commit comments