From a054761ee21bfebfcd08fee2633afe0aac917247 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 11:00:42 +0000 Subject: [PATCH] =?UTF-8?q?docs(skills,docs):=20=E4=B8=89=E5=A4=84=20hook?= =?UTF-8?q?=20=E6=96=87=E6=A1=A3=E4=B8=8D=E5=86=8D=E6=95=99=E3=80=8C?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=86=99=E7=9A=84=E8=A1=8C=E7=BA=A7=E8=B0=93?= =?UTF-8?q?=E8=AF=8D=E5=9C=A8=20ctx.input.ast=E3=80=8D(#5670)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引擎从未在写路径的 `HookContext` 上放过 AST。`packages/objectql/src/engine.ts` 的 5 个 `HookContext` 生产点里,`input: { ast }` 只出现在两条读路径(喂 `driver.find` / `findOne`);写路径构造的是 `{ id, data, options }` 与 `{ id, options }`,批量写的行级谓词走引擎内部的 `OperationContext.ast`(#2982), 从不进 `hookContext.input`。#5273 已把 spec 侧那份契约表改成引擎真正构造的形状 (PR #5668),真值由新增的 `packages/objectql/src/hook-input-shape-contract.test.ts` 钉死 —— 其中 `beforeUpdate` / `beforeDelete` 在 `multi: true` 上都断言 `'ast' in input === false`,并以 `beforeFind` 作阳性对照证明引擎并非到处不发 `ast`。 同一句从未兑现的陈述还活在三个面上,本单按已落地措辞逐处对齐: - `skills/objectstack-data/rules/hooks.md` - `skills/objectstack-data/references/data-hooks.md` - `content/docs/api/data-flow.mdx` 三处改法一致:删掉「the row-scoping predicate is in `ctx.input.ast`」半句,替换为 两句实情 —— 批量写不向 hook 暴露谓词(谓词在引擎内部 `OperationContext.ast`, composed 的 RLS / sharing 过滤器由此绑定 driver 调用本身,handler 无法放宽;要 限定批量就在调用方用 `options.where`),以及 #5038 之后 `after*` 按匹配行派发、 每行单记录形状、`input.id` 在那里绑定。主句(批量写触发同名事件、没有 `*Many` 事件)本身成立,原样保留。 前两个文件属已发布 skill 目录,沿其既有惯例带 issue 编号;`data-flow.mdx` 全文 零 issue 引用,故那处不引编号。三处都不写仓内 `packages/**` 测试路径 —— 这三份 文件现有引用里没有一条指向仓内源码路径,而 skill 经 `npx skills add` 装到第三方 项目后那种路径也不存在。 ⛔ `content/docs/releases/v16.mdx:185` 带同句但未动:发布态记录按 CLAUDE.md 不在 代码 PR 里改。`packages/spec/**` 未动(#5668 已修)。 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE --- content/docs/api/data-flow.mdx | 2 +- skills/objectstack-data/references/data-hooks.md | 15 ++++++++++----- skills/objectstack-data/rules/hooks.md | 11 ++++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) 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.