feat(core): Add support for pairedItem (beta) (#3012)

*  Add pairedItem support

* 👕 Fix lint issue

* 🐛 Fix resolution in frontend

* 🐛 Fix resolution issue

* 🐛 Fix resolution in frontend

* 🐛 Fix another resolution issue in frontend

*  Try to automatically add pairedItem data if possible

*  Cleanup

*  Display expression errors in editor UI

* 🐛 Fix issue that it did not display errors in production

* 🐛 Fix auto-fix of missing pairedItem data

* 🐛 Fix frontend resolution for not executed nodes

*  Fail execution on pairedItem resolve issue and display information
about itemIndex and runIndex

*  Allow that pairedItem is only set to number if runIndex is 0

*  Improve Expression Errors

*  Remove no longer needed code

*  Make errors more helpful

*  Add additional errors

* 👕 Fix lint issue

*  Add pairedItem support to core nodes

*  Improve support in Merge-Node

*  Fix issue with not correctly converted incoming pairedItem data

* 🐛 Fix frontend resolve issue

* 🐛 Fix frontend parameter name display issue

*  Improve errors

* 👕 Fix lint issue

*  Improve errors

*  Make it possible to display parameter name in error messages

*  Improve error messages

*  Fix error message

*  Improve error messages

*  Add another error message

*  Simplify
This commit is contained in:
Jan Oberhauser
2022-06-03 17:25:07 +02:00
committed by GitHub
parent 450a9aafea
commit bdb84130d6
52 changed files with 1317 additions and 152 deletions

View File

@@ -591,6 +591,7 @@ export class ActiveWorkflowRunner {
data: { data: {
main: data, main: data,
}, },
source: null,
}, },
]; ];
@@ -603,6 +604,7 @@ export class ActiveWorkflowRunner {
contextData: {}, contextData: {},
nodeExecutionStack, nodeExecutionStack,
waitingExecution: {}, waitingExecution: {},
waitingExecutionSource: {},
}, },
}; };

View File

@@ -190,6 +190,7 @@ export class CredentialsHelper extends ICredentialsHelper {
'internal', 'internal',
defaultTimezone, defaultTimezone,
additionalKeys, additionalKeys,
undefined,
'', '',
); );
@@ -366,6 +367,7 @@ export class CredentialsHelper extends ICredentialsHelper {
mode, mode,
timezone, timezone,
{}, {},
undefined,
false, false,
decryptedData, decryptedData,
) as ICredentialDataDecryptedObject; ) as ICredentialDataDecryptedObject;
@@ -398,6 +400,7 @@ export class CredentialsHelper extends ICredentialsHelper {
defaultTimezone, defaultTimezone,
{}, {},
undefined, undefined,
undefined,
decryptedData, decryptedData,
) as ICredentialDataDecryptedObject; ) as ICredentialDataDecryptedObject;
} }
@@ -642,6 +645,7 @@ export class CredentialsHelper extends ICredentialsHelper {
inputData, inputData,
runIndex, runIndex,
nodeTypeCopy, nodeTypeCopy,
{ node, data: {}, source: null },
NodeExecuteFunctions, NodeExecuteFunctions,
credentialsDecrypted, credentialsDecrypted,
); );

View File

@@ -198,6 +198,7 @@ export async function executeWebhook(
executionMode, executionMode,
additionalData.timezone, additionalData.timezone,
additionalKeys, additionalKeys,
undefined,
'onReceived', 'onReceived',
); );
const responseCode = workflow.expression.getSimpleParameterValue( const responseCode = workflow.expression.getSimpleParameterValue(
@@ -206,6 +207,7 @@ export async function executeWebhook(
executionMode, executionMode,
additionalData.timezone, additionalData.timezone,
additionalKeys, additionalKeys,
undefined,
200, 200,
) as number; ) as number;
@@ -215,6 +217,7 @@ export async function executeWebhook(
executionMode, executionMode,
additionalData.timezone, additionalData.timezone,
additionalKeys, additionalKeys,
undefined,
'firstEntryJson', 'firstEntryJson',
); );
@@ -288,6 +291,7 @@ export async function executeWebhook(
additionalData.timezone, additionalData.timezone,
additionalKeys, additionalKeys,
undefined, undefined,
undefined,
) as { ) as {
entries?: entries?:
| Array<{ | Array<{
@@ -373,6 +377,7 @@ export async function executeWebhook(
data: { data: {
main: webhookResultData.workflowData, main: webhookResultData.workflowData,
}, },
source: null,
}); });
runExecutionData = runExecutionData =
@@ -546,6 +551,7 @@ export async function executeWebhook(
additionalData.timezone, additionalData.timezone,
additionalKeys, additionalKeys,
undefined, undefined,
undefined,
); );
if (responsePropertyName !== undefined) { if (responsePropertyName !== undefined) {
@@ -559,6 +565,7 @@ export async function executeWebhook(
additionalData.timezone, additionalData.timezone,
additionalKeys, additionalKeys,
undefined, undefined,
undefined,
); );
if (responseContentType !== undefined) { if (responseContentType !== undefined) {
@@ -603,6 +610,7 @@ export async function executeWebhook(
executionMode, executionMode,
additionalData.timezone, additionalData.timezone,
additionalKeys, additionalKeys,
undefined,
'data', 'data',
); );

View File

@@ -397,6 +397,7 @@ export function hookFunctionsPreExecute(parentProcessMode?: string): IWorkflowEx
contextData: {}, contextData: {},
nodeExecutionStack: [], nodeExecutionStack: [],
waitingExecution: {}, waitingExecution: {},
waitingExecutionSource: {},
}, },
}; };
} }
@@ -752,6 +753,7 @@ export async function getRunData(
data: { data: {
main: [inputData], main: [inputData],
}, },
source: null,
}); });
const runExecutionData: IRunExecutionData = { const runExecutionData: IRunExecutionData = {
@@ -763,6 +765,7 @@ export async function getRunData(
contextData: {}, contextData: {},
nodeExecutionStack, nodeExecutionStack,
waitingExecution: {}, waitingExecution: {},
waitingExecutionSource: {},
}, },
}; };

View File

@@ -189,6 +189,7 @@ export async function executeErrorWorkflow(
], ],
], ],
}, },
source: null,
}); });
const runExecutionData: IRunExecutionData = { const runExecutionData: IRunExecutionData = {
@@ -200,6 +201,7 @@ export async function executeErrorWorkflow(
contextData: {}, contextData: {},
nodeExecutionStack, nodeExecutionStack,
waitingExecution: {}, waitingExecution: {},
waitingExecutionSource: {},
}, },
}; };

View File

@@ -210,6 +210,7 @@ export class LoadNodeParameterOptions {
inputData, inputData,
runIndex, runIndex,
tempNode, tempNode,
{ node: node!, source: null, data: {} },
NodeExecuteFunctions, NodeExecuteFunctions,
); );

View File

@@ -56,6 +56,7 @@ import {
WorkflowDataProxy, WorkflowDataProxy,
WorkflowExecuteMode, WorkflowExecuteMode,
LoggerProxy as Logger, LoggerProxy as Logger,
IExecuteData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { Agent } from 'https'; import { Agent } from 'https';
@@ -1447,6 +1448,7 @@ export function getNodeParameter(
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
timezone: string, timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys, additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
fallbackValue?: any, fallbackValue?: any,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object { ): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object {
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
@@ -1472,11 +1474,13 @@ export function getNodeParameter(
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
executeData,
); );
returnData = cleanupParameterData(returnData); returnData = cleanupParameterData(returnData);
} catch (e) { } catch (e) {
e.message += ` [Error in parameter: "${parameterName}"]`; if (e.context) e.context.parameter = parameterName;
e.cause = value;
throw e; throw e;
} }
@@ -1543,6 +1547,7 @@ export function getNodeWebhookUrl(
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
undefined,
false, false,
) as boolean; ) as boolean;
return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath); return NodeHelpers.getNodeWebhookUrl(baseUrl, workflow.id!, node, path.toString(), isFullPath);
@@ -1673,6 +1678,7 @@ export function getExecutePollFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
undefined,
fallbackValue, fallbackValue,
); );
}, },
@@ -1827,6 +1833,7 @@ export function getExecuteTriggerFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
undefined,
fallbackValue, fallbackValue,
); );
}, },
@@ -1940,6 +1947,7 @@ export function getExecuteFunctions(
inputData: ITaskDataConnections, inputData: ITaskDataConnections,
node: INode, node: INode,
additionalData: IWorkflowExecuteAdditionalData, additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData,
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
): IExecuteFunctions { ): IExecuteFunctions {
return ((workflow, runExecutionData, connectionInputData, inputData, node) => { return ((workflow, runExecutionData, connectionInputData, inputData, node) => {
@@ -1959,6 +1967,7 @@ export function getExecuteFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
executeData,
); );
}, },
async executeWorkflow( async executeWorkflow(
@@ -2035,6 +2044,7 @@ export function getExecuteFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
executeData,
fallbackValue, fallbackValue,
); );
}, },
@@ -2050,6 +2060,9 @@ export function getExecuteFunctions(
getTimezone: (): string => { getTimezone: (): string => {
return getTimezone(workflow, additionalData); return getTimezone(workflow, additionalData);
}, },
getExecuteData: (): IExecuteData => {
return executeData;
},
getWorkflow: () => { getWorkflow: () => {
return getWorkflowMetadata(workflow); return getWorkflowMetadata(workflow);
}, },
@@ -2065,6 +2078,7 @@ export function getExecuteFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
executeData,
); );
return dataProxy.getDataProxy(); return dataProxy.getDataProxy();
}, },
@@ -2199,6 +2213,7 @@ export function getExecuteSingleFunctions(
node: INode, node: INode,
itemIndex: number, itemIndex: number,
additionalData: IWorkflowExecuteAdditionalData, additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData,
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
): IExecuteSingleFunctions { ): IExecuteSingleFunctions {
return ((workflow, runExecutionData, connectionInputData, inputData, node, itemIndex) => { return ((workflow, runExecutionData, connectionInputData, inputData, node, itemIndex) => {
@@ -2219,6 +2234,7 @@ export function getExecuteSingleFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
executeData,
); );
}, },
getContext(type: string): IContextObject { getContext(type: string): IContextObject {
@@ -2276,6 +2292,9 @@ export function getExecuteSingleFunctions(
getTimezone: (): string => { getTimezone: (): string => {
return getTimezone(workflow, additionalData); return getTimezone(workflow, additionalData);
}, },
getExecuteData: (): IExecuteData => {
return executeData;
},
getNodeParameter: ( getNodeParameter: (
parameterName: string, parameterName: string,
fallbackValue?: any, fallbackValue?: any,
@@ -2296,6 +2315,7 @@ export function getExecuteSingleFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
executeData,
fallbackValue, fallbackValue,
); );
}, },
@@ -2314,6 +2334,7 @@ export function getExecuteSingleFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
executeData,
); );
return dataProxy.getDataProxy(); return dataProxy.getDataProxy();
}, },
@@ -2471,6 +2492,7 @@ export function getLoadOptionsFunctions(
'internal' as WorkflowExecuteMode, 'internal' as WorkflowExecuteMode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
undefined,
fallbackValue, fallbackValue,
); );
}, },
@@ -2601,6 +2623,7 @@ export function getExecuteHookFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
undefined,
fallbackValue, fallbackValue,
); );
}, },
@@ -2763,6 +2786,7 @@ export function getExecuteWebhookFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
getAdditionalKeys(additionalData), getAdditionalKeys(additionalData),
undefined,
fallbackValue, fallbackValue,
); );
}, },

View File

