diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts
index 037a6259f..31ae7ca36 100644
--- a/packages/cli/src/InternalHooks.ts
+++ b/packages/cli/src/InternalHooks.ts
@@ -160,8 +160,14 @@ export class InternalHooksClass implements IInternalHooksClass {
if (!properties.success && runData?.data.resultData.error) {
properties.error_message = runData?.data.resultData.error.message;
- let errorNodeName = runData?.data.resultData.error.node?.name;
- properties.error_node_type = runData?.data.resultData.error.node?.type;
+ let errorNodeName =
+ 'node' in runData?.data.resultData.error
+ ? runData?.data.resultData.error.node?.name
+ : undefined;
+ properties.error_node_type =
+ 'node' in runData?.data.resultData.error
+ ? runData?.data.resultData.error.node?.type
+ : undefined;
if (runData.data.resultData.lastNodeExecuted) {
const lastNode = TelemetryHelpers.getNodeTypeForName(
diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts
index 280cda93b..e6bfbfb5e 100644
--- a/packages/core/src/NodeExecuteFunctions.ts
+++ b/packages/core/src/NodeExecuteFunctions.ts
@@ -1487,11 +1487,20 @@ export async function requestWithAuthentication(
*/
export function getAdditionalKeys(
additionalData: IWorkflowExecuteAdditionalData,
+ mode: WorkflowExecuteMode,
): IWorkflowDataProxyAdditionalKeys {
const executionId = additionalData.executionId || PLACEHOLDER_EMPTY_EXECUTION_ID;
+ const resumeUrl = `${additionalData.webhookWaitingBaseUrl}/${executionId}`;
return {
+ $execution: {
+ id: executionId,
+ mode: mode === 'manual' ? 'test' : 'production',
+ resumeUrl,
+ },
+
+ // deprecated
$executionId: executionId,
- $resumeWebhookUrl: `${additionalData.webhookWaitingBaseUrl}/${executionId}`,
+ $resumeWebhookUrl: resumeUrl,
};
}
@@ -1601,7 +1610,7 @@ export async function getCredentials(
// TODO: solve using credentials via expression
// if (name.charAt(0) === '=') {
// // If the credential name is an expression resolve it
- // const additionalKeys = getAdditionalKeys(additionalData);
+ // const additionalKeys = getAdditionalKeys(additionalData, mode);
// name = workflow.expression.getParameterValue(
// name,
// runExecutionData || null,
@@ -1638,30 +1647,29 @@ export function getNode(node: INode): INode {
* Clean up parameter data to make sure that only valid data gets returned
* INFO: Currently only converts Luxon Dates as we know for sure it will not be breaking
*/
-function cleanupParameterData(inputData: NodeParameterValueType): NodeParameterValueType {
- if (inputData === null || inputData === undefined) {
- return inputData;
+function cleanupParameterData(inputData: NodeParameterValueType): void {
+ if (typeof inputData !== 'object' || inputData === null) {
+ return;
}
if (Array.isArray(inputData)) {
inputData.forEach((value) => cleanupParameterData(value));
- return inputData;
- }
-
- if (inputData.constructor.name === 'DateTime') {
- // Is a special luxon date so convert to string
- return inputData.toString();
+ return;
}
if (typeof inputData === 'object') {
Object.keys(inputData).forEach((key) => {
- inputData[key as keyof typeof inputData] = cleanupParameterData(
- inputData[key as keyof typeof inputData],
- );
+ if (typeof inputData[key as keyof typeof inputData] === 'object') {
+ if (inputData[key as keyof typeof inputData]?.constructor.name === 'DateTime') {
+ // Is a special luxon date so convert to string
+ inputData[key as keyof typeof inputData] =
+ inputData[key as keyof typeof inputData]?.toString();
+ } else {
+ cleanupParameterData(inputData[key as keyof typeof inputData]);
+ }
+ }
});
}
-
- return inputData;
}
/**
@@ -1710,7 +1718,7 @@ export function getNodeParameter(
executeData,
);
- returnData = cleanupParameterData(returnData);
+ cleanupParameterData(returnData);
} catch (e) {
if (e.context) e.context.parameter = parameterName;
e.cause = value;
@@ -1883,7 +1891,7 @@ export function getExecutePollFunctions(
itemIndex,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
undefined,
fallbackValue,
options,
@@ -2032,7 +2040,7 @@ export function getExecuteTriggerFunctions(
itemIndex,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
undefined,
fallbackValue,
options,
@@ -2160,7 +2168,7 @@ export function getExecuteFunctions(
connectionInputData,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
executeData,
);
},
@@ -2237,7 +2245,7 @@ export function getExecuteFunctions(
itemIndex,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
executeData,
fallbackValue,
options,
@@ -2272,7 +2280,7 @@ export function getExecuteFunctions(
{},
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
executeData,
);
return dataProxy.getDataProxy();
@@ -2421,7 +2429,7 @@ export function getExecuteSingleFunctions(
connectionInputData,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
executeData,
);
},
@@ -2501,7 +2509,7 @@ export function getExecuteSingleFunctions(
itemIndex,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
executeData,
fallbackValue,
options,
@@ -2521,7 +2529,7 @@ export function getExecuteSingleFunctions(
{},
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
executeData,
);
return dataProxy.getDataProxy();
@@ -2658,6 +2666,7 @@ export function getLoadOptionsFunctions(
const runExecutionData: IRunExecutionData | null = null;
const itemIndex = 0;
const runIndex = 0;
+ const mode = 'internal' as WorkflowExecuteMode;
const connectionInputData: INodeExecutionData[] = [];
return getNodeParameter(
@@ -2668,9 +2677,9 @@ export function getLoadOptionsFunctions(
node,
parameterName,
itemIndex,
- 'internal' as WorkflowExecuteMode,
+ mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
undefined,
fallbackValue,
options,
@@ -2792,7 +2801,7 @@ export function getExecuteHookFunctions(
itemIndex,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
undefined,
fallbackValue,
options,
@@ -2806,7 +2815,7 @@ export function getExecuteHookFunctions(
additionalData,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
isTest,
);
},
@@ -2945,7 +2954,7 @@ export function getExecuteWebhookFunctions(
itemIndex,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
undefined,
fallbackValue,
options,
@@ -2983,7 +2992,7 @@ export function getExecuteWebhookFunctions(
additionalData,
mode,
additionalData.timezone,
- getAdditionalKeys(additionalData),
+ getAdditionalKeys(additionalData, mode),
);
},
getTimezone: (): string => {
diff --git a/packages/editor-ui/src/components/CodeEdit.vue b/packages/editor-ui/src/components/CodeEdit.vue
index cfe829099..41106a201 100644
--- a/packages/editor-ui/src/components/CodeEdit.vue
+++ b/packages/editor-ui/src/components/CodeEdit.vue
@@ -78,6 +78,13 @@ export default mixins(
const connectionInputData = this.connectionInputData(parentNode, activeNode!.name, inputName, runIndex, nodeConnection);
const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = {
+ $execution: {
+ id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
+ mode: 'test',
+ resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
+ },
+
+ // deprecated
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
};
diff --git a/packages/editor-ui/src/components/Error/NodeErrorView.vue b/packages/editor-ui/src/components/Error/NodeErrorView.vue
index 01a695a92..244741870 100644
--- a/packages/editor-ui/src/components/Error/NodeErrorView.vue
+++ b/packages/editor-ui/src/components/Error/NodeErrorView.vue
@@ -2,7 +2,7 @@
@@ -139,28 +139,34 @@ export default mixins(
},
},
methods: {
+ replacePlaceholders (parameter: string, message: string): string {
+ const parameterName = this.parameterDisplayName(parameter, false);
+ const parameterFullName = this.parameterDisplayName(parameter, true);
+ return message.replace(/%%PARAMETER%%/g, parameterName).replace(/%%PARAMETER_FULL%%/g, parameterFullName);
+ },
getErrorDescription (): string {
if (!this.error.context || !this.error.context.descriptionTemplate) {
return this.error.description;
}
-
- const parameterName = this.parameterDisplayName(this.error.context.parameter);
- return this.error.context.descriptionTemplate.replace(/%%PARAMETER%%/g, parameterName);
+ return this.replacePlaceholders(this.error.context.parameter, this.error.context.descriptionTemplate);
},
getErrorMessage (): string {
if (!this.error.context || !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);
+ return this.replacePlaceholders(this.error.context.parameter, this.error.context.messageTemplate);
},
- parameterDisplayName(path: string) {
+ parameterDisplayName(path: string, fullPath = true) {
try {
const parameters = this.parameterName(this.parameters, path.split('.'));
if (!parameters.length) {
throw new Error();
}
+
+ if (fullPath === false) {
+ return parameters.pop()!.displayName;
+ }
return parameters.map(parameter => parameter.displayName).join(' > ');
} catch (error) {
return `Could not find parameter "${path}"`;
diff --git a/packages/editor-ui/src/components/VariableSelector.vue b/packages/editor-ui/src/components/VariableSelector.vue
index 3e7e70eaa..0aa25880f 100644
--- a/packages/editor-ui/src/components/VariableSelector.vue
+++ b/packages/editor-ui/src/components/VariableSelector.vue
@@ -420,6 +420,13 @@ export default mixins(
}
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
+ $execution: {
+ id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
+ mode: 'test',
+ resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
+ },
+
+ // deprecated
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
};
diff --git a/packages/editor-ui/src/components/mixins/pushConnection.ts b/packages/editor-ui/src/components/mixins/pushConnection.ts
index 4030bac46..0d596c4b6 100644
--- a/packages/editor-ui/src/components/mixins/pushConnection.ts
+++ b/packages/editor-ui/src/components/mixins/pushConnection.ts
@@ -1,12 +1,6 @@
import {
IExecutionsCurrentSummaryExtended,
IPushData,
- IPushDataConsoleMessage,
- IPushDataExecutionFinished,
- IPushDataExecutionStarted,
- IPushDataNodeExecuteAfter,
- IPushDataNodeExecuteBefore,
- IPushDataTestWebhook,
} from '../../Interface';
import { externalHooks } from '@/components/mixins/externalHooks';
@@ -16,7 +10,11 @@ import { titleChange } from '@/components/mixins/titleChange';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import {
+ ExpressionError,
+ IDataObject,
INodeTypeNameVersion,
+ IWorkflowBase,
+ TelemetryHelpers,
} from 'n8n-workflow';
import mixins from 'vue-typed-mixins';
@@ -215,7 +213,7 @@ export const pushConnection = mixins(
const runDataExecuted = pushData.data;
- const runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data.resultData.error);
+ const runDataExecutedErrorMessage = this.$getExecutionError(runDataExecuted.data);
const workflow = this.getCurrentWorkflow();
if (runDataExecuted.waitTill !== undefined) {
@@ -251,8 +249,48 @@ export const pushConnection = mixins(
} else if (runDataExecuted.finished !== true) {
this.$titleSet(workflow.name as string, 'ERROR');
+ if (
+ runDataExecuted.data.resultData.error!.name === 'ExpressionError' &&
+ (runDataExecuted.data.resultData.error as ExpressionError).context.functionality === 'pairedItem'
+ ) {
+ const error = runDataExecuted.data.resultData.error as ExpressionError;
+
+ this.getWorkflowDataToSave().then((workflowData) => {
+ const eventData: IDataObject = {
+ caused_by_credential: false,
+ error_message: error.description,
+ error_title: error.message,
+ error_type: error.context.type,
+ node_graph_string: JSON.stringify(TelemetryHelpers.generateNodesGraph(workflowData as IWorkflowBase, this.getNodeTypes()).nodeGraph),
+ workflow_id: this.$store.getters.workflowId,
+ };
+
+ if (error.context.nodeCause && ['no pairing info', 'invalid pairing info'].includes(error.context.type as string)) {
+ const node = workflow.getNode(error.context.nodeCause as string);
+
+ if (node) {
+ eventData.is_pinned = !!workflow.getPinDataOfNode(node.name);
+ eventData.mode = node.parameters.mode;
+ eventData.node_type = node.type;
+ eventData.operation = node.parameters.operation;
+ eventData.resource = node.parameters.resource;
+ }
+ }
+
+ this.$telemetry.track('Instance FE emitted paired item error', eventData);
+ });
+
+ }
+
+ let title: string;
+ if (runDataExecuted.data.resultData.lastNodeExecuted) {
+ title = `Problem in node ‘${runDataExecuted.data.resultData.lastNodeExecuted}‘`;
+ } else {
+ title = 'Problem executing workflow';
+ }
+
this.$showMessage({
- title: 'Problem executing workflow',
+ title,
message: runDataExecutedErrorMessage,
type: 'error',
duration: 0,
diff --git a/packages/editor-ui/src/components/mixins/showMessage.ts b/packages/editor-ui/src/components/mixins/showMessage.ts
index 814a9dc65..473fc8555 100644
--- a/packages/editor-ui/src/components/mixins/showMessage.ts
+++ b/packages/editor-ui/src/components/mixins/showMessage.ts
@@ -3,10 +3,9 @@ import { ElNotificationComponent, ElNotificationOptions } from 'element-ui/types
import mixins from 'vue-typed-mixins';
import { externalHooks } from '@/components/mixins/externalHooks';
-import { ExecutionError } from 'n8n-workflow';
+import { IRunExecutionData } from 'n8n-workflow';
import type { ElMessageBoxOptions } from 'element-ui/types/message-box';
import type { ElMessageComponent, ElMessageOptions, MessageType } from 'element-ui/types/message';
-import { isChildOf } from './helpers';
import { sanitizeHtml } from '@/utils';
let stickyNotificationQueue: ElNotificationComponent[] = [];
@@ -83,22 +82,29 @@ export const showMessage = mixins(externalHooks).extend({
return this.$message(config);
},
- $getExecutionError(error?: ExecutionError) {
- // There was a problem with executing the workflow
- let errorMessage = 'There was a problem executing the workflow!';
+ $getExecutionError(data: IRunExecutionData) {
+ const error = data.resultData.error;
- if (error && error.message) {
- let nodeName: string | undefined;
- if (error.node) {
- nodeName = typeof error.node === 'string'
- ? error.node
- : error.node.name;
+ let errorMessage: string;
+
+ if (data.resultData.lastNodeExecuted && error) {
+ errorMessage = error.message;
+ } else {
+ errorMessage = 'There was a problem executing the workflow!';
+
+ if (error && error.message) {
+ let nodeName: string | undefined;
+ if ('node' in error) {
+ nodeName = typeof error.node === 'string'
+ ? error.node
+ : error.node!.name;
+ }
+
+ const receivedError = nodeName
+ ? `${nodeName}: ${error.message}`
+ : error.message;
+ errorMessage = `There was a problem executing the workflow:
"${receivedError}"`;
}
-
- const receivedError = nodeName
- ? `${nodeName}: ${error.message}`
- : error.message;
- errorMessage = `There was a problem executing the workflow:
"${receivedError}"`;
}
return errorMessage;
diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts
index 8fc15819f..e3f237776 100644
--- a/packages/editor-ui/src/components/mixins/workflowHelpers.ts
+++ b/packages/editor-ui/src/components/mixins/workflowHelpers.ts
@@ -498,7 +498,7 @@ export const workflowHelpers = mixins(
getWebhookUrl (webhookData: IWebhookDescription, node: INode, showUrlFor?: string): string {
if (webhookData.restartWebhook === true) {
- return '$resumeWebhookUrl';
+ return '$execution.resumeUrl';
}
let baseUrl = this.$store.getters.getWebhookUrl;
if (showUrlFor === 'test') {
@@ -577,6 +577,13 @@ export const workflowHelpers = mixins(
}
const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
+ $execution: {
+ id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
+ mode: 'test',
+ resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
+ },
+
+ // deprecated
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
};
diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts
index 3569541e8..fdf8ae06b 100644
--- a/packages/editor-ui/src/constants.ts
+++ b/packages/editor-ui/src/constants.ts
@@ -309,7 +309,27 @@ export const TEST_PIN_DATA = [
code: 2,
},
];
-export const MAPPING_PARAMS = [`$evaluateExpression`, `$item`, `$jmespath`, `$node`, `$binary`, `$data`, `$env`, `$json`, `$now`, `$parameters`, `$position`, `$resumeWebhookUrl`, `$runIndex`, `$today`, `$workflow`, '$parameter'];
+export const MAPPING_PARAMS = [
+ '$binary',
+ '$data',
+ '$env',
+ '$evaluateExpression',
+ '$execution',
+ '$input',
+ '$item',
+ '$jmespath',
+ '$json',
+ '$node',
+ '$now',
+ '$parameter',
+ '$parameters',
+ '$position',
+ '$prevNode',
+ '$resumeWebhookUrl',
+ '$runIndex',
+ '$today',
+ '$workflow',
+];
export const DEFAULT_STICKY_HEIGHT = 160;
export const DEFAULT_STICKY_WIDTH = 240;
diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue
index 0c0f6663d..bd04e4e0a 100644
--- a/packages/editor-ui/src/views/NodeView.vue
+++ b/packages/editor-ui/src/views/NodeView.vue
@@ -535,8 +535,8 @@ export default mixins(
if (nodeErrorFound === false) {
const resultError = data.data.resultData.error;
- const errorMessage = this.$getExecutionError(resultError);
- const shouldTrack = resultError && resultError.node && resultError.node.type.startsWith('n8n-nodes-base');
+ const errorMessage = this.$getExecutionError(data.data);
+ const shouldTrack = resultError && 'node' in resultError && resultError.node!.type.startsWith('n8n-nodes-base');
this.$showMessage({
title: 'Failed execution',
message: errorMessage,
diff --git a/packages/nodes-base/nodes/Wait/Wait.node.ts b/packages/nodes-base/nodes/Wait/Wait.node.ts
index 0853fe387..e4b31a5a8 100644
--- a/packages/nodes-base/nodes/Wait/Wait.node.ts
+++ b/packages/nodes-base/nodes/Wait/Wait.node.ts
@@ -114,7 +114,7 @@ export class Wait implements INodeType {
],
default: 'none',
description:
- 'If and how incoming resume-webhook-requests to $resumeWebhookUrl should be authenticated for additional security',
+ 'If and how incoming resume-webhook-requests to $execution.resumeUrl should be authenticated for additional security',
},
{
displayName: 'Resume',
@@ -212,7 +212,7 @@ export class Wait implements INodeType {
// ----------------------------------
{
displayName:
- 'The webhook URL will be generated at run time. It can be referenced with the $resumeWebhookUrl variable. Send it somewhere before getting to this node. More info',
+ 'The webhook URL will be generated at run time. It can be referenced with the $execution.resumeUrl variable. Send it somewhere before getting to this node. More info',
name: 'webhookNotice',
type: 'notice',
displayOptions: {
diff --git a/packages/workflow/src/ExpressionError.ts b/packages/workflow/src/ExpressionError.ts
index a08ba186e..2d025d100 100644
--- a/packages/workflow/src/ExpressionError.ts
+++ b/packages/workflow/src/ExpressionError.ts
@@ -1,3 +1,5 @@
+/* eslint-disable import/no-cycle */
+import { IDataObject } from './Interfaces';
import { ExecutionBaseError } from './NodeErrors';
/**
@@ -10,11 +12,14 @@ export class ExpressionError extends ExecutionBaseError {
causeDetailed?: string;
description?: string;
descriptionTemplate?: string;
- runIndex?: number;
+ failExecution?: boolean;
+ functionality?: 'pairedItem';
itemIndex?: number;
messageTemplate?: string;
+ nodeCause?: string;
parameter?: string;
- failExecution?: boolean;
+ runIndex?: number;
+ type?: string;
},
) {
super(new Error(message));
@@ -23,30 +28,25 @@ export class ExpressionError extends ExecutionBaseError {
this.description = options.description;
}
- if (options?.descriptionTemplate !== undefined) {
- this.context.descriptionTemplate = options.descriptionTemplate;
- }
-
- 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;
+
+ const allowedKeys = [
+ 'causeDetailed',
+ 'descriptionTemplate',
+ 'functionality',
+ 'itemIndex',
+ 'messageTemplate',
+ 'nodeCause',
+ 'parameter',
+ 'runIndex',
+ 'type',
+ ];
+ if (options !== undefined) {
+ Object.keys(options as IDataObject).forEach((key) => {
+ if (allowedKeys.includes(key)) {
+ this.context[key] = (options as IDataObject)[key];
+ }
+ });
+ }
}
}
diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts
index 9346865a2..fe1f9bffc 100644
--- a/packages/workflow/src/Interfaces.ts
+++ b/packages/workflow/src/Interfaces.ts
@@ -11,6 +11,7 @@ import type { WorkflowHooks } from './WorkflowHooks';
import type { WorkflowActivationError } from './WorkflowActivationError';
import type { WorkflowOperationError } from './WorkflowErrors';
import type { NodeApiError, NodeOperationError } from './NodeErrors';
+import { ExpressionError } from './ExpressionError';
export interface IAdditionalCredentialOptions {
oauth2?: IOAuth2Options;
@@ -62,6 +63,7 @@ export interface IConnection {
}
export type ExecutionError =
+ | ExpressionError
| WorkflowActivationError
| WorkflowOperationError
| NodeOperationError
diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts
index 2da2f4305..adf4a8dd5 100644
--- a/packages/workflow/src/WorkflowDataProxy.ts
+++ b/packages/workflow/src/WorkflowDataProxy.ts
@@ -106,6 +106,16 @@ export class WorkflowDataProxy {
const that = this;
const node = this.workflow.nodes[nodeName];
+ if (!that.runExecutionData?.executionData) {
+ throw new ExpressionError(
+ `The workflow hasn't been executed yet, so you can't reference any context data`,
+ {
+ runIndex: that.runIndex,
+ itemIndex: that.itemIndex,
+ },
+ );
+ }
+
return new Proxy(
{},
{
@@ -128,11 +138,6 @@ export class WorkflowDataProxy {
name = name.toString();
const contextData = NodeHelpers.getContext(that.runExecutionData!, 'node', node);
- if (!contextData.hasOwnProperty(name)) {
- // Parameter does not exist on node
- throw new Error(`Could not find parameter "${name}" on context of node "${nodeName}"`);
- }
-
return contextData[name];
},
},
@@ -253,10 +258,13 @@ export class WorkflowDataProxy {
// Long syntax got used to return data from node in path
if (that.runExecutionData === null) {
- throw new ExpressionError(`Workflow did not run so do not have any execution-data.`, {
- runIndex: that.runIndex,
- itemIndex: that.itemIndex,
- });
+ throw new ExpressionError(
+ `The workflow hasn't been executed yet, so you can't reference any output data`,
+ {
+ runIndex: that.runIndex,
+ itemIndex: that.itemIndex,
+ },
+ );
}
if (!that.runExecutionData.resultData.runData.hasOwnProperty(nodeName)) {
@@ -450,6 +458,46 @@ export class WorkflowDataProxy {
);
}
+ private prevNodeGetter() {
+ const allowedValues = ['name', 'outputIndex', 'runIndex'];
+ const that = this;
+
+ return new Proxy(
+ {},
+ {
+ ownKeys(target) {
+ return allowedValues;
+ },
+ getOwnPropertyDescriptor(k) {
+ return {
+ enumerable: true,
+ configurable: true,
+ };
+ },
+ get(target, name, receiver) {
+ if (!that.executeData?.source) {
+ // Means the previous node did not get executed yet
+ return undefined;
+ }
+
+ const sourceData: ISourceData = that.executeData?.source.main![0] as ISourceData;
+
+ if (name === 'name') {
+ return sourceData.previousNode;
+ }
+ if (name === 'outputIndex') {
+ return sourceData.previousNodeOutput || 0;
+ }
+ if (name === 'runIndex') {
+ return sourceData.previousNodeRun || 0;
+ }
+
+ return Reflect.get(target, name, receiver);
+ },
+ },
+ );
+ }
+
/**
* Returns a proxy to query data from the workflow
*
@@ -472,11 +520,22 @@ export class WorkflowDataProxy {
};
},
get(target, name, receiver) {
- if (!allowedValues.includes(name.toString())) {
- throw new Error(`The key "${name.toString()}" is not supported!`);
+ if (allowedValues.includes(name.toString())) {
+ const value = that.workflow[name as keyof typeof target];
+
+ if (value === undefined && name === 'id') {
+ throw new ExpressionError('Workflow is not saved', {
+ description: `Please save the workflow first to use $workflow`,
+ runIndex: that.runIndex,
+ itemIndex: that.itemIndex,
+ failExecution: true,
+ });
+ }
+
+ return value;
}
- return that.workflow[name as keyof typeof target];
+ return Reflect.get(target, name, receiver);
},
},
);
@@ -528,27 +587,60 @@ export class WorkflowDataProxy {
return jmespath.search(data, query);
};
+ const isFunctionNode = (nodeName: string) => {
+ const node = that.workflow.getNode(nodeName);
+ return node && ['n8n-nodes-base.function', 'n8n-nodes-base.functionItem'].includes(node.type);
+ };
+
const createExpressionError = (
message: string,
context?: {
causeDetailed?: string;
description?: string;
descriptionTemplate?: string;
+ functionOverrides?: {
+ // Custom data to display for Function-Nodes
+ message?: string;
+ description?: string;
+ };
+ itemIndex?: number;
messageTemplate?: string;
+ moreInfoLink?: boolean;
+ nodeCause?: string;
+ runIndex?: number;
+ type?: string;
},
- nodeName?: string,
) => {
- if (nodeName) {
+ if (isFunctionNode(that.activeNodeName) && context?.functionOverrides) {
+ // If the node in which the error is thrown is a function node,
+ // display a different error message in case there is one defined
+ message = context.functionOverrides.message || message;
+ context.description = context.functionOverrides.description || context.description;
+ // The error will be in the code and not on an expression on a parameter
+ // so remove the messageTemplate as it would overwrite the message
+ context.messageTemplate = undefined;
+ }
+
+ if (context?.nodeCause) {
+ const nodeName = context.nodeCause;
const pinData = this.workflow.getPinDataOfNode(nodeName);
if (pinData) {
if (!context) {
context = {};
}
- message = `‘${nodeName}‘ must be unpinned to execute`;
- context.description = `To fetch the data the expression needs, The node ‘${nodeName}’ needs to execute without being pinned. Unpin it`;
- context.description = `To fetch the data for the expression, you must unpin the node '${nodeName}' and execute the workflow again.`;
- context.descriptionTemplate = `To fetch the data for the expression under '%%PARAMETER%%', you must unpin the node '${nodeName}' and execute the workflow again.`;
+ message = `‘Node ${nodeName}‘ must be unpinned to execute`;
+ context.messageTemplate = undefined;
+ context.description = `To fetch the data for the expression, you must unpin the node '${nodeName}' and execute the workflow again.`;
+ context.descriptionTemplate = `To fetch the data for the expression under '%%PARAMETER%%', you must unpin the node '${nodeName}' and execute the workflow again.`;
+ }
+
+ if (context.moreInfoLink && (pinData || isFunctionNode(nodeName))) {
+ const moreInfoLink =
+ ' More info';
+
+ context.description += moreInfoLink;
+ context.descriptionTemplate += moreInfoLink;
}
}
@@ -556,6 +648,7 @@ export class WorkflowDataProxy {
runIndex: that.runIndex,
itemIndex: that.itemIndex,
failExecution: true,
+ functionality: 'pairedItem',
...context,
});
};
@@ -575,6 +668,8 @@ export class WorkflowDataProxy {
};
}
+ let currentPairedItem = pairedItem;
+
let nodeBeforeLast: string | undefined;
while (sourceData !== null && destinationNodeName !== sourceData.previousNode) {
taskData =
@@ -584,46 +679,54 @@ export class WorkflowDataProxy {
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',
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
+ functionOverrides: {
+ message: 'Can’t get data',
},
- nodeBeforeLast,
- );
+ nodeCause: nodeBeforeLast,
+ 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',
+ type: 'internal',
+ });
}
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)`,
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
+ functionOverrides: {
+ message: 'Can’t get data',
},
- nodeBeforeLast,
- );
+ nodeCause: nodeBeforeLast,
+ description: `In node ‘${nodeBeforeLast!}’, output item ${
+ currentPairedItem.item || 0
+ } ${
+ sourceData.previousNodeRun
+ ? `of run ${(sourceData.previousNodeRun || 0).toString()} `
+ : ''
+ }points to an input item on node ‘${
+ sourceData.previousNode
+ }‘ that doesn’t exist.`,
+ type: 'invalid pairing info',
+ moreInfoLink: true,
+ });
}
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)`,
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
+ functionOverrides: {
+ message: 'Can’t get data',
},
- sourceData.previousNode,
- );
+ nodeCause: sourceData.previousNode,
+ 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}’ probably didn’t supply it)`,
+ type: 'no pairing info',
+ moreInfoLink: true,
+ });
}
if (Array.isArray(itemPreviousNode.pairedItem)) {
@@ -650,13 +753,20 @@ export class WorkflowDataProxy {
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`,
+ functionOverrides: {
+ description: `The code uses data in the node ‘${destinationNodeName}’ but there is more than one matching item in that node`,
+ message: 'Invalid code',
+ },
+ description: `The expression uses data in the node ‘${destinationNodeName}’ but there is more than one matching item in that node`,
+ type: 'multiple matches',
});
}
return results[0];
}
+ currentPairedItem = pairedItem;
+
// pairedItem is not an array
if (typeof itemPreviousNode.pairedItem === 'number') {
pairedItem = {
@@ -672,19 +782,30 @@ export class WorkflowDataProxy {
// 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).`,
+ functionOverrides: {
+ description: `The code uses data in the node ‘${destinationNodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
+ message: 'Invalid code',
+ },
+ description: `The expression uses data in the node ‘${destinationNodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
+ type: 'no connection',
+ moreInfoLink: true,
});
}
- // `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)`,
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
+ functionOverrides: {
+ message: `Can’t get data`,
},
- nodeBeforeLast,
- );
+ nodeCause: nodeBeforeLast,
+ description: `In node ‘${sourceData.previousNode}’, output item ${
+ currentPairedItem.item || 0
+ } of ${
+ sourceData.previousNodeRun
+ ? `of run ${(sourceData.previousNodeRun || 0).toString()} `
+ : ''
+ }points to a branch that doesn’t exist.`,
+ type: 'invalid pairing info',
+ });
}
nodeBeforeLast = sourceData.previousNode;
@@ -692,15 +813,16 @@ export class WorkflowDataProxy {
}
if (sourceData === null) {
- // 'Could not resolve, probably no pairedItem exists.'
- throw createExpressionError(
- 'Can’t get data for expression',
- {
- messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’`,
- description: `Could not resolve, probably no pairedItem exists`,
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
+ functionOverrides: {
+ message: `Can’t get data`,
},
- nodeBeforeLast,
- );
+ nodeCause: nodeBeforeLast,
+ description: `Could not resolve, proably no pairedItem exists`,
+ type: 'no pairing info',
+ moreInfoLink: true,
+ });
}
taskData =
@@ -710,25 +832,36 @@ export class WorkflowDataProxy {
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%%’`,
+ messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
+ functionOverrides: {
+ message: `Can’t get data`,
+ },
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)`,
+ type: 'invalid pairing info',
});
}
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)`,
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
+ functionOverrides: {
+ message: `Can’t get data`,
},
- nodeBeforeLast,
- );
+ nodeCause: nodeBeforeLast,
+ description: `In node ‘${nodeBeforeLast!}’, output item ${
+ currentPairedItem.item || 0
+ } ${
+ sourceData.previousNodeRun
+ ? `of run ${(sourceData.previousNodeRun || 0).toString()} `
+ : ''
+ }points to an input item on node ‘${
+ sourceData.previousNode
+ }‘ that doesn’t exist.`,
+ type: 'invalid pairing info',
+ moreInfoLink: true,
+ });
}
return taskData.data!.main[previousNodeOutput]![pairedItem.item];
@@ -737,20 +870,26 @@ export class WorkflowDataProxy {
const base = {
$: (nodeName: string) => {
if (!nodeName) {
- throw new ExpressionError('When calling $(), please specify a node', {
- runIndex: that.runIndex,
- itemIndex: that.itemIndex,
- failExecution: true,
- });
+ throw createExpressionError('When calling $(), please specify a node');
+ }
+
+ const referencedNode = that.workflow.getNode(nodeName);
+ if (referencedNode === null) {
+ throw createExpressionError(`No node called ‘${nodeName}‘`);
}
return new Proxy(
{},
{
get(target, property, receiver) {
- if (property === 'pairedItem') {
- return (itemIndex?: number) => {
+ if (['pairedItem', 'itemMatching', 'item'].includes(property as string)) {
+ const pairedItemMethod = (itemIndex?: number) => {
if (itemIndex === undefined) {
+ if (property === 'itemMatching') {
+ throw createExpressionError('Missing item index for .itemMatching()', {
+ itemIndex,
+ });
+ }
itemIndex = that.itemIndex;
}
@@ -762,24 +901,27 @@ export class WorkflowDataProxy {
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,
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: `Can’t get data for expression under ‘%%PARAMETER%%’ field`,
+ functionOverrides: {
+ description: `To fetch the data from other nodes that this code needs, more information is needed from the node ‘${that.activeNodeName}‘`,
+ message: `Can’t get data`,
+ },
+ 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}‘ probably didn’t supply it)`,
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%%’',
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
+ functionOverrides: {
+ message: `Can’t get data`,
+ },
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,
});
}
@@ -787,12 +929,14 @@ export class WorkflowDataProxy {
// graph before the current one
const parentNodes = that.workflow.getParentNodes(that.activeNodeName);
if (!parentNodes.includes(nodeName)) {
- throw new ExpressionError('Invalid expression', {
+ throw createExpressionError('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,
+ functionOverrides: {
+ description: `The code uses data in the node ‘${nodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
+ message: `No path back to node ‘${nodeName}’`,
+ },
+ description: `The expression uses data in the node ‘${nodeName}’ but there is no path back to it. Please check this node is connected to it (there can be other nodes in between).`,
itemIndex,
- failExecution: true,
});
}
@@ -802,49 +946,11 @@ export class WorkflowDataProxy {
return getPairedItem(nodeName, sourceData, pairedItem);
};
- }
- if (property === 'item') {
- return (itemIndex?: number, branchIndex?: number, runIndex?: number) => {
- if (itemIndex === undefined) {
- itemIndex = that.itemIndex;
- branchIndex = 0;
- runIndex = that.runIndex;
- }
- const executionData = getNodeOutput(nodeName, branchIndex, runIndex);
- if (executionData[itemIndex]) {
- return executionData[itemIndex];
- }
- let errorMessage = '';
-
- if (branchIndex === undefined && runIndex === undefined) {
- errorMessage = `
- No item found at index ${itemIndex}
- (for node "${nodeName}")`;
- throw new Error(errorMessage);
- }
- if (branchIndex === undefined) {
- errorMessage = `
- No item found at index ${itemIndex}
- in run ${runIndex || that.runIndex}
- (for node "${nodeName}")`;
- throw new Error(errorMessage);
- }
- if (runIndex === undefined) {
- errorMessage = `
- No item found at index ${itemIndex}
- of branch ${branchIndex || 0}
- (for node "${nodeName}")`;
- throw new Error(errorMessage);
- }
-
- errorMessage = `
- No item found at index ${itemIndex}
- of branch ${branchIndex || 0}
- in run ${runIndex || that.runIndex}
- (for node "${nodeName}")`;
- throw new Error(errorMessage);
- };
+ if (property === 'item') {
+ return pairedItemMethod();
+ }
+ return pairedItemMethod;
}
if (property === 'first') {
return (branchIndex?: number, runIndex?: number) => {
@@ -882,22 +988,25 @@ export class WorkflowDataProxy {
$input: new Proxy(
{},
{
+ ownKeys(target) {
+ return ['all', 'context', 'first', 'item', 'last', 'params'];
+ },
+ getOwnPropertyDescriptor(k) {
+ return {
+ enumerable: true,
+ configurable: true,
+ };
+ },
get(target, property, receiver) {
- if (property === 'thisItem') {
+ if (property === 'item') {
return that.connectionInputData[that.itemIndex];
}
- if (property === 'item') {
- return (itemIndex?: number) => {
- if (itemIndex === undefined) itemIndex = that.itemIndex;
- const result = that.connectionInputData;
- if (result[itemIndex]) {
- return result[itemIndex];
- }
- return undefined;
- };
- }
if (property === 'first') {
- return () => {
+ return (...args: unknown[]) => {
+ if (args.length) {
+ throw createExpressionError('$input.first() should have no arguments');
+ }
+
const result = that.connectionInputData;
if (result[0]) {
return result[0];
@@ -906,7 +1015,11 @@ export class WorkflowDataProxy {
};
}
if (property === 'last') {
- return () => {
+ return (...args: unknown[]) => {
+ if (args.length) {
+ throw createExpressionError('$input.last() should have no arguments');
+ }
+
const result = that.connectionInputData;
if (result.length && result[result.length - 1]) {
return result[result.length - 1];
@@ -923,12 +1036,37 @@ export class WorkflowDataProxy {
return [];
};
}
+
+ if (['context', 'params'].includes(property as string)) {
+ // For the following properties we need the source data so fail in case it is missing
+ // for some reason (even though that should actually never happen)
+ if (!that.executeData?.source) {
+ throw createExpressionError('Can’t get data for expression', {
+ messageTemplate: 'Can’t get data for expression under ‘%%PARAMETER%%’ field',
+ functionOverrides: {
+ message: 'Can’t get data',
+ },
+ description: `Apologies, this is an internal error. See details for more information`,
+ causeDetailed: `Missing sourceData (probably an internal error)`,
+ runIndex: that.runIndex,
+ });
+ }
+
+ const sourceData: ISourceData = that.executeData?.source.main![0] as ISourceData;
+
+ if (property === 'context') {
+ return that.nodeContextGetter(sourceData.previousNode);
+ }
+ if (property === 'params') {
+ return that.workflow.getNode(sourceData.previousNode)?.parameters;
+ }
+ }
+
return Reflect.get(target, property, receiver);
},
},
),
- $thisItem: that.connectionInputData[that.itemIndex],
$binary: {}, // Placeholder
$data: {}, // Placeholder
$env: this.envGetter(),
@@ -982,15 +1120,14 @@ export class WorkflowDataProxy {
$node: this.nodeGetter(),
$self: this.selfGetter(),
$parameter: this.nodeParameterGetter(this.activeNodeName),
- $position: this.itemIndex,
+ $prevNode: this.prevNodeGetter(),
$runIndex: this.runIndex,
$mode: this.mode,
$workflow: this.workflowGetter(),
- $thisRunIndex: this.runIndex,
- $thisItemIndex: this.itemIndex,
+ $itemIndex: this.itemIndex,
$now: DateTime.now(),
$today: DateTime.now().set({ hour: 0, minute: 0, second: 0, millisecond: 0 }),
- $jmespath: jmespathWrapper,
+ $jmesPath: jmespathWrapper,
// eslint-disable-next-line @typescript-eslint/naming-convention
DateTime,
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -998,6 +1135,13 @@ export class WorkflowDataProxy {
// eslint-disable-next-line @typescript-eslint/naming-convention
Duration,
...that.additionalKeys,
+
+ // deprecated
+ $jmespath: jmespathWrapper,
+ $position: this.itemIndex,
+ $thisItem: that.connectionInputData[that.itemIndex],
+ $thisItemIndex: this.itemIndex,
+ $thisRunIndex: this.runIndex,
};
return new Proxy(base, {
diff --git a/packages/workflow/test/WorkflowDataProxy.test.ts b/packages/workflow/test/WorkflowDataProxy.test.ts
index 2a9cfadbd..16980fd36 100644
--- a/packages/workflow/test/WorkflowDataProxy.test.ts
+++ b/packages/workflow/test/WorkflowDataProxy.test.ts
@@ -1,46 +1,48 @@
import { Workflow, WorkflowDataProxy } from '../src';
import * as Helpers from './Helpers';
-import { IConnections, INode, INodeExecutionData, IRunExecutionData } from '../src/Interfaces';
+import { IConnections, IExecuteData, INode, IRunExecutionData } from '../src/Interfaces';
describe('WorkflowDataProxy', () => {
describe('test data proxy', () => {
const nodes: INode[] = [
{
- parameters: {},
name: 'Start',
type: 'test.set',
+ parameters: {},
typeVersion: 1,
id: 'uuid-1',
position: [100, 200],
},
{
+ name: 'Function',
+ type: 'test.set',
parameters: {
functionCode:
'// Code here will run only once, no matter how many input items there are.\n// More info and help: https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.function/\nconst { DateTime, Duration, Interval } = require("luxon");\n\nconst data = [\n {\n "length": 105\n },\n {\n "length": 160\n },\n {\n "length": 121\n },\n {\n "length": 275\n },\n {\n "length": 950\n },\n];\n\nreturn data.map(fact => ({json: fact}));',
},
- name: 'Function',
- type: 'test.set',
typeVersion: 1,
id: 'uuid-2',
position: [280, 200],
},
{
- parameters: {
- keys: {
- key: [
- {
- currentKey: 'length',
- newKey: 'data',
- },
- ],
- },
- },
name: 'Rename',
type: 'test.set',
+ parameters: {
+ value1: 'data',
+ value2: 'initialName',
+ },
typeVersion: 1,
id: 'uuid-3',
position: [460, 200],
},
+ {
+ name: 'End',
+ type: 'test.set',
+ parameters: {},
+ typeVersion: 1,
+ id: 'uuid-4',
+ position: [640, 200],
+ },
];
const connections: IConnections = {
@@ -66,11 +68,38 @@ describe('WorkflowDataProxy', () => {
],
],
},
+ Rename: {
+ main: [
+ [
+ {
+ node: 'End',
+ type: 'main',
+ index: 0,
+ },
+ ],
+ ],
+ },
};
const runExecutionData: IRunExecutionData = {
resultData: {
runData: {
+ Start: [
+ {
+ startTime: 1,
+ executionTime: 1,
+ data: {
+ main: [
+ [
+ {
+ json: {},
+ },
+ ],
+ ],
+ },
+ source: [],
+ },
+ ],
Function: [
{
startTime: 1,
@@ -79,24 +108,33 @@ describe('WorkflowDataProxy', () => {
main: [
[
{
- json: { length: 105 },
+ json: { initialName: 105 },
+ pairedItem: { item: 0 },
},
{
- json: { length: 160 },
+ json: { initialName: 160 },
+ pairedItem: { item: 0 },
},
{
- json: { length: 121 },
+ json: { initialName: 121 },
+ pairedItem: { item: 0 },
},
{
- json: { length: 275 },
+ json: { initialName: 275 },
+ pairedItem: { item: 0 },
},
{
- json: { length: 950 },
+ json: { initialName: 950 },
+ pairedItem: { item: 0 },
},
],
],
},
- source: [],
+ source: [
+ {
+ previousNode: 'Start',
+ },
+ ],
},
],
Rename: [
@@ -108,51 +146,109 @@ describe('WorkflowDataProxy', () => {
[
{
json: { data: 105 },
+ pairedItem: { item: 0 },
},
{
json: { data: 160 },
+ pairedItem: { item: 1 },
},
{
json: { data: 121 },
+ pairedItem: { item: 2 },
},
{
json: { data: 275 },
+ pairedItem: { item: 3 },
},
{
json: { data: 950 },
+ pairedItem: { item: 4 },
},
],
],
},
- source: [],
+ source: [
+ {
+ previousNode: 'Function',
+ },
+ ],
+ },
+ ],
+ End: [
+ {
+ startTime: 1,
+ executionTime: 1,
+ data: {
+ main: [
+ [
+ {
+ json: { data: 105 },
+ pairedItem: { item: 0 },
+ },
+ {
+ json: { data: 160 },
+ pairedItem: { item: 1 },
+ },
+ {
+ json: { data: 121 },
+ pairedItem: { item: 2 },
+ },
+ {
+ json: { data: 275 },
+ pairedItem: { item: 3 },
+ },
+ {
+ json: { data: 950 },
+ pairedItem: { item: 4 },
+ },
+ ],
+ ],
+ },
+ source: [
+ {
+ previousNode: 'Rename',
+ },
+ ],
},
],
},
},
};
- const renameNodeConnectionInputData: INodeExecutionData[] = [
- { json: { length: 105 } },
- { json: { length: 160 } },
- { json: { length: 121 } },
- { json: { length: 275 } },
- { json: { length: 950 } },
- ];
-
const nodeTypes = Helpers.NodeTypes();
- const workflow = new Workflow({ nodes, connections, active: false, nodeTypes });
+ const workflow = new Workflow({
+ id: '123',
+ name: 'test workflow',
+ nodes,
+ connections,
+ active: false,
+ nodeTypes,
+ });
+ const nameLastNode = 'End';
+
+ const lastNodeConnectionInputData =
+ runExecutionData.resultData.runData[nameLastNode][0].data!.main[0];
+
+ const executeData: IExecuteData = {
+ data: runExecutionData.resultData.runData[nameLastNode][0].data!,
+ node: nodes.find((node) => node.name === nameLastNode) as INode,
+ source: {
+ main: runExecutionData.resultData.runData[nameLastNode][0].source!,
+ },
+ };
const dataProxy = new WorkflowDataProxy(
workflow,
runExecutionData,
0,
0,
- 'Rename',
- renameNodeConnectionInputData || [],
+ nameLastNode,
+ lastNodeConnectionInputData || [],
{},
'manual',
'America/New_York',
{},
+ executeData,
);
const proxy = dataProxy.getDataProxy();
@@ -162,11 +258,17 @@ describe('WorkflowDataProxy', () => {
test('test $("NodeName").all() length', () => {
expect(proxy.$('Rename').all().length).toEqual(5);
});
- test('test $("NodeName").item()', () => {
- expect(proxy.$('Rename').item().json.data).toEqual(105);
+ test('test $("NodeName").item', () => {
+ expect(proxy.$('Rename').item).toEqual({ json: { data: 105 }, pairedItem: { item: 0 } });
});
- test('test $("NodeName").item(2)', () => {
- expect(proxy.$('Rename').item(2).json.data).toEqual(121);
+ test('test $("NodeNameEarlier").item', () => {
+ expect(proxy.$('Function').item).toEqual({
+ json: { initialName: 105 },
+ pairedItem: { item: 0 },
+ });
+ });
+ test('test $("NodeName").itemMatching(2)', () => {
+ expect(proxy.$('Rename').itemMatching(2).json.data).toEqual(121);
});
test('test $("NodeName").first()', () => {
expect(proxy.$('Rename').first().json.data).toEqual(105);
@@ -175,26 +277,55 @@ describe('WorkflowDataProxy', () => {
expect(proxy.$('Rename').last().json.data).toEqual(950);
});
+ test('test $("NodeName").params', () => {
+ expect(proxy.$('Rename').params).toEqual({ value1: 'data', value2: 'initialName' });
+ });
+
test('test $input.all()', () => {
- expect(proxy.$input.all()[1].json.length).toEqual(160);
+ expect(proxy.$input.all()[1].json.data).toEqual(160);
});
test('test $input.all() length', () => {
expect(proxy.$input.all().length).toEqual(5);
});
- test('test $input.item()', () => {
- expect(proxy.$input.item().json.length).toEqual(105);
- });
- test('test $thisItem', () => {
- expect(proxy.$thisItem.json.length).toEqual(105);
- });
- test('test $input.item(2)', () => {
- expect(proxy.$input.item(2).json.length).toEqual(121);
- });
test('test $input.first()', () => {
- expect(proxy.$input.first().json.length).toEqual(105);
+ expect(proxy.$input.first().json.data).toEqual(105);
});
test('test $input.last()', () => {
- expect(proxy.$input.last().json.length).toEqual(950);
+ expect(proxy.$input.last().json.data).toEqual(950);
+ });
+ test('test $input.item', () => {
+ expect(proxy.$input.item.json.data).toEqual(105);
+ });
+ test('test $thisItem', () => {
+ expect(proxy.$thisItem.json.data).toEqual(105);
+ });
+
+ test('test $binary', () => {
+ expect(proxy.$binary).toEqual({});
+ });
+
+ test('test $json', () => {
+ expect(proxy.$json).toEqual({ data: 105 });
+ });
+
+ test('test $itemIndex', () => {
+ expect(proxy.$itemIndex).toEqual(0);
+ });
+
+ test('test $prevNode', () => {
+ expect(proxy.$prevNode).toEqual({ name: 'Rename', outputIndex: 0, runIndex: 0 });
+ });
+
+ test('test $runIndex', () => {
+ expect(proxy.$runIndex).toEqual(0);
+ });
+
+ test('test $workflow', () => {
+ expect(proxy.$workflow).toEqual({
+ active: false,
+ id: '123',
+ name: 'test workflow',
+ });
});
});
});