feat(automation): opt-in single-hop lookup expansion for record-change flow templates (#3475)#3482
Merged
Merged
Conversation
…e flow templates (#3475) A record-change flow can declare `expand: ['<lookup>', …]` on its start-node config so node templates resolve `{record.<lookup>.<field>}` (e.g. `{record.account.name}` — the #3426 lookup gap). The engine re-reads the declared relations AFTER identity resolution, as the run's OWN principal (`resolveRunDataContext` honors `runAs`), and grafts the expanded objects onto the run record. So a `runAs:'user'` run reads the referenced object as the triggering USER — RLS/FLS enforced — not system- elevated, which is exactly what made expansion unsafe in the trigger's re-read (no resolved grants there). New `AutomationEngine.setRecordExpander`, bridged by the plugin to the same data engine the CRUD nodes use. Only declared keys are grafted, so bare lookup ids, `multiple` lookup arrays (#1872), and trigger-hydrated formula fields are untouched. Opt-in ⇒ zero cost otherwise; best-effort ⇒ a re-read failure leaves the record unexpanded and never breaks the flow. The `flow-template-lookup-traversal` lint warning (#3472) is suppressed once a relation is declared in `config.expand`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
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.
Closes #3475 (the lookup half of #3426). Implements the plan posted on #3475, after mapping the automation engine.
What
A record-change flow can now declare which lookup relations to expand, so node templates traverse them:
{record.crm_account.name}now resolves instead of rendering blank.How — expand engine-side, as the run's own identity
The key finding from scoping #3475: the triggering user's grants are resolved engine-side (
resolveRunContext, #3356), and there's already a user-scoped data path (resolveRunDataContext). So expansion belongs in the engine, not the trigger:AutomationEngine.setRecordExpander(fn)— new injectable, mirroringsetUserGrantsResolver. InresolveRunContext, after identity resolution, the engine readsstartNode.config.expand, calls the expander, and grafts the expanded relations onto the run record in place (the same object the run's variable map already references, so every per-node{record.<lookup>.<field>}resolves with no per-node change). Placed inresolveRunContext, it covers bothexecute()andexecuteWithoutRetry().findOne(object, { where: { id }, expand, context: resolveRunDataContext(runContext) })on the same data engine the CRUD nodes use.resolveRunDataContexthonorsrunAs, so:runAs:'user'→ the referenced object is read as the triggering user (RLS/FLS enforced) — closing the leak that made a trigger-side (system-elevated) expand unsafe. The trigger structurally can't do this: its hook session carries onlyuserId, not resolved grants.runAs:'system'→ elevated, as intended.Safety & semantics
multiplelookup arrays ([P2] record-change context does not hydrate multi-lookup (junction) fields #1872), and the formula fields the trigger already hydrated stay untouched. Formula hydration (trigger, system, own-row) and lookup expansion (engine, user-scoped, cross-object) remain separate layers — deliberately not unified, since a whole-record user-scoped re-read would FLS-mask formula fields the trigger returns unmasked.expandis absent. Best-effort ⇒ a re-read failure logs and leaves the record unexpanded (template renders the scalar id); expansion never breaks the flow.configis an open passthrough,findOnealready supportsexpand, andexpandRelatedRecordsinfers the target from the field'sreferenceand applies the referenced object's RLS/FLS.flow-template-lookup-traversal(fix(#3426): build-time warning for unresolvable flow templates + guard the formula re-read #3472) is suppressed once a relation is declared inconfig.expand.Testing
@objectstack/service-automation: 358 tests pass (33 files), including a newrecord-lookup-expand.integration.test.ts(4 cases): user-scoped expansion resolves{record.account.name}and the re-read carries the user context (not system);runAs:'system'reads elevated; only the declared relation is grafted (a non-declared lookup stays a scalar id, [P2] record-change context does not hydrate multi-lookup (junction) fields #1872); a re-read error is fail-safe (flow still succeeds, record unexpanded).tscclean.@objectstack/lint: 323 tests pass; the flow-template rule gains two cases (declaredexpandsuppresses the traversal warning; a non-covered relation still warns).tscclean.Follow-up (optional, out of scope)
Multi-hop expansion, and a typed
StartNodeConfigSchemafor Studio/AI discoverability ofexpand(today it rides the generic passthroughconfig).🤖 Generated with Claude Code