From 5526057efc7d27a1373dc2c46beda2737ed54689 Mon Sep 17 00:00:00 2001 From: Jan Oberhauser Date: Thu, 29 Sep 2022 23:02:25 +0200 Subject: [PATCH] feat(core): Improve paired item and add additional variables (#3765) * :zap: Remove duplicate and old string * :zap: Add telemetry * :zap: Futher improvements * :zap: Change error message and display only name of last parameter * :shirt: Fix lint issue * :zap: Remove not needed comments * :zap: Rename properties, add new ones and improve error messages * :zap: Add support for $execution, $prevNode and make it possible to use proxies as object * :zap: Some small improvements * :bug: Fix error message * :zap: Improve some error messages * :zap: Change resumeUrl variable and display in editor * :zap: Fix and extend tests * :zap: Multiple pairedItem improvements * :zap: Display "More Info" link with error messages if user can fix issue * :zap: Display different errors in Function Nodes --- packages/cli/src/InternalHooks.ts | 10 +- packages/core/src/NodeExecuteFunctions.ts | 71 +-- .../editor-ui/src/components/CodeEdit.vue | 7 + .../src/components/Error/NodeErrorView.vue | 20 +- .../src/components/VariableSelector.vue | 7 + .../src/components/mixins/pushConnection.ts | 54 ++- .../src/components/mixins/showMessage.ts | 38 +- .../src/components/mixins/workflowHelpers.ts | 9 +- packages/editor-ui/src/constants.ts | 22 +- packages/editor-ui/src/views/NodeView.vue | 4 +- packages/nodes-base/nodes/Wait/Wait.node.ts | 4 +- packages/workflow/src/ExpressionError.ts | 50 +- packages/workflow/src/Interfaces.ts | 2 + packages/workflow/src/WorkflowDataProxy.ts | 458 ++++++++++++------ .../workflow/test/WorkflowDataProxy.test.ts | 223 +++++++-- 15 files changed, 681 insertions(+), 298 deletions(-) diff --git a/packages/cli/src/InternalHooks.ts b/packages/cli/src/InternalHooks.ts index 037a6259f41bb..31ae7ca36e0fe 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 280cda93ba4dc..e6bfbfb5e80b5 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 cfe829099fce7..41106a20106be 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 01a695a92f2f8..2447418708ad2 100644 --- a/packages/editor-ui/src/components/Error/NodeErrorView.vue +++ b/packages/editor-ui/src/components/Error/NodeErrorView.vue @@ -2,7 +2,7 @@
{{ $locale.baseText('nodeErrorView.error') + ': ' + getErrorMessage() }}
-
{{getErrorDescription()}}
+
@@ -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 3e7e70eaa101c..0aa25880f719e 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 4030bac46343d..0d596c4b62510 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 814a9dc6539cb..473fc85550f02 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!'; - const receivedError = nodeName - ? `${nodeName}: ${error.message}` - : error.message; - errorMessage = `There was a problem executing the workflow:
"${receivedError}"`; + 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}"`; + } } return errorMessage; diff --git a/packages/editor-ui/src/components/mixins/workflowHelpers.ts b/packages/editor-ui/src/components/mixins/workflowHelpers.ts index 8fc15819fa461..e3f237776feb2 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 3569541e8eeeb..fdf8ae06b7c89 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 0c0f6663d3eb9..bd04e4e0a4257 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 0853fe387fd3c..e4b31a5a80d2a 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 a08ba186e9eba..2d025d1005506 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; - } + this.context.failExecution = !!options?.failExecution; - if (options?.messageTemplate !== undefined) { - this.context.messageTemplate = options.messageTemplate; + 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]; + } + }); } - - this.context.failExecution = !!options?.failExecution; } } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 9346865a2b510..fe1f9bffc5d3e 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 2da2f43055cbf..adf4a8dd55bc3 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') { - 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; - }; + return that.connectionInputData[that.itemIndex]; } 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 2a9cfadbd1d44..16980fd367135 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 $("NodeNameEarlier").item', () => { + expect(proxy.$('Function').item).toEqual({ + json: { initialName: 105 }, + pairedItem: { item: 0 }, + }); }); - test('test $("NodeName").item(2)', () => { - expect(proxy.$('Rename').item(2).json.data).toEqual(121); + 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 $input.first()', () => { + expect(proxy.$input.first().json.data).toEqual(105); + }); + test('test $input.last()', () => { + 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.length).toEqual(105); + expect(proxy.$thisItem.json.data).toEqual(105); }); - test('test $input.item(2)', () => { - expect(proxy.$input.item(2).json.length).toEqual(121); + + test('test $binary', () => { + expect(proxy.$binary).toEqual({}); }); - test('test $input.first()', () => { - expect(proxy.$input.first().json.length).toEqual(105); + + test('test $json', () => { + expect(proxy.$json).toEqual({ data: 105 }); }); - test('test $input.last()', () => { - expect(proxy.$input.last().json.length).toEqual(950); + + 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', + }); }); }); });