Skip to content

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

Description

@yinlianghui

Part of objectstack-ai/hotcrm#788

现象

suppliedKeys 在 hook 运行之前从调用方 payload 快照,而 stripReadonlyFieldsbeforeUpdate 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 修复前后完全一致(非新引入)。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions