@fachkraftfreund/n8n-nodes-supabase 1.2.24 → 1.3.0

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.
@@ -135,6 +135,12 @@ class Supabase {
135
135
  description: 'Return existing row matching criteria, or create it if not found',
136
136
  action: 'Find or create row',
137
137
  },
138
+ {
139
+ name: 'Update by Query',
140
+ value: 'updateByQuery',
141
+ description: 'Update all rows matching filter conditions (e.g. update where id IN list)',
142
+ action: 'Update rows by query',
143
+ },
138
144
  ],
139
145
  default: 'read',
140
146
  },
@@ -244,7 +250,7 @@ class Supabase {
244
250
  displayOptions: {
245
251
  show: {
246
252
  resource: ['database'],
247
- operation: ['create', 'read', 'update', 'delete', 'upsert'],
253
+ operation: ['create', 'read', 'update', 'delete', 'upsert', 'updateByQuery'],
248
254
  },
249
255
  },
250
256
  },
@@ -261,7 +267,7 @@ class Supabase {
261
267
  displayOptions: {
262
268
  show: {
263
269
  resource: ['database'],
264
- operation: ['create', 'read', 'update', 'delete', 'upsert', 'findOrCreate'],
270
+ operation: ['create', 'read', 'update', 'delete', 'upsert', 'findOrCreate', 'updateByQuery'],
265
271
  },
266
272
  },
267
273
  },
