背景
从 #3356 的「次级问题(可拆单)」拆出。#3356 的主因(runAs:'user' 凭证空心 —— 触发上下文从不携带触发人的权限集/岗位)已由 #3389 修复,随 16.1.0 发布。本单只跟进剩下的可观测性缺口。
现象
update_record 节点无条件返回 success,即使请求写入的字段被服务端丢弃:
// packages/services/service-automation/src/builtin/crud-nodes.ts
const result = await data.update(objectName, fields, { where: filter, context: dataCtx });
return { success: true, output: { result, object: objectName } };
字段丢弃发生在数据层若干处,各自都是合法语义:
数据层确实打了 warn(packages/objectql/src/validation/rule-validator.ts:335),但那条日志落在服务端 logger,不进流程运行的步骤日志。作者看 run trace 只看到一条 3ms 的 success。
这正是 #3356 里两条审批流的 stage 回写整链失效被掩盖的原因:sharingModel: 'public_read_write' 对象上对象级不拦,readonly 镜像字段被静默剥离,节点报 success 而 DB 真值恒空,极难察觉。
需要说明的是:#3356 的授权根因修好之后,「因为零权限集被剥离」这一类已经不再发生;但 readonly / readonlyWhen / FLS 这些语义上正确的剥离依旧静默,所以这条可观测性缺口独立存在,不随 #3389 消失。
期望
update_record(以及同理的 create_record)在「请求写 N 个字段、实际生效 < N」时,至少在步骤结果上带一条 warning,列出被丢弃的字段名。
success 与否可以维持不变(剥离本身是合法语义,不该升级成失败),关键是不要沉默。
设计要点 / 待拍板
需要一个把「被剥离的键」从数据层回传到调用方的通道。今天 IDataEngine.update 的返回类型是 any,节点侧拿不到「哪些键没落」的结构化信息。可选方向:
- 引擎回传:update 结果里附带
droppedFields(或经 options 回填),节点直接读。最干净,但要动 data-engine 契约(packages/spec/src/contracts/data-engine.ts)。
- 节点侧 diff:比对请求的
fields 键集与返回行的实际值。脆 —— 返回行不一定回显全部字段,值相等也可能是巧合。不推荐。
- 日志路由:把数据层已有的 warn 通过 run-scoped logger 路由进流程步骤日志。侵入最小,但依赖 logger 上下文透传。
倾向 1 或 3,需要先拍板再动手。
边界:
runAs: 'system' 走系统上下文本就跳过 readonly 剥离,不受影响;
- 批量 update(
where 匹配多行)的措辞要留意 —— readonlyWhen 已经是「≥1 matched row」语义,不是逐行精确。
参考
背景
从 #3356 的「次级问题(可拆单)」拆出。#3356 的主因(
runAs:'user'凭证空心 —— 触发上下文从不携带触发人的权限集/岗位)已由 #3389 修复,随 16.1.0 发布。本单只跟进剩下的可观测性缺口。现象
update_record节点无条件返回success,即使请求写入的字段被服务端丢弃:字段丢弃发生在数据层若干处,各自都是合法语义:
readonly: true字段的 caller-supplied 写 ——stripReadonlyFields(security: 服务端 readonly 字段在 UPDATE 时未强制(可被用户上下文覆盖) #2948);readonlyWhen条件只读;数据层确实打了 warn(
packages/objectql/src/validation/rule-validator.ts:335),但那条日志落在服务端 logger,不进流程运行的步骤日志。作者看 run trace 只看到一条 3ms 的success。这正是 #3356 里两条审批流的 stage 回写整链失效被掩盖的原因:
sharingModel: 'public_read_write'对象上对象级不拦,readonly 镜像字段被静默剥离,节点报 success 而 DB 真值恒空,极难察觉。需要说明的是:#3356 的授权根因修好之后,「因为零权限集被剥离」这一类已经不再发生;但
readonly/readonlyWhen/ FLS 这些语义上正确的剥离依旧静默,所以这条可观测性缺口独立存在,不随 #3389 消失。期望
update_record(以及同理的create_record)在「请求写 N 个字段、实际生效 < N」时,至少在步骤结果上带一条 warning,列出被丢弃的字段名。success与否可以维持不变(剥离本身是合法语义,不该升级成失败),关键是不要沉默。设计要点 / 待拍板
需要一个把「被剥离的键」从数据层回传到调用方的通道。今天
IDataEngine.update的返回类型是any,节点侧拿不到「哪些键没落」的结构化信息。可选方向:droppedFields(或经 options 回填),节点直接读。最干净,但要动data-engine契约(packages/spec/src/contracts/data-engine.ts)。fields键集与返回行的实际值。脆 —— 返回行不一定回显全部字段,值相等也可能是巧合。不推荐。倾向 1 或 3,需要先拍板再动手。
边界:
runAs: 'system'走系统上下文本就跳过 readonly 剥离,不受影响;where匹配多行)的措辞要留意 ——readonlyWhen已经是「≥1 matched row」语义,不是逐行精确。参考
stripReadonlyFields—packages/objectql/src/validation/rule-validator.ts:299packages/services/service-automation/src/builtin/crud-nodes.ts