mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
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:
parent
450a9aafea
commit
bdb84130d6
|
@ -591,6 +591,7 @@ export class ActiveWorkflowRunner {
|
|||
data: {
|
||||
main: data,
|
||||
},
|
||||
source: null,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -603,6 +604,7 @@ export class ActiveWorkflowRunner {
|
|||
contextData: {},
|
||||
nodeExecutionStack,
|
||||
waitingExecution: {},
|
||||
waitingExecutionSource: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
|
||||
|
|
|
@ -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: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -210,6 +210,7 @@ export class LoadNodeParameterOptions {
|
|||
inputData,
|
||||
runIndex,
|
||||
tempNode,
|
||||
{ node: node!, source: null, data: {} },
|
||||
NodeExecuteFunctions,
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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[];
|
||||
|
||||
|
|
|
@ -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 = {}) {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -355,6 +355,9 @@ export class ICalendar implements INodeType {
|
|||
binary: {
|
||||
[binaryPropertyName]: binaryData,
|
||||
},
|
||||
pairedItem: {
|
||||
item: i,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -65,6 +65,9 @@ export class ReadBinaryFiles implements INodeType {
|
|||
[dataPropertyName]: await this.helpers.prepareBinaryData(data, filePath),
|
||||
},
|
||||
json: {},
|
||||
pairedItem: {
|
||||
item: 0,
|
||||
},
|
||||
};
|
||||
|
||||
items.push(item);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -145,6 +145,7 @@ export class Set implements INodeType {
|
|||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
pairedItem: item.pairedItem,
|
||||
};
|
||||
|
||||
if (keepOnlySet !== true) {
|
||||
|
|
|
@ -94,6 +94,12 @@ export class SplitInBatches implements INodeType {
|
|||
return null;
|
||||
}
|
||||
|
||||
returnItems.map((item, index) => {
|
||||
item.pairedItem = {
|
||||
item: index,
|
||||
};
|
||||
});
|
||||
|
||||
return this.prepareOutputData(returnItems);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
48
packages/workflow/src/ExpressionError.ts
Normal file
48
packages/workflow/src/ExpressionError.ts
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t 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('Can’t get data for expression', {
|
||||
messageTemplate: `Can’t 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('Can’t get data for expression', {
|
||||
messageTemplate: `Can’t 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('Can’t get data for expression', {
|
||||
messageTemplate: `Can’t 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('Can’t get data for expression', {
|
||||
messageTemplate: `Can’t 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('Can’t get data for expression', {
|
||||
messageTemplate: `Can’t 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('Can’t get data for expression', {
|
||||
messageTemplate: `Can’t 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('Can’t get data for expression', {
|
||||
messageTemplate: `Can’t 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('Can’t get data for expression', {
|
||||
messageTemplate: 'Can’t 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();
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -1243,6 +1243,7 @@ describe('Workflow', () => {
|
|||
],
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -94,6 +94,7 @@ describe('WorkflowDataProxy', () => {
|
|||
],
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
Rename: [
|
||||
|
@ -122,6 +123,7 @@ describe('WorkflowDataProxy', () => {
|
|||
],
|
||||
],
|
||||
},
|
||||
source: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue