@fachkraftfreund/n8n-nodes-supabase 1.2.7 → 1.2.9
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 +64 -24
- package/dist/nodes/Supabase/SupabaseTrigger.node.d.ts +11 -0
- package/dist/nodes/Supabase/SupabaseTrigger.node.js +312 -0
- package/dist/nodes/Supabase/operations/database/index.d.ts +1 -0
- package/dist/nodes/Supabase/operations/database/index.js +113 -154
- package/package.json +3 -2
|
@@ -335,6 +335,23 @@ class Supabase {
|
|
|
335
335
|
},
|
|
336
336
|
},
|
|
337
337
|
},
|
|
338
|
+
{
|
|
339
|
+
displayName: 'Match Column',
|
|
340
|
+
name: 'matchColumn',
|
|
341
|
+
type: 'options',
|
|
342
|
+
typeOptions: {
|
|
343
|
+
loadOptionsMethod: 'getColumns',
|
|
344
|
+
},
|
|
345
|
+
required: true,
|
|
346
|
+
default: '',
|
|
347
|
+
description: 'Column used to match rows for updating (typically the primary key). Each input item must include this column in its data. Uses upsert internally, so rows will be created if no match is found.',
|
|
348
|
+
displayOptions: {
|
|
349
|
+
show: {
|
|
350
|
+
resource: ['database'],
|
|
351
|
+
operation: ['update'],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
},
|
|
338
355
|
{
|
|
339
356
|
displayName: 'Match Columns',
|
|
340
357
|
name: 'matchColumns',
|
|
@@ -431,7 +448,7 @@ class Supabase {
|
|
|
431
448
|
displayOptions: {
|
|
432
449
|
show: {
|
|
433
450
|
resource: ['database'],
|
|
434
|
-
operation: ['read', '
|
|
451
|
+
operation: ['read', 'delete'],
|
|
435
452
|
uiMode: ['simple'],
|
|
436
453
|
},
|
|
437
454
|
},
|
|
@@ -491,7 +508,7 @@ class Supabase {
|
|
|
491
508
|
displayOptions: {
|
|
492
509
|
show: {
|
|
493
510
|
resource: ['database'],
|
|
494
|
-
operation: ['read', '
|
|
511
|
+
operation: ['read', 'delete'],
|
|
495
512
|
uiMode: ['advanced'],
|
|
496
513
|
},
|
|
497
514
|
},
|
|
@@ -874,6 +891,12 @@ class Supabase {
|
|
|
874
891
|
async execute() {
|
|
875
892
|
const items = this.getInputData();
|
|
876
893
|
const returnData = [];
|
|
894
|
+
const firstItem = items[0];
|
|
895
|
+
if (items.length === 1 &&
|
|
896
|
+
(firstItem === null || firstItem === void 0 ? void 0 : firstItem.json) &&
|
|
897
|
+
Object.keys(firstItem.json).length === 0) {
|
|
898
|
+
return [[{ json: {} }]];
|
|
899
|
+
}
|
|
877
900
|
const credentials = await this.getCredentials('supabaseExtendedApi');
|
|
878
901
|
try {
|
|
879
902
|
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
@@ -883,34 +906,51 @@ class Supabase {
|
|
|
883
906
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Invalid credentials: ${errorMessage}`);
|
|
884
907
|
}
|
|
885
908
|
const supabase = (0, supabaseClient_1.createSupabaseClient)(credentials);
|
|
886
|
-
|
|
909
|
+
const resource = this.getNodeParameter('resource', 0);
|
|
910
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
911
|
+
if (resource === 'database' && ['create', 'upsert', 'update'].includes(operation)) {
|
|
887
912
|
try {
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
let operationResults = [];
|
|
891
|
-
if (resource === 'database') {
|
|
892
|
-
operationResults = await database_1.executeDatabaseOperation.call(this, supabase, operation, itemIndex);
|
|
893
|
-
}
|
|
894
|
-
else if (resource === 'storage') {
|
|
895
|
-
operationResults = await storage_1.executeStorageOperation.call(this, supabase, operation, itemIndex);
|
|
896
|
-
}
|
|
897
|
-
else {
|
|
898
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown resource: ${resource}`);
|
|
899
|
-
}
|
|
900
|
-
returnData.push(...operationResults);
|
|
913
|
+
const results = await database_1.executeBulkDatabaseOperation.call(this, supabase, operation, items.length);
|
|
914
|
+
returnData.push(...results);
|
|
901
915
|
}
|
|
902
916
|
catch (error) {
|
|
903
917
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
904
918
|
if (this.continueOnFail()) {
|
|
905
|
-
returnData.push({
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
919
|
+
returnData.push({ json: { error: errorMessage } });
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
928
|
+
try {
|
|
929
|
+
let operationResults = [];
|
|
930
|
+
if (resource === 'database') {
|
|
931
|
+
operationResults = await database_1.executeDatabaseOperation.call(this, supabase, operation, itemIndex);
|
|
932
|
+
}
|
|
933
|
+
else if (resource === 'storage') {
|
|
934
|
+
operationResults = await storage_1.executeStorageOperation.call(this, supabase, operation, itemIndex);
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown resource: ${resource}`);
|
|
938
|
+
}
|
|
939
|
+
returnData.push(...operationResults);
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
943
|
+
if (this.continueOnFail()) {
|
|
944
|
+
returnData.push({
|
|
945
|
+
json: {
|
|
946
|
+
error: errorMessage,
|
|
947
|
+
itemIndex,
|
|
948
|
+
},
|
|
949
|
+
});
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage, { itemIndex });
|
|
912
953
|
}
|
|
913
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), errorMessage, { itemIndex });
|
|
914
954
|
}
|
|
915
955
|
}
|
|
916
956
|
return [returnData];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription, IPollFunctions } from 'n8n-workflow';
|
|
2
|
+
export declare class SupabaseTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
|
|
5
|
+
methods: {
|
|
6
|
+
loadOptions: {
|
|
7
|
+
getTables(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
8
|
+
getColumns(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SupabaseTrigger = void 0;
|
|
4
|
+
const supabaseClient_1 = require("./utils/supabaseClient");
|
|
5
|
+
class SupabaseTrigger {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.description = {
|
|
8
|
+
displayName: 'Supabase Trigger',
|
|
9
|
+
name: 'supabaseExtendedTrigger',
|
|
10
|
+
icon: 'file:icons/supabase.svg',
|
|
11
|
+
group: ['trigger'],
|
|
12
|
+
version: 1,
|
|
13
|
+
subtitle: '={{$parameter["event"]}}',
|
|
14
|
+
description: 'Triggers when rows are created or updated in a Supabase table',
|
|
15
|
+
polling: true,
|
|
16
|
+
defaults: {
|
|
17
|
+
name: 'Supabase Trigger',
|
|
18
|
+
},
|
|
19
|
+
inputs: [],
|
|
20
|
+
outputs: ['main'],
|
|
21
|
+
credentials: [
|
|
22
|
+
{
|
|
23
|
+
name: 'supabaseExtendedApi',
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
properties: [
|
|
28
|
+
{
|
|
29
|
+
displayName: 'Table',
|
|
30
|
+
name: 'table',
|
|
31
|
+
type: 'options',
|
|
32
|
+
typeOptions: {
|
|
33
|
+
loadOptionsMethod: 'getTables',
|
|
34
|
+
},
|
|
35
|
+
required: true,
|
|
36
|
+
default: '',
|
|
37
|
+
description: 'Table to watch for changes',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
displayName: 'Event',
|
|
41
|
+
name: 'event',
|
|
42
|
+
type: 'options',
|
|
43
|
+
options: [
|
|
44
|
+
{
|
|
45
|
+
name: 'Row Created',
|
|
46
|
+
value: 'rowCreated',
|
|
47
|
+
description: 'Trigger when new rows are inserted',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Row Updated',
|
|
51
|
+
value: 'rowUpdated',
|
|
52
|
+
description: 'Trigger when existing rows are updated',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Row Created or Updated',
|
|
56
|
+
value: 'rowCreatedOrUpdated',
|
|
57
|
+
description: 'Trigger on both inserts and updates',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
default: 'rowCreated',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
displayName: 'Timestamp Column',
|
|
64
|
+
name: 'timestampColumn',
|
|
65
|
+
type: 'options',
|
|
66
|
+
typeOptions: {
|
|
67
|
+
loadOptionsMethod: 'getColumns',
|
|
68
|
+
},
|
|
69
|
+
required: true,
|
|
70
|
+
default: '',
|
|
71
|
+
description: 'Column that tracks when the row was created or last updated',
|
|
72
|
+
displayOptions: {
|
|
73
|
+
show: {
|
|
74
|
+
event: ['rowCreated', 'rowUpdated'],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
displayName: 'Created At Column',
|
|
80
|
+
name: 'createdAtColumn',
|
|
81
|
+
type: 'options',
|
|
82
|
+
typeOptions: {
|
|
83
|
+
loadOptionsMethod: 'getColumns',
|
|
84
|
+
},
|
|
85
|
+
required: true,
|
|
86
|
+
default: '',
|
|
87
|
+
description: 'Column that stores row creation time',
|
|
88
|
+
displayOptions: {
|
|
89
|
+
show: {
|
|
90
|
+
event: ['rowCreatedOrUpdated'],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
displayName: 'Updated At Column',
|
|
96
|
+
name: 'updatedAtColumn',
|
|
97
|
+
type: 'options',
|
|
98
|
+
typeOptions: {
|
|
99
|
+
loadOptionsMethod: 'getColumns',
|
|
100
|
+
},
|
|
101
|
+
required: true,
|
|
102
|
+
default: '',
|
|
103
|
+
description: 'Column that stores row last-update time',
|
|
104
|
+
displayOptions: {
|
|
105
|
+
show: {
|
|
106
|
+
event: ['rowCreatedOrUpdated'],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
displayName: 'Key Column',
|
|
112
|
+
name: 'keyColumn',
|
|
113
|
+
type: 'options',
|
|
114
|
+
typeOptions: {
|
|
115
|
+
loadOptionsMethod: 'getColumns',
|
|
116
|
+
},
|
|
117
|
+
required: true,
|
|
118
|
+
default: '',
|
|
119
|
+
description: 'Unique identifier column (e.g. id) used for deduplication and field-change tracking',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
displayName: 'Watched Fields',
|
|
123
|
+
name: 'watchedFields',
|
|
124
|
+
type: 'multiOptions',
|
|
125
|
+
typeOptions: {
|
|
126
|
+
loadOptionsMethod: 'getColumns',
|
|
127
|
+
},
|
|
128
|
+
default: [],
|
|
129
|
+
description: 'Only trigger when these specific fields change. Leave empty to trigger on any change.',
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
this.methods = {
|
|
134
|
+
loadOptions: {
|
|
135
|
+
async getTables() {
|
|
136
|
+
const credentials = await this.getCredentials('supabaseExtendedApi');
|
|
137
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
138
|
+
const host = credentials.host.replace(/\/$/, '');
|
|
139
|
+
try {
|
|
140
|
+
const response = await this.helpers.request({
|
|
141
|
+
method: 'GET',
|
|
142
|
+
url: `${host}/rest/v1/`,
|
|
143
|
+
headers: {
|
|
144
|
+
apikey: credentials.serviceKey,
|
|
145
|
+
Authorization: `Bearer ${credentials.serviceKey}`,
|
|
146
|
+
},
|
|
147
|
+
json: true,
|
|
148
|
+
});
|
|
149
|
+
const definitions = response.definitions || {};
|
|
150
|
+
const tables = Object.keys(definitions).sort();
|
|
151
|
+
if (tables.length === 0) {
|
|
152
|
+
return [{ name: 'No tables found', value: '', description: 'No tables are exposed via the REST API' }];
|
|
153
|
+
}
|
|
154
|
+
return tables.map((t) => ({ name: t, value: t }));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
158
|
+
return [{ name: `Error: ${msg}`, value: '', description: 'Failed to load tables. Check your credentials.' }];
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
async getColumns() {
|
|
162
|
+
const credentials = await this.getCredentials('supabaseExtendedApi');
|
|
163
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
164
|
+
const table = this.getCurrentNodeParameter('table');
|
|
165
|
+
if (!table) {
|
|
166
|
+
return [{ name: 'Select a table first', value: '', description: 'Choose a table to load its columns' }];
|
|
167
|
+
}
|
|
168
|
+
const host = credentials.host.replace(/\/$/, '');
|
|
169
|
+
try {
|
|
170
|
+
const response = await this.helpers.request({
|
|
171
|
+
method: 'GET',
|
|
172
|
+
url: `${host}/rest/v1/`,
|
|
173
|
+
headers: {
|
|
174
|
+
apikey: credentials.serviceKey,
|
|
175
|
+
Authorization: `Bearer ${credentials.serviceKey}`,
|
|
176
|
+
},
|
|
177
|
+
json: true,
|
|
178
|
+
});
|
|
179
|
+
const definitions = response.definitions || {};
|
|
180
|
+
const tableSchema = definitions[table];
|
|
181
|
+
if (!(tableSchema === null || tableSchema === void 0 ? void 0 : tableSchema.properties)) {
|
|
182
|
+
return [{ name: 'No columns found', value: '', description: `Table "${table}" not found or has no columns` }];
|
|
183
|
+
}
|
|
184
|
+
return Object.keys(tableSchema.properties).sort().map((col) => {
|
|
185
|
+
const colDef = tableSchema.properties[col];
|
|
186
|
+
const typeLabel = colDef.format ? `${colDef.type} (${colDef.format})` : colDef.type;
|
|
187
|
+
return { name: col, value: col, description: typeLabel };
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
192
|
+
return [{ name: `Error: ${msg}`, value: '', description: 'Failed to load columns. Check your credentials.' }];
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async poll() {
|
|
199
|
+
const credentials = await this.getCredentials('supabaseExtendedApi');
|
|
200
|
+
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
201
|
+
const supabase = (0, supabaseClient_1.createSupabaseClient)(credentials);
|
|
202
|
+
const table = this.getNodeParameter('table');
|
|
203
|
+
const event = this.getNodeParameter('event');
|
|
204
|
+
const keyColumn = this.getNodeParameter('keyColumn');
|
|
205
|
+
const watchedFields = this.getNodeParameter('watchedFields', []);
|
|
206
|
+
const staticData = this.getWorkflowStaticData('node');
|
|
207
|
+
if (!staticData.lastTimestamp) {
|
|
208
|
+
if (this.getMode() === 'manual') {
|
|
209
|
+
const lookback = new Date();
|
|
210
|
+
lookback.setHours(lookback.getHours() - 24);
|
|
211
|
+
staticData.lastTimestamp = lookback.toISOString();
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
staticData.lastTimestamp = new Date().toISOString();
|
|
215
|
+
staticData.fieldStates = {};
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const lastTimestamp = staticData.lastTimestamp;
|
|
220
|
+
let query = supabase.from(table).select('*');
|
|
221
|
+
if (event === 'rowCreatedOrUpdated') {
|
|
222
|
+
const createdAtCol = this.getNodeParameter('createdAtColumn');
|
|
223
|
+
const updatedAtCol = this.getNodeParameter('updatedAtColumn');
|
|
224
|
+
query = query.or(`${createdAtCol}.gt.${lastTimestamp},${updatedAtCol}.gt.${lastTimestamp}`);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const timestampCol = this.getNodeParameter('timestampColumn');
|
|
228
|
+
query = query.gt(timestampCol, lastTimestamp);
|
|
229
|
+
}
|
|
230
|
+
const orderCol = event === 'rowCreatedOrUpdated'
|
|
231
|
+
? this.getNodeParameter('updatedAtColumn')
|
|
232
|
+
: this.getNodeParameter('timestampColumn');
|
|
233
|
+
query = query.order(orderCol, { ascending: true }).limit(1000);
|
|
234
|
+
const { data, error } = await query;
|
|
235
|
+
if (error) {
|
|
236
|
+
throw new Error(`Supabase trigger error: ${error.message}`);
|
|
237
|
+
}
|
|
238
|
+
if (!data || data.length === 0) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
const uniqueRows = [];
|
|
243
|
+
for (const row of data) {
|
|
244
|
+
const key = String(row[keyColumn]);
|
|
245
|
+
if (!seen.has(key)) {
|
|
246
|
+
seen.add(key);
|
|
247
|
+
uniqueRows.push(row);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
let maxTimestamp = lastTimestamp;
|
|
251
|
+
for (const row of uniqueRows) {
|
|
252
|
+
if (event === 'rowCreatedOrUpdated') {
|
|
253
|
+
const createdAtCol = this.getNodeParameter('createdAtColumn');
|
|
254
|
+
const updatedAtCol = this.getNodeParameter('updatedAtColumn');
|
|
255
|
+
const c = row[createdAtCol] || '';
|
|
256
|
+
const u = row[updatedAtCol] || '';
|
|
257
|
+
if (c > maxTimestamp)
|
|
258
|
+
maxTimestamp = c;
|
|
259
|
+
if (u > maxTimestamp)
|
|
260
|
+
maxTimestamp = u;
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const timestampCol = this.getNodeParameter('timestampColumn');
|
|
264
|
+
const ts = row[timestampCol] || '';
|
|
265
|
+
if (ts > maxTimestamp)
|
|
266
|
+
maxTimestamp = ts;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
staticData.lastTimestamp = maxTimestamp;
|
|
270
|
+
let resultRows = uniqueRows;
|
|
271
|
+
if (watchedFields.length > 0) {
|
|
272
|
+
const fieldStates = (staticData.fieldStates || {});
|
|
273
|
+
const changedRows = [];
|
|
274
|
+
for (const row of uniqueRows) {
|
|
275
|
+
const rowKey = String(row[keyColumn]);
|
|
276
|
+
const prevState = fieldStates[rowKey];
|
|
277
|
+
const currentState = {};
|
|
278
|
+
for (const field of watchedFields) {
|
|
279
|
+
currentState[field] = row[field];
|
|
280
|
+
}
|
|
281
|
+
if (!prevState) {
|
|
282
|
+
const changed = { ...row, _changedFields: watchedFields };
|
|
283
|
+
changedRows.push(changed);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const changed = watchedFields.filter((f) => JSON.stringify(prevState[f]) !== JSON.stringify(currentState[f]));
|
|
287
|
+
if (changed.length > 0) {
|
|
288
|
+
changedRows.push({ ...row, _changedFields: changed });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
fieldStates[rowKey] = currentState;
|
|
292
|
+
}
|
|
293
|
+
const keys = Object.keys(fieldStates);
|
|
294
|
+
if (keys.length > 10000) {
|
|
295
|
+
const excess = keys.length - 10000;
|
|
296
|
+
for (let i = 0; i < excess; i++) {
|
|
297
|
+
const key = keys[i];
|
|
298
|
+
if (key !== undefined) {
|
|
299
|
+
delete fieldStates[key];
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
staticData.fieldStates = fieldStates;
|
|
304
|
+
resultRows = changedRows;
|
|
305
|
+
}
|
|
306
|
+
if (resultRows.length === 0) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return [resultRows.map((row) => ({ json: row }))];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
exports.SupabaseTrigger = SupabaseTrigger;
|
|
@@ -2,3 +2,4 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
|
|
2
2
|
import { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
|
3
3
|
import { DatabaseOperation } from '../../types';
|
|
4
4
|
export declare function executeDatabaseOperation(this: IExecuteFunctions, supabase: SupabaseClient, operation: DatabaseOperation, itemIndex: number): Promise<INodeExecutionData[]>;
|
|
5
|
+
export declare function executeBulkDatabaseOperation(this: IExecuteFunctions, supabase: SupabaseClient, operation: DatabaseOperation, itemCount: number): Promise<INodeExecutionData[]>;
|
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.executeDatabaseOperation = void 0;
|
|
3
|
+
exports.executeBulkDatabaseOperation = exports.executeDatabaseOperation = void 0;
|
|
4
4
|
const supabaseClient_1 = require("../../utils/supabaseClient");
|
|
5
5
|
async function executeDatabaseOperation(supabase, operation, itemIndex) {
|
|
6
6
|
const returnData = [];
|
|
7
7
|
try {
|
|
8
8
|
switch (operation) {
|
|
9
|
-
case 'create':
|
|
10
|
-
returnData.push(...await handleCreate.call(this, supabase, itemIndex));
|
|
11
|
-
break;
|
|
12
9
|
case 'read':
|
|
13
10
|
returnData.push(...await handleRead.call(this, supabase, itemIndex));
|
|
14
11
|
break;
|
|
15
|
-
case 'update':
|
|
16
|
-
returnData.push(...await handleUpdate.call(this, supabase, itemIndex));
|
|
17
|
-
break;
|
|
18
12
|
case 'delete':
|
|
19
13
|
returnData.push(...await handleDelete.call(this, supabase, itemIndex));
|
|
20
14
|
break;
|
|
21
|
-
case 'upsert':
|
|
22
|
-
returnData.push(...await handleUpsert.call(this, supabase, itemIndex));
|
|
23
|
-
break;
|
|
24
15
|
case 'createTable':
|
|
25
16
|
returnData.push(...await handleCreateTable.call(this, supabase, itemIndex));
|
|
26
17
|
break;
|
|
@@ -55,56 +46,123 @@ async function executeDatabaseOperation(supabase, operation, itemIndex) {
|
|
|
55
46
|
return returnData;
|
|
56
47
|
}
|
|
57
48
|
exports.executeDatabaseOperation = executeDatabaseOperation;
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
49
|
+
function collectRowData(context, itemCount) {
|
|
50
|
+
const rows = [];
|
|
51
|
+
for (let i = 0; i < itemCount; i++) {
|
|
52
|
+
const uiMode = context.getNodeParameter('uiMode', i, 'simple');
|
|
53
|
+
let row;
|
|
54
|
+
if (uiMode === 'advanced') {
|
|
55
|
+
const jsonData = context.getNodeParameter('jsonData', i, '{}');
|
|
56
|
+
try {
|
|
57
|
+
row = JSON.parse(jsonData);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
throw new Error(`Invalid JSON data at item ${i}`);
|
|
61
|
+
}
|
|
67
62
|
}
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
else {
|
|
64
|
+
const columns = context.getNodeParameter('columns.column', i, []);
|
|
65
|
+
row = {};
|
|
66
|
+
for (const column of columns) {
|
|
67
|
+
if (column.name && column.value !== undefined) {
|
|
68
|
+
row[column.name] = column.value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
70
71
|
}
|
|
72
|
+
rows.push(row);
|
|
71
73
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
async function executeBulkDatabaseOperation(supabase, operation, itemCount) {
|
|
77
|
+
try {
|
|
78
|
+
switch (operation) {
|
|
79
|
+
case 'create':
|
|
80
|
+
return await handleBulkCreate.call(this, supabase, itemCount);
|
|
81
|
+
case 'upsert':
|
|
82
|
+
return await handleBulkUpsert.call(this, supabase, itemCount);
|
|
83
|
+
case 'update':
|
|
84
|
+
return await handleBulkUpdate.call(this, supabase, itemCount);
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Operation ${operation} does not support bulk mode`);
|
|
79
87
|
}
|
|
80
88
|
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new Error(`Database operation failed: ${(0, supabaseClient_1.formatSupabaseError)(error)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
exports.executeBulkDatabaseOperation = executeBulkDatabaseOperation;
|
|
94
|
+
async function handleBulkCreate(supabase, itemCount) {
|
|
95
|
+
const table = this.getNodeParameter('table', 0);
|
|
96
|
+
(0, supabaseClient_1.validateTableName)(table);
|
|
97
|
+
const rows = collectRowData(this, itemCount);
|
|
81
98
|
const { data, error } = await supabase
|
|
82
99
|
.from(table)
|
|
83
|
-
.insert(
|
|
100
|
+
.insert(rows)
|
|
84
101
|
.select();
|
|
85
|
-
if (error)
|
|
102
|
+
if (error)
|
|
86
103
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
104
|
+
if (Array.isArray(data)) {
|
|
105
|
+
return data.map((row) => ({ json: row }));
|
|
87
106
|
}
|
|
88
107
|
return [{ json: { data, operation: 'create', table } }];
|
|
89
108
|
}
|
|
90
|
-
async function
|
|
91
|
-
const table = this.getNodeParameter('table',
|
|
92
|
-
const
|
|
109
|
+
async function handleBulkUpsert(supabase, itemCount) {
|
|
110
|
+
const table = this.getNodeParameter('table', 0);
|
|
111
|
+
const onConflict = this.getNodeParameter('onConflict', 0, '');
|
|
93
112
|
(0, supabaseClient_1.validateTableName)(table);
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
|
|
113
|
+
const rows = collectRowData(this, itemCount);
|
|
114
|
+
const options = {};
|
|
115
|
+
if (onConflict)
|
|
116
|
+
options.onConflict = onConflict;
|
|
117
|
+
const { data, error } = await supabase
|
|
118
|
+
.from(table)
|
|
119
|
+
.upsert(rows, options)
|
|
120
|
+
.select();
|
|
121
|
+
if (error)
|
|
122
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
123
|
+
if (Array.isArray(data)) {
|
|
124
|
+
return data.map((row) => ({ json: row }));
|
|
98
125
|
}
|
|
126
|
+
return [{ json: { data, operation: 'upsert', table } }];
|
|
127
|
+
}
|
|
128
|
+
async function handleBulkUpdate(supabase, itemCount) {
|
|
129
|
+
const table = this.getNodeParameter('table', 0);
|
|
130
|
+
const matchColumn = this.getNodeParameter('matchColumn', 0);
|
|
131
|
+
(0, supabaseClient_1.validateTableName)(table);
|
|
132
|
+
if (!matchColumn) {
|
|
133
|
+
throw new Error('Match Column is required for update operations');
|
|
134
|
+
}
|
|
135
|
+
const rows = collectRowData(this, itemCount);
|
|
136
|
+
for (let i = 0; i < rows.length; i++) {
|
|
137
|
+
const row = rows[i];
|
|
138
|
+
if (!row || row[matchColumn] === undefined) {
|
|
139
|
+
throw new Error(`Item ${i} is missing the match column "${matchColumn}"`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const { data, error } = await supabase
|
|
143
|
+
.from(table)
|
|
144
|
+
.upsert(rows, { onConflict: matchColumn })
|
|
145
|
+
.select();
|
|
146
|
+
if (error)
|
|
147
|
+
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
148
|
+
if (Array.isArray(data)) {
|
|
149
|
+
return data.map((row) => ({ json: row }));
|
|
150
|
+
}
|
|
151
|
+
return [{ json: { data, operation: 'update', table } }];
|
|
152
|
+
}
|
|
153
|
+
function buildReadQuery(context, supabase, table, returnFields, itemIndex, options) {
|
|
154
|
+
const selectFields = returnFields && returnFields !== '*' ? returnFields : '*';
|
|
155
|
+
let query = supabase.from(table).select(selectFields, options);
|
|
156
|
+
const uiMode = context.getNodeParameter('uiMode', itemIndex, 'simple');
|
|
99
157
|
if (uiMode === 'simple') {
|
|
100
|
-
const filters =
|
|
158
|
+
const filters = context.getNodeParameter('filters.filter', itemIndex, []);
|
|
101
159
|
for (const filter of filters) {
|
|
102
160
|
const operator = (0, supabaseClient_1.convertFilterOperator)(filter.operator);
|
|
103
161
|
query = query.filter(filter.column, operator, (0, supabaseClient_1.normalizeFilterValue)(filter.operator, filter.value));
|
|
104
162
|
}
|
|
105
163
|
}
|
|
106
164
|
else {
|
|
107
|
-
const advancedFilters =
|
|
165
|
+
const advancedFilters = context.getNodeParameter('advancedFilters', itemIndex, '');
|
|
108
166
|
if (advancedFilters) {
|
|
109
167
|
try {
|
|
110
168
|
const filters = JSON.parse(advancedFilters);
|
|
@@ -123,10 +181,16 @@ async function handleRead(supabase, itemIndex) {
|
|
|
123
181
|
}
|
|
124
182
|
}
|
|
125
183
|
}
|
|
126
|
-
const sort =
|
|
184
|
+
const sort = context.getNodeParameter('sort.sortField', itemIndex, []);
|
|
127
185
|
for (const sortField of sort) {
|
|
128
186
|
query = query.order(sortField.column, { ascending: sortField.ascending });
|
|
129
187
|
}
|
|
188
|
+
return query;
|
|
189
|
+
}
|
|
190
|
+
async function handleRead(supabase, itemIndex) {
|
|
191
|
+
const table = this.getNodeParameter('table', itemIndex);
|
|
192
|
+
(0, supabaseClient_1.validateTableName)(table);
|
|
193
|
+
const returnFields = this.getNodeParameter('returnFields', itemIndex, '*');
|
|
130
194
|
const returnAll = this.getNodeParameter('returnAll', itemIndex, false);
|
|
131
195
|
const returnData = [];
|
|
132
196
|
if (returnAll) {
|
|
@@ -134,42 +198,8 @@ async function handleRead(supabase, itemIndex) {
|
|
|
134
198
|
let offset = 0;
|
|
135
199
|
let hasMore = true;
|
|
136
200
|
while (hasMore) {
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
if (batchFilterMode === 'simple') {
|
|
140
|
-
const batchFilters = this.getNodeParameter('filters.conditions', itemIndex, []);
|
|
141
|
-
for (const filter of batchFilters) {
|
|
142
|
-
if (filter.column && filter.value !== undefined) {
|
|
143
|
-
batchQuery = batchQuery.eq(filter.column, filter.value);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
const batchAdvancedFilters = this.getNodeParameter('advancedFilters', itemIndex, '');
|
|
149
|
-
if (batchAdvancedFilters) {
|
|
150
|
-
try {
|
|
151
|
-
const parsed = JSON.parse(batchAdvancedFilters);
|
|
152
|
-
for (const condition of parsed) {
|
|
153
|
-
const { column, operator, value } = condition;
|
|
154
|
-
if (operator) {
|
|
155
|
-
batchQuery = batchQuery.filter(column, (0, supabaseClient_1.convertFilterOperator)(operator), (0, supabaseClient_1.normalizeFilterValue)(operator, value));
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
batchQuery = batchQuery.eq(column, condition);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
throw new Error('Invalid advanced filters JSON');
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const batchSort = this.getNodeParameter('sort.sortField', itemIndex, []);
|
|
168
|
-
for (const sortField of batchSort) {
|
|
169
|
-
batchQuery = batchQuery.order(sortField.column, { ascending: sortField.ascending });
|
|
170
|
-
}
|
|
171
|
-
batchQuery = batchQuery.range(offset, offset + batchSize - 1);
|
|
172
|
-
const { data: batchData, error: batchError } = await batchQuery;
|
|
201
|
+
const batchQuery = buildReadQuery(this, supabase, table, returnFields, itemIndex, { count: 'exact' });
|
|
202
|
+
const { data: batchData, error: batchError } = await batchQuery.range(offset, offset + batchSize - 1);
|
|
173
203
|
if (batchError) {
|
|
174
204
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(batchError));
|
|
175
205
|
}
|
|
@@ -188,6 +218,7 @@ async function handleRead(supabase, itemIndex) {
|
|
|
188
218
|
else {
|
|
189
219
|
const limit = this.getNodeParameter('limit', itemIndex, undefined);
|
|
190
220
|
const offset = this.getNodeParameter('offset', itemIndex, undefined);
|
|
221
|
+
let query = buildReadQuery(this, supabase, table, returnFields, itemIndex);
|
|
191
222
|
if (limit !== undefined) {
|
|
192
223
|
query = query.limit(limit);
|
|
193
224
|
}
|
|
@@ -217,41 +248,6 @@ async function handleRead(supabase, itemIndex) {
|
|
|
217
248
|
}
|
|
218
249
|
return returnData;
|
|
219
250
|
}
|
|
220
|
-
async function handleUpdate(supabase, itemIndex) {
|
|
221
|
-
const table = this.getNodeParameter('table', itemIndex);
|
|
222
|
-
const uiMode = this.getNodeParameter('uiMode', itemIndex, 'simple');
|
|
223
|
-
(0, supabaseClient_1.validateTableName)(table);
|
|
224
|
-
let dataToUpdate;
|
|
225
|
-
if (uiMode === 'advanced') {
|
|
226
|
-
const jsonData = this.getNodeParameter('jsonData', itemIndex, '{}');
|
|
227
|
-
try {
|
|
228
|
-
dataToUpdate = JSON.parse(jsonData);
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
throw new Error('Invalid JSON data provided');
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
const columns = this.getNodeParameter('columns.column', itemIndex, []);
|
|
236
|
-
dataToUpdate = {};
|
|
237
|
-
for (const column of columns) {
|
|
238
|
-
if (column.name && column.value !== undefined) {
|
|
239
|
-
dataToUpdate[column.name] = column.value;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
let query = supabase.from(table).update(dataToUpdate);
|
|
244
|
-
const filters = this.getNodeParameter('filters.filter', itemIndex, []);
|
|
245
|
-
for (const filter of filters) {
|
|
246
|
-
const operator = (0, supabaseClient_1.convertFilterOperator)(filter.operator);
|
|
247
|
-
query = query.filter(filter.column, operator, (0, supabaseClient_1.normalizeFilterValue)(filter.operator, filter.value));
|
|
248
|
-
}
|
|
249
|
-
const { data, error } = await query.select();
|
|
250
|
-
if (error) {
|
|
251
|
-
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
252
|
-
}
|
|
253
|
-
return [{ json: { data, operation: 'update', table, updated: (data === null || data === void 0 ? void 0 : data.length) || 0 } }];
|
|
254
|
-
}
|
|
255
251
|
async function handleDelete(supabase, itemIndex) {
|
|
256
252
|
const table = this.getNodeParameter('table', itemIndex);
|
|
257
253
|
(0, supabaseClient_1.validateTableName)(table);
|
|
@@ -270,43 +266,6 @@ async function handleDelete(supabase, itemIndex) {
|
|
|
270
266
|
}
|
|
271
267
|
return [{ json: { data, operation: 'delete', table, deleted: (data === null || data === void 0 ? void 0 : data.length) || 0 } }];
|
|
272
268
|
}
|
|
273
|
-
async function handleUpsert(supabase, itemIndex) {
|
|
274
|
-
const table = this.getNodeParameter('table', itemIndex);
|
|
275
|
-
const uiMode = this.getNodeParameter('uiMode', itemIndex, 'simple');
|
|
276
|
-
const onConflict = this.getNodeParameter('onConflict', itemIndex, '');
|
|
277
|
-
(0, supabaseClient_1.validateTableName)(table);
|
|
278
|
-
let dataToUpsert;
|
|
279
|
-
if (uiMode === 'advanced') {
|
|
280
|
-
const jsonData = this.getNodeParameter('jsonData', itemIndex, '{}');
|
|
281
|
-
try {
|
|
282
|
-
dataToUpsert = JSON.parse(jsonData);
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
throw new Error('Invalid JSON data provided');
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
const columns = this.getNodeParameter('columns.column', itemIndex, []);
|
|
290
|
-
dataToUpsert = {};
|
|
291
|
-
for (const column of columns) {
|
|
292
|
-
if (column.name && column.value !== undefined) {
|
|
293
|
-
dataToUpsert[column.name] = column.value;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
const options = {};
|
|
298
|
-
if (onConflict) {
|
|
299
|
-
options.onConflict = onConflict;
|
|
300
|
-
}
|
|
301
|
-
const { data, error } = await supabase
|
|
302
|
-
.from(table)
|
|
303
|
-
.upsert(dataToUpsert, options)
|
|
304
|
-
.select();
|
|
305
|
-
if (error) {
|
|
306
|
-
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
307
|
-
}
|
|
308
|
-
return [{ json: { data, operation: 'upsert', table } }];
|
|
309
|
-
}
|
|
310
269
|
async function handleCreateTable(supabase, itemIndex) {
|
|
311
270
|
const tableName = this.getNodeParameter('tableName', itemIndex);
|
|
312
271
|
const columns = this.getNodeParameter('columnDefinitions.column', itemIndex, []);
|
|
@@ -334,7 +293,7 @@ async function handleCreateTable(supabase, itemIndex) {
|
|
|
334
293
|
columnDefs.push(columnDef);
|
|
335
294
|
}
|
|
336
295
|
sql += columnDefs.join(', ') + ')';
|
|
337
|
-
const {
|
|
296
|
+
const { error } = await supabase.rpc('exec_sql', { sql });
|
|
338
297
|
if (error) {
|
|
339
298
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
340
299
|
}
|
|
@@ -345,7 +304,7 @@ async function handleDropTable(supabase, itemIndex) {
|
|
|
345
304
|
const cascade = this.getNodeParameter('cascade', itemIndex, false);
|
|
346
305
|
(0, supabaseClient_1.validateTableName)(tableName);
|
|
347
306
|
const sql = `DROP TABLE "${tableName}"${cascade ? ' CASCADE' : ''}`;
|
|
348
|
-
const {
|
|
307
|
+
const { error } = await supabase.rpc('exec_sql', { sql });
|
|
349
308
|
if (error) {
|
|
350
309
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
351
310
|
}
|
|
@@ -363,7 +322,7 @@ async function handleAddColumn(supabase, itemIndex) {
|
|
|
363
322
|
if (columnDefinition.defaultValue) {
|
|
364
323
|
sql += ` DEFAULT ${columnDefinition.defaultValue}`;
|
|
365
324
|
}
|
|
366
|
-
const {
|
|
325
|
+
const { error } = await supabase.rpc('exec_sql', { sql });
|
|
367
326
|
if (error) {
|
|
368
327
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
369
328
|
}
|
|
@@ -376,7 +335,7 @@ async function handleDropColumn(supabase, itemIndex) {
|
|
|
376
335
|
(0, supabaseClient_1.validateTableName)(tableName);
|
|
377
336
|
(0, supabaseClient_1.validateColumnName)(columnName);
|
|
378
337
|
const sql = `ALTER TABLE "${tableName}" DROP COLUMN "${columnName}"${cascade ? ' CASCADE' : ''}`;
|
|
379
|
-
const {
|
|
338
|
+
const { error } = await supabase.rpc('exec_sql', { sql });
|
|
380
339
|
if (error) {
|
|
381
340
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
382
341
|
}
|
|
@@ -393,7 +352,7 @@ async function handleCreateIndex(supabase, itemIndex) {
|
|
|
393
352
|
const method = indexDefinition.method ? ` USING ${indexDefinition.method}` : '';
|
|
394
353
|
const columnList = indexDefinition.columns.map(col => `"${col}"`).join(', ');
|
|
395
354
|
const sql = `CREATE ${uniqueKeyword}INDEX "${indexDefinition.name}" ON "${tableName}"${method} (${columnList})`;
|
|
396
|
-
const {
|
|
355
|
+
const { error } = await supabase.rpc('exec_sql', { sql });
|
|
397
356
|
if (error) {
|
|
398
357
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
399
358
|
}
|
|
@@ -406,7 +365,7 @@ async function handleDropIndex(supabase, itemIndex) {
|
|
|
406
365
|
throw new Error('Index name is required');
|
|
407
366
|
}
|
|
408
367
|
const sql = `DROP INDEX "${indexName}"${cascade ? ' CASCADE' : ''}`;
|
|
409
|
-
const {
|
|
368
|
+
const { error } = await supabase.rpc('exec_sql', { sql });
|
|
410
369
|
if (error) {
|
|
411
370
|
throw new Error((0, supabaseClient_1.formatSupabaseError)(error));
|
|
412
371
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fachkraftfreund/n8n-nodes-supabase",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Comprehensive n8n community node for Supabase with database and storage operations",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"dist/credentials/SupabaseExtendedApi.credentials.js"
|
|
38
38
|
],
|
|
39
39
|
"nodes": [
|
|
40
|
-
"dist/nodes/Supabase/Supabase.node.js"
|
|
40
|
+
"dist/nodes/Supabase/Supabase.node.js",
|
|
41
|
+
"dist/nodes/Supabase/SupabaseTrigger.node.js"
|
|
41
42
|
]
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|