diff --git a/content/docs/api/data-flow.mdx b/content/docs/api/data-flow.mdx index 80599d7249..f18e8ef9f7 100644 --- a/content/docs/api/data-flow.mdx +++ b/content/docs/api/data-flow.mdx @@ -310,7 +310,7 @@ flowchart TD end ``` -The lifecycle events are defined by the `HookEvent` enum (8 events). Reads fire `beforeFind`/`afterFind` — for **both** `find` and `findOne`, so one subscription covers every read shape. Bulk writes (`multi: true`) fire the **same** `beforeUpdate`/`beforeDelete`/`afterUpdate`/`afterDelete` events as single-id writes, with the row-scoping predicate in `ctx.input.ast`. There are deliberately no per-method (`findOne`/`count`/`aggregate`) or `*Many` events: read authorization and row filtering are RLS/permission-rule concerns, and field masking is field-level metadata. +The lifecycle events are defined by the `HookEvent` enum (8 events). Reads fire `beforeFind`/`afterFind` — for **both** `find` and `findOne`, so one subscription covers every read shape. Bulk writes (`multi: true`) fire the **same** `beforeUpdate`/`beforeDelete`/`afterUpdate`/`afterDelete` events as single-id writes. A bulk write hands hooks **no** row-scoping predicate: it lives on the engine-internal `OperationContext.ast`, so the RLS / sharing filters composed onto it bind the driver call itself, where no handler can widen them — scope a batch through `options.where` at the caller. The `after*` events instead dispatch **once per matched row**, each on a single-record-shaped context whose `input.id` names that row. There are deliberately no per-method (`findOne`/`count`/`aggregate`) or `*Many` events: read authorization and row filtering are RLS/permission-rule concerns, and field masking is field-level metadata. | Hook | Phase | Can Modify? | Can Abort? | |:---|:---|:---|:---| diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index 5eac6ec6dc..6e7a31fd57 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -58,11 +58,16 @@ ObjectStack provides **8 lifecycle events** organized by operation type: > **Why only 8?** The read events fire for `findOne` as well as `find` (the event > attaches to record materialization, not the engine method), so one subscription > covers every read shape — there is no `beforeFindOne`/`afterFindOne`. Likewise the -> write events fire on bulk `multi:true` operations (the row-scoping predicate is in -> `ctx.input.ast`), so there is no `*Many` event. And there is no `beforeCount`/ -> `beforeAggregate`: read authorization and row filtering belong to **RLS / permission -> rules**, and field masking to **field-level metadata** — declarative mechanisms that -> apply everywhere, rather than a hook every author must remember to re-attach. +> write events fire on bulk `multi:true` operations, so there is no `*Many` event. A +> bulk write hands hooks **no** row-scoping predicate: it lives on the engine-internal +> `OperationContext.ast` (#2982), so the RLS / sharing filters composed onto it bind +> the driver call itself, where no handler can widen them — scope a batch through +> `options.where` at the caller. The `after*` events instead dispatch **once per +> matched row**, each on a single-record-shaped context whose `input.id` names that +> row (#5038). And there is no `beforeCount`/`beforeAggregate`: read authorization and +> row filtering belong to **RLS / permission rules**, and field masking to +> **field-level metadata** — declarative mechanisms that apply everywhere, rather than +> a hook every author must remember to re-attach. ### Before vs After Hooks diff --git a/skills/objectstack-data/rules/hooks.md b/skills/objectstack-data/rules/hooks.md index e011dca211..7269a0dc64 100644 --- a/skills/objectstack-data/rules/hooks.md +++ b/skills/objectstack-data/rules/hooks.md @@ -112,9 +112,14 @@ Sandbox essentials (full contract in > **One read event, one write event per kind.** `beforeFind`/`afterFind` fire for > `findOne` too (the event attaches to record materialization, not the method), and -> the write events fire on bulk `multi:true` operations as well — the row-scoping -> predicate is in `ctx.input.ast`. There is no `beforeFindOne`, `beforeCount`, -> `beforeAggregate`, or `*Many` event. +> the write events fire on bulk `multi:true` operations as well. A bulk write hands +> hooks **no** row-scoping predicate: it lives on the engine-internal +> `OperationContext.ast` (#2982), so the RLS / sharing filters composed onto it bind +> the driver call itself, where no handler can widen them — scope a batch through +> `options.where` at the caller. The `after*` events instead dispatch **once per +> matched row**, each on a single-record-shaped context whose `input.id` names that +> row (#5038). There is no `beforeFindOne`, `beforeCount`, `beforeAggregate`, or +> `*Many` event. > > **Don't reach for a hook when a declarative mechanism already fits:** > - Read authorization / row filtering → **RLS / permission rules**, not a `beforeFind` hook.