mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
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:
parent
f0dfc3cf4e
commit
d97edbcffa
|
@ -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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
@ -702,6 +702,7 @@ export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
|||
maxExecutionTimeout?: number;
|
||||
callerIds?: string;
|
||||
callerPolicy?: WorkflowSettings.CallerPolicy;
|
||||
executionOrder: NonNullable<IWorkflowSettingsWorkflow['executionOrder']>;
|
||||
}
|
||||
|
||||
export interface ITimeoutHMS {
|
||||
|
|
|
@ -7,6 +7,7 @@ export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
|||
return {
|
||||
name: response.name,
|
||||
onboardingFlowEnabled: response.onboardingFlowEnabled === true,
|
||||
settings: response.defaultSettings,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 } }
|
||||
: {}),
|
||||
};
|
||||
},
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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'],
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -2366,7 +2366,9 @@
|
|||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "6b37555c-fd78-4135-9c59-4912a71e18db",
|
||||
"id": "1107",
|
||||
"meta": {
|
||||
|
|
|
@ -30,7 +30,6 @@ const versionDescription: INodeTypeDescription = {
|
|||
inputs: ['main', 'main'],
|
||||
outputs: ['main'],
|
||||
inputNames: ['Input 1', 'Input 2'],
|
||||
forceInputNodeExecution: true,
|
||||
properties: [
|
||||
oldVersionNotice,
|
||||
{
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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[] = [];
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue