From 1dbca4402579ce1e323ab30c5caebef247a19c8d Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Thu, 15 Jun 2023 16:52:39 +0300 Subject: [PATCH] feat(Item Lists Node): Improvements (#6190) --- .../nodes/ItemLists/ItemLists.node.ts | 3 +- .../nodes/ItemLists/V2/ItemListsV2.node.ts | 210 +++++----- .../test/node/workflow.update_2_2.json | 361 ++++++++++++++++++ 3 files changed, 481 insertions(+), 93 deletions(-) create mode 100644 packages/nodes-base/nodes/ItemLists/test/node/workflow.update_2_2.json diff --git a/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts b/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts index 255c459fd7..dade91b64f 100644 --- a/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts +++ b/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts @@ -14,13 +14,14 @@ export class ItemLists extends VersionedNodeType { group: ['input'], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Helper for working with lists of items and transforming arrays', - defaultVersion: 2.1, + defaultVersion: 2.2, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new ItemListsV1(baseDescription), 2: new ItemListsV2(baseDescription), 2.1: new ItemListsV2(baseDescription), + 2.2: new ItemListsV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts b/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts index 53bfeb8659..1a064a83da 100644 --- a/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts +++ b/packages/nodes-base/nodes/ItemLists/V2/ItemListsV2.node.ts @@ -10,7 +10,7 @@ import type { INodeTypeBaseDescription, INodeTypeDescription, } from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; +import { NodeOperationError, deepCopy } from 'n8n-workflow'; import get from 'lodash.get'; import isEmpty from 'lodash.isempty'; @@ -68,7 +68,7 @@ export class ItemListsV2 implements INodeType { constructor(baseDescription: INodeTypeBaseDescription) { this.description = { ...baseDescription, - version: [2, 2.1], + version: [2, 2.1, 2.2], defaults: { name: 'Item Lists', }, @@ -136,7 +136,7 @@ export class ItemListsV2 implements INodeType { }, // Split out items - Fields { - displayName: 'Field To Split Out', + displayName: 'Fields To Split Out', name: 'fieldToSplitOut', type: 'string', default: '', @@ -147,8 +147,8 @@ export class ItemListsV2 implements INodeType { operation: ['splitOutItems'], }, }, - description: 'The name of the input field to break out into separate items', - requiresDataPath: 'single', + description: 'The name of the input fields to break out into separate items', + requiresDataPath: 'multiple', }, { displayName: 'Include', @@ -755,6 +755,7 @@ return 0;`, displayName: 'Destination Field Name', name: 'destinationFieldName', type: 'string', + requiresDataPath: 'multiple', displayOptions: { show: { '/operation': ['splitOutItems'], @@ -808,63 +809,124 @@ return 0;`, if (resource === 'itemList') { if (operation === 'splitOutItems') { for (let i = 0; i < length; i++) { - const fieldToSplitOut = this.getNodeParameter('fieldToSplitOut', i) as string; + const fieldsToSplitOut = (this.getNodeParameter('fieldToSplitOut', i) as string) + .split(',') + .map((field) => field.trim()); const disableDotNotation = this.getNodeParameter( 'options.disableDotNotation', 0, false, ) as boolean; - const destinationFieldName = this.getNodeParameter( - 'options.destinationFieldName', - i, - '', - ) as string; - const include = this.getNodeParameter('include', i) as string; - let arrayToSplit; - if (!disableDotNotation) { - arrayToSplit = get(items[i].json, fieldToSplitOut); - } else { - arrayToSplit = items[i].json[fieldToSplitOut]; - } + const destinationFields = ( + this.getNodeParameter('options.destinationFieldName', i, '') as string + ) + .split(',') + .filter((field) => field.trim() !== '') + .map((field) => field.trim()); - if (arrayToSplit === undefined) { - if (nodeVersion < 2.1) { - if (fieldToSplitOut.includes('.') && disableDotNotation) { - throw new NodeOperationError( - this.getNode(), - `Couldn't find the field '${fieldToSplitOut}' in the input data`, - { - description: - "If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options", - }, - ); - } else { - throw new NodeOperationError( - this.getNode(), - `Couldn't find the field '${fieldToSplitOut}' in the input data`, - { itemIndex: i }, - ); - } - } else { - arrayToSplit = []; - } - } - - if (typeof arrayToSplit !== 'object' || arrayToSplit === null) { + if (destinationFields.length && destinationFields.length !== fieldsToSplitOut.length) { throw new NodeOperationError( this.getNode(), - `The provided field '${fieldToSplitOut}' is not an array or object`, - { itemIndex: i }, + 'If multiple fields to split out are given, the same number of destination fields must be given', ); } - if (!Array.isArray(arrayToSplit)) { - arrayToSplit = Object.values(arrayToSplit); + const include = this.getNodeParameter('include', i) as + | 'selectedOtherFields' + | 'allOtherFields' + | 'noOtherFields'; + + const multiSplit = fieldsToSplitOut.length > 1; + + const item = { ...items[i].json }; + const splited: IDataObject[] = []; + for (const [entryIndex, fieldToSplitOut] of fieldsToSplitOut.entries()) { + const destinationFieldName = destinationFields[entryIndex] || ''; + + let arrayToSplit; + if (!disableDotNotation) { + arrayToSplit = get(item, fieldToSplitOut); + } else { + arrayToSplit = item[fieldToSplitOut]; + } + + if (arrayToSplit === undefined) { + if (nodeVersion < 2.1) { + if (fieldToSplitOut.includes('.') && disableDotNotation) { + throw new NodeOperationError( + this.getNode(), + `Couldn't find the field '${fieldToSplitOut}' in the input data`, + { + description: + "If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options", + }, + ); + } else { + throw new NodeOperationError( + this.getNode(), + `Couldn't find the field '${fieldToSplitOut}' in the input data`, + { itemIndex: i }, + ); + } + } else { + arrayToSplit = []; + } + } + + if (typeof arrayToSplit !== 'object' || arrayToSplit === null) { + if (nodeVersion < 2.2) { + throw new NodeOperationError( + this.getNode(), + `The provided field '${fieldToSplitOut}' is not an array or object`, + { itemIndex: i }, + ); + } else { + arrayToSplit = [arrayToSplit]; + } + } + + if (!Array.isArray(arrayToSplit)) { + arrayToSplit = Object.values(arrayToSplit); + } + + for (const [elementIndex, element] of arrayToSplit.entries()) { + if (splited[elementIndex] === undefined) { + splited[elementIndex] = {}; + } + + const fieldName = destinationFieldName || fieldToSplitOut; + + if (typeof element === 'object' && element !== null && include === 'noOtherFields') { + if (destinationFieldName === '' && !multiSplit) { + splited[elementIndex] = { ...splited[elementIndex], ...element }; + } else { + splited[elementIndex][fieldName] = element; + } + } else { + splited[elementIndex][fieldName] = element; + } + } } - for (const element of arrayToSplit) { - let newItem = {}; + for (const splitEntry of splited) { + let newItem: IDataObject = {}; + + if (include === 'noOtherFields') { + newItem = splitEntry; + } + + if (include === 'allOtherFields') { + const itemCopy = deepCopy(item); + for (const fieldToSplitOut of fieldsToSplitOut) { + if (!disableDotNotation) { + unset(itemCopy, fieldToSplitOut); + } else { + delete itemCopy[fieldToSplitOut]; + } + } + newItem = { ...itemCopy, ...splitEntry }; + } if (include === 'selectedOtherFields') { const fieldsToInclude = ( @@ -877,51 +939,15 @@ return 0;`, }); } - newItem = { - ...fieldsToInclude.reduce((prev, field) => { - if (field === fieldToSplitOut) { - return prev; - } - let value; - if (!disableDotNotation) { - value = get(items[i].json, field); - } else { - value = items[i].json[field]; - } - prev = { ...prev, [field]: value }; - return prev; - }, {}), - }; - } else if (include === 'allOtherFields') { - const keys = Object.keys(items[i].json); + for (const field of fieldsToInclude) { + if (!disableDotNotation) { + splitEntry[field] = get(item, field); + } else { + splitEntry[field] = item[field]; + } + } - newItem = { - ...keys.reduce((prev, field) => { - let value; - if (!disableDotNotation) { - value = get(items[i].json, field); - } else { - value = items[i].json[field]; - } - prev = { ...prev, [field]: value }; - return prev; - }, {}), - }; - - unset(newItem, fieldToSplitOut); - } - - if ( - typeof element === 'object' && - include === 'noOtherFields' && - destinationFieldName === '' - ) { - newItem = { ...newItem, ...element }; - } else { - newItem = { - ...newItem, - [destinationFieldName || fieldToSplitOut]: element, - }; + newItem = splitEntry; } returnData.push({ diff --git a/packages/nodes-base/nodes/ItemLists/test/node/workflow.update_2_2.json b/packages/nodes-base/nodes/ItemLists/test/node/workflow.update_2_2.json new file mode 100644 index 0000000000..f1c97ee32c --- /dev/null +++ b/packages/nodes-base/nodes/ItemLists/test/node/workflow.update_2_2.json @@ -0,0 +1,361 @@ +{ + "name": "item_list 2.2", + "nodes": [ + { + "parameters": {}, + "id": "f2d01806-a457-4a3a-8bd9-aeb005aecb99", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-80, 820] + }, + { + "parameters": { + "fieldToSplitOut": "data, data2", + "include": "selectedOtherFields", + "fieldsToInclude": { + "fields": [ + { + "fieldName": "tag" + } + ] + }, + "options": {} + }, + "id": "b0dbb504-d111-49ee-a904-1ece920a2e7a", + "name": "Item Lists1", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.2, + "position": [520, 360] + }, + { + "parameters": { + "jsCode": "const data = {\n entry1: {\n id: 1,\n info: 'some info 1',\n },\n entry2: {\n id: 2,\n info: 'some info 2',\n },\n entry3: {\n id: 3,\n info: 'some info 3',\n },\n};\n\n\nconst data2 = [\n 'a', 'b', 'c'\n];\n\nconst data3 = {\n a: 1,\n b: 2,\n c: 3,\n};\n\nreturn {data, data2, data3, data4: null, tag: 'bar'};" + }, + "id": "64fa7b5c-c85c-4dd8-8863-11d6e0ee8426", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [160, 820] + }, + { + "parameters": { + "fieldToSplitOut": "data2, tag", + "include": "selectedOtherFields", + "fieldsToInclude": { + "fields": [ + { + "fieldName": "data4" + } + ] + }, + "options": { + "destinationFieldName": "fromArray, singleField" + } + }, + "id": "80b46f60-19c2-4aec-a232-901b54bdb7c0", + "name": "Item Lists", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.2, + "position": [520, 780] + }, + { + "parameters": { + "fieldToSplitOut": "data3, data2, data", + "options": {} + }, + "id": "c2bbf08c-4571-4d28-a95d-82b0dd22edd9", + "name": "Item Lists2", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.2, + "position": [520, 560] + }, + { + "parameters": { + "fieldToSplitOut": "data2, data3", + "include": "allOtherFields", + "options": {} + }, + "id": "ef10bb23-43e1-4e48-b8b7-c634e4c41b56", + "name": "Item Lists3", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.2, + "position": [520, 1180] + }, + { + "parameters": { + "fieldToSplitOut": " tag, data4", + "options": { + "destinationFieldName": "entry1, entry2" + } + }, + "id": "cd083969-5c27-47f6-9eb2-aa52fc2f5cb2", + "name": "Item Lists4", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.2, + "position": [520, 1400], + "continueOnFail": true + }, + { + "parameters": { + "fieldToSplitOut": "data2", + "include": "selectedOtherFields", + "fieldsToInclude": { + "fields": [ + { + "fieldName": "data4" + } + ] + }, + "options": { + "destinationFieldName": "fromArray" + } + }, + "id": "866154c7-2967-44d7-a278-1600752749d3", + "name": "Item Lists5", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2.2, + "position": [520, 960] + } + ], + "pinData": { + "Item Lists1": [ + { + "json": { + "data": { + "id": 1, + "info": "some info 1" + }, + "data2": "a", + "tag": "bar" + } + }, + { + "json": { + "data": { + "id": 2, + "info": "some info 2" + }, + "data2": "b", + "tag": "bar" + } + }, + { + "json": { + "data": { + "id": 3, + "info": "some info 3" + }, + "data2": "c", + "tag": "bar" + } + } + ], + "Item Lists2": [ + { + "json": { + "data3": 1, + "data2": "a", + "data": { + "id": 1, + "info": "some info 1" + } + } + }, + { + "json": { + "data3": 2, + "data2": "b", + "data": { + "id": 2, + "info": "some info 2" + } + } + }, + { + "json": { + "data3": 3, + "data2": "c", + "data": { + "id": 3, + "info": "some info 3" + } + } + } + ], + "Item Lists": [ + { + "json": { + "fromArray": "a", + "singleField": "bar", + "data4": null + } + }, + { + "json": { + "fromArray": "b", + "data4": null + } + }, + { + "json": { + "fromArray": "c", + "data4": null + } + } + ], + "Item Lists5": [ + { + "json": { + "fromArray": "a", + "data4": null + } + }, + { + "json": { + "fromArray": "b", + "data4": null + } + }, + { + "json": { + "fromArray": "c", + "data4": null + } + } + ], + "Item Lists3": [ + { + "json": { + "data": { + "entry1": { + "id": 1, + "info": "some info 1" + }, + "entry2": { + "id": 2, + "info": "some info 2" + }, + "entry3": { + "id": 3, + "info": "some info 3" + } + }, + "data4": null, + "tag": "bar", + "data2": "a", + "data3": 1 + } + }, + { + "json": { + "data": { + "entry1": { + "id": 1, + "info": "some info 1" + }, + "entry2": { + "id": 2, + "info": "some info 2" + }, + "entry3": { + "id": 3, + "info": "some info 3" + } + }, + "data4": null, + "tag": "bar", + "data2": "b", + "data3": 2 + } + }, + { + "json": { + "data": { + "entry1": { + "id": 1, + "info": "some info 1" + }, + "entry2": { + "id": 2, + "info": "some info 2" + }, + "entry3": { + "id": 3, + "info": "some info 3" + } + }, + "data4": null, + "tag": "bar", + "data2": "c", + "data3": 3 + } + } + ], + "Item Lists4": [ + { + "json": { + "entry1": "bar", + "entry2": null + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code1": { + "main": [ + [ + { + "node": "Item Lists1", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists2", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists3", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists4", + "type": "main", + "index": 0 + }, + { + "node": "Item Lists5", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "1f311937-f825-4bda-a39e-e27c6ffe5906", + "id": "170", + "meta": { + "instanceId": "36203ea1ce3cef713fa25999bd9874ae26b9e4c2c3a90a365f2882a154d031d0" + }, + "tags": [] +}