@svadmin/supabase 0.11.1 → 0.11.3

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,11 +1,15 @@
1
1
  {
2
2
  "name": "@svadmin/supabase",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "description": "Supabase adapters — DataProvider, AuthProvider, LiveProvider, AuditHandler",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "files": [
8
- "src/**/*"
8
+ "src/**/*.ts",
9
+ "!src/**/*.test.*",
10
+ "!src/**/*.spec.*",
11
+ "!src/**/vitest.config.*",
12
+ "!src/**/setupTest.*"
9
13
  ],
10
14
  "main": "src/index.ts",
11
15
  "types": "src/index.ts",
@@ -20,7 +24,7 @@
20
24
  }
21
25
  },
22
26
  "peerDependencies": {
23
- "@svadmin/core": "^0.25.2",
27
+ "@svadmin/core": "^0.27.1",
24
28
  "@supabase/supabase-js": "^2.0.0",
25
29
  "@refinedev/supabase": "^5.0.0",
26
30
  "@supacloud/js": "^0.12.0"
@@ -1,7 +1,6 @@
1
1
  import type { DataProvider } from '@svadmin/core';
2
2
  import { createRefineAdapter } from '@svadmin/refine-adapter';
3
3
  // Static import — dynamic import() gets dropped by Vite/Rollup in static SPA builds
4
- // @ts-ignore Peer dependency
5
4
  import { dataProvider as refineDataProvider } from '@refinedev/supabase';
6
5
 
7
6
  /**
@@ -54,7 +54,7 @@ export function createSupabaseLiveProvider(client: SupabaseClient): LiveProvider
54
54
  return {
55
55
  subscribe({ resource, callback }) {
56
56
  if (!subscribers.has(resource)) subscribers.set(resource, new Set());
57
- subscribers.get(resource)!.add(callback);
57
+ subscribers.get(resource)?.add(callback);
58
58
 
59
59
  getOrCreateChannel(resource);
60
60
 
package/src/supacloud.ts CHANGED
@@ -23,7 +23,7 @@ export interface SupaCloudTaskClient<TTask extends SupaCloudTaskRecord = SupaClo
23
23
  subscribe?(
24
24
  taskId: string,
25
25
  callback: (task: TTask) => void
26
- ): TaskSubscription | (() => void) | void;
26
+ ): TaskSubscription | (() => void) | undefined;
27
27
  }
28
28
 
29
29
  export interface CreateSupaCloudTaskProviderOptions<
@@ -41,7 +41,7 @@ export interface CreateSupaCloudTaskLiveProviderOptions<
41
41
  }
42
42
 
43
43
  function normalizeTaskSubscription(
44
- subscription: TaskSubscription | (() => void) | void,
44
+ subscription: TaskSubscription | (() => void) | undefined,
45
45
  ): (() => void) | undefined {
46
46
  if (!subscription) return undefined;
47
47
  if (typeof subscription === 'function') return subscription;
@@ -1,369 +0,0 @@
1
- // @svadmin/supabase — Unit Tests
2
- import { describe, test, expect, mock } from 'bun:test';
3
- import type { SupabaseClient } from '@supabase/supabase-js';
4
-
5
- // ─── Mock @svadmin/core ───────────────────────────────────────────
6
- mock.module('@svadmin/core', () => {
7
- return {
8
- audit: mock(() => {}),
9
- };
10
- });
11
-
12
- // ─── Mock Refine Supabase Provider ────────────────────────────────
13
- mock.module('@refinedev/supabase', () => {
14
- return {
15
- dataProvider: (..._args: any[]) => {
16
- const mockDp: any = {
17
- getList: async () => ({ data: [{ id: 1, title: 'Item 1' }], total: 1 }),
18
- getOne: async () => ({ data: { id: 1, title: 'Item 1' } }),
19
- create: async () => ({ data: { id: 2, title: 'New Item' } }),
20
- update: async () => ({ data: { id: 1, title: 'Updated' } }),
21
- deleteOne: async () => ({ data: { id: 1, title: 'Deleted' } }),
22
- };
23
- return mockDp;
24
- }
25
- };
26
- });
27
-
28
- // ─── Mock Supabase Client ──────────────────────────────────────────
29
- function createMockSupabaseClient(overrides: Record<string, any> = {}) {
30
- const client = {
31
- auth: {
32
- signInWithPassword: mock(async ({ email, password }) => {
33
- if (password === 'bad') return { error: { message: 'Invalid credentials' } };
34
- return { data: { user: { id: 'user-1' } }, error: null };
35
- }),
36
- signOut: mock(async () => ({ error: null })),
37
- getSession: mock(async () => ({ data: { session: { access_token: 'valid-token' } } })),
38
- getUser: mock(async () => ({
39
- data: {
40
- user: {
41
- id: 'user-1',
42
- email: 'admin@test.com',
43
- user_metadata: { name: 'Admin', avatar_url: 'http://avatar', role: 'admin' }
44
- }
45
- }
46
- })),
47
- signUp: mock(async ({ email }) => {
48
- if (email === 'bad@test.com') return { error: { message: 'Signup failed' } };
49
- return { error: null };
50
- }),
51
- resetPasswordForEmail: mock(async () => ({ error: null })),
52
- updateUser: mock(async () => ({ error: null })),
53
- ...(overrides.auth || {}),
54
- },
55
- channel: mock((name: string) => {
56
- const c: any = {
57
- name,
58
- on: mock(() => c),
59
- subscribe: mock(() => c),
60
- unsubscribe: mock(() => c),
61
- send: mock(() => c)
62
- };
63
- return c;
64
- }),
65
- };
66
- // If there are top-level overrides besides auth, merge them
67
- for (const [key, val] of Object.entries(overrides)) {
68
- if (key !== 'auth') (client as any)[key] = val;
69
- }
70
- return client as unknown as SupabaseClient;
71
- }
72
-
73
-
74
- // ─── DataProvider Tests ──────────────────────────────────────────
75
- describe('Supabase DataProvider', () => {
76
- test('getList routes through refine-adapter', async () => {
77
- const { createSupabaseDataProvider } = await import('./data-provider');
78
- const dp = await createSupabaseDataProvider({} as any);
79
- const result = await dp.getList({ resource: 'posts' });
80
- expect(result.data).toHaveLength(1);
81
- expect(result.total).toBe(1);
82
- expect(result.data[0].id).toBe(1);
83
- });
84
-
85
- test('create returns new record', async () => {
86
- const { createSupabaseDataProvider } = await import('./data-provider');
87
- const dp = await createSupabaseDataProvider({} as any);
88
- const result = await dp.create({ resource: 'posts', variables: { title: 'New Item' } });
89
- expect(result.data.id).toBe(2);
90
- });
91
- });
92
-
93
-
94
- // ─── AuthProvider Tests ──────────────────────────────────────────
95
- describe('Supabase AuthProvider', () => {
96
- test('login success', async () => {
97
- const { createSupabaseAuthProvider } = await import('./auth-provider');
98
- const auth = createSupabaseAuthProvider(createMockSupabaseClient());
99
- const result = await auth.login({ email: 'admin@test.com', password: 'pass' });
100
- expect(result.success).toBe(true);
101
- expect(result.redirectTo).toBe('/');
102
- });
103
-
104
- test('login failure', async () => {
105
- const { createSupabaseAuthProvider } = await import('./auth-provider');
106
- const auth = createSupabaseAuthProvider(createMockSupabaseClient());
107
- const result = await auth.login({ email: 'admin@test.com', password: 'bad' });
108
- expect(result.success).toBe(false);
109
- expect(result.error?.message).toBe('Invalid credentials');
110
- });
111
-
112
- test('logout success', async () => {
113
- const { createSupabaseAuthProvider } = await import('./auth-provider');
114
- const auth = createSupabaseAuthProvider(createMockSupabaseClient());
115
- const result = await auth.logout();
116
- expect(result.success).toBe(true);
117
- expect(result.redirectTo).toBe('/login');
118
- });
119
-
120
- test('check returns authenticated when session exists', async () => {
121
- const { createSupabaseAuthProvider } = await import('./auth-provider');
122
- const auth = createSupabaseAuthProvider(createMockSupabaseClient());
123
- const result = await auth.check();
124
- expect(result.authenticated).toBe(true);
125
- });
126
-
127
- test('check returns unauthenticated when no session', async () => {
128
- const { createSupabaseAuthProvider } = await import('./auth-provider');
129
- const auth = createSupabaseAuthProvider(createMockSupabaseClient({
130
- auth: { getSession: mock(async () => ({ data: { session: null } })) }
131
- }));
132
- const result = await auth.check();
133
- expect(result.authenticated).toBe(false);
134
- expect(result.redirectTo).toBe('/login');
135
- expect(result.logout).toBe(true);
136
- });
137
-
138
- test('check clears invalid refresh token sessions', async () => {
139
- const { createSupabaseAuthProvider } = await import('./auth-provider');
140
- const client = createMockSupabaseClient({
141
- auth: {
142
- getSession: mock(async () => ({
143
- data: { session: null },
144
- error: new Error('Refresh token is not valid'),
145
- })),
146
- }
147
- });
148
- const auth = createSupabaseAuthProvider(client);
149
- const result = await auth.check();
150
- expect(result.authenticated).toBe(false);
151
- expect(result.logout).toBe(true);
152
- expect(result.redirectTo).toBe('/login');
153
- expect(client.auth.signOut).toHaveBeenCalled();
154
- });
155
-
156
- test('getIdentity surfaces user metadata', async () => {
157
- const { createSupabaseAuthProvider } = await import('./auth-provider');
158
- const auth = createSupabaseAuthProvider(createMockSupabaseClient());
159
- const identity = await auth.getIdentity();
160
- expect(identity).not.toBeNull();
161
- expect(identity!.id).toBe('user-1');
162
- expect(identity!.name).toBe('Admin');
163
- expect(identity!.avatar).toBe('http://avatar');
164
- });
165
-
166
- test('getPermissions surfaces role from user_metadata', async () => {
167
- const { createSupabaseAuthProvider } = await import('./auth-provider');
168
- const auth = createSupabaseAuthProvider(createMockSupabaseClient());
169
- const role = await auth.getPermissions?.();
170
- expect(role).toBe('admin');
171
- });
172
-
173
- test('getIdentity clears invalid refresh token sessions', async () => {
174
- const { createSupabaseAuthProvider } = await import('./auth-provider');
175
- const client = createMockSupabaseClient({
176
- auth: {
177
- getUser: mock(async () => ({
178
- data: { user: null },
179
- error: new Error('Invalid refresh token'),
180
- })),
181
- }
182
- });
183
- const auth = createSupabaseAuthProvider(client);
184
- const identity = await auth.getIdentity();
185
- expect(identity).toBeNull();
186
- expect(client.auth.signOut).toHaveBeenCalled();
187
- });
188
-
189
- test('onError returns logout true on 401 with no session', async () => {
190
- const { createSupabaseAuthProvider } = await import('./auth-provider');
191
- const client = createMockSupabaseClient({
192
- auth: { getSession: mock(async () => ({ data: { session: null } })) }
193
- });
194
- const auth = createSupabaseAuthProvider(client);
195
- const result = await auth.onError!(new Error('401 Unauthorized'));
196
- expect(result.logout).toBe(true);
197
- expect(result.redirectTo).toBe('/login');
198
- expect(client.auth.signOut).toHaveBeenCalled();
199
- });
200
-
201
- test('onError swallows 401 if token actually exists (race condition guard)', async () => {
202
- const { createSupabaseAuthProvider } = await import('./auth-provider');
203
- const client = createMockSupabaseClient();
204
- const auth = createSupabaseAuthProvider(client);
205
- const result = await auth.onError!(new Error('401 Unauthorized'));
206
- expect(result.logout).toBeUndefined();
207
- expect(client.auth.signOut).not.toHaveBeenCalled();
208
- });
209
-
210
- test('onError logs out on invalid refresh token', async () => {
211
- const { createSupabaseAuthProvider } = await import('./auth-provider');
212
- const client = createMockSupabaseClient();
213
- const auth = createSupabaseAuthProvider(client);
214
- const result = await auth.onError!(new Error('Refresh token is not valid'));
215
- expect(result.logout).toBe(true);
216
- expect(result.redirectTo).toBe('/login');
217
- expect(client.auth.signOut).toHaveBeenCalled();
218
- });
219
- });
220
-
221
-
222
- // ─── LiveProvider Tests ──────────────────────────────────────────
223
- describe('Supabase LiveProvider', () => {
224
- test('subscribe builds channel and binds events', async () => {
225
- const { createSupabaseLiveProvider } = await import('./live-provider');
226
- const client = createMockSupabaseClient();
227
- const live = createSupabaseLiveProvider(client);
228
-
229
- let callbackTriggered = false;
230
- const unsub = live.subscribe({
231
- resource: 'posts',
232
- callback: (event) => { callbackTriggered = true; }
233
- });
234
-
235
- expect(client.channel).toHaveBeenCalledWith('live-posts');
236
- const channelMock = (client.channel as ReturnType<typeof mock>).mock.results[0].value;
237
- expect(channelMock.on).toHaveBeenCalled();
238
- expect(channelMock.subscribe).toHaveBeenCalled();
239
-
240
- unsub();
241
- expect(channelMock.unsubscribe).toHaveBeenCalled();
242
- });
243
-
244
- test('publish triggers broadcast send', async () => {
245
- const { createSupabaseLiveProvider } = await import('./live-provider');
246
- const client = createMockSupabaseClient();
247
- const live = createSupabaseLiveProvider(client);
248
-
249
- live.publish?.({ type: 'INSERT', resource: 'posts', payload: { a: 1 } });
250
-
251
- const channelMock = (client.channel as ReturnType<typeof mock>).mock.results[0].value;
252
- expect(channelMock.send).toHaveBeenCalled();
253
- const sendArgs = channelMock.send.mock.calls[0][0];
254
- expect(sendArgs.type).toBe('broadcast');
255
- expect(sendArgs.event).toBe('live-event');
256
- expect(sendArgs.payload.type).toBe('INSERT');
257
- });
258
- });
259
-
260
-
261
- // ─── SupaCloud Task Provider Tests ───────────────────────────────
262
- describe('SupaCloud Task Provider', () => {
263
- test('submit proxies to supacloud tasks client', async () => {
264
- const { createSupaCloudTaskProvider } = await import('./supacloud');
265
- const taskHandle = {
266
- id: 'task-1',
267
- wait: mock(async () => ({ id: 'task-1', status: 'done' })),
268
- };
269
- const supacloud = {
270
- submit: mock(async () => taskHandle),
271
- get: mock(async () => ({ id: 'task-1', status: 'done' })),
272
- };
273
-
274
- const provider = createSupaCloudTaskProvider({ supacloud });
275
- const result = await provider.submit('image.generate', {
276
- body: { prompt: 'poster' },
277
- idempotencyKey: 'job-1',
278
- });
279
-
280
- expect(supacloud.submit).toHaveBeenCalledWith('image.generate', {
281
- body: { prompt: 'poster' },
282
- idempotencyKey: 'job-1',
283
- });
284
- expect(result).toBe(taskHandle);
285
- });
286
-
287
- test('list normalizes object payload with data field', async () => {
288
- const { createSupaCloudTaskProvider } = await import('./supacloud');
289
- const supacloud: import('@svadmin/core').TaskProvider<{ id: string; status: string }> = {
290
- submit: mock(async () => ({ wait: async () => ({ id: 'task-1', status: 'queued' }) })),
291
- get: mock(async () => ({ id: 'task-1', status: 'queued' })),
292
- list: mock(async () => ({ data: [{ id: 'task-1', status: 'queued' }] })),
293
- };
294
-
295
- const provider = createSupaCloudTaskProvider({ supacloud });
296
- if (!provider.list) throw new Error('provider.list should exist');
297
- const tasks = await provider.list();
298
-
299
- expect(tasks).toEqual({
300
- data: [{ id: 'task-1', status: 'queued' }],
301
- total: 1,
302
- });
303
- });
304
-
305
- test('listDlq throws clear error when capability is missing', async () => {
306
- const { createSupaCloudTaskProvider } = await import('./supacloud');
307
- const supacloud: import('@svadmin/core').TaskProvider<{ id: string }> = {
308
- submit: mock(async () => ({ wait: async () => ({ id: 'task-1' }) })),
309
- get: mock(async () => ({ id: 'task-1' })),
310
- };
311
-
312
- const provider = createSupaCloudTaskProvider({ supacloud });
313
- if (!provider.listDlq) throw new Error('provider.listDlq should exist');
314
-
315
- await expect(provider.listDlq()).rejects.toThrow('tasks.listDlq');
316
- });
317
- });
318
-
319
-
320
- // ─── SupaCloud Task LiveProvider Tests ───────────────────────────
321
- describe('SupaCloud Task LiveProvider', () => {
322
- test('subscribe requires taskId in liveParams', async () => {
323
- const { createSupaCloudTaskLiveProvider } = await import('./supacloud');
324
- const supacloud = {
325
- subscribe: mock(() => ({ unsubscribe: mock(() => {}) })),
326
- };
327
-
328
- const live = createSupaCloudTaskLiveProvider({ supacloud });
329
-
330
- expect(() =>
331
- live.subscribe({
332
- resource: 'tasks',
333
- callback: () => {},
334
- }),
335
- ).toThrow('liveParams.taskId');
336
- });
337
-
338
- test('subscribe maps task updates into svadmin live events', async () => {
339
- const { createSupaCloudTaskLiveProvider } = await import('./supacloud');
340
- let receivedTaskCallback: ((task: Record<string, unknown>) => void) | undefined;
341
- const unsubscribe = mock(() => {});
342
- const supacloud = {
343
- subscribe: mock((taskId: string, callback: (task: Record<string, unknown>) => void) => {
344
- receivedTaskCallback = callback;
345
- expect(taskId).toBe('task-42');
346
- return { unsubscribe };
347
- }),
348
- };
349
-
350
- const live = createSupaCloudTaskLiveProvider({ supacloud });
351
- const callback = mock(() => {});
352
- const stop = live.subscribe({
353
- resource: 'tasks',
354
- liveParams: { taskId: 'task-42' },
355
- callback,
356
- });
357
-
358
- receivedTaskCallback?.({ id: 'task-42', status: 'running' });
359
-
360
- expect(callback).toHaveBeenCalledWith({
361
- type: 'UPDATE',
362
- resource: 'tasks',
363
- payload: { id: 'task-42', status: 'running' },
364
- });
365
-
366
- stop();
367
- expect(unsubscribe).toHaveBeenCalled();
368
- });
369
- });