@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 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 Error(`Passed script value is not a function`);
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
- this._supabaseClient = (0, supabase_js_1.createClient)(this._params.url, this._params.key);
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.19",
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.21",
23
+ "@quatrain/backend": "^1.1.22",
24
24
  "@quatrain/cloudwrapper": "^1.1.14",
25
- "@supabase/supabase-js": "2.53.0"
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 Error(`Passed script value is not a function`)
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
- this._supabaseClient = createClient(this._params.url, this._params.key)
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
- public poll() {
185
- CloudWrapper.info(
186
- `Supabase client status: ${this._supabaseClient?.channel}`
187
- )
188
- this._channels.storage.forEach((channel) => {
189
- CloudWrapper.info(`${channel.name}: ${channel.state}`)
190
- console.log(channel.channel?.params)
191
- })
192
- setTimeout(() => this.poll(), 10000)
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: {