refactor(core): Split WorkflowExecute.runNode into smaller methods (#17864)

This commit is contained in:
Danny Martini
2025-08-19 10:20:00 +01:00
committed by GitHub
parent 961fc538d7
commit dd55201ee6
3 changed files with 1390 additions and 165 deletions

View File

@@ -3,7 +3,7 @@ import { nodeConfig } from '@n8n/eslint-config/node';
export default defineConfig( export default defineConfig(
nodeConfig, nodeConfig,
globalIgnores(['bin/*.js', 'nodes-testing/*.ts']), globalIgnores(['bin/*.js', 'nodes-testing/*.ts', 'coverage/*']),
{ {
rules: { rules: {
// TODO: Lower the complexity threshold // TODO: Lower the complexity threshold

File diff suppressed because it is too large Load Diff

View File

@@ -1089,23 +1089,10 @@ export class WorkflowExecute {
return customOperation; return customOperation;
} }
/** Executes the given node */ /**
// eslint-disable-next-line complexity * Handles execution of disabled nodes by passing through input data
async runNode( */
workflow: Workflow, private handleDisabledNode(inputData: ITaskDataConnections): IRunNodeResponse {
executionData: IExecuteData,
runExecutionData: IRunExecutionData,
runIndex: number,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
abortSignal?: AbortSignal,
): Promise<IRunNodeResponse> {
const { node } = executionData;
let inputData = executionData.data;
if (node.disabled === true) {
// If node is disabled simply pass the data through
// return NodeRunHelpers.
if (inputData.hasOwnProperty('main') && inputData.main.length > 0) { if (inputData.hasOwnProperty('main') && inputData.main.length > 0) {
// If the node is disabled simply return the data from the first main input // If the node is disabled simply return the data from the first main input
if (inputData.main[0] === null) { if (inputData.main[0] === null) {
@@ -1116,27 +1103,24 @@ export class WorkflowExecute {
return { data: undefined }; return { data: undefined };
} }
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); private prepareConnectionInputData(
workflow: Workflow,
const isDeclarativeNode = nodeType.description.requestDefaults !== undefined; nodeType: INodeType,
customOperation: ReturnType<WorkflowExecute['getCustomOperation']>,
const customOperation = this.getCustomOperation(node, nodeType); inputData: ITaskDataConnections,
): INodeExecutionData[] | null {
let connectionInputData: INodeExecutionData[] = [];
if ( if (
nodeType.execute || nodeType.execute ||
customOperation || customOperation ||
(!nodeType.poll && !nodeType.trigger && !nodeType.webhook) (!nodeType.poll && !nodeType.trigger && !nodeType.webhook)
) { ) {
// Only stop if first input is empty for execute runs. For all others run anyways if (!inputData.main?.length) {
// because then it is a trigger node. As they only pass data through and so the input-data return null;
// becomes output-data it has to be possible.
if (inputData.main?.length > 0) {
// We always use the data of main input and the first input for execute
connectionInputData = inputData.main[0] as INodeExecutionData[];
} }
// We always use the data of main input and the first input for execute
let connectionInputData = inputData.main[0] as INodeExecutionData[];
const forceInputNodeExecution = workflow.settings.executionOrder !== 'v1'; const forceInputNodeExecution = workflow.settings.executionOrder !== 'v1';
if (!forceInputNodeExecution) { if (!forceInputNodeExecution) {
// If the nodes do not get force executed data of some inputs may be missing // If the nodes do not get force executed data of some inputs may be missing
@@ -1150,11 +1134,20 @@ export class WorkflowExecute {
} }
if (connectionInputData.length === 0) { if (connectionInputData.length === 0) {
// No data for node so return return null;
return { data: undefined };
}
} }
return connectionInputData;
}
// For poll, trigger, and webhook nodes, we don't need to process input data
return [];
}
/**
* Handles re-throwing errors from previous node execution attempts
*/
private rethrowLastNodeError(runExecutionData: IRunExecutionData, node: INode): void {
if ( if (
runExecutionData.resultData.lastNodeExecuted === node.name && runExecutionData.resultData.lastNodeExecuted === node.name &&
runExecutionData.resultData.error !== undefined runExecutionData.resultData.error !== undefined
@@ -1173,7 +1166,12 @@ export class WorkflowExecute {
error.stack = runExecutionData.resultData.error.stack; error.stack = runExecutionData.resultData.error.stack;
throw error; throw error;
} }
}
/**
* Handles executeOnce logic by limiting input data to first item only
*/
private handleExecuteOnce(node: INode, inputData: ITaskDataConnections): ITaskDataConnections {
if (node.executeOnce === true) { if (node.executeOnce === true) {
// If node should be executed only once so use only the first input item // If node should be executed only once so use only the first input item
const newInputData: ITaskDataConnections = {}; const newInputData: ITaskDataConnections = {};
@@ -1182,36 +1180,19 @@ export class WorkflowExecute {
return input && input.slice(0, 1); return input && input.slice(0, 1);
}); });
} }
inputData = newInputData; return newInputData;
} }
return inputData;
if (nodeType.execute || customOperation) {
const closeFunctions: CloseFunction[] = [];
const context = new ExecuteContext(
workflow,
node,
additionalData,
mode,
runExecutionData,
runIndex,
connectionInputData,
inputData,
executionData,
closeFunctions,
abortSignal,
);
let data;
if (customOperation) {
data = await customOperation.call(context);
} else if (nodeType.execute) {
data =
nodeType instanceof Node
? await nodeType.execute(context)
: await nodeType.execute.call(context);
} }
/**
* Validates execution data for JSON compatibility and reports issues to Sentry
*/
private reportJsonIncompatibleOutput(
data: INodeExecutionData[][] | null,
workflow: Workflow,
node: INode,
): void {
if (Container.get(GlobalConfig).sentry.backendDsn) { if (Container.get(GlobalConfig).sentry.backendDsn) {
// If data is not json compatible then log it as incorrect output // If data is not json compatible then log it as incorrect output
// Does not block the execution from continuing // Does not block the execution from continuing
@@ -1232,6 +1213,49 @@ export class WorkflowExecute {
}); });
} }
} }
}
private async executeNode(
workflow: Workflow,
node: INode,
nodeType: INodeType,
customOperation: ReturnType<WorkflowExecute['getCustomOperation']>,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
executionData: IExecuteData,
abortSignal?: AbortSignal,
): Promise<IRunNodeResponse> {
const closeFunctions: CloseFunction[] = [];
const context = new ExecuteContext(
workflow,
node,
additionalData,
mode,
runExecutionData,
runIndex,
connectionInputData,
inputData,
executionData,
closeFunctions,
abortSignal,
);
let data: INodeExecutionData[][] | null = null;
if (customOperation) {
data = await customOperation.call(context);
} else if (nodeType.execute) {
data =
nodeType instanceof Node
? await nodeType.execute(context)
: await nodeType.execute.call(context);
}
this.reportJsonIncompatibleOutput(data, workflow, node);
const closeFunctionsResults = await Promise.allSettled( const closeFunctionsResults = await Promise.allSettled(
closeFunctions.map(async (fn) => await fn()), closeFunctions.map(async (fn) => await fn()),
@@ -1252,15 +1276,39 @@ export class WorkflowExecute {
} }
return { data, hints: context.hints }; return { data, hints: context.hints };
} else if (nodeType.poll) { }
/**
* Executes a poll node
*/
private async executePollNode(
workflow: Workflow,
node: INode,
nodeType: INodeType,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
inputData: ITaskDataConnections,
): Promise<IRunNodeResponse> {
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode run the poll function // In manual mode run the poll function
const context = new PollContext(workflow, node, additionalData, mode, 'manual'); const context = new PollContext(workflow, node, additionalData, mode, 'manual');
return { data: await nodeType.poll.call(context) }; return { data: await nodeType.poll!.call(context) };
} }
// In any other mode pass data through as it already contains the result of the poll // In any other mode pass data through as it already contains the result of the poll
return { data: inputData.main as INodeExecutionData[][] }; return { data: inputData.main as INodeExecutionData[][] };
} else if (nodeType.trigger) { }
/**
* Executes a trigger node
*/
private async executeTriggerNode(
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
inputData: ITaskDataConnections,
abortSignal?: AbortSignal,
): Promise<IRunNodeResponse> {
if (mode === 'manual') { if (mode === 'manual') {
// In manual mode start the trigger // In manual mode start the trigger
const triggerResponse = await Container.get(TriggersAndPollers).runTrigger( const triggerResponse = await Container.get(TriggersAndPollers).runTrigger(
@@ -1304,12 +1352,20 @@ export class WorkflowExecute {
} }
// For trigger nodes in any mode except "manual" do we simply pass the data through // For trigger nodes in any mode except "manual" do we simply pass the data through
return { data: inputData.main as INodeExecutionData[][] }; return { data: inputData.main as INodeExecutionData[][] };
} else if (nodeType.webhook && !isDeclarativeNode) { }
// Check if the node have requestDefaults(Declarative Node),
// else for webhook nodes always simply pass the data through private async executeDeclarativeNodeInTest(
// as webhook method would be called by WebhookService workflow: Workflow,
return { data: inputData.main as INodeExecutionData[][] }; node: INode,
} else { nodeType: INodeType,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
runExecutionData: IRunExecutionData,
runIndex: number,
connectionInputData: INodeExecutionData[],
inputData: ITaskDataConnections,
executionData: IExecuteData,
): Promise<IRunNodeResponse> {
// NOTE: This block is only called by nodes tests. // NOTE: This block is only called by nodes tests.
// In the application, declarative nodes get assigned a `.execute` method in NodeTypes. // In the application, declarative nodes get assigned a `.execute` method in NodeTypes.
const context = new ExecuteContext( const context = new ExecuteContext(
@@ -1328,6 +1384,98 @@ export class WorkflowExecute {
const data = await routingNode.runNode(); const data = await routingNode.runNode();
return { data }; return { data };
} }
/**
* Figures out the node type and state and calls the right execution
* implementation.
*/
async runNode(
workflow: Workflow,
executionData: IExecuteData,
runExecutionData: IRunExecutionData,
runIndex: number,
additionalData: IWorkflowExecuteAdditionalData,
mode: WorkflowExecuteMode,
abortSignal?: AbortSignal,
): Promise<IRunNodeResponse> {
const { node } = executionData;
let inputData = executionData.data;
if (node.disabled === true) {
return this.handleDisabledNode(inputData);
}
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
const customOperation = this.getCustomOperation(node, nodeType);
const connectionInputData = this.prepareConnectionInputData(
workflow,
nodeType,
customOperation,
inputData,
);
if (connectionInputData === null) {
return { data: undefined };
}
this.rethrowLastNodeError(runExecutionData, node);
inputData = this.handleExecuteOnce(node, inputData);
const isDeclarativeNode = nodeType.description.requestDefaults !== undefined;
if (nodeType.execute || customOperation) {
return await this.executeNode(
workflow,
node,
nodeType,
customOperation,
additionalData,
mode,
runExecutionData,
runIndex,
connectionInputData,
inputData,
executionData,
abortSignal,
);
}
if (nodeType.poll) {
return await this.executePollNode(workflow, node, nodeType, additionalData, mode, inputData);
}
if (nodeType.trigger) {
return await this.executeTriggerNode(
workflow,
node,
additionalData,
mode,
inputData,
abortSignal,
);
}
if (nodeType.webhook && !isDeclarativeNode) {
// Check if the node have requestDefaults(Declarative Node),
// else for webhook nodes always simply pass the data through
// as webhook method would be called by WebhookService
return { data: inputData.main as INodeExecutionData[][] };
}
return await this.executeDeclarativeNodeInTest(
workflow,
node,
nodeType,
additionalData,
mode,
runExecutionData,
runIndex,
connectionInputData,
inputData,
executionData,
);
} }
/** /**