Skip to content

fix(data): resolve field defaultValues before beforeInsert hook (#2703) - #2748

Merged
os-zhuang merged 1 commit into
mainfrom
claude/github-issue-2703-61c89c
Jul 10, 2026
Merged

fix(data): resolve field defaultValues before beforeInsert hook (#2703)#2748
os-zhuang merged 1 commit into
mainfrom
claude/github-issue-2703-61c89c

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

概述

修复 #2703:字段 defaultValue(含 Field.user({ defaultValue: 'current_user' }))由运行时在用户 beforeInsert hook 之后才解析,导致「从被默认的字段派生第二个字段」的 hook 读到还未填充的 null

根因

packages/objectql/src/engine.tsinsert() 里执行顺序颠倒:

  • triggerHooks('beforeInsert', …) 先跑用户 hook
  • applyFieldDefaults(…) 才回填默认值

于是 sales_person: Field.user({ defaultValue: 'current_user' }) 在 POST 不带该字段时,hook 内读到 sales_person == null,派生的 current_status 被算成 unassigned;显式传值才正确。

改动

applyFieldDefaults(单条 + 批量两条路径)提前到 beforeInsert hook 之前,回填后的数据喂给 hookContext.input.data:

  • 派生 hook 现在读得到默认值(含 current_user)。
  • Hook 仍有最终话语权 —— 它在默认值之后运行,可覆盖/新增任何字段。
  • 默认值只填 undefined 字段,客户端显式传值不受影响;调用方入参对象不再被就地改写。
  • 顺序其余不变:applyAutonumbers / encryptSecretFields / 校验仍在 hook 之后 —— 自增号格式里插值的默认字段照样先解析出来。

为什么这是平台级正确(执行顺序对齐)

关键区分:声明式默认值 ≠ 引擎生成值,成熟平台把二者插在 before-hook 两侧:

  • 声明式/元数据默认值(defaultValuecurrent_user)属于「记录初始化」,在 before 逻辑之前
  • 引擎自有生成值(自增号序列、加密、时间戳)在 before 逻辑之后

各家参照:

  • Salesforce:字段默认值(含 $User.Id,正对应 current_user)在记录实例化时填好,before trigger 的 Trigger.new 已带默认值;Auto-Number 在 save 阶段生成、before trigger 取不到。→ 本 PR 的新顺序(默认值在前、autonumber 在后)与其完全一致
  • ServiceNow:dictionary default 在 initialize() 时套用,before business rule 天然看得到。
  • Rails / Django:模型层默认值在实例化时套用,before_create / pre_save 看得到;仅 DB 级 default 滞后。

之前「默认值晚于 before hook」才是异类;本 PR 把 ObjectQL 挪回标准分层:默认值 → before hook → autonumber/加密 → 校验 → 持久化

Breaking note

beforeInsert hook 再也无法区分「客户端没传字段」与「默认值填的字段」(对声明 defaultValue 的字段,hook 总是看到已解析的默认值)。这与 Salesforce/ServiceNow 行为一致(before 逻辑看到的是已初始化的记录),是本次修复的预期语义。依赖「字段 undefined = 客户端未传」来分支的下游自定义 hook 需改判。

测试

  • 新增回归测试(engine.test.ts),复刻 steedos-labs/os-tianshun-mtc#29:一个 beforeInsert hook 从默认回填的 owner 派生 current_status,断言落库为 assigned
  • @objectstack/objectql 全量 63 文件 / 805 tests 全通过

后续(本 PR 不含)

  • 建议把这条执行顺序沉淀为公开契约(ADR / spec),对齐 issue 里「或至少文档明确顺序」的诉求。
  • 建议后续核对 update 路径的默认值处理是否遵循同样「声明式在前」原则,避免 insert/update 两套顺序。

Closes#2703

@vercel

vercelBot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 10, 2026 7:53am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/s labels Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql.

13 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx(via @objectstack/objectql)
  • content/docs/data-modeling/formulas.mdx(via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx(via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx(via @objectstack/objectql)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/objectql)
  • content/docs/kernel/services.mdx(via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx(via @objectstack/objectql)
  • content/docs/plugins/index.mdx(via @objectstack/objectql)
  • content/docs/plugins/packages.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectos/index.mdx(via @objectstack/objectql)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx(via @objectstack/objectql)
  • content/docs/releases/v9.mdx(via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@baozhoutao

Copy link
Copy Markdown
ContributorAuthor

update 路径核对结论(第二条后续线,未改代码)

按 review 要求核对了 update 路径是否遵循同样「声明式默认值在前」的原则。结论:不存在「insert/update 两套顺序」问题,现状与平台原则一致,update 无需改动。

1. update 根本没有默认值阶段 —— 设计正确,非遗漏

applyFieldDefaults 全引擎只有 insert 一处调用;update/updateMany 不套默认值,也无 upsert/findOrCreate 第三条写入路径。这符合主流平台语义:defaultValue创建时概念(Salesforce 字段默认值 / ServiceNow dictionary default 都只在 insert 套用)。若 update 对未传字段套默认值反而是 bug —— PATCH 只带变更字段,用默认值覆盖用户没动的字段会破坏已有数据。

2. 共享引擎步骤顺序,insert 与 update 一致

步骤insertupdate
默认值回填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.tsapplyToRecord)在 hook 阶段处理,不依赖 defaultValue——与 Salesforce/ServiceNow 把「最后修改人」作为系统审计字段而非默认值的做法一致。

建议(记入后续「执行顺序契约」ADR,本 PR 不含)

  1. 显式写明 defaultValueinsert-only,并说明 update 不套默认值是设计而非缺陷,避免后来者误「补」出 bug。
  2. 记一条已知边界:目前无「更新时声明式赋值」(类似 valueOnUpdate)概念;除审计字段外,更新时自动赋值需自写 beforeUpdate hook。这是功能边界,非缺陷。

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.
@os-zhuang
os-zhuang merged commit ff648ad into mainJul 10, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/github-issue-2703-61c89c branch July 10, 2026 11:33
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/steststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[objectql] 字段 defaultValue 解析晚于 beforeInsert hook,派生字段 hook 读到 null

2 participants

@baozhoutao@os-zhuang