diff --git a/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts b/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts index 0de5e3496a..dfac6d7366 100644 --- a/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts +++ b/packages/cli/src/workflows/__tests__/workflow-execution.service.test.ts @@ -1,7 +1,8 @@ import { mock } from 'jest-mock-extended'; -import type { INode, IWorkflowBase, IWorkflowExecuteAdditionalData } from 'n8n-workflow'; +import type { INode, IWorkflowBase, INodeType, IWorkflowExecuteAdditionalData } from 'n8n-workflow'; import type { User } from '@/databases/entities/user'; +import type { NodeTypes } from '@/node-types'; import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data'; import type { WorkflowRunner } from '@/workflow-runner'; import { WorkflowExecutionService } from '@/workflows/workflow-execution.service'; @@ -52,13 +53,14 @@ const hackerNewsNode: INode = { }; describe('WorkflowExecutionService', () => { + const nodeTypes = mock(); const workflowRunner = mock(); const workflowExecutionService = new WorkflowExecutionService( mock(), mock(), mock(), mock(), - mock(), + nodeTypes, mock(), workflowRunner, mock(), @@ -86,7 +88,10 @@ describe('WorkflowExecutionService', () => { const executionId = 'fake-execution-id'; const userId = 'user-id'; const user = mock({ id: userId }); - const runPayload = mock({ startNodes: [] }); + const runPayload = mock({ + startNodes: [], + destinationNode: undefined, + }); workflowRunner.run.mockResolvedValue(executionId); @@ -108,6 +113,41 @@ describe('WorkflowExecutionService', () => { expect(result).toEqual({ executionId }); }); + test('removes runData if the destination node is a trigger', async () => { + const executionId = 'fake-execution-id'; + const userId = 'user-id'; + const user = mock({ id: userId }); + const node = mock(); + const runPayload = mock({ + workflowData: { nodes: [node] }, + startNodes: [], + destinationNode: node.name, + }); + + jest + .spyOn(nodeTypes, 'getByNameAndVersion') + .mockReturnValueOnce(mock({ description: { group: ['trigger'] } })); + + workflowRunner.run.mockResolvedValue(executionId); + + const result = await workflowExecutionService.executeManually(runPayload, user); + + expect(workflowRunner.run).toHaveBeenCalledWith({ + destinationNode: runPayload.destinationNode, + executionMode: 'manual', + runData: undefined, + pinData: runPayload.workflowData.pinData, + pushRef: undefined, + workflowData: runPayload.workflowData, + userId, + partialExecutionVersion: 1, + startNodes: runPayload.startNodes, + dirtyNodeNames: runPayload.dirtyNodeNames, + triggerToStartFrom: runPayload.triggerToStartFrom, + }); + expect(result).toEqual({ executionId }); + }); + [ { name: 'trigger', diff --git a/packages/cli/src/workflows/workflow-execution.service.ts b/packages/cli/src/workflows/workflow-execution.service.ts index 6c9bdbc717..8693c4c643 100644 --- a/packages/cli/src/workflows/workflow-execution.service.ts +++ b/packages/cli/src/workflows/workflow-execution.service.ts @@ -87,6 +87,18 @@ export class WorkflowExecutionService { return await this.workflowRunner.run(runData, true, undefined, undefined, responsePromise); } + private isDestinationNodeATrigger(destinationNode: string, workflow: IWorkflowBase) { + const node = workflow.nodes.find((n) => n.name === destinationNode); + + if (node === undefined) { + return false; + } + + const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); + + return nodeType.description.group.includes('trigger'); + } + async executeManually( { workflowData, @@ -107,6 +119,23 @@ export class WorkflowExecutionService { pinData, ); + // TODO: Reverse the order of events, first find out if the execution is + // partial or full, if it's partial create the execution and run, if it's + // full get the data first and only then create the execution. + // + // If the destination node is a trigger, then per definition this + // is not a partial execution and thus we can ignore the run data. + // If we don't do this we'll end up creating an execution, calling the + // partial execution flow, finding out that we don't have run data to + // create the execution stack and have to cancel the execution, come back + // here and either create the runData (e.g. scheduler trigger) or wait for + // a webhook or event. + if (destinationNode) { + if (this.isDestinationNodeATrigger(destinationNode, workflowData)) { + runData = undefined; + } + } + // if we have a trigger to start from and it's not the pinned trigger // ignore the pinned trigger if (pinnedTrigger && triggerToStartFrom && pinnedTrigger.name !== triggerToStartFrom.name) { @@ -178,7 +207,7 @@ export class WorkflowExecutionService { }, resultData: { pinData, - runData, + runData: runData ?? {}, }, manualData: { userId: data.userId, diff --git a/packages/cli/src/workflows/workflow.request.ts b/packages/cli/src/workflows/workflow.request.ts index f8cb35aee1..300b8018d8 100644 --- a/packages/cli/src/workflows/workflow.request.ts +++ b/packages/cli/src/workflows/workflow.request.ts @@ -26,7 +26,7 @@ export declare namespace WorkflowRequest { type ManualRunPayload = { workflowData: IWorkflowBase; - runData: IRunData; + runData?: IRunData; startNodes?: StartNodeData[]; destinationNode?: string; dirtyNodeNames?: string[];