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
docs(spec,objectql): declare after* hooks fire inside the unit of work (#7477) (#7502)
`afterInsert`/`afterUpdate`/`afterDelete` are dispatched before the enclosing
transaction commits. What that guarantees was never written down, so it is now
declared per the maintainer ruling on #7477 (Option 1): an `after*` hook means
"the write has been requested and will happen unless this unit of work is
undone", not "the write happened" — a hook with side effects outside the engine
is responsible for tolerating a rollback.
Zero behaviour change. The statement lands as JSDoc on `HookEvent` and
`HookEventType` in @objectstack/spec, on `DISPATCHABLE_HOOK_EVENTS`,
`HookHandler` and `triggerHooks` in @objectstack/objectql, and as a new section
on content/docs/automation/hooks.mdx. The existing #7413 pin already asserted
this ordering; its comment now records the ruling instead of leaving the
question open — its assertions are unchanged.
Co-authored-by: os-zhuang <hr@objectstack.ai>
An `after*` hook does **not** mean "the write happened". It means **the write
204
+
has been requested and will happen unless this unit of work is undone**.
205
+
`afterInsert`, `afterUpdate` and `afterDelete` are dispatched *before* the
206
+
enclosing transaction commits, so a later refusal in the same unit can roll the
207
+
row back after your handler has already run.
208
+
209
+
Three ordinary operations put a write inside such a unit:
210
+
211
+
| Operation | What is inside the transaction |
212
+
| :--- | :--- |
213
+
| A by-id `delete()` that cascades to dependent records | Each **cascaded child's**`afterDelete`. The parent's own `afterDelete` runs after that unit closes, so it is unaffected |
214
+
|`batchData` / `deleteManyData` with `atomic: true`| Every member's `after*` — the batch aborts and rolls back on the first failure |
215
+
| Any write you wrapped yourself in `ctx.api.transaction(...)` or `engine.transaction(...)`| Everything in the callback |
216
+
217
+
What this means when you write a handler:
218
+
219
+
-**Effects that go back through the engine are safe.** Writes made with
220
+
`ctx.api.object('x')` join the same transaction and roll back with
221
+
everything else — that is what makes an in-engine audit or projection hook
222
+
correct in the first place.
223
+
-**Effects that leave the engine are yours to make rollback-tolerant.** A
224
+
webhook, a notification, an email, an external search-index update or a file
225
+
deletion has already gone out when the rollback happens, announcing a change
226
+
that did not survive. Make the effect idempotent and reconcilable, or hand it
227
+
to a worker that re-reads the record before acting rather than trusting the
228
+
event on its own.
229
+
230
+
Before hooks carry no such caveat: they run before the write is issued, and
231
+
throwing from one refuses the operation outright.
232
+
233
+
<Callouttype="info">
234
+
This is a deliberate, ruled semantics ([#7477](https://github.com/objectstack-ai/objectstack/issues/7477)),
235
+
not an implementation detail awaiting a fix. Deferring `after*` to commit time
236
+
would move a handler's own `ctx.api` writes *outside* the transaction the write
237
+
ran in, which is a worse guarantee than the one documented here.
238
+
</Callout>
239
+
198
240
## Hook Context
199
241
200
242
`handler` receives a `HookContext` with these fields:
@@ -233,6 +275,9 @@ ctx = {
233
275
- Trigger unbounded cascades of writes
234
276
- Perform heavy/long-running work inline in a hook
235
277
- Mutate `ctx.result` in before hooks (it is only populated for after hooks)
278
+
- Treat an `after*` hook as proof the write committed — it fires inside the
279
+
unit of work, so an un-retractable external effect there can outlive a
280
+
rollback (see [above](#after-hooks-run-inside-the-unit-of-work))
0 commit comments