@@ -22,9 +22,12 @@ import {
IRun, IRun,
IRunData, IRunData,
IRunExecutionData, IRunExecutionData,
ISourceData,
ITaskData, ITaskData,
ITaskDataConnections, ITaskDataConnections,
ITaskDataConnectionsSource,
IWaitingForExecution, IWaitingForExecution,
IWaitingForExecutionSource,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
LoggerProxy as Logger, LoggerProxy as Logger,
NodeApiError, NodeApiError,
@@ -61,6 +64,7 @@ export class WorkflowExecute {
contextData: {}, contextData: {},
nodeExecutionStack: [], nodeExecutionStack: [],
waitingExecution: {}, waitingExecution: {},
waitingExecutionSource: {},
}, },
}; };
} }
@@ -106,6 +110,7 @@ export class WorkflowExecute {
], ],
], ],
}, },
source: null,
}, },
]; ];
@@ -121,6 +126,7 @@ export class WorkflowExecute {
contextData: {}, contextData: {},
nodeExecutionStack, nodeExecutionStack,
waitingExecution: {}, waitingExecution: {},
waitingExecutionSource: {},
}, },
}; };
@@ -157,10 +163,12 @@ export class WorkflowExecute {
// the data from runData // the data from runData
const nodeExecutionStack: IExecuteData[] = []; const nodeExecutionStack: IExecuteData[] = [];
const waitingExecution: IWaitingForExecution = {}; const waitingExecution: IWaitingForExecution = {};
const waitingExecutionSource: IWaitingForExecutionSource = {};
for (const startNode of startNodes) { for (const startNode of startNodes) {
incomingNodeConnections = workflow.connectionsByDestinationNode[startNode]; incomingNodeConnections = workflow.connectionsByDestinationNode[startNode];
const incomingData: INodeExecutionData[][] = []; const incomingData: INodeExecutionData[][] = [];
let incomingSourceData: ITaskDataConnectionsSource | null = null;
if (incomingNodeConnections === undefined) { if (incomingNodeConnections === undefined) {
// If it has no incoming data add the default empty data // If it has no incoming data add the default empty data
@@ -171,6 +179,7 @@ export class WorkflowExecute {
]); ]);
} else { } else {
// Get the data of the incoming connections // Get the data of the incoming connections
incomingSourceData = { main: [] };
for (const connections of incomingNodeConnections.main) { for (const connections of incomingNodeConnections.main) {
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) { for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
connection = connections[inputIndex]; connection = connections[inputIndex];
@@ -178,6 +187,9 @@ export class WorkflowExecute {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
runData[connection.node][runIndex].data![connection.type][connection.index]!, runData[connection.node][runIndex].data![connection.type][connection.index]!,
); );
incomingSourceData.main.push({
previousNode: connection.node,
});
} }
} }
} }
@@ -187,6 +199,7 @@ export class WorkflowExecute {
data: { data: {
main: incomingData, main: incomingData,
}, },
source: incomingSourceData,
}; };
nodeExecutionStack.push(executeData); nodeExecutionStack.push(executeData);
@@ -201,12 +214,15 @@ export class WorkflowExecute {
if (waitingExecution[destinationNode] === undefined) { if (waitingExecution[destinationNode] === undefined) {
waitingExecution[destinationNode] = {}; waitingExecution[destinationNode] = {};
waitingExecutionSource[destinationNode] = {};
} }
if (waitingExecution[destinationNode][runIndex] === undefined) { if (waitingExecution[destinationNode][runIndex] === undefined) {
waitingExecution[destinationNode][runIndex] = {}; waitingExecution[destinationNode][runIndex] = {};
waitingExecutionSource[destinationNode][runIndex] = {};
} }
if (waitingExecution[destinationNode][runIndex][connection.type] === undefined) { if (waitingExecution[destinationNode][runIndex][connection.type] === undefined) {
waitingExecution[destinationNode][runIndex][connection.type] = []; waitingExecution[destinationNode][runIndex][connection.type] = [];
waitingExecutionSource[destinationNode][runIndex][connection.type] = [];
} }
if (runData[connection.node] !== undefined) { if (runData[connection.node] !== undefined) {
@@ -215,8 +231,14 @@ export class WorkflowExecute {
waitingExecution[destinationNode][runIndex][connection.type].push( waitingExecution[destinationNode][runIndex][connection.type].push(
runData[connection.node][runIndex].data![connection.type][connection.index], runData[connection.node][runIndex].data![connection.type][connection.index],
); );
waitingExecutionSource[destinationNode][runIndex][connection.type].push({
previousNode: connection.node,
previousNodeOutput: connection.index || undefined,
previousNodeRun: runIndex || undefined,
} as ISourceData);
} else { } else {
waitingExecution[destinationNode][runIndex][connection.type].push(null); waitingExecution[destinationNode][runIndex][connection.type].push(null);
waitingExecutionSource[destinationNode][runIndex][connection.type].push(null);
} }
} }
} }
@@ -241,6 +263,7 @@ export class WorkflowExecute {
contextData: {}, contextData: {},
nodeExecutionStack, nodeExecutionStack,
waitingExecution, waitingExecution,
waitingExecutionSource,
}, },
}; };
@@ -303,12 +326,17 @@ export class WorkflowExecute {
// Node has multiple inputs // Node has multiple inputs
let nodeWasWaiting = true; let nodeWasWaiting = true;
if (this.runExecutionData.executionData!.waitingExecutionSource === null) {
this.runExecutionData.executionData!.waitingExecutionSource = {};
}
// Check if there is already data for the node // Check if there is already data for the node
if ( if (
this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined
) { ) {
// Node does not have data yet so create a new empty one // Node does not have data yet so create a new empty one
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {}; this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node] = {};
nodeWasWaiting = false; nodeWasWaiting = false;
} }
if ( if (
@@ -319,6 +347,10 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = { this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: [], main: [],
}; };
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][runIndex] =
{
main: [],
};
for ( for (
let i = 0; let i = 0;
i < workflow.connectionsByDestinationNode[connectionData.node].main.length; i < workflow.connectionsByDestinationNode[connectionData.node].main.length;
@@ -327,6 +359,10 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][ this.runExecutionData.executionData!.waitingExecution[connectionData.node][
runIndex runIndex
].main.push(null); ].main.push(null);
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
].main.push(null);
} }
} }
@@ -335,10 +371,20 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
connectionData.index connectionData.index
] = null; ] = null;
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
].main[connectionData.index] = null;
} else { } else {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[ this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
connectionData.index connectionData.index
] = nodeSuccessData[outputIndex]; ] = nodeSuccessData[outputIndex];
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
].main[connectionData.index] = {
previousNode: parentNodeName,
previousNodeOutput: outputIndex || undefined,
previousNodeRun: runIndex || undefined,
};
} }
// Check if all data exists now // Check if all data exists now
@@ -364,15 +410,32 @@ export class WorkflowExecute {
if (allDataFound) { if (allDataFound) {
// All data exists for node to be executed // All data exists for node to be executed
// So add it to the execution stack // So add it to the execution stack
this.runExecutionData.executionData!.nodeExecutionStack.push({
const executionStackItem = {
node: workflow.nodes[connectionData.node], node: workflow.nodes[connectionData.node],
data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][ data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][
runIndex runIndex
], ],
}); source:
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
],
} as IExecuteData;
if (this.runExecutionData.executionData!.waitingExecutionSource !== null) {
executionStackItem.source =
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
];
}
this.runExecutionData.executionData!.nodeExecutionStack.push(executionStackItem);
// Remove the data from waiting // Remove the data from waiting
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex]; delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex];
delete this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
];
if ( if (
Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node]) Object.keys(this.runExecutionData.executionData!.waitingExecution[connectionData.node])
@@ -380,6 +443,7 @@ export class WorkflowExecute {
) { ) {
// No more data left for the node so also delete that one // No more data left for the node so also delete that one
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node]; delete this.runExecutionData.executionData!.waitingExecution[connectionData.node];
delete this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node];
} }
return; return;
} }
@@ -534,6 +598,15 @@ export class WorkflowExecute {
], ],
], ],
}, },
source: {
main: [
{
previousNode: parentNodeName,
previousNodeOutput: outputIndex || undefined,
previousNodeRun: runIndex || undefined,
},
],
},
}); });
} }
} }
@@ -571,6 +644,15 @@ export class WorkflowExecute {
data: { data: {
main: connectionDataArray, main: connectionDataArray,
}, },
source: {
main: [
{
previousNode: parentNodeName,
previousNodeOutput: outputIndex || undefined,
previousNodeRun: runIndex || undefined,
},
],
},
}); });
} }
} }
@@ -660,6 +742,7 @@ export class WorkflowExecute {
data: { data: {
main: executionData.data.main, main: executionData.data.main,
} as ITaskDataConnections, } as ITaskDataConnections,
source: [],
}, },
], ],
}, },
@@ -691,6 +774,29 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData; this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
executionNode = executionData.node; executionNode = executionData.node;
// Update the pairedItem information on items
const newTaskDataConnections: ITaskDataConnections = {};
for (const inputName of Object.keys(executionData.data)) {
newTaskDataConnections[inputName] = executionData.data[inputName].map(
(input, inputIndex) => {
if (input === null) {
return input;
}
return input.map((item, itemIndex) => {
return {
...item,
pairedItem: {
item: itemIndex,
input: inputIndex || undefined,
},
};
});
},
);
}
executionData.data = newTaskDataConnections;
Logger.debug(`Start processing node "${executionNode.name}"`, { Logger.debug(`Start processing node "${executionNode.name}"`, {
node: executionNode.name, node: executionNode.name,
workflowId: workflow.id, workflowId: workflow.id,
@@ -767,9 +873,6 @@ export class WorkflowExecute {
} }
} }
// Clone input data that nodes can not mess up data of parallel nodes which receive the same data
// TODO: Should only clone if multiple nodes get the same data or when it gets returned to frontned
// is very slow so only do if needed
startTime = new Date().getTime(); startTime = new Date().getTime();
let maxTries = 1; let maxTries = 1;
@@ -813,8 +916,7 @@ export class WorkflowExecute {
workflowId: workflow.id, workflowId: workflow.id,
}); });
const runNodeData = await workflow.runNode( const runNodeData = await workflow.runNode(
executionData.node, executionData,
executionData.data,
this.runExecutionData, this.runExecutionData,
runIndex, runIndex,
this.additionalData, this.additionalData,
@@ -834,6 +936,30 @@ export class WorkflowExecute {
workflowId: workflow.id, workflowId: workflow.id,
}); });
// Check if the output data contains pairedItem data
checkOutputData: for (const outputData of nodeSuccessData as INodeExecutionData[][]) {
if (outputData === null) {
continue;
}
for (const item of outputData) {
if (!item.pairedItem) {
// The pairedItem is missing so check if it can get automatically fixed
if (
executionData.data.main.length !== 1 ||
executionData.data.main[0]?.length !== 1
) {
// Automatically fixing is only possible if there is only one
// input and one input item
break checkOutputData;
}
item.pairedItem = {
item: 0,
};
}
}
}
if (nodeSuccessData === undefined) { if (nodeSuccessData === undefined) {
// Node did not get executed // Node did not get executed
nodeSuccessData = null; nodeSuccessData = null;
@@ -885,6 +1011,7 @@ export class WorkflowExecute {
taskData = { taskData = {
startTime, startTime,
executionTime: new Date().getTime() - startTime, executionTime: new Date().getTime() - startTime,
source: executionData.source === null ? [] : executionData.source.main,
}; };
if (executionError !== undefined) { if (executionError !== undefined) {

View File

@@ -144,7 +144,10 @@ export default mixins(
const workflow = this.getWorkflow(); const workflow = this.getWorkflow();
const activeNode: INodeUi | null = this.$store.getters.activeNode; const activeNode: INodeUi | null = this.$store.getters.activeNode;
const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1); const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1);
const inputIndex = workflow.getNodeConnectionOutputIndex(activeNode!.name, parentNode[0]) || 0; const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]) || {
sourceIndex: 0,
destinationIndex: 0,
};
const autocompleteData: string[] = []; const autocompleteData: string[] = [];
@@ -164,7 +167,7 @@ export default mixins(
} }
} }
const connectionInputData = this.connectionInputData(parentNode, inputName, runIndex, inputIndex); const connectionInputData = this.connectionInputData(parentNode, activeNode!.name, inputName, runIndex, nodeConnection);
const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = { const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = {
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, $executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div class="error-header"> <div class="error-header">
<div class="error-message">{{ $locale.baseText('nodeErrorView.error') + ': ' + error.message }}</div> <div class="error-message">{{ $locale.baseText('nodeErrorView.error') + ': ' + getErrorMessage() }}</div>
<div class="error-description" v-if="error.description">{{error.description}}</div> <div class="error-description" v-if="error.description">{{error.description}}</div>
</div> </div>
<details> <details>
@@ -9,6 +9,13 @@
<font-awesome-icon class="error-details__icon" icon="angle-right" /> {{ $locale.baseText('nodeErrorView.details') }} <font-awesome-icon class="error-details__icon" icon="angle-right" /> {{ $locale.baseText('nodeErrorView.details') }}
</summary> </summary>
<div class="error-details__content"> <div class="error-details__content">
<div v-if="error.context.causeDetailed">
<el-card class="box-card" shadow="never">
<div>
{{error.context.causeDetailed}}
</div>
</el-card>
</div>
<div v-if="error.timestamp"> <div v-if="error.timestamp">
<el-card class="box-card" shadow="never"> <el-card class="box-card" shadow="never">
<div slot="header" class="clearfix box-card__title"> <div slot="header" class="clearfix box-card__title">
@@ -19,6 +26,18 @@
</div> </div>
</el-card> </el-card>
</div> </div>
<div v-if="error.context && error.context.itemIndex !== undefined" class="el-card box-card is-never-shadow el-card__body">
<span class="error-details__summary">{{ $locale.baseText('nodeErrorView.itemIndex') }}:</span>
{{error.context.itemIndex}}
<span v-if="error.context.runIndex">
| <span class="error-details__summary">{{ $locale.baseText('nodeErrorView.itemIndex') }}:</span>
{{error.context.runIndex}}
</span>
<span v-if="error.context.parameter">
| <span class="error-details__summary">{{ $locale.baseText('nodeErrorView.inParameter') }}:</span>
{{ parameterDisplayName(error.context.parameter) }}
</span>
</div>
<div v-if="error.httpCode"> <div v-if="error.httpCode">
<el-card class="box-card" shadow="never"> <el-card class="box-card" shadow="never">
<div slot="header" class="clearfix box-card__title"> <div slot="header" class="clearfix box-card__title">
@@ -79,6 +98,16 @@ import mixins from 'vue-typed-mixins';
import { import {
MAX_DISPLAY_DATA_SIZE, MAX_DISPLAY_DATA_SIZE,
} from '@/constants'; } from '@/constants';
import {
INodeUi,
} from '@/Interface';
import {
INodeProperties,
INodePropertyCollection,
INodePropertyOptions,
INodeTypeDescription,
} from 'n8n-workflow';
export default mixins( export default mixins(
copyPaste, copyPaste,
@@ -95,8 +124,72 @@ export default mixins(
displayCause(): boolean { displayCause(): boolean {
return JSON.stringify(this.error.cause).length < MAX_DISPLAY_DATA_SIZE; return JSON.stringify(this.error.cause).length < MAX_DISPLAY_DATA_SIZE;
}, },
parameters (): INodeProperties[] {
const node = this.$store.getters.activeNode;
if (!node) {
return [];
}
const nodeType = this.$store.getters.nodeType(node.type, node.typeVersion);
if (nodeType === null) {
return [];
}
return nodeType.properties;
},
}, },
methods: { methods: {
getErrorMessage (): string {
if (!this.error.context.messageTemplate) {
return this.error.message;
}
const parameterName = this.parameterDisplayName(this.error.context.parameter);
return this.error.context.messageTemplate.replace(/%%PARAMETER%%/g, parameterName);
},
parameterDisplayName(path: string) {
try {
const parameters = this.parameterName(this.parameters, path.split('.'));
if (!parameters.length) {
throw new Error();
}
return parameters.map(parameter => parameter.displayName).join(' > ');
} catch (error) {
return `Could not find parameter "${path}"`;
}
},
parameterName(parameters: Array<(INodePropertyOptions | INodeProperties | INodePropertyCollection)>, pathParts: string[]): Array<(INodeProperties | INodePropertyCollection)> {
let currentParameterName = pathParts.shift();
if (currentParameterName === undefined) {
return [];
}
const arrayMatch = currentParameterName.match(/(.*)\[([\d])\]$/);
if (arrayMatch !== null && arrayMatch.length > 0) {
currentParameterName = arrayMatch[1];
}
const currentParameter = parameters.find(parameter => parameter.name === currentParameterName) as unknown as INodeProperties | INodePropertyCollection;
if (currentParameter === undefined) {
throw new Error(`Could not find parameter "${currentParameterName}"`);
}
if (pathParts.length === 0) {
return [currentParameter];
}
if (currentParameter.hasOwnProperty('options')) {
return [currentParameter, ...this.parameterName((currentParameter as INodeProperties).options!, pathParts)];
}
if (currentParameter.hasOwnProperty('values')) {
return [currentParameter, ...this.parameterName((currentParameter as INodePropertyCollection).values, pathParts)];
}
// We can not resolve any deeper so lets stop here and at least return hopefully something useful
return [currentParameter];
},
copyCause() { copyCause() {
this.copyToClipboard(JSON.stringify(this.error.cause)); this.copyToClipboard(JSON.stringify(this.error.cause));
this.copySuccess(); this.copySuccess();

View File

@@ -366,6 +366,7 @@ export default mixins(
// Get the resolved parameter values of the current node // Get the resolved parameter values of the current node
const currentNodeParameters = this.$store.getters.activeNode.parameters; const currentNodeParameters = this.$store.getters.activeNode.parameters;
try {
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters); const resolvedNodeParameters = this.resolveParameter(currentNodeParameters);
const returnValues: string[] = []; const returnValues: string[] = [];
@@ -374,6 +375,9 @@ export default mixins(
} }
return returnValues.join('|'); return returnValues.join('|');
} catch (error) {
return null;
}
}, },
node (): INodeUi | null { node (): INodeUi | null {
return this.$store.getters.activeNode; return this.$store.getters.activeNode;
@@ -698,9 +702,9 @@ export default mixins(
// Get the resolved parameter values of the current node // Get the resolved parameter values of the current node
const currentNodeParameters = this.$store.getters.activeNode.parameters; const currentNodeParameters = this.$store.getters.activeNode.parameters;
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters;
try { try {
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters) as INodeParameters;
const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined; const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined;
const loadOptions = this.getArgument('loadOptions') as ILoadOptions | undefined; const loadOptions = this.getArgument('loadOptions') as ILoadOptions | undefined;

View File

@@ -374,13 +374,19 @@ export default mixins(
return returnData; return returnData;
}, },
getNodeContext (workflow: Workflow, runExecutionData: IRunExecutionData | null, parentNode: string[], nodeName: string, filterText: string): IVariableSelectorOption[] | null { getNodeContext (workflow: Workflow, runExecutionData: IRunExecutionData | null, parentNode: string[], nodeName: string, filterText: string): IVariableSelectorOption[] | null {
const inputIndex = 0;
const itemIndex = 0; const itemIndex = 0;
const inputName = 'main'; const inputName = 'main';
const runIndex = 0; const runIndex = 0;
const returnData: IVariableSelectorOption[] = []; const returnData: IVariableSelectorOption[] = [];
const connectionInputData = this.connectionInputData(parentNode, inputName, runIndex, inputIndex); const activeNode: INodeUi | null = this.$store.getters.activeNode;
if (activeNode === null) {
return returnData;
}
const nodeConnection = this.workflow.getNodeConnectionIndexes(activeNode.name, parentNode[0], 'main');
const connectionInputData = this.connectionInputData(parentNode, nodeName, inputName, runIndex, nodeConnection);
if (connectionInputData === null) { if (connectionInputData === null) {
return returnData; return returnData;
@@ -493,7 +499,8 @@ export default mixins(
// Check from which output to read the data. // Check from which output to read the data.
// Depends on how the nodes are connected. // Depends on how the nodes are connected.
// (example "IF" node. If node is connected to "true" or to "false" output) // (example "IF" node. If node is connected to "true" or to "false" output)
const outputIndex = this.workflow.getNodeConnectionOutputIndex(activeNode.name, parentNode[0], 'main'); const nodeConnection = this.workflow.getNodeConnectionIndexes(activeNode.name, parentNode[0], 'main');
const outputIndex = nodeConnection === undefined ? 0: nodeConnection.sourceIndex;
tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[]; tempOutputData = this.getNodeOutputData(runData, parentNode[0], filterText, itemIndex, 0, 'main', outputIndex, true) as IVariableSelectorOption[];

View File

@@ -27,6 +27,8 @@ import {
IWorkflowDataProxyAdditionalKeys, IWorkflowDataProxyAdditionalKeys,
Workflow, Workflow,
NodeHelpers, NodeHelpers,
IExecuteData,
INodeConnection,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
@@ -59,9 +61,12 @@ export const workflowHelpers = mixins(
) )
.extend({ .extend({
methods: { methods: {
// Returns connectionInputData to be able to execute an expression. executeData(parentNode: string[], currentNode: string, inputName: string, runIndex: number): IExecuteData {
connectionInputData (parentNode: string[], inputName: string, runIndex: number, inputIndex: number): INodeExecutionData[] | null { const executeData = {
let connectionInputData = null; node: {},
data: {},
source: null,
} as IExecuteData;
if (parentNode.length) { if (parentNode.length) {
// Add the input data to be able to also resolve the short expression format // Add the input data to be able to also resolve the short expression format
@@ -70,18 +75,58 @@ export const workflowHelpers = mixins(
const workflowRunData = this.$store.getters.getWorkflowRunData as IRunData | null; const workflowRunData = this.$store.getters.getWorkflowRunData as IRunData | null;
if (workflowRunData === null) { if (workflowRunData === null) {
return null; return executeData;
} }
if (!workflowRunData[parentNodeName] || if (!workflowRunData[parentNodeName] ||
workflowRunData[parentNodeName].length <= runIndex || workflowRunData[parentNodeName].length <= runIndex ||
!workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') || !workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') ||
workflowRunData[parentNodeName][runIndex].data === undefined || workflowRunData[parentNodeName][runIndex].data === undefined ||
!workflowRunData[parentNodeName][runIndex].data!.hasOwnProperty(inputName) || !workflowRunData[parentNodeName][runIndex].data!.hasOwnProperty(inputName)
workflowRunData[parentNodeName][runIndex].data![inputName].length <= inputIndex
) { ) {
executeData.data = {};
} else {
executeData.data = workflowRunData[parentNodeName][runIndex].data!;
if (workflowRunData[currentNode] && workflowRunData[currentNode][runIndex]) {
executeData.source = {
[inputName]: workflowRunData[currentNode][runIndex].source!,
};
} else {
// The curent node did not get executed in UI yet so build data manually
executeData.source = {
[inputName]: [
{
previousNode: parentNodeName,
},
],
};
}
}
}
return executeData;
},
// Returns connectionInputData to be able to execute an expression.
connectionInputData (parentNode: string[], currentNode: string, inputName: string, runIndex: number, nodeConnection: INodeConnection = { sourceIndex: 0, destinationIndex: 0 }): INodeExecutionData[] | null {
let connectionInputData = null;
const executeData = this.executeData(parentNode, currentNode, inputName, runIndex);
if (parentNode.length) {
if (!Object.keys(executeData.data).length || executeData.data[inputName].length <= nodeConnection.sourceIndex) {
connectionInputData = []; connectionInputData = [];
} else { } else {
connectionInputData = workflowRunData[parentNodeName][runIndex].data![inputName][inputIndex]; connectionInputData = executeData.data![inputName][nodeConnection.sourceIndex];
if (connectionInputData !== null) {
// Update the pairedItem information on items
connectionInputData = connectionInputData.map((item, itemIndex) => {
return {
...item,
pairedItem: {
item: itemIndex,
input: nodeConnection.destinationIndex,
},
};
});
}
} }
} }
@@ -386,14 +431,20 @@ export const workflowHelpers = mixins(
resolveParameter(parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) { resolveParameter(parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[]) {
const itemIndex = 0; const itemIndex = 0;
const runIndex = 0;
const inputName = 'main'; const inputName = 'main';
const activeNode = this.$store.getters.activeNode; const activeNode = this.$store.getters.activeNode;
const workflow = this.getWorkflow(); const workflow = this.getWorkflow();
const parentNode = workflow.getParentNodes(activeNode.name, inputName, 1); const parentNode = workflow.getParentNodes(activeNode.name, inputName, 1);
const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null; const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null;
const inputIndex = workflow.getNodeConnectionOutputIndex(activeNode!.name, parentNode[0]) || 0;
let connectionInputData = this.connectionInputData(parentNode, inputName, runIndex, inputIndex); const workflowRunData = this.$store.getters.getWorkflowRunData as IRunData | null;
let runIndexParent = 0;
if (workflowRunData !== null && parentNode.length) {
runIndexParent = workflowRunData[parentNode[0]].length -1;
}
const nodeConnection = workflow.getNodeConnectionIndexes(activeNode!.name, parentNode[0]);
let connectionInputData = this.connectionInputData(parentNode, activeNode.name, inputName, runIndexParent, nodeConnection);
let runExecutionData: IRunExecutionData; let runExecutionData: IRunExecutionData;
if (executionData === null) { if (executionData === null) {
@@ -415,7 +466,13 @@ export const workflowHelpers = mixins(
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, $resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
}; };
return workflow.expression.getParameterValue(parameter, runExecutionData, runIndex, itemIndex, activeNode.name, connectionInputData, 'manual', this.$store.getters.timezone, additionalKeys, false) as IDataObject; let runIndexCurrent = 0;
if (workflowRunData !== null && workflowRunData[activeNode.name]) {
runIndexCurrent = workflowRunData[activeNode.name].length -1;
}
const executeData = this.executeData(parentNode, activeNode.name, inputName, runIndexCurrent);
return workflow.expression.getParameterValue(parameter, runExecutionData, runIndexCurrent, itemIndex, activeNode.name, connectionInputData, 'manual', this.$store.getters.timezone, additionalKeys, executeData, false) as IDataObject;
}, },
resolveExpression(expression: string, siblingParameters: INodeParameters = {}) { resolveExpression(expression: string, siblingParameters: INodeParameters = {}) {

View File

@@ -400,6 +400,9 @@
"nodeErrorView.details": "Details", "nodeErrorView.details": "Details",
"nodeErrorView.error": "ERROR", "nodeErrorView.error": "ERROR",
"nodeErrorView.httpCode": "HTTP Code", "nodeErrorView.httpCode": "HTTP Code",
"nodeErrorView.inParameter": "In or underneath Parameter",
"nodeErrorView.itemIndex": "Item Index",
"nodeErrorView.runIndex": "Run Index",
"nodeErrorView.showMessage.title": "Copied to clipboard", "nodeErrorView.showMessage.title": "Copied to clipboard",
"nodeErrorView.stack": "Stack", "nodeErrorView.stack": "Stack",
"nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed": "The error cause is too large to be displayed", "nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed": "The error cause is too large to be displayed",

View File

@@ -257,6 +257,9 @@ export class Compression implements INodeType {
returnData.push({ returnData.push({
json: items[i].json, json: items[i].json,
binary: binaryObject, binary: binaryObject,
pairedItem: {
item: i,
},
}); });
} }
@@ -314,6 +317,9 @@ export class Compression implements INodeType {
binary: { binary: {
[binaryPropertyOutput]: data, [binaryPropertyOutput]: data,
}, },
pairedItem: {
item: i,
},
}); });
} }
@@ -321,13 +327,23 @@ export class Compression implements INodeType {
returnData.push({ returnData.push({
json: items[i].json, json: items[i].json,
binary: binaryObject, binary: binaryObject,
pairedItem: {
item: i,
},
}); });
} }
} }
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } }); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -493,11 +493,17 @@ export class Crypto implements INodeType {
// Uses dot notation so copy all data // Uses dot notation so copy all data
newItem = { newItem = {
json: JSON.parse(JSON.stringify(item.json)), json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: i,
},
}; };
} else { } else {
// Does not use dot notation so shallow copy is enough // Does not use dot notation so shallow copy is enough
newItem = { newItem = {
json: { ...item.json }, json: { ...item.json },
pairedItem: {
item: i,
},
}; };
} }
@@ -511,7 +517,14 @@ export class Crypto implements INodeType {
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({ json: { error: (error as JsonObject).message } }); returnData.push({
json: {
error: (error as JsonObject).message,
},
pairedItem: {
item: i,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -446,11 +446,17 @@ export class DateTime implements INodeType {
// Uses dot notation so copy all data // Uses dot notation so copy all data
newItem = { newItem = {
json: JSON.parse(JSON.stringify(item.json)), json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: i,
},
}; };
} else { } else {
// Does not use dot notation so shallow copy is enough // Does not use dot notation so shallow copy is enough
newItem = { newItem = {
json: { ...item.json }, json: { ...item.json },
pairedItem: {
item: i,
},
}; };
} }
@@ -485,11 +491,17 @@ export class DateTime implements INodeType {
// Uses dot notation so copy all data // Uses dot notation so copy all data
newItem = { newItem = {
json: JSON.parse(JSON.stringify(item.json)), json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: i,
},
}; };
} else { } else {
// Does not use dot notation so shallow copy is enough // Does not use dot notation so shallow copy is enough
newItem = { newItem = {
json: { ...item.json }, json: { ...item.json },
pairedItem: {
item: i,
},
}; };
} }
@@ -504,7 +516,14 @@ export class DateTime implements INodeType {
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }}); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -1211,6 +1211,9 @@ export class EditImage implements INodeType {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: item.json, json: item.json,
binary: {}, binary: {},
pairedItem: {
item: itemIndex,
},
}; };
if (operation === 'information') { if (operation === 'information') {
@@ -1394,7 +1397,14 @@ export class EditImage implements INodeType {
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }}); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -204,11 +204,23 @@ export class EmailSend implements INodeType {
// Send the email // Send the email
const info = await transporter.sendMail(mailOptions); const info = await transporter.sendMail(mailOptions);
returnData.push({ json: info as unknown as IDataObject }); returnData.push({
json: info as unknown as IDataObject,
pairedItem: {
item: itemIndex,
},
});
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }}); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -119,12 +119,22 @@ export class ExecuteCommand implements INodeType {
stderr, stderr,
stdout, stdout,
}, },
pairedItem: {
item: itemIndex,
},
}, },
); );
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnItems.push({json:{ error: error.message }}); returnItems.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -163,6 +163,9 @@ return item;`,
const returnItem: INodeExecutionData = { const returnItem: INodeExecutionData = {
json: cleanupData(jsonData), json: cleanupData(jsonData),
pairedItem: {
item: itemIndex,
},
}; };
if (item.binary) { if (item.binary) {
@@ -172,7 +175,14 @@ return item;`,
returnData.push(returnItem); returnData.push(returnItem);
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }}); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -259,7 +259,14 @@ export class Git implements INodeType {
await git.add(pathsToAdd.split(',')); await git.add(pathsToAdd.split(','));
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'addConfig') { } else if (operation === 'addConfig') {
// ---------------------------------- // ----------------------------------
@@ -275,7 +282,14 @@ export class Git implements INodeType {
} }
await git.addConfig(key, value, append); await git.addConfig(key, value, append);
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'clone') { } else if (operation === 'clone') {
// ---------------------------------- // ----------------------------------
@@ -287,7 +301,14 @@ export class Git implements INodeType {
await git.clone(sourceRepository, '.'); await git.clone(sourceRepository, '.');
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'commit') { } else if (operation === 'commit') {
// ---------------------------------- // ----------------------------------
@@ -303,7 +324,14 @@ export class Git implements INodeType {
await git.commit(message, pathsToAdd); await git.commit(message, pathsToAdd);
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'fetch') { } else if (operation === 'fetch') {
// ---------------------------------- // ----------------------------------
@@ -311,7 +339,14 @@ export class Git implements INodeType {
// ---------------------------------- // ----------------------------------
await git.fetch(); await git.fetch();
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'log') { } else if (operation === 'log') {
// ---------------------------------- // ----------------------------------
@@ -331,7 +366,12 @@ export class Git implements INodeType {
const log = await git.log(logOptions); const log = await git.log(logOptions);
// @ts-ignore // @ts-ignore
returnItems.push(...this.helpers.returnJsonArray(log.all)); returnItems.push(...this.helpers.returnJsonArray(log.all).map(item => {
return {
...item,
pairedItem: { item: itemIndex },
};
}));
} else if (operation === 'pull') { } else if (operation === 'pull') {
// ---------------------------------- // ----------------------------------
@@ -339,7 +379,14 @@ export class Git implements INodeType {
// ---------------------------------- // ----------------------------------
await git.pull(); await git.pull();
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'push') { } else if (operation === 'push') {
// ---------------------------------- // ----------------------------------
@@ -370,7 +417,14 @@ export class Git implements INodeType {
} }
} }
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'pushTags') { } else if (operation === 'pushTags') {
// ---------------------------------- // ----------------------------------
@@ -378,7 +432,14 @@ export class Git implements INodeType {
// ---------------------------------- // ----------------------------------
await git.pushTags(); await git.pushTags();
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} else if (operation === 'listConfig') { } else if (operation === 'listConfig') {
// ---------------------------------- // ----------------------------------
@@ -396,7 +457,12 @@ export class Git implements INodeType {
} }
// @ts-ignore // @ts-ignore
returnItems.push(...this.helpers.returnJsonArray(data)); returnItems.push(...this.helpers.returnJsonArray(data).map(item => {
return {
...item,
pairedItem: { item: itemIndex },
};
}));
} else if (operation === 'status') { } else if (operation === 'status') {
// ---------------------------------- // ----------------------------------
@@ -406,7 +472,12 @@ export class Git implements INodeType {
const status = await git.status(); const status = await git.status();
// @ts-ignore // @ts-ignore
returnItems.push(...this.helpers.returnJsonArray([status])); returnItems.push(...this.helpers.returnJsonArray([status]).map(item => {
return {
...item,
pairedItem: { item: itemIndex },
};
}));
} else if (operation === 'tag') { } else if (operation === 'tag') {
// ---------------------------------- // ----------------------------------
@@ -416,14 +487,27 @@ export class Git implements INodeType {
const name = this.getNodeParameter('name', itemIndex, '') as string; const name = this.getNodeParameter('name', itemIndex, '') as string;
await git.addTag(name); await git.addTag(name);
returnItems.push({ json: { success: true } }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: itemIndex,
},
});
} }
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnItems.push({ json: { error: error.toString() } }); returnItems.push({
json: {
error: error.toString(),
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }

View File

@@ -254,6 +254,9 @@ export class HtmlExtract implements INodeType {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: {}, json: {},
pairedItem: {
item: itemIndex,
},
}; };
// Itterate over all the defined values which should be extracted // Itterate over all the defined values which should be extracted
@@ -277,7 +280,14 @@ export class HtmlExtract implements INodeType {
} }
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } }); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -1232,6 +1232,9 @@ export class HttpRequest implements INodeType {
json: { json: {
error: response.reason, error: response.reason,
}, },
pairedItem: {
item: itemIndex,
},
}, },
); );
continue; continue;
@@ -1251,6 +1254,9 @@ export class HttpRequest implements INodeType {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: {}, json: {},
binary: {}, binary: {},
pairedItem: {
item: itemIndex,
},
}; };
if (items[itemIndex].binary !== undefined) { if (items[itemIndex].binary !== undefined) {
@@ -1295,12 +1301,20 @@ export class HttpRequest implements INodeType {
returnItem[property] = response![property]; returnItem[property] = response![property];
} }
returnItems.push({ json: returnItem }); returnItems.push({
json: returnItem,
pairedItem: {
item: itemIndex,
},
});
} else { } else {
returnItems.push({ returnItems.push({
json: { json: {
[dataPropertyName]: response, [dataPropertyName]: response,
}, },
pairedItem: {
item: itemIndex,
},
}); });
} }
} else { } else {
@@ -1319,7 +1333,12 @@ export class HttpRequest implements INodeType {
} }
} }
returnItems.push({ json: returnItem }); returnItems.push({
json: returnItem,
pairedItem: {
item: itemIndex,
},
});
} else { } else {
if (responseFormat === 'json' && typeof response === 'string') { if (responseFormat === 'json' && typeof response === 'string') {
try { try {
@@ -1330,9 +1349,19 @@ export class HttpRequest implements INodeType {
} }
if (options.splitIntoItems === true && Array.isArray(response)) { if (options.splitIntoItems === true && Array.isArray(response)) {
response.forEach(item => returnItems.push({ json: item })); response.forEach(item => returnItems.push({
json: item,
pairedItem: {
item: itemIndex,
},
}));
} else { } else {
returnItems.push({ json: response }); returnItems.push({
json: response,
pairedItem: {
item: itemIndex,
},
});
} }
} }
} }

