feat(Summarize Node): Turns error when field not found in items into warning (#11889)

Co-authored-by: Dana Lee <dana@n8n.io>
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
Ria Scholz
2025-01-28 14:19:51 +01:00
committed by GitHub
parent 69a97bd32d
commit d7dda3f5de
5 changed files with 237 additions and 27 deletions

View File

@@ -0,0 +1,53 @@
import { NodeOperationError, type IExecuteFunctions, type IDataObject } from 'n8n-workflow';
import { checkIfFieldExists, type Aggregations } from '../../utils';
describe('Test Summarize Node, checkIfFieldExists', () => {
let mockExecuteFunctions: IExecuteFunctions;
beforeEach(() => {
mockExecuteFunctions = {
getNode: jest.fn().mockReturnValue({ name: 'test-node' }),
} as unknown as IExecuteFunctions;
});
const items = [{ a: 1 }, { b: 2 }, { c: 3 }];
it('should not throw error if all fields exist', () => {
const aggregations: Aggregations = [
{ aggregation: 'sum', field: 'a' },
{ aggregation: 'count', field: 'c' },
];
const getValue = (item: IDataObject, field: string) => item[field];
expect(() => {
checkIfFieldExists.call(mockExecuteFunctions, items, aggregations, getValue);
}).not.toThrow();
});
it('should throw NodeOperationError if any field does not exist', () => {
const aggregations: Aggregations = [
{ aggregation: 'sum', field: 'b' },
{ aggregation: 'count', field: 'd' },
];
const getValue = (item: IDataObject, field: string) => item[field];
expect(() => {
checkIfFieldExists.call(mockExecuteFunctions, items, aggregations, getValue);
}).toThrow(NodeOperationError);
});
it("should throw NodeOperationError with error message containing the field name that doesn't exist", () => {
const aggregations: Aggregations = [{ aggregation: 'count', field: 'D' }];
const getValue = (item: IDataObject, field: string) => item[field];
expect(() => {
checkIfFieldExists.call(mockExecuteFunctions, items, aggregations, getValue);
}).toThrow("The field 'D' does not exist in any items");
});
it('should not throw error if field is empty string', () => {
const aggregations: Aggregations = [{ aggregation: 'count', field: '' }];
const getValue = (item: IDataObject, field: string) => item[field];
expect(() => {
checkIfFieldExists.call(mockExecuteFunctions, items, aggregations, getValue);
}).not.toThrow();
});
});

View File

@@ -0,0 +1,80 @@
import type { MockProxy } from 'jest-mock-extended';
import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions } from 'n8n-workflow';
import { NodeExecutionOutput, NodeOperationError } from 'n8n-workflow';
import { Summarize } from '../../Summarize.node';
import type { Aggregations } from '../../utils';
let summarizeNode: Summarize;
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
describe('Test Summarize Node, execute', () => {
beforeEach(() => {
summarizeNode = new Summarize();
mockExecuteFunctions = mock<IExecuteFunctions>({
getNode: jest.fn().mockReturnValue({ name: 'test-node' }),
getNodeParameter: jest.fn(),
getInputData: jest.fn(),
helpers: {
constructExecutionMetaData: jest.fn().mockReturnValue([]),
},
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should handle field not found with hints if version > 1', async () => {
mockExecuteFunctions.getInputData.mockReturnValue([{ json: { someField: 1 } }]);
mockExecuteFunctions.getNode.mockReturnValue({
id: '1',
name: 'test-node',
type: 'test-type',
position: [0, 0],
parameters: {},
typeVersion: 1.1,
});
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce({}) // options
.mockReturnValueOnce('') // fieldsToSplitBy
.mockReturnValueOnce([{ field: 'nonexistentField', aggregation: 'sum' }]); // fieldsToSummarize
const result = await summarizeNode.execute.call(mockExecuteFunctions);
expect(result).toBeInstanceOf(NodeExecutionOutput);
expect(result).toEqual([[{ json: { sum_nonexistentField: 0 }, pairedItem: [{ item: 0 }] }]]);
expect((result as NodeExecutionOutput).getHints()).toEqual([
{
location: 'outputPane',
message: "The field 'nonexistentField' does not exist in any items",
},
]);
});
it('should throw error if node version is < 1.1 and fields not found', async () => {
const items = [{ json: { a: 1, b: 2, c: 3 } }];
const aggregations: Aggregations = [
{ aggregation: 'sum', field: 'b' },
{ aggregation: 'count', field: 'd' },
];
mockExecuteFunctions.getNode.mockReturnValue({
id: '1',
name: 'test-node',
type: 'test-type',
position: [0, 0],
parameters: {},
typeVersion: 1,
});
mockExecuteFunctions.getInputData.mockReturnValue(items);
mockExecuteFunctions.getNodeParameter
.mockReturnValueOnce({}) // options
.mockReturnValueOnce('') // fieldsToSplitBy
.mockReturnValueOnce(aggregations); // fieldsToSummarize
await expect(async () => {
await summarizeNode.execute.bind(mockExecuteFunctions)();
}).rejects.toThrow(NodeOperationError);
});
});

View File

@@ -0,0 +1,70 @@
{
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [520, -80],
"id": "9ff1d8e0-ebe8-4c52-931a-e482e14ac911",
"name": "When clicking Test workflow"
},
{
"parameters": {
"category": "randomData",
"randomDataSeed": "ria",
"randomDataCount": 5
},
"type": "n8n-nodes-base.debugHelper",
"typeVersion": 1,
"position": [700, -80],
"id": "50cea846-f8ed-4b5a-a14a-c11bf4ba0f5d",
"name": "DebugHelper2"
},
{
"parameters": {
"fieldsToSummarize": {
"values": [
{
"field": "passwor"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.summarize",
"typeVersion": 1.1,
"position": [900, -80],
"id": "d39fedc9-62e1-45bc-8c9a-d20f2b63b543",
"name": "Summarize1"
}
],
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "DebugHelper2",
"type": "main",
"index": 0
}
]
]
},
"DebugHelper2": {
"main": [
[
{
"node": "Summarize1",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {},
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "ee90fdf8d57662f949e6c691dc07fa0fd2f66e1eee28ed82ef06658223e67255"
}
}