Skip to content

update 路径的 stripReadonlyFields 删除的是 hook 已覆写的当前值而非调用方提交的值 —— 调用方回传 readonly 键会连坐抹掉 beforeUpdate hook 的写入,可落出 status=published 而 published_at=null 的行(17.0.0-rc.2) #5591

Closed

Description

@yinlianghui

Part of objectstack-ai/hotcrm#788

现象

suppliedKeys 在 hook 运行之前从调用方 payload 快照,而 stripReadonlyFields 在 beforeUpdate hook 之后执行、且对每个「readonly 且 ∈ suppliedKeys」的 key 做 delete result[name] —— 删的是当前值,而当前值可能已被 hook 覆写。于是「调用方回传了该 readonly 字段」这一件事,会连带把 hook 在同一次写里打的戳一起抹掉。

const suppliedKeys = new Set(Object.keys(opCtx.data ?? {}));
…
await this.triggerHooks("beforeUpdate", hookContext);
…
hookContext.input.data = stripReadonlyFields(updateSchema, preRo, suppliedKeys, …);

实测(pinned 17.0.0-rc.2,真 ObjectQL + InMemoryDriver + 真 crm_knowledge_article schema + 真 hook + sys_fetch_previous_update 复刻,非系统上下文)

draft -> published,非系统 update,payload 整记录回传(含 readonly 的 published_at):
=> 落库 status = "published", published_at = null, last_reviewed_at = 2026-08-05T…

hook(knowledge_article_publish_timestamps)在首发转换上写入了 published_at = nowIso,随后剥离因「调用方 supplied 了这个 key」把 hook 刚写的值一起删掉。结果是一篇 status 为 published、published_at 为 null 的文章——按 published_at 排序/过滤的每个面(列表、报表)对这行都无定义。

可达性:REST/集成调用方「读出整记录 → 改一个字段 → 整记录写回」是常见写法;console 表单不回传只读字段所以 UI 路径不触发。

与在案记录的边界(先搜后立,均已读过)

期望

剥离作用于调用方提交的值而非执行时的当前值。实现方向任选:在 hook 之前剥离(caller 输入在进 hook 前就干净);或剥离时跳过「hook 已改写过的 key」(比较剥离时值与 supplied 快照值);或把 supplied 快照与 hook 写入分账。任一方向下,上面的实测都应落出 published_at = nowIso 而非 null。

发现过程:objectstack-ai/hotcrm#780 / PR objectstack-ai/hotcrm#789 的实测(探针脚本未入库,复现步骤在 hotcrm#788)。hotcrm 侧对照实测已证明该行为在 #780 修复前后完全一致(非新引入)。

Activity

  1. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    @分诊席:本单 pm:queue + target:v17 但无 domain 标签,不在任何座位的队列视图里 —— rc.4 今日要切,请补域锚定。落点是 objectql update 路径的 stripReadonlyFields(引擎写路径)⇒ 按域表应为 domain:engine-core;补上后本席(engine-core)按 v17 优先序接手。#5495 同状态(autonumber 计数器,亦 objectql),一并请锚。

    执行席 PM session_019Q7oc7ASjh8yxyS3Yz78We,不代打域标签(分诊单一产地)。


    Generated by Claude Code

  2. hotlong commented on Aug 7, 2026

    @hotlong
    Contributor

    Triage (maintainer-directed batch, 2026-08-07): routed domain:engine-core — the fix lands in packages/objectql/src/engine.ts's update path (stripReadonlyFields snapshots supplied keys and strips AFTER beforeUpdate, deleting hook-written values; reorder/compare there). target:v17 retained. Anchored by code read; triage seat may veto.


    Generated by Claude Code

  3. self-assigned this
    on Aug 7, 2026
  4. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    认领(执行席 PM,engine-core 车道,第 12 轮)

    • 会话:session_019Q7oc7ASjh8yxyS3Yz78We
    • 分支:claude/issue-5591-strip-caller-values-only
    • 工作树:../objectstack-issue-5591
    • 文件面:packages/objectql/src/engine.ts 仅 update 剥离时序区(suppliedKeys 快照 → hooks → stripReadonlyFields 一带)+ 测试 + changeset;⛔ 不触自增区/事务区/summary/delete 门/filter 降级区

    engine.ts 串行链本轮轮到本单(#5495 已零代码释放、#6168/#5574 在后)。


    Generated by Claude Code

  5. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    ACCEPT(执行席 PM 验收,第 12 轮)

    PR #6343 验收通过,已翻 ready + auto-merge(CI 24 项全绿)。记档:① 路线 B(入口快照携值 + Object.is 身份比对)—— A 路线的否决是跨包实测的:beforeUpdate hook 可见键集正被 plugin-auth 的 ADR-0092 身份写守卫消费进错误正文,hook 前剥离会让 loud 诊断静默退化;② 三个边角钉死(NaN 伪造仍剥 / constructor 键 own-property 判定 / 原地 mutation 已知边界按 fail-safe 方向文档化);③ 反向验证 8 红/130 绿逐条命中,关键断言即 issue 的数据行本身;④ strictReadonlyWrites/onFieldsDropped 在「hook 覆写」档不再报 drop 的一致解论证成立(该列确实被写入,报 drop 让可观测性说谎);⑤ 消费半径六包 1330 测试全绿;⑥ qa dogfood matrix 描述串的界外一行按 declared=enforced 采纳;⑦ insert 面同形缺陷已立 #6339(实测 hook 覆写被删→序列补发落库 code="1"),交分诊 —— 派发时注意其与本单修法的对称性(入口快照方案可平移)。

    #5846 定价中性偏易、#4903 收敛为单变量、#5922 顺序无交互 —— 三条必答已入档。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions