@fachkraftfreund/n8n-nodes-supabase 1.3.4 → 1.3.6

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.
@@ -341,6 +341,19 @@ class Supabase {
341
341
  },
342
342
  },
343
343
  },
344
+ {
345
+ displayName: 'Remove Duplicate Rows',
346
+ name: 'deduplicateByConflict',
347
+ type: 'boolean',
348
+ default: false,
349
+ description: 'When enabled, removes duplicate rows by the conflict/match column(s) before sending to Supabase. Keeps the last occurrence. Prevents the "ON CONFLICT DO UPDATE command cannot affect row a second time" error.',
350
+ displayOptions: {
351
+ show: {
352
+ resource: ['database'],
353
+ operation: ['upsert', 'update'],
354
+ },
355
+ },
356
+ },
344
357
  {
345
358
  displayName: 'Match Column',
346
359
  name: 'matchColumn',
@@ -55,7 +55,6 @@ 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
58
  function deduplicateByConflictKeys(rows, conflictColumns) {
60
59
  const keys = conflictColumns.split(',').map((k) => k.trim()).filter(Boolean);
61
60
  if (keys.length === 0)
@@ -205,32 +204,30 @@ async function handleBulkUpsert(supabase, itemCount) {
205
204
  const table = this.getNodeParameter('table', 0);
206
205
  const onConflict = this.getNodeParameter('onConflict', 0, '');
207
206
  (0, supabaseClient_1.validateTableName)(table);
208
- const rows = collectRowData(this, itemCount);
207
+ const deduplicate = this.getNodeParameter('deduplicateByConflict', 0, false);
208
+ let rows = collectRowData(this, itemCount);
209
209
  const options = {};
210
- if (onConflict)
210
+ if (onConflict) {
211
211
  options.onConflict = onConflict;
212
+ if (deduplicate) {
213
+ const before = rows.length;
214
+ rows = deduplicateByConflictKeys(rows, onConflict);
215
+ if (rows.length < before) {
216
+ console.log(`[Supabase UPSERT ${table}] deduplicated input: ${before} → ${rows.length} rows by conflict key "${onConflict}"`);
217
+ }
218
+ }
219
+ }
212
220
  const returnData = [];
213
221
  for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
214
- let batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
222
+ const batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
215
223
  const batchLabel = `UPSERT ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`;
216
224
  const data = await withRetry(async () => {
217
225
  const { data, error } = await supabase
218
226
  .from(table)
219
227
  .upsert(batch, options)
220
228
  .select();
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
- }
229
+ if (error)
230
+ throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
234
231
  return data;
235
232
  }, batchLabel);
236
233
  if (Array.isArray(data)) {
@@ -249,35 +246,32 @@ async function handleBulkUpdate(supabase, itemCount) {
249
246
  if (!matchColumn) {
250
247
  throw new Error('Match Column is required for update operations');
251
248
  }
252
- const rows = collectRowData(this, itemCount);
249
+ const deduplicate = this.getNodeParameter('deduplicateByConflict', 0, false);
250
+ let rows = collectRowData(this, itemCount);
253
251
  for (let i = 0; i < rows.length; i++) {
254
252
  const row = rows[i];
255
253
  if (!row || row[matchColumn] === undefined) {
256
254
  throw new Error(`Item ${i} is missing the match column "${matchColumn}"`);
257
255
  }
258
256
  }
257
+ if (deduplicate) {
258
+ const before = rows.length;
259
+ rows = deduplicateByConflictKeys(rows, matchColumn);
260
+ if (rows.length < before) {
261
+ console.log(`[Supabase UPDATE ${table}] deduplicated input: ${before} → ${rows.length} rows by match column "${matchColumn}"`);
262
+ }
263
+ }
259
264
  const returnData = [];
260
265
  for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
261
- let batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
266
+ const batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
262
267
  const batchLabel = `UPDATE ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`;
263
268
  const data = await withRetry(async () => {
264
269
  const { data, error } = await supabase
265
270
  .from(table)
266
271
  .upsert(batch, { onConflict: matchColumn })
267
272
  .select();
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
- }
273
+ if (error)
274
+ throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
281
275
  return data;
282
276
  }, batchLabel);
283
277
  if (Array.isArray(data)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fachkraftfreund/n8n-nodes-supabase",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
4
4
  "description": "Comprehensive n8n community node for Supabase with database and storage operations",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",