@fachkraftfreund/n8n-nodes-supabase 1.2.4 → 1.2.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.
@@ -129,6 +129,12 @@ class Supabase {
129
129
  description: 'Execute custom SQL query',
130
130
  action: 'Execute custom query',
131
131
  },
132
+ {
133
+ name: 'Find or Create',
134
+ value: 'findOrCreate',
135
+ description: 'Return existing row matching criteria, or create it if not found',
136
+ action: 'Find or create row',
137
+ },
132
138
  ],
133
139
  default: 'read',
134
140
  },
@@ -255,7 +261,7 @@ class Supabase {
255
261
  displayOptions: {
256
262
  show: {
257
263
  resource: ['database'],
258
- operation: ['create', 'read', 'update', 'delete', 'upsert'],
264
+ operation: ['create', 'read', 'update', 'delete', 'upsert', 'findOrCreate'],
259
265
  },
260
266
  },
261
267
  },
@@ -316,14 +322,12 @@ class Supabase {
316
322
  },
317
323
  },
318
324
  {
319
- displayName: 'On Conflict Column',
325
+ displayName: 'On Conflict Columns',
320
326
  name: 'onConflict',
321
- type: 'options',
322
- typeOptions: {
323
- loadOptionsMethod: 'getColumns',
324
- },
327
+ type: 'string',
325
328
  default: '',
326
- description: 'Column to use for conflict resolution (usually the primary key). Leave empty to use the default.',
329
+ placeholder: 'id or company_id,name',
330
+ description: 'Comma-separated column(s) to use for conflict resolution. Required for composite unique constraints (e.g. company_id,name).',
327
331
  displayOptions: {
328
332
  show: {
329
333
  resource: ['database'],
@@ -331,6 +335,90 @@ class Supabase {
331
335
  },
332
336
  },
333
337
  },
338
+ {
339
+ displayName: 'Match Columns',
340
+ name: 'matchColumns',
341
+ type: 'fixedCollection',
342
+ typeOptions: {
343
+ multipleValues: true,
344
+ },
345
+ default: {},
346
+ placeholder: 'Add Match Column',
347
+ description: 'Columns used to look up an existing row. If a row matches all values, it is returned; otherwise a new row is created.',
348
+ displayOptions: {
349
+ show: {
350
+ resource: ['database'],
351
+ operation: ['findOrCreate'],
352
+ },
353
+ },
354
+ options: [
355
+ {
356
+ displayName: 'Column',
357
+ name: 'column',
358
+ values: [
359
+ {
360
+ displayName: 'Column',
361
+ name: 'name',
362
+ type: 'options',
363
+ typeOptions: {
364
+ loadOptionsMethod: 'getColumns',
365
+ },
366
+ default: '',
367
+ description: 'Column to match on',
368
+ },
369
+ {
370
+ displayName: 'Value',
371
+ name: 'value',
372
+ type: 'string',
373
+ default: '',
374
+ description: 'Value to match',
375
+ },
376
+ ],
377
+ },
378
+ ],
379
+ },
380
+ {
381
+ displayName: 'Additional Columns',
382
+ name: 'additionalColumns',
383
+ type: 'fixedCollection',
384
+ typeOptions: {
385
+ multipleValues: true,
386
+ },
387
+ default: {},
388
+ placeholder: 'Add Column',
389
+ description: 'Extra columns to set when creating a new row (not used when a match is found)',
390
+ displayOptions: {
391
+ show: {
392
+ resource: ['database'],
393
+ operation: ['findOrCreate'],
394
+ },
395
+ },
396
+ options: [
397
+ {
398
+ displayName: 'Column',
399
+ name: 'column',
400
+ values: [
401
+ {
402
+ displayName: 'Column',
403
+ name: 'name',
404
+ type: 'options',
405
+ typeOptions: {
406
+ loadOptionsMethod: 'getColumns',
407
+ },
408
+ default: '',
409
+ description: 'Column name',
410
+ },
411
+ {
412
+ displayName: 'Value',
413
+ name: 'value',
414
+ type: 'string',
415
+ default: '',
416
+ description: 'Value to set',
417
+ },
418
+ ],
419
+ },
420
+ ],
421
+ },
334
422
  {
335
423
  displayName: 'Filters',
336
424
  name: 'filters',
@@ -42,6 +42,9 @@ async function executeDatabaseOperation(supabase, operation, itemIndex) {
42
42
  case 'customQuery':
43
43
  returnData.push(...await handleCustomQuery.call(this, supabase, itemIndex));
44
44
  break;
45
+ case 'findOrCreate':
46
+ returnData.push(...await handleFindOrCreate.call(this, supabase, itemIndex));
47
+ break;
45
48
  default:
46
49
  throw new Error(`Unknown database operation: ${operation}`);
47
50
  }
@@ -355,7 +358,8 @@ async function handleCustomQuery(supabase, itemIndex) {
355
358
  if (!(customSql === null || customSql === void 0 ? void 0 : customSql.trim())) {
356
359
  throw new Error('SQL query is required');
357
360
  }
358
- if (customSql.trim().toLowerCase().startsWith('select')) {
361
+ const returnsRows = /^\s*(select|with|explain|table)\b/i.test(customSql);
362
+ if (returnsRows) {
359
363
  const { data, error } = await supabase.rpc('exec_sql_select', { sql: customSql });
360
364
  if (error) {
361
365
  throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
@@ -386,3 +390,39 @@ async function handleCustomQuery(supabase, itemIndex) {
386
390
  return [{ json: { operation: 'customQuery', sql: customSql, result: data, success: true } }];
387
391
  }
388
392
  }
393
+ async function handleFindOrCreate(supabase, itemIndex) {
394
+ const table = this.getNodeParameter('table', itemIndex);
395
+ (0, supabaseClient_1.validateTableName)(table);
396
+ const matchCols = this.getNodeParameter('matchColumns.column', itemIndex, []);
397
+ if (matchCols.length === 0) {
398
+ throw new Error('At least one Match Column is required for Find or Create');
399
+ }
400
+ let query = supabase.from(table).select('*');
401
+ for (const col of matchCols) {
402
+ if (!col.name)
403
+ continue;
404
+ (0, supabaseClient_1.validateColumnName)(col.name);
405
+ query = query.eq(col.name, col.value);
406
+ }
407
+ query = query.limit(1);
408
+ const { data: existing, error: readError } = await query;
409
+ if (readError)
410
+ throw new Error((0, supabaseClient_1.formatSupabaseError)(readError));
411
+ if (existing && existing.length > 0) {
412
+ return [{ json: { ...existing[0], _found: true, _created: false } }];
413
+ }
414
+ const additionalCols = this.getNodeParameter('additionalColumns.column', itemIndex, []);
415
+ const dataToInsert = {};
416
+ for (const col of [...matchCols, ...additionalCols]) {
417
+ if (col.name)
418
+ dataToInsert[col.name] = col.value;
419
+ }
420
+ const { data: created, error: insertError } = await supabase
421
+ .from(table)
422
+ .insert(dataToInsert)
423
+ .select()
424
+ .single();
425
+ if (insertError)
426
+ throw new Error((0, supabaseClient_1.formatSupabaseError)(insertError));
427
+ return [{ json: { ...created, _found: false, _created: true } }];
428
+ }
@@ -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';
78
+ export type DatabaseOperation = 'create' | 'read' | 'update' | 'delete' | 'upsert' | 'createTable' | 'dropTable' | 'addColumn' | 'dropColumn' | 'createIndex' | 'dropIndex' | 'customQuery' | 'findOrCreate';
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.4",
3
+ "version": "1.2.6",
4
4
  "description": "Comprehensive n8n community node for Supabase with database and storage operations",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",