@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
|
-
|
|
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
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 (
|
|
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
|
-
|
|
102
|
-
|
|
104
|
+
yield data;
|
|
105
|
+
totalYielded += data.length;
|
|
103
106
|
lastId = data[data.length - 1].id;
|
|
104
|
-
hasMore = data.length ===
|
|
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 +
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
hasMore = data.length ===
|
|
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 +=
|
|
129
|
+
offset += BATCH_SIZE;
|
|
127
130
|
}
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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(
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
567
|
+
const { delimiter, quoteChar } = csvOptions;
|
|
568
|
+
const csvChunks = [];
|
|
569
|
+
const ids = [];
|
|
570
|
+
let rowCount = 0;
|
|
571
|
+
let headers = null;
|
|
577
572
|
try {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
|
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
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
|
615
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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