@fachkraftfreund/n8n-nodes-supabase 1.2.22 → 1.2.24

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.
@@ -527,6 +527,58 @@ class Supabase {
527
527
  },
528
528
  },
529
529
  },
530
+ {
531
+ displayName: 'Joins',
532
+ name: 'joins',
533
+ type: 'fixedCollection',
534
+ typeOptions: {
535
+ multipleValues: true,
536
+ },
537
+ default: {},
538
+ placeholder: 'Add Join',
539
+ description: 'Join related tables via foreign keys (PostgREST resource embedding)',
540
+ displayOptions: {
541
+ show: {
542
+ resource: ['database'],
543
+ operation: ['read'],
544
+ },
545
+ },
546
+ options: [
547
+ {
548
+ displayName: 'Join',
549
+ name: 'join',
550
+ values: [
551
+ {
552
+ displayName: 'Table',
553
+ name: 'table',
554
+ type: 'string',
555
+ default: '',
556
+ placeholder: 'company_email',
557
+ description: 'Related table to join',
558
+ },
559
+ {
560
+ displayName: 'Columns',
561
+ name: 'columns',
562
+ type: 'string',
563
+ default: '*',
564
+ placeholder: 'email,verified',
565
+ description: 'Comma-separated columns to select from the joined table (* for all)',
566
+ },
567
+ {
568
+ displayName: 'Join Type',
569
+ name: 'joinType',
570
+ type: 'options',
571
+ options: [
572
+ { name: 'Left Join', value: 'left' },
573
+ { name: 'Inner Join', value: 'inner' },
574
+ ],
575
+ default: 'left',
576
+ description: 'Left returns all rows even without a match; Inner only returns rows with a match',
577
+ },
578
+ ],
579
+ },
580
+ ],
581
+ },
530
582
  {
531
583
  displayName: 'Sort',
532
584
  name: 'sort',
@@ -49,6 +49,34 @@ async function executeDatabaseOperation(supabase, operation, itemIndex, hostUrl)
49
49
  return returnData;
50
50
  }
51
51
  exports.executeDatabaseOperation = executeDatabaseOperation;
52
+ function sanitizeString(value) {
53
+ return value
54
+ .replace(/\x00/g, '')
55
+ .replace(/\\u[0-9a-fA-F]{4}/g, '')
56
+ .replace(/\\/g, '');
57
+ }
58
+ function sanitizeRow(obj) {
59
+ for (const key of Object.keys(obj)) {
60
+ const val = obj[key];
61
+ if (typeof val === 'string') {
62
+ obj[key] = sanitizeString(val);
63
+ }
64
+ else if (Array.isArray(val)) {
65
+ for (let i = 0; i < val.length; i++) {
66
+ if (typeof val[i] === 'string') {
67
+ val[i] = sanitizeString(val[i]);
68
+ }
69
+ else if (val[i] && typeof val[i] === 'object') {
70
+ sanitizeRow(val[i]);
71
+ }
72
+ }
73
+ }
74
+ else if (val && typeof val === 'object') {
75
+ sanitizeRow(val);
76
+ }
77
+ }
78
+ return obj;
79
+ }
52
80
  function collectRowData(context, itemCount) {
53
81
  const rows = [];
54
82
  for (let i = 0; i < itemCount; i++) {
@@ -72,7 +100,7 @@ function collectRowData(context, itemCount) {
72
100
  }
73
101
  }
74
102
  }
75
- rows.push(row);
103
+ rows.push(sanitizeRow(row));
76
104
  }
77
105
  return rows;
78
106
  }
