@fachkraftfreund/n8n-nodes-supabase 1.2.8 → 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.
|
@@ -891,6 +891,12 @@ class Supabase {
|
|
|
891
891
|
async execute() {
|
|
892
892
|
const items = this.getInputData();
|
|
893
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
|
+
}
|
|
894
900
|
const credentials = await this.getCredentials('supabaseExtendedApi');
|
|
895
901
|
try {
|
|
896
902
|
(0, supabaseClient_1.validateCredentials)(credentials);
|
|
@@ -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;
|
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": {
|