|
1 | 1 | # @objectstack/example-showcase |
2 | 2 |
|
| 3 | +## 0.3.14-rc.3 |
| 4 | + |
| 5 | +### Patch Changes |
| 6 | + |
| 7 | +- 3905c00: feat(objectql)!: a predicate bulk write evaluates and fires after-hooks PER ROW (#5038) |
| 8 | + |
| 9 | + The 2026-08-04 maintainer ruling on #4800 / #4862, recorded as ADR-0058's |
| 10 | + bulk-write addendum: **a bulk write is N record changes**, so every record-scoped |
| 11 | + declaration on it is evaluated per row — `record` = that row's state, `previous` = |
| 12 | + that row's pre-write state. Validation predicates have worked this way since |
| 13 | + #3106; hook `condition`s and the record-change flow triggers riding the same |
| 14 | + lifecycle hooks now join them. |
| 15 | + |
| 16 | + **What was broken.** A `multi: true` update reaches `driver.updateMany`, which |
| 17 | + resolves an affected COUNT. The lifecycle hook fired **once**, `previous` was |
| 18 | + never assigned (only the single-id branch fetched a prior row), and `record` |
| 19 | + degraded to the write's bare payload. So the transition condition the docs, the |
| 20 | + formula skill and ten showcase flows all teach — |
| 21 | + `status == "done" && previous.status != "done"` — could not be evaluated on a |
| 22 | + bulk write. Hook conditions rejected the write (#4775/#5037); record-change flow |
| 23 | + triggers were **silent**, firing zero times or once for a record that did not |
| 24 | + exist. A missing audit row is the one failure nobody goes looking for. |
| 25 | + |
| 26 | + **What changed.** The engine's bulk `update` / `delete` branches now read the |
| 27 | + matched row set **once** — the same `driver.find` #3106 already issues, with |
| 28 | + "this object has after-hooks" added to its demand test — and dispatch |
| 29 | + `afterUpdate` / `afterDelete` once per matched row, each on a context with the |
| 30 | + **single-record shape**: `input.id` = the row, `previous` = its pre-image, |
| 31 | + `result` = its state. That is #2922's batch-INSERT ruling restated, and it is why |
| 32 | + this fix has no code in the consumers: `hook-wrappers`' `record`/`previous` |
| 33 | + bindings, the record-change trigger's context builder and plugin-audit's diff all |
| 34 | + read those same fields and became correct at the producer. |
| 35 | + |
| 36 | + - **Per-row dispatch is uniform across after-hooks.** It is deliberately NOT |
| 37 | + keyed on whether a condition mentions `previous` — the ruling rejected that as |
| 38 | + a hidden rule that would make a hook's firing count depend on its condition |
| 39 | + text. |
| 40 | + - **`ctx.result` per row is the ROW**, composed as `row ⊕ payload` from the |
| 41 | + pre-image already in hand, so the batch still costs one extra query, not one |
| 42 | + per row. A bulk DELETE has no post-state: its per-row context sets no `result`, |
| 43 | + and consumers fall back to `previous`. |
| 44 | + - **`onError` needed no new meaning** — it governs a handler on a record-scoped |
| 45 | + context, which is now what it always gets: `abort` fails the operation, `log` |
| 46 | + swallows that row and the batch continues. |
| 47 | + - **A ceiling, enforced as a refusal.** Past 10 000 matched rows a predicate |
| 48 | + write against an object with after-hooks is rejected _before_ the driver call |
| 49 | + (`ERR_BULK_PER_ROW_HOOK_LIMIT`), so nothing is written. It is never downgraded |
| 50 | + to one dispatch for the batch — that would skip the hook for N-1 rows silently. |
| 51 | + |
| 52 | + **Breaking for hook authors, in the direction the contract declares.** An |
| 53 | + after-hook on an object that takes predicate writes now runs once per matched row |
| 54 | + instead of once per batch: a notification hook sends N messages, a |
| 55 | + cache-invalidation hook runs N times. Objects with no after-hooks are untouched |
| 56 | + and pay for no extra read. The write's own contract is unchanged — a predicate |
| 57 | + write still resolves the affected count and still publishes ONE aggregate |
| 58 | + `data.records.updated` (#4639). |
| 59 | + |
| 60 | + **`before*` hooks stay batch-scoped, and that is not a gap.** `beforeUpdate` / |
| 61 | + `beforeDelete` fire once for the whole batch because they may still rewrite the |
| 62 | + payload, and one `updateMany` carries one payload. #5037's `HookConditionError` |
| 63 | + and its `limitation` discriminator therefore **survive, rescoped to that |
| 64 | + dispatch** — with a message that no longer promises an expiry that has already |
| 65 | + happened, names the phase as the reason, and points at the matching `after*` |
| 66 | + event where the same condition evaluates per row as authored. It also now names a |
| 67 | + record-change flow trigger as a real route: #5037 refused to, on measured |
| 68 | + evidence that the trigger shared the same unbound `previous`; that fact changed. |
| 69 | + |
| 70 | + Docs (`data-modeling/formulas.mdx`) and `skills/objectstack-formula` §5 are |
| 71 | + updated to teach one transition shape for both write forms, with the `before*` |
| 72 | + exception called out. |
| 73 | + |
| 74 | +- 9f41ee6: test(e2e,showcase): showcase 的 `apis:` 回迁,并由真实 boot 探针证明它真的在服务(#5040 E8) |
| 75 | + |
| 76 | + #4936 把 showcase 的两条声明式端点注释掉,不是因为它们写错了,而是因为当时整条端点链零执行:没有任何路由为声明的 `path` 挂载,没有匹配器,每一个键 —— 包括 `authRequired` —— 解析通过而不生效。那时候留着它们就是在演示一个运行时不兑现的能力(Prime Directive #10)。 |
| 77 | + |
| 78 | + #5040 的 E1–E7 把执行器建起来、把整面硬拒收窄成逐端点 publish 门之后,那条理由不复存在。本单按**原意**恢复这两条 —— 同名、同 target、同 `authRequired`、同 `cacheTtl` —— 只做 ADR-0121 D1 要求的一处修改:路径迁进本应用的命名空间保留区。 |
| 79 | + |
| 80 | + ``` |
| 81 | + - path: '/api/v1/showcase/tasks' |
| 82 | + + path: '/api/v1/apps/showcase/tasks' |
| 83 | +
|
| 84 | + - path: '/api/v1/showcase/inquiries/purge' |
| 85 | + + path: '/api/v1/apps/showcase/inquiries/purge' |
| 86 | + ``` |
| 87 | + |
| 88 | + 这处修改不是装饰:`manifest.namespace: 'showcase'` 从此是发布的前置条件(声明了 `apis:` 却没有显式 namespace 会被 publish 拒绝),而 `apps/{namespace}/` 这一段让路由归属变成结构性的 —— 没有任何内建域住在 `apps/` 下,两个包也不可能因为 namespace 不同而撞车。 |
| 89 | + |
| 90 | + **匿名面没有增加**:两条历史声明本来就都是 `authRequired: true`,回迁后仍然是。一个例子不该长出它从来没有过的公开面。 |
| 91 | + |
| 92 | + coverage 清单里 `apis` 从 `waived` 翻回 `demonstrated`,理由重写为「由真实 boot 测量」而不是「声明即证明」—— 后者正是 #4936 抓到的那类假覆盖。支撑它的是两份新的真实 boot e2e:showcase 那份走真实 artifact 摄入路径,证明匹配命中执行(find 的 data 与内建 `/data` 路由逐字节相同)、匿名 401、`cacheTtl` 只随成功答案上线、挂载点下未声明路径与挂载点外的裸 404 完全一致、`/meta/api` 与 `/openapi.json` 描述的正是挂载的东西;fixture 那份补上 ADR-0121 D6 的匿名分支 —— 省略 `authRequired` 拒绝匿名、显式 `false` 服务匿名、已装配预算耗尽后 429 且 `Retry-After` 真的在线上。 |
| 93 | + |
| 94 | +- Updated dependencies [29c6c9d] |
| 95 | +- Updated dependencies [d21c001] |
| 96 | +- Updated dependencies [f1cc3a3] |
| 97 | +- Updated dependencies [553a47f] |
| 98 | +- Updated dependencies [2e284b2] |
| 99 | +- Updated dependencies [0161c7f] |
| 100 | +- Updated dependencies [a019e52] |
| 101 | +- Updated dependencies [64fc6d5] |
| 102 | +- Updated dependencies [eaaf03c] |
| 103 | +- Updated dependencies [d17df80] |
| 104 | +- Updated dependencies [7d0e7b5] |
| 105 | +- Updated dependencies [c142ced] |
| 106 | +- Updated dependencies [18b8eaa] |
| 107 | +- Updated dependencies [37e38d1] |
| 108 | +- Updated dependencies [78adc2e] |
| 109 | +- Updated dependencies [0f17114] |
| 110 | +- Updated dependencies [81e2744] |
| 111 | +- Updated dependencies [277eb36] |
| 112 | +- Updated dependencies [41e605e] |
| 113 | +- Updated dependencies [2649ccb] |
| 114 | +- Updated dependencies [a70cd0a] |
| 115 | +- Updated dependencies [c52e608] |
| 116 | +- Updated dependencies [77be690] |
| 117 | +- Updated dependencies [b49ccfd] |
| 118 | +- Updated dependencies [c7406b0] |
| 119 | +- Updated dependencies [546ab3c] |
| 120 | +- Updated dependencies [d9971d3] |
| 121 | +- Updated dependencies [175d789] |
| 122 | +- Updated dependencies [55dbbba] |
| 123 | +- Updated dependencies [471839d] |
| 124 | +- Updated dependencies [26e1029] |
| 125 | +- Updated dependencies [4addd9d] |
| 126 | +- Updated dependencies [108ba8d] |
| 127 | +- Updated dependencies [1203bb2] |
| 128 | +- Updated dependencies [2ddba89] |
| 129 | +- Updated dependencies [1c3da1f] |
| 130 | +- Updated dependencies [4845f85] |
| 131 | +- Updated dependencies [94f7b6a] |
| 132 | +- Updated dependencies [f98fa65] |
| 133 | +- Updated dependencies [c5a5996] |
| 134 | +- Updated dependencies [f61c8cf] |
| 135 | +- Updated dependencies [04fab5e] |
| 136 | +- Updated dependencies [193cd5c] |
| 137 | +- Updated dependencies [5aae790] |
| 138 | +- Updated dependencies [5278e11] |
| 139 | +- Updated dependencies [ba98e26] |
| 140 | +- Updated dependencies [f8cfbb4] |
| 141 | +- Updated dependencies [c89d18c] |
| 142 | +- Updated dependencies [1e6ab15] |
| 143 | +- Updated dependencies [c87ef70] |
| 144 | +- Updated dependencies [3cb0618] |
| 145 | +- Updated dependencies [32a0874] |
| 146 | +- Updated dependencies [7055c22] |
| 147 | +- Updated dependencies [785a748] |
| 148 | +- Updated dependencies [3af0354] |
| 149 | +- Updated dependencies [866ff16] |
| 150 | +- Updated dependencies [5a85e67] |
| 151 | +- Updated dependencies [c183a12] |
| 152 | +- Updated dependencies [8064b07] |
| 153 | +- Updated dependencies [4a56dbd] |
| 154 | +- Updated dependencies [06df4fa] |
| 155 | + - @objectstack/spec@17.0.0-rc.3 |
| 156 | + - @objectstack/runtime@17.0.0-rc.3 |
| 157 | + - @objectstack/driver-sql@17.0.0-rc.3 |
| 158 | + - @objectstack/cloud-connection@17.0.0-rc.3 |
| 159 | + - @objectstack/connector-mcp@17.0.0-rc.3 |
| 160 | + - @objectstack/connector-openapi@17.0.0-rc.3 |
| 161 | + - @objectstack/connector-rest@17.0.0-rc.3 |
| 162 | + - @objectstack/connector-slack@17.0.0-rc.3 |
| 163 | + - @objectstack/service-datasource@17.0.0-rc.3 |
| 164 | + |
3 | 165 | ## 0.3.14-rc.2 |
4 | 166 |
|
5 | 167 | ### Patch Changes |
|
0 commit comments