@quatrain/cloudwrapper-supabase 1.1.26 → 1.1.27
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.
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { AbstractCloudWrapper, DatabaseTriggerType, StorageTriggerType } from '@quatrain/cloudwrapper';
|
|
3
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
|
4
|
+
import { FileType } from '@quatrain/storage';
|
|
5
|
+
export type SupabaseParams = {
|
|
6
|
+
useEmulator?: boolean;
|
|
7
|
+
url?: string;
|
|
8
|
+
key?: string;
|
|
9
|
+
exitOnDisconnect?: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const eventMap: {
|
|
12
|
+
create: string;
|
|
13
|
+
update: string;
|
|
14
|
+
delete: string;
|
|
15
|
+
};
|
|
16
|
+
export declare class SupabaseCloudWrapper extends AbstractCloudWrapper {
|
|
17
|
+
protected _supabaseClient: SupabaseClient | undefined;
|
|
18
|
+
protected _isInitialized: boolean;
|
|
19
|
+
protected _heartbeatOkReceived: boolean;
|
|
20
|
+
protected _connectionTimeout: NodeJS.Timeout | undefined;
|
|
21
|
+
constructor(params: SupabaseParams);
|
|
22
|
+
databaseTrigger(trigger: DatabaseTriggerType): void | {
|
|
23
|
+
event: any;
|
|
24
|
+
schema: string;
|
|
25
|
+
table: string;
|
|
26
|
+
};
|
|
27
|
+
storageTrigger(trigger: StorageTriggerType): void | {
|
|
28
|
+
event: any;
|
|
29
|
+
schema: string;
|
|
30
|
+
table: string;
|
|
31
|
+
};
|
|
32
|
+
protected _initialize(): void;
|
|
33
|
+
protected _payload2File(payload: {
|
|
34
|
+
[x: string]: any;
|
|
35
|
+
}): FileType | undefined;
|
|
36
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
12
|
+
var t = {};
|
|
13
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
14
|
+
t[p] = s[p];
|
|
15
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
16
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
17
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
18
|
+
t[p[i]] = s[p[i]];
|
|
19
|
+
}
|
|
20
|
+
return t;
|
|
21
|
+
};
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.SupabaseCloudWrapper = exports.eventMap = void 0;
|
|
27
|
+
const cloudwrapper_1 = require("@quatrain/cloudwrapper");
|
|
28
|
+
const backend_1 = require("@quatrain/backend");
|
|
29
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
30
|
+
const ws_1 = __importDefault(require("ws"));
|
|
31
|
+
exports.eventMap = {
|
|
32
|
+
[backend_1.BackendAction.CREATE]: 'INSERT',
|
|
33
|
+
[backend_1.BackendAction.UPDATE]: 'UPDATE',
|
|
34
|
+
[backend_1.BackendAction.DELETE]: 'DELETE',
|
|
35
|
+
};
|
|
36
|
+
class SupabaseCloudWrapper extends cloudwrapper_1.AbstractCloudWrapper {
|
|
37
|
+
constructor(params) {
|
|
38
|
+
super(params);
|
|
39
|
+
this._isInitialized = false;
|
|
40
|
+
this._heartbeatOkReceived = false;
|
|
41
|
+
this._initialize();
|
|
42
|
+
}
|
|
43
|
+
databaseTrigger(trigger) {
|
|
44
|
+
var _a;
|
|
45
|
+
this._initialize();
|
|
46
|
+
if (!(this._supabaseClient instanceof supabase_js_1.SupabaseClient)) {
|
|
47
|
+
throw new TypeError(`Supabase client is not enabled`);
|
|
48
|
+
}
|
|
49
|
+
if (typeof trigger.script !== 'function') {
|
|
50
|
+
throw new TypeError(`Passed script value is not a function`);
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(trigger.event)) {
|
|
53
|
+
const params = trigger.event.forEach((event) => {
|
|
54
|
+
return this.databaseTrigger(Object.assign(Object.assign({}, trigger), { event, name: `${trigger.name}-${event}` }));
|
|
55
|
+
});
|
|
56
|
+
return params;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const schema = trigger.schema || 'public';
|
|
60
|
+
const params = {
|
|
61
|
+
event: Reflect.get(exports.eventMap, trigger.event),
|
|
62
|
+
schema,
|
|
63
|
+
table: trigger.model,
|
|
64
|
+
};
|
|
65
|
+
cloudwrapper_1.CloudWrapper.info(`Set up DB trigger ${trigger.name} for ${trigger.event} event on table ${schema}.${params.table}`);
|
|
66
|
+
(_a = this._supabaseClient) === null || _a === void 0 ? void 0 : _a.channel(trigger.name).on('postgres_changes', params, (_b) => __awaiter(this, void 0, void 0, function* () {
|
|
67
|
+
var { old: before, new: after } = _b, context = __rest(_b, ["old", "new"]);
|
|
68
|
+
cloudwrapper_1.CloudWrapper.info(`Triggering function on event ${trigger.name}`);
|
|
69
|
+
try {
|
|
70
|
+
return yield trigger.script({ before, after, context });
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
cloudwrapper_1.CloudWrapper.error(err.message);
|
|
74
|
+
console.log(err);
|
|
75
|
+
}
|
|
76
|
+
})).subscribe();
|
|
77
|
+
return params;
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
cloudwrapper_1.CloudWrapper.error(err);
|
|
81
|
+
throw new Error(`Realtime channel subscription failed`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
storageTrigger(trigger) {
|
|
85
|
+
var _a;
|
|
86
|
+
this._initialize();
|
|
87
|
+
if (typeof trigger.script !== 'function') {
|
|
88
|
+
throw new TypeError(`Passed script value is not a function`);
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(trigger.event)) {
|
|
91
|
+
const params = trigger.event.forEach((event) => {
|
|
92
|
+
return this.storageTrigger(Object.assign(Object.assign({}, trigger), { event, name: `${trigger.name}-${event}` }));
|
|
93
|
+
});
|
|
94
|
+
return params;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const params = {
|
|
98
|
+
event: Reflect.get(exports.eventMap, trigger.event),
|
|
99
|
+
schema: 'storage',
|
|
100
|
+
table: 'objects',
|
|
101
|
+
};
|
|
102
|
+
cloudwrapper_1.CloudWrapper.info(`Set up Storage trigger ${trigger.name} for ${trigger.event} event`);
|
|
103
|
+
(_a = this._supabaseClient) === null || _a === void 0 ? void 0 : _a.channel(trigger.name).on('postgres_changes', params, (_b) => __awaiter(this, void 0, void 0, function* () {
|
|
104
|
+
var { old: before, new: after } = _b, context = __rest(_b, ["old", "new"]);
|
|
105
|
+
cloudwrapper_1.CloudWrapper.info(`Triggering storage function on event ${trigger.name}`);
|
|
106
|
+
try {
|
|
107
|
+
const payload = {
|
|
108
|
+
before: this._payload2File(before),
|
|
109
|
+
after: this._payload2File(after),
|
|
110
|
+
context,
|
|
111
|
+
};
|
|
112
|
+
cloudwrapper_1.CloudWrapper.debug(payload);
|
|
113
|
+
return yield trigger.script(payload);
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
cloudwrapper_1.CloudWrapper.error(err.message);
|
|
117
|
+
}
|
|
118
|
+
})).subscribe();
|
|
119
|
+
return params;
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.log(err);
|
|
123
|
+
throw new Error(`Realtime channel subscription failed`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
_initialize() {
|
|
127
|
+
if (this._isInitialized === false) {
|
|
128
|
+
cloudwrapper_1.CloudWrapper.info(`Starting Supabase client with heartbeat monitoring`);
|
|
129
|
+
if (this._connectionTimeout) {
|
|
130
|
+
clearTimeout(this._connectionTimeout);
|
|
131
|
+
}
|
|
132
|
+
this._supabaseClient = (0, supabase_js_1.createClient)(this._params.url, this._params.key, {
|
|
133
|
+
realtime: {
|
|
134
|
+
transport: ws_1.default,
|
|
135
|
+
heartbeatIntervalMs: 5000,
|
|
136
|
+
heartbeatCallback: (status) => {
|
|
137
|
+
switch (status) {
|
|
138
|
+
case 'ok':
|
|
139
|
+
if (!this._heartbeatOkReceived) {
|
|
140
|
+
cloudwrapper_1.CloudWrapper.info(`🛜 Supabase Realtime connection established`);
|
|
141
|
+
if (this._connectionTimeout) {
|
|
142
|
+
clearTimeout(this._connectionTimeout);
|
|
143
|
+
}
|
|
144
|
+
this._heartbeatOkReceived = true;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
case 'timeout':
|
|
148
|
+
case 'error':
|
|
149
|
+
case 'disconnected':
|
|
150
|
+
// Reconnect only if exitOnDisconnect is explicitly set to false.
|
|
151
|
+
if (this._params.exitOnDisconnect === false) {
|
|
152
|
+
cloudwrapper_1.CloudWrapper.warn(`❌ Supabase connection lost (status: ${status}). Attempting to reconnect...`);
|
|
153
|
+
this._isInitialized = false;
|
|
154
|
+
this._heartbeatOkReceived = false;
|
|
155
|
+
this._initialize();
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Default behavior: exit to allow for a clean restart by the orchestrator.
|
|
159
|
+
cloudwrapper_1.CloudWrapper.error(`❌ Supabase connection lost (status: ${status}). Exiting to allow for a clean restart.`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
// do nothing
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
this._isInitialized = true;
|
|
171
|
+
cloudwrapper_1.CloudWrapper.info(`Supabase App initialized`);
|
|
172
|
+
cloudwrapper_1.CloudWrapper.info(`🕘 Supabase Realtime connection is being established`);
|
|
173
|
+
this._connectionTimeout = setTimeout(() => {
|
|
174
|
+
if (!this._heartbeatOkReceived) {
|
|
175
|
+
cloudwrapper_1.CloudWrapper.error(`❌ Supabase Realtime connection failed to establish after 30 seconds. Exiting.`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
}, 30000);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
_payload2File(payload) {
|
|
182
|
+
if (!payload.metadata) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if (Object.keys(payload).length === 0) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const { size, mimetype } = (payload === null || payload === void 0 ? void 0 : payload.metadata) || {};
|
|
189
|
+
return {
|
|
190
|
+
host: 'supabase',
|
|
191
|
+
bucket: payload.bucket_id,
|
|
192
|
+
ref: payload.name,
|
|
193
|
+
contentType: mimetype,
|
|
194
|
+
size,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
exports.SupabaseCloudWrapper = SupabaseCloudWrapper;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const SupabaseCloudWrapper_1 = require("./SupabaseCloudWrapper");
|
|
13
|
+
const backend_1 = require("@quatrain/backend");
|
|
14
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
15
|
+
// Mock dependencies
|
|
16
|
+
jest.mock('@supabase/supabase-js');
|
|
17
|
+
jest.mock('ws');
|
|
18
|
+
describe('SupabaseCloudWrapper', () => {
|
|
19
|
+
let wrapper;
|
|
20
|
+
let mockChannel;
|
|
21
|
+
let mockSubscribe;
|
|
22
|
+
let mockOn;
|
|
23
|
+
let mockSupabaseClient;
|
|
24
|
+
let processExitSpy;
|
|
25
|
+
let heartbeatCallback;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
jest.clearAllTimers();
|
|
29
|
+
jest.useFakeTimers();
|
|
30
|
+
// Spy on process.exit to prevent actual exits
|
|
31
|
+
processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
32
|
+
throw new Error('process.exit called');
|
|
33
|
+
});
|
|
34
|
+
// Create mock subscription chain
|
|
35
|
+
mockSubscribe = jest.fn().mockReturnValue(undefined);
|
|
36
|
+
mockOn = jest.fn().mockReturnValue({ subscribe: mockSubscribe });
|
|
37
|
+
mockChannel = jest.fn().mockReturnValue({ on: mockOn });
|
|
38
|
+
// Create mock Supabase client
|
|
39
|
+
mockSupabaseClient = {
|
|
40
|
+
channel: mockChannel,
|
|
41
|
+
};
|
|
42
|
+
supabase_js_1.createClient.mockImplementation((url, key, options) => {
|
|
43
|
+
var _a;
|
|
44
|
+
heartbeatCallback = (_a = options === null || options === void 0 ? void 0 : options.realtime) === null || _a === void 0 ? void 0 : _a.heartbeatCallback;
|
|
45
|
+
const client = new supabase_js_1.SupabaseClient(url, key, options);
|
|
46
|
+
Object.assign(client, { channel: mockChannel });
|
|
47
|
+
return client;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
jest.useRealTimers();
|
|
52
|
+
processExitSpy.mockRestore();
|
|
53
|
+
});
|
|
54
|
+
describe('eventMap', () => {
|
|
55
|
+
it('should map BackendAction to Supabase events', () => {
|
|
56
|
+
expect(SupabaseCloudWrapper_1.eventMap[backend_1.BackendAction.CREATE]).toBe('INSERT');
|
|
57
|
+
expect(SupabaseCloudWrapper_1.eventMap[backend_1.BackendAction.UPDATE]).toBe('UPDATE');
|
|
58
|
+
expect(SupabaseCloudWrapper_1.eventMap[backend_1.BackendAction.DELETE]).toBe('DELETE');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('Constructor and Initialization', () => {
|
|
62
|
+
it('should create Supabase client with correct params', () => {
|
|
63
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
64
|
+
url: 'https://test.supabase.co',
|
|
65
|
+
key: 'test-key-123',
|
|
66
|
+
});
|
|
67
|
+
expect(supabase_js_1.createClient).toHaveBeenCalledWith('https://test.supabase.co', 'test-key-123', expect.objectContaining({
|
|
68
|
+
realtime: expect.objectContaining({
|
|
69
|
+
transport: expect.anything(),
|
|
70
|
+
heartbeatIntervalMs: 5000,
|
|
71
|
+
heartbeatCallback: expect.any(Function),
|
|
72
|
+
}),
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
it('should set initialization flag', () => {
|
|
76
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
77
|
+
url: 'https://test.supabase.co',
|
|
78
|
+
key: 'test-key',
|
|
79
|
+
});
|
|
80
|
+
expect(wrapper._isInitialized).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it('should initialize heartbeat monitoring', () => {
|
|
83
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
84
|
+
url: 'https://test.supabase.co',
|
|
85
|
+
key: 'test-key',
|
|
86
|
+
});
|
|
87
|
+
expect(heartbeatCallback).toBeDefined();
|
|
88
|
+
expect(typeof heartbeatCallback).toBe('function');
|
|
89
|
+
});
|
|
90
|
+
it('should set connection timeout', () => {
|
|
91
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
92
|
+
url: 'https://test.supabase.co',
|
|
93
|
+
key: 'test-key',
|
|
94
|
+
});
|
|
95
|
+
expect(wrapper._connectionTimeout).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('Heartbeat Monitoring', () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
101
|
+
url: 'https://test.supabase.co',
|
|
102
|
+
key: 'test-key',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it('should handle "ok" heartbeat status', () => {
|
|
106
|
+
expect(wrapper._heartbeatOkReceived).toBe(false);
|
|
107
|
+
heartbeatCallback('ok');
|
|
108
|
+
expect(wrapper._heartbeatOkReceived).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
it('should clear timeout on successful heartbeat', () => {
|
|
111
|
+
const initialTimeout = wrapper._connectionTimeout;
|
|
112
|
+
heartbeatCallback('ok');
|
|
113
|
+
expect(wrapper._connectionTimeout).toBe(initialTimeout);
|
|
114
|
+
});
|
|
115
|
+
it('should handle "timeout" status', () => {
|
|
116
|
+
// Should not throw or exit
|
|
117
|
+
expect(() => heartbeatCallback('timeout')).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
it('should reconnect on "disconnected" when exitOnDisconnect is false', () => {
|
|
120
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
121
|
+
url: 'https://test.supabase.co',
|
|
122
|
+
key: 'test-key',
|
|
123
|
+
exitOnDisconnect: false,
|
|
124
|
+
});
|
|
125
|
+
const createClientCallsBefore = supabase_js_1.createClient.mock.calls
|
|
126
|
+
.length;
|
|
127
|
+
heartbeatCallback = supabase_js_1.createClient.mock.calls[createClientCallsBefore - 1][2].realtime.heartbeatCallback;
|
|
128
|
+
heartbeatCallback('disconnected');
|
|
129
|
+
// Should attempt reconnection (call createClient again)
|
|
130
|
+
expect(supabase_js_1.createClient.mock.calls.length).toBeGreaterThan(createClientCallsBefore);
|
|
131
|
+
});
|
|
132
|
+
it('should exit on "disconnected" by default', () => {
|
|
133
|
+
expect(() => heartbeatCallback('disconnected')).toThrow('process.exit called');
|
|
134
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
135
|
+
});
|
|
136
|
+
it('should exit on "disconnected" when exitOnDisconnect is true', () => {
|
|
137
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
138
|
+
url: 'https://test.supabase.co',
|
|
139
|
+
key: 'test-key',
|
|
140
|
+
exitOnDisconnect: true,
|
|
141
|
+
});
|
|
142
|
+
heartbeatCallback = supabase_js_1.createClient.mock.calls[1][2]
|
|
143
|
+
.realtime.heartbeatCallback;
|
|
144
|
+
expect(() => heartbeatCallback('disconnected')).toThrow('process.exit called');
|
|
145
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe('Connection Timeout', () => {
|
|
149
|
+
it('should exit if heartbeat not received within 30 seconds', () => {
|
|
150
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
151
|
+
url: 'https://test.supabase.co',
|
|
152
|
+
key: 'test-key',
|
|
153
|
+
});
|
|
154
|
+
expect(() => jest.advanceTimersByTime(30000)).toThrow('process.exit called');
|
|
155
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
156
|
+
});
|
|
157
|
+
it('should not exit if heartbeat received before timeout', () => {
|
|
158
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
159
|
+
url: 'https://test.supabase.co',
|
|
160
|
+
key: 'test-key',
|
|
161
|
+
});
|
|
162
|
+
heartbeatCallback('ok');
|
|
163
|
+
expect(() => jest.advanceTimersByTime(30000)).not.toThrow();
|
|
164
|
+
expect(processExitSpy).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
describe('databaseTrigger()', () => {
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
170
|
+
url: 'https://test.supabase.co',
|
|
171
|
+
key: 'test-key',
|
|
172
|
+
});
|
|
173
|
+
heartbeatCallback('ok'); // Simulate successful connection
|
|
174
|
+
});
|
|
175
|
+
it('should set up single event trigger', () => {
|
|
176
|
+
const trigger = {
|
|
177
|
+
name: 'test-trigger',
|
|
178
|
+
event: backend_1.BackendAction.CREATE,
|
|
179
|
+
script: jest.fn(),
|
|
180
|
+
model: 'users',
|
|
181
|
+
path: '/users/{id}',
|
|
182
|
+
schema: 'public',
|
|
183
|
+
};
|
|
184
|
+
wrapper.databaseTrigger(trigger);
|
|
185
|
+
expect(mockChannel).toHaveBeenCalledWith('test-trigger');
|
|
186
|
+
expect(mockOn).toHaveBeenCalledWith('postgres_changes', {
|
|
187
|
+
event: 'INSERT',
|
|
188
|
+
schema: 'public',
|
|
189
|
+
table: 'users',
|
|
190
|
+
}, expect.any(Function));
|
|
191
|
+
expect(mockSubscribe).toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
it('should use default schema if not provided', () => {
|
|
194
|
+
const trigger = {
|
|
195
|
+
name: 'test-trigger',
|
|
196
|
+
event: backend_1.BackendAction.UPDATE,
|
|
197
|
+
script: jest.fn(),
|
|
198
|
+
model: 'posts',
|
|
199
|
+
path: '/posts/{id}',
|
|
200
|
+
};
|
|
201
|
+
wrapper.databaseTrigger(trigger);
|
|
202
|
+
expect(mockOn).toHaveBeenCalledWith('postgres_changes', expect.objectContaining({
|
|
203
|
+
schema: 'public',
|
|
204
|
+
}), expect.any(Function));
|
|
205
|
+
});
|
|
206
|
+
it('should handle multiple events', () => {
|
|
207
|
+
const trigger = {
|
|
208
|
+
name: 'multi-trigger',
|
|
209
|
+
event: [backend_1.BackendAction.CREATE, backend_1.BackendAction.UPDATE],
|
|
210
|
+
script: jest.fn(),
|
|
211
|
+
model: 'comments',
|
|
212
|
+
path: '/comments/{id}',
|
|
213
|
+
};
|
|
214
|
+
wrapper.databaseTrigger(trigger);
|
|
215
|
+
expect(mockChannel).toHaveBeenCalledWith('multi-trigger-create');
|
|
216
|
+
expect(mockChannel).toHaveBeenCalledWith('multi-trigger-update');
|
|
217
|
+
expect(mockSubscribe).toHaveBeenCalledTimes(2);
|
|
218
|
+
});
|
|
219
|
+
it('should throw TypeError if script is not a function', () => {
|
|
220
|
+
const trigger = {
|
|
221
|
+
name: 'bad-trigger',
|
|
222
|
+
event: backend_1.BackendAction.CREATE,
|
|
223
|
+
script: 'not-a-function',
|
|
224
|
+
model: 'users',
|
|
225
|
+
path: '/users/{id}',
|
|
226
|
+
};
|
|
227
|
+
expect(() => wrapper.databaseTrigger(trigger)).toThrow(TypeError);
|
|
228
|
+
expect(() => wrapper.databaseTrigger(trigger)).toThrow('Passed script value is not a function');
|
|
229
|
+
});
|
|
230
|
+
it('should execute script with payload when event fires', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
231
|
+
const scriptMock = jest.fn().mockResolvedValue(undefined);
|
|
232
|
+
const trigger = {
|
|
233
|
+
name: 'test-trigger',
|
|
234
|
+
event: backend_1.BackendAction.CREATE,
|
|
235
|
+
script: scriptMock,
|
|
236
|
+
model: 'users',
|
|
237
|
+
path: '/users/{id}',
|
|
238
|
+
};
|
|
239
|
+
wrapper.databaseTrigger(trigger);
|
|
240
|
+
// Get the callback passed to .on()
|
|
241
|
+
const callback = mockOn.mock.calls[0][2];
|
|
242
|
+
const payload = {
|
|
243
|
+
old: { id: 1, name: 'old' },
|
|
244
|
+
new: { id: 1, name: 'new' },
|
|
245
|
+
extra: 'context',
|
|
246
|
+
};
|
|
247
|
+
yield callback(payload);
|
|
248
|
+
expect(scriptMock).toHaveBeenCalledWith({
|
|
249
|
+
before: payload.old,
|
|
250
|
+
after: payload.new,
|
|
251
|
+
context: { extra: 'context' },
|
|
252
|
+
});
|
|
253
|
+
}));
|
|
254
|
+
});
|
|
255
|
+
describe('storageTrigger()', () => {
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
258
|
+
url: 'https://test.supabase.co',
|
|
259
|
+
key: 'test-key',
|
|
260
|
+
});
|
|
261
|
+
heartbeatCallback('ok');
|
|
262
|
+
});
|
|
263
|
+
it('should set up single event storage trigger', () => {
|
|
264
|
+
const trigger = {
|
|
265
|
+
name: 'storage-trigger',
|
|
266
|
+
event: backend_1.BackendAction.CREATE,
|
|
267
|
+
script: jest.fn(),
|
|
268
|
+
};
|
|
269
|
+
wrapper.storageTrigger(trigger);
|
|
270
|
+
expect(mockChannel).toHaveBeenCalledWith('storage-trigger');
|
|
271
|
+
expect(mockOn).toHaveBeenCalledWith('postgres_changes', {
|
|
272
|
+
event: 'INSERT',
|
|
273
|
+
schema: 'storage',
|
|
274
|
+
table: 'objects',
|
|
275
|
+
}, expect.any(Function));
|
|
276
|
+
expect(mockSubscribe).toHaveBeenCalled();
|
|
277
|
+
});
|
|
278
|
+
it('should handle multiple events', () => {
|
|
279
|
+
const trigger = {
|
|
280
|
+
name: 'multi-storage',
|
|
281
|
+
event: [backend_1.BackendAction.CREATE, backend_1.BackendAction.DELETE],
|
|
282
|
+
script: jest.fn(),
|
|
283
|
+
};
|
|
284
|
+
wrapper.storageTrigger(trigger);
|
|
285
|
+
expect(mockChannel).toHaveBeenCalledWith('multi-storage-create');
|
|
286
|
+
expect(mockChannel).toHaveBeenCalledWith('multi-storage-delete');
|
|
287
|
+
expect(mockSubscribe).toHaveBeenCalledTimes(2);
|
|
288
|
+
});
|
|
289
|
+
it('should throw TypeError if script is not a function', () => {
|
|
290
|
+
const trigger = {
|
|
291
|
+
name: 'bad-storage',
|
|
292
|
+
event: backend_1.BackendAction.CREATE,
|
|
293
|
+
script: 123,
|
|
294
|
+
};
|
|
295
|
+
expect(() => wrapper.storageTrigger(trigger)).toThrow(TypeError);
|
|
296
|
+
});
|
|
297
|
+
it('should transform payload and execute script', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
298
|
+
const scriptMock = jest.fn().mockResolvedValue(undefined);
|
|
299
|
+
const trigger = {
|
|
300
|
+
name: 'storage-trigger',
|
|
301
|
+
event: backend_1.BackendAction.CREATE,
|
|
302
|
+
script: scriptMock,
|
|
303
|
+
};
|
|
304
|
+
wrapper.storageTrigger(trigger);
|
|
305
|
+
const callback = mockOn.mock.calls[0][2];
|
|
306
|
+
const payload = {
|
|
307
|
+
old: {},
|
|
308
|
+
new: {
|
|
309
|
+
bucket_id: 'uploads',
|
|
310
|
+
name: 'test.jpg',
|
|
311
|
+
metadata: { size: 1024, mimetype: 'image/jpeg' },
|
|
312
|
+
},
|
|
313
|
+
context: 'test-context',
|
|
314
|
+
};
|
|
315
|
+
yield callback(payload);
|
|
316
|
+
expect(scriptMock).toHaveBeenCalledWith({
|
|
317
|
+
before: undefined,
|
|
318
|
+
after: {
|
|
319
|
+
host: 'supabase',
|
|
320
|
+
bucket: 'uploads',
|
|
321
|
+
ref: 'test.jpg',
|
|
322
|
+
contentType: 'image/jpeg',
|
|
323
|
+
size: 1024,
|
|
324
|
+
},
|
|
325
|
+
context: { context: 'test-context' },
|
|
326
|
+
});
|
|
327
|
+
}));
|
|
328
|
+
});
|
|
329
|
+
describe('_payload2File()', () => {
|
|
330
|
+
beforeEach(() => {
|
|
331
|
+
wrapper = new SupabaseCloudWrapper_1.SupabaseCloudWrapper({
|
|
332
|
+
url: 'https://test.supabase.co',
|
|
333
|
+
key: 'test-key',
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
it('should convert valid payload to FileType', () => {
|
|
337
|
+
const payload = {
|
|
338
|
+
bucket_id: 'avatars',
|
|
339
|
+
name: 'profile.png',
|
|
340
|
+
metadata: {
|
|
341
|
+
size: 2048,
|
|
342
|
+
mimetype: 'image/png',
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
const result = wrapper._payload2File(payload);
|
|
346
|
+
expect(result).toEqual({
|
|
347
|
+
host: 'supabase',
|
|
348
|
+
bucket: 'avatars',
|
|
349
|
+
ref: 'profile.png',
|
|
350
|
+
contentType: 'image/png',
|
|
351
|
+
size: 2048,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
it('should return undefined for payload without metadata', () => {
|
|
355
|
+
const payload = {
|
|
356
|
+
bucket_id: 'uploads',
|
|
357
|
+
name: 'file.txt',
|
|
358
|
+
};
|
|
359
|
+
const result = wrapper._payload2File(payload);
|
|
360
|
+
expect(result).toBeUndefined();
|
|
361
|
+
});
|
|
362
|
+
it('should return undefined for empty payload', () => {
|
|
363
|
+
const payload = {};
|
|
364
|
+
const result = wrapper._payload2File(payload);
|
|
365
|
+
expect(result).toBeUndefined();
|
|
366
|
+
});
|
|
367
|
+
it('should handle missing size or mimetype', () => {
|
|
368
|
+
const payload = {
|
|
369
|
+
bucket_id: 'docs',
|
|
370
|
+
name: 'document.pdf',
|
|
371
|
+
metadata: {},
|
|
372
|
+
};
|
|
373
|
+
const result = wrapper._payload2File(payload);
|
|
374
|
+
expect(result).toEqual({
|
|
375
|
+
host: 'supabase',
|
|
376
|
+
bucket: 'docs',
|
|
377
|
+
ref: 'document.pdf',
|
|
378
|
+
contentType: undefined,
|
|
379
|
+
size: undefined,
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
describe('Module Exports', () => {
|
|
384
|
+
it('should export SupabaseCloudWrapper', () => {
|
|
385
|
+
expect(SupabaseCloudWrapper_1.SupabaseCloudWrapper).toBeDefined();
|
|
386
|
+
expect(typeof SupabaseCloudWrapper_1.SupabaseCloudWrapper).toBe('function');
|
|
387
|
+
});
|
|
388
|
+
it('should export eventMap', () => {
|
|
389
|
+
expect(SupabaseCloudWrapper_1.eventMap).toBeDefined();
|
|
390
|
+
expect(typeof SupabaseCloudWrapper_1.eventMap).toBe('object');
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SupabaseCloudWrapper = void 0;
|
|
4
|
+
const SupabaseCloudWrapper_1 = require("./SupabaseCloudWrapper");
|
|
5
|
+
Object.defineProperty(exports, "SupabaseCloudWrapper", { enumerable: true, get: function () { return SupabaseCloudWrapper_1.SupabaseCloudWrapper; } });
|
package/package.json
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
2
|
+
"name": "@quatrain/cloudwrapper-supabase",
|
|
3
|
+
"version": "1.1.27",
|
|
4
|
+
"license": "AGPL-3.0-only",
|
|
5
|
+
"description": "Cloud Wrapper adapter for Supabase",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/quatrain/Core.git",
|
|
9
|
+
"directory": "packages/cloudwrapper-supabase"
|
|
10
|
+
},
|
|
11
|
+
"main": "dist/index.js",
|
|
12
|
+
"types": "dist/index.d.ts",
|
|
13
|
+
"bun": "src/index.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"LICENSE.md",
|
|
16
|
+
"src/",
|
|
17
|
+
"dist/",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"author": "Quatrain Développement SAS <developers@quatrain.com>",
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@quatrain/core": "^1.1.44"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@quatrain/backend": "^1.1.29",
|
|
26
|
+
"@quatrain/cloudwrapper": "^1.1.18",
|
|
27
|
+
"@quatrain/core": "^1.1.44",
|
|
28
|
+
"@quatrain/storage": "^1.1.24",
|
|
29
|
+
"@supabase/supabase-js": "^2.87.3",
|
|
30
|
+
"ws": "^8.18.3"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@tsconfig/recommended": "^1.0.1",
|
|
34
|
+
"@types/jest": "^30.0.0",
|
|
35
|
+
"@types/node": "^22.10.1",
|
|
36
|
+
"@types/ws": "^8.18.1",
|
|
37
|
+
"jest": "^30.2.0",
|
|
38
|
+
"jest-node-exports-resolver": "^1.1.6",
|
|
39
|
+
"jest-serial-runner": "^1.2.2",
|
|
40
|
+
"trace-unhandled": "^2.0.1",
|
|
41
|
+
"ts-jest": "^29.4.1",
|
|
42
|
+
"ts-node": "^10.9.1",
|
|
43
|
+
"typescript": "^5.2.2"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test-ci": "jest --runInBand",
|
|
47
|
+
"build": "tsc",
|
|
48
|
+
"wbuild": "tsc --watch",
|
|
49
|
+
"bump-to": "yarn version"
|
|
50
|
+
}
|
|
51
|
+
}
|