fix(Execution Data Node): Set nulish values as empty string, continue on fail support (#16696)

Co-authored-by: Elias Meire <elias@meire.dev>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
Michael Kret
2025-06-25 21:23:54 +03:00
committed by GitHub
parent b70cc944fc
commit e6515a2a74
2 changed files with 76 additions and 14 deletions

View File

@@ -1,11 +1,14 @@
import type { import type {
IDataObject,
IExecuteFunctions, IExecuteFunctions,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription, INodeTypeDescription,
} from 'n8n-workflow'; } 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 { export class ExecutionData implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@@ -14,7 +17,7 @@ export class ExecutionData implements INodeType {
icon: 'fa:tasks', icon: 'fa:tasks',
group: ['input'], group: ['input'],
iconColor: 'light-green', iconColor: 'light-green',
version: 1, version: [1, 1.1],
description: 'Add execution data for search', description: 'Add execution data for search',
defaults: { defaults: {
name: 'Execution Data', name: 'Execution Data',
@@ -70,6 +73,7 @@ export class ExecutionData implements INodeType {
type: 'string', type: 'string',
default: '', default: '',
placeholder: 'e.g. myKey', placeholder: 'e.g. myKey',
requiresDataPath: 'single',
}, },
{ {
displayName: 'Value', displayName: 'Value',
@@ -102,26 +106,51 @@ export class ExecutionData implements INodeType {
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const context = this.getWorkflowDataProxy(0); const dataProxy = this.getWorkflowDataProxy(0);
const nodeVersion = this.getNode().typeVersion;
const items = this.getInputData(); const items = this.getInputData();
const operations = this.getNodeParameter('operation', 0); const operations = this.getNodeParameter('operation', 0);
const returnData: INodeExecutionData[] = [];
if (operations === 'save') { if (operations === 'save') {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const dataToSave = try {
((this.getNodeParameter('dataToSave', i, {}) as IDataObject).values as IDataObject[]) || const dataToSave =
[]; (this.getNodeParameter('dataToSave', i, {}) as DataToSave).values || [];
const values = dataToSave.reduce((acc, { key, value }) => { const values = dataToSave.reduce(
acc[key as string] = value; (acc, { key, value }) => {
return acc; const valueToSet = value ? value : nodeVersion >= 1.1 ? '' : value;
}, {} as IDataObject); 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];
} }
} }

View File

@@ -1,6 +1,11 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness'; import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import { mock } from 'jest-mock-extended'; 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'; import { ExecutionData } from '../ExecutionData.node';
@@ -12,11 +17,39 @@ describe('ExecutionData Node', () => {
]; ];
const executeFns = mock<IExecuteFunctions>({ const executeFns = mock<IExecuteFunctions>({
getInputData: () => mockInputData, getInputData: () => mockInputData,
getNode: () => mock<INode>({ typeVersion: 1 }),
}); });
const result = await new ExecutionData().execute.call(executeFns); const result = await new ExecutionData().execute.call(executeFns);
expect(result).toEqual([mockInputData]); 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<IExecuteFunctions>({
getInputData: () => mockInputData,
getWorkflowDataProxy: () =>
mock<IWorkflowDataProxyData>({ $execution: { customData: { setAll: setAllMock } } }),
getNode: () => mock<INode>({ 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', () => { describe('ExecutionData -> Should run the workflow', () => {