mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(core)!: Change data processing for multi-input-nodes (#4238)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
parent
9194d8bb0e
commit
b8458a53f6
|
@ -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;
|
||||
|
|
|
@ -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
|
@ -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'],
|
||||
|
|
|
@ -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": []
|
||||
}
|
|
@ -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);
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -30,6 +30,7 @@ const versionDescription: INodeTypeDescription = {
|
|||
inputs: ['main', 'main'],
|
||||
outputs: ['main'],
|
||||
inputNames: ['Input 1', 'Input 2'],
|
||||
forceInputNodeExecution: true,
|
||||
properties: [
|
||||
oldVersionNotice,
|
||||
{
|
||||
|
|
|
@ -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' ||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in a new issue