@fachkraftfreund/n8n-nodes-supabase 1.3.0 → 1.3.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.
@@ -4,6 +4,9 @@ exports.SupabaseCsvExport = void 0;
4
4
  const n8n_workflow_1 = require("n8n-workflow");
5
5
  const supabaseClient_1 = require("./utils/supabaseClient");
6
6
  const supabaseClient_2 = require("./utils/supabaseClient");
7
+ function escapeRegExp(s) {
8
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
+ }
7
10
  function escapeCsvField(value, delimiter, quoteChar) {
8
11
  if (value === null || value === undefined)
9
12
  return '';
@@ -12,31 +15,28 @@ function escapeCsvField(value, delimiter, quoteChar) {
12
15
  str.includes(quoteChar) ||
13
16
  str.includes('\n') ||
14
17
  str.includes('\r')) {
15
- return quoteChar + str.replace(new RegExp(escapeRegExp(quoteChar), 'g'), quoteChar + quoteChar) + quoteChar;
18
+ const escaped = quoteChar + str.replace(new RegExp(escapeRegExp(quoteChar), 'g'), quoteChar + quoteChar) + quoteChar;
19
+ return escaped;
16
20
  }
17
21
  return str;
18
22
  }
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();
23
+ function discoverHeaders(rows) {
24
+ const set = new Set();
27
25
  for (const row of rows) {
28
26
  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));
27
+ set.add(key);
35
28
  }
36
- for (const row of rows) {
37
- lines.push(headers.map((h) => escapeCsvField(row[h], delimiter, quoteChar)).join(delimiter));
29
+ return [...set];
30
+ }
31
+ function batchToCsvBuffer(rows, headers, delimiter, quoteChar) {
32
+ const lines = new Array(rows.length);
33
+ for (let i = 0; i < rows.length; i++) {
34
+ const row = rows[i];
35
+ lines[i] = headers
36
+ .map((h) => escapeCsvField(row[h], delimiter, quoteChar))
37
+ .join(delimiter);
38
38
  }
39
- return lines.join('\n');
39
+ return Buffer.from(lines.join('\n'), 'utf-8');
40
40
  }
41
41
  function buildSelectQuery(supabase, table, selectFields, filters, sort) {
42
42
  let query = supabase.from(table).select(selectFields);
@@ -75,33 +75,36 @@ function parseFilters(context, itemIndex) {
75
75
  throw new Error('Invalid advanced filters JSON');
76
76
  }
77
77
  }
