@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/elysia",
3
- "version": "0.11.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.35.0",
24
- "elysia": ">=1.4.29",
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/zuohuadong/svadmin.git",
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
- "elysia": ">=1.4.29",
46
+ "zod": "^4.4.3",
47
+ "elysia": ">=1.4.30",
47
48
  "@elysiajs/eden": ">=1.4.9"
48
49
  }
49
50
  }
@@ -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, 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, 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?: <TData extends BaseRecord = BaseRecord>(
40
+ parseListResponse?: (
43
41
  json: unknown,
44
42
  context: ElysiaListContext,
45
- ) => GetListResult<TData>;
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?: <TData extends BaseRecord = BaseRecord>(
76
+ parseListResponse?: (
79
77
  json: unknown,
80
78
  resource: string,
81
- ) => GetListResult<TData>;
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<T>(response: Response): Promise<T> {
245
+ async function parseResponse(response: Response): Promise<unknown> {
248
246
  if (response.status === 204 || response.status === 205) {
249
- return undefined as unknown as T;
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 as unknown as T;
252
+ return undefined;
255
253
  }
256
254
 
257
255
  const body = await response.text();
258
256
  if (!body || body.trim() === '') {
259
- return undefined as unknown as T;
257
+ return undefined;
260
258
  }
261
259
 
262
- return JSON.parse(body) as T;
260
+ return JSON.parse(body);
263
261
  }
264
262
 
265
- async function request<T>(url: string, headers: Record<string, string>, init?: RequestOptions, withCredentials?: boolean): Promise<T> {
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<T>(response);
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<TData extends BaseRecord>(
283
+ function normalizeObjectListResponse(
286
284
  response: Record<string, unknown>,
287
285
  recordsKey: 'items' | 'data',
288
- ): GetListResult<TData> {
286
+ ): unknown {
289
287
  const { [recordsKey]: rawRecords, ...metadata } = response;
290
- const records = rawRecords as TData[];
291
288
  return {
292
289
  ...metadata,
293
- data: records,
294
- total: response.total !== undefined ? Number(response.total) : records.length,
290
+ data: rawRecords,
291
+ total: response['total'] !== undefined ? response['total'] : Array.isArray(rawRecords) ? rawRecords.length : undefined,
295
292
  };
296
293
  }
297
294
 
298
- function defaultParseListResponse<TData extends BaseRecord>(json: unknown): GetListResult<TData> {
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 as TData[], total: json.length };
301
+ return { data: json, total: json.length };
301
302
  }
302
- const obj = json as Record<string, unknown>;
303
- if (Array.isArray(obj.items)) {
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(obj.data)) {
307
- return normalizeObjectListResponse<TData>(obj, 'data');
306
+ if (isObject(json) && Array.isArray(json['data'])) {
307
+ return normalizeObjectListResponse(json, 'data');
308
308
  }
309
- throw new Error('Unrecognized list response format. Expected { items, total }, { data, total }, or an array.');
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
- return {
336
+ const transport: DataTransport = {
336
337
  getApiUrl: () => apiUrl,
337
338
 
338
- async getList<TData extends BaseRecord = BaseRecord>({ resource, pagination, sorters, filters, meta }: GetListParams): Promise<GetListResult<TData>> {
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<unknown>(url, headers, undefined, withCredentials);
355
+ const json = await request(url, headers, undefined, withCredentials);
355
356
 
356
357
  if (adapter?.parseListResponse) {
357
- return adapter.parseListResponse<TData>(json, context);
358
+ return adapter.parseListResponse(json, context);
358
359
  }
359
360
  if (opts.parseListResponse) {
360
- return opts.parseListResponse<TData>(json, resource);
361
+ return opts.parseListResponse(json, resource);
361
362
  }
362
- return defaultParseListResponse<TData>(json);
363
+ return defaultParseListResponse(json);
363
364
  },
364
365
 
365
- async getOne<TData extends BaseRecord = BaseRecord>({ resource, id, meta }: GetOneParams): Promise<GetOneResult<TData>> {
366
+ async getOne({ resource, id, meta }) {
366
367
  const baseUrl = resolveResourceUrl(opts, resource, meta);
367
- const data = await request<TData>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), undefined, withCredentials);
368
+ const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), undefined, withCredentials);
368
369
  return { data };
369
370
  },
370
371
 
371
- async create<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables, meta }: CreateParams<TVariables>): Promise<CreateResult<TData>> {
372
+ async create({ resource, variables, meta }) {
372
373
  const baseUrl = resolveResourceUrl(opts, resource, meta);
373
- const data = await request<TData>(baseUrl, resolveHeaders(opts), {
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<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id, variables, meta }: UpdateParams<TVariables>): Promise<UpdateResult<TData>> {
381
+ async update({ resource, id, variables, meta }) {
381
382
  const baseUrl = resolveResourceUrl(opts, resource, meta);
382
- const data = await request<TData>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
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<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id, meta }: DeleteParams<TVariables>): Promise<DeleteResult<TData>> {
390
+ async deleteOne({ resource, id, meta }) {
390
391
  const baseUrl = resolveResourceUrl(opts, resource, meta);
391
- const data = await request<TData | undefined>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
392
+ const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
392
393
  method: 'DELETE',
393
394
  }, withCredentials);
394
- return { data: data === undefined ? { id } as unknown as TData : data };
395
+ return { data: data === undefined ? { id } : data };
395
396
  },
396
397
 
397
- async getMany<TData extends BaseRecord = BaseRecord>({ resource, ids, meta }: GetManyParams): Promise<GetManyResult<TData>> {
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<TData[]>(`${baseUrl}?${params}`, resolveHeaders(opts), undefined, withCredentials);
401
+ const data = await request(`${baseUrl}?${params}`, resolveHeaders(opts), undefined, withCredentials);
401
402
  return { data };
402
403
  },
403
404
 
404
- async createMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables, meta }: CreateManyParams<TVariables>): Promise<CreateManyResult<TData>> {
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<TData>(baseUrl, resolveHeaders(opts), {
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<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, ids, variables, meta }: UpdateManyParams<TVariables>): Promise<UpdateManyResult<TData>> {
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<TData>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
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<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, ids, meta }: DeleteManyParams<TVariables>): Promise<DeleteManyResult<TData>> {
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<TData | undefined>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
435
+ request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
435
436
  method: 'DELETE',
436
- }, withCredentials).then(data => data === undefined ? { id } as unknown as TData : data)
437
+ }, withCredentials).then(data => data === undefined ? { id } : data)
437
438
  )
438
439
  );
439
440
  return { data: results };
440
441
  },
441
442
 
442
- async custom<TData = unknown, TVariables = unknown>({ url, method, payload, query, headers, sorters, filters }: CustomParams<TVariables>): Promise<CustomResult<TData>> {
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<TData>(requestUrl, requestHeaders, {
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
  }