feat(core)!: Change data processing for multi-input-nodes (#4238)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Jan Oberhauser 2023-06-23 12:07:52 +02:00 committed by GitHub
parent 9194d8bb0e
commit b8458a53f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 7032 additions and 67 deletions

View file

@ -143,6 +143,26 @@ 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;
}
/**
* Executes the given workflow but only
*
@ -329,6 +349,27 @@ export class WorkflowExecute {
return true;
}
prepareWaitingToExecution(nodeName: string, numberOfConnections: number, runIndex: number) {
if (!this.runExecutionData.executionData!.waitingExecutionSource) {
this.runExecutionData.executionData!.waitingExecutionSource = {};
}
this.runExecutionData.executionData!.waitingExecution[nodeName][runIndex] = {
main: [],
};
this.runExecutionData.executionData!.waitingExecutionSource[nodeName][runIndex] = {
main: [],
};
for (let i = 0; i < numberOfConnections; i++) {
this.runExecutionData.executionData!.waitingExecution[nodeName][runIndex].main.push(null);
this.runExecutionData.executionData!.waitingExecutionSource[nodeName][runIndex].main.push(
null,
);
}
}
addNodeToBeExecuted(
workflow: Workflow,
connectionData: IConnection,
@ -338,6 +379,7 @@ export class WorkflowExecute {
runIndex: number,
): void {
let stillDataMissing = false;
let waitingNodeIndex: number | undefined;
// Check if node has multiple inputs as then we have to wait for all input data
// to be present before we can add it to the node-execution-stack
@ -358,47 +400,64 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node] = {};
nodeWasWaiting = false;
}
if (
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] ===
undefined
) {
// Node does not have data for runIndex yet so create also empty one and init it
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: [],
};
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][runIndex] =
{
main: [],
};
for (
let i = 0;
i < workflow.connectionsByDestinationNode[connectionData.node].main.length;
i++
) {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][
runIndex
].main.push(null);
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
].main.push(null);
// Figure out if the node is already waiting with partial data to which to add the
// data to or if a new entry has to get created
let createNewWaitingEntry = true;
if (
Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node])
.length > 0
) {
// Check if there is already data for the input on all of the waiting nodes
for (const index of Object.keys(
this.runExecutionData.executionData!.waitingExecution[connectionData.node],
)) {
if (
!this.runExecutionData.executionData!.waitingExecution[connectionData.node][
parseInt(index)
].main[connectionData.index]
) {
// Data for the input is missing so we can add it to the existing entry
createNewWaitingEntry = false;
waitingNodeIndex = parseInt(index);
break;
}
}
}
if (waitingNodeIndex === undefined) {
waitingNodeIndex = Object.values(
this.runExecutionData.executionData!.waitingExecution[connectionData.node],
).length;
}
if (createNewWaitingEntry) {
// There is currently no node waiting that does not already have data for
// the given input, so create a new entry
this.prepareWaitingToExecution(
connectionData.node,
workflow.connectionsByDestinationNode[connectionData.node].main.length,
waitingNodeIndex,
);
}
// Add the new data
if (nodeSuccessData === null) {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
connectionData.index
] = null;
this.runExecutionData.executionData!.waitingExecution[connectionData.node][
waitingNodeIndex
].main[connectionData.index] = null;
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
waitingNodeIndex
].main[connectionData.index] = null;
} else {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
connectionData.index
] = nodeSuccessData[outputIndex];
this.runExecutionData.executionData!.waitingExecution[connectionData.node][
waitingNodeIndex
].main[connectionData.index] = nodeSuccessData[outputIndex];
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
waitingNodeIndex
].main[connectionData.index] = {
previousNode: parentNodeName,
previousNodeOutput: outputIndex || undefined,
@ -412,14 +471,14 @@ export class WorkflowExecute {
for (
let i = 0;
i <
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main
.length;
this.runExecutionData.executionData!.waitingExecution[connectionData.node][waitingNodeIndex]
.main.length;
i++
) {
thisExecutionData =
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
i
];
this.runExecutionData.executionData!.waitingExecution[connectionData.node][
waitingNodeIndex
].main[i];
if (thisExecutionData === null) {
allDataFound = false;
break;
@ -433,11 +492,11 @@ export class WorkflowExecute {
const executionStackItem = {
node: workflow.nodes[connectionData.node],
data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][
runIndex
waitingNodeIndex
],
source:
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
waitingNodeIndex
],
} as IExecuteData;
@ -447,16 +506,18 @@ export class WorkflowExecute {
) {
executionStackItem.source =
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
waitingNodeIndex
];
}
this.runExecutionData.executionData!.nodeExecutionStack.unshift(executionStackItem);
// Remove the data from waiting
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex];
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][
waitingNodeIndex
];
delete this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
waitingNodeIndex
];
if (
@ -492,6 +553,10 @@ export class WorkflowExecute {
// checked. So we have to go through all the inputs and check if they
// 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!);
for (
let inputIndex = 0;
inputIndex < workflow.connectionsByDestinationNode[connectionData.node].main.length;
@ -540,6 +605,12 @@ export class WorkflowExecute {
continue;
}
if (!forceInputNodeExecution) {
// Do not automatically follow all incoming nodes and force them
// to execute
continue;
}
// Check if any of the parent nodes does not have any inputs. That
// would mean that it has to get added to the list of nodes to process.
const parentNodes = workflow.getParentNodes(inputData.node, 'main', -1);
@ -650,14 +721,26 @@ export class WorkflowExecute {
}
if (stillDataMissing) {
waitingNodeIndex = waitingNodeIndex!;
// Additional data is needed to run node so add it to waiting
if (
!this.runExecutionData.executionData!.waitingExecution.hasOwnProperty(connectionData.node)
) {
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
}
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: connectionDataArray,
this.prepareWaitingToExecution(
connectionData.node,
workflow.connectionsByDestinationNode[connectionData.node].main.length,
waitingNodeIndex,
);
this.runExecutionData.executionData!.waitingExecution[connectionData.node][waitingNodeIndex] =
{
main: connectionDataArray,
};
this.runExecutionData.executionData!.waitingExecutionSource![connectionData.node][
waitingNodeIndex
].main[connectionData.index] = {
previousNode: parentNodeName,
previousNodeOutput: outputIndex || undefined,
previousNodeRun: runIndex || undefined,
};
} else {
// All data is there so add it directly to stack
@ -854,6 +937,8 @@ 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
@ -886,17 +971,20 @@ export class WorkflowExecute {
continue executionLoop;
}
// 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.
if (
executionData.data.main.length < connectionIndex ||
executionData.data.main[connectionIndex] === null
) {
// Does not have the data of the connections so add back to stack
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry;
continue executionLoop;
if (this.forceInputNodeExecution(workflow, node!)) {
// 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.
if (
executionData.data.main.length < connectionIndex ||
executionData.data.main[connectionIndex] === null
) {
// Does not have the data of the connections so add back to stack
this.runExecutionData.executionData!.nodeExecutionStack.push(executionData);
lastExecutionTry = currentExecutionTry;
continue executionLoop;
}
}
}
}
@ -1207,9 +1295,16 @@ 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)
(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);
@ -1260,6 +1355,163 @@ export class WorkflowExecute {
taskData,
this.runExecutionData,
]);
let waitingNodes: string[] = Object.keys(
this.runExecutionData.executionData!.waitingExecution,
);
if (
this.runExecutionData.executionData!.nodeExecutionStack.length === 0 &&
waitingNodes.length
) {
// There are no more nodes in the execution stack. Check if there are
// waiting nodes that do not require data on all inputs and execute them,
// one by one.
// TODO: Should this also care about workflow position (top-left first?)
for (let i = 0; i < waitingNodes.length; i++) {
const nodeName = waitingNodes[i];
const checkNode = workflow.getNode(nodeName);
if (!checkNode) {
continue;
}
const nodeType = workflow.nodeTypes.getByNameAndVersion(
checkNode.type,
checkNode.typeVersion,
);
// Check if the node is only allowed execute if all inputs received data
let requiredInputs = nodeType.description.requiredInputs;
if (requiredInputs !== undefined) {
if (typeof requiredInputs === 'string') {
requiredInputs = workflow.expression.getSimpleParameterValue(
checkNode,
requiredInputs,
this.mode,
this.additionalData.timezone,
{ $version: checkNode.typeVersion },
undefined,
[],
) as number[];
}
if (
(requiredInputs !== undefined &&
Array.isArray(requiredInputs) &&
requiredInputs.length === nodeType.description.inputs.length) ||
requiredInputs === nodeType.description.inputs.length
) {
// All inputs are required, but not all have data so do not continue
continue;
}
}
const parentNodes = workflow.getParentNodes(nodeName);
// Check if input nodes (of same run) got already executed
// eslint-disable-next-line @typescript-eslint/no-loop-func
const parentIsWaiting = parentNodes.some((value) => waitingNodes.includes(value));
if (parentIsWaiting) {
// Execute node later as one of its dependencies is still outstanding
continue;
}
const runIndexes = Object.keys(
this.runExecutionData.executionData!.waitingExecution[nodeName],
).sort();
// The run-index of the earliest outstanding one
const firstRunIndex = parseInt(runIndexes[0]);
// Find all the inputs which received any kind of data, even if it was an empty
// array as this shows that the parent nodes executed but they did not have any
// data to pass on.
const inputsWithData = this.runExecutionData
.executionData!.waitingExecution[nodeName][firstRunIndex].main.map((data, index) =>
data === null ? null : index,
)
.filter((data) => data !== null);
if (requiredInputs !== undefined) {
// Certain inputs are required that the node can execute
if (Array.isArray(requiredInputs)) {
// Specific inputs are required (array of input indexes)
let inputDataMissing = false;
for (const requiredInput of requiredInputs) {
if (!inputsWithData.includes(requiredInput)) {
inputDataMissing = true;
break;
}
}
if (inputDataMissing) {
continue;
}
} else {
// A certain amout of inputs are required (amount of inputs)
if (inputsWithData.length < requiredInputs) {
continue;
}
}
}
const taskDataMain = this.runExecutionData.executionData!.waitingExecution[nodeName][
firstRunIndex
].main.map((data) => {
// For the inputs for which never any data got received set it to an empty array
return data === null ? [] : data;
});
if (taskDataMain.filter((data) => data.length).length !== 0) {
// Add the node to be executed
// Make sure that each input at least receives an empty array
if (taskDataMain.length < nodeType.description.inputs.length) {
for (; taskDataMain.length < nodeType.description.inputs.length; ) {
taskDataMain.push([]);
}
}
this.runExecutionData.executionData!.nodeExecutionStack.push({
node: workflow.nodes[nodeName],
data: {
main: taskDataMain,
},
source:
this.runExecutionData.executionData!.waitingExecutionSource![nodeName][
firstRunIndex
],
});
}
// Remove the node from waiting
delete this.runExecutionData.executionData!.waitingExecution[nodeName][firstRunIndex];
delete this.runExecutionData.executionData!.waitingExecutionSource![nodeName][
firstRunIndex
];
if (
Object.keys(this.runExecutionData.executionData!.waitingExecution[nodeName])
.length === 0
) {
// No more data left for the node so also delete that one
delete this.runExecutionData.executionData!.waitingExecution[nodeName];
delete this.runExecutionData.executionData!.waitingExecutionSource![nodeName];
}
if (taskDataMain.filter((data) => data.length).length !== 0) {
// Node to execute got found and added to stop
break;
} else {
// Node to add did not get found, rather an empty one removed so continue with search
waitingNodes = Object.keys(this.runExecutionData.executionData!.waitingExecution);
// Set counter to start again from the beginning. Set it to -1 as it auto increments
// after run. So only like that will we end up again ot 0.
i = -1;
}
}
}
}
return;

View file

@ -57,7 +57,6 @@ describe('WorkflowExecute', () => {
return nodeData.data.main[0]!.map((entry) => entry.json);
});
// expect(resultData).toEqual(testData.output.nodeData[nodeName]);
expect(resultData).toEqual(testData.output.nodeData[nodeName]);
}

File diff suppressed because it is too large Load diff

View file

@ -18,12 +18,14 @@ export class CompareDatasets implements INodeType {
name: 'compareDatasets',
icon: 'file:compare.svg',
group: ['transform'],
version: [1, 2, 2.1, 2.2],
version: [1, 2, 2.1, 2.2, 2.3],
description: 'Compare two inputs for changes',
defaults: { name: 'Compare Datasets' },
// 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 }}',
// 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

@ -0,0 +1,496 @@
{
"name": "Compare Datasets Node Test",
"nodes": [
{
"parameters": {},
"id": "0312bddf-aae0-423c-9041-d54fb124934f",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [480, 720]
},
{
"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",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [900, 640]
},
{
"parameters": {
"mergeByFields": {
"values": [
{
"field1": "number",
"field2": "number"
}
]
},
"options": {}
},
"id": "f3e5e43b-a3bf-46c7-acd7-ae3d7d19d9f9",
"name": "Compare Datasets 2.2 - Old",
"type": "n8n-nodes-base.compareDatasets",
"typeVersion": 2.2,
"position": [1260, 40]
},
{
"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",
"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]
},
{
"parameters": {},
"id": "4ae12a83-5d3f-4d5b-845b-65c930d8ef5a",
"name": "Old - A only",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, -180]
},
{
"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",
"name": "Old - Same",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, -40]
},
{
"parameters": {},
"id": "199ea52c-b30a-401d-a920-9db5c8e10d38",
"name": "Old - Different",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1520, 100]
},
{
"parameters": {},
"id": "1ebcb5bb-3061-47ef-8c79-847ae8bdb568",
"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]
}
],
"pinData": {
"Old - A only": [
{
"json": {
"number": 2
}
}
],
"Old - Same": [
{
"json": {
"number": 0
}
}
],
"Old - Different": [
{
"json": {
"keys": {
"number": 1
},
"same": {
"number": 1
},
"different": {
"k": {
"inputA": null,
"inputB": 2
}
}
}
}
],
"Old - B only": [
{
"json": {
"number": 10
}
},
{
"json": {
"number": 11
}
},
{
"json": {
"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": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
},
{
"node": "Code3",
"type": "main",
"index": 0
}
]
]
},
"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
}
]
]
},
"Code1": {
"main": [
[
{
"node": "Compare Datasets 2.2 - Old",
"type": "main",
"index": 1
}
]
]
},
"Code2": {
"main": [
[
{
"node": "Compare Datasets 2.3 - New - Not Connected",
"type": "main",
"index": 1
}
]
]
},
"Compare Datasets 2.2 - Old": {
"main": [
[
{
"node": "Old - A only",
"type": "main",
"index": 0
}
],
[
{
"node": "Old - Same",
"type": "main",
"index": 0
}
],
[
{
"node": "Old - Different",
"type": "main",
"index": 0
}
],
[
{
"node": "Old - B only",
"type": "main",
"index": 0
}
]
]
},
"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",
"meta": {
"instanceId": "021d3c82ba2d3bc090cbf4fc81c9312668bcc34297e022bb3438c5c88a43a5ff"
},
"tags": []
}

View file

@ -20,6 +20,7 @@ export class Merge extends VersionedNodeType {
1: new MergeV1(baseDescription),
2: new MergeV2(baseDescription),
2.1: new MergeV2(baseDescription),
2.2: new MergeV2(baseDescription),
};
super(nodeVersions, baseDescription);

View file

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

View file

@ -35,7 +35,7 @@ const versionDescription: INodeTypeDescription = {
name: 'merge',
icon: 'fa:code-branch',
group: ['transform'],
version: [2, 2.1],
version: [2, 2.1, 2.2],
subtitle: '={{$parameter["mode"]}}',
description: 'Merges data of multiple streams once data from both is available',
defaults: {
@ -46,6 +46,11 @@ const versionDescription: INodeTypeDescription = {
inputs: ['main', 'main'],
outputs: ['main'],
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 }}',
properties: [
{
displayName: 'Mode',
@ -374,6 +379,12 @@ export class MergeV2 implements INodeType {
let input1 = this.getInputData(0);
let input2 = this.getInputData(1);
if (input1.length === 0 || input2.length === 0) {
// If data of any input is missing, return the data of
// the input that contains data
return [[...input1, ...input2]];
}
if (clashHandling.resolveClash === 'preferInput1') {
[input1, input2] = [input2, input1];
}
@ -454,6 +465,7 @@ export class MergeV2 implements INodeType {
let input1 = this.getInputData(0);
let input2 = this.getInputData(1);
if (nodeVersion < 2.1) {
input1 = checkInput(
this.getInputData(0),
@ -473,6 +485,24 @@ export class MergeV2 implements INodeType {
if (!input1) return [returnData];
}
if (input1.length === 0 || input2.length === 0) {
if (joinMode === 'keepMatches') {
// Stop the execution
return [[]];
} else if (joinMode === 'enrichInput1' && input1.length === 0) {
// No data to enrich so stop
return [[]];
} else if (joinMode === 'enrichInput2' && input2.length === 0) {
// No data to enrich so stop
return [[]];
} else {
// Return the data of any of the inputs that contains data
return [[...input1, ...input2]];
}
}
if (!input1) return [returnData];
if (!input2 || !matchFields.length) {
if (
joinMode === 'keepMatches' ||

View file

@ -358,8 +358,8 @@ export class Expression {
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultValue?: boolean | number | string,
): boolean | number | string | undefined {
defaultValue?: boolean | number | string | unknown[],
): boolean | number | string | undefined | unknown[] {
if (parameterValue === undefined) {
// Value is not set so return the default
return defaultValue;

View file

@ -1429,6 +1429,8 @@ 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;
inputNames?: string[];
outputs: string[];
outputNames?: string[];

View file

@ -1186,11 +1186,35 @@ export class Workflow {
// because then it is a trigger node. As they only pass data through and so the input-data
// becomes output-data it has to be possible.
if (inputData.hasOwnProperty('main') && inputData.main.length > 0) {
if (inputData.main?.length > 0) {
// We always use the data of main input and the first input for execute
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;
}
}
}
}
if (connectionInputData.length === 0) {
// No data for node so return
return { data: undefined };