fix(trigger-record-change): 让 record-change 流程种子记录携带公式(formula)字段 (#3426)#3445
Merged
Merged
Conversation
… seeded flow record (#3426) A `formula` field is a read-time virtual — evaluated post-fetch on `find`/`findOne`, never on the write path — so it was absent from the raw after-create/after-update row a record-change flow is seeded with. A notify node template like `{record.full_name}`, or a start condition on the same field, therefore resolved to an empty string, silently producing notifications such as `"New lead to assign: "` with the name missing. The record-change trigger now re-reads the just-written record through the data engine, so the seeded `record` carries the same computed fields a data-API read returns. The fix lives at the trigger — the producer of the flow's `record` — so it benefits the whole flow (start condition, every node, and notify `title`/`body` templates), not just the notify node. Conservative by design: - Only for `afterInsert` / `afterUpdate` (the row exists post-write); `before*` and `afterDelete` keep the raw hook record. - Reads as an elevated system principal, so it only ADDS computed fields and never lets RLS/FLS on the re-read shrink the snapshot the flow already saw. - Raw hook fields win on merge, preserving trigger-time scalars and the #1872 multi-lookup input overlay; the re-read only fills in the formula virtuals. - Any failure (no read surface, no id, a throw, an empty read) falls back to the raw record — hydration never breaks the flow it feeds. Lookup traversal (`{record.account.name}`) is intentionally not hydrated: a default data-API read does not expand relations either, and expanding would turn `record.account` from its scalar FK id into an object, breaking templates that use the bare id (e.g. #1872's `{record.target_channels.0}`). Adds seven focused unit tests plus an end-to-end integration test that boots a real ObjectQL + automation + record-change stack with a formula field and proves `{record.full_name}` resolves in a seeded flow record. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01StfHcpZduPXA6zSAG6beFR
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 1 changed package(s). ✅ |
os-zhuang
marked this pull request as ready for review
July 24, 2026 15:06
os-zhuang
added a commit
that referenced
this pull request
Jul 25, 2026
…+ guard the #3426 re-read (#3426) (#3472) Two follow-ups to #3426, after #3445 closed the formula half. Build-time signal (the issue's fallback ask): `os validate` now flags a record-change flow node whose `{record.<path>}` template can't resolve — the previous SILENT blank becomes an advisory warning. New @objectstack/lint rule `validateFlowTemplatePaths` with two findings: flow-template-unknown-field (a typo) and flow-template-lookup-traversal (a cross-object hop still unsupported). Wired into `os validate` beside the flow-trigger-wiring check. Hydration re-read guards: the trigger-record-change computed-field re-read (#3445) is now skipped when the object declares no formula field (via the engine's optional getObjectConfig) and memoized per write on the shared HookContext, so N flows on one written record share one re-read instead of N. Lookup expansion ({record.account.name}) is intentionally NOT implemented here: scoping shows it needs a read-identity design decision — expanding in the trigger's elevated (system) re-read would bypass the triggering user's RLS/FLS on the referenced object. Left for an ADR; the lint rule flags it unsupported. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
notify节点模板里的{record.full_name}(一个formula公式字段)在record_change流程里被解析成 空字符串,于是收件箱里出现"New lead to assign: "这类缺名字的通知,而且没有任何构建期/运行期信号。同一条记录上的普通标量列({record.company}、{record.first_name})却能正常插值。见 #3426。根因
formula是读取时(read-time)虚拟字段:引擎只在find/findOne取数后用applyFormulaPlan计算它,写入路径不会计算、也不落库。而 record-change 触发器给流程种子的record直接来自生命周期钩子的原始行(ctx.result叠加输入 doc,见 #1872),这一行根本不含公式字段。因此流程上下文里record.full_name是undefined,插值时被渲染为''。这不是消费端(notify 节点)的问题,而是
record这个生产者(触发器)喂进来的数据本身就缺字段——所以修复放在触发器,整条流程(开始条件、所有节点、notify 的title/body)都受益,而不是只给 notify 打补丁。方案
RecordChangeTrigger在组装种子record后,通过数据引擎回读刚写入的记录,让种子record与「用数据 API 读这条记录」拿到的计算字段保持一致。刻意保守:
afterInsert/afterUpdate生效(行已处于写后状态);before*(行尚未落库)与afterDelete(行已删除)保持原始钩子记录不变。findOne、无id、抛错、读到空)都回退到原始记录——补水绝不能弄坏它所服务的流程。取舍:lookup 遍历暂不处理
{record.account.name}这类 lookup 跨表遍历有意不做补水:数据 API 的默认读取本身也不展开关联;而一旦展开,record.account就会从标量外键 id 变成对象,反而破坏那些把它当 id 用的模板/条件(例如 #1872 的{record.target_channels.0})。因此本 PR 让种子记录严格对齐「数据 API 默认读取」的语义——公式字段补齐、关联不展开。lookup 遍历(以及构建期对不可解析路径的告警)仍留在 #3426 跟踪。测试
hydrateComputedFields的单元测试:after-create 补齐公式字段、原始字段优先(触发时刻保真 + [P2] record-change context does not hydrate multi-lookup (junction) fields #1872)、before*/afterDelete/无 id/无findOne时不回读、回读抛错时回退。formula-context.test.ts:用真实 ObjectQL + automation + record-change 栈注册一个公式字段,证明{record.full_name}在种子流程记录里能被解析(修复前会渲染成空)。@objectstack/trigger-record-change(33 tests)与@objectstack/service-automation(347 tests)全绿;类型构建 + lint 通过;附带 changeset。关联 #3426。
🤖 Generated with Claude Code
https://claude.ai/code/session_01StfHcpZduPXA6zSAG6beFR
Generated by Claude Code