feat(Item Lists Node): Split merge binary data (#7297)

Github issue / Community forum post (link here to close automatically):

---------

Co-authored-by: Marcus <marcus@n8n.io>
This commit is contained in:
Michael Kret 2023-10-11 10:59:51 +03:00 committed by GitHub
parent e2c3c7aceb
commit 965db8f7f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 197 additions and 63 deletions

View file

@ -13,7 +13,7 @@ import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty'; import isEmpty from 'lodash/isEmpty';
import set from 'lodash/set'; import set from 'lodash/set';
import { prepareFieldsArray } from '../../helpers/utils'; import { addBinariesToItem, prepareFieldsArray } from '../../helpers/utils';
import { disableDotNotationBoolean } from '../common.descriptions'; import { disableDotNotationBoolean } from '../common.descriptions';
const properties: INodeProperties[] = [ const properties: INodeProperties[] = [
@ -159,13 +159,15 @@ const properties: INodeProperties[] = [
type: 'collection', type: 'collection',
placeholder: 'Add Field', placeholder: 'Add Field',
default: {}, default: {},
displayOptions: {
hide: {
aggregate: ['aggregateAllItemData'],
},
},
options: [ options: [
disableDotNotationBoolean, {
...disableDotNotationBoolean,
displayOptions: {
hide: {
'/aggregate': ['aggregateAllItemData'],
},
},
},
{ {
displayName: 'Merge Lists', displayName: 'Merge Lists',
name: 'mergeLists', name: 'mergeLists',
@ -173,6 +175,31 @@ const properties: INodeProperties[] = [
default: false, default: false,
description: description:
'Whether to merge the output into a single flat list (rather than a list of lists), if the field to aggregate is a list', 'Whether to merge the output into a single flat list (rather than a list of lists), if the field to aggregate is a list',
displayOptions: {
hide: {
'/aggregate': ['aggregateAllItemData'],
},
},
},
{
displayName: 'Include Binaries',
name: 'includeBinaries',
type: 'boolean',
default: false,
description: 'Whether to include the binary data in the new item',
},
{
displayName: 'Keep Only Unique Binaries',
name: 'keepOnlyUnique',
type: 'boolean',
default: false,
description:
'Whether to keep only unique binaries by comparing mime types, file types, file sizes and file extensions',
displayOptions: {
show: {
includeBinaries: [true],
},
},
}, },
{ {
displayName: 'Keep Missing And Null Values', displayName: 'Keep Missing And Null Values',
@ -181,6 +208,11 @@ const properties: INodeProperties[] = [
default: false, default: false,
description: description:
'Whether to add a null entry to the aggregated list when there is a missing or null value', 'Whether to add a null entry to the aggregated list when there is a missing or null value',
displayOptions: {
hide: {
'/aggregate': ['aggregateAllItemData'],
},
},
}, },
], ],
}, },
@ -199,7 +231,7 @@ export async function execute(
this: IExecuteFunctions, this: IExecuteFunctions,
items: INodeExecutionData[], items: INodeExecutionData[],
): Promise<INodeExecutionData[]> { ): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = []; let returnData: INodeExecutionData = { json: {}, pairedItem: [] };
const aggregate = this.getNodeParameter('aggregate', 0, '') as string; const aggregate = this.getNodeParameter('aggregate', 0, '') as string;
@ -305,7 +337,7 @@ export async function execute(
} }
} }
returnData.push(newItem); returnData = newItem;
} else { } else {
let newItems: IDataObject[] = items.map((item) => item.json); let newItems: IDataObject[] = items.map((item) => item.json);
let pairedItem: IPairedItemData[] = []; let pairedItem: IPairedItemData[] = [];
@ -353,8 +385,23 @@ export async function execute(
} }
const output: INodeExecutionData = { json: { [destinationFieldName]: newItems }, pairedItem }; const output: INodeExecutionData = { json: { [destinationFieldName]: newItems }, pairedItem };
returnData.push(output);
returnData = output;
} }
return returnData; const includeBinaries = this.getNodeParameter('options.includeBinaries', 0, false) as boolean;
if (includeBinaries) {
const pairedItems = (returnData.pairedItem || []) as IPairedItemData[];
const aggregatedItems = pairedItems.map((item) => {
return items[item.item];
});
const keepOnlyUnique = this.getNodeParameter('options.keepOnlyUnique', 0, false) as boolean;
addBinariesToItem(returnData, aggregatedItems, keepOnlyUnique);
}
return [returnData];
} }

