Skip to content

Commit 7f4a8a1

Browse files
os-zhuangclaude
andauthored
fix(lint): flag never-firing record--prefixed trigger tokens incl. record-change (#3427) (#3469)
Generalizes the `flow-trigger-unknown-event` rule to flag any `record-`-prefixed `triggerType` that isn't a valid firing token (`record-{before,after}-{create,insert,update,delete,write}`), not just `record-(before|after)-<bad-op>` typos. This closes the `record-change` trap (the engine routes it to the record-change trigger, which maps it to no hook so it never fires) at `os validate` time, and also covers bad-phase tokens like `record-during-update`. Warning severity, unchanged. Companion objectui change removes `record-change` from the Studio flow picker. Claude-Session: https://claude.ai/code/session_018939yJ413zG3irLzcTtqaa Co-authored-by: Claude <noreply@anthropic.com>
1 parent a749273 commit 7f4a8a1

3 files changed

Lines changed: 54 additions & 36 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/lint": patch
3+
---
4+
5+
fix(lint): flag every never-firing `record-`-prefixed trigger token, incl. `record-change` (#3427)
6+
7+
Generalizes the `flow-trigger-unknown-event` rule: it now flags ANY `record-`-prefixed
8+
`triggerType` that is not a valid firing token
9+
(`record-{before,after}-{create,insert,update,delete,write}`) — not just
10+
`record-(before|after)-<bad-op>` typos. This closes the `record-change` trap: the
11+
engine routes `record-change` ("Record changed (any)") to the record-change trigger,
12+
which maps it to no hook so it never fires — now caught at `os validate` time instead
13+
of only a runtime warn. Also covers bad-phase tokens like `record-during-update`.
14+
Warning severity, unchanged.

packages/lint/src/validate-flow-trigger-readiness.test.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ describe('validateFlowTriggerReadiness', () => {
190190
expect(findings).toHaveLength(1);
191191
expect(findings[0].rule).toBe(FLOW_TRIGGER_UNKNOWN_EVENT);
192192
expect(findings[0].severity).toBe('warning');
193-
expect(findings[0].message).toContain("'updated'");
193+
expect(findings[0].message).toContain("'record-after-updated'");
194194
expect(findings[0].message).toMatch(/never fires/i);
195195
expect(findings[0].path).toBe('flows[0].nodes[0].config.triggerType');
196196
});
@@ -222,13 +222,21 @@ describe('validateFlowTriggerReadiness', () => {
222222
}
223223
});
224224

225-
it('does not flag bare record-<noun> shapes (e.g. record-change) with this rule', () => {
226-
// `record-change` lacks a before/after phase, so it is out of this rule's
227-
// scope (a separate concern); the UNKNOWN_EVENT rule must stay silent on it.
225+
it('flags the phase-less `record-change` bare noun — it never fires', () => {
226+
// `record-change` (once offered by the Studio picker as "Record changed
227+
// (any)") has no before/after phase, so it maps to no hook and never fires.
228228
const flow = recordFlow({ status: 'active' });
229229
(flow.nodes[0] as { config: Record<string, unknown> }).config.triggerType = 'record-change';
230230
const findings = validateFlowTriggerReadiness({ objects: [candidateObject], flows: [flow] });
231-
expect(findings.some((f) => f.rule === FLOW_TRIGGER_UNKNOWN_EVENT)).toBe(false);
231+
expect(findings.map((f) => f.rule)).toEqual([FLOW_TRIGGER_UNKNOWN_EVENT]);
232+
expect(findings[0].message).toContain("'record-change'");
233+
});
234+
235+
it('flags a bad-phase token (record-during-update)', () => {
236+
const flow = recordFlow({ status: 'active' });
237+
(flow.nodes[0] as { config: Record<string, unknown> }).config.triggerType = 'record-during-update';
238+
const findings = validateFlowTriggerReadiness({ objects: [candidateObject], flows: [flow] });
239+
expect(findings.map((f) => f.rule)).toEqual([FLOW_TRIGGER_UNKNOWN_EVENT]);
232240
});
233241

234242
it('does not flag non-record triggerTypes (schedule/api/manual)', () => {

packages/lint/src/validate-flow-trigger-readiness.ts

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,15 @@ export const FLOW_TRIGGER_UNKNOWN_EVENT = 'flow-trigger-unknown-event';
4545
type AnyRec = Record<string, unknown>;
4646

4747
/**
48-
* Recognized record-change lifecycle ops. A start node's `triggerType` fires only
49-
* when it matches `record-(before|after)-<op>` with `op` in this set — the exact
50-
* grammar the record-change trigger's `triggerTypeToHookEvents` maps to ObjectQL
51-
* hooks. `insert` is a synonym for `create`; `write` is the create-OR-update union
52-
* (#3427). Kept in sync with that trigger (one small, stable contract).
48+
* The record-change trigger fires only for a `triggerType` matching this exact
49+
* grammar — the same set its `triggerTypeToHookEvents` maps to ObjectQL hooks.
50+
* `insert` is a synonym for `create`; `write` is the create-OR-update union
51+
* (#3427). Any OTHER `record-`-prefixed token — a typo (`record-after-updated`),
52+
* a phase-less bare noun (`record-change`), or a bad phase (`record-during-update`)
53+
* — binds to the trigger but maps to NO hook and never fires. Kept in sync with
54+
* that trigger (one small, stable contract).
5355
*/
54-
const RECORD_TRIGGER_OPS = new Set(['create', 'insert', 'update', 'delete', 'write']);
55-
/** A record-lifecycle-SHAPED token: `record-before-…` / `record-after-…`. */
56-
const RECORD_EVENT_SHAPE = /^record-(?:before|after)-(.+)$/;
56+
const VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
5757

5858
/** Coerce an array-or-name-keyed-map collection to an array (name injected). */
5959
function asArray(v: unknown): AnyRec[] {
@@ -142,29 +142,25 @@ export function validateFlowTriggerReadiness(stack: AnyRec): FlowTriggerReadines
142142
}
143143
}
144144

145-
// 1c. Record-lifecycle-SHAPED triggerType (`record-before|after-…`) whose op
146-
// is not one the trigger can map — a typo like `record-after-updated`. The
147-
// engine still routes any `record-` token to the record-change trigger,
148-
// which then maps it to NO hook and never fires (only a runtime warn). This
149-
// is a definite never-fire defect, so surface it at authoring time. (Bare
150-
// `record-<noun>` shapes without a before/after phase — e.g. `record-change`
151-
// — are a separate concern and not flagged here.)
152-
if (start && triggerType) {
153-
const shape = RECORD_EVENT_SHAPE.exec(triggerType);
154-
if (shape && !RECORD_TRIGGER_OPS.has(shape[1])) {
155-
findings.push({
156-
severity: 'warning',
157-
rule: FLOW_TRIGGER_UNKNOWN_EVENT,
158-
where: `flow "${flowName}" › start node`,
159-
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
160-
message:
161-
`triggerType '${triggerType}' names an unrecognized lifecycle event '${shape[1]}' — the flow binds to ` +
162-
`the record-change trigger but never fires (the runtime stays silent about it).`,
163-
hint:
164-
`Use record-{before,after}-{create,update,delete,write}. 'write' fires on create OR update in one ` +
165-
`flow (#3427); create/insert are synonyms.`,
166-
});
167-
}
145+
// 1c. A `record-`-prefixed triggerType the trigger cannot map to any hook —
146+
// a typo (`record-after-updated`), a phase-less bare noun (`record-change`,
147+
// which the Studio picker once offered as "Record changed (any)"), or a bad
148+
// phase (`record-during-update`). The engine routes any `record-` token to
149+
// the record-change trigger, which then binds to NO hook and never fires
150+
// (only a runtime warn). Surface the never-fire defect at authoring time.
151+
if (start && isRecordTriggered && !VALID_RECORD_TRIGGER.test((triggerType ?? '').trim())) {
152+
findings.push({
153+
severity: 'warning',
154+
rule: FLOW_TRIGGER_UNKNOWN_EVENT,
155+
where: `flow "${flowName}" › start node`,
156+
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
157+
message:
158+
`triggerType '${triggerType}' is not a recognized record trigger — the flow binds to the ` +
159+
`record-change trigger but never fires (the runtime stays silent about it).`,
160+
hint:
161+
`Use record-{before,after}-{create,update,delete,write}. 'write' fires on create OR update in one ` +
162+
`flow (#3427); create/insert are synonyms. There is no "any change" token — pick the specific event(s).`,
163+
});
168164
}
169165

170166
// 2. Auto-triggered flow whose status is 'draft' — authored or defaulted

0 commit comments

Comments
 (0)