View File

@@ -355,6 +355,9 @@ export class ICalendar implements INodeType {
binary: { binary: {
[binaryPropertyName]: binaryData, [binaryPropertyName]: binaryData,
}, },
pairedItem: {
item: i,
},
}, },
); );
} }

View File

@@ -752,7 +752,12 @@ return 0;`,
newItem = { ...newItem, [destinationFieldName as string || fieldToSplitOut as string]: element }; newItem = { ...newItem, [destinationFieldName as string || fieldToSplitOut as string]: element };
} }
returnData.push({ json: newItem }); returnData.push({
json: newItem,
pairedItem: {
item: i,
},
});
} }
} }
} }
@@ -790,8 +795,17 @@ return 0;`,
} }
} }
let newItem: INodeExecutionData; let newItem: INodeExecutionData;
newItem = { json: {} }; newItem = {
json: {},
pairedItem: Array.from({length}, (_, i) => i).map(index => {
return {
item: index,
};
}),
};
// tslint:disable-next-line: no-any // tslint:disable-next-line: no-any
const values: { [key: string]: any } = {}; const values: { [key: string]: any } = {};
const outputFields: string[] = []; const outputFields: string[] = [];
@@ -899,9 +913,10 @@ return 0;`,
} }
keys = fieldsToCompare.map(key => (key.trim())); keys = fieldsToCompare.map(key => (key.trim()));
} }
// This solution is O(nlogn) // This solution is O(nlogn)
// add original index to the items // add original index to the items
const newItems = items.map((item, index) => ({ json: { ...item['json'], __INDEX: index, }, } as INodeExecutionData)); const newItems = items.map((item, index) => ({ json: { ...item['json'], __INDEX: index, }, pairedItem: { item: index, } } as INodeExecutionData));
//sort items using the compare keys //sort items using the compare keys
newItems.sort((a, b) => { newItems.sort((a, b) => {
let result = 0; let result = 0;
@@ -962,7 +977,7 @@ return 0;`,
let data = items.filter((_, index) => !removedIndexes.includes(index)); let data = items.filter((_, index) => !removedIndexes.includes(index));
if (removeOtherFields) { if (removeOtherFields) {
data = data.map(item => ({ json: pick(item.json, ...keys) })); data = data.map((item, index) => ({ json: pick(item.json, ...keys), pairedItem: { item: index, } }));
} }
// return the filtered items // return the filtered items

