@svadmin/elysia 0.11.0 → 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 +6 -5
- package/src/data-provider.ts +61 -59
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,15 +20,15 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"@svadmin/core": "^0.
|
|
24
|
-
"elysia": ">=1.4.
|
|
23
|
+
"@svadmin/core": "^0.52.0",
|
|
24
|
+
"elysia": ">=1.4.30",
|
|
25
25
|
"@elysiajs/eden": ">=1.4.9"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"author": "zuohuadong",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/
|
|
31
|
+
"url": "git+https://github.com/vibeunion/svadmin.git",
|
|
32
32
|
"directory": "packages/elysia"
|
|
33
33
|
},
|
|
34
34
|
"peerDependenciesMeta": {
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"
|
|
46
|
+
"zod": "^4.4.3",
|
|
47
|
+
"elysia": ">=1.4.30",
|
|
47
48
|
"@elysiajs/eden": ">=1.4.9"
|
|
48
49
|
}
|
|
49
50
|
}
|
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,25 +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
|
|
263
|
+
async function request(url: string, headers: Record<string, string>, init?: RequestOptions, withCredentials?: boolean): Promise<unknown> {
|
|
266
264
|
const fetchInit: RequestInit = { ...init, headers: mergeHeaders(headers, init?.headers) };
|
|
267
265
|
if (withCredentials) {
|
|
268
266
|
fetchInit.credentials = 'include';
|
|
@@ -272,7 +270,7 @@ async function request<T>(url: string, headers: Record<string, string>, init?: R
|
|
|
272
270
|
const body = await response.text().catch(() => '');
|
|
273
271
|
throw new Error(`HTTP ${response.status}: ${response.statusText}${body ? ` — ${body}` : ''}`);
|
|
274
272
|
}
|
|
275
|
-
return parseResponse
|
|
273
|
+
return parseResponse(response);
|
|
276
274
|
}
|
|
277
275
|
|
|
278
276
|
/**
|
|
@@ -282,31 +280,34 @@ async function request<T>(url: string, headers: Record<string, string>, init?: R
|
|
|
282
280
|
* - `{ data: T[], total: number }` (common alternative)
|
|
283
281
|
* - `T[]` (raw array — total is inferred from array length)
|
|
284
282
|
*/
|
|
285
|
-
function normalizeObjectListResponse
|
|
283
|
+
function normalizeObjectListResponse(
|
|
286
284
|
response: Record<string, unknown>,
|
|
287
285
|
recordsKey: 'items' | 'data',
|
|
288
|
-
):
|
|
286
|
+
): unknown {
|
|
289
287
|
const { [recordsKey]: rawRecords, ...metadata } = response;
|
|
290
|
-
const records = rawRecords as TData[];
|
|
291
288
|
return {
|
|
292
289
|
...metadata,
|
|
293
|
-
data:
|
|
294
|
-
total: response
|
|
290
|
+
data: rawRecords,
|
|
291
|
+
total: response['total'] !== undefined ? response['total'] : Array.isArray(rawRecords) ? rawRecords.length : undefined,
|
|
295
292
|
};
|
|
296
293
|
}
|
|
297
294
|
|
|
298
|
-
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 {
|
|
299
300
|
if (Array.isArray(json)) {
|
|
300
|
-
return { data: json
|
|
301
|
+
return { data: json, total: json.length };
|
|
301
302
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return normalizeObjectListResponse<TData>(obj, 'items');
|
|
303
|
+
if (isObject(json) && Array.isArray(json['items'])) {
|
|
304
|
+
return normalizeObjectListResponse(json, 'items');
|
|
305
305
|
}
|
|
306
|
-
if (Array.isArray(
|
|
307
|
-
return normalizeObjectListResponse
|
|
306
|
+
if (isObject(json) && Array.isArray(json['data'])) {
|
|
307
|
+
return normalizeObjectListResponse(json, 'data');
|
|
308
308
|
}
|
|
309
|
-
|
|
309
|
+
// Keep malformed payloads unknown so the shared boundary rejects them.
|
|
310
|
+
return json;
|
|
310
311
|
}
|
|
311
312
|
|
|
312
313
|
/**
|
|
@@ -332,80 +333,80 @@ function defaultParseListResponse<TData extends BaseRecord>(json: unknown): GetL
|
|
|
332
333
|
export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataProvider {
|
|
333
334
|
const { apiUrl, updateMethod = 'PATCH', withCredentials = false } = opts;
|
|
334
335
|
|
|
335
|
-
|
|
336
|
+
const transport: DataTransport = {
|
|
336
337
|
getApiUrl: () => apiUrl,
|
|
337
338
|
|
|
338
|
-
async getList
|
|
339
|
+
async getList({ resource, pagination, sorters, filters, meta }) {
|
|
339
340
|
const { current = 1, pageSize = 10 } = pagination ?? {};
|
|
340
|
-
const context: ElysiaListContext = {
|
|
341
|
+
const context: ElysiaListContext = definedOptions({
|
|
341
342
|
apiUrl,
|
|
342
343
|
resource,
|
|
343
344
|
meta,
|
|
344
345
|
pagination: { current, pageSize, mode: pagination?.mode },
|
|
345
346
|
sorters: sorters ?? [],
|
|
346
347
|
filters: filters ?? [],
|
|
347
|
-
};
|
|
348
|
+
});
|
|
348
349
|
const adapter = resolveResourceAdapter(opts, resource);
|
|
349
350
|
const params = adapter?.buildListSearchParams?.(context) ?? buildDefaultListSearchParams(context);
|
|
350
351
|
const baseUrl = resolveResourceUrlWithAdapter(opts, context, adapter);
|
|
351
352
|
const query = params.toString();
|
|
352
353
|
const url = query ? `${baseUrl}?${query}` : baseUrl;
|
|
353
354
|
const headers = resolveHeaders(opts);
|
|
354
|
-
const json = await request
|
|
355
|
+
const json = await request(url, headers, undefined, withCredentials);
|
|
355
356
|
|
|
356
357
|
if (adapter?.parseListResponse) {
|
|
357
|
-
return adapter.parseListResponse
|
|
358
|
+
return adapter.parseListResponse(json, context);
|
|
358
359
|
}
|
|
359
360
|
if (opts.parseListResponse) {
|
|
360
|
-
return opts.parseListResponse
|
|
361
|
+
return opts.parseListResponse(json, resource);
|
|
361
362
|
}
|
|
362
|
-
return defaultParseListResponse
|
|
363
|
+
return defaultParseListResponse(json);
|
|
363
364
|
},
|
|
364
365
|
|
|
365
|
-
async getOne
|
|
366
|
+
async getOne({ resource, id, meta }) {
|
|
366
367
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
367
|
-
const data = await request
|
|
368
|
+
const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), undefined, withCredentials);
|
|
368
369
|
return { data };
|
|
369
370
|
},
|
|
370
371
|
|
|
371
|
-
async create
|
|
372
|
+
async create({ resource, variables, meta }) {
|
|
372
373
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
373
|
-
const data = await request
|
|
374
|
+
const data = await request(baseUrl, resolveHeaders(opts), {
|
|
374
375
|
method: 'POST',
|
|
375
376
|
body: JSON.stringify(variables),
|
|
376
377
|
}, withCredentials);
|
|
377
378
|
return { data };
|
|
378
379
|
},
|
|
379
380
|
|
|
380
|
-
async update
|
|
381
|
+
async update({ resource, id, variables, meta }) {
|
|
381
382
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
382
|
-
const data = await request
|
|
383
|
+
const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
|
|
383
384
|
method: updateMethod,
|
|
384
385
|
body: JSON.stringify(variables),
|
|
385
386
|
}, withCredentials);
|
|
386
387
|
return { data };
|
|
387
388
|
},
|
|
388
389
|
|
|
389
|
-
async deleteOne
|
|
390
|
+
async deleteOne({ resource, id, meta }) {
|
|
390
391
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
391
|
-
const data = await request
|
|
392
|
+
const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
|
|
392
393
|
method: 'DELETE',
|
|
393
394
|
}, withCredentials);
|
|
394
|
-
return { data: data === undefined ? { id }
|
|
395
|
+
return { data: data === undefined ? { id } : data };
|
|
395
396
|
},
|
|
396
397
|
|
|
397
|
-
async getMany
|
|
398
|
+
async getMany({ resource, ids, meta }) {
|
|
398
399
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
399
400
|
const params = ids.map(id => `id=${encodeURIComponent(String(id))}`).join('&');
|
|
400
|
-
const data = await request
|
|
401
|
+
const data = await request(`${baseUrl}?${params}`, resolveHeaders(opts), undefined, withCredentials);
|
|
401
402
|
return { data };
|
|
402
403
|
},
|
|
403
404
|
|
|
404
|
-
async createMany
|
|
405
|
+
async createMany({ resource, variables, meta }) {
|
|
405
406
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
406
407
|
const results = await Promise.all(
|
|
407
408
|
variables.map(vars =>
|
|
408
|
-
request
|
|
409
|
+
request(baseUrl, resolveHeaders(opts), {
|
|
409
410
|
method: 'POST',
|
|
410
411
|
body: JSON.stringify(vars),
|
|
411
412
|
}, withCredentials)
|
|
@@ -414,11 +415,11 @@ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataP
|
|
|
414
415
|
return { data: results };
|
|
415
416
|
},
|
|
416
417
|
|
|
417
|
-
async updateMany
|
|
418
|
+
async updateMany({ resource, ids, variables, meta }) {
|
|
418
419
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
419
420
|
const results = await Promise.all(
|
|
420
421
|
ids.map(id =>
|
|
421
|
-
request
|
|
422
|
+
request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
|
|
422
423
|
method: updateMethod,
|
|
423
424
|
body: JSON.stringify(variables),
|
|
424
425
|
}, withCredentials)
|
|
@@ -427,19 +428,19 @@ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataP
|
|
|
427
428
|
return { data: results };
|
|
428
429
|
},
|
|
429
430
|
|
|
430
|
-
async deleteMany
|
|
431
|
+
async deleteMany({ resource, ids, meta }) {
|
|
431
432
|
const baseUrl = resolveResourceUrl(opts, resource, meta);
|
|
432
433
|
const results = await Promise.all(
|
|
433
434
|
ids.map(id =>
|
|
434
|
-
request
|
|
435
|
+
request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
|
|
435
436
|
method: 'DELETE',
|
|
436
|
-
}, withCredentials).then(data => data === undefined ? { id }
|
|
437
|
+
}, withCredentials).then(data => data === undefined ? { id } : data)
|
|
437
438
|
)
|
|
438
439
|
);
|
|
439
440
|
return { data: results };
|
|
440
441
|
},
|
|
441
442
|
|
|
442
|
-
async custom
|
|
443
|
+
async custom({ url, method, payload, query, headers, sorters, filters }) {
|
|
443
444
|
const requestUrl = buildCustomUrl(url, apiUrl, query, sorters, filters);
|
|
444
445
|
const sameOrigin = isSameOrigin(apiUrl, requestUrl);
|
|
445
446
|
const providerHeaders = resolveHeaders(opts);
|
|
@@ -450,11 +451,12 @@ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataP
|
|
|
450
451
|
sameOrigin ? providerHeaders : DEFAULT_JSON_HEADERS,
|
|
451
452
|
headers,
|
|
452
453
|
);
|
|
453
|
-
const data = await request
|
|
454
|
+
const data = await request(requestUrl, requestHeaders, definedOptions({
|
|
454
455
|
method: method.toUpperCase(),
|
|
455
456
|
body: payload === undefined ? undefined : JSON.stringify(payload),
|
|
456
|
-
}, withCredentials && sameOrigin);
|
|
457
|
+
}), withCredentials && sameOrigin);
|
|
457
458
|
return { data };
|
|
458
459
|
},
|
|
459
460
|
};
|
|
461
|
+
return withValidatedResponses(transport);
|
|
460
462
|
}
|