fix(core): Make node execution order configurable, and backward-compatible (#6507)

* fix(core): Make node execution order configurable, and backward-compatible

*  Also add new Merge-Node behaviour

*  Fix typo

* Fix lint issue

* update labels

* rename legacy to v0

* remove the unnecessary log

* default all new workflows to use v1 execution-order

* remove the controller changes

* clone default settings to avoid it getting modified

---------

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-07-05 18:47:34 +02:00 committed by GitHub
parent f0dfc3cf4e
commit d97edbcffa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 2156 additions and 1042 deletions

View file

@ -338,12 +338,6 @@ export class Start extends BaseCommand {
const editorUrl = GenericHelpers.getBaseUrl(); const editorUrl = GenericHelpers.getBaseUrl();
this.log(`\nEditor is now accessible via:\n${editorUrl}`); this.log(`\nEditor is now accessible via:\n${editorUrl}`);
const saveManualExecutions = config.getEnv('executions.saveDataManualExecutions');
if (saveManualExecutions) {
this.log('\nManual executions will be visible only for the owner');
}
// Allow to open n8n editor by pressing "o" // Allow to open n8n editor by pressing "o"
if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) { if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) {
process.stdin.setRawMode(true); process.stdin.setRawMode(true);

View file

@ -143,24 +143,8 @@ export class WorkflowExecute {
return this.processRunExecutionData(workflow); return this.processRunExecutionData(workflow);
} }
forceInputNodeExecution(workflow: Workflow, node: INode): boolean { forceInputNodeExecution(workflow: Workflow): boolean {
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); return workflow.settings.executionOrder !== 'v1';
// Check if the incoming nodes should be forced to execute
let forceInputNodeExecution = nodeType.description.forceInputNodeExecution;
if (forceInputNodeExecution !== undefined) {
if (typeof forceInputNodeExecution === 'string') {
forceInputNodeExecution = !!workflow.expression.getSimpleParameterValue(
node,
forceInputNodeExecution,
this.mode,
this.additionalData.timezone,
{ $version: node.typeVersion },
);
}
return forceInputNodeExecution;
}
return false;
} }
/** /**
@ -379,6 +363,7 @@ export class WorkflowExecute {
runIndex: number, runIndex: number,
): void { ): void {
let stillDataMissing = false; let stillDataMissing = false;
const enqueueFn = workflow.settings.executionOrder === 'v1' ? 'unshift' : 'push';
let waitingNodeIndex: number | undefined; let waitingNodeIndex: number | undefined;
// Check if node has multiple inputs as then we have to wait for all input data // Check if node has multiple inputs as then we have to wait for all input data
@ -510,7 +495,7 @@ export class WorkflowExecute {
]; ];
} }
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionStackItem); this.runExecutionData.executionData!.nodeExecutionStack[enqueueFn](executionStackItem);
// Remove the data from waiting // Remove the data from waiting
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][ delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][
@ -554,8 +539,7 @@ export class WorkflowExecute {
// are already on the list to be processed. // are already on the list to be processed.
// If that is not the case add it. // If that is not the case add it.
const node = workflow.getNode(connectionData.node); const forceInputNodeExecution = this.forceInputNodeExecution(workflow);
const forceInputNodeExecution = this.forceInputNodeExecution(workflow, node!);
for ( for (
let inputIndex = 0; let inputIndex = 0;
@ -680,7 +664,7 @@ export class WorkflowExecute {
if (addEmptyItem) { if (addEmptyItem) {
// Add only node if it does not have any inputs because else it will // Add only node if it does not have any inputs because else it will
// be added by its input node later anyway. // be added by its input node later anyway.
this.runExecutionData.executionData!.nodeExecutionStack.unshift({ this.runExecutionData.executionData!.nodeExecutionStack[enqueueFn]({
node: workflow.getNode(nodeToAdd) as INode, node: workflow.getNode(nodeToAdd) as INode,
data: { data: {
main: [ main: [
@ -744,7 +728,7 @@ export class WorkflowExecute {
}; };
} else { } else {
// All data is there so add it directly to stack // All data is there so add it directly to stack
this.runExecutionData.executionData!.nodeExecutionStack.unshift({ this.runExecutionData.executionData!.nodeExecutionStack[enqueueFn]({
node: workflow.nodes[connectionData.node], node: workflow.nodes[connectionData.node],
data: { data: {
main: connectionDataArray, main: connectionDataArray,
@ -774,6 +758,7 @@ export class WorkflowExecute {
Logger.verbose('Workflow execution started', { workflowId: workflow.id }); Logger.verbose('Workflow execution started', { workflowId: workflow.id });
const startedAt = new Date(); const startedAt = new Date();
const forceInputNodeExecution = this.forceInputNodeExecution(workflow);
this.status = 'running'; this.status = 'running';
@ -937,8 +922,6 @@ export class WorkflowExecute {
continue; continue;
} }
const node = workflow.getNode(executionNode.name);
// Check if all the data which is needed to run the node is available // Check if all the data which is needed to run the node is available
if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) { if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) {
// Check if the node has incoming connections // Check if the node has incoming connections
@ -971,7 +954,7 @@ export class WorkflowExecute {
continue executionLoop; continue executionLoop;
} }
if (this.forceInputNodeExecution(workflow, node!)) { if (forceInputNodeExecution) {
// Check if it has the data for all the inputs // Check if it has the data for all the inputs
// The most nodes just have one but merge node for example has two and data // The most nodes just have one but merge node for example has two and data
// of both inputs has to be available to be able to process the node. // of both inputs has to be available to be able to process the node.
@ -1295,28 +1278,34 @@ export class WorkflowExecute {
); );
} }
const connectionDestinationNode = workflow.getNode(connectionData.node);
const forceInputNodeExecution = this.forceInputNodeExecution(
workflow,
connectionDestinationNode!,
);
if ( if (
nodeSuccessData![outputIndex] && nodeSuccessData![outputIndex] &&
(nodeSuccessData![outputIndex].length !== 0 || (nodeSuccessData![outputIndex].length !== 0 ||
(connectionData.index > 0 && forceInputNodeExecution)) (connectionData.index > 0 && forceInputNodeExecution))
) { ) {
// Add the node only if it did execute or if connected to second "optional" input // Add the node only if it did execute or if connected to second "optional" input
if (workflow.settings.executionOrder === 'v1') {
const nodeToAdd = workflow.getNode(connectionData.node); const nodeToAdd = workflow.getNode(connectionData.node);
nodesToAdd.push({ nodesToAdd.push({
position: nodeToAdd?.position || [0, 0], position: nodeToAdd?.position || [0, 0],
connection: connectionData, connection: connectionData,
outputIndex: parseInt(outputIndex, 10), outputIndex: parseInt(outputIndex, 10),
}); });
} else {
this.addNodeToBeExecuted(
workflow,
connectionData,
parseInt(outputIndex, 10),
executionNode.name,
nodeSuccessData!,
runIndex,
);
}
} }
} }
} }
if (workflow.settings.executionOrder === 'v1') {
// Always execute the node that is more to the top-left first // Always execute the node that is more to the top-left first
nodesToAdd.sort((a, b) => { nodesToAdd.sort((a, b) => {
if (a.position[1] < b.position[1]) { if (a.position[1] < b.position[1]) {
@ -1345,6 +1334,7 @@ export class WorkflowExecute {
} }
} }
} }
}
// If we got here, it means that we did not stop executing from manual executions / destination. // If we got here, it means that we did not stop executing from manual executions / destination.
// Execute hooks now to make sure that all hooks are executed properly // Execute hooks now to make sure that all hooks are executed properly
@ -1382,7 +1372,10 @@ export class WorkflowExecute {
); );
// Check if the node is only allowed execute if all inputs received data // Check if the node is only allowed execute if all inputs received data
let requiredInputs = nodeType.description.requiredInputs; let requiredInputs =
workflow.settings.executionOrder === 'v1'
? nodeType.description.requiredInputs
: undefined;
if (requiredInputs !== undefined) { if (requiredInputs !== undefined) {
if (typeof requiredInputs === 'string') { if (typeof requiredInputs === 'string') {
requiredInputs = workflow.expression.getSimpleParameterValue( requiredInputs = workflow.expression.getSimpleParameterValue(

View file

@ -4,15 +4,15 @@ import { WorkflowExecute } from '@/WorkflowExecute';
import * as Helpers from './helpers'; import * as Helpers from './helpers';
import { initLogger } from './helpers/utils'; import { initLogger } from './helpers/utils';
import { predefinedWorkflowExecuteTests } from './helpers/constants'; import { legacyWorkflowExecuteTests, v1WorkflowExecuteTests } from './helpers/constants';
describe('WorkflowExecute', () => { describe('WorkflowExecute', () => {
beforeAll(() => { beforeAll(() => {
initLogger(); initLogger();
}); });
describe('run', () => { describe('v0 execution order', () => {
const tests: WorkflowTestData[] = predefinedWorkflowExecuteTests; const tests: WorkflowTestData[] = legacyWorkflowExecuteTests;
const executionMode = 'manual'; const executionMode = 'manual';
const nodeTypes = Helpers.NodeTypes(); const nodeTypes = Helpers.NodeTypes();
@ -25,6 +25,9 @@ describe('WorkflowExecute', () => {
connections: testData.input.workflowData.connections, connections: testData.input.workflowData.connections,
active: false, active: false,
nodeTypes, nodeTypes,
settings: {
executionOrder: 'v0',
},
}); });
const waitPromise = await createDeferredPromise<IRun>(); const waitPromise = await createDeferredPromise<IRun>();
@ -71,6 +74,70 @@ describe('WorkflowExecute', () => {
} }
}); });
describe('v1 execution order', () => {
const tests: WorkflowTestData[] = v1WorkflowExecuteTests;
const executionMode = 'manual';
const nodeTypes = Helpers.NodeTypes();
for (const testData of tests) {
test(testData.description, async () => {
const workflowInstance = new Workflow({
id: 'test',
nodes: testData.input.workflowData.nodes,
connections: testData.input.workflowData.connections,
active: false,
nodeTypes,
settings: {
executionOrder: 'v1',
},
});
const waitPromise = await createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = [];
const additionalData = Helpers.WorkflowExecuteAdditionalData(
waitPromise,
nodeExecutionOrder,
);
const workflowExecute = new WorkflowExecute(additionalData, executionMode);
const executionData = await workflowExecute.run(workflowInstance);
const result = await waitPromise.promise();
// Check if the data from WorkflowExecute is identical to data received
// by the webhooks
expect(executionData).toEqual(result);
// Check if the output data of the nodes is correct
for (const nodeName of Object.keys(testData.output.nodeData)) {
if (result.data.resultData.runData[nodeName] === undefined) {
throw new Error(`Data for node "${nodeName}" is missing!`);
}
const resultData = result.data.resultData.runData[nodeName].map((nodeData) => {
if (nodeData.data === undefined) {
return null;
}
return nodeData.data.main[0]!.map((entry) => entry.json);
});
// expect(resultData).toEqual(testData.output.nodeData[nodeName]);
expect(resultData).toEqual(testData.output.nodeData[nodeName]);
}
// Check if the nodes did execute in the correct order
expect(nodeExecutionOrder).toEqual(testData.output.nodeExecutionOrder);
// Check if other data has correct value
expect(result.finished).toEqual(true);
expect(result.data.executionData!.contextData).toEqual({});
expect(result.data.executionData!.nodeExecutionStack).toEqual([]);
});
}
});
//run tests on json files from specified directory, default 'workflows' //run tests on json files from specified directory, default 'workflows'
//workflows must have pinned data that would be used to test output after execution //workflows must have pinned data that would be used to test output after execution
describe('run test workflows', () => { describe('run test workflows', () => {
@ -87,6 +154,7 @@ describe('WorkflowExecute', () => {
connections: testData.input.workflowData.connections, connections: testData.input.workflowData.connections,
active: false, active: false,
nodeTypes, nodeTypes,
settings: testData.input.workflowData.settings,
}); });
const waitPromise = await createDeferredPromise<IRun>(); const waitPromise = await createDeferredPromise<IRun>();

File diff suppressed because it is too large Load diff

View file

@ -702,6 +702,7 @@ export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
maxExecutionTimeout?: number; maxExecutionTimeout?: number;
callerIds?: string; callerIds?: string;
callerPolicy?: WorkflowSettings.CallerPolicy; callerPolicy?: WorkflowSettings.CallerPolicy;
executionOrder: NonNullable<IWorkflowSettingsWorkflow['executionOrder']>;
} }
export interface ITimeoutHMS { export interface ITimeoutHMS {

View file

@ -7,6 +7,7 @@ export async function getNewWorkflow(context: IRestApiContext, name?: string) {
return { return {
name: response.name, name: response.name,
onboardingFlowEnabled: response.onboardingFlowEnabled === true, onboardingFlowEnabled: response.onboardingFlowEnabled === true,
settings: response.defaultSettings,
}; };
} }

View file

@ -13,6 +13,31 @@
> >
<template #content> <template #content>
<div v-loading="isLoading" class="workflow-settings" data-test-id="workflow-settings-dialog"> <div v-loading="isLoading" class="workflow-settings" data-test-id="workflow-settings-dialog">
<el-row>
<el-col :span="10" class="setting-name">
{{ $locale.baseText('workflowSettings.executionOrder') + ':' }}
</el-col>
<el-col :span="14" class="ignore-key-press">
<n8n-select
v-model="workflowSettings.executionOrder"
placeholder="Select Execution Order"
size="medium"
filterable
:disabled="readOnlyEnv"
:limit-popper-width="true"
data-test-id="workflow-settings-execution-order"
>
<n8n-option
v-for="option in executionOrderOptions"
:key="option.key"
:label="option.value"
:value="option.key"
>
</n8n-option>
</n8n-select>
</el-col>
</el-row>
<el-row> <el-row>
<el-col :span="10" class="setting-name"> <el-col :span="10" class="setting-name">
{{ $locale.baseText('workflowSettings.errorWorkflow') + ':' }} {{ $locale.baseText('workflowSettings.errorWorkflow') + ':' }}
@ -421,9 +446,14 @@ export default defineComponent({
saveDataSuccessExecutionOptions: [] as Array<{ key: string; value: string }>, saveDataSuccessExecutionOptions: [] as Array<{ key: string; value: string }>,
saveExecutionProgressOptions: [] as Array<{ key: string | boolean; value: string }>, saveExecutionProgressOptions: [] as Array<{ key: string | boolean; value: string }>,
saveManualOptions: [] as Array<{ key: string | boolean; value: string }>, saveManualOptions: [] as Array<{ key: string | boolean; value: string }>,
executionOrderOptions: [
{ key: 'v0', value: 'v0 (legacy)' },
{ key: 'v1', value: 'v1 (recommended)' },
] as Array<{ key: string; value: string }>,
timezones: [] as Array<{ key: string; value: string }>, timezones: [] as Array<{ key: string; value: string }>,
workflowSettings: {} as IWorkflowSettings, workflowSettings: {} as IWorkflowSettings,
workflows: [] as IWorkflowShortResponse[], workflows: [] as IWorkflowShortResponse[],
executionOrder: 'v0',
executionTimeout: 0, executionTimeout: 0,
maxExecutionTimeout: 0, maxExecutionTimeout: 0,
timeoutHMS: { hours: 0, minutes: 0, seconds: 0 } as ITimeoutHMS, timeoutHMS: { hours: 0, minutes: 0, seconds: 0 } as ITimeoutHMS,
@ -535,6 +565,9 @@ export default defineComponent({
if (workflowSettings.maxExecutionTimeout === undefined) { if (workflowSettings.maxExecutionTimeout === undefined) {
workflowSettings.maxExecutionTimeout = this.rootStore.maxExecutionTimeout; workflowSettings.maxExecutionTimeout = this.rootStore.maxExecutionTimeout;
} }
if (workflowSettings.executionOrder === undefined) {
workflowSettings.executionOrder = 'v0';
}
this.workflowSettings = workflowSettings; this.workflowSettings = workflowSettings;
this.timeoutHMS = this.convertToHMS(workflowSettings.executionTimeout); this.timeoutHMS = this.convertToHMS(workflowSettings.executionTimeout);

View file

@ -1595,6 +1595,7 @@
"workflowSettings.defaultTimezone": "Default - {defaultTimezoneValue}", "workflowSettings.defaultTimezone": "Default - {defaultTimezoneValue}",
"workflowSettings.defaultTimezoneNotValid": "Default Timezone not valid", "workflowSettings.defaultTimezoneNotValid": "Default Timezone not valid",
"workflowSettings.errorWorkflow": "Error Workflow", "workflowSettings.errorWorkflow": "Error Workflow",
"workflowSettings.executionOrder": "Execution Order",
"workflowSettings.helpTexts.errorWorkflow": "A second workflow to run if the current one fails.<br />The second workflow should an 'Error Trigger' node.", "workflowSettings.helpTexts.errorWorkflow": "A second workflow to run if the current one fails.<br />The second workflow should an 'Error Trigger' node.",
"workflowSettings.helpTexts.executionTimeout": "How long the workflow should wait before timing out", "workflowSettings.helpTexts.executionTimeout": "How long the workflow should wait before timing out",
"workflowSettings.helpTexts.executionTimeoutToggle": "Whether to cancel workflow execution after a defined time", "workflowSettings.helpTexts.executionTimeoutToggle": "Whether to cancel workflow execution after a defined time",

View file

@ -82,24 +82,29 @@ import {
} from '@/utils'; } from '@/utils';
import { useNDVStore } from './ndv.store'; import { useNDVStore } from './ndv.store';
import { useNodeTypesStore } from './nodeTypes.store'; import { useNodeTypesStore } from './nodeTypes.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import type { NodeMetadataMap } from '@/Interface'; import type { NodeMetadataMap } from '@/Interface';
const createEmptyWorkflow = (): IWorkflowDb => ({ const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
id: PLACEHOLDER_EMPTY_WORKFLOW_ID,
name: '', name: '',
active: false, active: false,
createdAt: -1, createdAt: -1,
updatedAt: -1, updatedAt: -1,
connections: {}, connections: {},
nodes: [], nodes: [],
settings: {}, settings: {
executionOrder: 'v1',
},
tags: [], tags: [],
pinData: {}, pinData: {},
versionId: '', versionId: '',
usedCredentials: [], usedCredentials: [],
};
const createEmptyWorkflow = (): IWorkflowDb => ({
id: PLACEHOLDER_EMPTY_WORKFLOW_ID,
...defaults,
}); });
let cachedWorkflowKey: string | null = ''; let cachedWorkflowKey: string | null = '';
@ -135,10 +140,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
return this.workflow.versionId; return this.workflow.versionId;
}, },
workflowSettings(): IWorkflowSettings { workflowSettings(): IWorkflowSettings {
if (this.workflow.settings === undefined) { return this.workflow.settings ?? { ...defaults.settings };
return {};
}
return this.workflow.settings;
}, },
workflowTags(): string[] { workflowTags(): string[] {
return this.workflow.tags as string[]; return this.workflow.tags as string[];
@ -318,7 +320,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
// This has the advantage that it is very fast and does not cause problems with vuex // This has the advantage that it is very fast and does not cause problems with vuex
// when the workflow replaces the node-parameters. // when the workflow replaces the node-parameters.
getNodes(): INodeUi[] { getNodes(): INodeUi[] {
const nodes = useWorkflowsStore().allNodes; const nodes = this.allNodes;
const returnNodes: INodeUi[] = []; const returnNodes: INodeUi[] = [];
for (const node of nodes) { for (const node of nodes) {
@ -331,23 +333,21 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
// Returns a workflow instance. // Returns a workflow instance.
getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow {
const nodeTypes = this.getNodeTypes(); const nodeTypes = this.getNodeTypes();
let workflowId: string | undefined = useWorkflowsStore().workflowId; let workflowId: string | undefined = this.workflowId;
if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
workflowId = undefined; workflowId = undefined;
} }
const workflowName = useWorkflowsStore().workflowName;
cachedWorkflow = new Workflow({ cachedWorkflow = new Workflow({
id: workflowId, id: workflowId,
name: workflowName, name: this.workflowName,
nodes: copyData ? deepCopy(nodes) : nodes, nodes: copyData ? deepCopy(nodes) : nodes,
connections: copyData ? deepCopy(connections) : connections, connections: copyData ? deepCopy(connections) : connections,
active: false, active: false,
nodeTypes, nodeTypes,
settings: useWorkflowsStore().workflowSettings, settings: this.workflowSettings,
// @ts-ignore // @ts-ignore
pinData: useWorkflowsStore().getPinData, pinData: this.getPinData,
}); });
return cachedWorkflow; return cachedWorkflow;
@ -393,11 +393,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
}, },
async getNewWorkflowData(name?: string): Promise<INewWorkflowData> { async getNewWorkflowData(name?: string): Promise<INewWorkflowData> {
const workflowsEEStore = useWorkflowsEEStore();
let workflowData = { let workflowData = {
name: '', name: '',
onboardingFlowEnabled: false, onboardingFlowEnabled: false,
settings: { ...defaults.settings },
}; };
try { try {
const rootStore = useRootStore(); const rootStore = useRootStore();
@ -426,6 +425,25 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
} }
}, },
resetState(): void {
this.removeAllConnections({ setStateDirty: false });
this.removeAllNodes({ setStateDirty: false, removePinData: true });
// Reset workflow execution data
this.setWorkflowExecutionData(null);
this.resetAllNodesIssues();
this.setActive(defaults.active);
this.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
this.setWorkflowName({ newName: '', setStateDirty: false });
this.setWorkflowSettings({ ...defaults.settings });
this.setWorkflowTagIds([]);
this.activeExecutionId = null;
this.executingNode = null;
this.executionWaitingForWebhook = false;
},
setWorkflowId(id: string): void { setWorkflowId(id: string): void {
this.workflow.id = id === 'new' ? PLACEHOLDER_EMPTY_WORKFLOW_ID : id; this.workflow.id = id === 'new' ? PLACEHOLDER_EMPTY_WORKFLOW_ID : id;
}, },
@ -632,7 +650,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
...(!this.workflow.hasOwnProperty('updatedAt') ? { updatedAt: -1 } : {}), ...(!this.workflow.hasOwnProperty('updatedAt') ? { updatedAt: -1 } : {}),
...(!this.workflow.hasOwnProperty('id') ? { id: PLACEHOLDER_EMPTY_WORKFLOW_ID } : {}), ...(!this.workflow.hasOwnProperty('id') ? { id: PLACEHOLDER_EMPTY_WORKFLOW_ID } : {}),
...(!this.workflow.hasOwnProperty('nodes') ? { nodes: [] } : {}), ...(!this.workflow.hasOwnProperty('nodes') ? { nodes: [] } : {}),
...(!this.workflow.hasOwnProperty('settings') ? { settings: {} } : {}), ...(!this.workflow.hasOwnProperty('settings')
? { settings: { ...defaults.settings } }
: {}),
}; };
}, },

View file

@ -3523,22 +3523,7 @@ export default defineComponent({
// Ignore all errors // Ignore all errors
}); });
} }
this.workflowsStore.removeAllConnections({ setStateDirty: false }); this.workflowsStore.resetState();
this.workflowsStore.removeAllNodes({ setStateDirty: false, removePinData: true });
// Reset workflow execution data
this.workflowsStore.setWorkflowExecutionData(null);
this.workflowsStore.resetAllNodesIssues();
this.workflowsStore.setActive(false);
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
this.workflowsStore.setWorkflowName({ newName: '', setStateDirty: false });
this.workflowsStore.setWorkflowSettings({});
this.workflowsStore.setWorkflowTagIds([]);
this.workflowsStore.activeExecutionId = null;
this.workflowsStore.executingNode = null;
this.workflowsStore.executionWaitingForWebhook = false;
this.uiStore.removeActiveAction('workflowRunning'); this.uiStore.removeActiveAction('workflowRunning');
this.uiStore.resetSelectedNodes(); this.uiStore.resetSelectedNodes();

View file

@ -24,8 +24,7 @@ export class CompareDatasets implements INodeType {
// eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node // eslint-disable-next-line n8n-nodes-base/node-class-description-inputs-wrong-regular-node
inputs: ['main', 'main'], inputs: ['main', 'main'],
inputNames: ['Input A', 'Input B'], inputNames: ['Input A', 'Input B'],
forceInputNodeExecution: '={{ $version < 2.3 }}', requiredInputs: 1,
requiredInputs: '={{ $version < 2.3 ? undefined : 1 }}',
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong // eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
outputs: ['main', 'main', 'main', 'main'], outputs: ['main', 'main', 'main', 'main'],
outputNames: ['In A only', 'Same', 'Different', 'In B only'], outputNames: ['In A only', 'Same', 'Different', 'In B only'],

View file

@ -1,23 +1,23 @@
{ {
"name": "Compare Datasets Node Test", "name": "My workflow",
"nodes": [ "nodes": [
{ {
"parameters": {}, "parameters": {},
"id": "0312bddf-aae0-423c-9041-d54fb124934f", "id": "4013708d-2460-44b7-923d-e3cd36eb9287",
"name": "When clicking \"Execute Workflow\"", "name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger", "type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1, "typeVersion": 1,
"position": [480, 720] "position": [-6660, 8040]
}, },
{ {
"parameters": { "parameters": {
"jsCode": "return [\n {\n json: {\n number: 0\n }\n },\n {\n json: {\n number: 1\n }\n },\n {\n json: {\n number: 2\n }\n }\n];" "jsCode": "return [\n {\n json: {\n number: 0\n }\n },\n {\n json: {\n number: 1\n }\n },\n {\n json: {\n number: 2\n }\n }\n];"
}, },
"id": "0542886d-6ab2-4695-b686-2cd60729ba9a", "id": "be417cbe-7a4f-40eb-8a26-fca253a01882",
"name": "Code", "name": "Code",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 1, "typeVersion": 1,
"position": [900, 640] "position": [-6240, 8040]
}, },
{ {
"parameters": { "parameters": {
@ -31,173 +31,53 @@
}, },
"options": {} "options": {}
}, },
"id": "f3e5e43b-a3bf-46c7-acd7-ae3d7d19d9f9", "id": "97d65223-16c9-4d26-b31e-d818e1adbc8a",
"name": "Compare Datasets 2.2 - Old", "name": "Compare Datasets 2.2 - Old",
"type": "n8n-nodes-base.compareDatasets", "type": "n8n-nodes-base.compareDatasets",
"typeVersion": 2.2, "typeVersion": 2.2,
"position": [1260, 40] "position": [-5800, 8140]
}, },
{ {
"parameters": { "parameters": {
"jsCode": "return [\n {\n json: {\n number: 0\n }\n },\n {\n json: {\n number: 1,\n k: 2,\n }\n },\n {\n json: {\n number: 10\n }\n },\n {\n json: {\n number: 11\n }\n },\n {\n json: {\n number: 12\n }\n }\n];" "jsCode": "return [\n {\n json: {\n number: 0\n }\n },\n {\n json: {\n number: 1,\n k: 2,\n }\n },\n {\n json: {\n number: 10\n }\n },\n {\n json: {\n number: 11\n }\n },\n {\n json: {\n number: 12\n }\n }\n];"
}, },
"id": "c62e90b3-f84a-48a5-94bf-3267a4c8b69e", "id": "94580b8b-b698-4c49-98b6-12973b6f4220",
"name": "Code1", "name": "Code1",
"type": "n8n-nodes-base.code", "type": "n8n-nodes-base.code",
"typeVersion": 1, "typeVersion": 1,
"position": [900, 60] "position": [-6240, 8260]
},
{
"parameters": {
"jsCode": "return [\n {\n json: {\n number: 0\n }\n },\n {\n json: {\n number: 1,\n k: 2,\n }\n },\n {\n json: {\n number: 10\n }\n },\n {\n json: {\n number: 11\n }\n },\n {\n json: {\n number: 12\n }\n }\n];"
},
"id": "46320ca2-8e8e-4ecf-b4f6-5899807c1500",
"name": "Code2",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [900, 840]
}, },
{ {
"parameters": {}, "parameters": {},
"id": "4ae12a83-5d3f-4d5b-845b-65c930d8ef5a", "id": "78ec40a4-775d-467b-bacb-805658190b29",
"name": "Old - A only", "name": "Old - A only",
"type": "n8n-nodes-base.noOp", "type": "n8n-nodes-base.noOp",
"typeVersion": 1, "typeVersion": 1,
"position": [1520, -180] "position": [-5540, 7920]
}, },
{ {
"parameters": {}, "parameters": {},
"id": "d4f5fd94-4b46-4b8c-8b8a-073e8c32ad85", "id": "eaa4cc93-53c6-4d00-9407-bd5da23e868e",
"name": "New - A only",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 440]
},
{
"parameters": {},
"id": "0939f79b-fd75-4d2f-b40b-50780114c3f2",
"name": "Old - Same", "name": "Old - Same",
"type": "n8n-nodes-base.noOp", "type": "n8n-nodes-base.noOp",
"typeVersion": 1, "typeVersion": 1,
"position": [1520, -40] "position": [-5540, 8060]
}, },
{ {
"parameters": {}, "parameters": {},
"id": "199ea52c-b30a-401d-a920-9db5c8e10d38", "id": "d073db39-1902-411d-80dd-6ea8f42ac33b",
"name": "Old - Different", "name": "Old - Different",
"type": "n8n-nodes-base.noOp", "type": "n8n-nodes-base.noOp",
"typeVersion": 1, "typeVersion": 1,
"position": [1520, 100] "position": [-5540, 8200]
}, },
{ {
"parameters": {}, "parameters": {},
"id": "1ebcb5bb-3061-47ef-8c79-847ae8bdb568", "id": "29f92258-4869-43c1-9cef-9c281397ccc8",
"name": "Old - B only", "name": "Old - B only",
"type": "n8n-nodes-base.noOp", "type": "n8n-nodes-base.noOp",
"typeVersion": 1, "typeVersion": 1,
"position": [1520, 240] "position": [-5540, 8340]
},
{
"parameters": {},
"id": "38689dbf-49f2-4f3b-855b-abd821ec316f",
"name": "New - B only",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 860]
},
{
"parameters": {},
"id": "dfcac903-95dc-4519-b49f-a6a65bf8fdb8",
"name": "New - Different",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 720]
},
{
"parameters": {},
"id": "b8588ebc-4dc8-41f5-9a0a-64d151d7122e",
"name": "New - Same",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 580]
},
{
"parameters": {
"jsCode": "return [\n {\n json: {\n number: 0\n }\n },\n {\n json: {\n number: 1,\n k: 2,\n }\n },\n {\n json: {\n number: 10\n }\n },\n {\n json: {\n number: 11\n }\n },\n {\n json: {\n number: 12\n }\n }\n];"
},
"id": "d35f3f52-c967-46e8-be3a-0bf709b20ef8",
"name": "Code3",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [880, 1340]
},
{
"parameters": {},
"id": "75690ad6-c870-4950-9085-12fdd9c12ddd",
"name": "New - A only1",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 1100]
},
{
"parameters": {},
"id": "9abb523f-349c-48b0-b36b-0c74064a6219",
"name": "New - B only1",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 1520]
},
{
"parameters": {},
"id": "c4230d94-eaa6-420d-baff-288463722a03",
"name": "New - Different1",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 1380]
},
{
"parameters": {},
"id": "58287181-dc90-4806-a053-83b3ef36e673",
"name": "New - Same1",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 1240]
},
{
"parameters": {
"mergeByFields": {
"values": [
{
"field1": "number",
"field2": "number"
}
]
},
"options": {}
},
"id": "5514c636-ee64-45f3-8832-77ad6652cc08",
"name": "Compare Datasets 2.3 - New - Connected",
"type": "n8n-nodes-base.compareDatasets",
"typeVersion": 2.3,
"position": [1260, 1320]
},
{
"parameters": {
"mergeByFields": {
"values": [
{
"field1": "number",
"field2": "number"
}
]
},
"options": {}
},
"id": "a25f4766-9bb4-40a0-9ae2-ef0004f5938a",
"name": "Compare Datasets 2.3 - New - Not Connected",
"type": "n8n-nodes-base.compareDatasets",
"typeVersion": 2.3,
"position": [1260, 660]
} }
], ],
"pinData": { "pinData": {
@ -249,72 +129,6 @@
"number": 12 "number": 12
} }
} }
],
"New - A only": [
{
"json": {
"number": 0
}
},
{
"json": {
"number": 1
}
},
{
"json": {
"number": 2
}
}
],
"New - A only1": [
{
"json": {
"number": 2
}
}
],
"New - Same1": [
{
"json": {
"number": 0
}
}
],
"New - Different1": [
{
"json": {
"keys": {
"number": 1
},
"same": {
"number": 1
},
"different": {
"k": {
"inputA": null,
"inputB": 2
}
}
}
}
],
"New - B only1": [
{
"json": {
"number": 10
}
},
{
"json": {
"number": 11
}
},
{
"json": {
"number": 12
}
}
] ]
}, },
"connections": { "connections": {
@ -325,11 +139,6 @@
"node": "Code", "node": "Code",
"type": "main", "type": "main",
"index": 0 "index": 0
},
{
"node": "Code3",
"type": "main",
"index": 0
} }
] ]
] ]
@ -337,20 +146,10 @@
"Code": { "Code": {
"main": [ "main": [
[ [
{
"node": "Compare Datasets 2.3 - New - Not Connected",
"type": "main",
"index": 0
},
{ {
"node": "Compare Datasets 2.2 - Old", "node": "Compare Datasets 2.2 - Old",
"type": "main", "type": "main",
"index": 0 "index": 0
},
{
"node": "Compare Datasets 2.3 - New - Connected",
"type": "main",
"index": 0
} }
] ]
] ]
@ -366,17 +165,6 @@
] ]
] ]
}, },
"Code2": {
"main": [
[
{
"node": "Compare Datasets 2.3 - New - Not Connected",
"type": "main",
"index": 1
}
]
]
},
"Compare Datasets 2.2 - Old": { "Compare Datasets 2.2 - Old": {
"main": [ "main": [
[ [
@ -408,87 +196,14 @@
} }
] ]
] ]
},
"Code3": {
"main": [
[
{
"node": "Compare Datasets 2.3 - New - Connected",
"type": "main",
"index": 1
}
]
]
},
"Compare Datasets 2.3 - New - Connected": {
"main": [
[
{
"node": "New - A only1",
"type": "main",
"index": 0
}
],
[
{
"node": "New - Same1",
"type": "main",
"index": 0
}
],
[
{
"node": "New - Different1",
"type": "main",
"index": 0
}
],
[
{
"node": "New - B only1",
"type": "main",
"index": 0
}
]
]
},
"Compare Datasets 2.3 - New - Not Connected": {
"main": [
[
{
"node": "New - A only",
"type": "main",
"index": 0
}
],
[
{
"node": "New - Same",
"type": "main",
"index": 0
}
],
[
{
"node": "New - Different",
"type": "main",
"index": 0
}
],
[
{
"node": "New - B only",
"type": "main",
"index": 0
}
]
]
} }
}, },
"active": false, "active": false,
"settings": {}, "settings": {
"versionId": "d5c0f040-7406-4e69-bd5d-a362a739c8d8", "executionOrder": "v0"
"id": "1114", },
"versionId": "49a52ed2-ec4b-44b1-9dbd-c13fac4144f2",
"id": "ZpXvXjaKKZihfA2x",
"meta": { "meta": {
"instanceId": "021d3c82ba2d3bc090cbf4fc81c9312668bcc34297e022bb3438c5c88a43a5ff" "instanceId": "021d3c82ba2d3bc090cbf4fc81c9312668bcc34297e022bb3438c5c88a43a5ff"
}, },

View file

@ -2366,7 +2366,9 @@
} }
}, },
"active": false, "active": false,
"settings": {}, "settings": {
"executionOrder": "v1"
},
"versionId": "6b37555c-fd78-4135-9c59-4912a71e18db", "versionId": "6b37555c-fd78-4135-9c59-4912a71e18db",
"id": "1107", "id": "1107",
"meta": { "meta": {

View file

@ -30,7 +30,6 @@ const versionDescription: INodeTypeDescription = {
inputs: ['main', 'main'], inputs: ['main', 'main'],
outputs: ['main'], outputs: ['main'],
inputNames: ['Input 1', 'Input 2'], inputNames: ['Input 1', 'Input 2'],
forceInputNodeExecution: true,
properties: [ properties: [
oldVersionNotice, oldVersionNotice,
{ {

View file

@ -48,9 +48,7 @@ const versionDescription: INodeTypeDescription = {
inputNames: ['Input 1', 'Input 2'], inputNames: ['Input 1', 'Input 2'],
// If the node is of version 2.2 or if mode is chooseBranch data from both branches is required // If the node is of version 2.2 or if mode is chooseBranch data from both branches is required
// to continue, else data from any input suffices // to continue, else data from any input suffices
requiredInputs: requiredInputs: '={{ $parameter["mode"] === "chooseBranch" ? [0, 1] : 1 }}',
'={{ $version < 2.2 ? undefined : ($parameter["mode"] === "chooseBranch" ? [0, 1] : 1) }}',
forceInputNodeExecution: '={{ $version < 2.2 }}',
properties: [ properties: [
{ {
displayName: 'Mode', displayName: 'Mode',

View file

@ -12,6 +12,7 @@ export async function executeWorkflow(testData: WorkflowTestData, nodeTypes: INo
connections: testData.input.workflowData.connections, connections: testData.input.workflowData.connections,
active: false, active: false,
nodeTypes, nodeTypes,
settings: testData.input.workflowData.settings,
}); });
const waitPromise = await createDeferredPromise<IRun>(); const waitPromise = await createDeferredPromise<IRun>();
const nodeExecutionOrder: string[] = []; const nodeExecutionOrder: string[] = [];

View file

@ -1429,8 +1429,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
eventTriggerDescription?: string; eventTriggerDescription?: string;
activationMessage?: string; activationMessage?: string;
inputs: string[]; inputs: string[];
forceInputNodeExecution?: string | boolean; // TODO: This option should be deprecated after a while requiredInputs?: string | number[] | number; // Ony available with executionOrder => "v1"
requiredInputs?: string | number[] | number;
inputNames?: string[]; inputNames?: string[];
outputs: string[]; outputs: string[];
outputNames?: string[]; outputNames?: string[];
@ -1770,6 +1769,7 @@ export interface IWorkflowSettings {
saveManualExecutions?: 'DEFAULT' | boolean; saveManualExecutions?: 'DEFAULT' | boolean;
saveExecutionProgress?: 'DEFAULT' | boolean; saveExecutionProgress?: 'DEFAULT' | boolean;
executionTimeout?: number; executionTimeout?: number;
executionOrder?: 'v0' | 'v1';
} }
export interface WorkflowTestData { export interface WorkflowTestData {

View file

@ -1191,18 +1191,7 @@ export class Workflow {
connectionInputData = inputData.main[0] as INodeExecutionData[]; connectionInputData = inputData.main[0] as INodeExecutionData[];
} }
let forceInputNodeExecution = nodeType.description.forceInputNodeExecution; const forceInputNodeExecution = this.settings.executionOrder !== 'v1';
if (forceInputNodeExecution !== undefined) {
if (typeof forceInputNodeExecution === 'string') {
forceInputNodeExecution = !!this.expression.getSimpleParameterValue(
node,
forceInputNodeExecution,
mode,
additionalData.timezone,
{ $version: node.typeVersion },
);
}
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
// for that reason do we use the data of the first one that contains any // for that reason do we use the data of the first one that contains any
@ -1213,7 +1202,6 @@ export class Workflow {
} }
} }
} }
}
if (connectionInputData.length === 0) { if (connectionInputData.length === 0) {
// No data for node so return // No data for node so return