78
- async function fetchAllRows(supabase, table, selectFields, filters, sort, hostUrl, returnAll, limit) {
78
+ const BATCH_SIZE = 1000;
79
+ async function* fetchBatches(supabase, table, selectFields, filters, sort, hostUrl, returnAll, limit, preferOffset) {
79
80
  const overhead = (0, supabaseClient_2.estimateUrlOverhead)(hostUrl, table, selectFields, filters, sort);
80
81
  const maxInChars = Math.max(500, supabaseClient_2.MAX_SAFE_URL_LENGTH - overhead);
81
82
  const maxItems = (0, supabaseClient_2.computeMaxIdsPerChunk)(selectFields);
82
83
  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) {
84
+ let totalYielded = 0;
85
+ const maxRows = returnAll ? Infinity : limit;
86
+ for (const chunkFilters of filterChunks) {
87
+ if (totalYielded >= maxRows)
88
+ break;
89
+ if (returnAll) {
90
+ const useKeyset = !preferOffset &&
91
+ (selectFields === '*' || selectFields.split(',').some((f) => f.trim() === 'id'));
88
92
  let hasMore = true;
89
- if (hasIdColumn) {
93
+ if (useKeyset) {
90
94
  let lastId = null;
91
95
  while (hasMore) {
92
96
  let query = buildSelectQuery(supabase, table, selectFields, chunkFilters, []);
93
- if (lastId !== null) {
97
+ if (lastId !== null)
94
98
  query = query.gt('id', lastId);
95
- }
96
- query = query.order('id', { ascending: true }).limit(batchSize);
99
+ query = query.order('id', { ascending: true }).limit(BATCH_SIZE);
97
100
  const { data, error } = await query;
98
101
  if (error)
99
102
  throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
100
103
  if (Array.isArray(data) && data.length > 0) {
101
- for (const row of data)
102
- allRows.push(row);
104
+ yield data;
105
+ totalYielded += data.length;
103
106
  lastId = data[data.length - 1].id;
104
- hasMore = data.length === batchSize;
107
+ hasMore = data.length === BATCH_SIZE;
105
108
  }
106
109
  else {
107
110
  hasMore = false;
@@ -112,58 +115,35 @@ async function fetchAllRows(supabase, table, selectFields, filters, sort, hostUr
112
115
  let offset = 0;
113
116
  while (hasMore) {
114
117
  const query = buildSelectQuery(supabase, table, selectFields, chunkFilters, sort);
115
- const { data, error } = await query.range(offset, offset + batchSize - 1);
118
+ const { data, error } = await query.range(offset, offset + BATCH_SIZE - 1);
116
119
  if (error)
117
120
  throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
118
121
  if (Array.isArray(data) && data.length > 0) {
119
- for (const row of data)
120
- allRows.push(row);
121
- hasMore = data.length === batchSize;
122
+ yield data;
123
+ totalYielded += data.length;
124
+ hasMore = data.length === BATCH_SIZE;
122
125
  }
123
126
  else {
124
127
  hasMore = false;
125
128
  }
126
- offset += batchSize;
129
+ offset += BATCH_SIZE;
127
130
  }
128
131
  }
129
132
  }
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) {
133
+ else {
134
+ const remaining = maxRows - totalYielded;
135
+ if (remaining <= 0)
136
+ break;
153
137
  const query = buildSelectQuery(supabase, table, selectFields, chunkFilters, sort);
154
- const { data, error } = await query.limit(limit);
138
+ const { data, error } = await query.limit(remaining);
155
139
  if (error)
156
140
  throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
157
- if (Array.isArray(data)) {
158
- for (const row of data)
159
- allRows.push(row);
141
+ if (Array.isArray(data) && data.length > 0) {
142
+ yield data;
143
+ totalYielded += data.length;
160
144
  }
161
145
  }
162
- if (allRows.length > limit) {
163
- allRows.length = limit;
164
- }
165
146
  }
166
- return allRows;
167
147
  }
168
148
  class SupabaseCsvExport {
169
149
  constructor() {
@@ -366,7 +346,9 @@ class SupabaseCsvExport {
366
346
  name: 'enableTransform',
367
347
  type: 'boolean',
368
348
  default: false,
369
- description: 'Whether to apply a JavaScript transform before generating the CSV',
349
+ description: 'Whether to apply a JavaScript transform before generating the CSV. ' +
350
+ 'The transform runs per batch (~1000 rows) so it stays memory-efficient ' +
351
+ 'even for very large exports. Use .filter() and .map() to shape your data.',
370
352
  },
371
353
  {
372
354
  displayName: 'Transform Parameters',
@@ -564,6 +546,15 @@ class SupabaseCsvExport {
564
546
  const limit = returnAll ? 0 : this.getNodeParameter('limit', 0, 100);
565
547
  const filters = parseFilters(this, 0);
566
548
  const sort = this.getNodeParameter('sort.sortField', 0, []);
549
+ const enableTransform = this.getNodeParameter('enableTransform', 0, false);
550
+ const idColumn = this.getNodeParameter('idColumn', 0, 'id');
551
+ const csvOpts = this.getNodeParameter('csvOptions', 0, {});
552
+ const csvOptions = {
553
+ delimiter: csvOpts.delimiter || ',',
554
+ quoteChar: csvOpts.quoteChar || '"',
555
+ includeHeaders: csvOpts.includeHeaders !== false,
556
+ fileName: csvOpts.fileName || 'export.csv',
557
+ };
567
558
  const joins = this.getNodeParameter('joins.join', 0, []);
568
559
  let selectWithJoins = returnFields;
569
560
  for (const j of joins) {
@@ -573,66 +564,96 @@ class SupabaseCsvExport {
573
564
  const hint = j.joinType === 'inner' ? `${j.table}!inner` : j.table;
574
565
  selectWithJoins += `,${hint}(${cols})`;
575
566
  }
576
- let rows;
567
+ const { delimiter, quoteChar } = csvOptions;
568
+ const csvChunks = [];
569
+ const ids = [];
570
+ let rowCount = 0;
571
+ let headers = null;
577
572
  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;
573
+ let transformFn = null;
574
+ let params = {};
575
+ if (enableTransform) {
576
+ const paramEntries = this.getNodeParameter('transformParams.param', 0, []);
577
+ for (const entry of paramEntries) {
578
+ if (entry.name)
579
+ params[entry.name] = entry.value;
580
+ }
581
+ const code = this.getNodeParameter('transformCode', 0, 'return rows;');
582
+ try {
583
+ transformFn = new Function('rows', 'params', code);
584
+ }
585
+ catch (error) {
586
+ const msg = error instanceof Error ? error.message : 'Unknown error';
587
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Transform code syntax error: ${msg}`);
591
588
  }
592
589
  }
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);
590
+ const preferOffset = sort.length > 0;
591
+ for await (const batch of fetchBatches(supabase, table, selectWithJoins, filters, sort, credentials.host, returnAll, limit, preferOffset)) {
592
+ let rows = batch;
593
+ if (transformFn) {
594
+ try {
595
+ const result = transformFn(batch, params);
596
+ if (!Array.isArray(result)) {
597
+ throw new Error('Transform code must return an array. Got: ' + typeof result);
598
+ }
599
+ rows = result;
600
+ }
601
+ catch (error) {
602
+ if (error instanceof n8n_workflow_1.NodeOperationError)
603
+ throw error;
604
+ const msg = error instanceof Error ? error.message : 'Unknown error';
605
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Transform code error: ${msg}`);
606
+ }
599
607
  }
600
- rows = result;
608
+ if (rows.length === 0)
609
+ continue;
610
+ if (headers === null) {
611
+ headers = discoverHeaders(rows);
612
+ if (csvOptions.includeHeaders) {
613
+ const headerLine = headers
614
+ .map((h) => escapeCsvField(h, delimiter, quoteChar))
615
+ .join(delimiter);
616
+ csvChunks.push(Buffer.from(headerLine + '\n', 'utf-8'));
617
+ }
618
+ }
619
+ for (const row of rows) {
620
+ if (row[idColumn] != null)
621
+ ids.push(row[idColumn]);
622
+ }
623
+ const buf = batchToCsvBuffer(rows, headers, delimiter, quoteChar);
624
+ csvChunks.push(buf);
625
+ csvChunks.push(Buffer.from('\n', 'utf-8'));
626
+ rowCount += rows.length;
601
627
  }
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}`);
628
+ if (csvChunks.length > 0) {
629
+ const last = csvChunks[csvChunks.length - 1];
630
+ if (last.length === 1 && last[0] === 0x0a) {
631
+ csvChunks.pop();
632
+ }
605
633
  }
606
634
  }
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
- }
635
+ catch (error) {
636
+ const msg = error instanceof Error ? error.message : 'Unknown error';
637
+ if (error instanceof n8n_workflow_1.NodeOperationError)
638
+ throw error;
639
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Export failed: ${msg}`);
613
640
  }
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');
641
+ const csvBuffer = Buffer.concat(csvChunks);
642
+ csvChunks.length = 0;
623
643
  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]];
644
+ return [[
645
+ {
646
+ json: {
647
+ table,
648
+ rowCount,
649
+ ids,
650
+ fileName: csvOptions.fileName,
651
+ },
652
+ binary: {
653
+ data: binaryData,
654
+ },
655
+ },
656
+ ]];
636
657
  }
637
658
  }
638
659
  exports.SupabaseCsvExport = SupabaseCsvExport;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fachkraftfreund/n8n-nodes-supabase",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Comprehensive n8n community node for Supabase with database and storage operations",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",