From e6515a2a743f558e1322c50a825cb23ca56147b5 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Wed, 25 Jun 2025 21:23:54 +0300 Subject: [PATCH] fix(Execution Data Node): Set nulish values as empty string, continue on fail support (#16696) Co-authored-by: Elias Meire Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../nodes/ExecutionData/ExecutionData.node.ts | 55 ++++++++++++++----- .../test/ExecutionData.node.test.ts | 35 +++++++++++- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/packages/nodes-base/nodes/ExecutionData/ExecutionData.node.ts b/packages/nodes-base/nodes/ExecutionData/ExecutionData.node.ts index f97e1f78ae..591d9a1b78 100644 --- a/packages/nodes-base/nodes/ExecutionData/ExecutionData.node.ts +++ b/packages/nodes-base/nodes/ExecutionData/ExecutionData.node.ts @@ -1,11 +1,14 @@ import type { - IDataObject, IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription, } from 'n8n-workflow'; -import { NodeConnectionTypes } from 'n8n-workflow'; +import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow'; + +type DataToSave = { + values: Array<{ key: string; value: string }>; +}; export class ExecutionData implements INodeType { description: INodeTypeDescription = { @@ -14,7 +17,7 @@ export class ExecutionData implements INodeType { icon: 'fa:tasks', group: ['input'], iconColor: 'light-green', - version: 1, + version: [1, 1.1], description: 'Add execution data for search', defaults: { name: 'Execution Data', @@ -70,6 +73,7 @@ export class ExecutionData implements INodeType { type: 'string', default: '', placeholder: 'e.g. myKey', + requiresDataPath: 'single', }, { displayName: 'Value', @@ -102,26 +106,51 @@ export class ExecutionData implements INodeType { }; async execute(this: IExecuteFunctions): Promise { - const context = this.getWorkflowDataProxy(0); + const dataProxy = this.getWorkflowDataProxy(0); + const nodeVersion = this.getNode().typeVersion; const items = this.getInputData(); const operations = this.getNodeParameter('operation', 0); + const returnData: INodeExecutionData[] = []; + if (operations === 'save') { for (let i = 0; i < items.length; i++) { - const dataToSave = - ((this.getNodeParameter('dataToSave', i, {}) as IDataObject).values as IDataObject[]) || - []; + try { + const dataToSave = + (this.getNodeParameter('dataToSave', i, {}) as DataToSave).values || []; - const values = dataToSave.reduce((acc, { key, value }) => { - acc[key as string] = value; - return acc; - }, {} as IDataObject); + const values = dataToSave.reduce( + (acc, { key, value }) => { + const valueToSet = value ? value : nodeVersion >= 1.1 ? '' : value; + acc[key] = valueToSet; + return acc; + }, + {} as { [key: string]: string }, + ); - context.$execution.customData.setAll(values); + dataProxy.$execution.customData.setAll(values); + + returnData.push(items[i]); + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + pairedItem: { + item: i, + }, + }); + continue; + } + throw new NodeOperationError(this.getNode(), error); + } } + } else { + return [items]; } - return [items]; + return [returnData]; } } diff --git a/packages/nodes-base/nodes/ExecutionData/test/ExecutionData.node.test.ts b/packages/nodes-base/nodes/ExecutionData/test/ExecutionData.node.test.ts index c0a347fc42..d0db830ad4 100644 --- a/packages/nodes-base/nodes/ExecutionData/test/ExecutionData.node.test.ts +++ b/packages/nodes-base/nodes/ExecutionData/test/ExecutionData.node.test.ts @@ -1,6 +1,11 @@ import { NodeTestHarness } from '@nodes-testing/node-test-harness'; import { mock } from 'jest-mock-extended'; -import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import type { + IExecuteFunctions, + INodeExecutionData, + IWorkflowDataProxyData, + INode, +} from 'n8n-workflow'; import { ExecutionData } from '../ExecutionData.node'; @@ -12,11 +17,39 @@ describe('ExecutionData Node', () => { ]; const executeFns = mock({ getInputData: () => mockInputData, + getNode: () => mock({ typeVersion: 1 }), }); + const result = await new ExecutionData().execute.call(executeFns); expect(result).toEqual([mockInputData]); }); + + it('should set nullish values to empty string', async () => { + const mockInputData: INodeExecutionData[] = [ + { json: { item: 0, foo: undefined } }, + { json: { item: 1, foo: null } }, + { json: { item: 1, foo: 'bar' } }, + ]; + const setAllMock = jest.fn(); + const executeFns = mock({ + getInputData: () => mockInputData, + getWorkflowDataProxy: () => + mock({ $execution: { customData: { setAll: setAllMock } } }), + getNode: () => mock({ typeVersion: 1.1 }), + }); + executeFns.getNodeParameter.mockReturnValueOnce('save'); + executeFns.getNodeParameter.mockReturnValueOnce({ values: [{ key: 'foo', value: undefined }] }); + executeFns.getNodeParameter.mockReturnValueOnce({ values: [{ key: 'foo', value: null }] }); + executeFns.getNodeParameter.mockReturnValueOnce({ values: [{ key: 'foo', value: 'bar' }] }); + const result = await new ExecutionData().execute.call(executeFns); + + expect(setAllMock).toBeCalledTimes(3); + expect(setAllMock).toBeCalledWith({ foo: '' }); + expect(setAllMock).toBeCalledWith({ foo: '' }); + expect(setAllMock).toBeCalledWith({ foo: 'bar' }); + expect(result).toEqual([mockInputData]); + }); }); describe('ExecutionData -> Should run the workflow', () => {