View File

@@ -44,6 +44,7 @@ export async function router(this: IExecuteFunctions): Promise<INodeExecutionDat
if (this.continueOnFail()) { if (this.continueOnFail()) {
operationResult.push({json: this.getInputData(i)[0].json, error: err}); operationResult.push({json: this.getInputData(i)[0].json, error: err});
} else { } else {
if (err.context) err.context.itemIndex = i;
throw err; throw err;
} }
} }

View File

@@ -6,6 +6,7 @@ import {
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
IPairedItemData,
} from 'n8n-workflow'; } from 'n8n-workflow';
@@ -261,6 +262,10 @@ export class Merge implements INodeType {
newItem = { newItem = {
json: {}, json: {},
pairedItem: [
dataInput1[i].pairedItem as IPairedItemData,
dataInput2[i].pairedItem as IPairedItemData,
],
}; };
if (dataInput1[i].binary !== undefined) { if (dataInput1[i].binary !== undefined) {
@@ -305,7 +310,15 @@ export class Merge implements INodeType {
for (entry1 of dataInput1) { for (entry1 of dataInput1) {
for (entry2 of dataInput2) { for (entry2 of dataInput2) {
returnData.push({json: {...(entry1.json), ...(entry2.json)}}); returnData.push({
json: {
...(entry1.json), ...(entry2.json),
},
pairedItem: [
entry1.pairedItem as IPairedItemData,
entry2.pairedItem as IPairedItemData,
],
});
} }
} }
return [returnData]; return [returnData];

View File

@@ -380,6 +380,9 @@ export class MoveBinaryData implements INodeType {
// Copy the whole JSON data as data on any level can be renamed // Copy the whole JSON data as data on any level can be renamed
newItem = { newItem = {
json: {}, json: {},
pairedItem: {
item: itemIndex,
},
}; };
if (mode === 'binaryToJson') { if (mode === 'binaryToJson') {

View File

@@ -76,6 +76,9 @@ export class ReadBinaryFile implements INodeType {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: item.json, json: item.json,
binary: {}, binary: {},
pairedItem: {
item: itemIndex,
},
}; };
if (item.binary !== undefined) { if (item.binary !== undefined) {
@@ -90,7 +93,14 @@ export class ReadBinaryFile implements INodeType {
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }}); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -65,6 +65,9 @@ export class ReadBinaryFiles implements INodeType {
[dataPropertyName]: await this.helpers.prepareBinaryData(data, filePath), [dataPropertyName]: await this.helpers.prepareBinaryData(data, filePath),
}, },
json: {}, json: {},
pairedItem: {
item: 0,
},
}; };
items.push(item); items.push(item);

View File

@@ -60,7 +60,14 @@ export class ReadPdf implements INodeType {
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }}); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -88,6 +88,9 @@ export class RenameKeys implements INodeType {
// Copy the whole JSON data as data on any level can be renamed // Copy the whole JSON data as data on any level can be renamed
newItem = { newItem = {
json: JSON.parse(JSON.stringify(item.json)), json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: itemIndex,
},
}; };
if (item.binary !== undefined) { if (item.binary !== undefined) {

View File

@@ -145,6 +145,7 @@ export class Set implements INodeType {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: {}, json: {},
pairedItem: item.pairedItem,
}; };
if (keepOnlySet !== true) { if (keepOnlySet !== true) {

View File

@@ -94,6 +94,12 @@ export class SplitInBatches implements INodeType {
return null; return null;
} }
returnItems.map((item, index) => {
item.pairedItem = {
item: index,
};
});
return this.prepareOutputData(returnItems); return this.prepareOutputData(returnItems);
} }
} }

