@svadmin/elysia 0.3.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 ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@svadmin/elysia",
3
+ "version": "0.3.0",
4
+ "description": "Elysia DataProvider for svadmin — CRUD convention + InferResourceMap type utility",
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.3.0",
20
+ "elysia": ">=1.0.0",
21
+ "@elysiajs/eden": ">=1.0.0"
22
+ },
23
+ "license": "MIT",
24
+ "author": "zuohuadong",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/zuohuadong/svadmin.git"
28
+ }
29
+ }
@@ -0,0 +1,149 @@
1
+ // Elysia DataProvider — CRUD convention compatible
2
+ // Expects backend routes following: GET /resource, GET /resource/:id, POST /resource, PATCH /resource/:id, DELETE /resource/:id
3
+ // Response format for lists: { items: T[], total: number }
4
+
5
+ import type {
6
+ DataProvider, GetListParams, GetListResult, GetOneParams, GetOneResult,
7
+ CreateParams, CreateResult, UpdateParams, UpdateResult, DeleteParams, DeleteResult,
8
+ GetManyParams, GetManyResult, CreateManyParams, CreateManyResult,
9
+ UpdateManyParams, UpdateManyResult, DeleteManyParams, DeleteManyResult,
10
+ CustomParams, CustomResult, BaseRecord,
11
+ } from '@svadmin/core';
12
+
13
+ export interface ElysiaDataProviderOptions {
14
+ /** Base API URL, e.g. 'http://localhost:3000' */
15
+ apiUrl: string;
16
+ /** Static headers or a function returning headers (useful for auth tokens) */
17
+ headers?: Record<string, string> | (() => Record<string, string>);
18
+ }
19
+
20
+ function resolveHeaders(opts: ElysiaDataProviderOptions): Record<string, string> {
21
+ const base = { 'Content-Type': 'application/json' };
22
+ const extra = typeof opts.headers === 'function' ? opts.headers() : (opts.headers ?? {});
23
+ return { ...base, ...extra };
24
+ }
25
+
26
+ async function request<T>(url: string, headers: Record<string, string>, init?: RequestInit): Promise<T> {
27
+ const response = await fetch(url, { ...init, headers: { ...headers, ...init?.headers } });
28
+ if (!response.ok) {
29
+ const body = await response.text().catch(() => '');
30
+ throw new Error(`HTTP ${response.status}: ${response.statusText}${body ? ` — ${body}` : ''}`);
31
+ }
32
+ return response.json();
33
+ }
34
+
35
+ /**
36
+ * Creates a DataProvider for Elysia backends using the CRUD plugin convention.
37
+ *
38
+ * List responses are expected to return `{ items: T[], total: number }`.
39
+ * Single-record responses return the record directly.
40
+ */
41
+ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataProvider {
42
+ const { apiUrl } = opts;
43
+
44
+ return {
45
+ getApiUrl: () => apiUrl,
46
+
47
+ async getList<TData extends BaseRecord = BaseRecord>({ resource, pagination, sorters, filters }: GetListParams): Promise<GetListResult<TData>> {
48
+ const params = new URLSearchParams();
49
+ const { current = 1, pageSize = 10 } = pagination ?? {};
50
+ params.set('_page', String(current));
51
+ params.set('_limit', String(pageSize));
52
+
53
+ if (sorters?.length) {
54
+ params.set('_sort', sorters.map(s => s.field).join(','));
55
+ params.set('_order', sorters.map(s => s.order).join(','));
56
+ }
57
+
58
+ if (filters?.length) {
59
+ for (const f of filters) {
60
+ if (f.operator === 'eq') params.set(f.field, String(f.value));
61
+ else if (f.operator === 'contains') params.set(`${f.field}_like`, String(f.value));
62
+ else params.set(`${f.field}_${f.operator}`, String(f.value));
63
+ }
64
+ }
65
+
66
+ const url = `${apiUrl}/${resource}?${params.toString()}`;
67
+ const headers = resolveHeaders(opts);
68
+ const json = await request<{ items: TData[]; total: number }>(url, headers);
69
+ return { data: json.items, total: json.total };
70
+ },
71
+
72
+ async getOne<TData extends BaseRecord = BaseRecord>({ resource, id }: GetOneParams): Promise<GetOneResult<TData>> {
73
+ const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts));
74
+ return { data };
75
+ },
76
+
77
+ async create<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables }: CreateParams<TVariables>): Promise<CreateResult<TData>> {
78
+ const data = await request<TData>(`${apiUrl}/${resource}`, resolveHeaders(opts), {
79
+ method: 'POST',
80
+ body: JSON.stringify(variables),
81
+ });
82
+ return { data };
83
+ },
84
+
85
+ async update<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id, variables }: UpdateParams<TVariables>): Promise<UpdateResult<TData>> {
86
+ const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts), {
87
+ method: 'PATCH',
88
+ body: JSON.stringify(variables),
89
+ });
90
+ return { data };
91
+ },
92
+
93
+ async deleteOne<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id }: DeleteParams<TVariables>): Promise<DeleteResult<TData>> {
94
+ const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts), {
95
+ method: 'DELETE',
96
+ });
97
+ return { data };
98
+ },
99
+
100
+ async getMany<TData extends BaseRecord = BaseRecord>({ resource, ids }: GetManyParams): Promise<GetManyResult<TData>> {
101
+ const params = ids.map(id => `id=${id}`).join('&');
102
+ const data = await request<TData[]>(`${apiUrl}/${resource}?${params}`, resolveHeaders(opts));
103
+ return { data };
104
+ },
105
+
106
+ async createMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables }: CreateManyParams<TVariables>): Promise<CreateManyResult<TData>> {
107
+ const results: TData[] = [];
108
+ for (const vars of variables) {
109
+ const data = await request<TData>(`${apiUrl}/${resource}`, resolveHeaders(opts), {
110
+ method: 'POST',
111
+ body: JSON.stringify(vars),
112
+ });
113
+ results.push(data);
114
+ }
115
+ return { data: results };
116
+ },
117
+
118
+ async updateMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, ids, variables }: UpdateManyParams<TVariables>): Promise<UpdateManyResult<TData>> {
119
+ const results: TData[] = [];
120
+ for (const id of ids) {
121
+ const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts), {
122
+ method: 'PATCH',
123
+ body: JSON.stringify(variables),
124
+ });
125
+ results.push(data);
126
+ }
127
+ return { data: results };
128
+ },
129
+
130
+ async deleteMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, ids }: DeleteManyParams<TVariables>): Promise<DeleteManyResult<TData>> {
131
+ const results: TData[] = [];
132
+ for (const id of ids) {
133
+ const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts), {
134
+ method: 'DELETE',
135
+ });
136
+ results.push(data);
137
+ }
138
+ return { data: results };
139
+ },
140
+
141
+ async custom<TData = unknown, TVariables = unknown>({ url, method, payload, headers }: CustomParams<TVariables>): Promise<CustomResult<TData>> {
142
+ const data = await request<TData>(url, { ...resolveHeaders(opts), ...headers }, {
143
+ method: method.toUpperCase(),
144
+ body: payload ? JSON.stringify(payload) : undefined,
145
+ });
146
+ return { data };
147
+ },
148
+ };
149
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ // @svadmin/elysia — Elysia DataProvider + type utilities
2
+
3
+ export { createElysiaDataProvider } from './data-provider';
4
+ export type { ElysiaDataProviderOptions } from './data-provider';
5
+ export type { InferResourceMap } from './types';
package/src/types.ts ADDED
@@ -0,0 +1,53 @@
1
+ // Type utility: Infer ResourceTypeMap from an Elysia App type
2
+ //
3
+ // This extracts CRUD resource names and their response types from an Elysia app
4
+ // that uses the CRUD plugin convention (GET /resource → list, GET /resource/:id → record).
5
+ //
6
+ // Usage:
7
+ // import type { InferResourceMap } from '@svadmin/elysia'
8
+ // import type { App } from '../server'
9
+ //
10
+ // declare module '@svadmin/core' {
11
+ // interface ResourceTypeMap extends InferResourceMap<App> {}
12
+ // }
13
+
14
+ /**
15
+ * Extract resource types from an Elysia App type.
16
+ *
17
+ * This works by inspecting the app's route schema type:
18
+ * - Looks for GET routes at `/:resource` that return `{ items: T[], total: number }`
19
+ * - Extracts `T` as the resource data type
20
+ *
21
+ * Note: This is a best-effort type inference. For complex Elysia setups,
22
+ * you may want to manually declare your ResourceTypeMap instead.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // server.ts
27
+ * const app = new Elysia()
28
+ * .use(crud('users', { schema: UserSchema, ... }))
29
+ * .use(crud('posts', { schema: PostSchema, ... }))
30
+ *
31
+ * export type App = typeof app
32
+ *
33
+ * // client-side types.d.ts
34
+ * import type { InferResourceMap } from '@svadmin/elysia'
35
+ * import type { App } from '../server'
36
+ *
37
+ * declare module '@svadmin/core' {
38
+ * interface ResourceTypeMap extends InferResourceMap<App> {}
39
+ * }
40
+ * ```
41
+ */
42
+ export type InferResourceMap<App> = App extends { _routes: infer Routes }
43
+ ? {
44
+ [K in keyof Routes as K extends `/${infer Resource}`
45
+ ? Resource extends `${string}/${string}` ? never : Resource
46
+ : never
47
+ ]: Routes[K] extends { get: { response: { 200: infer Res } } }
48
+ ? Res extends { items: (infer Item)[] }
49
+ ? Item
50
+ : Res
51
+ : never
52
+ }
53
+ : Record<string, never>;