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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 1317 additions and 152 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,9 +22,12 @@ import {
IRun,
IRunData,
IRunExecutionData,
ISourceData,
ITaskData,
ITaskDataConnections,
ITaskDataConnectionsSource,
IWaitingForExecution,
IWaitingForExecutionSource,
IWorkflowExecuteAdditionalData,
LoggerProxy as Logger,
NodeApiError,
@ -61,6 +64,7 @@ export class WorkflowExecute {
contextData: {},
nodeExecutionStack: [],
waitingExecution: {},
waitingExecutionSource: {},
},
};
}
@ -106,6 +110,7 @@ export class WorkflowExecute {
],
],
},
source: null,
},
];
@ -121,6 +126,7 @@ export class WorkflowExecute {
contextData: {},
nodeExecutionStack,
waitingExecution: {},
waitingExecutionSource: {},
},
};
@ -157,10 +163,12 @@ export class WorkflowExecute {
// the data from runData
const nodeExecutionStack: IExecuteData[] = [];
const waitingExecution: IWaitingForExecution = {};
const waitingExecutionSource: IWaitingForExecutionSource = {};
for (const startNode of startNodes) {
incomingNodeConnections = workflow.connectionsByDestinationNode[startNode];
const incomingData: INodeExecutionData[][] = [];
let incomingSourceData: ITaskDataConnectionsSource | null = null;
if (incomingNodeConnections === undefined) {
// If it has no incoming data add the default empty data
@ -171,6 +179,7 @@ export class WorkflowExecute {
]);
} else {
// Get the data of the incoming connections
incomingSourceData = { main: [] };
for (const connections of incomingNodeConnections.main) {
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
connection = connections[inputIndex];
@ -178,6 +187,9 @@ export class WorkflowExecute {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
runData[connection.node][runIndex].data![connection.type][connection.index]!,
);
incomingSourceData.main.push({
previousNode: connection.node,
});
}
}
}
@ -187,6 +199,7 @@ export class WorkflowExecute {
data: {
main: incomingData,
},
source: incomingSourceData,
};
nodeExecutionStack.push(executeData);
@ -201,12 +214,15 @@ export class WorkflowExecute {
if (waitingExecution[destinationNode] === undefined) {
waitingExecution[destinationNode] = {};
waitingExecutionSource[destinationNode] = {};
}
if (waitingExecution[destinationNode][runIndex] === undefined) {
waitingExecution[destinationNode][runIndex] = {};
waitingExecutionSource[destinationNode][runIndex] = {};
}
if (waitingExecution[destinationNode][runIndex][connection.type] === undefined) {
waitingExecution[destinationNode][runIndex][connection.type] = [];
waitingExecutionSource[destinationNode][runIndex][connection.type] = [];
}
if (runData[connection.node] !== undefined) {
@ -215,8 +231,14 @@ export class WorkflowExecute {
waitingExecution[destinationNode][runIndex][connection.type].push(
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 {
waitingExecution[destinationNode][runIndex][connection.type].push(null);
waitingExecutionSource[destinationNode][runIndex][connection.type].push(null);
}
}
}
@ -241,6 +263,7 @@ export class WorkflowExecute {
contextData: {},
nodeExecutionStack,
waitingExecution,
waitingExecutionSource,
},
};
@ -303,12 +326,17 @@ export class WorkflowExecute {
// Node has multiple inputs
let nodeWasWaiting = true;
if (this.runExecutionData.executionData!.waitingExecutionSource === null) {
this.runExecutionData.executionData!.waitingExecutionSource = {};
}
// Check if there is already data for the node
if (
this.runExecutionData.executionData!.waitingExecution[connectionData.node] === undefined
) {
// Node does not have data yet so create a new empty one
this.runExecutionData.executionData!.waitingExecution[connectionData.node] = {};
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node] = {};
nodeWasWaiting = false;
}
if (
@ -319,6 +347,10 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex] = {
main: [],
};
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][runIndex] =
{
main: [],
};
for (
let i = 0;
i < workflow.connectionsByDestinationNode[connectionData.node].main.length;
@ -327,6 +359,10 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][
runIndex
].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[
connectionData.index
] = null;
this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
].main[connectionData.index] = null;
} else {
this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex].main[
connectionData.index
] = 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
@ -364,15 +410,32 @@ export class WorkflowExecute {
if (allDataFound) {
// All data exists for node to be executed
// So add it to the execution stack
this.runExecutionData.executionData!.nodeExecutionStack.push({
const executionStackItem = {
node: workflow.nodes[connectionData.node],
data: this.runExecutionData.executionData!.waitingExecution[connectionData.node][
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
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node][runIndex];
delete this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node][
runIndex
];
if (
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
delete this.runExecutionData.executionData!.waitingExecution[connectionData.node];
delete this.runExecutionData.executionData!.waitingExecutionSource[connectionData.node];
}
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: {
main: connectionDataArray,
},
source: {
main: [
{
previousNode: parentNodeName,
previousNodeOutput: outputIndex || undefined,
previousNodeRun: runIndex || undefined,
},
],
},
});
}
}
@ -660,6 +742,7 @@ export class WorkflowExecute {
data: {
main: executionData.data.main,
} as ITaskDataConnections,
source: [],
},
],
},
@ -691,6 +774,29 @@ export class WorkflowExecute {
this.runExecutionData.executionData!.nodeExecutionStack.shift() as IExecuteData;
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}"`, {
node: executionNode.name,
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();
let maxTries = 1;
@ -813,8 +916,7 @@ export class WorkflowExecute {
workflowId: workflow.id,
});
const runNodeData = await workflow.runNode(
executionData.node,
executionData.data,
executionData,
this.runExecutionData,
runIndex,
this.additionalData,
@ -834,6 +936,30 @@ export class WorkflowExecute {
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) {
// Node did not get executed
nodeSuccessData = null;
@ -885,6 +1011,7 @@ export class WorkflowExecute {
taskData = {
startTime,
executionTime: new Date().getTime() - startTime,
source: executionData.source === null ? [] : executionData.source.main,
};
if (executionError !== undefined) {

View file

@ -144,7 +144,10 @@ export default mixins(
const workflow = this.getWorkflow();
const activeNode: INodeUi | null = this.$store.getters.activeNode;
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[] = [];
@ -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 = {
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,

View file

@ -1,7 +1,7 @@
<template>
<div>
<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>
<details>
@ -9,6 +9,13 @@
<font-awesome-icon class="error-details__icon" icon="angle-right" /> {{ $locale.baseText('nodeErrorView.details') }}
</summary>
<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">
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix box-card__title">
@ -19,6 +26,18 @@
</div>
</el-card>
</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">
<el-card class="box-card" shadow="never">
<div slot="header" class="clearfix box-card__title">
@ -79,6 +98,16 @@ import mixins from 'vue-typed-mixins';
import {
MAX_DISPLAY_DATA_SIZE,
} from '@/constants';
import {
INodeUi,
} from '@/Interface';
import {
INodeProperties,
INodePropertyCollection,
INodePropertyOptions,
INodeTypeDescription,
} from 'n8n-workflow';
export default mixins(
copyPaste,
@ -95,8 +124,72 @@ export default mixins(
displayCause(): boolean {
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: {
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() {
this.copyToClipboard(JSON.stringify(this.error.cause));
this.copySuccess();

View file

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

View file

@ -374,13 +374,19 @@ export default mixins(
return returnData;
},
getNodeContext (workflow: Workflow, runExecutionData: IRunExecutionData | null, parentNode: string[], nodeName: string, filterText: string): IVariableSelectorOption[] | null {
const inputIndex = 0;
const itemIndex = 0;
const inputName = 'main';
const runIndex = 0;
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) {
return returnData;
@ -493,7 +499,8 @@ export default mixins(
// Check from which output to read the data.
// Depends on how the nodes are connected.
// (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[];

View file

@ -27,6 +27,8 @@ import {
IWorkflowDataProxyAdditionalKeys,
Workflow,
NodeHelpers,
IExecuteData,
INodeConnection,
} from 'n8n-workflow';
import {
@ -59,9 +61,12 @@ export const workflowHelpers = mixins(
)
.extend({
methods: {
// Returns connectionInputData to be able to execute an expression.
connectionInputData (parentNode: string[], inputName: string, runIndex: number, inputIndex: number): INodeExecutionData[] | null {
let connectionInputData = null;
executeData(parentNode: string[], currentNode: string, inputName: string, runIndex: number): IExecuteData {
const executeData = {
node: {},
data: {},
source: null,
} as IExecuteData;
if (parentNode.length) {
// 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;
if (workflowRunData === null) {
return null;
return executeData;
}
if (!workflowRunData[parentNodeName] ||
workflowRunData[parentNodeName].length <= runIndex ||
!workflowRunData[parentNodeName][runIndex].hasOwnProperty('data') ||
workflowRunData[parentNodeName][runIndex].data === undefined ||
!workflowRunData[parentNodeName][runIndex].data!.hasOwnProperty(inputName) ||
workflowRunData[parentNodeName][runIndex].data![inputName].length <= inputIndex
!workflowRunData[parentNodeName][runIndex].data!.hasOwnProperty(inputName)
) {
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 = [];
} 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[]) {
const itemIndex = 0;
const runIndex = 0;
const inputName = 'main';
const activeNode = this.$store.getters.activeNode;
const workflow = this.getWorkflow();
const parentNode = workflow.getParentNodes(activeNode.name, inputName, 1);
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;
if (executionData === null) {
@ -415,7 +466,13 @@ export const workflowHelpers = mixins(
$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 = {}) {

View file

@ -400,6 +400,9 @@
"nodeErrorView.details": "Details",
"nodeErrorView.error": "ERROR",
"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.stack": "Stack",
"nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed": "The error cause is too large to be displayed",

View file

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

View file

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

View file

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

View file

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

View file

@ -204,11 +204,23 @@ export class EmailSend implements INodeType {
// Send the email
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()) {
returnData.push({json:{ error: error.message }});
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
throw error;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -752,7 +752,12 @@ return 0;`,
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;
newItem = { json: {} };
newItem = {
json: {},
pairedItem: Array.from({length}, (_, i) => i).map(index => {
return {
item: index,
};
}),
};
// tslint:disable-next-line: no-any
const values: { [key: string]: any } = {};
const outputFields: string[] = [];
@ -899,9 +913,10 @@ return 0;`,
}
keys = fieldsToCompare.map(key => (key.trim()));
}
// This solution is O(nlogn)
// 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
newItems.sort((a, b) => {
let result = 0;
@ -962,7 +977,7 @@ return 0;`,
let data = items.filter((_, index) => !removedIndexes.includes(index));
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -60,7 +60,14 @@ export class ReadPdf implements INodeType {
} catch (error) {
if (this.continueOnFail()) {
returnData.push({json:{ error: error.message }});
returnData.push({
json: {
error: error.message,
},
pairedItem: {
item: itemIndex,
},
});
continue;
}
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
newItem = {
json: JSON.parse(JSON.stringify(item.json)),
pairedItem: {
item: itemIndex,
},
};
if (item.binary !== undefined) {

View file

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

View file

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

View file

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

View file

@ -281,7 +281,7 @@ export class Ssh implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const returnItems: INodeExecutionData[] = [];
const resource = this.getNodeParameter('resource', 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 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 = {
json: items[i].json,
binary: {},
pairedItem: {
item: i,
},
};
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}`);
returnData.push({ success: true });
returnItems.push({
json: {
success: true,
},
pairedItem: {
item: i,
},
});
}
}
} catch (error) {
@ -407,7 +422,14 @@ export class Ssh implements INodeType {
},
};
} else {
returnData.push({ error: error.message });
returnItems.push({
json: {
error: error.message,
},
pairedItem: {
item: i,
},
});
}
continue;
}
@ -428,7 +450,7 @@ export class Ssh implements INodeType {
// For file downloads the files get attached to the existing items
return this.prepareOutputData(items);
} else {
return [this.helpers.returnJsonArray(returnData)];
return this.prepareOutputData(returnItems);
}
}
}

View file

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

View file

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

View file

@ -4,6 +4,8 @@ import { DateTime, Duration, Interval } from 'luxon';
// eslint-disable-next-line import/no-cycle
import {
ExpressionError,
IExecuteData,
INode,
INodeExecutionData,
INodeParameters,
@ -21,10 +23,11 @@ import {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
tmpl.brackets.set('{{ }}');
// Make sure that it does not always print an error when it could not resolve
// a variable
// Make sure that error get forwarded
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
tmpl.tmpl.errorHandler = () => {};
tmpl.tmpl.errorHandler = (error: Error) => {
throw error;
};
export class Expression {
workflow: Workflow;
@ -71,6 +74,7 @@ export class Expression {
mode: WorkflowExecuteMode,
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
returnObjectAsString = false,
selfData = {},
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
@ -98,6 +102,7 @@ export class Expression {
mode,
timezone,
additionalKeys,
executeData,
-1,
selfData,
);
@ -148,27 +153,35 @@ export class Expression {
data.constructor = {};
// Execute the expression
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let returnValue;
try {
if (/([^a-zA-Z0-9"']window[^a-zA-Z0-9"'])/g.test(parameterValue)) {
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
const returnValue = tmpl.tmpl(parameterValue, data);
if (typeof returnValue === 'function') {
throw new Error('Expression resolved to a function. Please add "()"');
} else if (returnValue !== null && typeof returnValue === 'object') {
if (returnObjectAsString) {
return this.convertObjectValueToString(returnValue);
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;
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
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}`);
}
if (typeof returnValue === 'function') {
throw new Error('Expression resolved to a function. Please add "()"');
} else if (returnValue !== null && typeof returnValue === 'object') {
if (returnObjectAsString) {
return this.convertObjectValueToString(returnValue);
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return returnValue;
}
/**
@ -186,6 +199,7 @@ export class Expression {
mode: WorkflowExecuteMode,
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultValue?: boolean | number | string,
): boolean | number | string | undefined {
if (parameterValue === undefined) {
@ -213,6 +227,7 @@ export class Expression {
mode,
timezone,
additionalKeys,
executeData,
) as boolean | number | string | undefined;
}
@ -231,6 +246,7 @@ export class Expression {
mode: WorkflowExecuteMode,
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultValue:
| NodeParameterValue
| INodeParameters
@ -265,6 +281,7 @@ export class Expression {
mode,
timezone,
additionalKeys,
executeData,
false,
selfData,
);
@ -280,6 +297,7 @@ export class Expression {
mode,
timezone,
additionalKeys,
executeData,
false,
selfData,
);
@ -310,6 +328,7 @@ export class Expression {
mode: WorkflowExecuteMode,
timezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
returnObjectAsString = false,
selfData = {},
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] {
@ -336,6 +355,7 @@ export class Expression {
mode,
timezone,
additionalKeys,
executeData,
returnObjectAsString,
selfData,
);
@ -351,6 +371,7 @@ export class Expression {
mode,
timezone,
additionalKeys,
executeData,
returnObjectAsString,
selfData,
);
@ -369,6 +390,7 @@ export class Expression {
mode,
timezone,
additionalKeys,
executeData,
returnObjectAsString,
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)
export type NodeInputConnections = IConnection[][];
export interface INodeConnection {
sourceIndex: number;
destinationIndex: number;
}
export interface INodeConnections {
// Input name
[key: string]: NodeInputConnections;
@ -363,6 +368,7 @@ export interface IGetExecuteFunctions {
inputData: ITaskDataConnections,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData,
mode: WorkflowExecuteMode,
): IExecuteFunctions;
}
@ -377,6 +383,7 @@ export interface IGetExecuteSingleFunctions {
node: INode,
itemIndex: number,
additionalData: IWorkflowExecuteAdditionalData,
executeData: IExecuteData,
mode: WorkflowExecuteMode,
): IExecuteSingleFunctions;
}
@ -403,9 +410,17 @@ export interface IGetExecuteWebhookFunctions {
): 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 {
data: ITaskDataConnections;
node: INode;
source: ITaskDataConnectionsSource | null;
}
export type IContextObject = {
@ -514,6 +529,7 @@ export interface IExecuteFunctions {
getWorkflowStaticData(type: string): IDataObject;
getRestApiUrl(): string;
getTimezone(): string;
getExecuteData(): IExecuteData;
getWorkflow(): IWorkflowMetadata;
prepareOutputData(
outputData: INodeExecutionData[],
@ -553,6 +569,7 @@ export interface IExecuteSingleFunctions {
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object;
getRestApiUrl(): string;
getTimezone(): string;
getExecuteData(): IExecuteData;
getWorkflow(): IWorkflowMetadata;
getWorkflowDataProxy(): IWorkflowDataProxyData;
getWorkflowStaticData(type: string): IDataObject;
@ -801,11 +818,25 @@ export interface IBinaryKeyData {
[key: string]: IBinaryData;
}
export interface IPairedItemData {
item: number;
input?: number; // If undefined "0" gets used
}
export interface INodeExecutionData {
[key: string]: IDataObject | IBinaryKeyData | NodeApiError | NodeOperationError | undefined;
[key: string]:
| IDataObject
| IBinaryKeyData
| IPairedItemData
| IPairedItemData[]
| NodeApiError
| NodeOperationError
| number
| undefined;
json: IDataObject;
binary?: IBinaryKeyData;
error?: NodeApiError | NodeOperationError;
pairedItem?: IPairedItemData | IPairedItemData[] | number;
}
export interface INodeExecuteFunctions {
@ -1262,6 +1293,7 @@ export interface IRunExecutionData {
contextData: IExecuteContextData;
nodeExecutionStack: IExecuteData[];
waitingExecution: IWaitingForExecution;
waitingExecutionSource: IWaitingForExecutionSource | null;
};
waitTill?: Date;
}
@ -1277,9 +1309,16 @@ export interface ITaskData {
executionTime: number;
data?: ITaskDataConnections;
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 {
// 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
@ -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 {
id?: number | string | any;
name: string;

View file

@ -7,7 +7,7 @@
// eslint-disable-next-line max-classes-per-file
import { parseString } from 'xml2js';
// 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.
@ -56,29 +56,42 @@ const ERROR_STATUS_PROPERTIES = [
*/
const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
/**
* Base class for specific NodeError-types, with functionality for finding
* a value recursively inside an error object.
*/
abstract class NodeError extends Error {
export abstract class ExecutionBaseError extends Error {
description: string | null | undefined;
cause: Error | JsonObject;
node: INode;
timestamp: number;
constructor(node: INode, error: Error | JsonObject) {
context: IDataObject = {};
constructor(error: Error | ExecutionBaseError | JsonObject) {
super();
this.name = this.constructor.name;
this.cause = error;
this.node = node;
this.timestamp = Date.now();
if (error.message) {
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.
*/
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') {
error = new Error(error);
}
@ -212,6 +229,14 @@ export class NodeOperationError extends NodeError {
if (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,
httpCode,
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);
if (error.error) {
@ -272,6 +306,9 @@ export class NodeApiError extends NodeError {
}
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) {

View file

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

View file

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

View file

@ -48,8 +48,10 @@ import {
import {
IConnection,
IDataObject,
IConnectedNode,
IDataObject,
IExecuteData,
INodeConnection,
IObservableObject,
IRun,
IRunNodeResponse,
@ -805,34 +807,28 @@ export class Workflow {
}
/**
* Returns via which output of the parent-node the node
* is connected to.
* Returns via which output of the parent-node and index the current node
* they are connected
*
* @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} [type='main']
* @param {*} [depth=-1]
* @param {string[]} [checkedNodes]
* @returns {(number | undefined)}
* @returns {(INodeConnection | undefined)}
* @memberof Workflow
*/
getNodeConnectionOutputIndex(
getNodeConnectionIndexes(
nodeName: string,
parentNodeName: string,
type = 'main',
depth = -1,
checkedNodes?: string[],
): number | undefined {
): INodeConnection | undefined {
const node = this.getNode(parentNodeName);
if (node === null) {
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;
const newDepth = depth === -1 ? depth : depth - 1;
@ -860,11 +856,19 @@ export class Workflow {
checkedNodes.push(nodeName);
let outputIndex: number | undefined;
let outputIndex: INodeConnection | undefined;
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) {
return connection.index;
return {
sourceIndex: connection.index,
destinationIndex,
};
}
if (checkedNodes.includes(connection.node)) {
@ -872,7 +876,7 @@ export class Workflow {
continue;
}
outputIndex = this.getNodeConnectionOutputIndex(
outputIndex = this.getNodeConnectionIndexes(
connection.node,
parentNodeName,
type,
@ -1157,8 +1161,7 @@ export class Workflow {
/**
* Executes the given node.
*
* @param {INode} node
* @param {ITaskDataConnections} inputData
* @param {IExecuteData} executionData
* @param {IRunExecutionData} runExecutionData
* @param {number} runIndex
* @param {IWorkflowExecuteAdditionalData} additionalData
@ -1168,14 +1171,16 @@ export class Workflow {
* @memberof Workflow
*/
async runNode(
node: INode,
inputData: ITaskDataConnections,
executionData: IExecuteData,
runExecutionData: IRunExecutionData,
runIndex: number,
additionalData: IWorkflowExecuteAdditionalData,
nodeExecuteFunctions: INodeExecuteFunctions,
mode: WorkflowExecuteMode,
): Promise<IRunNodeResponse> {
const { node } = executionData;
let inputData = executionData.data;
if (node.disabled === true) {
// If node is disabled simply pass the data through
// return NodeRunHelpers.
@ -1254,6 +1259,7 @@ export class Workflow {
node,
itemIndex,
additionalData,
executionData,
mode,
);
@ -1283,6 +1289,7 @@ export class Workflow {
inputData,
node,
additionalData,
executionData,
mode,
);
return { data: await nodeType.execute.call(thisArgs) };
@ -1356,7 +1363,13 @@ export class Workflow {
);
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
import {
ExpressionError,
IDataObject,
IExecuteData,
INodeExecutionData,
INodeParameters,
IPairedItemData,
IRunExecutionData,
ISourceData,
ITaskData,
IWorkflowDataProxyAdditionalKeys,
IWorkflowDataProxyData,
NodeHelpers,
@ -47,6 +52,8 @@ export class WorkflowDataProxy {
private additionalKeys: IWorkflowDataProxyAdditionalKeys;
private executeData: IExecuteData | undefined;
private defaultTimezone: string;
private timezone: string;
@ -62,6 +69,7 @@ export class WorkflowDataProxy {
mode: WorkflowExecuteMode,
defaultTimezone: string,
additionalKeys: IWorkflowDataProxyAdditionalKeys,
executeData?: IExecuteData,
defaultReturnRunIndex = -1,
selfData = {},
) {
@ -78,7 +86,7 @@ export class WorkflowDataProxy {
this.timezone = (this.workflow.settings.timezone as string) || this.defaultTimezone;
this.selfData = selfData;
this.additionalKeys = additionalKeys;
this.executeData = executeData;
Settings.defaultZone = this.timezone;
}
@ -202,6 +210,7 @@ export class WorkflowDataProxy {
that.mode,
that.timezone,
that.additionalKeys,
that.executeData,
);
}
@ -234,17 +243,26 @@ export class WorkflowDataProxy {
// Long syntax got used to return data from node in path
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.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`,
{
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;
@ -252,32 +270,42 @@ export class WorkflowDataProxy {
runIndex === -1 ? that.runExecutionData.resultData.runData[nodeName].length - 1 : 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!;
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 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.
// Depends on how the nodes are connected.
// (example "IF" node. If node is connected to "true" or to "false" output)
if (outputIndex === undefined) {
// eslint-disable-next-line @typescript-eslint/no-shadow
const outputIndex = that.workflow.getNodeConnectionOutputIndex(
const nodeConnection = that.workflow.getNodeConnectionIndexes(
that.activeNodeName,
nodeName,
'main',
);
if (outputIndex === undefined) {
throw new Error(
if (nodeConnection === undefined) {
throw new ExpressionError(
`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) {
@ -285,7 +313,10 @@ export class WorkflowDataProxy {
}
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[];
@ -328,9 +359,11 @@ export class WorkflowDataProxy {
if (['binary', 'data', 'json'].includes(name)) {
const executionData = that.getNodeExecutionData(nodeName, shortSyntax, undefined);
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)) {
@ -486,10 +519,177 @@ export class WorkflowDataProxy {
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 = {
$: (nodeName: string) => {
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(
@ -497,12 +697,58 @@ export class WorkflowDataProxy {
{
get(target, property, receiver) {
if (property === 'pairedItem') {
return () => {
const executionData = getNodeOutput(nodeName, 0, that.runIndex);
if (executionData[that.itemIndex]) {
return executionData[that.itemIndex];
return (itemIndex?: number) => {
if (itemIndex === undefined) {
itemIndex = 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') {
@ -513,6 +759,7 @@ export class WorkflowDataProxy {
runIndex = that.runIndex;
}
const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
if (executionData[itemIndex]) {
return executionData[itemIndex];
}
@ -645,6 +892,7 @@ export class WorkflowDataProxy {
that.mode,
that.timezone,
that.additionalKeys,
that.executeData,
);
},
$item: (itemIndex: number, runIndex?: number) => {
@ -660,6 +908,7 @@ export class WorkflowDataProxy {
that.mode,
that.defaultTimezone,
that.additionalKeys,
that.executeData,
defaultReturnRunIndex,
);
return dataProxy.getDataProxy();

View file

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

View file

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

View file

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

View file

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

View file

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