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(metadata-protocol): `insertManyData` reports the dropped-field union at BATCH level instead of naming rows it cannot identify (#17290)
8
+
9
+
<!-- adr-0087: not-required (no-migration-prescription) nothing authored or stored moves: no authorable key, no Zod schema and no stored `sys_metadata` shape changes — `packages/spec` declares no response schema for this face at all, so `objectstack migrate meta` has nothing to visit, `spec-changes.json` has nothing to project and the upgrade guide gains no row. What moves is one optional member on an inline TypeScript response type of a runtime protocol method, and the channel that reaches every affected consumer is the compiler at their own call site, which names the site more precisely than a ledger line could. The `packages/spec` file in this diff is a `.describe()` STRING — customer-facing prose that this change would otherwise leave false — not a schema, a key or an accept set; nothing it declares moves. -->
10
+
11
+
**BREAKING** — `@objectstack/metadata-protocol`'s `insertManyData` no longer hangs
12
+
`droppedFields` on each entry of `outcomes`; the response itself carries it, beside
13
+
`outcomes`, exactly as `createManyData` already does. A TypeScript consumer that read
14
+
the per-row member stops compiling, and the compiler names the site. The set reported
15
+
is the same set — what is gone is a per-row attribution that could not be computed
16
+
here and was wrong whenever it mattered. Nothing authored or stored changes shape.
17
+
18
+
**What it got wrong.** Every create-side strip is the engine's, and its
19
+
`onFieldsDropped` event is the UNION over the batch — the listener signature
20
+
carries no row index. This seam reconstructed a row set from that union by
21
+
asking which rows SUPPLIED each dropped name
22
+
(`[...engineDropped].filter((f) => f in supplied)`), on the stated premise that
23
+
"the strip only removes keys the ROW ITSELF supplied, so a dropped name belongs
24
+
to exactly the rows whose supplied payload carried it". Maintainer ruling C
25
+
falsifies the premise: the static-`readonly` strip runs INSIDE `engine.insert`,
26
+
AFTER the `beforeInsert` hooks, and exempts keys a hook itself assigned —
27
+
recorded per row (`hookWrittenKeys: rowHookWrittenKeys[i]`). So in a batch where
28
+
a hook stamps a protected key on some rows and not others:
29
+
30
+
- row A supplied `approval_status`, no hook write ⇒ stripped, enters the union;
31
+
- row B supplied `approval_status`, its hook re-assigned it ⇒ **kept and
Copy file name to clipboardExpand all lines: content/docs/references/api/protocol.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -570,7 +570,7 @@ A write-path strip event: caller-supplied fields legally dropped from the payloa
570
570
|**object**|`string`| ✅ | Object name |
571
571
|**records**|`Record<string, any>[]`| ✅ | Created records |
572
572
|**count**|`number`| ✅ | Number of records created |
573
-
| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability: caller-supplied `readonly` fields the in-engine create-side strip (`engine.insert`, `isSystem`-gated) removed before the rows were written. AGGREGATED across the batch (one event per object/reason with the UNION of dropped field names) rather than per-row, because this response is `{ object, records, count }` and has no per-row slot to hang a drop set on — a union is the only view it can represent. So read a name here as "at least one row dropped this field", NOT "every row dropped the same set": the strip runs INSIDE `engine.insert` after the `beforeInsert` hooks and exempts keys a hook itself wrote, tracked per row: rows where a hook stamped a protected key drop a different set from rows where it did not. Present ONLY when ≥1 field was dropped; the creates still succeeded without them (count/success unchanged). Optional — omit-when-empty keeps the shape backward-compatible. (The per-row `insertMany`/`batch` paths carry per-row `droppedFields` on each result instead — see BatchOperationResultSchema.) |
573
+
| **droppedFields** | `{ object: string; fields: string[]; reason: Enum<'readonly' \| 'readonly_when' \| 'primary_key'> }[]` | optional | Write-observability: caller-supplied `readonly` fields the in-engine create-side strip (`engine.insert`, `isSystem`-gated) removed before the rows were written. AGGREGATED across the batch (one event per object/reason with the UNION of dropped field names) rather than per-row, because this response is `{ object, records, count }` and has no per-row slot to hang a drop set on — a union is the only view it can represent. So read a name here as "at least one row dropped this field", NOT "every row dropped the same set": the strip runs INSIDE `engine.insert` after the `beforeInsert` hooks and exempts keys a hook itself wrote, tracked per row: rows where a hook stamped a protected key drop a different set from rows where it did not. Present ONLY when ≥1 field was dropped; the creates still succeeded without them (count/success unchanged). Optional — omit-when-empty keeps the shape backward-compatible. (The same reading applies to the partial-success bulk create, whose batch-level `droppedFields` names no row for the same reason. The paths that DO carry per-row `droppedFields` on each result are the bulk UPDATE and the mixed batch, where each row is its own engine call — see BatchOperationResultSchema.) |
0 commit comments