@svadmin/simple-rest 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 +31 -0
- package/src/auth-provider.ts +92 -0
- package/src/data-provider.ts +137 -0
- package/src/index.ts +2 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@svadmin/simple-rest",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Simple REST adapters — fetch-based DataProvider and JWT/Cookie AuthProvider",
|
|
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
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "zuohuadong",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/zuohuadong/svadmin.git"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/zuohuadong/svadmin#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/zuohuadong/svadmin/issues"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Simple REST AuthProvider — JWT/Cookie-based, zero dependencies
|
|
2
|
+
|
|
3
|
+
import type { AuthProvider, Identity, AuthActionResult, CheckResult } from '@svadmin/core';
|
|
4
|
+
|
|
5
|
+
export interface SimpleRestAuthOptions {
|
|
6
|
+
loginUrl: string;
|
|
7
|
+
logoutUrl?: string;
|
|
8
|
+
identityUrl?: string;
|
|
9
|
+
/** Storage key for JWT token. Set to null for cookie-based auth. */
|
|
10
|
+
tokenKey?: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createSimpleRestAuthProvider(opts: SimpleRestAuthOptions): AuthProvider {
|
|
14
|
+
const { tokenKey = 'auth_token' } = opts;
|
|
15
|
+
|
|
16
|
+
function getAuthHeader(): Record<string, string> {
|
|
17
|
+
if (!tokenKey) return {};
|
|
18
|
+
const token = localStorage.getItem(tokenKey);
|
|
19
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
async login(params: Record<string, unknown>): Promise<AuthActionResult> {
|
|
24
|
+
try {
|
|
25
|
+
const response = await fetch(opts.loginUrl, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify(params),
|
|
29
|
+
credentials: 'include',
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const error = await response.json().catch(() => ({ message: 'Login failed' }));
|
|
33
|
+
return { success: false, error: { message: error.message ?? 'Login failed' } };
|
|
34
|
+
}
|
|
35
|
+
const data = await response.json();
|
|
36
|
+
if (tokenKey && data.token) {
|
|
37
|
+
localStorage.setItem(tokenKey, data.token);
|
|
38
|
+
}
|
|
39
|
+
return { success: true, redirectTo: '/' };
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return { success: false, error: { message: e instanceof Error ? e.message : 'Login failed' } };
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async logout(): Promise<AuthActionResult> {
|
|
46
|
+
if (opts.logoutUrl) {
|
|
47
|
+
await fetch(opts.logoutUrl, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: getAuthHeader(),
|
|
50
|
+
credentials: 'include',
|
|
51
|
+
}).catch((e) => console.warn('[auth] logout request failed:', e));
|
|
52
|
+
}
|
|
53
|
+
if (tokenKey) localStorage.removeItem(tokenKey);
|
|
54
|
+
return { success: true, redirectTo: '/login' };
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async check(): Promise<CheckResult> {
|
|
58
|
+
if (tokenKey) {
|
|
59
|
+
const token = localStorage.getItem(tokenKey);
|
|
60
|
+
if (!token) return { authenticated: false, redirectTo: '/login', logout: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (opts.identityUrl) {
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(opts.identityUrl, {
|
|
66
|
+
headers: getAuthHeader(),
|
|
67
|
+
credentials: 'include',
|
|
68
|
+
});
|
|
69
|
+
if (response.ok) return { authenticated: true };
|
|
70
|
+
} catch (e) { console.warn('[auth] identity check failed:', e); }
|
|
71
|
+
return { authenticated: false, redirectTo: '/login', logout: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { authenticated: true };
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
async getIdentity(): Promise<Identity | null> {
|
|
78
|
+
if (!opts.identityUrl) return null;
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(opts.identityUrl, {
|
|
81
|
+
headers: getAuthHeader(),
|
|
82
|
+
credentials: 'include',
|
|
83
|
+
});
|
|
84
|
+
if (!response.ok) return null;
|
|
85
|
+
return await response.json();
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn('[auth] getIdentity failed:', e);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Simple REST DataProvider — fetch-based, zero dependencies
|
|
2
|
+
// Compatible with json-server, RESTful APIs, and any JSON backend
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
DataProvider, GetListParams, GetListResult, GetOneParams, GetOneResult,
|
|
6
|
+
CreateParams, CreateResult, UpdateParams, UpdateResult, DeleteParams, DeleteResult,
|
|
7
|
+
GetManyParams, GetManyResult, CreateManyParams, CreateManyResult,
|
|
8
|
+
UpdateManyParams, UpdateManyResult, DeleteManyParams, DeleteManyResult,
|
|
9
|
+
CustomParams, CustomResult,
|
|
10
|
+
} from '@svadmin/core';
|
|
11
|
+
|
|
12
|
+
export interface SimpleRestOptions {
|
|
13
|
+
apiUrl: string;
|
|
14
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getHeaders(opts: SimpleRestOptions): Record<string, string> {
|
|
18
|
+
const base = { 'Content-Type': 'application/json' };
|
|
19
|
+
const extra = typeof opts.headers === 'function' ? opts.headers() : (opts.headers ?? {});
|
|
20
|
+
return { ...base, ...extra };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createSimpleRestDataProvider(opts: SimpleRestOptions): DataProvider {
|
|
24
|
+
const { apiUrl } = opts;
|
|
25
|
+
|
|
26
|
+
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
|
27
|
+
const response = await fetch(url, { ...init, headers: { ...getHeaders(opts), ...init?.headers } });
|
|
28
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
29
|
+
return response.json();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
getApiUrl: () => apiUrl,
|
|
34
|
+
|
|
35
|
+
async getList<T>({ resource, pagination, sorters, filters }: GetListParams): Promise<GetListResult<T>> {
|
|
36
|
+
const params = new URLSearchParams();
|
|
37
|
+
const { current = 1, pageSize = 10 } = pagination ?? {};
|
|
38
|
+
params.set('_page', String(current));
|
|
39
|
+
params.set('_limit', String(pageSize));
|
|
40
|
+
|
|
41
|
+
if (sorters?.length) {
|
|
42
|
+
params.set('_sort', sorters.map(s => s.field).join(','));
|
|
43
|
+
params.set('_order', sorters.map(s => s.order).join(','));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (filters?.length) {
|
|
47
|
+
for (const f of filters) {
|
|
48
|
+
if (f.operator === 'eq') params.set(f.field, String(f.value));
|
|
49
|
+
else if (f.operator === 'contains') params.set(`${f.field}_like`, String(f.value));
|
|
50
|
+
else params.set(`${f.field}_${f.operator}`, String(f.value));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const url = `${apiUrl}/${resource}?${params.toString()}`;
|
|
55
|
+
const response = await fetch(url, { headers: getHeaders(opts) });
|
|
56
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
57
|
+
|
|
58
|
+
const data = await response.json() as T[];
|
|
59
|
+
const total = parseInt(response.headers.get('X-Total-Count') ?? String(data.length), 10);
|
|
60
|
+
return { data, total };
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async getOne<T>({ resource, id }: GetOneParams): Promise<GetOneResult<T>> {
|
|
64
|
+
const data = await request<T>(`${apiUrl}/${resource}/${id}`);
|
|
65
|
+
return { data };
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async create<T>({ resource, variables }: CreateParams): Promise<CreateResult<T>> {
|
|
69
|
+
const data = await request<T>(`${apiUrl}/${resource}`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: JSON.stringify(variables),
|
|
72
|
+
});
|
|
73
|
+
return { data };
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
async update<T>({ resource, id, variables }: UpdateParams): Promise<UpdateResult<T>> {
|
|
77
|
+
const data = await request<T>(`${apiUrl}/${resource}/${id}`, {
|
|
78
|
+
method: 'PATCH',
|
|
79
|
+
body: JSON.stringify(variables),
|
|
80
|
+
});
|
|
81
|
+
return { data };
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async deleteOne<T>({ resource, id }: DeleteParams): Promise<DeleteResult<T>> {
|
|
85
|
+
const data = await request<T>(`${apiUrl}/${resource}/${id}`, { method: 'DELETE' });
|
|
86
|
+
return { data };
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async getMany<T>({ resource, ids }: GetManyParams): Promise<GetManyResult<T>> {
|
|
90
|
+
const params = ids.map(id => `id=${id}`).join('&');
|
|
91
|
+
const data = await request<T[]>(`${apiUrl}/${resource}?${params}`);
|
|
92
|
+
return { data };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async createMany<T>({ resource, variables }: CreateManyParams): Promise<CreateManyResult<T>> {
|
|
96
|
+
const results: T[] = [];
|
|
97
|
+
for (const vars of variables) {
|
|
98
|
+
const data = await request<T>(`${apiUrl}/${resource}`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
body: JSON.stringify(vars),
|
|
101
|
+
});
|
|
102
|
+
results.push(data);
|
|
103
|
+
}
|
|
104
|
+
return { data: results };
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
async updateMany<T>({ resource, ids, variables }: UpdateManyParams): Promise<UpdateManyResult<T>> {
|
|
108
|
+
const results: T[] = [];
|
|
109
|
+
for (const id of ids) {
|
|
110
|
+
const data = await request<T>(`${apiUrl}/${resource}/${id}`, {
|
|
111
|
+
method: 'PATCH',
|
|
112
|
+
body: JSON.stringify(variables),
|
|
113
|
+
});
|
|
114
|
+
results.push(data);
|
|
115
|
+
}
|
|
116
|
+
return { data: results };
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
async deleteMany<T>({ resource, ids }: DeleteManyParams): Promise<DeleteManyResult<T>> {
|
|
120
|
+
const results: T[] = [];
|
|
121
|
+
for (const id of ids) {
|
|
122
|
+
const data = await request<T>(`${apiUrl}/${resource}/${id}`, { method: 'DELETE' });
|
|
123
|
+
results.push(data);
|
|
124
|
+
}
|
|
125
|
+
return { data: results };
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
async custom<T>({ url, method, payload, headers }: CustomParams): Promise<CustomResult<T>> {
|
|
129
|
+
const data = await request<T>(url, {
|
|
130
|
+
method: method.toUpperCase(),
|
|
131
|
+
headers,
|
|
132
|
+
body: payload ? JSON.stringify(payload) : undefined,
|
|
133
|
+
});
|
|
134
|
+
return { data };
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
package/src/index.ts
ADDED