View File

@@ -391,17 +391,36 @@ export class SpreadsheetFile implements INodeType {
if (options.headerRow === false) { if (options.headerRow === false) {
// Data was returned as an array - https://github.com/SheetJS/sheetjs#json // Data was returned as an array - https://github.com/SheetJS/sheetjs#json
for (const rowData of sheetJson) { for (const rowData of sheetJson) {
newItems.push({ json: { row: rowData } } as INodeExecutionData); newItems.push({
json: {
row: rowData,
},
pairedItem: {
item: i,
},
} as INodeExecutionData);
} }
} else { } else {
for (const rowData of sheetJson) { for (const rowData of sheetJson) {
newItems.push({ json: rowData } as INodeExecutionData); newItems.push({
json: rowData,
pairedItem: {
item: i,
},
} as INodeExecutionData);
} }
} }
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
newItems.push({json:{ error: error.message }}); newItems.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
continue; continue;
} }
throw error; throw error;
@@ -466,6 +485,9 @@ export class SpreadsheetFile implements INodeType {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: {}, json: {},
binary: {}, binary: {},
pairedItem: {
item: 0,
},
}; };
let fileName = `spreadsheet.${fileFormat}`; let fileName = `spreadsheet.${fileFormat}`;
@@ -478,7 +500,14 @@ export class SpreadsheetFile implements INodeType {
newItems.push(newItem); newItems.push(newItem);
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
newItems.push({json:{ error: error.message }}); newItems.push({
json: {
error: error.message,
},
pairedItem: {
item: 0,
},
});
} else { } else {
throw error; throw error;
} }

View File

@@ -281,7 +281,7 @@ export class Ssh implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
const returnData: IDataObject[] = []; const returnItems: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 0) as string; const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string; const operation = this.getNodeParameter('operation', 0) as string;
@@ -333,7 +333,12 @@ export class Ssh implements INodeType {
const command = this.getNodeParameter('command', i) as string; const command = this.getNodeParameter('command', i) as string;
const cwd = this.getNodeParameter('cwd', i) as string; const cwd = this.getNodeParameter('cwd', i) as string;
returnData.push(await ssh.execCommand(command, { cwd, })); returnItems.push({
json: await ssh.execCommand(command, { cwd, }),
pairedItem: {
item: i,
},
});
} }
} }
@@ -352,6 +357,9 @@ export class Ssh implements INodeType {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: items[i].json, json: items[i].json,
binary: {}, binary: {},
pairedItem: {
item: i,
},
}; };
if (items[i].binary !== undefined) { if (items[i].binary !== undefined) {
@@ -395,7 +403,14 @@ export class Ssh implements INodeType {
await ssh.putFile(path, `${parameterPath}${(parameterPath.charAt(parameterPath.length - 1) === '/') ? '' : '/'}${fileName || binaryData.fileName}`); await ssh.putFile(path, `${parameterPath}${(parameterPath.charAt(parameterPath.length - 1) === '/') ? '' : '/'}${fileName || binaryData.fileName}`);
returnData.push({ success: true }); returnItems.push({
json: {
success: true,
},
pairedItem: {
item: i,
},
});
} }
} }
} catch (error) { } catch (error) {
@@ -407,7 +422,14 @@ export class Ssh implements INodeType {
}, },
}; };
} else { } else {
returnData.push({ error: error.message }); returnItems.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
} }
continue; continue;
} }
@@ -428,7 +450,7 @@ export class Ssh implements INodeType {
// For file downloads the files get attached to the existing items // For file downloads the files get attached to the existing items
return this.prepareOutputData(items); return this.prepareOutputData(items);
} else { } else {
return [this.helpers.returnJsonArray(returnData)]; return this.prepareOutputData(returnItems);
} }
} }
} }

View File

@@ -76,6 +76,9 @@ export class WriteBinaryFile implements INodeType {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: {}, json: {},
pairedItem: {
item: itemIndex,
},
}; };
Object.assign(newItem.json, item.json); Object.assign(newItem.json, item.json);
@@ -100,7 +103,14 @@ export class WriteBinaryFile implements INodeType {
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } }); returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -262,13 +262,23 @@ export class Xml implements INodeType {
json: { json: {
[dataPropertyName]: builder.buildObject(items[itemIndex].json), [dataPropertyName]: builder.buildObject(items[itemIndex].json),
}, },
pairedItem: {
item: itemIndex,
},
}); });
} else { } else {
throw new NodeOperationError(this.getNode(), `The operation "${mode}" is not known!`); throw new NodeOperationError(this.getNode(), `The operation "${mode}" is not known!`);
} }
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
items[itemIndex] = ({json:{ error: error.message }}); items[itemIndex] = ({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue; continue;
} }
throw error; throw error;

View File