@@ -196,16 +224,26 @@ async function handleRead(supabase, itemIndex, hostUrl) {
196
224
  (0, supabaseClient_1.validateTableName)(table);
197
225
  const returnFields = this.getNodeParameter('returnFields', itemIndex, '*');
198
226
  const returnAll = this.getNodeParameter('returnAll', itemIndex, false);
227
+ const singleItem = this.getNodeParameter('singleItem', itemIndex, false);
199
228
  const filters = getFilters(this, itemIndex);
200
229
  const sort = this.getNodeParameter('sort.sortField', itemIndex, []);
230
+ const joins = this.getNodeParameter('joins.join', itemIndex, []);
231
+ let selectWithJoins = returnFields;
232
+ for (const j of joins) {
233
+ if (!j.table)
234
+ continue;
235
+ const cols = j.columns || '*';
236
+ const hint = j.joinType === 'inner' ? `${j.table}!inner` : j.table;
237
+ selectWithJoins += `,${hint}(${cols})`;
238
+ }
201
239
  for (const f of filters) {
202
240
  const valType = Array.isArray(f.value) ? `array[${f.value.length}]` : typeof f.value;
203
241
  const valLen = typeof f.value === 'string' ? f.value.length : Array.isArray(f.value) ? f.value.length : 0;
204
242
  console.log(`[Supabase READ] item=${itemIndex} filter: ${f.column} ${f.operator} (${valType}, len=${valLen})`);
205
243
  }
206
- const overhead = (0, supabaseClient_1.estimateUrlOverhead)(hostUrl, table, returnFields, filters, sort);
244
+ const overhead = (0, supabaseClient_1.estimateUrlOverhead)(hostUrl, table, selectWithJoins, filters, sort);
207
245
  const maxInChars = Math.max(500, supabaseClient_1.MAX_SAFE_URL_LENGTH - overhead);
208
- const maxItems = (0, supabaseClient_1.computeMaxIdsPerChunk)(returnFields);
246
+ const maxItems = (0, supabaseClient_1.computeMaxIdsPerChunk)(selectWithJoins);
209
247
  const filterChunks = (0, supabaseClient_1.expandChunkedFilters)(filters, maxInChars, maxItems);
210
248
  console.log(`[Supabase READ] item=${itemIndex} table=${table} returnAll=${returnAll} chunks=${filterChunks.length} maxItems=${maxItems} maxInChars=${maxInChars}`);
211
249
  const returnData = [];
@@ -224,7 +262,7 @@ async function handleRead(supabase, itemIndex, hostUrl) {
224
262
  if (hasIdColumn) {
225
263
  let lastId = null;
226
264
  while (hasMore) {
227
- let query = buildReadQuery(supabase, table, returnFields, chunkFilters, []);
265
+ let query = buildReadQuery(supabase, table, selectWithJoins, chunkFilters, []);
228
266
  if (lastId !== null) {
229
267
  query = query.gt('id', lastId);
230
268
  }
@@ -252,7 +290,7 @@ async function handleRead(supabase, itemIndex, hostUrl) {
252
290
  else {
253
291
  let batchOffset = 0;
254
292
  while (hasMore) {
255
- const query = buildReadQuery(supabase, table, returnFields, chunkFilters, sort);
293
+ const query = buildReadQuery(supabase, table, selectWithJoins, chunkFilters, sort);
256
294
  const { data, error } = await query.range(batchOffset, batchOffset + batchSize - 1);
257
295
  if (error) {
258
296
  console.log(`[Supabase READ] chunk ${ci + 1} batch ${batchCount + 1} FAILED after ${Date.now() - chunkStart}ms: ${(0, supabaseClient_1.formatSupabaseError)(error)}`);
@@ -301,7 +339,7 @@ async function handleRead(supabase, itemIndex, hostUrl) {
301
339
  const userOffset = this.getNodeParameter('offset', itemIndex, 0);
302
340
  const isMultiChunk = filterChunks.length > 1;
303
341
  for (const chunkFilters of filterChunks) {
304
- let query = buildReadQuery(supabase, table, returnFields, chunkFilters, sort);
342
+ let query = buildReadQuery(supabase, table, selectWithJoins, chunkFilters, sort);
305
343
  if (isMultiChunk) {
306
344
  query = query.limit(userOffset + limit);
307
345
  }
@@ -341,10 +379,13 @@ async function handleRead(supabase, itemIndex, hostUrl) {
341
379
  },
342
380
  });
343
381
  }
344
- const singleItem = this.getNodeParameter('singleItem', itemIndex, false);
345
382
  if (singleItem && returnData.length > 0) {
346
- const allRows = returnData.map(item => item.json);
347
- return [{ json: { data: allRows, count: allRows.length } }];
383
+ const count = returnData.length;
384
+ const allRows = new Array(count);
385
+ for (let i = 0; i < count; i++)
386
+ allRows[i] = returnData[i].json;
387
+ returnData.length = 0;
388
+ return [{ json: { data: allRows, count } }];
348
389
  }
349
390
  return returnData;
350
391
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fachkraftfreund/n8n-nodes-supabase",
3
- "version": "1.2.22",
3
+ "version": "1.2.24",
4
4
  "description": "Comprehensive n8n community node for Supabase with database and storage operations",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",