@svadmin/supabase 0.9.10 → 0.11.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/supabase",
3
- "version": "0.9.10",
3
+ "version": "0.11.0",
4
4
  "description": "Supabase adapters — DataProvider, AuthProvider, LiveProvider, AuditHandler",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -13,12 +13,17 @@
13
13
  ".": {
14
14
  "types": "./src/index.ts",
15
15
  "default": "./src/index.ts"
16
+ },
17
+ "./supacloud": {
18
+ "types": "./src/supacloud.ts",
19
+ "default": "./src/supacloud.ts"
16
20
  }
17
21
  },
18
22
  "peerDependencies": {
19
- "@svadmin/core": "^0.23.6",
23
+ "@svadmin/core": "^0.25.0",
20
24
  "@supabase/supabase-js": "^2.0.0",
21
- "@refinedev/supabase": "^5.0.0"
25
+ "@refinedev/supabase": "^5.0.0",
26
+ "@supacloud/js": "^0.10.0"
22
27
  },
23
28
  "license": "MIT",
24
29
  "author": "zuohuadong",
@@ -40,6 +45,9 @@
40
45
  },
41
46
  "@refinedev/supabase": {
42
47
  "optional": true
48
+ },
49
+ "@supacloud/js": {
50
+ "optional": true
43
51
  }
44
52
  },
45
53
  "dependencies": {
package/src/index.ts CHANGED
@@ -4,3 +4,13 @@ export { createSupabaseDataProvider } from './data-provider';
4
4
  export { createSupabaseAuthProvider } from './auth-provider';
5
5
  export { createSupabaseLiveProvider } from './live-provider';
6
6
  export { createSupabaseAuditHandler } from './audit-handler';
7
+ export {
8
+ createSupaCloudTaskProvider,
9
+ createSupaCloudTaskLiveProvider,
10
+ } from './supacloud';
11
+ export type {
12
+ SupaCloudTaskClient,
13
+ SupaCloudTaskRecord,
14
+ CreateSupaCloudTaskProviderOptions,
15
+ CreateSupaCloudTaskLiveProviderOptions,
16
+ } from './supacloud';
@@ -212,3 +212,114 @@ describe('Supabase LiveProvider', () => {
212
212
  expect(sendArgs.payload.type).toBe('INSERT');
213
213
  });
214
214
  });
