first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.summarize",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"details": "",
|
||||
"categories": ["Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.summarize/"
|
||||
}
|
||||
],
|
||||
"generic": []
|
||||
},
|
||||
"alias": [
|
||||
"Append",
|
||||
"Array",
|
||||
"Average",
|
||||
"Concatenate",
|
||||
"Count",
|
||||
"Group",
|
||||
"Item",
|
||||
"List",
|
||||
"Max",
|
||||
"Min",
|
||||
"Pivot",
|
||||
"Sum",
|
||||
"Summarise",
|
||||
"Summarize",
|
||||
"Transform",
|
||||
"Unique"
|
||||
],
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Data Transformation"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeType,
|
||||
type INodeTypeDescription,
|
||||
NodeConnectionTypes,
|
||||
type NodeExecutionHint,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
type Aggregations,
|
||||
NUMERICAL_AGGREGATIONS,
|
||||
type SummarizeOptions,
|
||||
aggregateAndSplitData,
|
||||
checkIfFieldExists,
|
||||
fieldValueGetter,
|
||||
flattenAggregationResultToArray,
|
||||
flattenAggregationResultToObject,
|
||||
} from './utils';
|
||||
|
||||
export class Summarize implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Summarize',
|
||||
name: 'summarize',
|
||||
icon: 'file:summarize.svg',
|
||||
group: ['transform'],
|
||||
subtitle: '',
|
||||
version: [1, 1.1],
|
||||
description: 'Sum, count, max, etc. across items',
|
||||
defaults: {
|
||||
name: 'Summarize',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Fields to Summarize',
|
||||
name: 'fieldsToSummarize',
|
||||
type: 'fixedCollection',
|
||||
placeholder: 'Add Field',
|
||||
default: { values: [{ aggregation: 'count', field: '' }] },
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: '',
|
||||
name: 'values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Aggregation',
|
||||
name: 'aggregation',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Append',
|
||||
value: 'append',
|
||||
},
|
||||
{
|
||||
name: 'Average',
|
||||
value: 'average',
|
||||
},
|
||||
{
|
||||
name: 'Concatenate',
|
||||
value: 'concatenate',
|
||||
},
|
||||
{
|
||||
name: 'Count',
|
||||
value: 'count',
|
||||
},
|
||||
{
|
||||
name: 'Count Unique',
|
||||
value: 'countUnique',
|
||||
},
|
||||
{
|
||||
name: 'Max',
|
||||
value: 'max',
|
||||
},
|
||||
{
|
||||
name: 'Min',
|
||||
value: 'min',
|
||||
},
|
||||
{
|
||||
name: 'Sum',
|
||||
value: 'sum',
|
||||
},
|
||||
],
|
||||
default: 'count',
|
||||
description: 'How to combine the values of the field you want to summarize',
|
||||
},
|
||||
//field repeated to have different descriptions for different aggregations --------------------------------
|
||||
{
|
||||
displayName: 'Field',
|
||||
name: 'field',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The name of an input field that you want to summarize',
|
||||
placeholder: 'e.g. cost',
|
||||
hint: ' Enter the field name as text',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
aggregation: [...NUMERICAL_AGGREGATIONS, 'countUnique', 'count', 'max', 'min'],
|
||||
},
|
||||
},
|
||||
requiresDataPath: 'single',
|
||||
},
|
||||
{
|
||||
displayName: 'Field',
|
||||
name: 'field',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The name of an input field that you want to summarize. The field should contain numerical values; null, undefined, empty strings would be ignored.',
|
||||
placeholder: 'e.g. cost',
|
||||
hint: ' Enter the field name as text',
|
||||
displayOptions: {
|
||||
show: {
|
||||
aggregation: NUMERICAL_AGGREGATIONS,
|
||||
},
|
||||
},
|
||||
requiresDataPath: 'single',
|
||||
},
|
||||
{
|
||||
displayName: 'Field',
|
||||
name: 'field',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description:
|
||||
'The name of an input field that you want to summarize; null, undefined, empty strings would be ignored',
|
||||
placeholder: 'e.g. cost',
|
||||
hint: ' Enter the field name as text',
|
||||
displayOptions: {
|
||||
show: {
|
||||
aggregation: ['countUnique', 'count', 'max', 'min'],
|
||||
},
|
||||
},
|
||||
requiresDataPath: 'single',
|
||||
},
|
||||
// ----------------------------------------------------------------------------------------------------------
|
||||
{
|
||||
displayName: 'Include Empty Values',
|
||||
name: 'includeEmpty',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
aggregation: ['append', 'concatenate', 'count', 'countUnique'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Separator',
|
||||
name: 'separateBy',
|
||||
type: 'options',
|
||||
default: ',',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: 'Comma',
|
||||
value: ',',
|
||||
},
|
||||
{
|
||||
name: 'Comma and Space',
|
||||
value: ', ',
|
||||
},
|
||||
{
|
||||
name: 'New Line',
|
||||
value: '\n',
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: '',
|
||||
},
|
||||
{
|
||||
name: 'Space',
|
||||
value: ' ',
|
||||
},
|
||||
{
|
||||
name: 'Other',
|
||||
value: 'other',
|
||||
},
|
||||
],
|
||||
hint: 'What to insert between values',
|
||||
displayOptions: {
|
||||
show: {
|
||||
aggregation: ['concatenate'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Separator',
|
||||
name: 'customSeparator',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
aggregation: ['concatenate'],
|
||||
separateBy: ['other'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
// fieldsToSplitBy repeated to have different displayName for singleItem and separateItems -----------------------------
|
||||
{
|
||||
displayName: 'Fields to Split By',
|
||||
name: 'fieldsToSplitBy',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. country, city',
|
||||
default: '',
|
||||
description: 'The name of the input fields that you want to split the summary by',
|
||||
hint: 'Enter the name of the fields as text (separated by commas)',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'/options.outputFormat': ['singleItem'],
|
||||
},
|
||||
},
|
||||
requiresDataPath: 'multiple',
|
||||
},
|
||||
{
|
||||
displayName: 'Fields to Group By',
|
||||
name: 'fieldsToSplitBy',
|
||||
type: 'string',
|
||||
placeholder: 'e.g. country, city',
|
||||
default: '',
|
||||
description: 'The name of the input fields that you want to split the summary by',
|
||||
hint: 'Enter the name of the fields as text (separated by commas)',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/options.outputFormat': ['singleItem'],
|
||||
},
|
||||
},
|
||||
requiresDataPath: 'multiple',
|
||||
},
|
||||
// ----------------------------------------------------------------------------------------------------------
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Continue if Field Not Found',
|
||||
name: 'continueIfFieldNotFound',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
"Whether to continue if field to summarize can't be found in any items and return single empty item, otherwise an error would be thrown",
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'@version': [{ _cnd: { gte: 1.1 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Disable Dot Notation',
|
||||
name: 'disableDotNotation',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to disallow referencing child fields using `parent.child` in the field name',
|
||||
},
|
||||
{
|
||||
displayName: 'Output Format',
|
||||
name: 'outputFormat',
|
||||
type: 'options',
|
||||
default: 'separateItems',
|
||||
options: [
|
||||
{
|
||||
name: 'Each Split in a Separate Item',
|
||||
value: 'separateItems',
|
||||
},
|
||||
{
|
||||
name: 'All Splits in a Single Item',
|
||||
value: 'singleItem',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||
displayName: 'Ignore items without valid fields to group by',
|
||||
name: 'skipEmptySplitFields',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const newItems = items.map(({ json }, i) => ({ ...json, _itemIndex: i }));
|
||||
|
||||
const options = this.getNodeParameter('options', 0, {}) as SummarizeOptions;
|
||||
|
||||
const fieldsToSplitBy = (this.getNodeParameter('fieldsToSplitBy', 0, '') as string)
|
||||
.split(',')
|
||||
.map((field) => field.trim())
|
||||
.filter((field) => field);
|
||||
|
||||
const fieldsToSummarize = this.getNodeParameter(
|
||||
'fieldsToSummarize.values',
|
||||
0,
|
||||
[],
|
||||
) as Aggregations;
|
||||
|
||||
if (fieldsToSummarize.filter((aggregation) => aggregation.field !== '').length === 0) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
"You need to add at least one aggregation to 'Fields to Summarize' with non empty 'Field'",
|
||||
);
|
||||
}
|
||||
|
||||
const getValue = fieldValueGetter(options.disableDotNotation);
|
||||
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
const aggregationResult = aggregateAndSplitData({
|
||||
splitKeys: fieldsToSplitBy,
|
||||
inputItems: newItems,
|
||||
fieldsToSummarize,
|
||||
options,
|
||||
getValue,
|
||||
convertKeysToString: nodeVersion === 1,
|
||||
});
|
||||
|
||||
const fieldsNotFound: NodeExecutionHint[] = [];
|
||||
try {
|
||||
checkIfFieldExists.call(this, newItems, fieldsToSummarize, getValue);
|
||||
} catch (error) {
|
||||
if (nodeVersion > 1 || options.continueIfFieldNotFound) {
|
||||
const fieldNotFoundHint: NodeExecutionHint = {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
location: 'outputPane',
|
||||
};
|
||||
fieldsNotFound.push(fieldNotFoundHint);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldsNotFound.length) {
|
||||
this.addExecutionHints(...fieldsNotFound);
|
||||
}
|
||||
|
||||
if (options.outputFormat === 'singleItem') {
|
||||
const executionData: INodeExecutionData = {
|
||||
json: flattenAggregationResultToObject(aggregationResult),
|
||||
pairedItem: newItems.map((_v, index) => ({
|
||||
item: index,
|
||||
})),
|
||||
};
|
||||
return [[executionData]];
|
||||
} else {
|
||||
if (!fieldsToSplitBy.length && 'pairedItems' in aggregationResult) {
|
||||
const { pairedItems, returnData } = aggregationResult;
|
||||
const executionData: INodeExecutionData = {
|
||||
json: returnData,
|
||||
pairedItem: (pairedItems ?? []).map((index) => ({ item: index })),
|
||||
};
|
||||
return [[executionData]];
|
||||
}
|
||||
const flatAggregationResults = flattenAggregationResultToArray(aggregationResult);
|
||||
const executionData = flatAggregationResults.map((item) => {
|
||||
const { pairedItems, returnData } = item;
|
||||
return {
|
||||
json: returnData,
|
||||
pairedItem: (pairedItems ?? []).map((index) => ({ item: index })),
|
||||
};
|
||||
});
|
||||
return [executionData];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none"><path fill="#F92" fill-rule="evenodd" d="M132 91c-15.464 0-28 12.536-28 28v317c0 15.464 12.536 28 28 28h248c15.464 0 28-12.536 28-28V119c0-15.464-12.536-28-28-28h-6a6 6 0 0 1-6-6V49a6 6 0 0 1 6-6h6c41.974 0 76 34.026 76 76v317c0 41.974-34.026 76-76 76H132c-41.974 0-76-34.026-76-76V119c0-41.974 34.026-76 76-76h6a6 6 0 0 1 6 6v36a6 6 0 0 1-6 6z" clip-rule="evenodd"/><path fill="#F92" fill-rule="evenodd" d="M256 0c-27.232 0-50.227 18.142-57.558 43H182a6 6 0 0 0-6 6v70a6 6 0 0 0 6 6h148a6 6 0 0 0 6-6V49a6 6 0 0 0-6-6h-16.442C306.227 18.142 283.232 0 256 0m0 40a19.9 19.9 0 0 0-10.541 3C239.781 46.528 236 52.823 236 60c0 11.046 8.954 20 20 20s20-8.954 20-20c0-7.177-3.781-13.472-9.459-17A19.9 19.9 0 0 0 256 40m101 179c0 6.627-5.373 12-12 12H233c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h112c6.627 0 12 5.373 12 12z" clip-rule="evenodd"/><path fill="#F92" d="M197 207c0 13.255-10.745 24-24 24s-24-10.745-24-24 10.745-24 24-24 24 10.745 24 24"/><path fill="#F92" fill-rule="evenodd" d="M357 395c0 6.627-5.373 12-12 12H233c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h112c6.627 0 12 5.373 12 12z" clip-rule="evenodd"/><path fill="#F92" d="M197 383c0 13.255-10.745 24-24 24s-24-10.745-24-24 10.745-24 24-24 24 10.745 24 24"/><path fill="#F92" fill-rule="evenodd" d="M357 307c0 6.627-5.373 12-12 12H233c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h112c6.627 0 12 5.373 12 12z" clip-rule="evenodd"/><path fill="#F92" d="M197 295c0 13.255-10.745 24-24 24s-24-10.745-24-24 10.745-24 24-24 24 10.745 24 24"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,5 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
|
||||
describe('Test Summarize Node', () => {
|
||||
new NodeTestHarness().setupTests();
|
||||
});
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should handle multiple split field values containing null when convertKeysToString is false: split-field-with-spaces-array 1`] = `
|
||||
[
|
||||
{
|
||||
"pairedItems": [
|
||||
0,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "Widget A",
|
||||
"Warehouse": "WH1",
|
||||
"appended_Warehouse": [
|
||||
"WH1",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "Widget A",
|
||||
"Warehouse": null,
|
||||
"appended_Warehouse": [
|
||||
null,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": null,
|
||||
"Warehouse": "WH2",
|
||||
"appended_Warehouse": [
|
||||
"WH2",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should handle multiple split field values containing null when convertKeysToString is false: split-field-with-spaces-result 1`] = `
|
||||
{
|
||||
"fieldName": "Product",
|
||||
"splits": Map {
|
||||
"Widget A" => {
|
||||
"fieldName": "Warehouse",
|
||||
"splits": Map {
|
||||
"WH1" => {
|
||||
"pairedItems": [
|
||||
0,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"WH1",
|
||||
],
|
||||
},
|
||||
},
|
||||
null => {
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
null,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null => {
|
||||
"fieldName": "Warehouse",
|
||||
"splits": Map {
|
||||
"WH2" => {
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"WH2",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should handle multiple split field values containing null when convertKeysToString is true: split-field-with-spaces-array 1`] = `
|
||||
[
|
||||
{
|
||||
"pairedItems": [
|
||||
0,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "Widget A",
|
||||
"Warehouse": "WH1",
|
||||
"appended_Warehouse": [
|
||||
"WH1",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "Widget A",
|
||||
"Warehouse": "null",
|
||||
"appended_Warehouse": [
|
||||
null,
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "null",
|
||||
"Warehouse": "WH2",
|
||||
"appended_Warehouse": [
|
||||
"WH2",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should handle multiple split field values containing null when convertKeysToString is true: split-field-with-spaces-result 1`] = `
|
||||
{
|
||||
"fieldName": "Product",
|
||||
"splits": Map {
|
||||
"Widget A" => {
|
||||
"fieldName": "Warehouse",
|
||||
"splits": Map {
|
||||
"WH1" => {
|
||||
"pairedItems": [
|
||||
0,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"WH1",
|
||||
],
|
||||
},
|
||||
},
|
||||
"null" => {
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
null,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"null" => {
|
||||
"fieldName": "Warehouse",
|
||||
"splits": Map {
|
||||
"WH2" => {
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"WH2",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should handle split field values containing spaces when convertKeysToString is not set: split-field-with-spaces-array 1`] = `
|
||||
[
|
||||
{
|
||||
"pairedItems": [
|
||||
0,
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "Widget A",
|
||||
"appended_Warehouse": [
|
||||
"WH1",
|
||||
"WH3",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "Widget B",
|
||||
"appended_Warehouse": [
|
||||
"WH2",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should handle split field values containing spaces when convertKeysToString is not set: split-field-with-spaces-result 1`] = `
|
||||
{
|
||||
"fieldName": "Product",
|
||||
"splits": Map {
|
||||
"Widget A" => {
|
||||
"pairedItems": [
|
||||
0,
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"WH1",
|
||||
"WH3",
|
||||
],
|
||||
},
|
||||
},
|
||||
"Widget B" => {
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"WH2",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should handle split field values containing spaces when convertKeysToString is true: split-field-with-spaces-array 1`] = `
|
||||
[
|
||||
{
|
||||
"pairedItems": [
|
||||
0,
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "Widget A",
|
||||
"appended_Warehouse": [
|
||||
"WH1",
|
||||
"WH3",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"Product": "Widget B",
|
||||
"appended_Warehouse": [
|
||||
"WH2",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should handle split field values containing spaces when convertKeysToString is true: split-field-with-spaces-result 1`] = `
|
||||
{
|
||||
"fieldName": "Product",
|
||||
"splits": Map {
|
||||
"Widget A" => {
|
||||
"pairedItems": [
|
||||
0,
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"WH1",
|
||||
"WH3",
|
||||
],
|
||||
},
|
||||
},
|
||||
"Widget B" => {
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"WH2",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should not convert numbers to strings: array 1`] = `
|
||||
[
|
||||
{
|
||||
"pairedItems": [
|
||||
0,
|
||||
],
|
||||
"returnData": {
|
||||
"Qty": 1,
|
||||
"Sku": 12345,
|
||||
"appended_Warehouse": [
|
||||
"BER_0G",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"Qty": 2,
|
||||
"Sku": 12345,
|
||||
"appended_Warehouse": [
|
||||
"BER_0L",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"Qty": 1,
|
||||
"Sku": 6534563534,
|
||||
"appended_Warehouse": [
|
||||
"BER_0L",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should not convert numbers to strings: result 1`] = `
|
||||
{
|
||||
"fieldName": "Sku",
|
||||
"splits": Map {
|
||||
12345 => {
|
||||
"fieldName": "Qty",
|
||||
"splits": Map {
|
||||
1 => {
|
||||
"pairedItems": [
|
||||
0,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"BER_0G",
|
||||
],
|
||||
},
|
||||
},
|
||||
2 => {
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"BER_0L",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
6534563534 => {
|
||||
"fieldName": "Qty",
|
||||
"splits": Map {
|
||||
1 => {
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"BER_0L",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should not convert strings to numbers: array 1`] = `
|
||||
[
|
||||
{
|
||||
"pairedItems": [
|
||||
0,
|
||||
],
|
||||
"returnData": {
|
||||
"Qty": "1",
|
||||
"Sku": "012345",
|
||||
"appended_Warehouse": [
|
||||
"BER_0G",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"Qty": "2",
|
||||
"Sku": "012345",
|
||||
"appended_Warehouse": [
|
||||
"BER_0L",
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"Qty": "1",
|
||||
"Sku": "06534563534",
|
||||
"appended_Warehouse": [
|
||||
"BER_0L",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData should not convert strings to numbers: result 1`] = `
|
||||
{
|
||||
"fieldName": "Sku",
|
||||
"splits": Map {
|
||||
"012345" => {
|
||||
"fieldName": "Qty",
|
||||
"splits": Map {
|
||||
"1" => {
|
||||
"pairedItems": [
|
||||
0,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"BER_0G",
|
||||
],
|
||||
},
|
||||
},
|
||||
"2" => {
|
||||
"pairedItems": [
|
||||
1,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"BER_0L",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"06534563534" => {
|
||||
"fieldName": "Qty",
|
||||
"splits": Map {
|
||||
"1" => {
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"appended_Warehouse": [
|
||||
"BER_0L",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData with skipEmptySplitFields=true should skip empty split fields: array 1`] = `
|
||||
[
|
||||
{
|
||||
"pairedItems": [
|
||||
0,
|
||||
3,
|
||||
],
|
||||
"returnData": {
|
||||
"Sku": 12345,
|
||||
"concatenated_Warehouse": "BER_0G//{"name":"BER_0G3"}",
|
||||
},
|
||||
},
|
||||
{
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"Sku": "{}",
|
||||
"concatenated_Warehouse": "BER_0L",
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Test Summarize Node, aggregateAndSplitData with skipEmptySplitFields=true should skip empty split fields: result 1`] = `
|
||||
{
|
||||
"fieldName": "Sku",
|
||||
"splits": Map {
|
||||
12345 => {
|
||||
"pairedItems": [
|
||||
0,
|
||||
3,
|
||||
],
|
||||
"returnData": {
|
||||
"concatenated_Warehouse": "BER_0G//{"name":"BER_0G3"}",
|
||||
},
|
||||
},
|
||||
"{}" => {
|
||||
"pairedItems": [
|
||||
2,
|
||||
],
|
||||
"returnData": {
|
||||
"concatenated_Warehouse": "BER_0L",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
+53
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { 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).toEqual([[{ json: { sum_nonexistentField: 0 }, pairedItem: [{ item: 0 }] }]]);
|
||||
expect(mockExecuteFunctions.addExecutionHints).toHaveBeenCalledWith({
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,314 @@
|
||||
import {
|
||||
fieldValueGetter,
|
||||
aggregateAndSplitData,
|
||||
flattenAggregationResultToArray,
|
||||
type Aggregations,
|
||||
} from '../../utils';
|
||||
|
||||
describe('Test Summarize Node, aggregateAndSplitData', () => {
|
||||
test('should not convert strings to numbers', () => {
|
||||
const data = [
|
||||
{
|
||||
Sku: '012345',
|
||||
Warehouse: 'BER_0G',
|
||||
Qty: '1',
|
||||
_itemIndex: 0,
|
||||
},
|
||||
{
|
||||
Sku: '012345',
|
||||
Warehouse: 'BER_0L',
|
||||
Qty: '2',
|
||||
_itemIndex: 1,
|
||||
},
|
||||
{
|
||||
Sku: '06534563534',
|
||||
Warehouse: 'BER_0L',
|
||||
Qty: '1',
|
||||
_itemIndex: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const aggregations: Aggregations = [
|
||||
{
|
||||
aggregation: 'append',
|
||||
field: 'Warehouse',
|
||||
includeEmpty: true,
|
||||
},
|
||||
];
|
||||
|
||||
const result = aggregateAndSplitData({
|
||||
splitKeys: ['Sku', 'Qty'],
|
||||
inputItems: data,
|
||||
fieldsToSummarize: aggregations,
|
||||
options: { continueIfFieldNotFound: true },
|
||||
getValue: fieldValueGetter(),
|
||||
});
|
||||
expect(result).toMatchSnapshot('result');
|
||||
expect(flattenAggregationResultToArray(result)).toMatchSnapshot('array');
|
||||
});
|
||||
|
||||
test('should not convert numbers to strings', () => {
|
||||
const data = [
|
||||
{
|
||||
Sku: 12345,
|
||||
Warehouse: 'BER_0G',
|
||||
Qty: 1,
|
||||
_itemIndex: 0,
|
||||
},
|
||||
{
|
||||
Sku: 12345,
|
||||
Warehouse: 'BER_0L',
|
||||
Qty: 2,
|
||||
_itemIndex: 1,
|
||||
},
|
||||
{
|
||||
Sku: 6534563534,
|
||||
Warehouse: 'BER_0L',
|
||||
Qty: 1,
|
||||
_itemIndex: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const aggregations: Aggregations = [
|
||||
{
|
||||
aggregation: 'append',
|
||||
field: 'Warehouse',
|
||||
includeEmpty: true,
|
||||
},
|
||||
];
|
||||
|
||||
const result = aggregateAndSplitData({
|
||||
splitKeys: ['Sku', 'Qty'],
|
||||
inputItems: data,
|
||||
fieldsToSummarize: aggregations,
|
||||
options: { continueIfFieldNotFound: true },
|
||||
getValue: fieldValueGetter(),
|
||||
});
|
||||
expect(result).toMatchSnapshot('result');
|
||||
expect(flattenAggregationResultToArray(result)).toMatchSnapshot('array');
|
||||
});
|
||||
|
||||
test('should handle split field values containing spaces when convertKeysToString is not set', () => {
|
||||
const data = [
|
||||
{
|
||||
Product: 'Widget A',
|
||||
Warehouse: 'WH1',
|
||||
Qty: '5',
|
||||
_itemIndex: 0,
|
||||
},
|
||||
{
|
||||
Product: 'Widget B',
|
||||
Warehouse: 'WH2',
|
||||
Qty: '3',
|
||||
_itemIndex: 1,
|
||||
},
|
||||
{
|
||||
Product: 'Widget A',
|
||||
Warehouse: 'WH3',
|
||||
Qty: '2',
|
||||
_itemIndex: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const aggregations: Aggregations = [
|
||||
{
|
||||
aggregation: 'append',
|
||||
field: 'Warehouse',
|
||||
includeEmpty: true,
|
||||
},
|
||||
];
|
||||
|
||||
const result = aggregateAndSplitData({
|
||||
splitKeys: ['Product'],
|
||||
inputItems: data,
|
||||
fieldsToSummarize: aggregations,
|
||||
options: { continueIfFieldNotFound: true },
|
||||
getValue: fieldValueGetter(),
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot('split-field-with-spaces-result');
|
||||
expect(flattenAggregationResultToArray(result)).toMatchSnapshot(
|
||||
'split-field-with-spaces-array',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle split field values containing spaces when convertKeysToString is true', () => {
|
||||
const data = [
|
||||
{
|
||||
Product: 'Widget A',
|
||||
Warehouse: 'WH1',
|
||||
Qty: '5',
|
||||
_itemIndex: 0,
|
||||
},
|
||||
{
|
||||
Product: 'Widget B',
|
||||
Warehouse: 'WH2',
|
||||
Qty: '3',
|
||||
_itemIndex: 1,
|
||||
},
|
||||
{
|
||||
Product: 'Widget A',
|
||||
Warehouse: 'WH3',
|
||||
Qty: '2',
|
||||
_itemIndex: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const aggregations: Aggregations = [
|
||||
{
|
||||
aggregation: 'append',
|
||||
field: 'Warehouse',
|
||||
includeEmpty: true,
|
||||
},
|
||||
];
|
||||
|
||||
const result = aggregateAndSplitData({
|
||||
splitKeys: ['Product'],
|
||||
inputItems: data,
|
||||
fieldsToSummarize: aggregations,
|
||||
options: { continueIfFieldNotFound: true },
|
||||
getValue: fieldValueGetter(),
|
||||
convertKeysToString: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot('split-field-with-spaces-result');
|
||||
expect(flattenAggregationResultToArray(result)).toMatchSnapshot(
|
||||
'split-field-with-spaces-array',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle multiple split field values containing null when convertKeysToString is true', () => {
|
||||
const data = [
|
||||
{
|
||||
Product: 'Widget A',
|
||||
Warehouse: 'WH1',
|
||||
Qty: '5',
|
||||
_itemIndex: 0,
|
||||
},
|
||||
{
|
||||
Product: null,
|
||||
Warehouse: 'WH2',
|
||||
Qty: '3',
|
||||
_itemIndex: 1,
|
||||
},
|
||||
{
|
||||
Product: 'Widget A',
|
||||
Warehouse: null,
|
||||
Qty: '2',
|
||||
_itemIndex: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const aggregations: Aggregations = [
|
||||
{
|
||||
aggregation: 'append',
|
||||
field: 'Warehouse',
|
||||
includeEmpty: true,
|
||||
},
|
||||
];
|
||||
|
||||
const result = aggregateAndSplitData({
|
||||
splitKeys: ['Product', 'Warehouse'],
|
||||
inputItems: data,
|
||||
fieldsToSummarize: aggregations,
|
||||
options: { continueIfFieldNotFound: true },
|
||||
getValue: fieldValueGetter(),
|
||||
convertKeysToString: true,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot('split-field-with-spaces-result');
|
||||
expect(flattenAggregationResultToArray(result)).toMatchSnapshot(
|
||||
'split-field-with-spaces-array',
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle multiple split field values containing null when convertKeysToString is false', () => {
|
||||
const data = [
|
||||
{
|
||||
Product: 'Widget A',
|
||||
Warehouse: 'WH1',
|
||||
Qty: '5',
|
||||
_itemIndex: 0,
|
||||
},
|
||||
{
|
||||
Product: null,
|
||||
Warehouse: 'WH2',
|
||||
Qty: '3',
|
||||
_itemIndex: 1,
|
||||
},
|
||||
{
|
||||
Product: 'Widget A',
|
||||
Warehouse: null,
|
||||
Qty: '2',
|
||||
_itemIndex: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const aggregations: Aggregations = [
|
||||
{
|
||||
aggregation: 'append',
|
||||
field: 'Warehouse',
|
||||
includeEmpty: true,
|
||||
},
|
||||
];
|
||||
|
||||
const result = aggregateAndSplitData({
|
||||
splitKeys: ['Product', 'Warehouse'],
|
||||
inputItems: data,
|
||||
fieldsToSummarize: aggregations,
|
||||
options: { continueIfFieldNotFound: true },
|
||||
getValue: fieldValueGetter(),
|
||||
convertKeysToString: false,
|
||||
});
|
||||
|
||||
expect(result).toMatchSnapshot('split-field-with-spaces-result');
|
||||
expect(flattenAggregationResultToArray(result)).toMatchSnapshot(
|
||||
'split-field-with-spaces-array',
|
||||
);
|
||||
});
|
||||
|
||||
describe('with skipEmptySplitFields=true', () => {
|
||||
test('should skip empty split fields', () => {
|
||||
const data = [
|
||||
{
|
||||
Sku: 12345,
|
||||
Warehouse: 'BER_0G',
|
||||
_itemIndex: 0,
|
||||
},
|
||||
{
|
||||
Warehouse: 'BER_0L',
|
||||
_itemIndex: 1,
|
||||
},
|
||||
{
|
||||
Sku: {},
|
||||
Warehouse: 'BER_0L',
|
||||
_itemIndex: 2,
|
||||
},
|
||||
{
|
||||
Sku: 12345,
|
||||
Warehouse: { name: 'BER_0G3' },
|
||||
_itemIndex: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const aggregations: Aggregations = [
|
||||
{
|
||||
aggregation: 'concatenate',
|
||||
field: 'Warehouse',
|
||||
separateBy: 'other',
|
||||
customSeparator: '//',
|
||||
},
|
||||
];
|
||||
|
||||
const result = aggregateAndSplitData({
|
||||
splitKeys: ['Sku'],
|
||||
inputItems: data,
|
||||
fieldsToSummarize: aggregations,
|
||||
options: { continueIfFieldNotFound: true, skipEmptySplitFields: true },
|
||||
getValue: fieldValueGetter(),
|
||||
});
|
||||
expect(result).toMatchSnapshot('result');
|
||||
expect(flattenAggregationResultToArray(result)).toMatchSnapshot('array');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,416 @@
|
||||
{
|
||||
"name": "summarize test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-160, 340],
|
||||
"id": "0cc16e22-72e3-4f2c-a920-b1613fd4bcaf",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [
|
||||
{
|
||||
"category": "red",
|
||||
"text": "foo",
|
||||
"char": "a",
|
||||
"value": 1,
|
||||
"params.interval": "1 day",
|
||||
"params": {
|
||||
"interval": "1 day"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": "blue",
|
||||
"text": "spam",
|
||||
"char": "b",
|
||||
"value": 2,
|
||||
"params": {
|
||||
"interval": "1 hour"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": "green",
|
||||
"text": "bar",
|
||||
"char": "c",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"category": "red",
|
||||
"text": "foo",
|
||||
"char": "a",
|
||||
"value": 4
|
||||
},
|
||||
{
|
||||
"category": "red",
|
||||
"text": "bar",
|
||||
"char": "a",
|
||||
"value": 5,
|
||||
"params": {
|
||||
"interval": "1 minute"
|
||||
}
|
||||
},
|
||||
{
|
||||
"category": "blue",
|
||||
"text": "foo",
|
||||
"char": "a",
|
||||
"value": 6
|
||||
},
|
||||
{
|
||||
"category": "blue",
|
||||
"text": "foo",
|
||||
"char": "a",
|
||||
"value": 7,
|
||||
"params": {
|
||||
"interval": "1 second"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": "5a71d6f2-96fb-4616-94b7-5644c71e3bfb",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [60, 340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "append",
|
||||
"field": "char"
|
||||
},
|
||||
{
|
||||
"field": "char"
|
||||
},
|
||||
{
|
||||
"aggregation": "countUnique",
|
||||
"field": "char"
|
||||
},
|
||||
{
|
||||
"aggregation": "concatenate",
|
||||
"field": "char",
|
||||
"separateBy": ", "
|
||||
}
|
||||
]
|
||||
},
|
||||
"fieldsToSplitBy": "category, text",
|
||||
"options": {}
|
||||
},
|
||||
"id": "281b21e0-5737-4150-bce8-8331a3f83366",
|
||||
"name": "Summarize1",
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1,
|
||||
"position": [280, -60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "append",
|
||||
"field": "char"
|
||||
},
|
||||
{
|
||||
"field": "char"
|
||||
},
|
||||
{
|
||||
"aggregation": "countUnique",
|
||||
"field": "char"
|
||||
},
|
||||
{
|
||||
"aggregation": "concatenate",
|
||||
"field": "char",
|
||||
"separateBy": ", "
|
||||
}
|
||||
]
|
||||
},
|
||||
"fieldsToSplitBy": "category, text",
|
||||
"options": {
|
||||
"outputFormat": "singleItem"
|
||||
}
|
||||
},
|
||||
"id": "04bf538c-24d4-46d8-9c43-76ebc4fe7368",
|
||||
"name": "Summarize2",
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1,
|
||||
"position": [280, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "average",
|
||||
"field": "value"
|
||||
},
|
||||
{
|
||||
"aggregation": "max",
|
||||
"field": "value"
|
||||
},
|
||||
{
|
||||
"aggregation": "min",
|
||||
"field": "value"
|
||||
},
|
||||
{
|
||||
"aggregation": "max",
|
||||
"field": "value"
|
||||
},
|
||||
{
|
||||
"aggregation": "sum",
|
||||
"field": "value"
|
||||
},
|
||||
{
|
||||
"aggregation": "append",
|
||||
"field": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"fieldsToSplitBy": "category",
|
||||
"options": {}
|
||||
},
|
||||
"id": "e6f750f2-15c1-4de9-80a1-a255649d6e99",
|
||||
"name": "Summarize3",
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1,
|
||||
"position": [280, 340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "append",
|
||||
"field": "params.interval"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [280, 540],
|
||||
"id": "bfe0521a-c060-454f-99bb-b682aab44f63",
|
||||
"name": "Dot notation"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "append",
|
||||
"field": "params.interval"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"disableDotNotation": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [280, 740],
|
||||
"id": "b2c05f23-449f-49aa-80a3-4e1cd9156aa1",
|
||||
"name": "Dot notation (disabled)"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Summarize1": [
|
||||
{
|
||||
"json": {
|
||||
"category": "red",
|
||||
"text": "foo",
|
||||
"appended_char": ["a", "a"],
|
||||
"count_char": 2,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "a, a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"category": "red",
|
||||
"text": "bar",
|
||||
"appended_char": ["a"],
|
||||
"count_char": 1,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"category": "blue",
|
||||
"text": "spam",
|
||||
"appended_char": ["b"],
|
||||
"count_char": 1,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "b"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"category": "blue",
|
||||
"text": "foo",
|
||||
"appended_char": ["a", "a"],
|
||||
"count_char": 2,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "a, a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"category": "green",
|
||||
"text": "bar",
|
||||
"appended_char": ["c"],
|
||||
"count_char": 1,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "c"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Summarize2": [
|
||||
{
|
||||
"json": {
|
||||
"red": {
|
||||
"foo": {
|
||||
"appended_char": ["a", "a"],
|
||||
"count_char": 2,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "a, a"
|
||||
},
|
||||
"bar": {
|
||||
"appended_char": ["a"],
|
||||
"count_char": 1,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "a"
|
||||
}
|
||||
},
|
||||
"blue": {
|
||||
"spam": {
|
||||
"appended_char": ["b"],
|
||||
"count_char": 1,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "b"
|
||||
},
|
||||
"foo": {
|
||||
"appended_char": ["a", "a"],
|
||||
"count_char": 2,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "a, a"
|
||||
}
|
||||
},
|
||||
"green": {
|
||||
"bar": {
|
||||
"appended_char": ["c"],
|
||||
"count_char": 1,
|
||||
"unique_count_char": 1,
|
||||
"concatenated_char": "c"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Summarize3": [
|
||||
{
|
||||
"json": {
|
||||
"category": "red",
|
||||
"average_value": 3.3333333333333335,
|
||||
"max_value": 5,
|
||||
"min_value": 1,
|
||||
"sum_value": 10,
|
||||
"appended_value": [1, 4, 5]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"category": "blue",
|
||||
"average_value": 5,
|
||||
"max_value": 7,
|
||||
"min_value": 2,
|
||||
"sum_value": 15,
|
||||
"appended_value": [2, 6, 7]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"category": "green",
|
||||
"average_value": 3,
|
||||
"max_value": 3,
|
||||
"min_value": 3,
|
||||
"sum_value": 3,
|
||||
"appended_value": [3]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Dot notation": [
|
||||
{
|
||||
"json": {
|
||||
"appended_params_interval": ["1 day", "1 hour", "1 minute", "1 second"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Dot notation (disabled)": [
|
||||
{
|
||||
"json": {
|
||||
"appended_params_interval": ["1 day"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Summarize2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Summarize3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Dot notation",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Dot notation (disabled)",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "60e793d6-8c99-4bd2-aeb8-8ff350fc3101",
|
||||
"meta": {
|
||||
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||
},
|
||||
"id": "0HNQZNduHDmRCBBW",
|
||||
"tags": []
|
||||
}
|
||||
+366
@@ -0,0 +1,366 @@
|
||||
{
|
||||
"name": "Summarize Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "e46eed6e-4a6b-4e1f-bd7a-96cf579dd2ee",
|
||||
"name": "When clicking \"Execute workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-900, 260]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [
|
||||
{ "review": "Great product!" },
|
||||
{ "review": "" },
|
||||
{ "review": null },
|
||||
{ "review": null },
|
||||
{ "review": "Bad product" },
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [-520, -80],
|
||||
"id": "80f3462d-7ea5-4af1-bd13-f3f46b0ee43f",
|
||||
"name": "sample data1"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"field": "review"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-280, -320],
|
||||
"id": "98ff6686-bf9f-436a-8a64-3b3b1e648184",
|
||||
"name": "count non-empty"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "countUnique",
|
||||
"field": "review"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-280, -160],
|
||||
"id": "aa094d4c-5e00-49a4-9799-e7692b49f204",
|
||||
"name": "countUnique non-empty"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"field": "review",
|
||||
"includeEmpty": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-280, -40],
|
||||
"id": "c4a439c9-4d25-4ebb-b36c-c72d0d738049",
|
||||
"name": "count with Empty"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "countUnique",
|
||||
"field": "review",
|
||||
"includeEmpty": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-280, 100],
|
||||
"id": "e8abc054-5285-4dcc-980b-4bba4ef45ae0",
|
||||
"name": "countUnique non-empty1"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [
|
||||
{
|
||||
"region": "North",
|
||||
"sales": 100
|
||||
},
|
||||
{
|
||||
"region": "North",
|
||||
"sales": 200
|
||||
},
|
||||
{
|
||||
"region": "South",
|
||||
"sales": null
|
||||
},
|
||||
{
|
||||
"region": "",
|
||||
"sales": 300
|
||||
},
|
||||
{
|
||||
"region": "East"
|
||||
},
|
||||
{
|
||||
"region": "West",
|
||||
"sales": 400
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [-520, 500],
|
||||
"id": "0de1d232-09c1-412f-9481-0120db0792bf",
|
||||
"name": "sample data2"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"field": "region"
|
||||
},
|
||||
{
|
||||
"field": "sales"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-260, 300],
|
||||
"id": "b81e9c48-828c-4ac8-b7ca-818022384ca6",
|
||||
"name": "count non-empty1"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "countUnique",
|
||||
"field": "region"
|
||||
},
|
||||
{
|
||||
"field": "sales"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-260, 440],
|
||||
"id": "22bd90da-9b57-4b38-9cc3-356901a6f666",
|
||||
"name": "countUnique non-empty2"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"field": "region",
|
||||
"includeEmpty": true
|
||||
},
|
||||
{
|
||||
"field": "sales",
|
||||
"includeEmpty": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-260, 580],
|
||||
"id": "02bfe12f-eb71-483b-bef5-6007af739ace",
|
||||
"name": "count with Empty1"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "countUnique",
|
||||
"field": "region",
|
||||
"includeEmpty": true
|
||||
},
|
||||
{
|
||||
"field": "sales",
|
||||
"includeEmpty": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [-260, 720],
|
||||
"id": "e67cd4b5-47ae-4d73-b933-905b04183db8",
|
||||
"name": "countUnique non-empty3"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"count non-empty": [
|
||||
{
|
||||
"json": {
|
||||
"count_review": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"countUnique non-empty": [
|
||||
{
|
||||
"json": {
|
||||
"unique_count_review": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"count with Empty": [
|
||||
{
|
||||
"json": {
|
||||
"count_review": 7
|
||||
}
|
||||
}
|
||||
],
|
||||
"countUnique non-empty1": [
|
||||
{
|
||||
"json": {
|
||||
"unique_count_review": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"count non-empty1": [
|
||||
{
|
||||
"json": {
|
||||
"count_region": 5,
|
||||
"count_sales": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"countUnique non-empty2": [
|
||||
{
|
||||
"json": {
|
||||
"unique_count_region": 4,
|
||||
"count_sales": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"count with Empty1": [
|
||||
{
|
||||
"json": {
|
||||
"count_region": 6,
|
||||
"count_sales": 6
|
||||
}
|
||||
}
|
||||
],
|
||||
"countUnique non-empty3": [
|
||||
{
|
||||
"json": {
|
||||
"unique_count_region": 5,
|
||||
"count_sales": 6
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "sample data1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "sample data2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"sample data1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "count non-empty",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "countUnique non-empty",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "count with Empty",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "countUnique non-empty1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"sample data2": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "count non-empty1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "countUnique non-empty2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "count with Empty1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "countUnique non-empty3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "bb837da6-9160-4c61-b3a4-e7ef192bd5c6",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "b0d1a7f453aa2195078ce658269afa7743e3dc59edb733395b6c75bde57da7ff"
|
||||
},
|
||||
"id": "8ja9H2443AhLdBsS",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"name": "Summarize Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [-320, -80],
|
||||
"id": "509c50eb-a5c3-4172-a553-82cce7ca0455",
|
||||
"name": "When clicking ‘Execute workflow’"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": [
|
||||
{
|
||||
"uid": "a0e797c2-ffcf-495b-96f3-982f7bc4eae5",
|
||||
"email": "Alfredo83@gmail.com",
|
||||
"firstname": "Charlie",
|
||||
"lastname": "Jacobi",
|
||||
"password": "34~XLKdGit",
|
||||
"Group": 0
|
||||
},
|
||||
{
|
||||
"uid": "5713682e-4f32-47dc-ae89-ae4a8ac28a46",
|
||||
"email": "Zachary43@yahoo.com",
|
||||
"firstname": "Leonard",
|
||||
"lastname": "Blick",
|
||||
"password": "4sh5z!R",
|
||||
"Group": 0
|
||||
},
|
||||
{
|
||||
"uid": "1bb2a432-d2fb-48e4-a7b0-39030ef0d784",
|
||||
"email": "Cory_Wilderman@yahoo.com",
|
||||
"firstname": "Debra",
|
||||
"lastname": "Corkery",
|
||||
"password": "-8X1CKMFh",
|
||||
"Group": 1
|
||||
},
|
||||
{
|
||||
"uid": "4edb11bc-9256-4f26-9be6-f8b04e90ecb2",
|
||||
"email": "Mindy_Murazik86@gmail.com",
|
||||
"firstname": "Wendy",
|
||||
"lastname": "Schiller",
|
||||
"password": "WF373t,fe",
|
||||
"Group": 2
|
||||
},
|
||||
{
|
||||
"uid": "ec6efd3d-508c-4f1e-acae-c9467b74f5a6",
|
||||
"email": "Darin_Vandervort@yahoo.com",
|
||||
"firstname": "Dolores",
|
||||
"lastname": "Walter",
|
||||
"password": ".45KCBk",
|
||||
"Group": 2
|
||||
},
|
||||
{
|
||||
"uid": "173ee780-4e4f-4164-aa39-41a0402cd2ad",
|
||||
"email": "Claude.Grady6@gmail.com",
|
||||
"firstname": "Leonard",
|
||||
"lastname": "Krajcik",
|
||||
"password": "6x6-MN8vch",
|
||||
"Group": null
|
||||
},
|
||||
{
|
||||
"uid": "b1f191d8-4f95-49f6-8533-042048b4c162",
|
||||
"email": "Ismael_Volkman@hotmail.com",
|
||||
"firstname": "Terri",
|
||||
"lastname": "Kuphal",
|
||||
"password": "62+BNNCk",
|
||||
"Group": null
|
||||
},
|
||||
{
|
||||
"uid": "ee8c6d18-1e77-435d-8c9a-5215141d7697",
|
||||
"email": "Paulette.Hudson@hotmail.com",
|
||||
"firstname": "Clinton",
|
||||
"lastname": "Murphy",
|
||||
"password": "2+R8IRxHL",
|
||||
"Group": null
|
||||
},
|
||||
{
|
||||
"uid": "feeb4357-0e20-4b15-9199-f161c23da1a2",
|
||||
"email": "Guillermo.Kub33@gmail.com",
|
||||
"firstname": "Edward",
|
||||
"lastname": "Schaden",
|
||||
"password": "Kv23S1_H",
|
||||
"Group": 0
|
||||
},
|
||||
{
|
||||
"uid": "676003ec-8a40-4105-8237-ba1d9eb51fc2",
|
||||
"email": "Cindy.White@hotmail.com",
|
||||
"firstname": "Esther",
|
||||
"lastname": "Monahan",
|
||||
"password": "5q$RyBhp",
|
||||
"Group": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [-100, -80],
|
||||
"id": "691f2744-8689-4b5c-91e7-2bb378691ecf",
|
||||
"name": "sample data"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "append",
|
||||
"field": "email",
|
||||
"includeEmpty": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"fieldsToSplitBy": "Group",
|
||||
"options": {
|
||||
"outputFormat": "separateItems"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1.1,
|
||||
"position": [160, 0],
|
||||
"id": "d1267da5-0cab-453c-9450-1bbd78346bd6",
|
||||
"name": "Summarize v1.1"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldsToSummarize": {
|
||||
"values": [
|
||||
{
|
||||
"aggregation": "append",
|
||||
"field": "email",
|
||||
"includeEmpty": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"fieldsToSplitBy": "Group",
|
||||
"options": {
|
||||
"continueIfFieldNotFound": false,
|
||||
"outputFormat": "separateItems"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.summarize",
|
||||
"typeVersion": 1,
|
||||
"position": [160, -160],
|
||||
"id": "680b7269-c920-4cce-b9f5-b2189955848d",
|
||||
"name": "Summarize v1"
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Summarize v1": [
|
||||
{
|
||||
"json": {
|
||||
"Group": "0",
|
||||
"appended_email": [
|
||||
"Alfredo83@gmail.com",
|
||||
"Zachary43@yahoo.com",
|
||||
"Guillermo.Kub33@gmail.com"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"Group": "1",
|
||||
"appended_email": ["Cory_Wilderman@yahoo.com"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"Group": "2",
|
||||
"appended_email": [
|
||||
"Mindy_Murazik86@gmail.com",
|
||||
"Darin_Vandervort@yahoo.com",
|
||||
"Cindy.White@hotmail.com"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"Group": "null",
|
||||
"appended_email": [
|
||||
"Claude.Grady6@gmail.com",
|
||||
"Ismael_Volkman@hotmail.com",
|
||||
"Paulette.Hudson@hotmail.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"Summarize v1.1": [
|
||||
{
|
||||
"json": {
|
||||
"Group": 0,
|
||||
"appended_email": [
|
||||
"Alfredo83@gmail.com",
|
||||
"Zachary43@yahoo.com",
|
||||
"Guillermo.Kub33@gmail.com"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"Group": 1,
|
||||
"appended_email": ["Cory_Wilderman@yahoo.com"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"Group": 2,
|
||||
"appended_email": [
|
||||
"Mindy_Murazik86@gmail.com",
|
||||
"Darin_Vandervort@yahoo.com",
|
||||
"Cindy.White@hotmail.com"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"Group": null,
|
||||
"appended_email": [
|
||||
"Claude.Grady6@gmail.com",
|
||||
"Ismael_Volkman@hotmail.com",
|
||||
"Paulette.Hudson@hotmail.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "sample data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"sample data": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize v1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Summarize v1.1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "7304eae2-3bae-4934-8d1f-1135a0ff8ab8",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "b0d1a7f453aa2195078ce658269afa7743e3dc59edb733395b6c75bde57da7ff"
|
||||
},
|
||||
"id": "8ja9H2443AhLdBsS",
|
||||
"tags": []
|
||||
}
|
||||
@@ -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 ‘Execute 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 ‘Execute workflow’": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "DebugHelper2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"DebugHelper2": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Summarize1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {},
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "ee90fdf8d57662f949e6c691dc07fa0fd2f66e1eee28ed82ef06658223e67255"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import get from 'lodash/get';
|
||||
import {
|
||||
type GenericValue,
|
||||
type IDataObject,
|
||||
type IExecuteFunctions,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
type AggregationType =
|
||||
| 'append'
|
||||
| 'average'
|
||||
| 'concatenate'
|
||||
| 'count'
|
||||
| 'countUnique'
|
||||
| 'max'
|
||||
| 'min'
|
||||
| 'sum';
|
||||
|
||||
export type Aggregation = {
|
||||
aggregation: AggregationType;
|
||||
field: string;
|
||||
includeEmpty?: boolean;
|
||||
separateBy?: string;
|
||||
customSeparator?: string;
|
||||
};
|
||||
|
||||
export type Aggregations = Aggregation[];
|
||||
|
||||
const AggregationDisplayNames = {
|
||||
append: 'appended_',
|
||||
average: 'average_',
|
||||
concatenate: 'concatenated_',
|
||||
count: 'count_',
|
||||
countUnique: 'unique_count_',
|
||||
max: 'max_',
|
||||
min: 'min_',
|
||||
sum: 'sum_',
|
||||
};
|
||||
|
||||
export const NUMERICAL_AGGREGATIONS = ['average', 'sum'];
|
||||
|
||||
export type SummarizeOptions = {
|
||||
continueIfFieldNotFound: boolean;
|
||||
disableDotNotation?: boolean;
|
||||
outputFormat?: 'separateItems' | 'singleItem';
|
||||
skipEmptySplitFields?: boolean;
|
||||
};
|
||||
|
||||
export type ValueGetterFn = (
|
||||
item: IDataObject,
|
||||
field: string,
|
||||
) => IDataObject | IDataObject[] | GenericValue | GenericValue[];
|
||||
|
||||
function isEmpty<T>(value: T) {
|
||||
return value === undefined || value === null || value === '';
|
||||
}
|
||||
|
||||
function normalizeFieldName(fieldName: string) {
|
||||
return fieldName.replace(/[\]\["]/g, '').replace(/[ .]/g, '_');
|
||||
}
|
||||
|
||||
export const fieldValueGetter = (disableDotNotation?: boolean) => {
|
||||
return (item: IDataObject, field: string) =>
|
||||
disableDotNotation ? item[field] : get(item, field);
|
||||
};
|
||||
|
||||
export function checkIfFieldExists(
|
||||
this: IExecuteFunctions,
|
||||
items: IDataObject[],
|
||||
aggregations: Aggregations,
|
||||
getValue: ValueGetterFn,
|
||||
) {
|
||||
for (const aggregation of aggregations) {
|
||||
if (aggregation.field === '') {
|
||||
continue;
|
||||
}
|
||||
const exist = items.some((item) => getValue(item, aggregation.field) !== undefined);
|
||||
if (!exist) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`The field '${aggregation.field}' does not exist in any items`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function aggregate(items: IDataObject[], entry: Aggregation, getValue: ValueGetterFn) {
|
||||
const { aggregation, field } = entry;
|
||||
let data = [...items];
|
||||
|
||||
if (NUMERICAL_AGGREGATIONS.includes(aggregation)) {
|
||||
data = data.filter(
|
||||
(item) => typeof getValue(item, field) === 'number' && !isEmpty(getValue(item, field)),
|
||||
);
|
||||
}
|
||||
|
||||
switch (aggregation) {
|
||||
//combine operations
|
||||
case 'append':
|
||||
if (!entry.includeEmpty) {
|
||||
data = data.filter((item) => !isEmpty(getValue(item, field)));
|
||||
}
|
||||
return data.map((item) => getValue(item, field));
|
||||
case 'concatenate':
|
||||
const separateBy = entry.separateBy === 'other' ? entry.customSeparator : entry.separateBy;
|
||||
if (!entry.includeEmpty) {
|
||||
data = data.filter((item) => !isEmpty(getValue(item, field)));
|
||||
}
|
||||
return data
|
||||
.map((item) => {
|
||||
let value = getValue(item, field);
|
||||
if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
if (typeof value === 'undefined') {
|
||||
value = 'undefined';
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
.join(separateBy);
|
||||
|
||||
//numerical operations
|
||||
case 'average':
|
||||
return (
|
||||
data.reduce((acc, item) => {
|
||||
return acc + (getValue(item, field) as number);
|
||||
}, 0) / data.length
|
||||
);
|
||||
case 'sum':
|
||||
return data.reduce((acc, item) => {
|
||||
return acc + (getValue(item, field) as number);
|
||||
}, 0);
|
||||
//comparison operations
|
||||
case 'min':
|
||||
let min;
|
||||
for (const item of data) {
|
||||
const value = getValue(item, field);
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (min === undefined || value < min) {
|
||||
min = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return min ?? null;
|
||||
case 'max':
|
||||
let max;
|
||||
for (const item of data) {
|
||||
const value = getValue(item, field);
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
if (max === undefined || value > max) {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return max ?? null;
|
||||
|
||||
//count operations
|
||||
case 'countUnique':
|
||||
if (!entry.includeEmpty) {
|
||||
return new Set(data.map((item) => getValue(item, field)).filter((item) => !isEmpty(item)))
|
||||
.size;
|
||||
}
|
||||
return new Set(data.map((item) => getValue(item, field))).size;
|
||||
|
||||
default:
|
||||
//count by default
|
||||
if (!entry.includeEmpty) {
|
||||
return data.filter((item) => !isEmpty(getValue(item, field))).length;
|
||||
}
|
||||
return data.length;
|
||||
}
|
||||
}
|
||||
|
||||
function aggregateData(
|
||||
data: IDataObject[],
|
||||
fieldsToSummarize: Aggregations,
|
||||
options: SummarizeOptions,
|
||||
getValue: ValueGetterFn,
|
||||
): { returnData: IDataObject; pairedItems?: number[] } {
|
||||
const returnData = Object.fromEntries(
|
||||
fieldsToSummarize.map((aggregation) => {
|
||||
const key = normalizeFieldName(
|
||||
`${AggregationDisplayNames[aggregation.aggregation]}${aggregation.field}`,
|
||||
);
|
||||
const result = aggregate(data, aggregation, getValue);
|
||||
return [key, result];
|
||||
}),
|
||||
);
|
||||
|
||||
if (options.outputFormat === 'singleItem') {
|
||||
return { returnData };
|
||||
}
|
||||
|
||||
return { returnData, pairedItems: data.map((item) => item._itemIndex as number) };
|
||||
}
|
||||
|
||||
type AggregationResult = { returnData: IDataObject; pairedItems?: number[] };
|
||||
type NestedAggregationResult =
|
||||
| AggregationResult
|
||||
| { fieldName: string; splits: Map<unknown, NestedAggregationResult> };
|
||||
|
||||
// Using Map to preserve types
|
||||
// With a plain JS object, keys are converted to string
|
||||
export function aggregateAndSplitData({
|
||||
splitKeys,
|
||||
inputItems,
|
||||
fieldsToSummarize,
|
||||
options,
|
||||
getValue,
|
||||
convertKeysToString = false,
|
||||
}: {
|
||||
splitKeys: string[] | undefined;
|
||||
inputItems: IDataObject[];
|
||||
fieldsToSummarize: Aggregations;
|
||||
options: SummarizeOptions;
|
||||
getValue: ValueGetterFn;
|
||||
convertKeysToString?: boolean; // Legacy option for v1
|
||||
}): NestedAggregationResult {
|
||||
if (!splitKeys?.length) {
|
||||
return aggregateData(inputItems, fieldsToSummarize, options, getValue);
|
||||
}
|
||||
|
||||
const [firstSplitKey, ...restSplitKeys] = splitKeys;
|
||||
|
||||
const groupedItems = new Map<unknown, IDataObject[]>();
|
||||
for (const item of inputItems) {
|
||||
let splitValue = getValue(item, firstSplitKey);
|
||||
|
||||
if (splitValue && typeof splitValue === 'object') {
|
||||
splitValue = JSON.stringify(splitValue);
|
||||
}
|
||||
|
||||
if (convertKeysToString) {
|
||||
splitValue = String(splitValue);
|
||||
}
|
||||
|
||||
if (options.skipEmptySplitFields && typeof splitValue !== 'number' && !splitValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const group = groupedItems.get(splitValue) ?? [];
|
||||
groupedItems.set(splitValue, group.concat([item]));
|
||||
}
|
||||
|
||||
const splits = new Map(
|
||||
Array.from(groupedItems.entries()).map(([groupKey, items]) => [
|
||||
groupKey,
|
||||
aggregateAndSplitData({
|
||||
splitKeys: restSplitKeys,
|
||||
inputItems: items,
|
||||
fieldsToSummarize,
|
||||
options,
|
||||
getValue,
|
||||
convertKeysToString,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
return { fieldName: firstSplitKey, splits };
|
||||
}
|
||||
|
||||
export function flattenAggregationResultToObject(result: NestedAggregationResult): IDataObject {
|
||||
if ('splits' in result) {
|
||||
return Object.fromEntries(
|
||||
Array.from(result.splits.entries()).map(([key, value]) => [
|
||||
key,
|
||||
flattenAggregationResultToObject(value),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
return result.returnData;
|
||||
}
|
||||
|
||||
export function flattenAggregationResultToArray(
|
||||
result: NestedAggregationResult,
|
||||
): AggregationResult[] {
|
||||
if ('splits' in result) {
|
||||
return Array.from(result.splits.entries()).flatMap(([value, innerResult]) =>
|
||||
flattenAggregationResultToArray(innerResult).map((v) => {
|
||||
v.returnData[normalizeFieldName(result.fieldName)] = value as IDataObject;
|
||||
return v;
|
||||
}),
|
||||
);
|
||||
}
|
||||
return [result];
|
||||
}
|
||||
Reference in New Issue
Block a user