@fachkraftfreund/n8n-nodes-supabase 1.3.2 → 1.3.4
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.
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SupabaseCsvExport = void 0;
|
|
4
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const os_1 = require("os");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
5
9
|
const supabaseClient_1 = require("./utils/supabaseClient");
|
|
6
10
|
const supabaseClient_2 = require("./utils/supabaseClient");
|
|
7
11
|
function escapeRegExp(s) {
|
|
@@ -28,7 +32,7 @@ function discoverHeaders(rows) {
|
|
|
28
32
|
}
|
|
29
33
|
return [...set];
|
|
30
34
|
}
|
|
31
|
-
function
|
|
35
|
+
function batchToCsvLines(rows, headers, delimiter, quoteChar) {
|
|
32
36
|
const lines = new Array(rows.length);
|
|
33
37
|
for (let i = 0; i < rows.length; i++) {
|
|
34
38
|
const row = rows[i];
|
|
@@ -36,7 +40,19 @@ function batchToCsvBuffer(rows, headers, delimiter, quoteChar) {
|
|
|
36
40
|
.map((h) => escapeCsvField(row[h], delimiter, quoteChar))
|
|
37
41
|
.join(delimiter);
|
|
38
42
|
}
|
|
39
|
-
return
|
|
43
|
+
return lines.join('\n') + '\n';
|
|
44
|
+
}
|
|
45
|
+
function streamWrite(stream, data) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const ok = stream.write(data, 'utf-8');
|
|
48
|
+
if (ok) {
|
|
49
|
+
resolve();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
stream.once('drain', resolve);
|
|
53
|
+
stream.once('error', reject);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
40
56
|
}
|
|
41
57
|
function buildSelectQuery(supabase, table, selectFields, filters, sort) {
|
|
42
58
|
let query = supabase.from(table).select(selectFields);
|
|
@@ -580,7 +596,8 @@ class SupabaseCsvExport {
|
|
|
580
596
|
selectWithJoins += `,${hint}(${cols})`;
|
|
581
597
|
}
|
|
582
598
|
const { delimiter, quoteChar } = csvOptions;
|
|
583
|
-
const
|
|
599
|
+
const tmpPath = (0, path_1.join)((0, os_1.tmpdir)(), `n8n-csv-${(0, crypto_1.randomBytes)(8).toString('hex')}.csv`);
|
|
600
|
+
const fileStream = (0, fs_1.createWriteStream)(tmpPath, { encoding: 'utf-8' });
|
|
584
601
|
const ids = [];
|
|
585
602
|
let rowCount = 0;
|
|
586
603
|
let headers = null;
|
|
@@ -626,25 +643,37 @@ class SupabaseCsvExport {
|
|
|
626
643
|
if (csvOptions.includeHeaders) {
|
|
627
644
|
const headerLine = headers
|
|
628
645
|
.map((h) => escapeCsvField(h, delimiter, quoteChar))
|
|
629
|
-
.join(delimiter);
|
|
630
|
-
|
|
646
|
+
.join(delimiter) + '\n';
|
|
647
|
+
await streamWrite(fileStream, headerLine);
|
|
631
648
|
}
|
|
632
649
|
}
|
|
633
650
|
for (const row of rows) {
|
|
634
651
|
if (row[idColumn] != null)
|
|
635
652
|
ids.push(row[idColumn]);
|
|
636
653
|
}
|
|
637
|
-
|
|
638
|
-
csvChunks.push(buf);
|
|
639
|
-
csvChunks.push(Buffer.from('\n', 'utf-8'));
|
|
654
|
+
await streamWrite(fileStream, batchToCsvLines(rows, headers, delimiter, quoteChar));
|
|
640
655
|
rowCount += rows.length;
|
|
641
656
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
657
|
+
await new Promise((resolve, reject) => {
|
|
658
|
+
fileStream.end(() => resolve());
|
|
659
|
+
fileStream.on('error', reject);
|
|
660
|
+
});
|
|
661
|
+
console.log(`[Supabase CSV] wrote ${rowCount} rows to temp file, passing to n8n binary storage`);
|
|
662
|
+
const readStream = (0, fs_1.createReadStream)(tmpPath);
|
|
663
|
+
const binaryData = await this.helpers.prepareBinaryData(readStream, csvOptions.fileName, 'text/csv');
|
|
664
|
+
return [[
|
|
665
|
+
{
|
|
666
|
+
json: {
|
|
667
|
+
table,
|
|
668
|
+
rowCount,
|
|
669
|
+
ids,
|
|
670
|
+
fileName: csvOptions.fileName,
|
|
671
|
+
},
|
|
672
|
+
binary: {
|
|
673
|
+
data: binaryData,
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
]];
|
|
648
677
|
}
|
|
649
678
|
catch (error) {
|
|
650
679
|
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -652,22 +681,16 @@ class SupabaseCsvExport {
|
|
|
652
681
|
throw error;
|
|
653
682
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Export failed: ${msg}`);
|
|
654
683
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
},
|
|
666
|
-
binary: {
|
|
667
|
-
data: binaryData,
|
|
668
|
-
},
|
|
669
|
-
},
|
|
670
|
-
]];
|
|
684
|
+
finally {
|
|
685
|
+
try {
|
|
686
|
+
fileStream.destroy();
|
|
687
|
+
}
|
|
688
|
+
catch { }
|
|
689
|
+
try {
|
|
690
|
+
(0, fs_1.unlinkSync)(tmpPath);
|
|
691
|
+
}
|
|
692
|
+
catch { }
|
|
693
|
+
}
|
|
671
694
|
}
|
|
672
695
|
}
|
|
673
696
|
exports.SupabaseCsvExport = SupabaseCsvExport;
|
|
@@ -55,6 +55,19 @@ exports.executeDatabaseOperation = executeDatabaseOperation;
|
|
|
55
55
|
const BULK_BATCH_SIZE = 500;
|
|
56
56
|
const MAX_RETRIES = 3;
|
|
57
57
|
const RETRY_BASE_DELAY_MS = 1000;
|
|
58
|
+
const DUPLICATE_ROW_ERROR = 'cannot affect row a second time';
|
|
59
|
+
function deduplicateByConflictKeys(rows, conflictColumns) {
|
|
60
|
+
const keys = conflictColumns.split(',').map((k) => k.trim()).filter(Boolean);
|
|
61
|
+
if (keys.length === 0)
|
|
62
|
+
return rows;
|
|
63
|
+
const seen = new Map();
|
|
64
|
+
for (let i = 0; i < rows.length; i++) {
|
|
65
|
+
const row = rows[i];
|
|
66
|
+
const compositeKey = keys.map((k) => { var _a; return String((_a = row[k]) !== null && _a !== void 0 ? _a : ''); }).join('\0');
|
|
67
|
+
seen.set(compositeKey, i);
|
|
68
|
+
}
|
|
69
|
+
return Array.from(seen.values()).sort((a, b) => a - b).map((i) => rows[i]);
|
|
70
|
+
}
|
|
58
71
|
function isRetryableError(msg) {
|
|
59
72
|
const lower = msg.toLowerCase();
|
|
60
73
|
return (lower.includes('lock timeout') ||
|
|
@@ -198,16 +211,28 @@ async function handleBulkUpsert(supabase, itemCount) {
|
|
|
198
211
|
options.onConflict = onConflict;
|
|
199
212
|
const returnData = [];
|
|
200
213
|
for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
|
|
201
|
-
|
|
214
|
+
let batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
|
|
215
|
+
const batchLabel = `UPSERT ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`;
|
|
202
216
|
const data = await withRetry(async () => {
|
|
203
217
|
const { data, error } = await supabase
|
|
204
218
|
.from(table)
|
|
205
219
|
.upsert(batch, options)
|
|
206
220
|
.select();
|
|
207
|
-
if (error)
|
|
208
|
-
|
|
221
|
+
if (error) {
|
|
222
|
+
const msg = (0, supabaseClient_1.formatSupabaseError)(error);
|
|
223
|
+
if (msg.toLowerCase().includes(DUPLICATE_ROW_ERROR) && onConflict) {
|
|
224
|
+
const before = batch.length;
|
|
225
|
+
batch = deduplicateByConflictKeys(batch, onConflict);
|
|
226
|
+
console.log(`[Supabase ${batchLabel}] removed ${before - batch.length} duplicate(s) by conflict key "${onConflict}", retrying ${batch.length} rows`);
|
|
227
|
+
const retry = await supabase.from(table).upsert(batch, options).select();
|
|
228
|
+
if (retry.error)
|
|
229
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(retry.error));
|
|
230
|
+
return retry.data;
|
|
231
|
+
}
|
|
232
|
+
throw new Error(msg);
|
|
233
|
+
}
|
|
209
234
|
return data;
|
|
210
|
-
},
|
|
235
|
+
}, batchLabel);
|
|
211
236
|
if (Array.isArray(data)) {
|
|
212
237
|
for (const row of data)
|
|
213
238
|
returnData.push({ json: row });
|
|
@@ -233,16 +258,28 @@ async function handleBulkUpdate(supabase, itemCount) {
|
|
|
233
258
|
}
|
|
234
259
|
const returnData = [];
|
|
235
260
|
for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
|
|
236
|
-
|
|
261
|
+
let batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
|
|
262
|
+
const batchLabel = `UPDATE ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`;
|
|
237
263
|
const data = await withRetry(async () => {
|
|
238
264
|
const { data, error } = await supabase
|
|
239
265
|
.from(table)
|
|
240
266
|
.upsert(batch, { onConflict: matchColumn })
|
|
241
267
|
.select();
|
|
242
|
-
if (error)
|
|
243
|
-
|
|
268
|
+
if (error) {
|
|
269
|
+
const msg = (0, supabaseClient_1.formatSupabaseError)(error);
|
|
270
|
+
if (msg.toLowerCase().includes(DUPLICATE_ROW_ERROR)) {
|
|
271
|
+
const before = batch.length;
|
|
272
|
+
batch = deduplicateByConflictKeys(batch, matchColumn);
|
|
273
|
+
console.log(`[Supabase ${batchLabel}] removed ${before - batch.length} duplicate(s) by match column "${matchColumn}", retrying ${batch.length} rows`);
|
|
274
|
+
const retry = await supabase.from(table).upsert(batch, { onConflict: matchColumn }).select();
|
|
275
|
+
if (retry.error)
|
|
276
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(retry.error));
|
|
277
|
+
return retry.data;
|
|
278
|
+
}
|
|
279
|
+
throw new Error(msg);
|
|
280
|
+
}
|
|
244
281
|
return data;
|
|
245
|
-
},
|
|
282
|
+
}, batchLabel);
|
|
246
283
|
if (Array.isArray(data)) {
|
|
247
284
|
for (const row of data)
|
|
248
285
|
returnData.push({ json: row });
|
package/package.json
CHANGED