@fachkraftfreund/n8n-nodes-supabase 1.2.24 → 1.3.0
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.
- package/dist/nodes/Supabase/Supabase.node.js +12 -6
- package/dist/nodes/Supabase/SupabaseCsvExport.node.d.ts +11 -0
- package/dist/nodes/Supabase/SupabaseCsvExport.node.js +638 -0
- package/dist/nodes/Supabase/operations/database/index.js +155 -27
- package/dist/nodes/Supabase/types/index.d.ts +1 -1
- package/package.json +3 -2
|
@@ -135,6 +135,12 @@ class Supabase {
|
|
|
135
135
|
description: 'Return existing row matching criteria, or create it if not found',
|
|
136
136
|
action: 'Find or create row',
|
|
137
137
|
},
|
|
138
|
+
{
|
|
139
|
+
name: 'Update by Query',
|
|
140
|
+
value: 'updateByQuery',
|
|
141
|
+
description: 'Update all rows matching filter conditions (e.g. update where id IN list)',
|
|
142
|
+
action: 'Update rows by query',
|
|
143
|
+
},
|
|
138
144
|
],
|
|
139
145
|
default: 'read',
|
|
140
146
|
},
|
|
@@ -244,7 +250,7 @@ class Supabase {
|
|
|
244
250
|
displayOptions: {
|
|
245
251
|
show: {
|
|
246
252
|
resource: ['database'],
|
|
247
|
-
operation: ['create', 'read', 'update', 'delete', 'upsert'],
|
|
253
|
+
operation: ['create', 'read', 'update', 'delete', 'upsert', 'updateByQuery'],
|
|
248
254
|
},
|
|
249
255
|
},
|
|
250
256
|
},
|
|
@@ -261,7 +267,7 @@ class Supabase {
|
|
|
261
267
|
displayOptions: {
|
|
262
268
|
show: {
|
|
263
269
|
resource: ['database'],
|
|
264
|
-
operation: ['create', 'read', 'update', 'delete', 'upsert', 'findOrCreate'],
|
|
270
|
+
operation: ['create', 'read', 'update', 'delete', 'upsert', 'findOrCreate', 'updateByQuery'],
|
|
265
271
|
},
|
|
266
272
|
},
|
|
267
273
|
},
|
|
@@ -277,7 +283,7 @@ class Supabase {
|
|
|
277
283
|
displayOptions: {
|
|
278
284
|
show: {
|
|
279
285
|
resource: ['database'],
|
|
280
|
-
operation: ['create', 'update', 'upsert'],
|
|
286
|
+
operation: ['create', 'update', 'upsert', 'updateByQuery'],
|
|
281
287
|
uiMode: ['simple'],
|
|
282
288
|
},
|
|
283
289
|
},
|
|
@@ -316,7 +322,7 @@ class Supabase {
|
|
|
316
322
|
displayOptions: {
|
|
317
323
|
show: {
|
|
318
324
|
resource: ['database'],
|
|
319
|
-
operation: ['create', 'update', 'upsert'],
|
|
325
|
+
operation: ['create', 'update', 'upsert', 'updateByQuery'],
|
|
320
326
|
uiMode: ['advanced'],
|
|
321
327
|
},
|
|
322
328
|
},
|
|
@@ -448,7 +454,7 @@ class Supabase {
|
|
|
448
454
|
displayOptions: {
|
|
449
455
|
show: {
|
|
450
456
|
resource: ['database'],
|
|
451
|
-
operation: ['read', 'delete'],
|
|
457
|
+
operation: ['read', 'delete', 'updateByQuery'],
|
|
452
458
|
uiMode: ['simple'],
|
|
453
459
|
},
|
|
454
460
|
},
|
|
@@ -508,7 +514,7 @@ class Supabase {
|
|
|
508
514
|
displayOptions: {
|
|
509
515
|
show: {
|
|
510
516
|
resource: ['database'],
|
|
511
|
-
operation: ['read', 'delete'],
|
|
517
|
+
operation: ['read', 'delete', 'updateByQuery'],
|
|
512
518
|
uiMode: ['advanced'],
|
|
513
519
|
},
|
|
514
520
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class SupabaseCsvExport implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
loadOptions: {
|
|
6
|
+
getTables(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
|
+
getColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SupabaseCsvExport = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const supabaseClient_1 = require("./utils/supabaseClient");
|
|
6
|
+
const supabaseClient_2 = require("./utils/supabaseClient");
|
|
7
|
+
function escapeCsvField(value, delimiter, quoteChar) {
|
|
8
|
+
if (value === null || value === undefined)
|
|
9
|
+
return '';
|
|
10
|
+
const str = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
11
|
+
if (str.includes(delimiter) ||
|
|
12
|
+
str.includes(quoteChar) ||
|
|
13
|
+
str.includes('\n') ||
|
|
14
|
+
str.includes('\r')) {
|
|
15
|
+
return quoteChar + str.replace(new RegExp(escapeRegExp(quoteChar), 'g'), quoteChar + quoteChar) + quoteChar;
|
|
16
|
+
}
|
|
17
|
+
return str;
|
|
18
|
+
}
|
|
19
|
+
function escapeRegExp(s) {
|
|
20
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
|
+
}
|
|
22
|
+
function generateCsv(rows, options) {
|
|
23
|
+
if (rows.length === 0)
|
|
24
|
+
return '';
|
|
25
|
+
const { delimiter, quoteChar, includeHeaders } = options;
|
|
26
|
+
const headerSet = new Set();
|
|
27
|
+
for (const row of rows) {
|
|
28
|
+
for (const key of Object.keys(row))
|
|
29
|
+
headerSet.add(key);
|
|
30
|
+
}
|
|
31
|
+
const headers = [...headerSet];
|
|
32
|
+
const lines = [];
|
|
33
|
+
if (includeHeaders) {
|
|
34
|
+
lines.push(headers.map((h) => escapeCsvField(h, delimiter, quoteChar)).join(delimiter));
|
|
35
|
+
}
|
|
36
|
+
for (const row of rows) {
|
|
37
|
+
lines.push(headers.map((h) => escapeCsvField(row[h], delimiter, quoteChar)).join(delimiter));
|
|
38
|
+
}
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
function buildSelectQuery(supabase, table, selectFields, filters, sort) {
|
|
42
|
+
let query = supabase.from(table).select(selectFields);
|
|
43
|
+
for (const filter of filters) {
|
|
44
|
+
const operator = (0, supabaseClient_2.convertFilterOperator)(filter.operator);
|
|
45
|
+
query = query.filter(filter.column, operator, (0, supabaseClient_2.normalizeFilterValue)(filter.operator, filter.value));
|
|
46
|
+
}
|
|
47
|
+
for (const s of sort) {
|
|
48
|
+
query = query.order(s.column, { ascending: s.ascending });
|
|
49
|
+
}
|
|
50
|
+
return query;
|
|
51
|
+
}
|
|
52
|
+
function parseFilters(context, itemIndex) {
|
|
53
|
+
const uiMode = context.getNodeParameter('uiMode', itemIndex, 'simple');
|
|
54
|
+
if (uiMode === 'simple') {
|
|
55
|
+
return context.getNodeParameter('filters.filter', itemIndex, []);
|
|
56
|
+
}
|
|
57
|
+
const raw = context.getNodeParameter('advancedFilters', itemIndex, '');
|
|
58
|
+
if (!raw)
|
|
59
|
+
return [];
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
const filters = [];
|
|
63
|
+
for (const [column, condition] of Object.entries(parsed)) {
|
|
64
|
+
if (typeof condition === 'object' && condition !== null) {
|
|
65
|
+
const [operator, value] = Object.entries(condition)[0];
|
|
66
|
+
filters.push({ column, operator: operator, value: value });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
filters.push({ column, operator: 'eq', value: condition });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return filters;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new Error('Invalid advanced filters JSON');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function fetchAllRows(supabase, table, selectFields, filters, sort, hostUrl, returnAll, limit) {
|
|
79
|
+
const overhead = (0, supabaseClient_2.estimateUrlOverhead)(hostUrl, table, selectFields, filters, sort);
|
|
80
|
+
const maxInChars = Math.max(500, supabaseClient_2.MAX_SAFE_URL_LENGTH - overhead);
|
|
81
|
+
const maxItems = (0, supabaseClient_2.computeMaxIdsPerChunk)(selectFields);
|
|
82
|
+
const filterChunks = (0, supabaseClient_2.expandChunkedFilters)(filters, maxInChars, maxItems);
|
|
83
|
+
const allRows = [];
|
|
84
|
+
if (returnAll) {
|
|
85
|
+
const batchSize = 1000;
|
|
86
|
+
const hasIdColumn = selectFields === '*' || selectFields.split(',').some((f) => f.trim() === 'id');
|
|
87
|
+
for (const chunkFilters of filterChunks) {
|
|
88
|
+
let hasMore = true;
|
|
89
|
+
if (hasIdColumn) {
|
|
90
|
+
let lastId = null;
|
|
91
|
+
while (hasMore) {
|
|
92
|
+
let query = buildSelectQuery(supabase, table, selectFields, chunkFilters, []);
|
|
93
|
+
if (lastId !== null) {
|
|
94
|
+
query = query.gt('id', lastId);
|
|
95
|
+
}
|
|
96
|
+
query = query.order('id', { ascending: true }).limit(batchSize);
|
|
97
|
+
const { data, error } = await query;
|
|
98
|
+
if (error)
|
|
99
|
+
throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
|
|
100
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
101
|
+
for (const row of data)
|
|
102
|
+
allRows.push(row);
|
|
103
|
+
lastId = data[data.length - 1].id;
|
|
104
|
+
hasMore = data.length === batchSize;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
hasMore = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
let offset = 0;
|
|
113
|
+
while (hasMore) {
|
|
114
|
+
const query = buildSelectQuery(supabase, table, selectFields, chunkFilters, sort);
|
|
115
|
+
const { data, error } = await query.range(offset, offset + batchSize - 1);
|
|
116
|
+
if (error)
|
|
117
|
+
throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
|
|
118
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
119
|
+
for (const row of data)
|
|
120
|
+
allRows.push(row);
|
|
121
|
+
hasMore = data.length === batchSize;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
hasMore = false;
|
|
125
|
+
}
|
|
126
|
+
offset += batchSize;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (hasIdColumn && sort.length > 0) {
|
|
131
|
+
allRows.sort((a, b) => {
|
|
132
|
+
var _a, _b;
|
|
133
|
+
for (const s of sort) {
|
|
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) {
|
|
153
|
+
const query = buildSelectQuery(supabase, table, selectFields, chunkFilters, sort);
|
|
154
|
+
const { data, error } = await query.limit(limit);
|
|
155
|
+
if (error)
|
|
156
|
+
throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
|
|
157
|
+
if (Array.isArray(data)) {
|
|
158
|
+
for (const row of data)
|
|
159
|
+
allRows.push(row);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (allRows.length > limit) {
|
|
163
|
+
allRows.length = limit;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return allRows;
|
|
167
|
+
}
|
|
168
|
+
class SupabaseCsvExport {
|
|
169
|
+
constructor() {
|
|
170
|
+
this.description = {
|
|
171
|
+
displayName: 'Supabase CSV Export',
|
|
172
|
+
name: 'supabaseCsvExport',
|
|
173
|
+
icon: 'file:icons/supabase.svg',
|
|
174
|
+
group: ['output'],
|
|
175
|
+
version: 1,
|
|
176
|
+
subtitle: '={{$parameter["table"]}}',
|
|
177
|
+
description: 'Fetch data from Supabase, transform with JavaScript, and export as CSV file',
|
|
178
|
+
defaults: {
|
|
179
|
+
name: 'Supabase CSV Export',
|
|
180
|
+
},
|
|
181
|
+
inputs: ['main'],
|
|
182
|
+
outputs: ['main'],
|
|
183
|
+
credentials: [
|
|
184
|
+
{
|
|
185
|
+
name: 'supabaseExtendedApi',
|
|
186
|
+
required: true,
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
properties: [
|
|
190
|
+
{
|
|
191
|
+
displayName: 'Table',
|
|
192
|
+
name: 'table',
|
|
193
|
+
type: 'options',
|
|
194
|
+
typeOptions: {
|
|
195
|
+
loadOptionsMethod: 'getTables',
|
|
196
|
+
},
|
|
197
|
+
required: true,
|
|
198
|
+
default: '',
|
|
199
|
+
description: 'Table to export data from',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
displayName: 'Return Fields',
|
|
203
|
+
name: 'returnFields',
|
|
204
|
+
type: 'string',
|
|
205
|
+
default: '*',
|
|
206
|
+
placeholder: 'id,name,email',
|
|
207
|
+
description: 'Comma-separated list of columns to fetch (* for all)',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
displayName: 'Joins',
|
|
211
|
+
name: 'joins',
|
|
212
|
+
type: 'fixedCollection',
|
|
213
|
+
typeOptions: { multipleValues: true },
|
|
214
|
+
default: {},
|
|
215
|
+
placeholder: 'Add Join',
|
|
216
|
+
description: 'Join related tables via foreign keys (PostgREST resource embedding)',
|
|
217
|
+
options: [
|
|
218
|
+
{
|
|
219
|
+
displayName: 'Join',
|
|
220
|
+
name: 'join',
|
|
221
|
+
values: [
|
|
222
|
+
{
|
|
223
|
+
displayName: 'Table',
|
|
224
|
+
name: 'table',
|
|
225
|
+
type: 'string',
|
|
226
|
+
default: '',
|
|
227
|
+
placeholder: 'related_table',
|
|
228
|
+
description: 'Related table to join',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
displayName: 'Columns',
|
|
232
|
+
name: 'columns',
|
|
233
|
+
type: 'string',
|
|
234
|
+
default: '*',
|
|
235
|
+
placeholder: 'col1,col2',
|
|
236
|
+
description: 'Comma-separated columns from the joined table (* for all)',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
displayName: 'Join Type',
|
|
240
|
+
name: 'joinType',
|
|
241
|
+
type: 'options',
|
|
242
|
+
options: [
|
|
243
|
+
{ name: 'Left Join', value: 'left' },
|
|
244
|
+
{ name: 'Inner Join', value: 'inner' },
|
|
245
|
+
],
|
|
246
|
+
default: 'left',
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
displayName: 'UI Mode',
|
|
254
|
+
name: 'uiMode',
|
|
255
|
+
type: 'options',
|
|
256
|
+
options: [
|
|
257
|
+
{ name: 'Simple', value: 'simple', description: 'Use form fields' },
|
|
258
|
+
{ name: 'Advanced', value: 'advanced', description: 'Use JSON filters' },
|
|
259
|
+
],
|
|
260
|
+
default: 'simple',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
displayName: 'Filters',
|
|
264
|
+
name: 'filters',
|
|
265
|
+
type: 'fixedCollection',
|
|
266
|
+
typeOptions: { multipleValues: true },
|
|
267
|
+
default: {},
|
|
268
|
+
placeholder: 'Add Filter',
|
|
269
|
+
displayOptions: { show: { uiMode: ['simple'] } },
|
|
270
|
+
options: [
|
|
271
|
+
{
|
|
272
|
+
displayName: 'Filter',
|
|
273
|
+
name: 'filter',
|
|
274
|
+
values: [
|
|
275
|
+
{
|
|
276
|
+
displayName: 'Column',
|
|
277
|
+
name: 'column',
|
|
278
|
+
type: 'options',
|
|
279
|
+
typeOptions: { loadOptionsMethod: 'getColumns' },
|
|
280
|
+
default: '',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
displayName: 'Operator',
|
|
284
|
+
name: 'operator',
|
|
285
|
+
type: 'options',
|
|
286
|
+
options: [
|
|
287
|
+
{ name: 'Equals', value: 'eq' },
|
|
288
|
+
{ name: 'Not Equals', value: 'neq' },
|
|
289
|
+
{ name: 'Greater Than', value: 'gt' },
|
|
290
|
+
{ name: 'Greater Than or Equal', value: 'gte' },
|
|
291
|
+
{ name: 'Less Than', value: 'lt' },
|
|
292
|
+
{ name: 'Less Than or Equal', value: 'lte' },
|
|
293
|
+
{ name: 'Like', value: 'like' },
|
|
294
|
+
{ name: 'Case Insensitive Like', value: 'ilike' },
|
|
295
|
+
{ name: 'Is', value: 'is' },
|
|
296
|
+
{ name: 'In', value: 'in' },
|
|
297
|
+
{ name: 'Contains', value: 'cs' },
|
|
298
|
+
{ name: 'Contained By', value: 'cd' },
|
|
299
|
+
],
|
|
300
|
+
default: 'eq',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
displayName: 'Value',
|
|
304
|
+
name: 'value',
|
|
305
|
+
type: 'string',
|
|
306
|
+
default: '',
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
displayName: 'Advanced Filters',
|
|
314
|
+
name: 'advancedFilters',
|
|
315
|
+
type: 'json',
|
|
316
|
+
default: '{}',
|
|
317
|
+
description: 'JSON object with filter conditions',
|
|
318
|
+
displayOptions: { show: { uiMode: ['advanced'] } },
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
displayName: 'Sort',
|
|
322
|
+
name: 'sort',
|
|
323
|
+
type: 'fixedCollection',
|
|
324
|
+
typeOptions: { multipleValues: true },
|
|
325
|
+
default: {},
|
|
326
|
+
placeholder: 'Add Sort Field',
|
|
327
|
+
options: [
|
|
328
|
+
{
|
|
329
|
+
displayName: 'Sort Field',
|
|
330
|
+
name: 'sortField',
|
|
331
|
+
values: [
|
|
332
|
+
{
|
|
333
|
+
displayName: 'Column',
|
|
334
|
+
name: 'column',
|
|
335
|
+
type: 'options',
|
|
336
|
+
typeOptions: { loadOptionsMethod: 'getColumns' },
|
|
337
|
+
default: '',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
displayName: 'Ascending',
|
|
341
|
+
name: 'ascending',
|
|
342
|
+
type: 'boolean',
|
|
343
|
+
default: true,
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
displayName: 'Return All',
|
|
351
|
+
name: 'returnAll',
|
|
352
|
+
type: 'boolean',
|
|
353
|
+
default: true,
|
|
354
|
+
description: 'Whether to fetch all matching rows',
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
displayName: 'Limit',
|
|
358
|
+
name: 'limit',
|
|
359
|
+
type: 'number',
|
|
360
|
+
default: 100,
|
|
361
|
+
description: 'Maximum number of rows to fetch',
|
|
362
|
+
displayOptions: { show: { returnAll: [false] } },
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
displayName: 'Transform Data',
|
|
366
|
+
name: 'enableTransform',
|
|
367
|
+
type: 'boolean',
|
|
368
|
+
default: false,
|
|
369
|
+
description: 'Whether to apply a JavaScript transform before generating the CSV',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
displayName: 'Transform Parameters',
|
|
373
|
+
name: 'transformParams',
|
|
374
|
+
type: 'fixedCollection',
|
|
375
|
+
typeOptions: { multipleValues: true },
|
|
376
|
+
default: {},
|
|
377
|
+
placeholder: 'Add Parameter',
|
|
378
|
+
displayOptions: { show: { enableTransform: [true] } },
|
|
379
|
+
description: 'Key-value pairs passed into the transform code as a "params" object. ' +
|
|
380
|
+
'Values support n8n expressions, so you can reference previous nodes ' +
|
|
381
|
+
'(e.g. {{ $json.threshold }} or {{ $(\'My Node\').item.json.value }}).',
|
|
382
|
+
options: [
|
|
383
|
+
{
|
|
384
|
+
displayName: 'Parameter',
|
|
385
|
+
name: 'param',
|
|
386
|
+
values: [
|
|
387
|
+
{
|
|
388
|
+
displayName: 'Name',
|
|
389
|
+
name: 'name',
|
|
390
|
+
type: 'string',
|
|
391
|
+
default: '',
|
|
392
|
+
placeholder: 'threshold',
|
|
393
|
+
description: 'Name used in transform code as params.<name>',
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
displayName: 'Value',
|
|
397
|
+
name: 'value',
|
|
398
|
+
type: 'string',
|
|
399
|
+
default: '',
|
|
400
|
+
placeholder: '={{ $json.minScore }}',
|
|
401
|
+
description: 'Value (supports n8n expressions to reference previous nodes)',
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
displayName: 'Transform Code',
|
|
409
|
+
name: 'transformCode',
|
|
410
|
+
type: 'string',
|
|
411
|
+
typeOptions: {
|
|
412
|
+
editor: 'codeNodeEditor',
|
|
413
|
+
editorLanguage: 'javaScript',
|
|
414
|
+
},
|
|
415
|
+
displayOptions: { show: { enableTransform: [true] } },
|
|
416
|
+
default: '// "rows" — array of objects from the database\n' +
|
|
417
|
+
'// "params" — values passed in from Transform Parameters above\n' +
|
|
418
|
+
'// (use n8n expressions there to reference previous nodes)\n' +
|
|
419
|
+
'\n' +
|
|
420
|
+
'return rows\n' +
|
|
421
|
+
' .filter(row => row.score >= (params.threshold || 0))\n' +
|
|
422
|
+
' .map(row => ({\n' +
|
|
423
|
+
' id: row.id,\n' +
|
|
424
|
+
' name: row.name,\n' +
|
|
425
|
+
' email: row.email,\n' +
|
|
426
|
+
' }));\n',
|
|
427
|
+
description: 'JavaScript code that receives "rows" (array) and "params" (object). ' +
|
|
428
|
+
'Must return an array of objects. Each object becomes a CSV row.',
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
displayName: 'ID Column',
|
|
432
|
+
name: 'idColumn',
|
|
433
|
+
type: 'string',
|
|
434
|
+
default: 'id',
|
|
435
|
+
description: 'Column to extract row IDs from (returned alongside the CSV). ' +
|
|
436
|
+
'Uses the final (post-transform) data — make sure your transform preserves this column.',
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
displayName: 'CSV Options',
|
|
440
|
+
name: 'csvOptions',
|
|
441
|
+
type: 'collection',
|
|
442
|
+
placeholder: 'Add Option',
|
|
443
|
+
default: {},
|
|
444
|
+
options: [
|
|
445
|
+
{
|
|
446
|
+
displayName: 'Delimiter',
|
|
447
|
+
name: 'delimiter',
|
|
448
|
+
type: 'string',
|
|
449
|
+
default: ',',
|
|
450
|
+
description: 'Column separator character',
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
displayName: 'Include Headers',
|
|
454
|
+
name: 'includeHeaders',
|
|
455
|
+
type: 'boolean',
|
|
456
|
+
default: true,
|
|
457
|
+
description: 'Whether to include a header row',
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
displayName: 'File Name',
|
|
461
|
+
name: 'fileName',
|
|
462
|
+
type: 'string',
|
|
463
|
+
default: 'export.csv',
|
|
464
|
+
description: 'Name of the generated CSV file',
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
displayName: 'Quote Character',
|
|
468
|
+
name: 'quoteChar',
|
|
469
|
+
type: 'string',
|
|
470
|
+
default: '"',
|
|
471
|
+
description: 'Character used to quote fields containing special characters',
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
],
|
|
476
|
+
};
|
|
477
|
+
this.methods = {
|
|
478
|
+
loadOptions: {
|
|
479
|
+
async getTables() {
|
|
480
|
+
const credentials = (await this.getCredentials('supabaseExtendedApi'));
|
|
481
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
482
|
+
const host = credentials.host.replace(/\/$/, '');
|
|
483
|
+
try {
|
|
484
|
+
const response = await this.helpers.request({
|
|
485
|
+
method: 'GET',
|
|
486
|
+
url: `${host}/rest/v1/`,
|
|
487
|
+
headers: {
|
|
488
|
+
apikey: credentials.serviceKey,
|
|
489
|
+
Authorization: `Bearer ${credentials.serviceKey}`,
|
|
490
|
+
},
|
|
491
|
+
json: true,
|
|
492
|
+
});
|
|
493
|
+
const definitions = response.definitions || {};
|
|
494
|
+
const tables = Object.keys(definitions).sort();
|
|
495
|
+
if (tables.length === 0) {
|
|
496
|
+
return [
|
|
497
|
+
{
|
|
498
|
+
name: 'No tables found',
|
|
499
|
+
value: '',
|
|
500
|
+
description: 'No tables are exposed via the REST API',
|
|
501
|
+
},
|
|
502
|
+
];
|
|
503
|
+
}
|
|
504
|
+
return tables.map((t) => ({ name: t, value: t }));
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
508
|
+
return [{ name: `Error: ${msg}`, value: '' }];
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
async getColumns() {
|
|
512
|
+
var _a;
|
|
513
|
+
const credentials = (await this.getCredentials('supabaseExtendedApi'));
|
|
514
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
515
|
+
const table = this.getCurrentNodeParameter('table');
|
|
516
|
+
if (!table) {
|
|
517
|
+
return [{ name: 'Select a table first', value: '' }];
|
|
518
|
+
}
|
|
519
|
+
const host = credentials.host.replace(/\/$/, '');
|
|
520
|
+
try {
|
|
521
|
+
const response = await this.helpers.request({
|
|
522
|
+
method: 'GET',
|
|
523
|
+
url: `${host}/rest/v1/`,
|
|
524
|
+
headers: {
|
|
525
|
+
apikey: credentials.serviceKey,
|
|
526
|
+
Authorization: `Bearer ${credentials.serviceKey}`,
|
|
527
|
+
},
|
|
528
|
+
json: true,
|
|
529
|
+
});
|
|
530
|
+
const tableSchema = (_a = response.definitions) === null || _a === void 0 ? void 0 : _a[table];
|
|
531
|
+
if (!(tableSchema === null || tableSchema === void 0 ? void 0 : tableSchema.properties)) {
|
|
532
|
+
return [{ name: 'No columns found', value: '' }];
|
|
533
|
+
}
|
|
534
|
+
return Object.keys(tableSchema.properties)
|
|
535
|
+
.sort()
|
|
536
|
+
.map((col) => {
|
|
537
|
+
const def = tableSchema.properties[col];
|
|
538
|
+
const typeLabel = def.format ? `${def.type} (${def.format})` : def.type;
|
|
539
|
+
return { name: col, value: col, description: typeLabel };
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
544
|
+
return [{ name: `Error: ${msg}`, value: '' }];
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async execute() {
|
|
551
|
+
const credentials = (await this.getCredentials('supabaseExtendedApi'));
|
|
552
|
+
try {
|
|
553
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
557
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid credentials: ${msg}`);
|
|
558
|
+
}
|
|
559
|
+
const supabase = (0, supabaseClient_1.createSupabaseClient)(credentials);
|
|
560
|
+
const table = this.getNodeParameter('table', 0);
|
|
561
|
+
(0, supabaseClient_2.validateTableName)(table);
|
|
562
|
+
const returnFields = this.getNodeParameter('returnFields', 0, '*');
|
|
563
|
+
const returnAll = this.getNodeParameter('returnAll', 0, true);
|
|
564
|
+
const limit = returnAll ? 0 : this.getNodeParameter('limit', 0, 100);
|
|
565
|
+
const filters = parseFilters(this, 0);
|
|
566
|
+
const sort = this.getNodeParameter('sort.sortField', 0, []);
|
|
567
|
+
const joins = this.getNodeParameter('joins.join', 0, []);
|
|
568
|
+
let selectWithJoins = returnFields;
|
|
569
|
+
for (const j of joins) {
|
|
570
|
+
if (!j.table)
|
|
571
|
+
continue;
|
|
572
|
+
const cols = j.columns || '*';
|
|
573
|
+
const hint = j.joinType === 'inner' ? `${j.table}!inner` : j.table;
|
|
574
|
+
selectWithJoins += `,${hint}(${cols})`;
|
|
575
|
+
}
|
|
576
|
+
let rows;
|
|
577
|
+
try {
|
|
578
|
+
rows = await fetchAllRows(supabase, table, selectWithJoins, filters, sort, credentials.host, returnAll, limit);
|
|
579
|
+
}
|
|
580
|
+
catch (error) {
|
|
581
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
582
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to fetch data: ${msg}`);
|
|
583
|
+
}
|
|
584
|
+
const enableTransform = this.getNodeParameter('enableTransform', 0, false);
|
|
585
|
+
if (enableTransform) {
|
|
586
|
+
const paramEntries = this.getNodeParameter('transformParams.param', 0, []);
|
|
587
|
+
const params = {};
|
|
588
|
+
for (const entry of paramEntries) {
|
|
589
|
+
if (entry.name) {
|
|
590
|
+
params[entry.name] = entry.value;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const code = this.getNodeParameter('transformCode', 0, 'return rows;');
|
|
594
|
+
try {
|
|
595
|
+
const transformFn = new Function('rows', 'params', code);
|
|
596
|
+
const result = transformFn(rows, params);
|
|
597
|
+
if (!Array.isArray(result)) {
|
|
598
|
+
throw new Error('Transform code must return an array. Got: ' + typeof result);
|
|
599
|
+
}
|
|
600
|
+
rows = result;
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
604
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Transform code error: ${msg}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const idColumn = this.getNodeParameter('idColumn', 0, 'id');
|
|
608
|
+
const ids = [];
|
|
609
|
+
for (const row of rows) {
|
|
610
|
+
if (row[idColumn] !== undefined && row[idColumn] !== null) {
|
|
611
|
+
ids.push(row[idColumn]);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const csvOpts = this.getNodeParameter('csvOptions', 0, {});
|
|
615
|
+
const csvOptions = {
|
|
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');
|
|
623
|
+
const binaryData = await this.helpers.prepareBinaryData(csvBuffer, csvOptions.fileName, 'text/csv');
|
|
624
|
+
const returnItem = {
|
|
625
|
+
json: {
|
|
626
|
+
table,
|
|
627
|
+
rowCount: rows.length,
|
|
628
|
+
ids,
|
|
629
|
+
fileName: csvOptions.fileName,
|
|
630
|
+
},
|
|
631
|
+
binary: {
|
|
632
|
+
data: binaryData,
|
|
633
|
+
},
|
|
634
|
+
};
|
|
635
|
+
return [[returnItem]];
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
exports.SupabaseCsvExport = SupabaseCsvExport;
|
|
@@ -39,6 +39,9 @@ async function executeDatabaseOperation(supabase, operation, itemIndex, hostUrl)
|
|
|
39
39
|
case 'findOrCreate':
|
|
40
40
|
returnData.push(...await handleFindOrCreate.call(this, supabase, itemIndex));
|
|
41
41
|
break;
|
|
42
|
+
case 'updateByQuery':
|
|
43
|
+
returnData.push(...await handleUpdateByQuery.call(this, supabase, itemIndex, hostUrl));
|
|
44
|
+
break;
|
|
42
45
|
default:
|
|
43
46
|
throw new Error(`Unknown database operation: ${operation}`);
|
|
44
47
|
}
|
|
@@ -49,6 +52,44 @@ async function executeDatabaseOperation(supabase, operation, itemIndex, hostUrl)
|
|
|
49
52
|
return returnData;
|
|
50
53
|
}
|
|
51
54
|
exports.executeDatabaseOperation = executeDatabaseOperation;
|
|
55
|
+
const BULK_BATCH_SIZE = 500;
|
|
56
|
+
const MAX_RETRIES = 3;
|
|
57
|
+
const RETRY_BASE_DELAY_MS = 1000;
|
|
58
|
+
function isRetryableError(msg) {
|
|
59
|
+
const lower = msg.toLowerCase();
|
|
60
|
+
return (lower.includes('lock timeout') ||
|
|
61
|
+
lower.includes('canceling statement due to lock') ||
|
|
62
|
+
lower.includes('deadlock') ||
|
|
63
|
+
lower.includes('too many connections') ||
|
|
64
|
+
lower.includes('rate limit') ||
|
|
65
|
+
lower.includes('could not serialize access') ||
|
|
66
|
+
lower.includes('connection terminated') ||
|
|
67
|
+
lower.includes('connection reset') ||
|
|
68
|
+
lower.includes('econnreset') ||
|
|
69
|
+
lower.includes('timeout'));
|
|
70
|
+
}
|
|
71
|
+
async function withRetry(fn, label) {
|
|
72
|
+
var _a;
|
|
73
|
+
let lastError;
|
|
74
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
75
|
+
try {
|
|
76
|
+
return await fn();
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
const msg = (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : String(err);
|
|
80
|
+
if (attempt < MAX_RETRIES && isRetryableError(msg)) {
|
|
81
|
+
const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
|
|
82
|
+
console.log(`[Supabase ${label}] transient error (attempt ${attempt + 1}/${MAX_RETRIES + 1}), retrying in ${delay}ms: ${msg}`);
|
|
83
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
84
|
+
lastError = err instanceof Error ? err : new Error(msg);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw lastError;
|
|
92
|
+
}
|
|
52
93
|
function sanitizeString(value) {
|
|
53
94
|
return value
|
|
54
95
|
.replace(/\x00/g, '')
|
|
@@ -126,16 +167,26 @@ async function handleBulkCreate(supabase, itemCount) {
|
|
|
126
167
|
const table = this.getNodeParameter('table', 0);
|
|
127
168
|
(0, supabaseClient_1.validateTableName)(table);
|
|
128
169
|
const rows = collectRowData(this, itemCount);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
170
|
+
const returnData = [];
|
|
171
|
+
for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
|
|
172
|
+
const batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
|
|
173
|
+
const data = await withRetry(async () => {
|
|
174
|
+
const { data, error } = await supabase
|
|
175
|
+
.from(table)
|
|
176
|
+
.insert(batch)
|
|
177
|
+
.select();
|
|
178
|
+
if (error)
|
|
179
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
180
|
+
return data;
|
|
181
|
+
}, `CREATE ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`);
|
|
182
|
+
if (Array.isArray(data)) {
|
|
183
|
+
for (const row of data)
|
|
184
|
+
returnData.push({ json: row });
|
|
185
|
+
}
|
|
137
186
|
}
|
|
138
|
-
return
|
|
187
|
+
return returnData.length > 0
|
|
188
|
+
? returnData
|
|
189
|
+
: [{ json: { data: [], operation: 'create', table } }];
|
|
139
190
|
}
|
|
140
191
|
async function handleBulkUpsert(supabase, itemCount) {
|
|
141
192
|
const table = this.getNodeParameter('table', 0);
|
|
@@ -145,16 +196,26 @@ async function handleBulkUpsert(supabase, itemCount) {
|
|
|
145
196
|
const options = {};
|
|
146
197
|
if (onConflict)
|
|
147
198
|
options.onConflict = onConflict;
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
199
|
+
const returnData = [];
|
|
200
|
+
for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
|
|
201
|
+
const batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
|
|
202
|
+
const data = await withRetry(async () => {
|
|
203
|
+
const { data, error } = await supabase
|
|
204
|
+
.from(table)
|
|
205
|
+
.upsert(batch, options)
|
|
206
|
+
.select();
|
|
207
|
+
if (error)
|
|
208
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
209
|
+
return data;
|
|
210
|
+
}, `UPSERT ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`);
|
|
211
|
+
if (Array.isArray(data)) {
|
|
212
|
+
for (const row of data)
|
|
213
|
+
returnData.push({ json: row });
|
|
214
|
+
}
|
|
156
215
|
}
|
|
157
|
-
return
|
|
216
|
+
return returnData.length > 0
|
|
217
|
+
? returnData
|
|
218
|
+
: [{ json: { data: [], operation: 'upsert', table } }];
|
|
158
219
|
}
|
|
159
220
|
async function handleBulkUpdate(supabase, itemCount) {
|
|
160
221
|
const table = this.getNodeParameter('table', 0);
|
|
@@ -170,16 +231,26 @@ async function handleBulkUpdate(supabase, itemCount) {
|
|
|
170
231
|
throw new Error(`Item ${i} is missing the match column "${matchColumn}"`);
|
|
171
232
|
}
|
|
172
233
|
}
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
234
|
+
const returnData = [];
|
|
235
|
+
for (let offset = 0; offset < rows.length; offset += BULK_BATCH_SIZE) {
|
|
236
|
+
const batch = rows.slice(offset, offset + BULK_BATCH_SIZE);
|
|
237
|
+
const data = await withRetry(async () => {
|
|
238
|
+
const { data, error } = await supabase
|
|
239
|
+
.from(table)
|
|
240
|
+
.upsert(batch, { onConflict: matchColumn })
|
|
241
|
+
.select();
|
|
242
|
+
if (error)
|
|
243
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
244
|
+
return data;
|
|
245
|
+
}, `UPDATE ${table} batch ${Math.floor(offset / BULK_BATCH_SIZE) + 1}`);
|
|
246
|
+
if (Array.isArray(data)) {
|
|
247
|
+
for (const row of data)
|
|
248
|
+
returnData.push({ json: row });
|
|
249
|
+
}
|
|
181
250
|
}
|
|
182
|
-
return
|
|
251
|
+
return returnData.length > 0
|
|
252
|
+
? returnData
|
|
253
|
+
: [{ json: { data: [], operation: 'update', table } }];
|
|
183
254
|
}
|
|
184
255
|
function getFilters(context, itemIndex) {
|
|
185
256
|
const uiMode = context.getNodeParameter('uiMode', itemIndex, 'simple');
|
|
@@ -594,3 +665,60 @@ async function handleFindOrCreate(supabase, itemIndex) {
|
|
|
594
665
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(insertError));
|
|
595
666
|
return [{ json: { ...created, _found: false, _created: true } }];
|
|
596
667
|
}
|
|
668
|
+
async function handleUpdateByQuery(supabase, itemIndex, hostUrl) {
|
|
669
|
+
const table = this.getNodeParameter('table', itemIndex);
|
|
670
|
+
(0, supabaseClient_1.validateTableName)(table);
|
|
671
|
+
const uiMode = this.getNodeParameter('uiMode', itemIndex, 'simple');
|
|
672
|
+
let updateData;
|
|
673
|
+
if (uiMode === 'advanced') {
|
|
674
|
+
const jsonData = this.getNodeParameter('jsonData', itemIndex, '{}');
|
|
675
|
+
try {
|
|
676
|
+
updateData = JSON.parse(jsonData);
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
throw new Error('Invalid JSON data for update values');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
const columns = this.getNodeParameter('columns.column', itemIndex, []);
|
|
684
|
+
updateData = {};
|
|
685
|
+
for (const column of columns) {
|
|
686
|
+
if (column.name && column.value !== undefined) {
|
|
687
|
+
updateData[column.name] = column.value;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (Object.keys(updateData).length === 0) {
|
|
692
|
+
throw new Error('At least one column value is required for update');
|
|
693
|
+
}
|
|
694
|
+
sanitizeRow(updateData);
|
|
695
|
+
const filters = getFilters(this, itemIndex);
|
|
696
|
+
if (filters.length === 0) {
|
|
697
|
+
throw new Error('At least one filter is required for Update by Query to prevent accidental full-table updates');
|
|
698
|
+
}
|
|
699
|
+
const overhead = (0, supabaseClient_1.estimateUrlOverhead)(hostUrl, table, undefined, filters);
|
|
700
|
+
const maxInChars = Math.max(500, supabaseClient_1.MAX_SAFE_URL_LENGTH - overhead);
|
|
701
|
+
const filterChunks = (0, supabaseClient_1.expandChunkedFilters)(filters, maxInChars);
|
|
702
|
+
const returnData = [];
|
|
703
|
+
for (const chunkFilters of filterChunks) {
|
|
704
|
+
const data = await withRetry(async () => {
|
|
705
|
+
let query = supabase.from(table).update(updateData);
|
|
706
|
+
for (const filter of chunkFilters) {
|
|
707
|
+
const operator = (0, supabaseClient_1.convertFilterOperator)(filter.operator);
|
|
708
|
+
query = query.filter(filter.column, operator, (0, supabaseClient_1.normalizeFilterValue)(filter.operator, filter.value));
|
|
709
|
+
}
|
|
710
|
+
const { data, error } = await query.select();
|
|
711
|
+
if (error)
|
|
712
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
713
|
+
return data;
|
|
714
|
+
}, `UPDATE_BY_QUERY ${table}`);
|
|
715
|
+
if (Array.isArray(data)) {
|
|
716
|
+
for (const row of data)
|
|
717
|
+
returnData.push({ json: row });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (returnData.length === 0) {
|
|
721
|
+
return [{ json: { data: [], count: 0, operation: 'updateByQuery', table, message: 'No rows matched the filter conditions' } }];
|
|
722
|
+
}
|
|
723
|
+
return returnData;
|
|
724
|
+
}
|
|
@@ -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' | 'findOrCreate';
|
|
78
|
+
export type DatabaseOperation = 'create' | 'read' | 'update' | 'delete' | 'upsert' | 'createTable' | 'dropTable' | 'addColumn' | 'dropColumn' | 'createIndex' | 'dropIndex' | 'customQuery' | 'findOrCreate' | 'updateByQuery';
|
|
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.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Comprehensive n8n community node for Supabase with database and storage operations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
],
|
|
39
39
|
"nodes": [
|
|
40
40
|
"dist/nodes/Supabase/Supabase.node.js",
|
|
41
|
-
"dist/nodes/Supabase/SupabaseTrigger.node.js"
|
|
41
|
+
"dist/nodes/Supabase/SupabaseTrigger.node.js",
|
|
42
|
+
"dist/nodes/Supabase/SupabaseCsvExport.node.js"
|
|
42
43
|
]
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|