diff --git a/packages/nodes-base/nodes/SplitInBatches/SplitInBatches.node.ts b/packages/nodes-base/nodes/SplitInBatches/SplitInBatches.node.ts index c6ec5342e3..15add2e037 100644 --- a/packages/nodes-base/nodes/SplitInBatches/SplitInBatches.node.ts +++ b/packages/nodes-base/nodes/SplitInBatches/SplitInBatches.node.ts @@ -1,160 +1,25 @@ -import type { - IExecuteFunctions, - INodeExecutionData, - INodeType, - INodeTypeDescription, - IPairedItemData, -} from 'n8n-workflow'; -import { deepCopy } from 'n8n-workflow'; +import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'; +import { VersionedNodeType } from 'n8n-workflow'; -export class SplitInBatches implements INodeType { - description: INodeTypeDescription = { - displayName: 'Split In Batches', - name: 'splitInBatches', - icon: 'fa:th-large', - group: ['organization'], - version: 1, - description: 'Split data into batches and iterate over each batch', - defaults: { - name: 'Split In Batches', - color: '#007755', - }, - inputs: ['main'], - // eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong - outputs: ['main', 'main'], - outputNames: ['loop', 'done'], - properties: [ - { - displayName: - 'You may not need this node — n8n nodes automatically run once for each input item. More info', - name: 'splitInBatchesNotice', - type: 'notice', - default: '', - }, - { - displayName: 'Batch Size', - name: 'batchSize', - type: 'number', - typeOptions: { - minValue: 1, - }, - default: 10, - description: 'The number of items to return with each call', - }, - { - displayName: 'Options', - name: 'options', - type: 'collection', - placeholder: 'Add Option', - default: {}, - options: [ - { - displayName: 'Reset', - name: 'reset', - type: 'boolean', - default: false, - description: - 'Whether the node will be reset and so with the current input-data newly initialized', - }, - ], - }, - ], - }; +import { SplitInBatchesV1 } from './v1/SplitInBatchesV1.node'; +import { SplitInBatchesV2 } from './v2/SplitInBatchesV2.node'; - async execute(this: IExecuteFunctions): Promise { - // Get the input data and create a new array so that we can remove - // items without a problem - const items = this.getInputData().slice(); +export class SplitInBatches extends VersionedNodeType { + constructor() { + const baseDescription: INodeTypeBaseDescription = { + displayName: 'Split In Batches', + name: 'splitInBatches', + icon: 'fa:th-large', + group: ['organization'], + description: 'Split data into batches and iterate over each batch', + defaultVersion: 2, + }; - const nodeContext = this.getContext('node'); + const nodeVersions: IVersionedNodeType['nodeVersions'] = { + 1: new SplitInBatchesV1(), + 2: new SplitInBatchesV2(), + }; - const batchSize = this.getNodeParameter('batchSize', 0) as number; - - const returnItems: INodeExecutionData[] = []; - - const options = this.getNodeParameter('options', 0, {}); - - if (nodeContext.items === undefined || options.reset === true) { - // Is the first time the node runs - - const sourceData = this.getInputSourceData(); - - nodeContext.currentRunIndex = 0; - nodeContext.maxRunIndex = Math.ceil(items.length / batchSize); - nodeContext.sourceData = deepCopy(sourceData); - - // Get the items which should be returned - returnItems.push.apply(returnItems, items.splice(0, batchSize)); - - // Save the incoming items to be able to return them for later runs - nodeContext.items = [...items]; - - // Reset processedItems as they get only added starting from the first iteration - nodeContext.processedItems = []; - } else { - // The node has been called before. So return the next batch of items. - nodeContext.currentRunIndex += 1; - returnItems.push.apply( - returnItems, - (nodeContext.items as INodeExecutionData[]).splice(0, batchSize), - ); - - const addSourceOverwrite = (pairedItem: IPairedItemData | number): IPairedItemData => { - if (typeof pairedItem === 'number') { - return { - item: pairedItem, - sourceOverwrite: nodeContext.sourceData, - }; - } - - return { - ...pairedItem, - sourceOverwrite: nodeContext.sourceData, - }; - }; - - function getPairedItemInformation( - item: INodeExecutionData, - ): IPairedItemData | IPairedItemData[] { - if (item.pairedItem === undefined) { - return { - item: 0, - sourceOverwrite: nodeContext.sourceData, - }; - } - - if (Array.isArray(item.pairedItem)) { - return item.pairedItem.map(addSourceOverwrite); - } - - return addSourceOverwrite(item.pairedItem); - } - - const sourceOverwrite = this.getInputSourceData(); - - const newItems = items.map((item, index) => { - return { - ...item, - pairedItem: { - sourceOverwrite, - item: index, - }, - }; - }); - - nodeContext.processedItems = [...nodeContext.processedItems, ...newItems]; - - returnItems.map((item) => { - item.pairedItem = getPairedItemInformation(item); - }); - } - - nodeContext.noItemsLeft = nodeContext.items.length === 0; - - if (returnItems.length === 0) { - return [[], nodeContext.processedItems]; - } - - return [returnItems, []]; + super(nodeVersions, baseDescription); } } diff --git a/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.combineData.json b/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.combineData.json index 2971f99e29..94349deb7f 100644 --- a/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.combineData.json +++ b/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.combineData.json @@ -26,7 +26,7 @@ }, "name": "SplitInBatches1", "type": "n8n-nodes-base.splitInBatches", - "typeVersion": 1, + "typeVersion": 2, "position": [1340, 400], "id": "02d51797-ae62-4fd6-b703-426a4b3fb951" }, diff --git a/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.workflow.json b/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.workflow_v1.json similarity index 100% rename from packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.workflow.json rename to packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.workflow_v1.json diff --git a/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.workflow_v2.json b/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.workflow_v2.json new file mode 100644 index 0000000000..a39dd9f41d --- /dev/null +++ b/packages/nodes-base/nodes/SplitInBatches/test/SplitInBatches.workflow_v2.json @@ -0,0 +1,178 @@ +{ + "name": "Split in Batches Test", + "nodes": [ + { + "parameters": {}, + "id": "86b8149f-b0a0-489c-bb62-e59142988996", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [400, 220] + }, + { + "parameters": { + "batchSize": 1, + "options": {} + }, + "id": "30c5546e-bdcc-44ff-bfca-89c5fb97b678", + "name": "Split In Batches", + "type": "n8n-nodes-base.splitInBatches", + "typeVersion": 2, + "position": [1100, 220] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "data[0]", + "value": "n8n" + }, + { + "name": "data[1]", + "value": "test" + } + ] + }, + "options": {} + }, + "id": "92d386b8-60be-4f8b-801c-b6459ec206f7", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [640, 220] + }, + { + "parameters": { + "fieldToSplitOut": "data", + "options": {} + }, + "id": "74b7e63e-a9f8-4a82-9e1f-7b2429d9118d", + "name": "Item Lists", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 1, + "position": [860, 220] + }, + { + "parameters": { + "conditions": { + "boolean": [ + { + "value1": "={{ $node[\"Split In Batches\"].context[\"noItemsLeft\"] }}", + "value2": true + } + ] + } + }, + "id": "a5f68369-4e70-4f16-b260-3c8b74517993", + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [1280, 220] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "maxRunIndex", + "value": "={{ $node[\"Split In Batches\"].context[\"maxRunIndex\"] }}" + }, + { + "value": "={{ $node[\"Split In Batches\"].context[\"currentRunIndex\"] }}" + } + ] + }, + "options": {} + }, + "id": "1f44eb0a-5fb7-43a7-8281-84a8a7ec8464", + "name": "Output", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [1480, 200] + } + ], + "pinData": { + "Output": [ + { + "json": { + "data": "test", + "maxRunIndex": 2, + "propertyName": 1 + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "Split In Batches": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "Item Lists", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists": { + "main": [ + [ + { + "node": "Split In Batches", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "Output", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Split In Batches", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "de1a454e-43e9-4c2d-b786-18da5d97940f", + "id": "389", + "meta": { + "instanceId": "REMOVED" + }, + "tags": [] +} diff --git a/packages/nodes-base/nodes/SplitInBatches/v1/SplitInBatchesV1.node.ts b/packages/nodes-base/nodes/SplitInBatches/v1/SplitInBatchesV1.node.ts new file mode 100644 index 0000000000..ba7d2c497a --- /dev/null +++ b/packages/nodes-base/nodes/SplitInBatches/v1/SplitInBatchesV1.node.ts @@ -0,0 +1,143 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IPairedItemData, +} from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; + +export class SplitInBatchesV1 implements INodeType { + description: INodeTypeDescription = { + displayName: 'Split In Batches', + name: 'splitInBatches', + icon: 'fa:th-large', + group: ['organization'], + version: 1, + description: 'Split data into batches and iterate over each batch', + defaults: { + name: 'Split In Batches', + color: '#007755', + }, + inputs: ['main'], + outputs: ['main'], + properties: [ + { + displayName: + 'You may not need this node — n8n nodes automatically run once for each input item. More info', + name: 'splitInBatchesNotice', + type: 'notice', + default: '', + }, + { + displayName: 'Batch Size', + name: 'batchSize', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 10, + description: 'The number of items to return with each call', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Reset', + name: 'reset', + type: 'boolean', + default: false, + description: + 'Whether the node will be reset and so with the current input-data newly initialized', + }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + // Get the input data and create a new array so that we can remove + // items without a problem + const items = this.getInputData().slice(); + + const nodeContext = this.getContext('node'); + + const batchSize = this.getNodeParameter('batchSize', 0) as number; + + const returnItems: INodeExecutionData[] = []; + + const options = this.getNodeParameter('options', 0, {}); + + if (nodeContext.items === undefined || options.reset === true) { + // Is the first time the node runs + + const sourceData = this.getInputSourceData(); + + nodeContext.currentRunIndex = 0; + nodeContext.maxRunIndex = Math.ceil(items.length / batchSize); + nodeContext.sourceData = deepCopy(sourceData); + + // Get the items which should be returned + returnItems.push.apply(returnItems, items.splice(0, batchSize)); + + // Set the other items to be saved in the context to return at later runs + nodeContext.items = [...items]; + } else { + // The node has been called before. So return the next batch of items. + nodeContext.currentRunIndex += 1; + returnItems.push.apply( + returnItems, + (nodeContext.items as INodeExecutionData[]).splice(0, batchSize), + ); + + const addSourceOverwrite = (pairedItem: IPairedItemData | number): IPairedItemData => { + if (typeof pairedItem === 'number') { + return { + item: pairedItem, + sourceOverwrite: nodeContext.sourceData, + }; + } + + return { + ...pairedItem, + sourceOverwrite: nodeContext.sourceData, + }; + }; + + function getPairedItemInformation( + item: INodeExecutionData, + ): IPairedItemData | IPairedItemData[] { + if (item.pairedItem === undefined) { + return { + item: 0, + sourceOverwrite: nodeContext.sourceData, + }; + } + + if (Array.isArray(item.pairedItem)) { + return item.pairedItem.map(addSourceOverwrite); + } + + return addSourceOverwrite(item.pairedItem); + } + + returnItems.map((item) => { + item.pairedItem = getPairedItemInformation(item); + }); + } + + nodeContext.noItemsLeft = nodeContext.items.length === 0; + + if (returnItems.length === 0) { + // No data left to return so stop execution of the branch + return null; + } + + return [returnItems]; + } +} diff --git a/packages/nodes-base/nodes/SplitInBatches/v2/SplitInBatchesV2.node.ts b/packages/nodes-base/nodes/SplitInBatches/v2/SplitInBatchesV2.node.ts new file mode 100644 index 0000000000..31252fd483 --- /dev/null +++ b/packages/nodes-base/nodes/SplitInBatches/v2/SplitInBatchesV2.node.ts @@ -0,0 +1,161 @@ +/* eslint-disable n8n-nodes-base/node-filename-against-convention */ +import type { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + IPairedItemData, +} from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; + +export class SplitInBatchesV2 implements INodeType { + description: INodeTypeDescription = { + displayName: 'Split In Batches', + name: 'splitInBatches', + icon: 'fa:th-large', + group: ['organization'], + version: 2, + description: 'Split data into batches and iterate over each batch', + defaults: { + name: 'Split In Batches', + color: '#007755', + }, + inputs: ['main'], + // eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong + outputs: ['main', 'main'], + outputNames: ['loop', 'done'], + properties: [ + { + displayName: + 'You may not need this node — n8n nodes automatically run once for each input item. More info', + name: 'splitInBatchesNotice', + type: 'notice', + default: '', + }, + { + displayName: 'Batch Size', + name: 'batchSize', + type: 'number', + typeOptions: { + minValue: 1, + }, + default: 10, + description: 'The number of items to return with each call', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + options: [ + { + displayName: 'Reset', + name: 'reset', + type: 'boolean', + default: false, + description: + 'Whether the node will be reset and so with the current input-data newly initialized', + }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + // Get the input data and create a new array so that we can remove + // items without a problem + const items = this.getInputData().slice(); + + const nodeContext = this.getContext('node'); + + const batchSize = this.getNodeParameter('batchSize', 0) as number; + + const returnItems: INodeExecutionData[] = []; + + const options = this.getNodeParameter('options', 0, {}); + + if (nodeContext.items === undefined || options.reset === true) { + // Is the first time the node runs + + const sourceData = this.getInputSourceData(); + + nodeContext.currentRunIndex = 0; + nodeContext.maxRunIndex = Math.ceil(items.length / batchSize); + nodeContext.sourceData = deepCopy(sourceData); + + // Get the items which should be returned + returnItems.push.apply(returnItems, items.splice(0, batchSize)); + + // Save the incoming items to be able to return them for later runs + nodeContext.items = [...items]; + + // Reset processedItems as they get only added starting from the first iteration + nodeContext.processedItems = []; + } else { + // The node has been called before. So return the next batch of items. + nodeContext.currentRunIndex += 1; + returnItems.push.apply( + returnItems, + (nodeContext.items as INodeExecutionData[]).splice(0, batchSize), + ); + + const addSourceOverwrite = (pairedItem: IPairedItemData | number): IPairedItemData => { + if (typeof pairedItem === 'number') { + return { + item: pairedItem, + sourceOverwrite: nodeContext.sourceData, + }; + } + + return { + ...pairedItem, + sourceOverwrite: nodeContext.sourceData, + }; + }; + + function getPairedItemInformation( + item: INodeExecutionData, + ): IPairedItemData | IPairedItemData[] { + if (item.pairedItem === undefined) { + return { + item: 0, + sourceOverwrite: nodeContext.sourceData, + }; + } + + if (Array.isArray(item.pairedItem)) { + return item.pairedItem.map(addSourceOverwrite); + } + + return addSourceOverwrite(item.pairedItem); + } + + const sourceOverwrite = this.getInputSourceData(); + + const newItems = items.map((item, index) => { + return { + ...item, + pairedItem: { + sourceOverwrite, + item: index, + }, + }; + }); + + nodeContext.processedItems = [...nodeContext.processedItems, ...newItems]; + + returnItems.map((item) => { + item.pairedItem = getPairedItemInformation(item); + }); + } + + nodeContext.noItemsLeft = nodeContext.items.length === 0; + + if (returnItems.length === 0) { + return [[], nodeContext.processedItems]; + } + + return [returnItems, []]; + } +}