Files
n8n-enterprise-unlocked/packages/cli/src/manual-execution.service.ts
autologie b17cbec3af feat(editor): Add ‘execute workflow’ buttons below triggers on the canvas (#12769)
Co-authored-by: Danny Martini <danny@n8n.io>
Co-authored-by: Mutasem Aldmour <mutasem@n8n.io>
2025-02-10 09:14:39 +01:00

135 lines
3.9 KiB
TypeScript

import { Service } from '@n8n/di';
import * as a from 'assert/strict';
import {
DirectedGraph,
filterDisabledNodes,
recreateNodeExecutionStack,
WorkflowExecute,
Logger,
} from 'n8n-core';
import type {
IPinData,
IRun,
IRunExecutionData,
IWorkflowExecuteAdditionalData,
IWorkflowExecutionDataProcess,
Workflow,
} from 'n8n-workflow';
import type PCancelable from 'p-cancelable';
@Service()
export class ManualExecutionService {
constructor(private readonly logger: Logger) {}
getExecutionStartNode(data: IWorkflowExecutionDataProcess, workflow: Workflow) {
let startNode;
// If the user chose a trigger to start from we honor this.
if (data.triggerToStartFrom?.name) {
startNode = workflow.getNode(data.triggerToStartFrom.name) ?? undefined;
}
// Old logic for partial executions v1
if (
data.startNodes?.length === 1 &&
Object.keys(data.pinData ?? {}).includes(data.startNodes[0].name)
) {
startNode = workflow.getNode(data.startNodes[0].name) ?? undefined;
}
return startNode;
}
// eslint-disable-next-line @typescript-eslint/promise-function-async
runManually(
data: IWorkflowExecutionDataProcess,
workflow: Workflow,
additionalData: IWorkflowExecuteAdditionalData,
executionId: string,
pinData?: IPinData,
): PCancelable<IRun> {
if (data.triggerToStartFrom?.data && data.startNodes && !data.destinationNode) {
this.logger.debug(
`Execution ID ${executionId} had triggerToStartFrom. Starting from that trigger.`,
{ executionId },
);
const startNodes = data.startNodes.map((startNode) => {
const node = workflow.getNode(startNode.name);
a.ok(node, `Could not find a node named "${startNode.name}" in the workflow.`);
return node;
});
const runData = { [data.triggerToStartFrom.name]: [data.triggerToStartFrom.data] };
const { nodeExecutionStack, waitingExecution, waitingExecutionSource } =
recreateNodeExecutionStack(
filterDisabledNodes(DirectedGraph.fromWorkflow(workflow)),
new Set(startNodes),
runData,
data.pinData ?? {},
);
const executionData: IRunExecutionData = {
resultData: { runData, pinData },
executionData: {
contextData: {},
metadata: {},
nodeExecutionStack,
waitingExecution,
waitingExecutionSource,
},
};
const workflowExecute = new WorkflowExecute(
additionalData,
data.executionMode,
executionData,
);
return workflowExecute.processRunExecutionData(workflow);
} else if (
data.runData === undefined ||
data.startNodes === undefined ||
data.startNodes.length === 0
) {
// Full Execution
// TODO: When the old partial execution logic is removed this block can
// be removed and the previous one can be merged into
// `workflowExecute.runPartialWorkflow2`.
// Partial executions then require either a destination node from which
// everything else can be derived, or a triggerToStartFrom with
// triggerData.
this.logger.debug(`Execution ID ${executionId} will run executing all nodes.`, {
executionId,
});
// Execute all nodes
const startNode = this.getExecutionStartNode(data, workflow);
// Can execute without webhook so go on
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
return workflowExecute.run(workflow, startNode, data.destinationNode, data.pinData);
} else {
// Partial Execution
this.logger.debug(`Execution ID ${executionId} is a partial execution.`, { executionId });
// Execute only the nodes between start and destination nodes
const workflowExecute = new WorkflowExecute(additionalData, data.executionMode);
if (data.partialExecutionVersion === 2) {
return workflowExecute.runPartialWorkflow2(
workflow,
data.runData,
data.pinData,
data.dirtyNodeNames,
data.destinationNode,
);
} else {
return workflowExecute.runPartialWorkflow(
workflow,
data.runData,
data.startNodes,
data.destinationNode,
data.pinData,
);
}
}
}
}