@limetech/n8n-nodes-lime 3.10.1 → 3.11.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,7 @@ const moduleHandler = new N8NOperationModuleHandler([
24
24
  operations.bulkCreateManyObjects,
25
25
  operations.bulkUpdateManyObjects,
26
26
  operations.bulkCreateOrUpdateManyObjects,
27
+ operations.iterateCsv,
27
28
  ]);
28
29
 
29
30
  /**
@@ -140,6 +141,9 @@ export async function dataOperations(
140
141
  );
141
142
  return result && { json: result };
142
143
  }
144
+ case 'iterateCsv': {
145
+ return await operations.iterateCsv.execute.call(this, i);
146
+ }
143
147
  }
144
148
 
145
149
  throw new NodeOperationError(
@@ -34,3 +34,7 @@ export * as bulkUpdateManyObjects from './bulkUpdateManyObjects.operation';
34
34
  * @group Resources
35
35
  */
36
36
  export * as bulkCreateOrUpdateManyObjects from './bulkCreateOrUpdateManyObjects.operation';
37
+ /**
38
+ * @group Resources
39
+ */
40
+ export * as iterateCsv from './iterateCsv';
@@ -0,0 +1,377 @@
1
+ import { parse, type Parser } from 'csv-parse';
2
+ import {
3
+ IDataObject,
4
+ IExecuteFunctions,
5
+ INodeExecutionData,
6
+ INodeParameterResourceLocator,
7
+ INodeProperties,
8
+ NodeOperationError,
9
+ } from 'n8n-workflow';
10
+ import { Readable } from 'node:stream';
11
+ import { DATA_RESOURCE } from '../../../models';
12
+
13
+ export const description = {
14
+ name: 'Process CSV in Batches',
15
+ value: 'iterateCsv',
16
+ description:
17
+ 'Stream a CSV file from a binary field and run a sub-workflow for ' +
18
+ 'every N rows. Only one batch is held in memory at a time, so very ' +
19
+ 'large files can be processed without crashing the instance.',
20
+ action: 'Process a CSV file in batches via a sub-workflow',
21
+ };
22
+
23
+ export const properties: INodeProperties[] = [
24
+ {
25
+ displayName: 'Input Binary Field',
26
+ name: 'binaryPropertyName',
27
+ type: 'string',
28
+ default: 'data',
29
+ required: true,
30
+ description:
31
+ 'Name of the binary field that contains the CSV file. ' +
32
+ 'The file can come from any previous node (HTTP Request, ' +
33
+ 'Read/Write Files from Disk, a form upload, etc.).',
34
+ displayOptions: {
35
+ show: {
36
+ resource: [DATA_RESOURCE],
37
+ operation: ['iterateCsv'],
38
+ },
39
+ },
40
+ },
41
+ {
42
+ displayName: 'Sub-Workflow',
43
+ name: 'workflowId',
44
+ type: 'workflowSelector',
45
+ default: '',
46
+ required: true,
47
+ description:
48
+ 'The sub-workflow to run for each batch. It receives the batch ' +
49
+ 'rows as input items (one item per row).',
50
+ displayOptions: {
51
+ show: {
52
+ resource: [DATA_RESOURCE],
53
+ operation: ['iterateCsv'],
54
+ },
55
+ },
56
+ },
57
+ {
58
+ displayName: 'Batch Size',
59
+ name: 'batchSize',
60
+ type: 'number',
61
+ default: 100,
62
+ required: true,
63
+ typeOptions: {
64
+ minValue: 1,
65
+ },
66
+ description: 'Number of rows to send to the sub-workflow per run',
67
+ displayOptions: {
68
+ show: {
69
+ resource: [DATA_RESOURCE],
70
+ operation: ['iterateCsv'],
71
+ },
72
+ },
73
+ },
74
+ {
75
+ displayName: 'Options',
76
+ name: 'options',
77
+ type: 'collection',
78
+ placeholder: 'Add Option',
79
+ default: {},
80
+ displayOptions: {
81
+ show: {
82
+ resource: [DATA_RESOURCE],
83
+ operation: ['iterateCsv'],
84
+ },
85
+ },
86
+ options: [
87
+ {
88
+ displayName: 'Delimiter',
89
+ name: 'delimiter',
90
+ type: 'string',
91
+ default: ',',
92
+ description: 'Character that separates columns',
93
+ },
94
+ {
95
+ displayName: 'Header Row',
96
+ name: 'header',
97
+ type: 'boolean',
98
+ default: true,
99
+ description:
100
+ 'Whether the first row holds column names. When enabled, ' +
101
+ 'each row is an object keyed by those names; otherwise rows ' +
102
+ 'are positional arrays.',
103
+ },
104
+ {
105
+ displayName: 'Max Rows',
106
+ name: 'maxRows',
107
+ type: 'number',
108
+ default: 0,
109
+ typeOptions: {
110
+ minValue: 0,
111
+ },
112
+ description:
113
+ 'Maximum number of rows to read across all batches. Set to ' +
114
+ '0 to read the whole file.',
115
+ },
116
+ {
117
+ displayName: 'Skip Empty Lines',
118
+ name: 'skipEmptyLines',
119
+ type: 'boolean',
120
+ default: true,
121
+ description:
122
+ 'Whether to ignore empty lines instead of emitting them',
123
+ },
124
+ {
125
+ displayName: 'Trim Values',
126
+ name: 'trim',
127
+ type: 'boolean',
128
+ default: false,
129
+ description:
130
+ 'Whether to strip surrounding whitespace from each value',
131
+ },
132
+ {
133
+ displayName: 'Wait for Sub-Workflow',
134
+ name: 'waitForSubWorkflow',
135
+ type: 'boolean',
136
+ default: true,
137
+ description:
138
+ 'Whether to wait for each batch sub-workflow to finish ' +
139
+ 'before reading the next batch. Keep enabled to bound ' +
140
+ 'memory; disable to fire batches without waiting (faster, ' +
141
+ 'but batches may queue up and grow memory).',
142
+ },
143
+ ],
144
+ },
145
+ ];
146
+
147
+ interface ProcessCsvOptions {
148
+ delimiter?: string;
149
+ header?: boolean;
150
+ maxRows?: number;
151
+ skipEmptyLines?: boolean;
152
+ trim?: boolean;
153
+ waitForSubWorkflow?: boolean;
154
+ }
155
+
156
+ /**
157
+ * Resolve the CSV bytes as a readable stream.
158
+ *
159
+ * Prefers streaming from disk; falls back to the in-memory buffer for the
160
+ * legacy `default` binary-data mode where there is no file id.
161
+ *
162
+ * @param context - The n8n execution context
163
+ * @param i - The index of the current input item
164
+ * @param binaryPropertyName - Name of the binary field holding the CSV
165
+ */
166
+ async function resolveInputStream(
167
+ context: IExecuteFunctions,
168
+ i: number,
169
+ binaryPropertyName: string
170
+ ): Promise<Readable> {
171
+ const binaryData = context.helpers.assertBinaryData(i, binaryPropertyName);
172
+
173
+ if (binaryData.id) {
174
+ return await context.helpers.getBinaryStream(binaryData.id);
175
+ }
176
+
177
+ const buffer = await context.helpers.getBinaryDataBuffer(
178
+ i,
179
+ binaryPropertyName
180
+ );
181
+ return Readable.from(buffer);
182
+ }
183
+
184
+ interface RunBatchesConfig {
185
+ inputStream: Readable;
186
+ parser: Parser;
187
+ workflowId: string;
188
+ batchSize: number;
189
+ maxRows: number;
190
+ header: boolean;
191
+ waitForSubWorkflow: boolean;
192
+ itemIndex: number;
193
+ }
194
+
195
+ async function runCsvBatches(
196
+ context: IExecuteFunctions,
197
+ config: RunBatchesConfig
198
+ ): Promise<INodeExecutionData[]> {
199
+ const {
200
+ inputStream,
201
+ parser,
202
+ workflowId,
203
+ batchSize,
204
+ maxRows,
205
+ header,
206
+ waitForSubWorkflow,
207
+ itemIndex,
208
+ } = config;
209
+
210
+ const summaries: INodeExecutionData[] = [];
211
+ let currentBatch: IDataObject[] = [];
212
+ let rowsRead = 0;
213
+
214
+ const runBatch = async () => {
215
+ if (currentBatch.length === 0) return;
216
+
217
+ const batchIndex = summaries.length;
218
+ const rowCount = currentBatch.length;
219
+ const inputItems: INodeExecutionData[] = currentBatch.map((row) => ({
220
+ json: row,
221
+ pairedItem: { item: itemIndex },
222
+ }));
223
+
224
+ // Release the rows before awaiting so they can be collected while the
225
+ // sub-workflow runs.
226
+ currentBatch = [];
227
+
228
+ const result = await context.executeWorkflow(
229
+ { id: workflowId },
230
+ inputItems,
231
+ undefined,
232
+ { doNotWaitToFinish: !waitForSubWorkflow }
233
+ );
234
+
235
+ summaries.push({
236
+ json: { batchIndex, rowCount, executionId: result.executionId },
237
+ pairedItem: { item: itemIndex },
238
+ });
239
+ };
240
+
241
+ try {
242
+ inputStream.pipe(parser);
243
+
244
+ for await (const record of parser) {
245
+ currentBatch.push(
246
+ header ? (record as IDataObject) : { row: record }
247
+ );
248
+ rowsRead += 1;
249
+
250
+ if (currentBatch.length >= batchSize) {
251
+ await runBatch();
252
+ }
253
+
254
+ if (maxRows > 0 && rowsRead >= maxRows) {
255
+ break;
256
+ }
257
+ }
258
+
259
+ // Process the final, partially filled batch.
260
+ await runBatch();
261
+ } catch (error) {
262
+ if (error instanceof NodeOperationError) throw error;
263
+ throw new NodeOperationError(
264
+ context.getNode(),
265
+ 'Failed to process the CSV file',
266
+ {
267
+ itemIndex,
268
+ description: error.message,
269
+ }
270
+ );
271
+ } finally {
272
+ // Stop reading early (e.g. when Max Rows is hit) and release the
273
+ // underlying file handle / network socket.
274
+ parser.destroy();
275
+ inputStream.destroy();
276
+ }
277
+
278
+ return summaries;
279
+ }
280
+
281
+ /**
282
+ * Execute the "Process CSV in Batches" operation for Lime CRM.
283
+ *
284
+ * @remarks
285
+ * The CSV is read as a stream (from disk via
286
+ * {@link IExecuteFunctions.helpers.getBinaryStream} when the binary field is
287
+ * filesystem/database backed, otherwise from the in-memory buffer). Rows are
288
+ * grouped into batches of `batchSize`; for each full batch the configured
289
+ * sub-workflow is executed with the batch rows as input items, and the batch
290
+ * is then discarded before more rows are read.
291
+ *
292
+ * Because we `await` each sub-workflow run inside the parse loop, the stream is
293
+ * paused (backpressure) while a batch is processed, so at most one batch of
294
+ * rows is ever held in memory. This is what lets large files be processed
295
+ * without exhausting memory — unlike returning all rows/batches as node output.
296
+ *
297
+ * The node itself returns only small per-batch summaries, not the row data.
298
+ *
299
+ * @param i - The index of the current input item being processed
300
+ *
301
+ * @returns One summary {@link INodeExecutionData} per batch
302
+ *
303
+ * @public
304
+ */
305
+ export async function execute(
306
+ this: IExecuteFunctions,
307
+ i: number
308
+ ): Promise<INodeExecutionData[]> {
309
+ const binaryPropertyName = this.getNodeParameter(
310
+ 'binaryPropertyName',
311
+ i,
312
+ 'data'
313
+ ) as string;
314
+ // `workflowSelector` stores a resource-locator object ({ mode, value }).
315
+ // Read the `value` rather than relying on extractValue, matching the
316
+ // built-in Execute Workflow node.
317
+ const workflowIdParam = this.getNodeParameter('workflowId', i, '') as
318
+ | INodeParameterResourceLocator
319
+ | string;
320
+ const workflowId = (
321
+ typeof workflowIdParam === 'object' && workflowIdParam !== null
322
+ ? workflowIdParam.value
323
+ : workflowIdParam
324
+ ) as string;
325
+ const batchSize = this.getNodeParameter('batchSize', i, 100) as number;
326
+ const options = this.getNodeParameter(
327
+ 'options',
328
+ i,
329
+ {}
330
+ ) as ProcessCsvOptions;
331
+
332
+ if (batchSize < 1) {
333
+ throw new NodeOperationError(
334
+ this.getNode(),
335
+ 'Batch Size must be at least 1',
336
+ { itemIndex: i }
337
+ );
338
+ }
339
+
340
+ if (!workflowId) {
341
+ throw new NodeOperationError(
342
+ this.getNode(),
343
+ 'No sub-workflow selected',
344
+ { itemIndex: i }
345
+ );
346
+ }
347
+
348
+ const {
349
+ delimiter = ',',
350
+ header = true,
351
+ maxRows = 0,
352
+ skipEmptyLines = true,
353
+ trim = false,
354
+ waitForSubWorkflow = true,
355
+ } = options;
356
+
357
+ const inputStream = await resolveInputStream(this, i, binaryPropertyName);
358
+ const parser = parse({
359
+ bom: true,
360
+ columns: header,
361
+ delimiter,
362
+ trim,
363
+ skip_empty_lines: skipEmptyLines,
364
+ relax_column_count: true,
365
+ });
366
+
367
+ return await runCsvBatches(this, {
368
+ inputStream,
369
+ parser,
370
+ workflowId,
371
+ batchSize,
372
+ maxRows,
373
+ header,
374
+ waitForSubWorkflow,
375
+ itemIndex: i,
376
+ });
377
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@limetech/n8n-nodes-lime",
3
- "version": "3.10.1",
3
+ "version": "3.11.0-dev.1",
4
4
  "description": "n8n node to connect to Lime CRM",
5
5
  "author": {
6
6
  "name": "Lime Technologies",
@@ -67,5 +67,8 @@
67
67
  },
68
68
  "publishConfig": {
69
69
  "access": "public"
70
+ },
71
+ "dependencies": {
72
+ "csv-parse": "^7.0.0"
70
73
  }
71
74
  }