@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.
- package/CHANGELOG.md +7 -0
- package/dist/nodes/lime-crm/resources/data/index.js +4 -0
- package/dist/nodes/lime-crm/resources/data/index.js.map +1 -1
- package/dist/nodes/lime-crm/resources/data/operations/index.d.ts +1 -0
- package/dist/nodes/lime-crm/resources/data/operations/index.js +2 -1
- package/dist/nodes/lime-crm/resources/data/operations/index.js.map +1 -1
- package/dist/nodes/lime-crm/resources/data/operations/iterateCsv.d.ts +9 -0
- package/dist/nodes/lime-crm/resources/data/operations/iterateCsv.js +225 -0
- package/dist/nodes/lime-crm/resources/data/operations/iterateCsv.js.map +1 -0
- package/dist/package.json +3 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/nodes/lime-crm/resources/data/index.ts +4 -0
- package/nodes/lime-crm/resources/data/operations/index.ts +4 -0
- package/nodes/lime-crm/resources/data/operations/iterateCsv.ts +377 -0
- package/package.json +4 -1
|
@@ -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.
|
|
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
|
}
|