mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(core): Run full manual execution when a trigger is executed even if run data exists (#13194)
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { mock } from 'jest-mock-extended';
|
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 { User } from '@/databases/entities/user';
|
||||||
|
import type { NodeTypes } from '@/node-types';
|
||||||
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
|
||||||
import type { WorkflowRunner } from '@/workflow-runner';
|
import type { WorkflowRunner } from '@/workflow-runner';
|
||||||
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
|
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
|
||||||
@@ -52,13 +53,14 @@ const hackerNewsNode: INode = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('WorkflowExecutionService', () => {
|
describe('WorkflowExecutionService', () => {
|
||||||
|
const nodeTypes = mock<NodeTypes>();
|
||||||
const workflowRunner = mock<WorkflowRunner>();
|
const workflowRunner = mock<WorkflowRunner>();
|
||||||
const workflowExecutionService = new WorkflowExecutionService(
|
const workflowExecutionService = new WorkflowExecutionService(
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
mock(),
|
||||||
mock(),
|
nodeTypes,
|
||||||
mock(),
|
mock(),
|
||||||
workflowRunner,
|
workflowRunner,
|
||||||
mock(),
|
mock(),
|
||||||
@@ -86,7 +88,10 @@ describe('WorkflowExecutionService', () => {
|
|||||||
const executionId = 'fake-execution-id';
|
const executionId = 'fake-execution-id';
|
||||||
const userId = 'user-id';
|
const userId = 'user-id';
|
||||||
const user = mock<User>({ id: userId });
|
const user = mock<User>({ id: userId });
|
||||||
const runPayload = mock<WorkflowRequest.ManualRunPayload>({ startNodes: [] });
|
const runPayload = mock<WorkflowRequest.ManualRunPayload>({
|
||||||
|
startNodes: [],
|
||||||
|
destinationNode: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
workflowRunner.run.mockResolvedValue(executionId);
|
workflowRunner.run.mockResolvedValue(executionId);
|
||||||
|
|
||||||
@@ -108,6 +113,41 @@ describe('WorkflowExecutionService', () => {
|
|||||||
expect(result).toEqual({ executionId });
|
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<User>({ id: userId });
|
||||||
|
const node = mock<INode>();
|
||||||
|
const runPayload = mock<WorkflowRequest.ManualRunPayload>({
|
||||||
|
workflowData: { nodes: [node] },
|
||||||
|
startNodes: [],
|
||||||
|
destinationNode: node.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(nodeTypes, 'getByNameAndVersion')
|
||||||
|
.mockReturnValueOnce(mock<INodeType>({ 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',
|
name: 'trigger',
|
||||||
|
|||||||
@@ -87,6 +87,18 @@ export class WorkflowExecutionService {
|
|||||||
return await this.workflowRunner.run(runData, true, undefined, undefined, responsePromise);
|
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(
|
async executeManually(
|
||||||
{
|
{
|
||||||
workflowData,
|
workflowData,
|
||||||
@@ -107,6 +119,23 @@ export class WorkflowExecutionService {
|
|||||||
pinData,
|
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
|
// if we have a trigger to start from and it's not the pinned trigger
|
||||||
// ignore the pinned trigger
|
// ignore the pinned trigger
|
||||||
if (pinnedTrigger && triggerToStartFrom && pinnedTrigger.name !== triggerToStartFrom.name) {
|
if (pinnedTrigger && triggerToStartFrom && pinnedTrigger.name !== triggerToStartFrom.name) {
|
||||||
@@ -178,7 +207,7 @@ export class WorkflowExecutionService {
|
|||||||
},
|
},
|
||||||
resultData: {
|
resultData: {
|
||||||
pinData,
|
pinData,
|
||||||
runData,
|
runData: runData ?? {},
|
||||||
},
|
},
|
||||||
manualData: {
|
manualData: {
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export declare namespace WorkflowRequest {
|
|||||||
|
|
||||||
type ManualRunPayload = {
|
type ManualRunPayload = {
|
||||||
workflowData: IWorkflowBase;
|
workflowData: IWorkflowBase;
|
||||||
runData: IRunData;
|
runData?: IRunData;
|
||||||
startNodes?: StartNodeData[];
|
startNodes?: StartNodeData[];
|
||||||
destinationNode?: string;
|
destinationNode?: string;
|
||||||
dirtyNodeNames?: string[];
|
dirtyNodeNames?: string[];
|
||||||
|
|||||||
Reference in New Issue
Block a user