Skip to content

fix(hooks): give ctx.api.update the engine's real (document, options) shape (#616) - #619

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-616-hook-api-update-signature
Aug 2, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-616-hook-api-update-signature

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #616

问题

src/objects/_hook-api.ts 把 update 声明成 (id: string, doc),而运行时注入到 ctx.api 的两种实现(@objectstack/objectql 的 ObjectRepository、@objectstack/runtime 的 buildEngineRepoFacade)都直接转发到 engine.update(object, data, options) —— 第二个位置参数是 options,不是文档。于是钩子侧全部 9 处派生写入在每次调用时都抛:

update('crm_opportunity') does not recognise option 'amount'. The engine executes none of it,
so the call would succeed with the option silently ignored (#4371).

这 9 处都是 onError: 'log',所以唯一的症状是「父记录永远不动」:商机金额不随明细行重算、报价合计不重算、活动完成快照、客户升级、signed_date 盖章、个案服务汇总、报价接受后的商机关闭、任务活动冒泡——全是死的。

关键不在签名写错,而在于这个类型是一份编译器无法校验的手写描述(HookContext.api 是 unknown),所以声明本身就成了事实上的契约:编译器为 9 处调用背书,两个测试替身又照着声明而不是照着引擎实现,于是测试全绿而功能全死。

改动(按 issue 的 contract-first 顺序)

  1. 先改生产者 _hook-api.ts,让它描述真实 API,由编译器把其余调用点全部暴露出来:
    • update(doc: HookUpdateDoc, options: HookUpdateOptions),HookUpdateDoc = Doc & { id: string } —— id 走文档内部(引擎优先读 data.id)。把 id 设成必填,是让旧写法从运行时错误变成编译错误的关键:字符串不是文档。
    • delete({ where });HookUpdateOptions 只开放 where(必填),不开放 multi —— 钩子里的派生写入不该退化成批量写。多余的键由 excess-property check 在调用点拒掉。
    • 删掉 updateMany:两种注入实现都没有这个方法,声明它等于宣传一个运行时并不提供的能力(Prime Directive chore(deps-dev): bump eslint from 8.57.1 to 9.39.2 #10),真调用会 is not a function。
  2. 迁移全部 9 处调用点,统一成仓库里既有的写法 update({ id, …fields }, { where: { id } })(src/actions/contact.actions.ts 一直是这么写的,并已由 test/action-sandbox.test.ts 钉住)——一种写法,不是两种方言。
  3. 收紧测试替身:hook-harness.ts 现在会对「id 当文档传」「文档没有 id」「缺少行作用域」「where.id 与 doc.id 不一致」直接抛错;hooks-runtime.test.ts 里那个更宽松的第二份手写 makeApi(连 filter 都接受)删除,改用同一个 harness。

新测试:断言到达引擎的参数形状

test/hook-write-shape.test.ts。这条是本 issue 的重点——只验证"处理器 resolve 了"或"替身里的行变了"的测试,对着错误签名一样会绿。所以它断言的是引擎收到的参数列表:每个钩子的 shipped body 经由真实的 hookBodyRunnerFactory + QuickJS + 运行时自己的 repo facade 执行,然后逐条检查

  • args.length === 2,第一个参数是对象而非 id;
  • doc.id 等于目标行;
  • args[1] 严格等于 { where: { id } };
  • 存储行确实变了(形状对但没写进去,同样是死的 rollup)。

覆盖 9 处写入全部,并有一条计数断言:src/objects/ 里的 .update( 调用点数量必须等于本文件演练的数量,新增一处而不写用例会红。另有静态扫描:src/objects/** 里 .update( / .delete( 后面第一个非空白字符必须是 {。

反向验证(把 opportunity_line_item.hook.ts 临时改回旧写法):

× opportunity_amount_rollup — the opportunity amount re-rolls from its lines
  SandboxError: hook 'opportunity_amount_rollup' threw: Error: Update requires an ID or options.multi=true
× opportunity_line_item.hook.ts passes a document to update()/delete(), never an id
× (hooks-runtime.test.ts) hook-harness: update("opp1", …) was called with an id where the
  repository facade takes a DOCUMENT …

运行时验证(同一份 seed 的 A/B)

在 clean .objectstack/data 上各启一次 fresh 实例,同为 277 行 seed:

构建 [hook] handler failed
修复前(origin/main 源码重新 build 的 artifact) 96(opportunity_amount_rollup + quote_total_rollup,与 issue 记录一致)
修复后 0

并且真的会动了 —— REST 改一条明细行:

  • 商机 Vertex Enterprise Rollout:明细 quantity 1 → 11,amount 675,000 → 1,425,000(= 预期 +750,000);
  • 报价 presented 状态:明细 quantity 2 → 4,subtotal 500,000 → 550,000、discount_amount → 27,500、total_price → 563,000。

验证

pnpm validate && pnpm typecheck && pnpm lint && pnpm hygiene && pnpm build && pnpm test 全绿;Test Files 36 passed / Tests 745 passed | 1 skipped。build 仍报 all 24 callables are body-only(钩子照旧能纯元数据下发)。

范围说明

🤖 Generated with Claude Code

https://claude.ai/code/session_019SS7C5SXpniKeCApxgARyf


Generated by Claude Code

… shape (#616)

`HookObjectApi.update` was declared `(id: string, doc)` while both surfaces the
runtime can inject as `ctx.api` — ObjectRepository and the sandbox repo facade —
forward to `engine.update(object, data, options)`. The second positional
argument is the OPTIONS bag, so every hook-side derived write threw
"update('crm_opportunity') does not recognise option 'amount'" (#4371) on every
invocation: 96 throws on one boot of a freshly seeded install. All nine call
sites are `onError: 'log'`, so the only symptom was a parent record that never
moved.

The declaration was the contract as far as the compiler was concerned
(`HookContext.api` is `unknown`), and both hook stand-ins implemented the
declaration rather than the engine, so the suite was green throughout.

- `_hook-api.ts` now describes the real surface: `update(HookUpdateDoc,
  HookUpdateOptions)` with the id inside the document, `delete({ where })`, and
  no `updateMany` — a method neither injected shape has.
- Migrated all nine call sites (rollups, campaign snapshot, account promotion,
  signed_date, case rollup, quote close-out, activity bubble).
- `hook-harness.ts` rejects the broken shape, a missing/disagreeing row scope
  and a document without an id, instead of quietly honouring them; the second,
  more permissive ad-hoc stand-in in `hooks-runtime.test.ts` is deleted in
  favour of it.
- New `test/hook-write-shape.test.ts` asserts the ARGUMENT LIST reaching the
  engine for all nine writes, running each hook's shipped body through the real
  QuickJS sandbox, plus a static scan so a future `.update(id, …)` fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SS7C5SXpniKeCApxgARyf
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 2, 2026 12:11pm

Request Review

@github-actions github-actions Bot added ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces backend Server-side behaviour — hooks, flows, actions labels Aug 2, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 2, 2026 12:15
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 2, 2026
Merged via the queue into main with commit 01f084d Aug 2, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Every hook-side api.object(...).update(id, doc) is rejected at runtime — rollups, snapshots and account promotion all no-op

2 participants