View file

@ -1,4 +1,5 @@
import type { import type {
IBinaryData,
IDataObject, IDataObject,
IExecuteFunctions, IExecuteFunctions,
INodeExecutionData, INodeExecutionData,
@ -20,7 +21,9 @@ const properties: INodeProperties[] = [
type: 'string', type: 'string',
default: '', default: '',
required: true, required: true,
description: 'The name of the input fields to break out into separate items', placeholder: 'Drag fields from the left or type their names',
description:
'The name of the input fields to break out into separate items. Separate multiple field names by commas. For binary data, use $binary.',
requiresDataPath: 'multiple', requiresDataPath: 'multiple',
}, },
{ {
@ -74,6 +77,13 @@ const properties: INodeProperties[] = [
default: '', default: '',
description: 'The field in the output under which to put the split field contents', description: 'The field in the output under which to put the split field contents',
}, },
{
displayName: 'Include Binary',
name: 'includeBinary',
type: 'boolean',
default: false,
description: 'Whether to include the binary data in the new items',
},
], ],
}, },
]; ];
@ -96,16 +106,13 @@ export async function execute(
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const fieldsToSplitOut = (this.getNodeParameter('fieldToSplitOut', i) as string) const fieldsToSplitOut = (this.getNodeParameter('fieldToSplitOut', i) as string)
.split(',') .split(',')
.map((field) => field.trim()); .map((field) => field.trim().replace(/^\$json\./, ''));
const disableDotNotation = this.getNodeParameter(
'options.disableDotNotation',
0,
false,
) as boolean;
const destinationFields = ( const options = this.getNodeParameter('options', i, {});
this.getNodeParameter('options.destinationFieldName', i, '') as string
) const disableDotNotation = options.disableDotNotation as boolean;
const destinationFields = ((options.destinationFieldName as string) || '')
.split(',') .split(',')
.filter((field) => field.trim() !== '') .filter((field) => field.trim() !== '')
.map((field) => field.trim()); .map((field) => field.trim());
@ -125,54 +132,71 @@ export async function execute(
const multiSplit = fieldsToSplitOut.length > 1; const multiSplit = fieldsToSplitOut.length > 1;
const item = { ...items[i].json }; const item = { ...items[i].json };
const splited: IDataObject[] = []; const splited: INodeExecutionData[] = [];
for (const [entryIndex, fieldToSplitOut] of fieldsToSplitOut.entries()) { for (const [entryIndex, fieldToSplitOut] of fieldsToSplitOut.entries()) {
const destinationFieldName = destinationFields[entryIndex] || ''; const destinationFieldName = destinationFields[entryIndex] || '';
let arrayToSplit; let entityToSplit: IDataObject[] = [];
if (!disableDotNotation) {
arrayToSplit = get(item, fieldToSplitOut); if (fieldToSplitOut === '$binary') {
entityToSplit = Object.entries(items[i].binary || {}).map(([key, value]) => ({
[key]: value,
}));
} else { } else {
arrayToSplit = item[fieldToSplitOut]; if (!disableDotNotation) {
entityToSplit = get(item, fieldToSplitOut) as IDataObject[];
} else {
entityToSplit = item[fieldToSplitOut] as IDataObject[];
}
if (entityToSplit === undefined) {
entityToSplit = [];
}
if (typeof entityToSplit !== 'object' || entityToSplit === null) {
entityToSplit = [entityToSplit];
}
if (!Array.isArray(entityToSplit)) {
entityToSplit = Object.values(entityToSplit);
}
} }
if (arrayToSplit === undefined) { for (const [elementIndex, element] of entityToSplit.entries()) {
arrayToSplit = [];
}
if (typeof arrayToSplit !== 'object' || arrayToSplit === null) {
arrayToSplit = [arrayToSplit];
}
if (!Array.isArray(arrayToSplit)) {
arrayToSplit = Object.values(arrayToSplit);
}
for (const [elementIndex, element] of arrayToSplit.entries()) {
if (splited[elementIndex] === undefined) { if (splited[elementIndex] === undefined) {
splited[elementIndex] = {}; splited[elementIndex] = { json: {}, pairedItem: { item: i } };
} }
const fieldName = destinationFieldName || fieldToSplitOut; const fieldName = destinationFieldName || fieldToSplitOut;
if (fieldToSplitOut === '$binary') {
if (splited[elementIndex].binary === undefined) {
splited[elementIndex].binary = {};
}
splited[elementIndex].binary![Object.keys(element)[0]] = Object.values(
element,
)[0] as IBinaryData;
continue;
}
if (typeof element === 'object' && element !== null && include === 'noOtherFields') { if (typeof element === 'object' && element !== null && include === 'noOtherFields') {
if (destinationFieldName === '' && !multiSplit) { if (destinationFieldName === '' && !multiSplit) {
splited[elementIndex] = { ...splited[elementIndex], ...element }; splited[elementIndex] = {
json: { ...splited[elementIndex].json, ...element },
pairedItem: { item: i },
};
} else { } else {
splited[elementIndex][fieldName] = element; splited[elementIndex].json[fieldName] = element;
} }
} else { } else {
splited[elementIndex][fieldName] = element; splited[elementIndex].json[fieldName] = element;
} }
} }
} }
for (const splitEntry of splited) { for (const splitEntry of splited) {
let newItem: IDataObject = {}; let newItem: INodeExecutionData = splitEntry;
if (include === 'noOtherFields') {
newItem = splitEntry;
}
if (include === 'allOtherFields') { if (include === 'allOtherFields') {
const itemCopy = deepCopy(item); const itemCopy = deepCopy(item);
@ -183,7 +207,7 @@ export async function execute(
delete itemCopy[fieldToSplitOut]; delete itemCopy[fieldToSplitOut];
} }
} }
newItem = { ...itemCopy, ...splitEntry }; newItem.json = { ...itemCopy, ...splitEntry.json };
} }
if (include === 'selectedOtherFields') { if (include === 'selectedOtherFields') {
@ -200,21 +224,24 @@ export async function execute(
for (const field of fieldsToInclude) { for (const field of fieldsToInclude) {
if (!disableDotNotation) { if (!disableDotNotation) {
splitEntry[field] = get(item, field); splitEntry.json[field] = get(item, field);
} else { } else {
splitEntry[field] = item[field]; splitEntry.json[field] = item[field];
} }
} }
newItem = splitEntry; newItem = splitEntry;
} }
returnData.push({ const includeBinary = options.includeBinary as boolean;
json: newItem,
pairedItem: { if (includeBinary) {
item: i, if (items[i].binary && !newItem.binary) {
}, newItem.binary = items[i].binary;
}); }
}
returnData.push(newItem);
} }
} }

