@fachkraftfreund/n8n-nodes-supabase 1.2.25 → 1.3.1
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 +659 -0
- package/dist/nodes/Supabase/operations/database/index.js +60 -0
- 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,659 @@
|
|
|
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 escapeRegExp(s) {
|
|
8
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
}
|
|
10
|
+
function escapeCsvField(value, delimiter, quoteChar) {
|
|
11
|
+
if (value === null || value === undefined)
|
|
12
|
+
return '';
|
|
13
|
+
const str = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
14
|
+
if (str.includes(delimiter) ||
|
|
15
|
+
str.includes(quoteChar) ||
|
|
16
|
+
str.includes('\n') ||
|
|
17
|
+
str.includes('\r')) {
|
|
18
|
+
const escaped = quoteChar + str.replace(new RegExp(escapeRegExp(quoteChar), 'g'), quoteChar + quoteChar) + quoteChar;
|
|
19
|
+
return escaped;
|
|
20
|
+
}
|
|
21
|
+
return str;
|
|
22
|
+
}
|
|
23
|
+
function discoverHeaders(rows) {
|
|
24
|
+
const set = new Set();
|
|
25
|
+
for (const row of rows) {
|
|
26
|
+
for (const key of Object.keys(row))
|
|
27
|
+
set.add(key);
|
|
28
|
+
}
|
|
29
|
+
return [...set];
|
|
30
|
+
}
|
|
31
|
+
function batchToCsvBuffer(rows, headers, delimiter, quoteChar) {
|
|
32
|
+
const lines = new Array(rows.length);
|
|
33
|
+
for (let i = 0; i < rows.length; i++) {
|
|
34
|
+
const row = rows[i];
|
|
35
|
+
lines[i] = headers
|
|
36
|
+
.map((h) => escapeCsvField(row[h], delimiter, quoteChar))
|
|
37
|
+
.join(delimiter);
|
|
38
|
+
}
|
|
39
|
+
return Buffer.from(lines.join('\n'), 'utf-8');
|
|
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
|
+
const BATCH_SIZE = 1000;
|
|
79
|
+
async function* fetchBatches(supabase, table, selectFields, filters, sort, hostUrl, returnAll, limit, preferOffset) {
|
|
80
|
+
const overhead = (0, supabaseClient_2.estimateUrlOverhead)(hostUrl, table, selectFields, filters, sort);
|
|
81
|
+
const maxInChars = Math.max(500, supabaseClient_2.MAX_SAFE_URL_LENGTH - overhead);
|
|
82
|
+
const maxItems = (0, supabaseClient_2.computeMaxIdsPerChunk)(selectFields);
|
|
83
|
+
const filterChunks = (0, supabaseClient_2.expandChunkedFilters)(filters, maxInChars, maxItems);
|
|
84
|
+
let totalYielded = 0;
|
|
85
|
+
const maxRows = returnAll ? Infinity : limit;
|
|
86
|
+
for (const chunkFilters of filterChunks) {
|
|
87
|
+
if (totalYielded >= maxRows)
|
|
88
|
+
break;
|
|
89
|
+
if (returnAll) {
|
|
90
|
+
const useKeyset = !preferOffset &&
|
|
91
|
+
(selectFields === '*' || selectFields.split(',').some((f) => f.trim() === 'id'));
|
|
92
|
+
let hasMore = true;
|
|
93
|
+
if (useKeyset) {
|
|
94
|
+
let lastId = null;
|
|
95
|
+
while (hasMore) {
|
|
96
|
+
let query = buildSelectQuery(supabase, table, selectFields, chunkFilters, []);
|
|
97
|
+
if (lastId !== null)
|
|
98
|
+
query = query.gt('id', lastId);
|
|
99
|
+
query = query.order('id', { ascending: true }).limit(BATCH_SIZE);
|
|
100
|
+
const { data, error } = await query;
|
|
101
|
+
if (error)
|
|
102
|
+
throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
|
|
103
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
104
|
+
yield data;
|
|
105
|
+
totalYielded += data.length;
|
|
106
|
+
lastId = data[data.length - 1].id;
|
|
107
|
+
hasMore = data.length === BATCH_SIZE;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
hasMore = false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
let offset = 0;
|
|
116
|
+
while (hasMore) {
|
|
117
|
+
const query = buildSelectQuery(supabase, table, selectFields, chunkFilters, sort);
|
|
118
|
+
const { data, error } = await query.range(offset, offset + BATCH_SIZE - 1);
|
|
119
|
+
if (error)
|
|
120
|
+
throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
|
|
121
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
122
|
+
yield data;
|
|
123
|
+
totalYielded += data.length;
|
|
124
|
+
hasMore = data.length === BATCH_SIZE;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
hasMore = false;
|
|
128
|
+
}
|
|
129
|
+
offset += BATCH_SIZE;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const remaining = maxRows - totalYielded;
|
|
135
|
+
if (remaining <= 0)
|
|
136
|
+
break;
|
|
137
|
+
const query = buildSelectQuery(supabase, table, selectFields, chunkFilters, sort);
|
|
138
|
+
const { data, error } = await query.limit(remaining);
|
|
139
|
+
if (error)
|
|
140
|
+
throw new Error((0, supabaseClient_2.formatSupabaseError)(error));
|
|
141
|
+
if (Array.isArray(data) && data.length > 0) {
|
|
142
|
+
yield data;
|
|
143
|
+
totalYielded += data.length;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
class SupabaseCsvExport {
|
|
149
|
+
constructor() {
|
|
150
|
+
this.description = {
|
|
151
|
+
displayName: 'Supabase CSV Export',
|
|
152
|
+
name: 'supabaseCsvExport',
|
|
153
|
+
icon: 'file:icons/supabase.svg',
|
|
154
|
+
group: ['output'],
|
|
155
|
+
version: 1,
|
|
156
|
+
subtitle: '={{$parameter["table"]}}',
|
|
157
|
+
description: 'Fetch data from Supabase, transform with JavaScript, and export as CSV file',
|
|
158
|
+
defaults: {
|
|
159
|
+
name: 'Supabase CSV Export',
|
|
160
|
+
},
|
|
161
|
+
inputs: ['main'],
|
|
162
|
+
outputs: ['main'],
|
|
163
|
+
credentials: [
|
|
164
|
+
{
|
|
165
|
+
name: 'supabaseExtendedApi',
|
|
166
|
+
required: true,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
properties: [
|
|
170
|
+
{
|
|
171
|
+
displayName: 'Table',
|
|
172
|
+
name: 'table',
|
|
173
|
+
type: 'options',
|
|
174
|
+
typeOptions: {
|
|
175
|
+
loadOptionsMethod: 'getTables',
|
|
176
|
+
},
|
|
177
|
+
required: true,
|
|
178
|
+
default: '',
|
|
179
|
+
description: 'Table to export data from',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
displayName: 'Return Fields',
|
|
183
|
+
name: 'returnFields',
|
|
184
|
+
type: 'string',
|
|
185
|
+
default: '*',
|
|
186
|
+
placeholder: 'id,name,email',
|
|
187
|
+
description: 'Comma-separated list of columns to fetch (* for all)',
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
displayName: 'Joins',
|
|
191
|
+
name: 'joins',
|
|
192
|
+
type: 'fixedCollection',
|
|
193
|
+
typeOptions: { multipleValues: true },
|
|
194
|
+
default: {},
|
|
195
|
+
placeholder: 'Add Join',
|
|
196
|
+
description: 'Join related tables via foreign keys (PostgREST resource embedding)',
|
|
197
|
+
options: [
|
|
198
|
+
{
|
|
199
|
+
displayName: 'Join',
|
|
200
|
+
name: 'join',
|
|
201
|
+
values: [
|
|
202
|
+
{
|
|
203
|
+
displayName: 'Table',
|
|
204
|
+
name: 'table',
|
|
205
|
+
type: 'string',
|
|
206
|
+
default: '',
|
|
207
|
+
placeholder: 'related_table',
|
|
208
|
+
description: 'Related table to join',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
displayName: 'Columns',
|
|
212
|
+
name: 'columns',
|
|
213
|
+
type: 'string',
|
|
214
|
+
default: '*',
|
|
215
|
+
placeholder: 'col1,col2',
|
|
216
|
+
description: 'Comma-separated columns from the joined table (* for all)',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
displayName: 'Join Type',
|
|
220
|
+
name: 'joinType',
|
|
221
|
+
type: 'options',
|
|
222
|
+
options: [
|
|
223
|
+
{ name: 'Left Join', value: 'left' },
|
|
224
|
+
{ name: 'Inner Join', value: 'inner' },
|
|
225
|
+
],
|
|
226
|
+
default: 'left',
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
displayName: 'UI Mode',
|
|
234
|
+
name: 'uiMode',
|
|
235
|
+
type: 'options',
|
|
236
|
+
options: [
|
|
237
|
+
{ name: 'Simple', value: 'simple', description: 'Use form fields' },
|
|
238
|
+
{ name: 'Advanced', value: 'advanced', description: 'Use JSON filters' },
|
|
239
|
+
],
|
|
240
|
+
default: 'simple',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
displayName: 'Filters',
|
|
244
|
+
name: 'filters',
|
|
245
|
+
type: 'fixedCollection',
|
|
246
|
+
typeOptions: { multipleValues: true },
|
|
247
|
+
default: {},
|
|
248
|
+
placeholder: 'Add Filter',
|
|
249
|
+
displayOptions: { show: { uiMode: ['simple'] } },
|
|
250
|
+
options: [
|
|
251
|
+
{
|
|
252
|
+
displayName: 'Filter',
|
|
253
|
+
name: 'filter',
|
|
254
|
+
values: [
|
|
255
|
+
{
|
|
256
|
+
displayName: 'Column',
|
|
257
|
+
name: 'column',
|
|
258
|
+
type: 'options',
|
|
259
|
+
typeOptions: { loadOptionsMethod: 'getColumns' },
|
|
260
|
+
default: '',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
displayName: 'Operator',
|
|
264
|
+
name: 'operator',
|
|
265
|
+
type: 'options',
|
|
266
|
+
options: [
|
|
267
|
+
{ name: 'Equals', value: 'eq' },
|
|
268
|
+
{ name: 'Not Equals', value: 'neq' },
|
|
269
|
+
{ name: 'Greater Than', value: 'gt' },
|
|
270
|
+
{ name: 'Greater Than or Equal', value: 'gte' },
|
|
271
|
+
{ name: 'Less Than', value: 'lt' },
|
|
272
|
+
{ name: 'Less Than or Equal', value: 'lte' },
|
|
273
|
+
{ name: 'Like', value: 'like' },
|
|
274
|
+
{ name: 'Case Insensitive Like', value: 'ilike' },
|
|
275
|
+
{ name: 'Is', value: 'is' },
|
|
276
|
+
{ name: 'In', value: 'in' },
|
|
277
|
+
{ name: 'Contains', value: 'cs' },
|
|
278
|
+
{ name: 'Contained By', value: 'cd' },
|
|
279
|
+
],
|
|
280
|
+
default: 'eq',
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
displayName: 'Value',
|
|
284
|
+
name: 'value',
|
|
285
|
+
type: 'string',
|
|
286
|
+
default: '',
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
displayName: 'Advanced Filters',
|
|
294
|
+
name: 'advancedFilters',
|
|
295
|
+
type: 'json',
|
|
296
|
+
default: '{}',
|
|
297
|
+
description: 'JSON object with filter conditions',
|
|
298
|
+
displayOptions: { show: { uiMode: ['advanced'] } },
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
displayName: 'Sort',
|
|
302
|
+
name: 'sort',
|
|
303
|
+
type: 'fixedCollection',
|
|
304
|
+
typeOptions: { multipleValues: true },
|
|
305
|
+
default: {},
|
|
306
|
+
placeholder: 'Add Sort Field',
|
|
307
|
+
options: [
|
|
308
|
+
{
|
|
309
|
+
displayName: 'Sort Field',
|
|
310
|
+
name: 'sortField',
|
|
311
|
+
values: [
|
|
312
|
+
{
|
|
313
|
+
displayName: 'Column',
|
|
314
|
+
name: 'column',
|
|
315
|
+
type: 'options',
|
|
316
|
+
typeOptions: { loadOptionsMethod: 'getColumns' },
|
|
317
|
+
default: '',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
displayName: 'Ascending',
|
|
321
|
+
name: 'ascending',
|
|
322
|
+
type: 'boolean',
|
|
323
|
+
default: true,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
displayName: 'Return All',
|
|
331
|
+
name: 'returnAll',
|
|
332
|
+
type: 'boolean',
|
|
333
|
+
default: true,
|
|
334
|
+
description: 'Whether to fetch all matching rows',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
displayName: 'Limit',
|
|
338
|
+
name: 'limit',
|
|
339
|
+
type: 'number',
|
|
340
|
+
default: 100,
|
|
341
|
+
description: 'Maximum number of rows to fetch',
|
|
342
|
+
displayOptions: { show: { returnAll: [false] } },
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
displayName: 'Transform Data',
|
|
346
|
+
name: 'enableTransform',
|
|
347
|
+
type: 'boolean',
|
|
348
|
+
default: false,
|
|
349
|
+
description: 'Whether to apply a JavaScript transform before generating the CSV. ' +
|
|
350
|
+
'The transform runs per batch (~1000 rows) so it stays memory-efficient ' +
|
|
351
|
+
'even for very large exports. Use .filter() and .map() to shape your data.',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
displayName: 'Transform Parameters',
|
|
355
|
+
name: 'transformParams',
|
|
356
|
+
type: 'fixedCollection',
|
|
357
|
+
typeOptions: { multipleValues: true },
|
|
358
|
+
default: {},
|
|
359
|
+
placeholder: 'Add Parameter',
|
|
360
|
+
displayOptions: { show: { enableTransform: [true] } },
|
|
361
|
+
description: 'Key-value pairs passed into the transform code as a "params" object. ' +
|
|
362
|
+
'Values support n8n expressions, so you can reference previous nodes ' +
|
|
363
|
+
'(e.g. {{ $json.threshold }} or {{ $(\'My Node\').item.json.value }}).',
|
|
364
|
+
options: [
|
|
365
|
+
{
|
|
366
|
+
displayName: 'Parameter',
|
|
367
|
+
name: 'param',
|
|
368
|
+
values: [
|
|
369
|
+
{
|
|
370
|
+
displayName: 'Name',
|
|
371
|
+
name: 'name',
|
|
372
|
+
type: 'string',
|
|
373
|
+
default: '',
|
|
374
|
+
placeholder: 'threshold',
|
|
375
|
+
description: 'Name used in transform code as params.<name>',
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
displayName: 'Value',
|
|
379
|
+
name: 'value',
|
|
380
|
+
type: 'string',
|
|
381
|
+
default: '',
|
|
382
|
+
placeholder: '={{ $json.minScore }}',
|
|
383
|
+
description: 'Value (supports n8n expressions to reference previous nodes)',
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
displayName: 'Transform Code',
|
|
391
|
+
name: 'transformCode',
|
|
392
|
+
type: 'string',
|
|
393
|
+
typeOptions: {
|
|
394
|
+
editor: 'codeNodeEditor',
|
|
395
|
+
editorLanguage: 'javaScript',
|
|
396
|
+
},
|
|
397
|
+
displayOptions: { show: { enableTransform: [true] } },
|
|
398
|
+
default: '// "rows" — array of objects from the database\n' +
|
|
399
|
+
'// "params" — values passed in from Transform Parameters above\n' +
|
|
400
|
+
'// (use n8n expressions there to reference previous nodes)\n' +
|
|
401
|
+
'\n' +
|
|
402
|
+
'return rows\n' +
|
|
403
|
+
' .filter(row => row.score >= (params.threshold || 0))\n' +
|
|
404
|
+
' .map(row => ({\n' +
|
|
405
|
+
' id: row.id,\n' +
|
|
406
|
+
' name: row.name,\n' +
|
|
407
|
+
' email: row.email,\n' +
|
|
408
|
+
' }));\n',
|
|
409
|
+
description: 'JavaScript code that receives "rows" (array) and "params" (object). ' +
|
|
410
|
+
'Must return an array of objects. Each object becomes a CSV row.',
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
displayName: 'ID Column',
|
|
414
|
+
name: 'idColumn',
|
|
415
|
+
type: 'string',
|
|
416
|
+
default: 'id',
|
|
417
|
+
description: 'Column to extract row IDs from (returned alongside the CSV). ' +
|
|
418
|
+
'Uses the final (post-transform) data — make sure your transform preserves this column.',
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
displayName: 'CSV Options',
|
|
422
|
+
name: 'csvOptions',
|
|
423
|
+
type: 'collection',
|
|
424
|
+
placeholder: 'Add Option',
|
|
425
|
+
default: {},
|
|
426
|
+
options: [
|
|
427
|
+
{
|
|
428
|
+
displayName: 'Delimiter',
|
|
429
|
+
name: 'delimiter',
|
|
430
|
+
type: 'string',
|
|
431
|
+
default: ',',
|
|
432
|
+
description: 'Column separator character',
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
displayName: 'Include Headers',
|
|
436
|
+
name: 'includeHeaders',
|
|
437
|
+
type: 'boolean',
|
|
438
|
+
default: true,
|
|
439
|
+
description: 'Whether to include a header row',
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
displayName: 'File Name',
|
|
443
|
+
name: 'fileName',
|
|
444
|
+
type: 'string',
|
|
445
|
+
default: 'export.csv',
|
|
446
|
+
description: 'Name of the generated CSV file',
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
displayName: 'Quote Character',
|
|
450
|
+
name: 'quoteChar',
|
|
451
|
+
type: 'string',
|
|
452
|
+
default: '"',
|
|
453
|
+
description: 'Character used to quote fields containing special characters',
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
};
|
|
459
|
+
this.methods = {
|
|
460
|
+
loadOptions: {
|
|
461
|
+
async getTables() {
|
|
462
|
+
const credentials = (await this.getCredentials('supabaseExtendedApi'));
|
|
463
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
464
|
+
const host = credentials.host.replace(/\/$/, '');
|
|
465
|
+
try {
|
|
466
|
+
const response = await this.helpers.request({
|
|
467
|
+
method: 'GET',
|
|
468
|
+
url: `${host}/rest/v1/`,
|
|
469
|
+
headers: {
|
|
470
|
+
apikey: credentials.serviceKey,
|
|
471
|
+
Authorization: `Bearer ${credentials.serviceKey}`,
|
|
472
|
+
},
|
|
473
|
+
json: true,
|
|
474
|
+
});
|
|
475
|
+
const definitions = response.definitions || {};
|
|
476
|
+
const tables = Object.keys(definitions).sort();
|
|
477
|
+
if (tables.length === 0) {
|
|
478
|
+
return [
|
|
479
|
+
{
|
|
480
|
+
name: 'No tables found',
|
|
481
|
+
value: '',
|
|
482
|
+
description: 'No tables are exposed via the REST API',
|
|
483
|
+
},
|
|
484
|
+
];
|
|
485
|
+
}
|
|
486
|
+
return tables.map((t) => ({ name: t, value: t }));
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
490
|
+
return [{ name: `Error: ${msg}`, value: '' }];
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
async getColumns() {
|
|
494
|
+
var _a;
|
|
495
|
+
const credentials = (await this.getCredentials('supabaseExtendedApi'));
|
|
496
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
497
|
+
const table = this.getCurrentNodeParameter('table');
|
|
498
|
+
if (!table) {
|
|
499
|
+
return [{ name: 'Select a table first', value: '' }];
|
|
500
|
+
}
|
|
501
|
+
const host = credentials.host.replace(/\/$/, '');
|
|
502
|
+
try {
|
|
503
|
+
const response = await this.helpers.request({
|
|
504
|
+
method: 'GET',
|
|
505
|
+
url: `${host}/rest/v1/`,
|
|
506
|
+
headers: {
|
|
507
|
+
apikey: credentials.serviceKey,
|
|
508
|
+
Authorization: `Bearer ${credentials.serviceKey}`,
|
|
509
|
+
},
|
|
510
|
+
json: true,
|
|
511
|
+
});
|
|
512
|
+
const tableSchema = (_a = response.definitions) === null || _a === void 0 ? void 0 : _a[table];
|
|
513
|
+
if (!(tableSchema === null || tableSchema === void 0 ? void 0 : tableSchema.properties)) {
|
|
514
|
+
return [{ name: 'No columns found', value: '' }];
|
|
515
|
+
}
|
|
516
|
+
return Object.keys(tableSchema.properties)
|
|
517
|
+
.sort()
|
|
518
|
+
.map((col) => {
|
|
519
|
+
const def = tableSchema.properties[col];
|
|
520
|
+
const typeLabel = def.format ? `${def.type} (${def.format})` : def.type;
|
|
521
|
+
return { name: col, value: col, description: typeLabel };
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
526
|
+
return [{ name: `Error: ${msg}`, value: '' }];
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
async execute() {
|
|
533
|
+
const credentials = (await this.getCredentials('supabaseExtendedApi'));
|
|
534
|
+
try {
|
|
535
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
539
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid credentials: ${msg}`);
|
|
540
|
+
}
|
|
541
|
+
const supabase = (0, supabaseClient_1.createSupabaseClient)(credentials);
|
|
542
|
+
const table = this.getNodeParameter('table', 0);
|
|
543
|
+
(0, supabaseClient_2.validateTableName)(table);
|
|
544
|
+
const returnFields = this.getNodeParameter('returnFields', 0, '*');
|
|
545
|
+
const returnAll = this.getNodeParameter('returnAll', 0, true);
|
|
546
|
+
const limit = returnAll ? 0 : this.getNodeParameter('limit', 0, 100);
|
|
547
|
+
const filters = parseFilters(this, 0);
|
|
548
|
+
const sort = this.getNodeParameter('sort.sortField', 0, []);
|
|
549
|
+
const enableTransform = this.getNodeParameter('enableTransform', 0, false);
|
|
550
|
+
const idColumn = this.getNodeParameter('idColumn', 0, 'id');
|
|
551
|
+
const csvOpts = this.getNodeParameter('csvOptions', 0, {});
|
|
552
|
+
const csvOptions = {
|
|
553
|
+
delimiter: csvOpts.delimiter || ',',
|
|
554
|
+
quoteChar: csvOpts.quoteChar || '"',
|
|
555
|
+
includeHeaders: csvOpts.includeHeaders !== false,
|
|
556
|
+
fileName: csvOpts.fileName || 'export.csv',
|
|
557
|
+
};
|
|
558
|
+
const joins = this.getNodeParameter('joins.join', 0, []);
|
|
559
|
+
let selectWithJoins = returnFields;
|
|
560
|
+
for (const j of joins) {
|
|
561
|
+
if (!j.table)
|
|
562
|
+
continue;
|
|
563
|
+
const cols = j.columns || '*';
|
|
564
|
+
const hint = j.joinType === 'inner' ? `${j.table}!inner` : j.table;
|
|
565
|
+
selectWithJoins += `,${hint}(${cols})`;
|
|
566
|
+
}
|
|
567
|
+
const { delimiter, quoteChar } = csvOptions;
|
|
568
|
+
const csvChunks = [];
|
|
569
|
+
const ids = [];
|
|
570
|
+
let rowCount = 0;
|
|
571
|
+
let headers = null;
|
|
572
|
+
try {
|
|
573
|
+
let transformFn = null;
|
|
574
|
+
let params = {};
|
|
575
|
+
if (enableTransform) {
|
|
576
|
+
const paramEntries = this.getNodeParameter('transformParams.param', 0, []);
|
|
577
|
+
for (const entry of paramEntries) {
|
|
578
|
+
if (entry.name)
|
|
579
|
+
params[entry.name] = entry.value;
|
|
580
|
+
}
|
|
581
|
+
const code = this.getNodeParameter('transformCode', 0, 'return rows;');
|
|
582
|
+
try {
|
|
583
|
+
transformFn = new Function('rows', 'params', code);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
587
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Transform code syntax error: ${msg}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const preferOffset = sort.length > 0;
|
|
591
|
+
for await (const batch of fetchBatches(supabase, table, selectWithJoins, filters, sort, credentials.host, returnAll, limit, preferOffset)) {
|
|
592
|
+
let rows = batch;
|
|
593
|
+
if (transformFn) {
|
|
594
|
+
try {
|
|
595
|
+
const result = transformFn(batch, params);
|
|
596
|
+
if (!Array.isArray(result)) {
|
|
597
|
+
throw new Error('Transform code must return an array. Got: ' + typeof result);
|
|
598
|
+
}
|
|
599
|
+
rows = result;
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
if (error instanceof n8n_workflow_1.NodeOperationError)
|
|
603
|
+
throw error;
|
|
604
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
605
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Transform code error: ${msg}`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
if (rows.length === 0)
|
|
609
|
+
continue;
|
|
610
|
+
if (headers === null) {
|
|
611
|
+
headers = discoverHeaders(rows);
|
|
612
|
+
if (csvOptions.includeHeaders) {
|
|
613
|
+
const headerLine = headers
|
|
614
|
+
.map((h) => escapeCsvField(h, delimiter, quoteChar))
|
|
615
|
+
.join(delimiter);
|
|
616
|
+
csvChunks.push(Buffer.from(headerLine + '\n', 'utf-8'));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
for (const row of rows) {
|
|
620
|
+
if (row[idColumn] != null)
|
|
621
|
+
ids.push(row[idColumn]);
|
|
622
|
+
}
|
|
623
|
+
const buf = batchToCsvBuffer(rows, headers, delimiter, quoteChar);
|
|
624
|
+
csvChunks.push(buf);
|
|
625
|
+
csvChunks.push(Buffer.from('\n', 'utf-8'));
|
|
626
|
+
rowCount += rows.length;
|
|
627
|
+
}
|
|
628
|
+
if (csvChunks.length > 0) {
|
|
629
|
+
const last = csvChunks[csvChunks.length - 1];
|
|
630
|
+
if (last.length === 1 && last[0] === 0x0a) {
|
|
631
|
+
csvChunks.pop();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
637
|
+
if (error instanceof n8n_workflow_1.NodeOperationError)
|
|
638
|
+
throw error;
|
|
639
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Export failed: ${msg}`);
|
|
640
|
+
}
|
|
641
|
+
const csvBuffer = Buffer.concat(csvChunks);
|
|
642
|
+
csvChunks.length = 0;
|
|
643
|
+
const binaryData = await this.helpers.prepareBinaryData(csvBuffer, csvOptions.fileName, 'text/csv');
|
|
644
|
+
return [[
|
|
645
|
+
{
|
|
646
|
+
json: {
|
|
647
|
+
table,
|
|
648
|
+
rowCount,
|
|
649
|
+
ids,
|
|
650
|
+
fileName: csvOptions.fileName,
|
|
651
|
+
},
|
|
652
|
+
binary: {
|
|
653
|
+
data: binaryData,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
]];
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
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
|
}
|
|
@@ -662,3 +665,60 @@ async function handleFindOrCreate(supabase, itemIndex) {
|
|
|
662
665
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(insertError));
|
|
663
666
|
return [{ json: { ...created, _found: false, _created: true } }];
|
|
664
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.1",
|
|
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": {
|