import { NodeOperationError, type IExecuteFunctions, type INodeExecutionData, type INodeType, type INodeTypeDescription, } from 'n8n-workflow'; import { type Aggregations, NUMERICAL_AGGREGATIONS, type SummarizeOptions, aggregationToArray, checkIfFieldExists, fieldValueGetter, splitData, } from './utils'; import { generatePairedItemData } from '../../../utils/utilities'; export class Summarize implements INodeType { description: INodeTypeDescription = { displayName: 'Summarize', name: 'summarize', icon: 'file:summarize.svg', group: ['transform'], subtitle: '', version: 1, description: 'Sum, count, max, etc. across items', defaults: { name: 'Summarize', }, inputs: ['main'], outputs: ['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'], }, }, }, { 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, owerwise an error would be thrown", }, { 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 { 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; if (nodeVersion < 2.1) { try { checkIfFieldExists.call(this, newItems, fieldsToSummarize, getValue); } catch (error) { if (options.continueIfFieldNotFound) { const itemData = generatePairedItemData(items.length); return [[{ json: {}, pairedItem: itemData }]]; } else { throw error; } } } const aggregationResult = splitData( fieldsToSplitBy, newItems, fieldsToSummarize, options, getValue, ); if (options.outputFormat === 'singleItem') { const executionData: INodeExecutionData = { json: aggregationResult, pairedItem: newItems.map((_v, index) => ({ item: index, })), }; return [[executionData]]; } else { if (!fieldsToSplitBy.length) { const { pairedItems, ...json } = aggregationResult; const executionData: INodeExecutionData = { json, pairedItem: ((pairedItems as number[]) || []).map((index: number) => ({ item: index, })), }; return [[executionData]]; } const returnData = aggregationToArray(aggregationResult, fieldsToSplitBy); const executionData = returnData.map((item) => { const { pairedItems, ...json } = item; return { json, pairedItem: ((pairedItems as number[]) || []).map((index: number) => ({ item: index, })), }; }); return [executionData]; } } }