View file

@ -1,11 +1,12 @@
import { NodeVM } from '@n8n/vm2'; import { NodeVM } from '@n8n/vm2';
import { import type {
NodeOperationError, IDataObject,
type IDataObject, IExecuteFunctions,
type IExecuteFunctions, IBinaryData,
type INode, INode,
type INodeExecutionData, INodeExecutionData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import get from 'lodash/get'; import get from 'lodash/get';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
@ -87,3 +88,62 @@ export function sortByCode(
return vm.run(`module.exports = items.sort((a, b) => { ${code} })`); return vm.run(`module.exports = items.sort((a, b) => { ${code} })`);
} }
type PartialBinaryData = Omit<IBinaryData, 'data'>;
const isBinaryUniqueSetup = () => {
const binaries: PartialBinaryData[] = [];
return (binary: IBinaryData) => {
for (const existingBinary of binaries) {
if (
existingBinary.mimeType === binary.mimeType &&
existingBinary.fileType === binary.fileType &&
existingBinary.fileSize === binary.fileSize &&
existingBinary.fileExtension === binary.fileExtension
) {
return false;
}
}
binaries.push({
mimeType: binary.mimeType,
fileType: binary.fileType,
fileSize: binary.fileSize,
fileExtension: binary.fileExtension,
});
return true;
};
};
export function addBinariesToItem(
newItem: INodeExecutionData,
items: INodeExecutionData[],
uniqueOnly?: boolean,
) {
const isBinaryUnique = uniqueOnly ? isBinaryUniqueSetup() : undefined;
for (const item of items) {
if (item.binary === undefined) continue;
for (const key of Object.keys(item.binary)) {
if (!newItem.binary) newItem.binary = {};
let binaryKey = key;
const binary = item.binary[key];
if (isBinaryUnique && !isBinaryUnique(binary)) {
continue;
}
// If the binary key already exists add a suffix to it
let i = 1;
while (newItem.binary[binaryKey] !== undefined) {
binaryKey = `${key}_${i}`;
i++;
}
newItem.binary[binaryKey] = binary;
}
}
return newItem;
}