|
| 1 | +--- |
| 2 | +"@objectstack/trigger-record-change": patch |
| 3 | +--- |
| 4 | + |
| 5 | +fix(trigger-record-change)!: the record handed to a record-change flow no longer aliases the write's payload (#14744) |
| 6 | + |
| 7 | +<!-- adr-0087: not-required (no-migration-prescription) Nothing metadata-shaped moves: no spec key, no Zod schema, no `packages/spec` declaration, no export, no config field and no stored row changes spelling or shape, and a stored flow definition is byte-identical before and after — `objectstack migrate meta` has nothing to reach. What moves is the reference identity of the object one runtime seam hands a flow. The consumer note below names the supported node for writing a record; it prescribes no rewrite of any authored artifact, and the code it could affect is a stack author's own registered function body, which the metadata upgrader cannot see. --> |
| 8 | + |
| 9 | +**BREAKING** for a flow whose `script` node mutates a NESTED value of the |
| 10 | +triggering record IN PLACE: that mutation no longer affects the write the flow |
| 11 | +was triggered by. Shipped as `patch` — this change moves no public surface (no |
| 12 | +exported symbol, no accepted key or value), and under the maintainer's |
| 13 | +2026-09-04 rule (decision batch #35, on #15294) a `fix(` that changes no public |
| 14 | +surface stays `patch`, with breaking-ness carried by this banner and the |
| 15 | +ADR-0087 disposition rather than by the level. Maintainer ruling 2026-09-04 on |
| 16 | +#14744 (decision batch #38, verbatim 「同意」), adopting option A. |
| 17 | + |
| 18 | +**Why.** `buildContext` builds the flow's `record` as a shallow overlay of the |
| 19 | +pre-image, the mutation payload and the after-row. The top-level object was |
| 20 | +new, so a flow ASSIGNING a top-level key reached nothing — but every nested |
| 21 | +value in it was the engine's own object, shared by reference. One of those is |
| 22 | +`ctx.input.data`, and on a `multi: true` update ADR-0058 Addendum II D3 hands |
| 23 | +every per-row context that same payload object, which is the SET clause of the |
| 24 | +single `updateMany`. A registered function doing `record.tags.push(...)` |
| 25 | +therefore wrote the SET clause without assigning any key: every dispatch's |
| 26 | +contribution landed on EVERY matched row, including values derived from another |
| 27 | +row's pre-image, and #14099's key-set refusal could not see it because no key |
| 28 | +was assigned. Measured end to end on the memory driver and on |
| 29 | +`@objectstack/driver-sql` (#15356). |
| 30 | + |
| 31 | +**What changes.** Both flow-facing roots — `record` (and the `params` alias of |
| 32 | +it) and `previous` — are decoupled from the engine's state before the flow |
| 33 | +runs. Arrays, plain objects, `Date`, `RegExp`, `Map` and `Set` are copied; |
| 34 | +primitives, functions and other class instances are shared, which is the |
| 35 | +documented and pinned boundary. A flow still mutates its roots freely and still |
| 36 | +observes its own writes for the rest of the run; those writes simply reach |
| 37 | +nothing outside it. `previous` is decoupled in the same stroke because it is the |
| 38 | +engine's single pre-image object and the same hook context reaches every other |
| 39 | +flow bound to the same write. |
| 40 | + |
| 41 | +**What does NOT change.** The engine's write shape. ADR-0058 Addendum II D3 |
| 42 | +stands untouched: one payload still serves N rows and every per-row context is |
| 43 | +still handed that one object. #14099's key-set refusal is untouched and is not |
| 44 | +widened — a hook that assigns the same key with per-row values still passes it, |
| 45 | +and divergent key sets are still refused whole. Flow metadata with no registered |
| 46 | +function reached nothing before this change and reaches nothing after it: |
| 47 | +assignment nodes write the run's variable map, and `update_record` issues its own |
| 48 | +by-id write. Lookup expansion (`config.expand`) still grafts onto the record the |
| 49 | +flow holds. |
| 50 | + |
| 51 | +**Consumer note.** A flow that relied on an in-place nested mutation to persist |
| 52 | +— which on a by-id write did persist, and on a `multi: true` write corrupted |
| 53 | +every other matched row — writes the record with the `update_record` node |
| 54 | +instead. That node is the supported per-row write and is unaffected by this |
| 55 | +change. |
0 commit comments