@@ -4,6 +4,8 @@ import { DateTime, Duration, Interval } from 'luxon';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { import {
ExpressionError,
IExecuteData,
INode, INode,
INodeExecutionData, INodeExecutionData,
INodeParameters, INodeParameters,
@@ -21,10 +23,11 @@ import {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
tmpl.brackets.set('{{ }}'); tmpl.brackets.set('{{ }}');
// Make sure that it does not always print an error when it could not resolve // Make sure that error get forwarded
// a variable
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
tmpl.tmpl.errorHandler = () => {}; tmpl.tmpl.errorHandler = (error: Error) => {
throw error;
};
export class Expression { export class Expression {
workflow: Workflow; workflow: Workflow;
@@ -71,6 +74,7 @@ export class Expression {
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
timezone: string, timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys, additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
returnObjectAsString = false, returnObjectAsString = false,
selfData = {}, selfData = {},
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { ): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
@@ -98,6 +102,7 @@ export class Expression {
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
executeData,
-1, -1,
selfData, selfData,
); );
@@ -148,13 +153,24 @@ export class Expression {
data.constructor = {}; data.constructor = {};
// Execute the expression // Execute the expression
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let returnValue;
try { try {
if (/([^a-zA-Z0-9"']window[^a-zA-Z0-9"'])/g.test(parameterValue)) { if (/([^a-zA-Z0-9"']window[^a-zA-Z0-9"'])/g.test(parameterValue)) {
throw new Error(`window is not allowed`); throw new Error(`window is not allowed`);
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const returnValue = tmpl.tmpl(parameterValue, data); returnValue = tmpl.tmpl(parameterValue, data);
} catch (error) {
if (error instanceof ExpressionError) {
// Ignore all errors except if they are ExpressionErrors and they are supposed
// to fail the execution
if (error.context.failExecution) {
throw error;
}
}
}
if (typeof returnValue === 'function') { if (typeof returnValue === 'function') {
throw new Error('Expression resolved to a function. Please add "()"'); throw new Error('Expression resolved to a function. Please add "()"');
@@ -163,12 +179,9 @@ export class Expression {
return this.convertObjectValueToString(returnValue); return this.convertObjectValueToString(returnValue);
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return // eslint-disable-next-line @typescript-eslint/no-unsafe-return
return returnValue; return returnValue;
} catch (e) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
throw new Error(`Expression is not valid: ${e.message}`);
}
} }
/** /**
@@ -186,6 +199,7 @@ export class Expression {
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
timezone: string, timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys, additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultValue?: boolean | number | string, defaultValue?: boolean | number | string,
): boolean | number | string | undefined { ): boolean | number | string | undefined {
if (parameterValue === undefined) { if (parameterValue === undefined) {
@@ -213,6 +227,7 @@ export class Expression {
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
executeData,
) as boolean | number | string | undefined; ) as boolean | number | string | undefined;
} }
@@ -231,6 +246,7 @@ export class Expression {
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
timezone: string, timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys, additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultValue: defaultValue:
| NodeParameterValue | NodeParameterValue
| INodeParameters | INodeParameters
@@ -265,6 +281,7 @@ export class Expression {
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
executeData,
false, false,
selfData, selfData,
); );
@@ -280,6 +297,7 @@ export class Expression {
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
executeData,
false, false,
selfData, selfData,
); );
@@ -310,6 +328,7 @@ export class Expression {
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
timezone: string, timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys, additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
returnObjectAsString = false, returnObjectAsString = false,
selfData = {}, selfData = {},
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] { ): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
@@ -336,6 +355,7 @@ export class Expression {
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
executeData,
returnObjectAsString, returnObjectAsString,
selfData, selfData,
); );
@@ -351,6 +371,7 @@ export class Expression {
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
executeData,
returnObjectAsString, returnObjectAsString,
selfData, selfData,
); );
@@ -369,6 +390,7 @@ export class Expression {
mode, mode,
timezone, timezone,
additionalKeys, additionalKeys,
executeData,
returnObjectAsString, returnObjectAsString,
selfData, selfData,
); );

View File

@@ -0,0 +1,48 @@
// eslint-disable-next-line import/no-cycle
import { ExecutionBaseError } from './NodeErrors';
/**
* Class for instantiating an expression error
*/
export class ExpressionError extends ExecutionBaseError {
constructor(
message: string,
options?: {
causeDetailed?: string;
description?: string;
runIndex?: number;
itemIndex?: number;
messageTemplate?: string;
parameter?: string;
failExecution?: boolean;
},
) {
super(new Error(message));
if (options?.description !== undefined) {
this.description = options.description;
}
if (options?.causeDetailed !== undefined) {
this.context.causeDetailed = options.causeDetailed;
}
if (options?.runIndex !== undefined) {
this.context.runIndex = options.runIndex;
}
if (options?.itemIndex !== undefined) {
this.context.itemIndex = options.itemIndex;
}
if (options?.parameter !== undefined) {
this.context.parameter = options.parameter;
}
if (options?.messageTemplate !== undefined) {
this.context.messageTemplate = options.messageTemplate;
}
this.context.failExecution = !!options?.failExecution;
}
}

View File

@@ -306,6 +306,11 @@ export interface ICredentialDataDecryptedObject {
// Second array index: The different connections (if one node is connected to multiple nodes) // Second array index: The different connections (if one node is connected to multiple nodes)
export type NodeInputConnections = IConnection[][]; export type NodeInputConnections = IConnection[][];
export interface INodeConnection {
sourceIndex: number;
destinationIndex: number;
}
export interface INodeConnections { export interface INodeConnections {
// Input name // Input name
[key: string]: NodeInputConnections; [key: string]: NodeInputConnections;
@@ -363,6 +368,7 @@ export interface IGetExecuteFunctions {
inputData: ITaskDataConnections, inputData: ITaskDataConnections,
node: INode, node: INode,
additionalData: IWorkflowExecuteAdditionalData, additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData,
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
): IExecuteFunctions; ): IExecuteFunctions;
} }
@@ -377,6 +383,7 @@ export interface IGetExecuteSingleFunctions {
node: INode, node: INode,
itemIndex: number, itemIndex: number,
additionalData: IWorkflowExecuteAdditionalData, additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData,
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
): IExecuteSingleFunctions; ): IExecuteSingleFunctions;
} }
@@ -403,9 +410,17 @@ export interface IGetExecuteWebhookFunctions {
): IWebhookFunctions; ): IWebhookFunctions;
} }
export interface ISourceDataConnections {
// Key for each input type and because there can be multiple inputs of the same type it is an array
// null is also allowed because if we still need data for a later while executing the workflow set teompoary to null
// the nodes get as input TaskDataConnections which is identical to this one except that no null is allowed.
[key: string]: Array<ISourceData[] | null>;
}
export interface IExecuteData { export interface IExecuteData {
data: ITaskDataConnections; data: ITaskDataConnections;
node: INode; node: INode;
source: ITaskDataConnectionsSource | null;
} }
export type IContextObject = { export type IContextObject = {
@@ -514,6 +529,7 @@ export interface IExecuteFunctions {
getWorkflowStaticData(type: string): IDataObject; getWorkflowStaticData(type: string): IDataObject;
getRestApiUrl(): string; getRestApiUrl(): string;
getTimezone(): string; getTimezone(): string;
getExecuteData(): IExecuteData;
getWorkflow(): IWorkflowMetadata; getWorkflow(): IWorkflowMetadata;
prepareOutputData( prepareOutputData(
outputData: INodeExecutionData[], outputData: INodeExecutionData[],
@@ -553,6 +569,7 @@ export interface IExecuteSingleFunctions {
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object; ): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object;
getRestApiUrl(): string; getRestApiUrl(): string;
getTimezone(): string; getTimezone(): string;
getExecuteData(): IExecuteData;
getWorkflow(): IWorkflowMetadata; getWorkflow(): IWorkflowMetadata;
getWorkflowDataProxy(): IWorkflowDataProxyData; getWorkflowDataProxy(): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject; getWorkflowStaticData(type: string): IDataObject;
@@ -801,11 +818,25 @@ export interface IBinaryKeyData {
[key: string]: IBinaryData; [key: string]: IBinaryData;
} }
export interface IPairedItemData {
item: number;
input?: number; // If undefined "0" gets used
}
export interface INodeExecutionData { export interface INodeExecutionData {
[key: string]: IDataObject | IBinaryKeyData | NodeApiError | NodeOperationError | undefined; [key: string]:
| IDataObject
| IBinaryKeyData
| IPairedItemData
| IPairedItemData[]
| NodeApiError
| NodeOperationError
| number
| undefined;
json: IDataObject; json: IDataObject;
binary?: IBinaryKeyData; binary?: IBinaryKeyData;
error?: NodeApiError | NodeOperationError; error?: NodeApiError | NodeOperationError;
pairedItem?: IPairedItemData | IPairedItemData[] | number;
} }
export interface INodeExecuteFunctions { export interface INodeExecuteFunctions {
@@ -1262,6 +1293,7 @@ export interface IRunExecutionData {
contextData: IExecuteContextData; contextData: IExecuteContextData;
nodeExecutionStack: IExecuteData[]; nodeExecutionStack: IExecuteData[];
waitingExecution: IWaitingForExecution; waitingExecution: IWaitingForExecution;
waitingExecutionSource: IWaitingForExecutionSource | null;
}; };
waitTill?: Date; waitTill?: Date;
} }
@@ -1277,9 +1309,16 @@ export interface ITaskData {
executionTime: number; executionTime: number;
data?: ITaskDataConnections; data?: ITaskDataConnections;
error?: ExecutionError; error?: ExecutionError;
source: Array<ISourceData | null>; // Is an array as nodes have multiple inputs
} }
// The data for al the different kind of connectons (like main) and all the indexes export interface ISourceData {
previousNode: string;
previousNodeOutput?: number; // If undefined "0" gets used
previousNodeRun?: number; // If undefined "0" gets used
}
// The data for all the different kind of connectons (like main) and all the indexes
export interface ITaskDataConnections { export interface ITaskDataConnections {
// Key for each input type and because there can be multiple inputs of the same type it is an array // Key for each input type and because there can be multiple inputs of the same type it is an array
// null is also allowed because if we still need data for a later while executing the workflow set teompoary to null // null is also allowed because if we still need data for a later while executing the workflow set teompoary to null
@@ -1296,6 +1335,21 @@ export interface IWaitingForExecution {
}; };
} }
export interface ITaskDataConnectionsSource {
// Key for each input type and because there can be multiple inputs of the same type it is an array
// null is also allowed because if we still need data for a later while executing the workflow set teompoary to null
// the nodes get as input TaskDataConnections which is identical to this one except that no null is allowed.
[key: string]: Array<ISourceData | null>;
}
export interface IWaitingForExecutionSource {
// Node name
[key: string]: {
// Run index
[key: number]: ITaskDataConnectionsSource;
};
}
export interface IWorkflowBase { export interface IWorkflowBase {
id?: number | string | any; id?: number | string | any;
name: string; name: string;

View File

@@ -7,7 +7,7 @@
// eslint-disable-next-line max-classes-per-file // eslint-disable-next-line max-classes-per-file
import { parseString } from 'xml2js'; import { parseString } from 'xml2js';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { INode, IStatusCodeMessages, JsonObject } from '.'; import { IDataObject, INode, IStatusCodeMessages, JsonObject } from '.';
/** /**
* Top-level properties where an error message can be found in an API response. * Top-level properties where an error message can be found in an API response.
@@ -56,29 +56,42 @@ const ERROR_STATUS_PROPERTIES = [
*/ */
const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data']; const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
/** export abstract class ExecutionBaseError extends Error {
* Base class for specific NodeError-types, with functionality for finding
* a value recursively inside an error object.
*/
abstract class NodeError extends Error {
description: string | null | undefined; description: string | null | undefined;
cause: Error | JsonObject; cause: Error | JsonObject;
node: INode;
timestamp: number; timestamp: number;
constructor(node: INode, error: Error | JsonObject) { context: IDataObject = {};
constructor(error: Error | ExecutionBaseError | JsonObject) {
super(); super();
this.name = this.constructor.name; this.name = this.constructor.name;
this.cause = error; this.cause = error;
this.node = node;
this.timestamp = Date.now(); this.timestamp = Date.now();
if (error.message) { if (error.message) {
this.message = error.message as string; this.message = error.message as string;
} }
if (Object.prototype.hasOwnProperty.call(error, 'context')) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.context = (error as any).context;
}
}
}
/**
* Base class for specific NodeError-types, with functionality for finding
* a value recursively inside an error object.
*/
abstract class NodeError extends ExecutionBaseError {
node: INode;
constructor(node: INode, error: Error | JsonObject) {
super(error);
this.node = node;
} }
/** /**
@@ -203,7 +216,11 @@ abstract class NodeError extends Error {
* Class for instantiating an operational error, e.g. an invalid credentials error. * Class for instantiating an operational error, e.g. an invalid credentials error.
*/ */
export class NodeOperationError extends NodeError { export class NodeOperationError extends NodeError {
constructor(node: INode, error: Error | string, options?: { description: string }) { constructor(
node: INode,
error: Error | string,
options?: { description?: string; runIndex?: number; itemIndex?: number },
) {
if (typeof error === 'string') { if (typeof error === 'string') {
error = new Error(error); error = new Error(error);
} }
@@ -212,6 +229,14 @@ export class NodeOperationError extends NodeError {
if (options?.description) { if (options?.description) {
this.description = options.description; this.description = options.description;
} }
if (options?.runIndex !== undefined) {
this.context.runIndex = options.runIndex;
}
if (options?.itemIndex !== undefined) {
this.context.itemIndex = options.itemIndex;
}
} }
} }
@@ -249,7 +274,16 @@ export class NodeApiError extends NodeError {
description, description,
httpCode, httpCode,
parseXml, parseXml,
}: { message?: string; description?: string; httpCode?: string; parseXml?: boolean } = {}, runIndex,
itemIndex,
}: {
message?: string;
description?: string;
httpCode?: string;
parseXml?: boolean;
runIndex?: number;
itemIndex?: number;
} = {},
) { ) {
super(node, error); super(node, error);
if (error.error) { if (error.error) {
@@ -272,6 +306,9 @@ export class NodeApiError extends NodeError {
} }
this.description = this.findProperty(error, ERROR_MESSAGE_PROPERTIES, ERROR_NESTING_PROPERTIES); this.description = this.findProperty(error, ERROR_MESSAGE_PROPERTIES, ERROR_NESTING_PROPERTIES);
if (runIndex !== undefined) this.context.runIndex = runIndex;
if (itemIndex !== undefined) this.context.itemIndex = itemIndex;
} }
private setDescriptionFromXml(xml: string) { private setDescriptionFromXml(xml: string) {

View File

@@ -939,6 +939,7 @@ export function getNodeWebhooks(
'internal', 'internal',
additionalData.timezone, additionalData.timezone,
{}, {},
undefined,
false, false,
) as boolean; ) as boolean;
const restartWebhook: boolean = workflow.expression.getSimpleParameterValue( const restartWebhook: boolean = workflow.expression.getSimpleParameterValue(
@@ -947,6 +948,7 @@ export function getNodeWebhooks(
'internal', 'internal',
additionalData.timezone, additionalData.timezone,
{}, {},
undefined,
false, false,
) as boolean; ) as boolean;
const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath, restartWebhook); const path = getNodeWebhookPath(workflowId, node, nodeWebhookPath, isFullPath, restartWebhook);
@@ -957,6 +959,7 @@ export function getNodeWebhooks(
mode, mode,
additionalData.timezone, additionalData.timezone,
{}, {},
undefined,
'GET', 'GET',
); );

View File

@@ -29,7 +29,9 @@ import {
ITaskDataConnections, ITaskDataConnections,
IWorkflowDataProxyAdditionalKeys, IWorkflowDataProxyAdditionalKeys,
IWorkflowExecuteAdditionalData, IWorkflowExecuteAdditionalData,
NodeApiError,
NodeHelpers, NodeHelpers,
NodeOperationError,
NodeParameterValue, NodeParameterValue,
Workflow, Workflow,
WorkflowExecuteMode, WorkflowExecuteMode,
@@ -37,6 +39,7 @@ import {
import { import {
IDataObject, IDataObject,
IExecuteData,
IExecuteSingleFunctions, IExecuteSingleFunctions,
IN8nRequestOperations, IN8nRequestOperations,
INodeProperties, INodeProperties,
@@ -77,6 +80,7 @@ export class RoutingNode {
inputData: ITaskDataConnections, inputData: ITaskDataConnections,
runIndex: number, runIndex: number,
nodeType: INodeType, nodeType: INodeType,
executeData: IExecuteData,
nodeExecuteFunctions: INodeExecuteFunctions, nodeExecuteFunctions: INodeExecuteFunctions,
credentialsDecrypted?: ICredentialsDecrypted, credentialsDecrypted?: ICredentialsDecrypted,
): Promise<INodeExecutionData[][] | null | undefined> { ): Promise<INodeExecutionData[][] | null | undefined> {
@@ -97,6 +101,7 @@ export class RoutingNode {
inputData, inputData,
this.node, this.node,
this.additionalData, this.additionalData,
executeData,
this.mode, this.mode,
); );
@@ -119,6 +124,7 @@ export class RoutingNode {
this.node, this.node,
i, i,
this.additionalData, this.additionalData,
executeData,
this.mode, this.mode,
); );
@@ -145,6 +151,7 @@ export class RoutingNode {
value, value,
i, i,
runIndex, runIndex,
executeData,
{ $credentials: credentials }, { $credentials: credentials },
true, true,
) as string; ) as string;
@@ -160,6 +167,7 @@ export class RoutingNode {
value, value,
i, i,
runIndex, runIndex,
executeData,
{ $credentials: credentials }, { $credentials: credentials },
true, true,
) as string | NodeParameterValue; ) as string | NodeParameterValue;
@@ -198,7 +206,7 @@ export class RoutingNode {
returnData.push({ json: {}, error: error.message }); returnData.push({ json: {}, error: error.message });
continue; continue;
} }
throw error; throw new NodeApiError(this.node, error, { runIndex, itemIndex: i });
} }
} }
@@ -254,9 +262,10 @@ export class RoutingNode {
}); });
}); });
} catch (e) { } catch (e) {
throw new Error( throw new NodeOperationError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions this.node,
`The rootProperty "${action.properties.property}" could not be found on item.`, `The rootProperty "${action.properties.property}" could not be found on item.`,
{ runIndex, itemIndex },
); );
} }
} }
@@ -269,6 +278,7 @@ export class RoutingNode {
value, value,
itemIndex, itemIndex,
runIndex, runIndex,
executeSingleFunctions.getExecuteData(),
{ $response: responseData, $value: parameterValue }, { $response: responseData, $value: parameterValue },
false, false,
) as IDataObject, ) as IDataObject,
@@ -315,6 +325,7 @@ export class RoutingNode {
propertyValue, propertyValue,
itemIndex, itemIndex,
runIndex, runIndex,
executeSingleFunctions.getExecuteData(),
{ {
$response: responseData, $response: responseData,
$responseItem: item.json, $responseItem: item.json,
@@ -338,6 +349,7 @@ export class RoutingNode {
destinationProperty, destinationProperty,
itemIndex, itemIndex,
runIndex, runIndex,
executeSingleFunctions.getExecuteData(),
{ $response: responseData, $value: parameterValue }, { $response: responseData, $value: parameterValue },
false, false,
) as string; ) as string;
@@ -512,8 +524,10 @@ export class RoutingNode {
| IDataObject[] | IDataObject[]
| undefined; | undefined;
if (tempResponseValue === undefined) { if (tempResponseValue === undefined) {
throw new Error( throw new NodeOperationError(
this.node,
`The rootProperty "${properties.rootProperty}" could not be found on item.`, `The rootProperty "${properties.rootProperty}" could not be found on item.`,
{ runIndex, itemIndex },
); );
} }
@@ -546,6 +560,7 @@ export class RoutingNode {
parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], parameterValue: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
itemIndex: number, itemIndex: number,
runIndex: number, runIndex: number,
executeData: IExecuteData,
additionalKeys?: IWorkflowDataProxyAdditionalKeys, additionalKeys?: IWorkflowDataProxyAdditionalKeys,
returnObjectAsString = false, returnObjectAsString = false,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | string { ): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | string {
@@ -560,6 +575,7 @@ export class RoutingNode {
this.mode, this.mode,
this.additionalData.timezone, this.additionalData.timezone,
additionalKeys ?? {}, additionalKeys ?? {},
executeData,
returnObjectAsString, returnObjectAsString,
); );
} }
@@ -617,6 +633,7 @@ export class RoutingNode {
propertyValue, propertyValue,
itemIndex, itemIndex,
runIndex, runIndex,
executeSingleFunctions.getExecuteData(),
{ ...additionalKeys, $value: parameterValue }, { ...additionalKeys, $value: parameterValue },
true, true,
) as string; ) as string;
@@ -633,6 +650,7 @@ export class RoutingNode {
propertyName, propertyName,
itemIndex, itemIndex,
runIndex, runIndex,
executeSingleFunctions.getExecuteData(),
additionalKeys, additionalKeys,
true, true,
) as string; ) as string;
@@ -647,6 +665,7 @@ export class RoutingNode {
valueString, valueString,
itemIndex, itemIndex,
runIndex, runIndex,
executeSingleFunctions.getExecuteData(),
{ ...additionalKeys, $value: value }, { ...additionalKeys, $value: value },
true, true,
) as string; ) as string;
@@ -680,6 +699,7 @@ export class RoutingNode {
paginateValue, paginateValue,
itemIndex, itemIndex,
runIndex, runIndex,
executeSingleFunctions.getExecuteData(),
{ ...additionalKeys, $value: parameterValue }, { ...additionalKeys, $value: parameterValue },
true, true,
) as string; ) as string;
@@ -701,6 +721,7 @@ export class RoutingNode {
maxResultsValue, maxResultsValue,
itemIndex, itemIndex,
runIndex, runIndex,
executeSingleFunctions.getExecuteData(),
{ ...additionalKeys, $value: parameterValue }, { ...additionalKeys, $value: parameterValue },
true, true,
) as string; ) as string;

