fix: Detect workflow calling itself expression in migration report (#22516)

This commit is contained in:
Tomi Turtiainen
2025-12-01 11:31:32 +02:00
committed by GitHub
parent 82e0733cd4
commit 1ddb6344a5
2 changed files with 43 additions and 6 deletions
@@ -1,13 +1,21 @@
import type { INode } from 'n8n-workflow';
import { createNode, createWorkflow } from '../../../__tests__/test-helpers';
import { BreakingChangeCategory } from '../../../types';
import { WaitNodeSubworkflowRule } from '../wait-node-subworkflow.rule';
class TestRule extends WaitNodeSubworkflowRule {
testExtractCalledWorkflowId(node: INode, callerWorkflowId: string): string | undefined {
return super.extractCalledWorkflowId(node, callerWorkflowId);
}
}
describe('WaitNodeSubworkflowRule', () => {
let rule: WaitNodeSubworkflowRule;
let rule: TestRule;
beforeEach(() => {
jest.clearAllMocks();
rule = new WaitNodeSubworkflowRule();
rule = new TestRule();
rule.reset();
});
@@ -418,4 +426,27 @@ describe('WaitNodeSubworkflowRule', () => {
expect(report2.affectedWorkflows).toHaveLength(0);
});
});
describe('extractCalledWorkflowId()', () => {
it('should extract the called workflow ID', () => {
const node = createNode('ExecuteWorkflow', 'n8n-nodes-base.executeWorkflow', {
source: 'database',
workflowId: { value: 'sub-wf-1' },
});
const calledWorkflowId = rule.testExtractCalledWorkflowId(node, 'caller-wf-1');
expect(calledWorkflowId).toBe('sub-wf-1');
});
it('should return the caller workflow ID if it is an expression', () => {
const node = createNode('ExecuteWorkflow', 'n8n-nodes-base.executeWorkflow', {
workflowId: '={{ $workflow.id }}',
mode: 'each',
options: {},
});
const calledWorkflowId = rule.testExtractCalledWorkflowId(node, 'caller-wf-1');
expect(calledWorkflowId).toBe('caller-wf-1');
});
});
});
@@ -123,7 +123,7 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule
continue; // Skip if not waiting for sub-workflow completion
}
const calledWorkflowId = this.extractCalledWorkflowId(node);
const calledWorkflowId = this.extractCalledWorkflowId(node, workflow.id);
this.parentWorkflowsCalling.push({
parentWorkflowId: workflow.id,
@@ -199,7 +199,7 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule
return waitingNodes;
}
private extractCalledWorkflowId(node: INode): string | undefined {
protected extractCalledWorkflowId(node: INode, callerWorkflowId: string): string | undefined {
const source = node.parameters.source as string | undefined;
// Only handle database source - for other sources we can't determine the workflow ID statically
@@ -213,7 +213,8 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule
if (typeof workflowId === 'string') {
// Check if it's an expression (starts with =)
if (workflowId.startsWith('=')) {
return undefined; // Can't evaluate expressions statically
// Can't evaluate expressions statically
return this.isWorkflowItselfExpression(workflowId) ? callerWorkflowId : undefined;
}
return workflowId;
}
@@ -223,7 +224,8 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule
if (typeof value === 'string') {
// Check if it's an expression (starts with =)
if (value.startsWith('=')) {
return undefined; // Can't evaluate expressions statically
// Can't evaluate expressions statically
return this.isWorkflowItselfExpression(value) ? callerWorkflowId : undefined;
}
return value;
}
@@ -231,4 +233,8 @@ export class WaitNodeSubworkflowRule implements IBreakingChangeBatchWorkflowRule
return undefined;
}
private isWorkflowItselfExpression(workflowIdExpression: string): boolean {
return workflowIdExpression.replace('{{ ', '{{').replace(' }}', '}}') === '={{$workflow.id}}';
}
}