@@ -277,7 +283,7 @@ class Supabase {
277
283
  displayOptions: {
278
284
  show: {
279
285
  resource: ['database'],
280
- operation: ['create', 'update', 'upsert'],
286
+ operation: ['create', 'update', 'upsert', 'updateByQuery'],
281
287
  uiMode: ['simple'],
282
288
  },
283
289
  },
@@ -316,7 +322,7 @@ class Supabase {
316
322
  displayOptions: {
317
323
  show: {
318
324
  resource: ['database'],
319
- operation: ['create', 'update', 'upsert'],
325
+ operation: ['create', 'update', 'upsert', 'updateByQuery'],
320
326
  uiMode: ['advanced'],
321
327
  },
322
328
  },
@@ -448,7 +454,7 @@ class Supabase {
448
454
  displayOptions: {
449
455
  show: {
450
456
  resource: ['database'],
451
- operation: ['read', 'delete'],
457
+ operation: ['read', 'delete', 'updateByQuery'],
452
458
  uiMode: ['simple'],
453
459
  },
454
460
  },
@@ -508,7 +514,7 @@ class Supabase {
508
514
  displayOptions: {
509
515
  show: {
510
516
  resource: ['database'],
511
- operation: ['read', 'delete'],
517
+ operation: ['read', 'delete', 'updateByQuery'],
512
518
  uiMode: ['advanced'],
513
519
  },
514
520
  },
@@ -0,0 +1,11 @@
1
+ import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class SupabaseCsvExport implements INodeType {
3
+ description: INodeTypeDescription;
4
+ methods: {
5
+ loadOptions: {
6
+ getTables(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
7
+ getColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
8
+ };
9
+ };
10
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
11
+ }
@@ -0,0 +1,638 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SupabaseCsvExport = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const supabaseClient_1 = require("./utils/supabaseClient");
6
+ const supabaseClient_2 = require("./utils/supabaseClient");
7
+ function escapeCsvField(value, delimiter, quoteChar) {
8
+ if (value === null || value === undefined)
9
+ return '';
10
+ const str = typeof value === 'object' ? JSON.stringify(value) : String(value);
11
+ if (str.includes(delimiter) ||
12
+ str.includes(quoteChar) ||
13
+ str.includes('\n') ||
14
+ str.includes('\r')) {
15
+ return quoteChar + str.replace(new RegExp(escapeRegExp(quoteChar), 'g'), quoteChar + quoteChar) + quoteChar;
16
+ }
17
+ return str;
18
+ }
19
+ function escapeRegExp(s) {
20
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
21
+ }
22
+ function generateCsv(rows, options) {
23
+ if (rows.length === 0)
24
+ return '';
25
+ const { delimiter, quoteChar, includeHeaders } = options;
26
+ const headerSet = new Set();
27
+ for (const row of rows) {
28
+ for (const key of Object.keys(row))
29
+ headerSet.add(key);
30
+ }
31
+ const headers = [...headerSet];
32
+ const lines = [];
33
+ if (includeHeaders) {
34
+ lines.push(headers.map((h) => escapeCsvField(h, delimiter, quoteChar)).join(delimiter));
35
+ }
36
+ for (const row of rows) {
37
+ lines.push(headers.map((h) => escapeCsvField(row[h], delimiter, quoteChar)).join(delimiter));
38
+ }
39
+ return lines.join('\n');
40
+ }
41
+ function buildSelectQuery(supabase, table, selectFields, filters, sort) {
42
+ let query = supabase.from(table).select(selectFields);
43
+ for (const filter of filters) {
44
+ const operator = (0, supabaseClient_2.convertFilterOperator)(filter.operator);
45
+ query = query.filter(filter.column, operator, (0, supabaseClient_2.normalizeFilterValue)(filter.operator, filter.value));
46
+ }
47
+ for (const s of sort) {
48
+ query = query.order(s.column, { ascending: s.ascending });
49
+ }
50
+ return query;
51
+ }
52
+ function parseFilters(context, itemIndex) {
53
+ const uiMode = context.getNodeParameter('uiMode', itemIndex, 'simple');
54
+ if (uiMode === 'simple') {
55
+ return context.getNodeParameter('filters.filter', itemIndex, []);
56
+ }
57
+ const raw = context.getNodeParameter('advancedFilters', itemIndex, '');
58
+ if (!raw)
59
+ return [];
60
+ try {
61
+ const parsed = JSON.parse(raw);
62
+ const filters = [];
63
+ for (const [column, condition] of Object.entries(parsed)) {
64
+ if (typeof condition === 'object' && condition !== null) {
65
+ const [operator, value] = Object.entries(condition)[0];
66
+ filters.push({ column, operator: operator, value: value });
67
+ }
68
+ else {
69
+ filters.push({ column, operator: 'eq', value: condition });
70
+ }
71
+ }
72
+ return filters;
73
+ }
74
+ catch {
75
+ throw new Error('Invalid advanced filters JSON');
76
+ }
77
+ }
78
+ async function fetchAllRows(supabase, table, selectFields, filters, sort, hostUrl, returnAll, limit) {
79
+ const overhead = (0, supabaseClient_2.estimateUrlOverhead)(hostUrl, table, selectFields, filters, sort);
80
+ const maxInChars = Math.max(500, supabaseClient_2.MAX_SAFE_URL_LENGTH - overhead);
81
+ const maxItems = (0, supabaseClient_2.computeMaxIdsPerChunk)(selectFields);
82
+ const filterChunks = (0, supabaseClient_2.expandChunkedFilters)(filters, maxInChars, maxItems);
83
+ const allRows = [];
84
+ if (returnAll) {
85
+ const batchSize = 1000;
86
+ const hasIdColumn = selectFields === '*' || selectFields.split(',').some((f) => f.trim() === 'id');
87
+ for (const chunkFilters of filterChunks) {
88
+ let hasMore = true;
89
+ if (hasIdColumn) {
90
+ let lastId = null;
91
+ while (hasMore) {
92
+ let query = buildSelectQuery(supabase, table, selectFields, chunkFilters, []);
93
+ if (lastId !== null) {
94
+ query = query.gt('id', lastId);
95
+ }
96
+ query = query.order('id', { ascending: true }).limit(batchSize);
97
+ const { data, error } = await query;
98
+ if (error)
99
+ throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
100
+ if (Array.isArray(data) && data.length > 0) {
101
+ for (const row of data)
102
+ allRows.push(row);
103
+ lastId = data[data.length - 1].id;
104
+ hasMore = data.length === batchSize;
105
+ }
106
+ else {
107
+ hasMore = false;
108
+ }
109
+ }
110
+ }
111
+ else {
112
+ let offset = 0;
113
+ while (hasMore) {
114
+ const query = buildSelectQuery(supabase, table, selectFields, chunkFilters, sort);
115
+ const { data, error } = await query.range(offset, offset + batchSize - 1);
116
+ if (error)
117
+ throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
118
+ if (Array.isArray(data) && data.length > 0) {
119
+ for (const row of data)
120
+ allRows.push(row);
121
+ hasMore = data.length === batchSize;
122
+ }
123
+ else {
124
+ hasMore = false;
125
+ }
126
+ offset += batchSize;
127
+ }
128
+ }
129
+ }
130
+ if (hasIdColumn && sort.length > 0) {
131
+ allRows.sort((a, b) => {
132
+ var _a, _b;
133
+ for (const s of sort) {
134
+ const aVal = ((_a = a[s.column]) !== null && _a !== void 0 ? _a : null);
135
+ const bVal = ((_b = b[s.column]) !== null && _b !== void 0 ? _b : null);
136
+ if (aVal === bVal)
137
+ continue;
138
+ if (aVal === null)
139
+ return 1;
140
+ if (bVal === null)
141
+ return -1;
142
+ if (aVal < bVal)
143
+ return s.ascending ? -1 : 1;
144
+ if (aVal > bVal)
145
+ return s.ascending ? 1 : -1;
146
+ }
147
+ return 0;
148
+ });
149
+ }
150
+ }
151
+ else {
152
+ for (const chunkFilters of filterChunks) {
153
+ const query = buildSelectQuery(supabase, table, selectFields, chunkFilters, sort);
154
+ const { data, error } = await query.limit(limit);
155
+ if (error)
156
+ throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
157
+ if (Array.isArray(data)) {
158
+ for (const row of data)
159
+ allRows.push(row);
160
+ }
161
+ }
162
+ if (allRows.length > limit) {
163
+ allRows.length = limit;
164
+ }
165
+ }
166
+ return allRows;
167
+ }
168
+ class SupabaseCsvExport {
169
+ constructor() {
170
+ this.description = {
171
+ displayName: 'Supabase CSV Export',
172
+ name: 'supabaseCsvExport',
173
+ icon: 'file:icons/supabase.svg',
174
+ group: ['output'],
175
+ version: 1,
176
+ subtitle: '={{$parameter["table"]}}',
177
+ description: 'Fetch data from Supabase, transform with JavaScript, and export as CSV file',
178
+ defaults: {
179
+ name: 'Supabase CSV Export',
180
+ },
181
+ inputs: ['main'],
182
+ outputs: ['main'],
183
+ credentials: [
184
+ {
185
+ name: 'supabaseExtendedApi',
186
+ required: true,
187
+ },
188
+ ],
189
+ properties: [
190
+ {
191
+ displayName: 'Table',
192
+ name: 'table',
193
+ type: 'options',
194
+ typeOptions: {
195
+ loadOptionsMethod: 'getTables',
196
+ },
197
+ required: true,
198
+ default: '',
199
+ description: 'Table to export data from',
200
+ },
201
+ {
202
+ displayName: 'Return Fields',
203
+ name: 'returnFields',
204
+ type: 'string',
205
+ default: '*',
206
+ placeholder: 'id,name,email',
207
+ description: 'Comma-separated list of columns to fetch (* for all)',
208
+ },
209
+ {
210
+ displayName: 'Joins',
211
+ name: 'joins',
212
+ type: 'fixedCollection',
213
+ typeOptions: { multipleValues: true },
214
+ default: {},
215
+ placeholder: 'Add Join',
216
+ description: 'Join related tables via foreign keys (PostgREST resource embedding)',
217
+ options: [
218
+ {
219
+ displayName: 'Join',
220
+ name: 'join',
221
+ values: [
222
+ {
223
+ displayName: 'Table',
224
+ name: 'table',
225
+ type: 'string',
226
+ default: '',
227
+ placeholder: 'related_table',
228
+ description: 'Related table to join',
229
+ },
230
+ {
231
+ displayName: 'Columns',
232
+ name: 'columns',
233
+ type: 'string',
234
+ default: '*',
235
+ placeholder: 'col1,col2',
236
+ description: 'Comma-separated columns from the joined table (* for all)',
237
+ },
238
+ {
239
+ displayName: 'Join Type',
240
+ name: 'joinType',
241
+ type: 'options',
242
+ options: [
243
+ { name: 'Left Join', value: 'left' },
244
+ { name: 'Inner Join', value: 'inner' },
245
+ ],
246
+ default: 'left',
247
+ },
248
+ ],
249
+ },
250
+ ],
251
+ },
252
+ {
253
+ displayName: 'UI Mode',
254
+ name: 'uiMode',
255
+ type: 'options',
256
+ options: [
257
+ { name: 'Simple', value: 'simple', description: 'Use form fields' },
258
+ { name: 'Advanced', value: 'advanced', description: 'Use JSON filters' },
259
+ ],
260
+ default: 'simple',
261
+ },
262
+ {
263
+ displayName: 'Filters',
264
+ name: 'filters',
265
+ type: 'fixedCollection',
266
+ typeOptions: { multipleValues: true },
267
+ default: {},
268
+ placeholder: 'Add Filter',
269
+ displayOptions: { show: { uiMode: ['simple'] } },
270
+ options: [
271
+ {
272
+ displayName: 'Filter',
273
+ name: 'filter',
274
+ values: [
275
+ {
276
+ displayName: 'Column',
277
+ name: 'column',
278
+ type: 'options',
279
+ typeOptions: { loadOptionsMethod: 'getColumns' },
280
+ default: '',
281
+ },
282
+ {
283
+ displayName: 'Operator',
284
+ name: 'operator',
285
+ type: 'options',
286
+ options: [
287
+ { name: 'Equals', value: 'eq' },
288
+ { name: 'Not Equals', value: 'neq' },
289
+ { name: 'Greater Than', value: 'gt' },
290
+ { name: 'Greater Than or Equal', value: 'gte' },
291
+ { name: 'Less Than', value: 'lt' },
292
+ { name: 'Less Than or Equal', value: 'lte' },
293
+ { name: 'Like', value: 'like' },
294
+ { name: 'Case Insensitive Like', value: 'ilike' },
295
+ { name: 'Is', value: 'is' },
296
+ { name: 'In', value: 'in' },
297
+ { name: 'Contains', value: 'cs' },
298
+ { name: 'Contained By', value: 'cd' },
299
+ ],
300
+ default: 'eq',
301
+ },
302
+ {
303
+ displayName: 'Value',
304
+ name: 'value',
305
+ type: 'string',
306
+ default: '',
307
+ },
308
+ ],
309
+ },
310
+ ],
311
+ },
312
+ {
313
+ displayName: 'Advanced Filters',
314
+ name: 'advancedFilters',
315
+ type: 'json',
316
+ default: '{}',
317
+ description: 'JSON object with filter conditions',
318
+ displayOptions: { show: { uiMode: ['advanced'] } },
319
+ },
320
+ {
321
+ displayName: 'Sort',
322
+ name: 'sort',
323
+ type: 'fixedCollection',
324
+ typeOptions: { multipleValues: true },
325
+ default: {},
326
+ placeholder: 'Add Sort Field',
327
+ options: [
328
+ {
329
+ displayName: 'Sort Field',
330
+ name: 'sortField',
331
+ values: [
332
+ {
333
+ displayName: 'Column',
334
+ name: 'column',
335
+ type: 'options',
336
+ typeOptions: { loadOptionsMethod: 'getColumns' },
337
+ default: '',
338
+ },
339
+ {
340
+ displayName: 'Ascending',
341
+ name: 'ascending',
342
+ type: 'boolean',
343
+ default: true,
344
+ },
345
+ ],
346
+ },
347
+ ],
348
+ },
349
+ {
350
+ displayName: 'Return All',
351
+ name: 'returnAll',
352
+ type: 'boolean',
353
+ default: true,
354
+ description: 'Whether to fetch all matching rows',
355
+ },
356
+ {
357
+ displayName: 'Limit',
358
+ name: 'limit',
359
+ type: 'number',
360
+ default: 100,
361
+ description: 'Maximum number of rows to fetch',
362
+ displayOptions: { show: { returnAll: [false] } },
363
+ },
364
+ {
365
+ displayName: 'Transform Data',
366
+ name: 'enableTransform',
367
+ type: 'boolean',
368
+ default: false,
369
+ description: 'Whether to apply a JavaScript transform before generating the CSV',
370
+ },
371
+ {
372
+ displayName: 'Transform Parameters',
373
+ name: 'transformParams',
374
+ type: 'fixedCollection',
375
+ typeOptions: { multipleValues: true },
376
+ default: {},
377
+ placeholder: 'Add Parameter',
378
+ displayOptions: { show: { enableTransform: [true] } },
379
+ description: 'Key-value pairs passed into the transform code as a "params" object. ' +
380
+ 'Values support n8n expressions, so you can reference previous nodes ' +
381
+ '(e.g. {{ $json.threshold }} or {{ $(\'My Node\').item.json.value }}).',
382
+ options: [
383
+ {
384
+ displayName: 'Parameter',
385
+ name: 'param',
386
+ values: [
387
+ {
388
+ displayName: 'Name',
389
+ name: 'name',
390
+ type: 'string',
391
+ default: '',
392
+ placeholder: 'threshold',
393
+ description: 'Name used in transform code as params.<name>',
394
+ },
395
+ {
396
+ displayName: 'Value',
397
+ name: 'value',
398
+ type: 'string',
399
+ default: '',
400
+ placeholder: '={{ $json.minScore }}',
401
+ description: 'Value (supports n8n expressions to reference previous nodes)',
402
+ },
403
+ ],
404
+ },
405
+ ],
406
+ },
407
+ {
408
+ displayName: 'Transform Code',
409
+ name: 'transformCode',
410
+ type: 'string',
411
+ typeOptions: {
412
+ editor: 'codeNodeEditor',
413
+ editorLanguage: 'javaScript',
414
+ },
415
+ displayOptions: { show: { enableTransform: [true] } },
416
+ default: '// "rows" — array of objects from the database\n' +
417
+ '// "params" — values passed in from Transform Parameters above\n' +
418
+ '// (use n8n expressions there to reference previous nodes)\n' +
419
+ '\n' +
420
+ 'return rows\n' +
421
+ ' .filter(row => row.score >= (params.threshold || 0))\n' +
422
+ ' .map(row => ({\n' +
423
+ ' id: row.id,\n' +
424
+ ' name: row.name,\n' +
425
+ ' email: row.email,\n' +
426
+ ' }));\n',
427
+ description: 'JavaScript code that receives "rows" (array) and "params" (object). ' +
428
+ 'Must return an array of objects. Each object becomes a CSV row.',
429
+ },
430
+ {
431
+ displayName: 'ID Column',
432
+ name: 'idColumn',
433
+ type: 'string',
434
+ default: 'id',
435
+ description: 'Column to extract row IDs from (returned alongside the CSV). ' +
436
+ 'Uses the final (post-transform) data — make sure your transform preserves this column.',
437
+ },
438
+ {
439
+ displayName: 'CSV Options',
440
+ name: 'csvOptions',
441
+ type: 'collection',
442
+ placeholder: 'Add Option',
443
+ default: {},
444
+ options: [
445
+ {
446
+ displayName: 'Delimiter',
447
+ name: 'delimiter',
448
+ type: 'string',
449
+ default: ',',
450
+ description: 'Column separator character',
451
+ },
452
+ {
453
+ displayName: 'Include Headers',
454
+ name: 'includeHeaders',
455
+ type: 'boolean',
456
+ default: true,
457
+ description: 'Whether to include a header row',
458
+ },
459
+ {
460
+ displayName: 'File Name',
461
+ name: 'fileName',
462
+ type: 'string',
463
+ default: 'export.csv',
464
+ description: 'Name of the generated CSV file',
465
+ },
466
+ {
467
+ displayName: 'Quote Character',
468
+ name: 'quoteChar',
469
+ type: 'string',
470
+ default: '"',
471
+ description: 'Character used to quote fields containing special characters',
472
+ },
473
+ ],
474
+ },
475
+ ],
476
+ };
477
+ this.methods = {
478
+ loadOptions: {
479
+ async getTables() {
480
+ const credentials = (await this.getCredentials('supabaseExtendedApi'));
481
+ (0, supabaseClient_1.validateCredentials)(credentials);
482
+ const host = credentials.host.replace(/\/$/, '');
483
+ try {
484
+ const response = await this.helpers.request({
485
+ method: 'GET',
486
+ url: `${host}/rest/v1/`,
487
+ headers: {
488
+ apikey: credentials.serviceKey,
489
+ Authorization: `Bearer ${credentials.serviceKey}`,
490
+ },
491
+ json: true,
492
+ });
493
+ const definitions = response.definitions || {};
494
+ const tables = Object.keys(definitions).sort();
495
+ if (tables.length === 0) {
496
+ return [
497
+ {
498
+ name: 'No tables found',
499
+ value: '',
500
+ description: 'No tables are exposed via the REST API',
501
+ },
502
+ ];
503
+ }
504
+ return tables.map((t) => ({ name: t, value: t }));
505
+ }
506
+ catch (error) {
507
+ const msg = error instanceof Error ? error.message : 'Unknown error';
508
+ return [{ name: `Error: ${msg}`, value: '' }];
509
+ }
510
+ },
511
+ async getColumns() {
512
+ var _a;
513
+ const credentials = (await this.getCredentials('supabaseExtendedApi'));
514
+ (0, supabaseClient_1.validateCredentials)(credentials);
515
+ const table = this.getCurrentNodeParameter('table');
516
+ if (!table) {
517
+ return [{ name: 'Select a table first', value: '' }];
518
+ }
519
+ const host = credentials.host.replace(/\/$/, '');
520
+ try {
521
+ const response = await this.helpers.request({
522
+ method: 'GET',
523
+ url: `${host}/rest/v1/`,
524
+ headers: {
525
+ apikey: credentials.serviceKey,
526
+ Authorization: `Bearer ${credentials.serviceKey}`,
527
+ },
528
+ json: true,
529
+ });
530
+ const tableSchema = (_a = response.definitions) === null || _a === void 0 ? void 0 : _a[table];
531
+ if (!(tableSchema === null || tableSchema === void 0 ? void 0 : tableSchema.properties)) {
532
+ return [{ name: 'No columns found', value: '' }];
533
+ }
534
+ return Object.keys(tableSchema.properties)
535
+ .sort()
536
+ .map((col) => {
537
+ const def = tableSchema.properties[col];
538
+ const typeLabel = def.format ? `${def.type} (${def.format})` : def.type;
539
+ return { name: col, value: col, description: typeLabel };
540
+ });
541
+ }
542
+ catch (error) {
543
+ const msg = error instanceof Error ? error.message : 'Unknown error';
544
+ return [{ name: `Error: ${msg}`, value: '' }];
545
+ }
546
+ },
547
+ },
548
+ };
549
+ }
550
+ async execute() {
551
+ const credentials = (await this.getCredentials('supabaseExtendedApi'));
552
+ try {
553
+ (0, supabaseClient_1.validateCredentials)(credentials);
554
+ }
555
+ catch (error) {
556
+ const msg = error instanceof Error ? error.message : 'Unknown error';
557
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid credentials: ${msg}`);
558
+ }
559
+ const supabase = (0, supabaseClient_1.createSupabaseClient)(credentials);
560
+ const table = this.getNodeParameter('table', 0);
561
+ (0, supabaseClient_2.validateTableName)(table);
562
+ const returnFields = this.getNodeParameter('returnFields', 0, '*');
563
+ const returnAll = this.getNodeParameter('returnAll', 0, true);
564
+ const limit = returnAll ? 0 : this.getNodeParameter('limit', 0, 100);
565
+ const filters = parseFilters(this, 0);
566
+ const sort = this.getNodeParameter('sort.sortField', 0, []);
567
+ const joins = this.getNodeParameter('joins.join', 0, []);
568
+ let selectWithJoins = returnFields;
569
+ for (const j of joins) {
570
+ if (!j.table)
571
+ continue;
572
+ const cols = j.columns || '*';
573
+ const hint = j.joinType === 'inner' ? `${j.table}!inner` : j.table;
574
+ selectWithJoins += `,${hint}(${cols})`;
575
+ }
576
+ let rows;
577
+ try {
578
+ rows = await fetchAllRows(supabase, table, selectWithJoins, filters, sort, credentials.host, returnAll, limit);
579
+ }
580
+ catch (error) {
581
+ const msg = error instanceof Error ? error.message : 'Unknown error';
582
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to fetch data: ${msg}`);
583
+ }
584
+ const enableTransform = this.getNodeParameter('enableTransform', 0, false);
585
+ if (enableTransform) {
586
+ const paramEntries = this.getNodeParameter('transformParams.param', 0, []);
587
+ const params = {};
588
+ for (const entry of paramEntries) {
589
+ if (entry.name) {
590
+ params[entry.name] = entry.value;
591
+ }
592
+ }
593
+ const code = this.getNodeParameter('transformCode', 0, 'return rows;');
594
+ try {
595
+ const transformFn = new Function('rows', 'params', code);
596
+ const result = transformFn(rows, params);
597
+ if (!Array.isArray(result)) {
598
+ throw new Error('Transform code must return an array. Got: ' + typeof result);
599
+ }
600
+ rows = result;
601
+ }
602
+ catch (error) {
603
+ const msg = error instanceof Error ? error.message : 'Unknown error';
604
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Transform code error: ${msg}`);
605
+ }
606
+ }
607
+ const idColumn = this.getNodeParameter('idColumn', 0, 'id');
608
+ const ids = [];
609
+ for (const row of rows) {
610
+ if (row[idColumn] !== undefined && row[idColumn] !== null) {
611
+ ids.push(row[idColumn]);
612
+ }
613
+ }
614
+ const csvOpts = this.getNodeParameter('csvOptions', 0, {});
615
+ const csvOptions = {
616
+ delimiter: csvOpts.delimiter || ',',
617
+ quoteChar: csvOpts.quoteChar || '"',
618
+ includeHeaders: csvOpts.includeHeaders !== false,
619
+ fileName: csvOpts.fileName || 'export.csv',
620
+ };
621
+ const csvContent = generateCsv(rows, csvOptions);
622
+ const csvBuffer = Buffer.from(csvContent, 'utf-8');
623
+ const binaryData = await this.helpers.prepareBinaryData(csvBuffer, csvOptions.fileName, 'text/csv');
624
+ const returnItem = {
625
+ json: {
626
+ table,
627
+ rowCount: rows.length,
628
+ ids,
629
+ fileName: csvOptions.fileName,
630
+ },
631
+ binary: {
632
+ data: binaryData,
633
+ },
634
+ };
635
+ return [[returnItem]];
636
+ }
637
+ }
638
+ exports.SupabaseCsvExport = SupabaseCsvExport;
@@ -39,6 +39,9 @@ async function executeDatabaseOperation(supabase, operation, itemIndex, hostUrl)
39
39
  case 'findOrCreate':
40
40
  returnData.push(...await handleFindOrCreate.call(this, supabase, itemIndex));
41
41
  break;
42
+ case 'updateByQuery':
43
+ returnData.push(...await handleUpdateByQuery.call(this, supabase, itemIndex, hostUrl));
44
+ break;
42
45
  default:
43
46
  throw new Error(`Unknown database operation: ${operation}`);
44
47
  }
@@ -49,6 +52,44 @@ async function executeDatabaseOperation(supabase, operation, itemIndex, hostUrl)
49
52
  return returnData;
50
53
  }
51
54
  exports.executeDatabaseOperation = executeDatabaseOperation;
55
+ const BULK_BATCH_SIZE = 500;
56
+ const MAX_RETRIES = 3;
57
+ const RETRY_BASE_DELAY_MS = 1000;
58
+ function isRetryableError(msg) {
59
+ const lower = msg.toLowerCase();
60
+ return (lower.includes('lock timeout') ||
61
+ lower.includes('canceling statement due to lock') ||
62
+ lower.includes('deadlock') ||
63
+ lower.includes('too many connections') ||
64
+ lower.includes('rate limit') ||
65
+ lower.includes('could not serialize access') ||
66
+ lower.includes('connection terminated') ||
67
+ lower.includes('connection reset') ||
68
+ lower.includes('econnreset') ||
69
+ lower.includes('timeout'));
70
+ }
71
+ async function withRetry(fn, label) {
72
+ var _a;
73
+ let lastError;
74
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
75
+ try {
76
+ return await fn();
77
+ }
78
+ catch (err) {
79
+ const msg = (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err);
80
+ if (attempt < MAX_RETRIES && isRetryableError(msg)) {
81
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
82
+ console.log(`[Supabase ${label}] transient error (attempt ${attempt + 1}/${MAX_RETRIES + 1}), retrying in ${delay}ms: ${msg}`);
83
+ await new Promise((r) => setTimeout(r, delay));
84
+ lastError = err instanceof Error ? err : new Error(msg);
85
+ }
86
+ else {
87
+ throw err;
88
+ }
89
+ }
90
+ }
91
+ throw lastError;
92
+ }
52
93
  function sanitizeString(value) {
53
94
  return value
54
95
  .replace(/\x00/g, '')
@@ -126,16 +167,26 @@ async function handleBulkCreate(supabase, itemCount) {
126
167
  const table = this.getNodeParameter('table', 0);
127
168
  (0, supabaseClient_1.validateTableName)(table);
128
169
  const rows = collectRowData(this, itemCount);
129
- const { data, error } = await supabase
130
- .from(table)
131
- .insert(rows)
132
- .select();
133
- if (error)
134
- throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
135
- if (Array.isArray(data)) {
136
- return data.map((row) => ({ json: row }));
170
+ const returnData = [];
171
+ for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
172
+ const batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
173
+ const data = await withRetry(async () => {
174
+ const { data, error } = await supabase
175
+ .from(table)
176
+ .insert(batch)
177
+ .select();
178
+ if (error)
179
+ throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
180
+ return data;
181
+ }, `CREATE ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`);
182
+ if (Array.isArray(data)) {
183
+ for (const row of data)
184
+ returnData.push({ json: row });
185
+ }
137
186
  }
138
- return [{ json: { data, operation: 'create', table } }];
187
+ return returnData.length > 0
188
+ ? returnData
189
+ : [{ json: { data: [], operation: 'create', table } }];
139
190
  }
140
191
  async function handleBulkUpsert(supabase, itemCount) {
141
192
  const table = this.getNodeParameter('table', 0);
@@ -145,16 +196,26 @@ async function handleBulkUpsert(supabase, itemCount) {
145
196
  const options = {};
146
197
  if (onConflict)
147
198
  options.onConflict = onConflict;
148
- const { data, error } = await supabase
149
- .from(table)
150
- .upsert(rows, options)
151
- .select();
152
- if (error)
153
- throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
154
- if (Array.isArray(data)) {
155
- return data.map((row) => ({ json: row }));
199
+ const returnData = [];
200
+ for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
201
+ const batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
202
+ const data = await withRetry(async () => {
203
+ const { data, error } = await supabase
204
+ .from(table)
205
+ .upsert(batch, options)
206
+ .select();
207
+ if (error)
208
+ throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
209
+ return data;
210
+ }, `UPSERT ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`);
211
+ if (Array.isArray(data)) {
212
+ for (const row of data)
213
+ returnData.push({ json: row });
214
+ }
156
215
  }
157
- return [{ json: { data, operation: 'upsert', table } }];
216
+ return returnData.length > 0
217
+ ? returnData
218
+ : [{ json: { data: [], operation: 'upsert', table } }];
158
219
  }
159
220
  async function handleBulkUpdate(supabase, itemCount) {
160
221
  const table = this.getNodeParameter('table', 0);
@@ -170,16 +231,26 @@ async function handleBulkUpdate(supabase, itemCount) {
170
231
  throw new Error(`Item ${i} is missing the match column "${matchColumn}"`);
171
232
  }
172
233
  }
173
- const { data, error } = await supabase
174
- .from(table)
175
- .upsert(rows, { onConflict: matchColumn })
176
- .select();
177
- if (error)
178
- throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
179
- if (Array.isArray(data)) {
180
- return data.map((row) => ({ json: row }));
234
+ const returnData = [];
235
+ for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
236
+ const batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
237
+ const data = await withRetry(async () => {
238
+ const { data, error } = await supabase
239
+ .from(table)
240
+ .upsert(batch, { onConflict: matchColumn })
241
+ .select();
242
+ if (error)
243
+ throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
244
+ return data;
245
+ }, `UPDATE ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`);
246
+ if (Array.isArray(data)) {
247
+ for (const row of data)
248
+ returnData.push({ json: row });
249
+ }
181
250
  }
182
- return [{ json: { data, operation: 'update', table } }];
251
+ return returnData.length > 0
252
+ ? returnData
253
+ : [{ json: { data: [], operation: 'update', table } }];
183
254
  }
184
255
  function getFilters(context, itemIndex) {
185
256
  const uiMode = context.getNodeParameter('uiMode', itemIndex, 'simple');
@@ -594,3 +665,60 @@ async function handleFindOrCreate(supabase, itemIndex) {
594
665
  throw new Error((0, supabaseClient_1.formatSupabaseError)(insertError));
595
666
  return [{ json: { ...created, _found: false, _created: true } }];
596
667
  }
668
+ async function handleUpdateByQuery(supabase, itemIndex, hostUrl) {
669
+ const table = this.getNodeParameter('table', itemIndex);
670
+ (0, supabaseClient_1.validateTableName)(table);
671
+ const uiMode = this.getNodeParameter('uiMode', itemIndex, 'simple');
672
+ let updateData;
673
+ if (uiMode === 'advanced') {
674
+ const jsonData = this.getNodeParameter('jsonData', itemIndex, '{}');
675
+ try {
676
+ updateData = JSON.parse(jsonData);
677
+ }
678
+ catch {
679
+ throw new Error('Invalid JSON data for update values');
680
+ }
681
+ }
682
+ else {
683
+ const columns = this.getNodeParameter('columns.column', itemIndex, []);
684
+ updateData = {};
685
+ for (const column of columns) {
686
+ if (column.name && column.value !== undefined) {
687
+ updateData[column.name] = column.value;
688
+ }
689
+ }
690
+ }
691
+ if (Object.keys(updateData).length === 0) {
692
+ throw new Error('At least one column value is required for update');
693
+ }
694
+ sanitizeRow(updateData);
695
+ const filters = getFilters(this, itemIndex);
696
+ if (filters.length === 0) {
697
+ throw new Error('At least one filter is required for Update by Query to prevent accidental full-table updates');
698
+ }
699
+ const overhead = (0, supabaseClient_1.estimateUrlOverhead)(hostUrl, table, undefined, filters);
700
+ const maxInChars = Math.max(500, supabaseClient_1.MAX_SAFE_URL_LENGTH - overhead);
701
+ const filterChunks = (0, supabaseClient_1.expandChunkedFilters)(filters, maxInChars);
702
+ const returnData = [];
703
+ for (const chunkFilters of filterChunks) {
704
+ const data = await withRetry(async () => {
705
+ let query = supabase.from(table).update(updateData);
706
+ for (const filter of chunkFilters) {
707
+ const operator = (0, supabaseClient_1.convertFilterOperator)(filter.operator);
708
+ query = query.filter(filter.column, operator, (0, supabaseClient_1.normalizeFilterValue)(filter.operator, filter.value));
709
+ }
710
+ const { data, error } = await query.select();
711
+ if (error)
712
+ throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
713
+ return data;
714
+ }, `UPDATE_BY_QUERY ${table}`);
715
+ if (Array.isArray(data)) {
716
+ for (const row of data)
717
+ returnData.push({ json: row });
718
+ }
719
+ }
720
+ if (returnData.length === 0) {
721
+ return [{ json: { data: [], count: 0, operation: 'updateByQuery', table, message: 'No rows matched the filter conditions' } }];
722
+ }
723
+ return returnData;
724
+ }
@@ -75,7 +75,7 @@ export interface ISupabaseStorageResponse<T = any> {
75
75
  statusCode?: string;
76
76
  } | null;
77
77
  }
78
- export type DatabaseOperation = 'create' | 'read' | 'update' | 'delete' | 'upsert' | 'createTable' | 'dropTable' | 'addColumn' | 'dropColumn' | 'createIndex' | 'dropIndex' | 'customQuery' | 'findOrCreate';
78
+ export type DatabaseOperation = 'create' | 'read' | 'update' | 'delete' | 'upsert' | 'createTable' | 'dropTable' | 'addColumn' | 'dropColumn' | 'createIndex' | 'dropIndex' | 'customQuery' | 'findOrCreate' | 'updateByQuery';
79
79
  export type StorageOperation = 'uploadFile' | 'downloadFile' | 'listFiles' | 'deleteFile' | 'moveFile' | 'copyFile' | 'createBucket' | 'deleteBucket' | 'listBuckets' | 'getBucketDetails' | 'getFileInfo' | 'generateSignedUrl';
80
80
  export type SupabaseResource = 'database' | 'storage';
81
81
  export interface ISupabaseNodeParameters {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fachkraftfreund/n8n-nodes-supabase",
3
- "version": "1.2.24",
3
+ "version": "1.3.0",
4
4
  "description": "Comprehensive n8n community node for Supabase with database and storage operations",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
@@ -38,7 +38,8 @@
38
38
  ],
39
39
  "nodes": [
40
40
  "dist/nodes/Supabase/Supabase.node.js",
41
- "dist/nodes/Supabase/SupabaseTrigger.node.js"
41
+ "dist/nodes/Supabase/SupabaseTrigger.node.js",
42
+ "dist/nodes/Supabase/SupabaseCsvExport.node.js"
42
43
  ]
43
44
  },
44
45
  "devDependencies": {