@svadmin/supabase 0.0.1
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 +32 -0
- package/src/audit-handler.ts +16 -0
- package/src/auth-provider.ts +76 -0
- package/src/data-provider.ts +113 -0
- package/src/index.ts +6 -0
- package/src/live-provider.ts +33 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@svadmin/supabase",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Supabase adapters — DataProvider, AuthProvider, LiveProvider, AuditHandler",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"files": [
|
|
8
|
+
"src/**/*"
|
|
9
|
+
],
|
|
10
|
+
"main": "src/index.ts",
|
|
11
|
+
"types": "src/index.ts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"default": "./src/index.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@svadmin/core": "0.1.0",
|
|
20
|
+
"@supabase/supabase-js": "^2.0.0"
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": "zuohuadong",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/zuohuadong/svadmin.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/zuohuadong/svadmin#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/zuohuadong/svadmin/issues"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Supabase AuditHandler — writes audit entries to a Supabase table
|
|
2
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import type { AuditHandler, AuditEntry } from '@svadmin/core';
|
|
4
|
+
|
|
5
|
+
export function createSupabaseAuditHandler(client: SupabaseClient): AuditHandler {
|
|
6
|
+
return async (entry: AuditEntry) => {
|
|
7
|
+
await client.from('audit_log').insert({
|
|
8
|
+
action: entry.action,
|
|
9
|
+
resource: entry.resource,
|
|
10
|
+
record_id: entry.recordId ? String(entry.recordId) : null,
|
|
11
|
+
user_id: entry.userId,
|
|
12
|
+
details: entry.details,
|
|
13
|
+
created_at: entry.timestamp,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Supabase AuthProvider
|
|
2
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import type { AuthProvider, Identity, AuthActionResult, CheckResult } from '@svadmin/core';
|
|
4
|
+
import { audit } from '@svadmin/core';
|
|
5
|
+
|
|
6
|
+
export function createSupabaseAuthProvider(client: SupabaseClient): AuthProvider {
|
|
7
|
+
return {
|
|
8
|
+
async login({ email, password }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
9
|
+
const { data, error } = await client.auth.signInWithPassword({
|
|
10
|
+
email: email as string,
|
|
11
|
+
password: password as string,
|
|
12
|
+
});
|
|
13
|
+
if (error) return { success: false, error: { message: error.message } };
|
|
14
|
+
audit({ action: 'login', userId: data.user?.id });
|
|
15
|
+
return { success: true, redirectTo: '/' };
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
async logout(): Promise<AuthActionResult> {
|
|
19
|
+
const { data: { user } } = await client.auth.getUser();
|
|
20
|
+
await client.auth.signOut();
|
|
21
|
+
audit({ action: 'logout', userId: user?.id });
|
|
22
|
+
return { success: true, redirectTo: '/login' };
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async check(): Promise<CheckResult> {
|
|
26
|
+
const { data: { session } } = await client.auth.getSession();
|
|
27
|
+
if (session) return { authenticated: true };
|
|
28
|
+
return { authenticated: false, redirectTo: '/login', logout: true };
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
async getIdentity(): Promise<Identity | null> {
|
|
32
|
+
const { data: { user } } = await client.auth.getUser();
|
|
33
|
+
if (!user) return null;
|
|
34
|
+
return {
|
|
35
|
+
id: user.id,
|
|
36
|
+
name: user.user_metadata?.name ?? user.email?.split('@')[0] ?? 'User',
|
|
37
|
+
email: user.email,
|
|
38
|
+
avatar: user.user_metadata?.avatar_url,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async getPermissions(): Promise<unknown> {
|
|
43
|
+
const { data: { user } } = await client.auth.getUser();
|
|
44
|
+
return user?.user_metadata?.role ?? 'user';
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async register({ email, password, ...rest }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
48
|
+
const { error } = await client.auth.signUp({
|
|
49
|
+
email: email as string,
|
|
50
|
+
password: password as string,
|
|
51
|
+
options: { data: rest },
|
|
52
|
+
});
|
|
53
|
+
if (error) return { success: false, error: { message: error.message } };
|
|
54
|
+
return { success: true, redirectTo: '/login' };
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async forgotPassword({ email }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
58
|
+
const { error } = await client.auth.resetPasswordForEmail(email as string);
|
|
59
|
+
if (error) return { success: false, error: { message: error.message } };
|
|
60
|
+
return { success: true };
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async updatePassword({ password }: Record<string, unknown>): Promise<AuthActionResult> {
|
|
64
|
+
const { error } = await client.auth.updateUser({ password: password as string });
|
|
65
|
+
if (error) return { success: false, error: { message: error.message } };
|
|
66
|
+
return { success: true, redirectTo: '/' };
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async onError(error: unknown): Promise<{ redirectTo?: string; logout?: boolean }> {
|
|
70
|
+
if (error instanceof Error && error.message.includes('401')) {
|
|
71
|
+
return { redirectTo: '/login', logout: true };
|
|
72
|
+
}
|
|
73
|
+
return {};
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Supabase DataProvider
|
|
2
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
+
import type {
|
|
4
|
+
DataProvider, GetListParams, GetListResult, GetOneParams, GetOneResult,
|
|
5
|
+
CreateParams, CreateResult, UpdateParams, UpdateResult, DeleteParams, DeleteResult,
|
|
6
|
+
GetManyParams, GetManyResult, CreateManyParams, CreateManyResult,
|
|
7
|
+
UpdateManyParams, UpdateManyResult, DeleteManyParams, DeleteManyResult,
|
|
8
|
+
CustomParams, CustomResult,
|
|
9
|
+
} from '@svadmin/core';
|
|
10
|
+
|
|
11
|
+
export function createSupabaseDataProvider(client: SupabaseClient, apiUrl?: string): DataProvider {
|
|
12
|
+
const resolvedApiUrl = apiUrl ?? '';
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
getApiUrl: () => resolvedApiUrl,
|
|
16
|
+
|
|
17
|
+
async getList<T>({ resource, pagination, sorters, filters, meta }: GetListParams): Promise<GetListResult<T>> {
|
|
18
|
+
const { current = 1, pageSize = 10 } = pagination ?? {};
|
|
19
|
+
const from = (current - 1) * pageSize;
|
|
20
|
+
const to = from + pageSize - 1;
|
|
21
|
+
const select = (meta?.select as string) ?? '*';
|
|
22
|
+
|
|
23
|
+
let query = client.from(resource).select(select, { count: 'exact' }).range(from, to);
|
|
24
|
+
|
|
25
|
+
if (sorters?.length) {
|
|
26
|
+
for (const sort of sorters) {
|
|
27
|
+
query = query.order(sort.field, { ascending: sort.order === 'asc' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (filters?.length) {
|
|
32
|
+
for (const filter of filters) {
|
|
33
|
+
switch (filter.operator) {
|
|
34
|
+
case 'eq': query = query.eq(filter.field, filter.value); break;
|
|
35
|
+
case 'ne': query = query.neq(filter.field, filter.value); break;
|
|
36
|
+
case 'lt': query = query.lt(filter.field, filter.value); break;
|
|
37
|
+
case 'gt': query = query.gt(filter.field, filter.value); break;
|
|
38
|
+
case 'lte': query = query.lte(filter.field, filter.value); break;
|
|
39
|
+
case 'gte': query = query.gte(filter.field, filter.value); break;
|
|
40
|
+
case 'contains': query = query.ilike(filter.field, `%${filter.value}%`); break;
|
|
41
|
+
case 'in': query = query.in(filter.field, filter.value as unknown[]); break;
|
|
42
|
+
case 'null': query = query.is(filter.field, null); break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { data, count, error } = await query;
|
|
48
|
+
if (error) throw error;
|
|
49
|
+
return { data: (data ?? []) as T[], total: count ?? 0 };
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async getOne<T>({ resource, id, meta }: GetOneParams): Promise<GetOneResult<T>> {
|
|
53
|
+
const select = (meta?.select as string) ?? '*';
|
|
54
|
+
const { data, error } = await client.from(resource).select(select).eq('id', id).single();
|
|
55
|
+
if (error) throw error;
|
|
56
|
+
return { data: data as T };
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async create<T>({ resource, variables }: CreateParams): Promise<CreateResult<T>> {
|
|
60
|
+
const { data, error } = await client.from(resource).insert(variables).select().single();
|
|
61
|
+
if (error) throw error;
|
|
62
|
+
return { data: data as T };
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async update<T>({ resource, id, variables }: UpdateParams): Promise<UpdateResult<T>> {
|
|
66
|
+
const { data, error } = await client.from(resource).update(variables).eq('id', id).select().single();
|
|
67
|
+
if (error) throw error;
|
|
68
|
+
return { data: data as T };
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async deleteOne<T>({ resource, id }: DeleteParams): Promise<DeleteResult<T>> {
|
|
72
|
+
const { data, error } = await client.from(resource).delete().eq('id', id).select().single();
|
|
73
|
+
if (error) throw error;
|
|
74
|
+
return { data: data as T };
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async getMany<T>({ resource, ids, meta }: GetManyParams): Promise<GetManyResult<T>> {
|
|
78
|
+
const select = (meta?.select as string) ?? '*';
|
|
79
|
+
const { data, error } = await client.from(resource).select(select).in('id', ids);
|
|
80
|
+
if (error) throw error;
|
|
81
|
+
return { data: (data ?? []) as T[] };
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async createMany<T>({ resource, variables }: CreateManyParams): Promise<CreateManyResult<T>> {
|
|
85
|
+
const { data, error } = await client.from(resource).insert(variables).select();
|
|
86
|
+
if (error) throw error;
|
|
87
|
+
return { data: (data ?? []) as T[] };
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async updateMany<T>({ resource, ids, variables }: UpdateManyParams): Promise<UpdateManyResult<T>> {
|
|
91
|
+
const { data, error } = await client.from(resource).update(variables).in('id', ids).select();
|
|
92
|
+
if (error) throw error;
|
|
93
|
+
return { data: (data ?? []) as T[] };
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async deleteMany<T>({ resource, ids }: DeleteManyParams): Promise<DeleteManyResult<T>> {
|
|
97
|
+
const { data, error } = await client.from(resource).delete().in('id', ids).select();
|
|
98
|
+
if (error) throw error;
|
|
99
|
+
return { data: (data ?? []) as T[] };
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
async custom<T>({ url, method, payload, headers }: CustomParams): Promise<CustomResult<T>> {
|
|
103
|
+
const response = await fetch(url, {
|
|
104
|
+
method: method.toUpperCase(),
|
|
105
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
106
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) throw new Error(`Custom request failed: ${response.status}`);
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
return { data: data as T };
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// @svadmin/supabase — Supabase adapters
|
|
2
|
+
|
|
3
|
+
export { createSupabaseDataProvider } from './data-provider';
|
|
4
|
+
export { createSupabaseAuthProvider } from './auth-provider';
|
|
5
|
+
export { createSupabaseLiveProvider } from './live-provider';
|
|
6
|
+
export { createSupabaseAuditHandler } from './audit-handler';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Supabase LiveProvider — Real-time subscriptions via Supabase Realtime
|
|
2
|
+
import type { SupabaseClient, RealtimeChannel } from '@supabase/supabase-js';
|
|
3
|
+
import type { LiveProvider, LiveEvent } from '@svadmin/core';
|
|
4
|
+
|
|
5
|
+
export function createSupabaseLiveProvider(client: SupabaseClient): LiveProvider {
|
|
6
|
+
const channels = new Map<string, RealtimeChannel>();
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
subscribe({ resource, callback }) {
|
|
10
|
+
const channel = client
|
|
11
|
+
.channel(`live-${resource}`)
|
|
12
|
+
.on(
|
|
13
|
+
'postgres_changes',
|
|
14
|
+
{ event: '*', schema: 'public', table: resource },
|
|
15
|
+
(payload) => {
|
|
16
|
+
callback({
|
|
17
|
+
type: payload.eventType as LiveEvent['type'],
|
|
18
|
+
resource,
|
|
19
|
+
payload: (payload.new ?? payload.old ?? {}) as Record<string, unknown>,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
.subscribe();
|
|
24
|
+
|
|
25
|
+
channels.set(resource, channel);
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
channel.unsubscribe();
|
|
29
|
+
channels.delete(resource);
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|