@svadmin/elysia 0.12.7 → 0.13.6
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 +4 -3
- package/src/data-provider.ts +61 -61
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/elysia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.6",
|
|
4
4
|
"description": "Elysia DataProvider for svadmin — CRUD convention + InferResourceMap type utility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@svadmin/core": "^0.
|
|
23
|
+
"@svadmin/core": "^0.52.0",
|
|
24
24
|
"elysia": ">=1.4.30",
|
|
25
25
|
"@elysiajs/eden": ">=1.4.9"
|
|
26
26
|
},
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"author": "zuohuadong",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/vibeunion/svadmin.git",
|
|
31
|
+
"url": "git+https://github.com/vibeunion/svadmin.git",
|
|
32
32
|
"directory": "packages/elysia"
|
|
33
33
|
},
|
|
34
34
|
"peerDependenciesMeta": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
+
"zod": "^4.4.3",
|
|
46
47
|
"elysia": ">=1.4.30",
|
|
47
48
|
"@elysiajs/eden": ">=1.4.9"
|
|
48
49
|
}
|
package/src/data-provider.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import { definedOptions } from '@svadmin/core/options';
|
|
1
2
|
// Elysia DataProvider — CRUD convention compatible
|
|
2
3
|
// Expects backend routes following: GET /resource, GET /resource/:id, POST /resource, PATCH /resource/:id, DELETE /resource/:id
|
|
3
4
|
// Response format for lists: { items: T[], total: number } (also supports raw arrays)
|
|
4
5
|
|
|
5
6
|
import type {
|
|
6
|
-
DataProvider,
|
|
7
|
-
CreateParams, CreateResult, UpdateParams, UpdateResult, DeleteParams, DeleteResult,
|
|
8
|
-
GetManyParams, GetManyResult, CreateManyParams, CreateManyResult,
|
|
9
|
-
UpdateManyParams, UpdateManyResult, DeleteManyParams, DeleteManyResult,
|
|
10
|
-
CustomParams, CustomResult, BaseRecord, FieldFilter, Filter, Sort, Pagination,
|
|
7
|
+
DataProvider, FieldFilter, Filter, Sort, Pagination,
|
|
11
8
|
} from '@svadmin/core';
|
|
9
|
+
import { withValidatedResponses, type DataTransport } from '@svadmin/core/schema';
|
|
12
10
|
|
|
13
11
|
export interface ElysiaResourceContext {
|
|
14
12
|
apiUrl: string;
|
|
@@ -39,10 +37,10 @@ export interface ElysiaResourceAdapter {
|
|
|
39
37
|
/** Build the complete query string for list requests. */
|
|
40
38
|
buildListSearchParams?: (context: ElysiaListContext) => URLSearchParams;
|
|
41
39
|
/** Normalize a resource-specific list envelope while retaining extra result metadata. */
|
|
42
|
-
parseListResponse?:
|
|
40
|
+
parseListResponse?: (
|
|
43
41
|
json: unknown,
|
|
44
42
|
context: ElysiaListContext,
|
|
45
|
-
) =>
|
|
43
|
+
) => unknown;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
export interface ElysiaDataProviderOptions {
|
|
@@ -75,10 +73,10 @@ export interface ElysiaDataProviderOptions {
|
|
|
75
73
|
*
|
|
76
74
|
* @default Handles `{ items, total }` and raw arrays automatically
|
|
77
75
|
*/
|
|
78
|
-
parseListResponse?:
|
|
76
|
+
parseListResponse?: (
|
|
79
77
|
json: unknown,
|
|
80
78
|
resource: string,
|
|
81
|
-
) =>
|
|
79
|
+
) => unknown;
|
|
82
80
|
/**
|
|
83
81
|
* Ordered per-resource transport overrides. The first matching adapter wins.
|
|
84
82
|
* Existing global options remain the fallback for unmatched resources.
|
|
@@ -141,7 +139,7 @@ function resolveResourceUrl(
|
|
|
141
139
|
resource: string,
|
|
142
140
|
meta?: Record<string, unknown>,
|
|
143
141
|
): string {
|
|
144
|
-
const context: ElysiaResourceContext = { apiUrl: opts.apiUrl, resource, meta };
|
|
142
|
+
const context: ElysiaResourceContext = definedOptions({ apiUrl: opts.apiUrl, resource, meta });
|
|
145
143
|
return resolveResourceUrlWithAdapter(opts, context, resolveResourceAdapter(opts, resource));
|
|
146
144
|
}
|
|
147
145
|
|
|
@@ -244,26 +242,25 @@ function buildCustomUrl(
|
|
|
244
242
|
return parsed.toString();
|
|
245
243
|
}
|
|
246
244
|
|
|
247
|
-
async function parseResponse
|
|
245
|
+
async function parseResponse(response: Response): Promise<unknown> {
|
|
248
246
|
if (response.status === 204 || response.status === 205) {
|
|
249
|
-
return undefined
|
|
247
|
+
return undefined;
|
|
250
248
|
}
|
|
251
249
|
|
|
252
250
|
const contentLength = response.headers?.get('content-length');
|
|
253
251
|
if (contentLength?.trim() === '0') {
|
|
254
|
-
return undefined
|
|
252
|
+
return undefined;
|
|
255
253
|
}
|
|
256
254
|
|
|
257
255
|
const body = await response.text();
|
|
258
256
|
if (!body || body.trim() === '') {
|
|
259
|
-
return undefined
|
|
257
|
+
return undefined;
|
|
260
258
|
}
|
|
261
259
|
|
|
262
|
-
return JSON.parse(body)
|
|
260
|
+
return JSON.parse(body);
|
|
263
261
|
}
|
|
264
262
|
|
|
265
|
-
async function request
|
|
266
|
-
init?.signal?.throwIfAborted();
|
|
263
|
+
async function request(url: string, headers: Record<string, string>, init?: RequestOptions, withCredentials?: boolean): Promise<unknown> {
|
|
267
264
|
const fetchInit: RequestInit = { ...init, headers: mergeHeaders(headers, init?.headers) };
|
|
268
265
|
if (withCredentials) {
|
|
269
266
|
fetchInit.credentials = 'include';
|
|
@@ -273,7 +270,7 @@ async function request<T>(url: string, headers: Record<string, string>, init?: R
|
|
|
273
270
|
const body = await response.text().catch(() => '');
|
|
274
271
|
throw new Error(`HTTP ${response.status}: ${response.statusText}${body ? ` — ${body}` : ''}`);
|
|
275
272
|
}
|
|
276
|
-
return parseResponse
|
|
273
|
+
return parseResponse(response);
|
|
277
274
|
}
|
|
278
275
|
|
|
279
276
|
/**
|
|
@@ -283,31 +280,34 @@ async function request<T>(url: string, headers: Record<string, string>, init?: R
|
|
|
283
280
|
* - `{ data: T[], total: number }` (common alternative)
|
|
284
281
|
* - `T[]` (raw array — total is inferred from array length)
|
|
285
282
|
*/
|
|
286
|
-
function normalizeObjectListResponse
|
|
283
|
+
function normalizeObjectListResponse(
|
|
287
284
|
response: Record<string, unknown>,
|
|
288
285
|
recordsKey: 'items' | 'data',
|
|
289
|
-
):
|
|
286
|
+
): unknown {
|
|
290
287
|
const { [recordsKey]: rawRecords, ...metadata } = response;
|
|
291
|
-
const records = rawRecords as TData[];
|
|
292
288
|
return {
|
|
293
289
|
...metadata,
|
|
294
|
-
data:
|
|
295
|
-
total: response
|
|
290
|
+
data: rawRecords,
|
|
291
|
+
total: response['total'] !== undefined ? response['total'] : Array.isArray(rawRecords) ? rawRecords.length : undefined,
|
|
296
292
|
};
|
|
297
293
|
}
|
|
298
294
|
|
|
299
|
-
function
|
|
295
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
296
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function defaultParseListResponse(json: unknown): unknown {
|
|
300
300
|
if (Array.isArray(json)) {
|
|
301
|
-
return { data: json
|
|
301
|
+
return { data: json, total: json.length };
|
|
302
302
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return normalizeObjectListResponse<TData>(obj, 'items');
|
|
303
|
+
if (isObject(json) && Array.isArray(json['items'])) {
|
|
304
|
+
return normalizeObjectListResponse(json, 'items');
|
|
306
305
|
}
|
|
307
|
-
if (Array.isArray(
|
|
308
|
-
return normalizeObjectListResponse
|
|
306
|
+
if (isObject(json) && Array.isArray(json['data'])) {
|
|
307
|
+
return normalizeObjectListResponse(json, 'data');
|
|
309
308
|
}
|
|
310
|
-
|
|
309
|
+
// Keep malformed payloads unknown so the shared boundary rejects them.
|
|
310
|
+
return json;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/**
|
|
@@ -333,80 +333,80 @@ function defaultParseListResponse<TData extends BaseRecord>(json: unknown): GetL
|
|
|
333
333
|
export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataProvider {
|
|
334
334
|
const { apiUrl, updateMethod = 'PATCH', withCredentials = false } = opts;
|
|
335
335
|
|
|
336
|
-
|
|
336
|
+
const transport: DataTransport = {
|
|
337
337
|
getApiUrl: () => apiUrl,
|
|
338
338
|
|
|
339
|
-
async getList
|
|
339
|
+
async getList({ resource, pagination, sorters, filters, meta }) {
|
|
340
340
|
const { current = 1, pageSize = 10 } = pagination ?? {};
|
|
341
|
-
const context: ElysiaListContext = {
|
|
341
|
+
const context: ElysiaListContext = definedOptions({
|
|
342
342
|
apiUrl,
|
|
343
343
|
resource,
|
|
344
344
|
meta,
|
|
345
345
|
pagination: { current, pageSize, mode: pagination?.mode },
|
|
346
346
|
sorters: sorters ?? [],
|
|
347
347
|
filters: filters ?? [],
|
|
348
|
-
};
|
|
348
|
+
});
|
|
349
349
|
const adapter = resolveResourceAdapter(opts, resource);
|
|
350
350
|
const params = adapter?.buildListSearchParams?.(context) ?? buildDefaultListSearchParams(context);
|
|
351
351
|
const baseUrl = resolveResourceUrlWithAdapter(opts, context, adapter);
|
|
352
352
|
const query = params.toString();
|
|
353
353
|
const url = query ? `${baseUrl}?${query}` : baseUrl;
|
|
354
354
|
const headers = resolveHeaders(opts);
|
|
355
|
-
const json = await request
|
|
355
|
+
const json = await request(url, headers, undefined, withCredentials);
|
|
356
356
|
|
|
357
357
|
if (adapter?.parseListResponse) {
|
|
358
|
-
return adapter.parseListResponse
|
|
358
|
+
return adapter.parseListResponse(json, context);
|
|
359
359
|
}
|
|
360
360
|
if (opts.parseListResponse) {
|
|
361
|
-
return opts.parseListResponse
|
|
361
|
+
return opts.parseListResponse(json, resource);
|
|
362
362
|
}
|
|
363
|
-
return defaultParseListResponse
|
|
363
|
+
return defaultParseListResponse(json);
|
|
364
364
|
},
|
|
365
365
|
|
|
366
|
-
async getOne
|
|
366
|
+
async getOne({ resource, id, meta }) {
|
|
367
367
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
368
|
-
const data = await request
|
|
368
|
+
const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), undefined, withCredentials);
|
|
369
369
|
return { data };
|
|
370
370
|
},
|
|
371
371
|
|
|
372
|
-
async create
|
|
372
|
+
async create({ resource, variables, meta }) {
|
|
373
373
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
374
|
-
const data = await request
|
|
374
|
+
const data = await request(baseUrl, resolveHeaders(opts), {
|
|
375
375
|
method: 'POST',
|
|
376
376
|
body: JSON.stringify(variables),
|
|
377
377
|
}, withCredentials);
|
|
378
378
|
return { data };
|
|
379
379
|
},
|
|
380
380
|
|
|
381
|
-
async update
|
|
381
|
+
async update({ resource, id, variables, meta }) {
|
|
382
382
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
383
|
-
const data = await request
|
|
383
|
+
const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
|
|
384
384
|
method: updateMethod,
|
|
385
385
|
body: JSON.stringify(variables),
|
|
386
386
|
}, withCredentials);
|
|
387
387
|
return { data };
|
|
388
388
|
},
|
|
389
389
|
|
|
390
|
-
async deleteOne
|
|
390
|
+
async deleteOne({ resource, id, meta }) {
|
|
391
391
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
392
|
-
const data = await request
|
|
392
|
+
const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
|
|
393
393
|
method: 'DELETE',
|
|
394
394
|
}, withCredentials);
|
|
395
|
-
return { data: data === undefined ? { id }
|
|
395
|
+
return { data: data === undefined ? { id } : data };
|
|
396
396
|
},
|
|
397
397
|
|
|
398
|
-
async getMany
|
|
398
|
+
async getMany({ resource, ids, meta }) {
|
|
399
399
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
400
400
|
const params = ids.map(id => `id=${encodeURIComponent(String(id))}`).join('&');
|
|
401
|
-
const data = await request
|
|
401
|
+
const data = await request(`${baseUrl}?${params}`, resolveHeaders(opts), undefined, withCredentials);
|
|
402
402
|
return { data };
|
|
403
403
|
},
|
|
404
404
|
|
|
405
|
-
async createMany
|
|
405
|
+
async createMany({ resource, variables, meta }) {
|
|
406
406
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
407
407
|
const results = await Promise.all(
|
|
408
408
|
variables.map(vars =>
|
|
409
|
-
request
|
|
409
|
+
request(baseUrl, resolveHeaders(opts), {
|
|
410
410
|
method: 'POST',
|
|
411
411
|
body: JSON.stringify(vars),
|
|
412
412
|
}, withCredentials)
|
|
@@ -415,11 +415,11 @@ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataP
|
|
|
415
415
|
return { data: results };
|
|
416
416
|
},
|
|
417
417
|
|
|
418
|
-
async updateMany
|
|
418
|
+
async updateMany({ resource, ids, variables, meta }) {
|
|
419
419
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
420
420
|
const results = await Promise.all(
|
|
421
421
|
ids.map(id =>
|
|
422
|
-
request
|
|
422
|
+
request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
|
|
423
423
|
method: updateMethod,
|
|
424
424
|
body: JSON.stringify(variables),
|
|
425
425
|
}, withCredentials)
|
|
@@ -428,19 +428,19 @@ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataP
|
|
|
428
428
|
return { data: results };
|
|
429
429
|
},
|
|
430
430
|
|
|
431
|
-
async deleteMany
|
|
431
|
+
async deleteMany({ resource, ids, meta }) {
|
|
432
432
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
433
433
|
const results = await Promise.all(
|
|
434
434
|
ids.map(id =>
|
|
435
|
-
request
|
|
435
|
+
request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
|
|
436
436
|
method: 'DELETE',
|
|
437
|
-
}, withCredentials).then(data => data === undefined ? { id }
|
|
437
|
+
}, withCredentials).then(data => data === undefined ? { id } : data)
|
|
438
438
|
)
|
|
439
439
|
);
|
|
440
440
|
return { data: results };
|
|
441
441
|
},
|
|
442
442
|
|
|
443
|
-
async custom
|
|
443
|
+
async custom({ url, method, payload, query, headers, sorters, filters }) {
|
|
444
444
|
const requestUrl = buildCustomUrl(url, apiUrl, query, sorters, filters);
|
|
445
445
|
const sameOrigin = isSameOrigin(apiUrl, requestUrl);
|
|
446
446
|
const providerHeaders = resolveHeaders(opts);
|
|
@@ -451,12 +451,12 @@ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataP
|
|
|
451
451
|
sameOrigin ? providerHeaders : DEFAULT_JSON_HEADERS,
|
|
452
452
|
headers,
|
|
453
453
|
);
|
|
454
|
-
const data = await request
|
|
455
|
-
signal,
|
|
454
|
+
const data = await request(requestUrl, requestHeaders, definedOptions({
|
|
456
455
|
method: method.toUpperCase(),
|
|
457
456
|
body: payload === undefined ? undefined : JSON.stringify(payload),
|
|
458
|
-
}, withCredentials && sameOrigin);
|
|
457
|
+
}), withCredentials && sameOrigin);
|
|
459
458
|
return { data };
|
|
460
459
|
},
|
|
461
460
|
};
|
|
461
|
+
return withValidatedResponses(transport);
|
|
462
462
|
}
|