View File

@@ -48,8 +48,10 @@ import {
import { import {
IConnection, IConnection,
IDataObject,
IConnectedNode, IConnectedNode,
IDataObject,
IExecuteData,
INodeConnection,
IObservableObject, IObservableObject,
IRun, IRun,
IRunNodeResponse, IRunNodeResponse,
@@ -805,34 +807,28 @@ export class Workflow {
} }
/** /**
* Returns via which output of the parent-node the node * Returns via which output of the parent-node and index the current node
* is connected to. * they are connected
* *
* @param {string} nodeName The node to check how it is connected with parent node * @param {string} nodeName The node to check how it is connected with parent node
* @param {string} parentNodeName The parent node to get the output index of * @param {string} parentNodeName The parent node to get the output index of
* @param {string} [type='main'] * @param {string} [type='main']
* @param {*} [depth=-1] * @param {*} [depth=-1]
* @param {string[]} [checkedNodes] * @param {string[]} [checkedNodes]
* @returns {(number | undefined)} * @returns {(INodeConnection | undefined)}
* @memberof Workflow * @memberof Workflow
*/ */
getNodeConnectionOutputIndex( getNodeConnectionIndexes(
nodeName: string, nodeName: string,
parentNodeName: string, parentNodeName: string,
type = 'main', type = 'main',
depth = -1, depth = -1,
checkedNodes?: string[], checkedNodes?: string[],
): number | undefined { ): INodeConnection | undefined {
const node = this.getNode(parentNodeName); const node = this.getNode(parentNodeName);
if (node === null) { if (node === null) {
return undefined; return undefined;
} }
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion) as INodeType;
if (nodeType.description.outputs.length === 1) {
// If the parent node has only one output, it can only be connected
// to that one. So no further checking is required.
return 0;
}
depth = depth === -1 ? -1 : depth; depth = depth === -1 ? -1 : depth;
const newDepth = depth === -1 ? depth : depth - 1; const newDepth = depth === -1 ? depth : depth - 1;
@@ -860,11 +856,19 @@ export class Workflow {
checkedNodes.push(nodeName); checkedNodes.push(nodeName);
let outputIndex: number | undefined; let outputIndex: INodeConnection | undefined;
for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) { for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) {
for (const connection of connectionsByIndex) { for (
let destinationIndex = 0;
destinationIndex < connectionsByIndex.length;
destinationIndex++
) {
const connection = connectionsByIndex[destinationIndex];
if (parentNodeName === connection.node) { if (parentNodeName === connection.node) {
return connection.index; return {
sourceIndex: connection.index,
destinationIndex,
};
} }
if (checkedNodes.includes(connection.node)) { if (checkedNodes.includes(connection.node)) {
@@ -872,7 +876,7 @@ export class Workflow {
continue; continue;
} }
outputIndex = this.getNodeConnectionOutputIndex( outputIndex = this.getNodeConnectionIndexes(
connection.node, connection.node,
parentNodeName, parentNodeName,
type, type,
@@ -1157,8 +1161,7 @@ export class Workflow {
/** /**
* Executes the given node. * Executes the given node.
* *
* @param {INode} node * @param {IExecuteData} executionData
* @param {ITaskDataConnections} inputData
* @param {IRunExecutionData} runExecutionData * @param {IRunExecutionData} runExecutionData
* @param {number} runIndex * @param {number} runIndex
* @param {IWorkflowExecuteAdditionalData} additionalData * @param {IWorkflowExecuteAdditionalData} additionalData
@@ -1168,14 +1171,16 @@ export class Workflow {
* @memberof Workflow * @memberof Workflow
*/ */
async runNode( async runNode(
node: INode, executionData: IExecuteData,
inputData: ITaskDataConnections,
runExecutionData: IRunExecutionData, runExecutionData: IRunExecutionData,
runIndex: number, runIndex: number,
additionalData: IWorkflowExecuteAdditionalData, additionalData: IWorkflowExecuteAdditionalData,
nodeExecuteFunctions: INodeExecuteFunctions, nodeExecuteFunctions: INodeExecuteFunctions,
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
): Promise<IRunNodeResponse> { ): Promise<IRunNodeResponse> {
const { node } = executionData;
let inputData = executionData.data;
if (node.disabled === true) { if (node.disabled === true) {
// If node is disabled simply pass the data through // If node is disabled simply pass the data through
// return NodeRunHelpers. // return NodeRunHelpers.
@@ -1254,6 +1259,7 @@ export class Workflow {
node, node,
itemIndex, itemIndex,
additionalData, additionalData,
executionData,
mode, mode,
); );
@@ -1283,6 +1289,7 @@ export class Workflow {
inputData, inputData,
node, node,
additionalData, additionalData,
executionData,
mode, mode,
); );
return { data: await nodeType.execute.call(thisArgs) }; return { data: await nodeType.execute.call(thisArgs) };
@@ -1356,7 +1363,13 @@ export class Workflow {
); );
return { return {
data: await routingNode.runNode(inputData, runIndex, nodeType, nodeExecuteFunctions), data: await routingNode.runNode(
inputData,
runIndex,
nodeType,
executionData,
nodeExecuteFunctions,
),
}; };
} }

View File

@@ -12,10 +12,15 @@ import * as jmespath from 'jmespath';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import { import {
ExpressionError,
IDataObject, IDataObject,
IExecuteData,
INodeExecutionData, INodeExecutionData,
INodeParameters, INodeParameters,
IPairedItemData,
IRunExecutionData, IRunExecutionData,
ISourceData,
ITaskData,
IWorkflowDataProxyAdditionalKeys, IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData, IWorkflowDataProxyData,
NodeHelpers, NodeHelpers,
@@ -47,6 +52,8 @@ export class WorkflowDataProxy {
private additionalKeys: IWorkflowDataProxyAdditionalKeys; private additionalKeys: IWorkflowDataProxyAdditionalKeys;
private executeData: IExecuteData | undefined;
private defaultTimezone: string; private defaultTimezone: string;
private timezone: string; private timezone: string;
@@ -62,6 +69,7 @@ export class WorkflowDataProxy {
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
defaultTimezone: string, defaultTimezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys, additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultReturnRunIndex = -1, defaultReturnRunIndex = -1,
selfData = {}, selfData = {},
) { ) {
@@ -78,7 +86,7 @@ export class WorkflowDataProxy {
this.timezone = (this.workflow.settings.timezone as string) || this.defaultTimezone; this.timezone = (this.workflow.settings.timezone as string) || this.defaultTimezone;
this.selfData = selfData; this.selfData = selfData;
this.additionalKeys = additionalKeys; this.additionalKeys = additionalKeys;
this.executeData = executeData;
Settings.defaultZone = this.timezone; Settings.defaultZone = this.timezone;
} }
@@ -202,6 +210,7 @@ export class WorkflowDataProxy {
that.mode, that.mode,
that.timezone, that.timezone,
that.additionalKeys, that.additionalKeys,
that.executeData,
); );
} }
@@ -234,17 +243,26 @@ export class WorkflowDataProxy {
// Long syntax got used to return data from node in path // Long syntax got used to return data from node in path
if (that.runExecutionData === null) { if (that.runExecutionData === null) {
throw new Error(`Workflow did not run so do not have any execution-data.`); throw new ExpressionError(`Workflow did not run so do not have any execution-data.`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
} }
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) { if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
if (that.workflow.getNode(nodeName)) { if (that.workflow.getNode(nodeName)) {
throw new Error( throw new ExpressionError(
`The node "${nodeName}" hasn't been executed yet, so you can't reference its output data`, `The node "${nodeName}" hasn't been executed yet, so you can't reference its output data`,
{
runIndex: that.runIndex,
itemIndex: that.itemIndex,
},
); );
} else {
throw new Error(`No node called "${nodeName}" in this workflow`);
} }
throw new ExpressionError(`No node called "${nodeName}" in this workflow`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
} }
runIndex = runIndex === undefined ? that.defaultReturnRunIndex : runIndex; runIndex = runIndex === undefined ? that.defaultReturnRunIndex : runIndex;
@@ -252,32 +270,42 @@ export class WorkflowDataProxy {
runIndex === -1 ? that.runExecutionData.resultData.runData[nodeName].length - 1 : runIndex; runIndex === -1 ? that.runExecutionData.resultData.runData[nodeName].length - 1 : runIndex;
if (that.runExecutionData.resultData.runData[nodeName].length <= runIndex) { if (that.runExecutionData.resultData.runData[nodeName].length <= runIndex) {
throw new Error(`Run ${runIndex} of node "${nodeName}" not found`); throw new ExpressionError(`Run ${runIndex} of node "${nodeName}" not found`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
} }
const taskData = that.runExecutionData.resultData.runData[nodeName][runIndex].data!; const taskData = that.runExecutionData.resultData.runData[nodeName][runIndex].data!;
if (taskData.main === null || !taskData.main.length || taskData.main[0] === null) { if (taskData.main === null || !taskData.main.length || taskData.main[0] === null) {
// throw new Error(`No data found for item-index: "${itemIndex}"`); // throw new Error(`No data found for item-index: "${itemIndex}"`);
throw new Error(`No data found from "main" input.`); throw new ExpressionError(`No data found from "main" input.`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
} }
// Check from which output to read the data. // Check from which output to read the data.
// Depends on how the nodes are connected. // Depends on how the nodes are connected.
// (example "IF" node. If node is connected to "true" or to "false" output) // (example "IF" node. If node is connected to "true" or to "false" output)
if (outputIndex === undefined) { if (outputIndex === undefined) {
// eslint-disable-next-line @typescript-eslint/no-shadow const nodeConnection = that.workflow.getNodeConnectionIndexes(
const outputIndex = that.workflow.getNodeConnectionOutputIndex(
that.activeNodeName, that.activeNodeName,
nodeName, nodeName,
'main', 'main',
); );
if (outputIndex === undefined) { if (nodeConnection === undefined) {
throw new Error( throw new ExpressionError(
`The node "${that.activeNodeName}" is not connected with node "${nodeName}" so no data can get returned from it.`, `The node "${that.activeNodeName}" is not connected with node "${nodeName}" so no data can get returned from it.`,
{
runIndex: that.runIndex,
itemIndex: that.itemIndex,
},
); );
} }
outputIndex = nodeConnection.sourceIndex;
} }
if (outputIndex === undefined) { if (outputIndex === undefined) {
@@ -285,7 +313,10 @@ export class WorkflowDataProxy {
} }
if (taskData.main.length <= outputIndex) { if (taskData.main.length <= outputIndex) {
throw new Error(`Node "${nodeName}" has no branch with index ${outputIndex}.`); throw new ExpressionError(`Node "${nodeName}" has no branch with index ${outputIndex}.`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
} }
executionData = taskData.main[outputIndex] as INodeExecutionData[]; executionData = taskData.main[outputIndex] as INodeExecutionData[];
@@ -328,9 +359,11 @@ export class WorkflowDataProxy {
if (['binary', 'data', 'json'].includes(name)) { if (['binary', 'data', 'json'].includes(name)) {
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined); const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
if (executionData.length <= that.itemIndex) { if (executionData.length <= that.itemIndex) {
throw new Error(`No data found for item-index: "${that.itemIndex}"`); throw new ExpressionError(`No data found for item-index: "${that.itemIndex}"`, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
});
} }
if (['data', 'json'].includes(name)) { if (['data', 'json'].includes(name)) {
@@ -486,10 +519,177 @@ export class WorkflowDataProxy {
return jmespath.search(data, query); return jmespath.search(data, query);
}; };
const createExpressionError = (
message: string,
context?: {
messageTemplate?: string;
description?: string;
causeDetailed?: string;
},
) => {
return new ExpressionError(message, {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
failExecution: true,
...context,
});
};
const getPairedItem = (
destinationNodeName: string,
incomingSourceData: ISourceData | null,
pairedItem: IPairedItemData,
): INodeExecutionData | null => {
let taskData: ITaskData;
let sourceData: ISourceData | null = incomingSourceData;
if (typeof pairedItem === 'number') {
pairedItem = {
item: pairedItem,
};
}
while (sourceData !== null && destinationNodeName !== sourceData.previousNode) {
taskData =
that.runExecutionData!.resultData.runData[sourceData.previousNode][
sourceData?.previousNodeRun || 0
];
const previousNodeOutput = sourceData.previousNodeOutput || 0;
if (previousNodeOutput >= taskData.data!.main.length) {
// `Could not resolve as the defined node-output is not valid on node '${sourceData.previousNode}'.`
throw createExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%%',
description: `Apologies, this is an internal error. See details for more information`,
causeDetailed: 'Referencing a non-existent output on a node, problem with source data',
});
}
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
// `Could not resolve as the defined item index is not valid on node '${sourceData.previousNode}'.
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to an item which does not exist`,
causeDetailed: `The pairedItem data points to an item ${pairedItem.item} which does not exist on node ${sourceData.previousNode} (output node did probably supply a wrong one)`,
});
}
const itemPreviousNode: INodeExecutionData =
taskData.data!.main[previousNodeOutput]![pairedItem.item];
if (itemPreviousNode.pairedItem === undefined) {
// `Could not resolve, as pairedItem data is missing on node '${sourceData.previousNode}'.`,
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ${sourceData.previousNode}`,
causeDetailed: `Missing pairedItem data (node ${sourceData.previousNode} did probably not supply it)`,
});
}
if (Array.isArray(itemPreviousNode.pairedItem)) {
// Item is based on multiple items so check all of them
const results = itemPreviousNode.pairedItem
// eslint-disable-next-line @typescript-eslint/no-loop-func
.map((item) => {
try {
const itemInput = item.input || 0;
if (itemInput >= taskData.source.length) {
// `Could not resolve pairedItem as the defined node input '${itemInput}' does not exist on node '${sourceData!.previousNode}'.`
// Actual error does not matter as it gets caught below and `null` will be returned
throw new Error('Not found');
}
return getPairedItem(destinationNodeName, taskData.source[itemInput], item);
} catch (error) {
// Means pairedItem could not be found
return null;
}
})
.filter((result) => result !== null);
if (results.length !== 1) {
throw createExpressionError('Invalid expression', {
messageTemplate: 'Invalid expression under %%PARAMETER%%',
description: `The expression uses data in node ${destinationNodeName} but there is more than one matching item in that node`,
});
}
return results[0];
}
// pairedItem is not an array
if (typeof itemPreviousNode.pairedItem === 'number') {
pairedItem = {
item: itemPreviousNode.pairedItem,
};
} else {
pairedItem = itemPreviousNode.pairedItem;
}
const itemInput = pairedItem.input || 0;
if (itemInput >= taskData.source.length) {
if (taskData.source.length === 0) {
// A trigger node got reached, so looks like that that item can not be resolved
throw createExpressionError('Invalid expression', {
messageTemplate: 'Invalid expression under %%PARAMETER%%',
description: `The expression uses data in node ${destinationNodeName} but there is no path back to it. Please check this node is connected to node ${that.activeNodeName} (there can be other nodes in between).`,
});
}
// `Could not resolve pairedItem as the defined node input '${itemInput}' does not exist on node '${sourceData.previousNode}'.`
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to a node input which does not exist`,
causeDetailed: `The pairedItem data points to a node input ${itemInput} which does not exist on node ${sourceData.previousNode} (node did probably supply a wrong one)`,
});
}
sourceData = taskData.source[pairedItem.input || 0] || null;
}
if (sourceData === null) {
// 'Could not resolve, proably no pairedItem exists.'
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Could not resolve, proably no pairedItem exists`,
});
}
taskData =
that.runExecutionData!.resultData.runData[sourceData.previousNode][
sourceData?.previousNodeRun || 0
];
const previousNodeOutput = sourceData.previousNodeOutput || 0;
if (previousNodeOutput >= taskData.data!.main.length) {
// `Could not resolve pairedItem as the node output '${previousNodeOutput}' does not exist on node '${sourceData.previousNode}'`
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to a node output which does not exist`,
causeDetailed: `The sourceData points to a node output ${previousNodeOutput} which does not exist on node ${sourceData.previousNode} (output node did probably supply a wrong one)`,
});
}
if (pairedItem.item >= taskData.data!.main[previousNodeOutput]!.length) {
// `Could not resolve pairedItem as the item with the index '${pairedItem.item}' does not exist on node '${sourceData.previousNode}'.`
throw createExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `Item points to an item which does not exist`,
causeDetailed: `The pairedItem data points to an item ${pairedItem.item} which does not exist on node ${sourceData.previousNode} (output node did probably supply a wrong one)`,
});
}
return taskData.data!.main[previousNodeOutput]![pairedItem.item];
};
const base = { const base = {
$: (nodeName: string) => { $: (nodeName: string) => {
if (!nodeName) { if (!nodeName) {
throw new Error(`When calling $(), please specify a node`); throw new ExpressionError('When calling $(), please specify a node', {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
failExecution: true,
});
} }
return new Proxy( return new Proxy(
@@ -497,12 +697,58 @@ export class WorkflowDataProxy {
{ {
get(target, property, receiver) { get(target, property, receiver) {
if (property === 'pairedItem') { if (property === 'pairedItem') {
return () => { return (itemIndex?: number) => {
const executionData = getNodeOutput(nodeName, 0, that.runIndex); if (itemIndex === undefined) {
if (executionData[that.itemIndex]) { itemIndex = that.itemIndex;
return executionData[that.itemIndex];
} }
return undefined;
const executionData = that.connectionInputData;
// As we operate on the incoming item we can be sure that pairedItem is not an
// array. After all can it only come from exactly one previous node via a certain
// input. For that reason do we not have to consider the array case.
const pairedItem = executionData[itemIndex].pairedItem as IPairedItemData;
if (pairedItem === undefined) {
throw new ExpressionError('Cant get data for expression', {
messageTemplate: `Cant get data for expression under %%PARAMETER%%`,
description: `To fetch the data from other nodes that this expression needs, more information is needed from the node ${that.activeNodeName}`,
causeDetailed: `Missing pairedItem data (node ${that.activeNodeName} did probably not supply it)`,
runIndex: that.runIndex,
itemIndex,
failExecution: true,
});
}
if (!that.executeData?.source) {
throw new ExpressionError('Cant get data for expression', {
messageTemplate: 'Cant get data for expression under %%PARAMETER%%',
description: `Apologies, this is an internal error. See details for more information`,
causeDetailed: `Missing sourceData (probably an internal error)`,
runIndex: that.runIndex,
itemIndex,
failExecution: true,
});
}
// Before resolving the pairedItem make sure that the requested node comes in the
// graph before the current one
const parentNodes = that.workflow.getParentNodes(that.activeNodeName);
if (!parentNodes.includes(nodeName)) {
throw new ExpressionError('Invalid expression', {
messageTemplate: 'Invalid expression under %%PARAMETER%%',
description: `The expression uses data in node ${nodeName} but there is no path back to it. Please check this node is connected to node ${that.activeNodeName} (there can be other nodes in between).`,
runIndex: that.runIndex,
itemIndex,
failExecution: true,
});
}
const sourceData: ISourceData = that.executeData?.source.main![
pairedItem.input || 0
] as ISourceData;
return getPairedItem(nodeName, sourceData, pairedItem);
}; };
} }
if (property === 'item') { if (property === 'item') {
@@ -513,6 +759,7 @@ export class WorkflowDataProxy {
runIndex = that.runIndex; runIndex = that.runIndex;
} }
const executionData = getNodeOutput(nodeName, branchIndex, runIndex); const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
if (executionData[itemIndex]) { if (executionData[itemIndex]) {
return executionData[itemIndex]; return executionData[itemIndex];
} }
@@ -645,6 +892,7 @@ export class WorkflowDataProxy {
that.mode, that.mode,
that.timezone, that.timezone,
that.additionalKeys, that.additionalKeys,
that.executeData,
); );
}, },
$item: (itemIndex: number, runIndex?: number) => { $item: (itemIndex: number, runIndex?: number) => {
@@ -660,6 +908,7 @@ export class WorkflowDataProxy {
that.mode, that.mode,
that.defaultTimezone, that.defaultTimezone,
that.additionalKeys, that.additionalKeys,
that.executeData,
defaultReturnRunIndex, defaultReturnRunIndex,
); );
return dataProxy.getDataProxy(); return dataProxy.getDataProxy();

View File

@@ -6,6 +6,7 @@ import * as ObservableObject from './ObservableObject';
export * from './DeferredPromise'; export * from './DeferredPromise';
export * from './Interfaces'; export * from './Interfaces';
export * from './Expression'; export * from './Expression';
export * from './ExpressionError';
export * from './NodeErrors'; export * from './NodeErrors';
export * as TelemetryHelpers from './TelemetryHelpers'; export * as TelemetryHelpers from './TelemetryHelpers';
export * from './RoutingNode'; export * from './RoutingNode';

View File

@@ -9,6 +9,7 @@ import {
ICredentialsEncrypted, ICredentialsEncrypted,
ICredentialsHelper, ICredentialsHelper,
IDataObject, IDataObject,
IExecuteData,
IExecuteFunctions, IExecuteFunctions,
IExecuteResponsePromiseData, IExecuteResponsePromiseData,
IExecuteSingleFunctions, IExecuteSingleFunctions,
@@ -146,6 +147,7 @@ export function getNodeParameter(
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
timezone: string, timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys, additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData: IExecuteData,
fallbackValue?: any, fallbackValue?: any,
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object { ): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object {
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
@@ -189,6 +191,7 @@ export function getExecuteFunctions(
node: INode, node: INode,
itemIndex: number, itemIndex: number,
additionalData: IWorkflowExecuteAdditionalData, additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData,
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
): IExecuteFunctions { ): IExecuteFunctions {
return ((workflow, runExecutionData, connectionInputData, inputData, node) => { return ((workflow, runExecutionData, connectionInputData, inputData, node) => {
@@ -272,6 +275,9 @@ export function getExecuteFunctions(
getTimezone: (): string => { getTimezone: (): string => {
return additionalData.timezone; return additionalData.timezone;
}, },
getExecuteData: (): IExecuteData => {
return executeData;
},
getWorkflow: () => { getWorkflow: () => {
return { return {
id: workflow.id, id: workflow.id,
@@ -291,6 +297,7 @@ export function getExecuteFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
{}, {},
executeData,
); );
return dataProxy.getDataProxy(); return dataProxy.getDataProxy();
}, },
@@ -375,6 +382,7 @@ export function getExecuteSingleFunctions(
node: INode, node: INode,
itemIndex: number, itemIndex: number,
additionalData: IWorkflowExecuteAdditionalData, additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData,
mode: WorkflowExecuteMode, mode: WorkflowExecuteMode,
): IExecuteSingleFunctions { ): IExecuteSingleFunctions {
return ((workflow, runExecutionData, connectionInputData, inputData, node, itemIndex) => { return ((workflow, runExecutionData, connectionInputData, inputData, node, itemIndex) => {
@@ -431,6 +439,9 @@ export function getExecuteSingleFunctions(
getTimezone: (): string => { getTimezone: (): string => {
return additionalData.timezone; return additionalData.timezone;
}, },
getExecuteData: (): IExecuteData => {
return executeData;
},
getNodeParameter: ( getNodeParameter: (
parameterName: string, parameterName: string,
fallbackValue?: any, fallbackValue?: any,
@@ -473,6 +484,7 @@ export function getExecuteSingleFunctions(
mode, mode,
additionalData.timezone, additionalData.timezone,
{}, {},
executeData,
); );
return dataProxy.getDataProxy(); return dataProxy.getDataProxy();
}, },

View File

@@ -15,6 +15,7 @@ import {
INodeExecuteFunctions, INodeExecuteFunctions,
IN8nRequestOperations, IN8nRequestOperations,
INodeCredentialDescription, INodeCredentialDescription,
IExecuteData,
} from '../src'; } from '../src';
import * as Helpers from './Helpers'; import * as Helpers from './Helpers';
@@ -657,6 +658,11 @@ describe('RoutingNode', () => {
node, node,
itemIndex, itemIndex,
additionalData, additionalData,
{
node,
data: {},
source: null,
},
mode, mode,
); );
@@ -1636,6 +1642,12 @@ describe('RoutingNode', () => {
mode, mode,
); );
const executeData = {
data: {},
node,
source: null,
} as IExecuteData;
// @ts-ignore // @ts-ignore
const nodeExecuteFunctions: INodeExecuteFunctions = { const nodeExecuteFunctions: INodeExecuteFunctions = {
getExecuteFunctions: () => { getExecuteFunctions: () => {
@@ -1648,6 +1660,7 @@ describe('RoutingNode', () => {
node, node,
itemIndex, itemIndex,
additionalData, additionalData,
executeData,
mode, mode,
); );
}, },
@@ -1661,6 +1674,7 @@ describe('RoutingNode', () => {
node, node,
itemIndex, itemIndex,
additionalData, additionalData,
executeData,
mode, mode,
); );
}, },
@@ -1670,6 +1684,7 @@ describe('RoutingNode', () => {
inputData, inputData,
runIndex, runIndex,
nodeType, nodeType,
executeData,
nodeExecuteFunctions, nodeExecuteFunctions,
); );

View File

@@ -1243,6 +1243,7 @@ describe('Workflow', () => {
], ],
], ],
}, },
source: [],
}, },
], ],
}, },

View File

@@ -94,6 +94,7 @@ describe('WorkflowDataProxy', () => {
], ],
], ],
}, },
source: [],
}, },
], ],
Rename: [ Rename: [
@@ -122,6 +123,7 @@ describe('WorkflowDataProxy', () => {
], ],
], ],
}, },
source: [],
}, },
], ],
}, },