Uh oh!
There was an error while loading. Please reload this page.
fix(data): resolve field defaultValues before beforeInsert hook (#2703) - #2748
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
commented
Jul 10, 2026
|
| 步骤 | insert | update |
|---|---|---|
| 默认值回填 | hook 前(本 PR 修复) | —(设计上无) |
| before hook | ② | ① |
| 加密 secret / 多值归一化 / validateRecord / 校验规则 | hook 后 | hook 后 |
| driver 写入 | 最后 | 最后 |
共享步骤都是 before hook → 加密 → 归一化 → 校验 → 写库,完全对齐,无分叉。(stripReadonlyWhenFields 仅 update 出现是合理的——按记录状态锁字段,insert 无前态。)
3. 唯一不对称是固有且正确的
insert 的 hook 现在看到已回填默认值的数据;update 的 hook 看到客户端原样 PATCH(加审计 hook 补的字段)。这不是不一致 bug,而是语义本身决定——update 无默认值可预填,因此 update 的 hook 仍能区分「客户端有没有传某字段」,insert 不能。
4. 「更新时盖当前用户」已覆盖,且不走 defaultValue
updated_by/updated_at/created_by 由内置审计 hook(plugin.ts 的 applyToRecord)在 hook 阶段处理,不依赖 defaultValue——与 Salesforce/ServiceNow 把「最后修改人」作为系统审计字段而非默认值的做法一致。
建议(记入后续「执行顺序契约」ADR,本 PR 不含)
- 显式写明
defaultValue为 insert-only,并说明 update 不套默认值是设计而非缺陷,避免后来者误「补」出 bug。 - 记一条已知边界:目前无「更新时声明式赋值」(类似
valueOnUpdate)概念;除审计字段外,更新时自动赋值需自写beforeUpdatehook。这是功能边界,非缺陷。
Declarative field defaults (incl. the current_user token) were resolved after the beforeInsert hook, so a hook deriving one field from another read a stale null for any field about to be defaulted. Move applyFieldDefaults to record-initialization time, before beforeInsert — matching the standard order of execution (declarative defaults before before-triggers; autonumber/encryption/validation stay after). The hook still runs after and may override defaults; caller input is no longer mutated in place. Adds a regression test reproducing the os-tianshun-mtc#29 scenario.
概述
修复 #2703:字段
defaultValue(含Field.user({ defaultValue: 'current_user' }))由运行时在用户beforeInserthook 之后才解析,导致「从被默认的字段派生第二个字段」的 hook 读到还未填充的null。根因
packages/objectql/src/engine.ts的insert()里执行顺序颠倒:triggerHooks('beforeInsert', …)先跑用户 hookapplyFieldDefaults(…)才回填默认值于是
sales_person: Field.user({ defaultValue: 'current_user' })在 POST 不带该字段时,hook 内读到sales_person == null,派生的current_status被算成unassigned;显式传值才正确。改动
把
applyFieldDefaults(单条 + 批量两条路径)提前到beforeInserthook 之前,回填后的数据喂给hookContext.input.data:current_user)。undefined字段,客户端显式传值不受影响;调用方入参对象不再被就地改写。applyAutonumbers/encryptSecretFields/ 校验仍在 hook 之后 —— 自增号格式里插值的默认字段照样先解析出来。为什么这是平台级正确(执行顺序对齐)
关键区分:声明式默认值 ≠ 引擎生成值,成熟平台把二者插在 before-hook 两侧:
defaultValue、current_user)属于「记录初始化」,在 before 逻辑之前。各家参照:
$User.Id,正对应current_user)在记录实例化时填好,before trigger 的Trigger.new已带默认值;Auto-Number 在 save 阶段生成、before trigger 取不到。→ 本 PR 的新顺序(默认值在前、autonumber 在后)与其完全一致。initialize()时套用,beforebusiness rule 天然看得到。before_create/pre_save看得到;仅 DB 级 default 滞后。之前「默认值晚于 before hook」才是异类;本 PR 把 ObjectQL 挪回标准分层:
默认值 → before hook → autonumber/加密 → 校验 → 持久化。Breaking note
beforeInserthook 再也无法区分「客户端没传字段」与「默认值填的字段」(对声明defaultValue的字段,hook 总是看到已解析的默认值)。这与 Salesforce/ServiceNow 行为一致(before 逻辑看到的是已初始化的记录),是本次修复的预期语义。依赖「字段undefined= 客户端未传」来分支的下游自定义 hook 需改判。测试
engine.test.ts),复刻 steedos-labs/os-tianshun-mtc#29:一个beforeInserthook 从默认回填的owner派生current_status,断言落库为assigned。@objectstack/objectql全量 63 文件 / 805 tests 全通过。后续(本 PR 不含)
update路径的默认值处理是否遵循同样「声明式在前」原则,避免 insert/update 两套顺序。Closes#2703