215
+
216
+
217
+ // ─── SupaCloud Task Provider Tests ───────────────────────────────
218
+ describe('SupaCloud Task Provider', () => {
219
+ test('submit proxies to supacloud tasks client', async () => {
220
+ const { createSupaCloudTaskProvider } = await import('./supacloud');
221
+ const taskHandle = {
222
+ id: 'task-1',
223
+ wait: mock(async () => ({ id: 'task-1', status: 'done' })),
224
+ };
225
+ const supacloud = {
226
+ submit: mock(async () => taskHandle),
227
+ get: mock(async () => ({ id: 'task-1', status: 'done' })),
228
+ };
229
+
230
+ const provider = createSupaCloudTaskProvider({ supacloud });
231
+ const result = await provider.submit('image.generate', {
232
+ body: { prompt: 'poster' },
233
+ idempotencyKey: 'job-1',
234
+ });
235
+
236
+ expect(supacloud.submit).toHaveBeenCalledWith('image.generate', {
237
+ body: { prompt: 'poster' },
238
+ idempotencyKey: 'job-1',
239
+ });
240
+ expect(result).toBe(taskHandle);
241
+ });
242
+
243
+ test('list normalizes object payload with data field', async () => {
244
+ const { createSupaCloudTaskProvider } = await import('./supacloud');
245
+ const supacloud: import('@svadmin/core').TaskProvider<{ id: string; status: string }> = {
246
+ submit: mock(async () => ({ wait: async () => ({ id: 'task-1', status: 'queued' }) })),
247
+ get: mock(async () => ({ id: 'task-1', status: 'queued' })),
248
+ list: mock(async () => ({ data: [{ id: 'task-1', status: 'queued' }] })),
249
+ };
250
+
251
+ const provider = createSupaCloudTaskProvider({ supacloud });
252
+ if (!provider.list) throw new Error('provider.list should exist');
253
+ const tasks = await provider.list();
254
+
255
+ expect(tasks).toEqual({
256
+ data: [{ id: 'task-1', status: 'queued' }],
257
+ total: 1,
258
+ });
259
+ });
260
+
261
+ test('listDlq throws clear error when capability is missing', async () => {
262
+ const { createSupaCloudTaskProvider } = await import('./supacloud');
263
+ const supacloud: import('@svadmin/core').TaskProvider<{ id: string }> = {
264
+ submit: mock(async () => ({ wait: async () => ({ id: 'task-1' }) })),
265
+ get: mock(async () => ({ id: 'task-1' })),
266
+ };
267
+
268
+ const provider = createSupaCloudTaskProvider({ supacloud });
269
+ if (!provider.listDlq) throw new Error('provider.listDlq should exist');
270
+
271
+ await expect(provider.listDlq()).rejects.toThrow('tasks.listDlq');
272
+ });
273
+ });
274
+
275
+
276
+ // ─── SupaCloud Task LiveProvider Tests ───────────────────────────
277
+ describe('SupaCloud Task LiveProvider', () => {
278
+ test('subscribe requires taskId in liveParams', async () => {
279
+ const { createSupaCloudTaskLiveProvider } = await import('./supacloud');
280
+ const supacloud = {
281
+ subscribe: mock(() => ({ unsubscribe: mock(() => {}) })),
282
+ };
283
+
284
+ const live = createSupaCloudTaskLiveProvider({ supacloud });
285
+
286
+ expect(() =>
287
+ live.subscribe({
288
+ resource: 'tasks',
289
+ callback: () => {},
290
+ }),
291
+ ).toThrow('liveParams.taskId');
292
+ });
293
+
294
+ test('subscribe maps task updates into svadmin live events', async () => {
295
+ const { createSupaCloudTaskLiveProvider } = await import('./supacloud');
296
+ let receivedTaskCallback: ((task: Record<string, unknown>) => void) | undefined;
297
+ const unsubscribe = mock(() => {});
298
+ const supacloud = {
299
+ subscribe: mock((taskId: string, callback: (task: Record<string, unknown>) => void) => {
300
+ receivedTaskCallback = callback;
301
+ expect(taskId).toBe('task-42');
302
+ return { unsubscribe };
303
+ }),
304
+ };
305
+
306
+ const live = createSupaCloudTaskLiveProvider({ supacloud });
307
+ const callback = mock(() => {});
308
+ const stop = live.subscribe({
309
+ resource: 'tasks',
310
+ liveParams: { taskId: 'task-42' },
311
+ callback,
312
+ });
313
+
314
+ receivedTaskCallback?.({ id: 'task-42', status: 'running' });
315
+
316
+ expect(callback).toHaveBeenCalledWith({
317
+ type: 'UPDATE',
318
+ resource: 'tasks',
319
+ payload: { id: 'task-42', status: 'running' },
320
+ });
321
+
322
+ stop();
323
+ expect(unsubscribe).toHaveBeenCalled();
324
+ });
325
+ });
@@ -0,0 +1,144 @@
1
+ import type {
2
+ LiveEvent,
3
+ LiveProvider,
4
+ SubmitTaskOptions,
5
+ TaskHandle,
6
+ TaskListResult,
7
+ TaskProvider,
8
+ TaskRecord,
9
+ TaskSubscription,
10
+ } from '@svadmin/core';
11
+
12
+ export interface SupaCloudTaskRecord extends TaskRecord {
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ export interface SupaCloudTaskClient<TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord> {
17
+ submit(taskName: string, options?: SubmitTaskOptions): Promise<TaskHandle<TTask>>;
18
+ get(taskId: string): Promise<TTask>;
19
+ list?(params?: Record<string, unknown>): Promise<TTask[] | { data?: TTask[] }>;
20
+ listDlq?(params?: Record<string, unknown>): Promise<TTask[] | { data?: TTask[] }>;
21
+ cancel?(taskId: string): Promise<unknown>;
22
+ retry?(taskId: string): Promise<unknown>;
23
+ subscribe?(
24
+ taskId: string,
25
+ callback: (task: TTask) => void
26
+ ): TaskSubscription | (() => void) | void;
27
+ }
28
+
29
+ export interface CreateSupaCloudTaskProviderOptions<
30
+ TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord,
31
+ > {
32
+ supacloud: SupaCloudTaskClient<TTask>;
33
+ }
34
+
35
+ export interface CreateSupaCloudTaskLiveProviderOptions<
36
+ TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord,
37
+ > {
38
+ supacloud: Pick<SupaCloudTaskClient<TTask>, 'subscribe'>;
39
+ resource?: string;
40
+ mapTaskToEvent?: (task: TTask, resource: string) => LiveEvent;
41
+ }
42
+
43
+ function normalizeTaskSubscription(
44
+ subscription: TaskSubscription | (() => void) | void,
45
+ ): (() => void) | undefined {
46
+ if (!subscription) return undefined;
47
+ if (typeof subscription === 'function') return subscription;
48
+ if (typeof subscription.unsubscribe === 'function') {
49
+ return () => subscription.unsubscribe();
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ function normalizeTaskList<TTask extends SupaCloudTaskRecord>(
55
+ value: TTask[] | { data?: TTask[] },
56
+ ): TaskListResult<TTask> {
57
+ const data = Array.isArray(value) ? value : (value.data ?? []);
58
+ return {
59
+ data,
60
+ total: data.length,
61
+ };
62
+ }
63
+
64
+ function defaultMapTaskToEvent<TTask extends SupaCloudTaskRecord>(
65
+ task: TTask,
66
+ resource: string,
67
+ ): LiveEvent {
68
+ return {
69
+ type: 'UPDATE',
70
+ resource,
71
+ payload: task as Record<string, unknown>,
72
+ };
73
+ }
74
+
75
+ export function createSupaCloudTaskProvider<
76
+ TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord,
77
+ >(options: CreateSupaCloudTaskProviderOptions<TTask>): TaskProvider<TTask> {
78
+ const { supacloud } = options;
79
+
80
+ return {
81
+ submit(taskName, submitOptions) {
82
+ return supacloud.submit(taskName, submitOptions);
83
+ },
84
+ get(taskId) {
85
+ return supacloud.get(taskId);
86
+ },
87
+ async list(params) {
88
+ if (!supacloud.list) {
89
+ throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.list');
90
+ }
91
+ return normalizeTaskList(await supacloud.list(params));
92
+ },
93
+ async listDlq(params) {
94
+ if (!supacloud.listDlq) {
95
+ throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.listDlq');
96
+ }
97
+ return normalizeTaskList(await supacloud.listDlq(params));
98
+ },
99
+ cancel(taskId) {
100
+ if (!supacloud.cancel) {
101
+ throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.cancel');
102
+ }
103
+ return supacloud.cancel(taskId);
104
+ },
105
+ retry(taskId) {
106
+ if (!supacloud.retry) {
107
+ throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.retry');
108
+ }
109
+ return supacloud.retry(taskId);
110
+ },
111
+ subscribe(taskId, callback) {
112
+ if (!supacloud.subscribe) {
113
+ throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.subscribe');
114
+ }
115
+ return supacloud.subscribe(taskId, callback);
116
+ },
117
+ };
118
+ }
119
+
120
+ export function createSupaCloudTaskLiveProvider<
121
+ TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord,
122
+ >(options: CreateSupaCloudTaskLiveProviderOptions<TTask>): LiveProvider {
123
+ const resourceName = options.resource ?? 'tasks';
124
+ const mapTaskToEvent = options.mapTaskToEvent ?? defaultMapTaskToEvent<TTask>;
125
+
126
+ return {
127
+ subscribe({ resource, liveParams, callback }) {
128
+ if (!options.supacloud.subscribe) {
129
+ throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.subscribe');
130
+ }
131
+
132
+ const taskId = typeof liveParams?.taskId === 'string' ? liveParams.taskId : undefined;
133
+ if (!taskId) {
134
+ throw new Error('[svadmin/supabase] createSupaCloudTaskLiveProvider requires liveParams.taskId');
135
+ }
136
+
137
+ const unsubscribe = options.supacloud.subscribe(taskId, (task) => {
138
+ callback(mapTaskToEvent(task, resource || resourceName));
139
+ });
140
+
141
+ return normalizeTaskSubscription(unsubscribe) ?? (() => {});
142
+ },
143
+ };
144
+ }