@quatrain/cloudwrapper-supabase 1.1.19 → 1.1.21
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/README.md +54 -0
- package/lib/SupabaseCloudWrapper.d.ts +4 -1
- package/lib/SupabaseCloudWrapper.js +52 -12
- package/package.json +3 -3
- package/src/SupabaseCloudWrapper.ts +73 -13
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# @quatrain/cloudwrapper-supabase
|
|
2
|
+
|
|
3
|
+
This package provides a Cloud Wrapper adapter for Supabase Edge Functions. It allows you to invoke serverless functions in a consistent way across different BaaS providers.
|
|
4
|
+
This package provides a Cloud Wrapper for Supabase, enabling real-time database and storage triggers. It handles the WebSocket connection to Supabase's Realtime service, providing robust connection monitoring and configurable automatic reconnection logic.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Listens to database changes (`INSERT`, `UPDATE`, `DELETE`) on your Supabase Postgres database.
|
|
9
|
+
- Listens to storage changes in Supabase Storage.
|
|
10
|
+
- Monitors connection health with a heartbeat mechanism.
|
|
11
|
+
- Provides a configurable strategy for handling disconnections (exit process or attempt to reconnect).
|
|
12
|
+
- Consistent interface provided by `@quatrain/cloudwrapper`.
|
|
13
|
+
- Works with both SaaS and self-hosted Supabase instances.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# With npm
|
|
19
|
+
npm install @quatrain/cloudwrapper-supabase @supabase/supabase-js
|
|
20
|
+
|
|
21
|
+
# With yarn
|
|
22
|
+
yarn add @quatrain/cloudwrapper-supabase @supabase/supabase-js
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { CloudWrapper } from '@quatrain/cloudwrapper'
|
|
29
|
+
import { SupabaseCloudWrapperAdapter } from '@quatrain/cloudwrapper-supabase'
|
|
30
|
+
import { SupabaseCloudWrapper } from '@quatrain/cloudwrapper-supabase'
|
|
31
|
+
import { BackendAction } from '@quatrain/backend'
|
|
32
|
+
|
|
33
|
+
// Setup and use the adapter with the CloudWrapper singleton.
|
|
34
|
+
const wrapper = new SupabaseCloudWrapper({
|
|
35
|
+
url: process.env.SUPABASE_URL,
|
|
36
|
+
key: process.env.SUPABASE_KEY,
|
|
37
|
+
// Optional: Determines behavior on disconnection.
|
|
38
|
+
// - true (default): Exits the process. Ideal for containerized environments
|
|
39
|
+
// (e.g., Kubernetes) that will automatically restart the service.
|
|
40
|
+
// - false: Attempts to reconnect internally.
|
|
41
|
+
exitOnDisconnect: true,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Example: Set up a trigger for new rows in a 'profiles' table
|
|
45
|
+
wrapper.databaseTrigger({
|
|
46
|
+
name: 'new-profile-trigger',
|
|
47
|
+
model: 'profiles',
|
|
48
|
+
event: BackendAction.CREATE,
|
|
49
|
+
script: async ({ after }) => {
|
|
50
|
+
console.log(`A new profile was created:`, after)
|
|
51
|
+
// Add your business logic here
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
```
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { AbstractCloudWrapper, DatabaseTriggerType, StorageTriggerType } from '@quatrain/cloudwrapper';
|
|
2
3
|
import { SupabaseClient, RealtimeChannel } from '@supabase/supabase-js';
|
|
3
4
|
import { FileType } from '@quatrain/storage';
|
|
@@ -5,6 +6,7 @@ export type SupabaseParams = {
|
|
|
5
6
|
useEmulator?: boolean;
|
|
6
7
|
url?: string;
|
|
7
8
|
key?: string;
|
|
9
|
+
exitOnDisconnect?: boolean;
|
|
8
10
|
};
|
|
9
11
|
export type Channel = {
|
|
10
12
|
name: string;
|
|
@@ -24,6 +26,8 @@ export declare class SupabaseCloudWrapper extends AbstractCloudWrapper {
|
|
|
24
26
|
protected _supabaseClient: SupabaseClient | undefined;
|
|
25
27
|
protected _realtimeClient: RealtimeChannel | undefined;
|
|
26
28
|
protected _isInitialized: boolean;
|
|
29
|
+
protected _heartbeatOkReceived: boolean;
|
|
30
|
+
protected _connectionTimeout: NodeJS.Timeout | undefined;
|
|
27
31
|
protected _channels: Channels;
|
|
28
32
|
constructor(params: SupabaseParams);
|
|
29
33
|
databaseTrigger(trigger: DatabaseTriggerType): void | {
|
|
@@ -37,7 +41,6 @@ export declare class SupabaseCloudWrapper extends AbstractCloudWrapper {
|
|
|
37
41
|
table: string;
|
|
38
42
|
};
|
|
39
43
|
protected _initialize(): void;
|
|
40
|
-
poll(): void;
|
|
41
44
|
protected _payload2File(payload: {
|
|
42
45
|
[x: string]: any;
|
|
43
46
|
}): FileType | undefined;
|
|
@@ -33,6 +33,7 @@ class SupabaseCloudWrapper extends cloudwrapper_1.AbstractCloudWrapper {
|
|
|
33
33
|
constructor(params) {
|
|
34
34
|
super(params);
|
|
35
35
|
this._isInitialized = false;
|
|
36
|
+
this._heartbeatOkReceived = false;
|
|
36
37
|
this._channels = { database: [], storage: [] };
|
|
37
38
|
this._initialize();
|
|
38
39
|
}
|
|
@@ -86,7 +87,7 @@ class SupabaseCloudWrapper extends cloudwrapper_1.AbstractCloudWrapper {
|
|
|
86
87
|
var _a;
|
|
87
88
|
this._initialize();
|
|
88
89
|
if (typeof trigger.script !== 'function') {
|
|
89
|
-
throw new
|
|
90
|
+
throw new TypeError(`Passed script value is not a function`);
|
|
90
91
|
}
|
|
91
92
|
if (Array.isArray(trigger.event)) {
|
|
92
93
|
const params = trigger.event.forEach((event) => {
|
|
@@ -131,21 +132,60 @@ class SupabaseCloudWrapper extends cloudwrapper_1.AbstractCloudWrapper {
|
|
|
131
132
|
}
|
|
132
133
|
_initialize() {
|
|
133
134
|
if (this._isInitialized === false) {
|
|
134
|
-
|
|
135
|
+
cloudwrapper_1.CloudWrapper.info(`Starting Supabase client with heartbeat monitoring`);
|
|
136
|
+
if (this._connectionTimeout) {
|
|
137
|
+
clearTimeout(this._connectionTimeout);
|
|
138
|
+
}
|
|
139
|
+
this._supabaseClient = (0, supabase_js_1.createClient)(this._params.url, this._params.key, {
|
|
140
|
+
realtime: {
|
|
141
|
+
heartbeatIntervalMs: 5000,
|
|
142
|
+
heartbeatCallback: (status) => {
|
|
143
|
+
console.log('status received', status);
|
|
144
|
+
switch (status) {
|
|
145
|
+
case 'ok':
|
|
146
|
+
if (!this._heartbeatOkReceived) {
|
|
147
|
+
cloudwrapper_1.CloudWrapper.info(`🛜 Supabase Realtime connection established`);
|
|
148
|
+
if (this._connectionTimeout) {
|
|
149
|
+
clearTimeout(this._connectionTimeout);
|
|
150
|
+
}
|
|
151
|
+
this._heartbeatOkReceived = true;
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
case 'timeout':
|
|
155
|
+
cloudwrapper_1.CloudWrapper.warn(`⚠️ Supabase Realtime connection timed out`);
|
|
156
|
+
break;
|
|
157
|
+
case 'disconnected':
|
|
158
|
+
// Reconnect only if exitOnDisconnect is explicitly set to false.
|
|
159
|
+
if (this._params.exitOnDisconnect === false) {
|
|
160
|
+
cloudwrapper_1.CloudWrapper.warn(`❌ Supabase connection lost. Attempting to reconnect...`);
|
|
161
|
+
this._isInitialized = false;
|
|
162
|
+
this._heartbeatOkReceived = false;
|
|
163
|
+
this._initialize();
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Default behavior: exit to allow for a clean restart by the orchestrator.
|
|
167
|
+
cloudwrapper_1.CloudWrapper.error(`❌ Supabase connection lost. Exiting to allow for a clean restart.`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
// do nothing
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
135
178
|
this._isInitialized = true;
|
|
136
179
|
cloudwrapper_1.CloudWrapper.info(`Supabase App initialized`);
|
|
180
|
+
cloudwrapper_1.CloudWrapper.info(`🕘 Supabase Realtime connection is being established`);
|
|
181
|
+
this._connectionTimeout = setTimeout(() => {
|
|
182
|
+
if (!this._heartbeatOkReceived) {
|
|
183
|
+
cloudwrapper_1.CloudWrapper.error(`❌ Supabase Realtime connection failed to establish after 30 seconds. Exiting.`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
}, 30000);
|
|
137
187
|
}
|
|
138
188
|
}
|
|
139
|
-
poll() {
|
|
140
|
-
var _a;
|
|
141
|
-
cloudwrapper_1.CloudWrapper.info(`Supabase client status: ${(_a = this._supabaseClient) === null || _a === void 0 ? void 0 : _a.channel}`);
|
|
142
|
-
this._channels.storage.forEach((channel) => {
|
|
143
|
-
var _a;
|
|
144
|
-
cloudwrapper_1.CloudWrapper.info(`${channel.name}: ${channel.state}`);
|
|
145
|
-
console.log((_a = channel.channel) === null || _a === void 0 ? void 0 : _a.params);
|
|
146
|
-
});
|
|
147
|
-
setTimeout(() => this.poll(), 10000);
|
|
148
|
-
}
|
|
149
189
|
_payload2File(payload) {
|
|
150
190
|
if (!payload.metadata) {
|
|
151
191
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quatrain/cloudwrapper-supabase",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Cloud Wrapper adapter for Supabase",
|
|
6
6
|
"repository": {
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"@quatrain/core": "^1.1.22"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@quatrain/backend": "^1.1.
|
|
23
|
+
"@quatrain/backend": "^1.1.22",
|
|
24
24
|
"@quatrain/cloudwrapper": "^1.1.14",
|
|
25
|
-
"@supabase/supabase-js": "2.
|
|
25
|
+
"@supabase/supabase-js": "^2.82.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@tsconfig/recommended": "^1.0.1",
|
|
@@ -11,12 +11,14 @@ import {
|
|
|
11
11
|
SupabaseClient,
|
|
12
12
|
RealtimeChannel,
|
|
13
13
|
} from '@supabase/supabase-js'
|
|
14
|
+
import { HeartbeatStatus } from '@supabase/realtime-js/dist/module/RealtimeClient'
|
|
14
15
|
import { FileType } from '@quatrain/storage'
|
|
15
16
|
|
|
16
17
|
export type SupabaseParams = {
|
|
17
18
|
useEmulator?: boolean
|
|
18
19
|
url?: string
|
|
19
20
|
key?: string
|
|
21
|
+
exitOnDisconnect?: boolean
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export type Channel = {
|
|
@@ -40,6 +42,8 @@ export class SupabaseCloudWrapper extends AbstractCloudWrapper {
|
|
|
40
42
|
protected _supabaseClient: SupabaseClient | undefined
|
|
41
43
|
protected _realtimeClient: RealtimeChannel | undefined
|
|
42
44
|
protected _isInitialized = false
|
|
45
|
+
protected _heartbeatOkReceived = false
|
|
46
|
+
protected _connectionTimeout: NodeJS.Timeout | undefined
|
|
43
47
|
protected _channels: Channels = { database: [], storage: [] }
|
|
44
48
|
|
|
45
49
|
constructor(params: SupabaseParams) {
|
|
@@ -114,7 +118,7 @@ export class SupabaseCloudWrapper extends AbstractCloudWrapper {
|
|
|
114
118
|
this._initialize()
|
|
115
119
|
|
|
116
120
|
if (typeof trigger.script !== 'function') {
|
|
117
|
-
throw new
|
|
121
|
+
throw new TypeError(`Passed script value is not a function`)
|
|
118
122
|
}
|
|
119
123
|
|
|
120
124
|
if (Array.isArray(trigger.event)) {
|
|
@@ -175,21 +179,77 @@ export class SupabaseCloudWrapper extends AbstractCloudWrapper {
|
|
|
175
179
|
|
|
176
180
|
protected _initialize() {
|
|
177
181
|
if (this._isInitialized === false) {
|
|
178
|
-
|
|
182
|
+
CloudWrapper.info(`Starting Supabase client with heartbeat monitoring`)
|
|
183
|
+
|
|
184
|
+
if (this._connectionTimeout) {
|
|
185
|
+
clearTimeout(this._connectionTimeout)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this._supabaseClient = createClient(
|
|
189
|
+
this._params.url,
|
|
190
|
+
this._params.key,
|
|
191
|
+
{
|
|
192
|
+
realtime: {
|
|
193
|
+
heartbeatIntervalMs: 5000,
|
|
194
|
+
heartbeatCallback: (status: HeartbeatStatus) => {
|
|
195
|
+
console.log('status received', status)
|
|
196
|
+
switch (status) {
|
|
197
|
+
case 'ok':
|
|
198
|
+
if (!this._heartbeatOkReceived) {
|
|
199
|
+
CloudWrapper.info(
|
|
200
|
+
`🛜 Supabase Realtime connection established`
|
|
201
|
+
)
|
|
202
|
+
if (this._connectionTimeout) {
|
|
203
|
+
clearTimeout(this._connectionTimeout)
|
|
204
|
+
}
|
|
205
|
+
this._heartbeatOkReceived = true
|
|
206
|
+
}
|
|
207
|
+
break
|
|
208
|
+
case 'timeout':
|
|
209
|
+
CloudWrapper.warn(
|
|
210
|
+
`⚠️ Supabase Realtime connection timed out`
|
|
211
|
+
)
|
|
212
|
+
break
|
|
213
|
+
case 'disconnected':
|
|
214
|
+
// Reconnect only if exitOnDisconnect is explicitly set to false.
|
|
215
|
+
if (this._params.exitOnDisconnect === false) {
|
|
216
|
+
CloudWrapper.warn(
|
|
217
|
+
`❌ Supabase connection lost. Attempting to reconnect...`
|
|
218
|
+
)
|
|
219
|
+
this._isInitialized = false
|
|
220
|
+
this._heartbeatOkReceived = false
|
|
221
|
+
this._initialize()
|
|
222
|
+
} else {
|
|
223
|
+
// Default behavior: exit to allow for a clean restart by the orchestrator.
|
|
224
|
+
CloudWrapper.error(
|
|
225
|
+
`❌ Supabase connection lost. Exiting to allow for a clean restart.`
|
|
226
|
+
)
|
|
227
|
+
process.exit(1)
|
|
228
|
+
}
|
|
229
|
+
break
|
|
230
|
+
default:
|
|
231
|
+
// do nothing
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
}
|
|
237
|
+
)
|
|
179
238
|
this._isInitialized = true
|
|
180
239
|
CloudWrapper.info(`Supabase App initialized`)
|
|
181
|
-
|
|
182
|
-
|
|
240
|
+
CloudWrapper.info(
|
|
241
|
+
`🕘 Supabase Realtime connection is being established`
|
|
242
|
+
)
|
|
183
243
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
244
|
+
this._connectionTimeout = setTimeout(() => {
|
|
245
|
+
if (!this._heartbeatOkReceived) {
|
|
246
|
+
CloudWrapper.error(
|
|
247
|
+
`❌ Supabase Realtime connection failed to establish after 30 seconds. Exiting.`
|
|
248
|
+
)
|
|
249
|
+
process.exit(1)
|
|
250
|
+
}
|
|
251
|
+
}, 30000)
|
|
252
|
+
}
|
|
193
253
|
}
|
|
194
254
|
|
|
195
255
|
protected _payload2File(payload: {
|