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();
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"
if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) {
process.stdin.setRawMode(true);

View file

@ -143,24 +143,8 @@ export class WorkflowExecute {
return this.processRunExecutionData(workflow);
}
forceInputNodeExecution(workflow: Workflow, node: INode): boolean {
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
// 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;
forceInputNodeExecution(workflow: Workflow): boolean {
return workflow.settings.executionOrder !== 'v1';
}
/**
@ -379,6 +363,7 @@ export class WorkflowExecute {
runIndex: number,
): void {
let stillDataMissing = false;
const enqueueFn = workflow.settings.executionOrder === 'v1' ? 'unshift' : 'push';
let waitingNodeIndex: number | undefined;
// 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
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][
@ -554,8 +539,7 @@ export class WorkflowExecute {
// are already on the list to be processed.
// If that is not the case add it.
const node = workflow.getNode(connectionData.node);
const forceInputNodeExecution = this.forceInputNodeExecution(workflow, node!);
const forceInputNodeExecution = this.forceInputNodeExecution(workflow);
for (
let inputIndex = 0;
@ -680,7 +664,7 @@ export class WorkflowExecute {
if (addEmptyItem) {
// Add only node if it does not have any inputs because else it will
// be added by its input node later anyway.
this.runExecutionData.executionData!.nodeExecutionStack.unshift({
this.runExecutionData.executionData!.nodeExecutionStack[enqueueFn]({
node: workflow.getNode(nodeToAdd) as INode,
data: {
main: [
@ -744,7 +728,7 @@ export class WorkflowExecute {
};
} else {
// 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],
data: {
main: connectionDataArray,
@ -774,6 +758,7 @@ export class WorkflowExecute {
Logger.verbose('Workflow execution started', { workflowId: workflow.id });
const startedAt = new Date();
const forceInputNodeExecution = this.forceInputNodeExecution(workflow);
this.status = 'running';
@ -937,8 +922,6 @@ export class WorkflowExecute {
continue;
}
const node = workflow.getNode(executionNode.name);
// Check if all the data which is needed to run the node is available
if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) {
// Check if the node has incoming connections
@ -971,7 +954,7 @@ export class WorkflowExecute {
continue executionLoop;
}
if (this.forceInputNodeExecution(workflow, node!)) {
if (forceInputNodeExecution) {
// 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
// of both inputs has to be available to be able to process the node.
@ -1295,53 +1278,60 @@ export class WorkflowExecute {
);
}
const connectionDestinationNode = workflow.getNode(connectionData.node);
const forceInputNodeExecution = this.forceInputNodeExecution(
workflow,
connectionDestinationNode!,
);
if (
nodeSuccessData![outputIndex] &&
(nodeSuccessData![outputIndex].length !== 0 ||
(connectionData.index > 0 && forceInputNodeExecution))
) {
// Add the node only if it did execute or if connected to second "optional" input
const nodeToAdd = workflow.getNode(connectionData.node);
nodesToAdd.push({
position: nodeToAdd?.position || [0, 0],
connection: connectionData,
outputIndex: parseInt(outputIndex, 10),
});
if (workflow.settings.executionOrder === 'v1') {
const nodeToAdd = workflow.getNode(connectionData.node);
nodesToAdd.push({
position: nodeToAdd?.position || [0, 0],
connection: connectionData,
outputIndex: parseInt(outputIndex, 10),
});
} else {
this.addNodeToBeExecuted(
workflow,
connectionData,
parseInt(outputIndex, 10),
executionNode.name,
nodeSuccessData!,
runIndex,
);
}
}
}
}
// Always execute the node that is more to the top-left first
nodesToAdd.sort((a, b) => {
if (a.position[1] < b.position[1]) {
return 1;
}
if (a.position[1] > b.position[1]) {
return -1;
}
if (workflow.settings.executionOrder === 'v1') {
// Always execute the node that is more to the top-left first
nodesToAdd.sort((a, b) => {
if (a.position[1] < b.position[1]) {
return 1;
}
if (a.position[1] > b.position[1]) {
return -1;
}
if (a.position[0] > b.position[0]) {
return -1;
if (a.position[0] > b.position[0]) {
return -1;
}
return 0;
});
for (const nodeData of nodesToAdd) {
this.addNodeToBeExecuted(
workflow,
nodeData.connection,
nodeData.outputIndex,
executionNode.name,
nodeSuccessData!,
runIndex,
);
}
return 0;
});
for (const nodeData of nodesToAdd) {
this.addNodeToBeExecuted(
workflow,
nodeData.connection,
nodeData.outputIndex,
executionNode.name,
nodeSuccessData!,
runIndex,
);
}
}
}
@ -1382,7 +1372,10 @@ export class WorkflowExecute {
);
// 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 (typeof requiredInputs === 'string') {
requiredInputs = workflow.expression.getSimpleParameterValue(

View file

@ -4,15 +4,15 @@ import { WorkflowExecute } from '@/WorkflowExecute';
import * as Helpers from './helpers';
import { initLogger } from './helpers/utils';
import { predefinedWorkflowExecuteTests } from './helpers/constants';
import { legacyWorkflowExecuteTests, v1WorkflowExecuteTests } from './helpers/constants';
describe('WorkflowExecute', () => {
beforeAll(() => {
initLogger();
});
describe('run', () => {
const tests: WorkflowTestData[] = predefinedWorkflowExecuteTests;
describe('v0 execution order', () => {
const tests: WorkflowTestData[] = legacyWorkflowExecuteTests;
const executionMode = 'manual';
const nodeTypes = Helpers.NodeTypes();
@ -25,6 +25,9 @@ describe('WorkflowExecute', () => {
connections: testData.input.workflowData.connections,
active: false,
nodeTypes,
settings: {
executionOrder: 'v0',
},
});
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'
//workflows must have pinned data that would be used to test output after execution
describe('run test workflows', () => {
@ -87,6 +154,7 @@ describe('WorkflowExecute', () => {
connections: testData.input.workflowData.connections,
active: false,
nodeTypes,
settings: testData.input.workflowData.settings,
});
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;
callerIds?: string;
callerPolicy?: WorkflowSettings.CallerPolicy;
executionOrder: NonNullable<IWorkflowSettingsWorkflow['executionOrder']>;
}
export interface ITimeoutHMS {

View file

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

View file

@ -13,6 +13,31 @@
>
<template #content>
<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-col :span="10" class="setting-name">
{{ $locale.baseText('workflowSettings.errorWorkflow') + ':' }}
@ -421,9 +446,14 @@ export default defineComponent({
saveDataSuccessExecutionOptions: [] as Array<{ key: string; value: string }>,
saveExecutionProgressOptions: [] 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 }>,
workflowSettings: {} as IWorkflowSettings,
workflows: [] as IWorkflowShortResponse[],
executionOrder: 'v0',
executionTimeout: 0,
maxExecutionTimeout: 0,
timeoutHMS: { hours: 0, minutes: 0, seconds: 0 } as ITimeoutHMS,
@ -535,6 +565,9 @@ export default defineComponent({
if (workflowSettings.maxExecutionTimeout === undefined) {
workflowSettings.maxExecutionTimeout = this.rootStore.maxExecutionTimeout;
}
if (workflowSettings.executionOrder === undefined) {
workflowSettings.executionOrder = 'v0';
}
this.workflowSettings = workflowSettings;
this.timeoutHMS = this.convertToHMS(workflowSettings.executionTimeout);

View file

@ -1595,6 +1595,7 @@
"workflowSettings.defaultTimezone": "Default - {defaultTimezoneValue}",
"workflowSettings.defaultTimezoneNotValid": "Default Timezone not valid",
"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.executionTimeout": "How long the workflow should wait before timing out",
"workflowSettings.helpTexts.executionTimeoutToggle": "Whether to cancel workflow execution after a defined time",

View file

@ -82,24 +82,29 @@ import {
} from '@/utils';
import { useNDVStore } from './ndv.store';
import { useNodeTypesStore } from './nodeTypes.store';
import { useWorkflowsEEStore } from '@/stores/workflows.ee.store';
import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store';
import type { NodeMetadataMap } from '@/Interface';
const createEmptyWorkflow = (): IWorkflowDb => ({
id: PLACEHOLDER_EMPTY_WORKFLOW_ID,
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
name: '',
active: false,
createdAt: -1,
updatedAt: -1,
connections: {},
nodes: [],
settings: {},
settings: {
executionOrder: 'v1',
},
tags: [],
pinData: {},
versionId: '',
usedCredentials: [],
};
const createEmptyWorkflow = (): IWorkflowDb => ({
id: PLACEHOLDER_EMPTY_WORKFLOW_ID,
...defaults,
});
let cachedWorkflowKey: string | null = '';
@ -135,10 +140,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
return this.workflow.versionId;
},
workflowSettings(): IWorkflowSettings {
if (this.workflow.settings === undefined) {
return {};
}
return this.workflow.settings;
return this.workflow.settings ?? { ...defaults.settings };
},
workflowTags(): 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
// when the workflow replaces the node-parameters.
getNodes(): INodeUi[] {
const nodes = useWorkflowsStore().allNodes;
const nodes = this.allNodes;
const returnNodes: INodeUi[] = [];
for (const node of nodes) {
@ -331,23 +333,21 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
// Returns a workflow instance.
getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow {
const nodeTypes = this.getNodeTypes();
let workflowId: string | undefined = useWorkflowsStore().workflowId;
let workflowId: string | undefined = this.workflowId;
if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
workflowId = undefined;
}
const workflowName = useWorkflowsStore().workflowName;
cachedWorkflow = new Workflow({
id: workflowId,
name: workflowName,
name: this.workflowName,
nodes: copyData ? deepCopy(nodes) : nodes,
connections: copyData ? deepCopy(connections) : connections,
active: false,
nodeTypes,
settings: useWorkflowsStore().workflowSettings,
settings: this.workflowSettings,
// @ts-ignore
pinData: useWorkflowsStore().getPinData,
pinData: this.getPinData,
});
return cachedWorkflow;
@ -393,11 +393,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
},
async getNewWorkflowData(name?: string): Promise<INewWorkflowData> {
const workflowsEEStore = useWorkflowsEEStore();
let workflowData = {
name: '',
onboardingFlowEnabled: false,
settings: { ...defaults.settings },
};
try {
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 {
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('id') ? { id: PLACEHOLDER_EMPTY_WORKFLOW_ID } : {}),
...(!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
});
}
this.workflowsStore.removeAllConnections({ setStateDirty: false });
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.workflowsStore.resetState();
this.uiStore.removeActiveAction('workflowRunning');
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
inputs: ['main', 'main'],
inputNames: ['Input A', 'Input B'],
forceInputNodeExecution: '={{ $version < 2.3 }}',
requiredInputs: '={{ $version < 2.3 ? undefined : 1 }}',
requiredInputs: 1,
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
outputs: ['main', 'main', 'main', 'main'],
outputNames: ['In A only', 'Same', 'Different', 'In B only'],

View file

@ -1,23 +1,23 @@
{
"name": "Compare Datasets Node Test",
"name": "My workflow",
"nodes": [
{
"parameters": {},
"id": "0312bddf-aae0-423c-9041-d54fb124934f",
"id": "4013708d-2460-44b7-923d-e3cd36eb9287",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [480, 720]
"position": [-6660, 8040]
},
{
"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];"
},
"id": "0542886d-6ab2-4695-b686-2cd60729ba9a",
"id": "be417cbe-7a4f-40eb-8a26-fca253a01882",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [900, 640]
"position": [-6240, 8040]
},
{
"parameters": {
@ -31,173 +31,53 @@
},
"options": {}
},
"id": "f3e5e43b-a3bf-46c7-acd7-ae3d7d19d9f9",
"id": "97d65223-16c9-4d26-b31e-d818e1adbc8a",
"name": "Compare Datasets 2.2 - Old",
"type": "n8n-nodes-base.compareDatasets",
"typeVersion": 2.2,
"position": [1260, 40]
"position": [-5800, 8140]
},
{
"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": "c62e90b3-f84a-48a5-94bf-3267a4c8b69e",
"id": "94580b8b-b698-4c49-98b6-12973b6f4220",
"name": "Code1",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [900, 60]
},
{
"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]
"position": [-6240, 8260]
},
{
"parameters": {},
"id": "4ae12a83-5d3f-4d5b-845b-65c930d8ef5a",
"id": "78ec40a4-775d-467b-bacb-805658190b29",
"name": "Old - A only",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, -180]
"position": [-5540, 7920]
},
{
"parameters": {},
"id": "d4f5fd94-4b46-4b8c-8b8a-073e8c32ad85",
"name": "New - A only",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 440]
},
{
"parameters": {},
"id": "0939f79b-fd75-4d2f-b40b-50780114c3f2",
"id": "eaa4cc93-53c6-4d00-9407-bd5da23e868e",
"name": "Old - Same",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, -40]
"position": [-5540, 8060]
},
{
"parameters": {},
"id": "199ea52c-b30a-401d-a920-9db5c8e10d38",
"id": "d073db39-1902-411d-80dd-6ea8f42ac33b",
"name": "Old - Different",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 100]
"position": [-5540, 8200]
},
{
"parameters": {},
"id": "1ebcb5bb-3061-47ef-8c79-847ae8bdb568",
"id": "29f92258-4869-43c1-9cef-9c281397ccc8",
"name": "Old - B only",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 240]
},
{
"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]
"position": [-5540, 8340]
}
],
"pinData": {
@ -249,72 +129,6 @@
"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": {
@ -325,11 +139,6 @@
"node": "Code",
"type": "main",
"index": 0
},
{
"node": "Code3",
"type": "main",
"index": 0
}
]
]
@ -337,20 +146,10 @@
"Code": {
"main": [
[
{
"node": "Compare Datasets 2.3 - New - Not Connected",
"type": "main",
"index": 0
},
{
"node": "Compare Datasets 2.2 - Old",
"type": "main",
"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": {
"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,
"settings": {},
"versionId": "d5c0f040-7406-4e69-bd5d-a362a739c8d8",
"id": "1114",
"settings": {
"executionOrder": "v0"
},
"versionId": "49a52ed2-ec4b-44b1-9dbd-c13fac4144f2",
"id": "ZpXvXjaKKZihfA2x",
"meta": {
"instanceId": "021d3c82ba2d3bc090cbf4fc81c9312668bcc34297e022bb3438c5c88a43a5ff"
},

View file

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

View file

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

View file

@ -48,9 +48,7 @@ const versionDescription: INodeTypeDescription = {
inputNames: ['Input 1', 'Input 2'],
// 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
requiredInputs:
'={{ $version < 2.2 ? undefined : ($parameter["mode"] === "chooseBranch" ? [0, 1] : 1) }}',
forceInputNodeExecution: '={{ $version < 2.2 }}',
requiredInputs: '={{ $parameter["mode"] === "chooseBranch" ? [0, 1] : 1 }}',
properties: [
{
displayName: 'Mode',

View file

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

View file

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

View file

@ -1191,26 +1191,14 @@ export class Workflow {
connectionInputData = inputData.main[0] as INodeExecutionData[];
}
let forceInputNodeExecution = nodeType.description.forceInputNodeExecution;
if (forceInputNodeExecution !== undefined) {
if (typeof forceInputNodeExecution === 'string') {
forceInputNodeExecution = !!this.expression.getSimpleParameterValue(
node,
forceInputNodeExecution,
mode,
additionalData.timezone,
{ $version: node.typeVersion },
);
}
if (!forceInputNodeExecution) {
// 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 (const mainData of inputData.main) {
if (mainData?.length) {
connectionInputData = mainData;
break;
}
const forceInputNodeExecution = this.settings.executionOrder !== 'v1';
if (!forceInputNodeExecution) {
// 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 (const mainData of inputData.main) {
if (mainData?.length) {
connectionInputData = mainData;
break;
}
}
}