fix(objectql): insert 剥离作用于调用方提交的值,不再连坐抹掉 beforeInsert hook 的写入 (#6339) - #6611
Merged
Merged
Conversation
`stripRuntimeOwnedFields` 在 `beforeInsert` 之后执行,却用入口处的「键名集合」 判断该删什么。hook 一旦写入 runtime-owned 列,这两件事就不是同一回事: `delete result[name]` 删的是执行到剥离时键上的当前值,只要调用方 payload 恰好 也提交了同名键,被删掉的就是 hook 的写入。这是 update 面 #5591 / PR #6343 的 insert 面孪生缺陷,错因完全相同。 实测(真 ObjectQL + 内存驱动,对象 `{ title: text, code: autonumber }`, hook 赋值 `ctx.input.data.code`): - 调用方不提交 `code` ⇒ 落库为 hook 的值(hook 写入存活) - 调用方提交 `code` ⇒ 落库为 `"1"`,序列值(hook 写入被删) 两次调用的差别只有「调用方 payload 里是否碰巧出现同名键」,而前者正是 `runtimeOwnedStripWarning()` 正文对每个 hook 作者的承诺:「A beforeInsert/ beforeUpdate hook does NOT need either — hook-written keys are not caller-supplied」。所以这是把代码拉回它自己写下的契约,不是策略变更。 入口快照改为携带调用方的值,并显式浅拷贝、且提前到 hook 之前取:hook 原地改 `ctx.input.data` 不会篡改「调用方发了什么」的记录。实测 origin/main 上两者本 就不同引用(`applyFieldDefaults` 返回 `{ ...record }`),但它的 `!fields` 提前 返回会原样交回同一引用,`initializeSummaryFields` 也只在播种时复制 —— 拷贝把 这份「三处调用点的巧合」变成本路径自身的不变量。 不放松 #5503:没有 hook 覆写的调用方 seed 依旧被剥,单行与批量两条路径一致, 警告文案、`onFieldsDropped` 事件与 `strictReadonlyWrites` 拒绝均不变; `isSystem` / `preserveAudit` 未触碰。比对用 `Object.is` 而非 `===`,调用方伪造 的 `NaN` 仍被认定为调用方自己的值并剥离。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We
…ert-strip-caller-values
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 8, 2026 07:19
This was referenced Aug 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6339
stripRuntimeOwnedFields在beforeInsert之后执行,却用入口处的「键名集合」判断该删什么。hook 一旦写入 runtime-owned 列,这两件事就不是同一回事:delete result[name]删的是执行到剥离时键上的当前值,只要调用方 payload 恰好也提交了同名键,被删掉的就是 hook 的写入。这是 update 面 #5591 / PR #6343 的 insert 面孪生缺陷,错因完全相同 —— 本 PR 是其机械镜像。关键在于:「调用方不提交该键 ⇒ hook 写入存活」正是
runtimeOwnedStripWarning()正文对每个 hook 作者的承诺:键名集合判据让这句话只在「调用方碰巧没提交同名键」时成立。所以这是把代码拉回它自己写下的契约,不是策略变更。
前提复核(P1-P4,动手前逐条实测)
probe_num2 { title: text, code: autonumber },hook 赋值ctx.input.data.code):PROBE A code = "HOOK-OVERWRITE"/PROBE B code = "1"。#6343 未顺带盖住 insert,premise_still_valid: true9c82b8990:快照构造点engine.tsupdate 终端顶部(hook 之前)、比对点Object.is+hasOwnProperty、非导出签名ReadonlySet< string >→Readonly< Record< string, unknown > >。三处逐项镜像见下表suppliedPerRow(取自opCtx.data)与 hook 原地改的ctx.input.data是否同引用PROBE aliasing A/B: ctx.input.data === callerPayload ? false;插入后调用方对象未被改动({"title":"B","code":"CALLER-FORGED"})。原因:applyFieldDefaults返回{ ...record }(engine.ts:1993)。但这是巧合而非不变量:该函数在!fields提前返回时原样交回同一引用(engine.ts:1988),initializeSummaryFields也只在真正播种时复制。所以快照仍显式浅拷贝、并提前到 hook 之前取 —— 与 update 面 #6343 spread 的理由相同,只是那边拷贝是止血、这边是加固6f6fec72d,已在本分支 base 内;其engine.tshunk 全部落在@@ 2130/2140/2183/2203(seedAutonumber一带),本 PR 落在 5590-5720(insert 剥离一带)。merge origin/main 后git merge-tree --write-tree干净退出与 #6343 的镜像对照
stripReadonlyFieldsstripRuntimeOwnedFieldssuppliedKeys: ReadonlySet< string >→supplied: Readonly< Record< string, unknown > >Object.prototype.hasOwnProperty.call(supplied, name)(不用in,constructor是合法字段名)Object.is(result[name], supplied[name])(不用===,伪造NaN会被===判成「hook 改过」而留存)suppliedValues = { ...opCtx.data },取于 update 终端顶部、hook 之前suppliedPerRow = rows.map(r => ({ ...r })),提前到triggerHooks('beforeInsert')之前isSystem/preserveAudit未触碰autonumber值在所有受支持形态下都是标量,故此限制在本面是理论性的⛔ 未发明第二套形状:判据、拼写、注释结构均照搬 #6343。
反向验证(方向先写死,再实测)
预测(回退为键名集合判据 = 删掉
Object.is一行,保留 own-key 判断):② 类用例转红,①③ 类保持绿。预测清单写在实施前。实测:11 红,与预测逐条一致,无额外红绿。
B (THE REPORT)— 落库回到"1"A and B now agreea hook-overwritten code produces NO warningP3: the caller-value snapshot is NOT the object the hook mutates in placea hook that REPLACES ctx.input.data wholesale(键集仍含code,替换后的值一并被删)BULK: one batch, mixed rowsBULK: a caller-supplied value is never read from the WRONG rowonFieldsDropped: silent for a hook-overwritten codestrictReadonlyWrites refuses the real forge and admits the hook write#6339 > KEEPS a record number a hook OVERWROTE#6339 > warns only for the value it really droppedA (control)/#5503 UNCHANGED/isSystem/preserveAudit/hook can still SEE#5503全组 4 条;#6339的STILL drops when unchanged/NaN/Object.prototype key回退实测输出:
Tests 11 failed | 151 passed (162)。恢复后全绿。测试
新测试落专用文件
packages/objectql/src/engine-insert-runtime-owned-strip.test.ts(15 例,⛔ 未触engine.test.ts),另在rule-validator.test.ts补#6339单测组(5 例)并把既有 4 条#5503用例改成值对象形态(签名变更所致,与 #6343 对 update 单测的改法一致)。覆盖:① A 用例 pin(hook 新增键存活)② B 用例翻正 ③ 调用方值确实被剥 + 警告文案不变 ④ P3 快照不被 hook 原地改污染 ⑤ bulk 多行混合形态(新增/覆写/未覆写/空)+ 跨行取值防串行,以及
onFieldsDropped/strictReadonlyWrites/isSystem/preserveAudit/ hook 仍可读到调用方原值。边界
readonly的豁免、DataProtocol ingress 剥离);patch @objectstack/objectql(用户可见行为变更)。Generated by Claude Code