@svadmin/elysia 0.12.7 → 0.13.7

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.12.7",
3
+ "version": "0.13.7",
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.50.0",
23
+ "@svadmin/core": "^0.53.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
+ "@sinclair/typebox": "^0.34.52",
46
47
  "elysia": ">=1.4.30",
47
48
  "@elysiajs/eden": ">=1.4.9"
48
49
  }
@@ -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,26 +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> {
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<T>(response);
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<TData extends BaseRecord>(
283
+ function normalizeObjectListResponse(
287
284
  response: Record<string, unknown>,
288
285
  recordsKey: 'items' | 'data',
289
- ): GetListResult<TData> {
286
+ ): unknown {
290
287
  const { [recordsKey]: rawRecords, ...metadata } = response;
291
- const records = rawRecords as TData[];
292
288
  return {
293
289
  ...metadata,
294
- data: records,
295
- 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,
296
292
  };
297
293
  }
298
294
 
299
- 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 {
300
300
  if (Array.isArray(json)) {
301
- return { data: json as TData[], total: json.length };
301
+ return { data: json, total: json.length };
302
302
  }
303
- const obj = json as Record<string, unknown>;
304
- if (Array.isArray(obj.items)) {
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(obj.data)) {
308
- return normalizeObjectListResponse<TData>(obj, 'data');
306
+ if (isObject(json) && Array.isArray(json['data'])) {
307
+ return normalizeObjectListResponse(json, 'data');
309
308
  }
310
- 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;
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
- return {
336
+ const transport: DataTransport = {
337
337
  getApiUrl: () => apiUrl,
338
338
 
339
- async getList<TData extends BaseRecord = BaseRecord>({ resource, pagination, sorters, filters, meta, signal }: GetListParams): Promise<GetListResult<TData>> {
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<unknown>(url, headers, { signal }, withCredentials);
355
+ const json = await request(url, headers, undefined, withCredentials);
356
356
 
357
357
  if (adapter?.parseListResponse) {
358
- return adapter.parseListResponse<TData>(json, context);
358
+ return adapter.parseListResponse(json, context);
359
359
  }
360
360
  if (opts.parseListResponse) {
361
- return opts.parseListResponse<TData>(json, resource);
361
+ return opts.parseListResponse(json, resource);
362
362
  }
363
- return defaultParseListResponse<TData>(json);
363
+ return defaultParseListResponse(json);
364
364
  },
365
365
 
366
- async getOne<TData extends BaseRecord = BaseRecord>({ resource, id, meta, signal }: GetOneParams): Promise<GetOneResult<TData>> {
366
+ async getOne({ resource, id, meta }) {
367
367
  const baseUrl = resolveResourceUrl(opts, resource, meta);
368
- const data = await request<TData>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), { signal }, withCredentials);
368
+ const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), undefined, withCredentials);
369
369
  return { data };
370
370
  },
371
371
 
372
- async create<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables, meta }: CreateParams<TVariables>): Promise<CreateResult<TData>> {
372
+ async create({ resource, variables, meta }) {
373
373
  const baseUrl = resolveResourceUrl(opts, resource, meta);
374
- const data = await request<TData>(baseUrl, resolveHeaders(opts), {
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<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id, variables, meta }: UpdateParams<TVariables>): Promise<UpdateResult<TData>> {
381
+ async update({ resource, id, variables, meta }) {
382
382
  const baseUrl = resolveResourceUrl(opts, resource, meta);
383
- const data = await request<TData>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
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<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id, meta }: DeleteParams<TVariables>): Promise<DeleteResult<TData>> {
390
+ async deleteOne({ resource, id, meta }) {
391
391
  const baseUrl = resolveResourceUrl(opts, resource, meta);
392
- const data = await request<TData | undefined>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
392
+ const data = await request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
393
393
  method: 'DELETE',
394
394
  }, withCredentials);
395
- return { data: data === undefined ? { id } as unknown as TData : data };
395
+ return { data: data === undefined ? { id } : data };
396
396
  },
397
397
 
398
- async getMany<TData extends BaseRecord = BaseRecord>({ resource, ids, meta, signal }: GetManyParams): Promise<GetManyResult<TData>> {
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<TData[]>(`${baseUrl}?${params}`, resolveHeaders(opts), { signal }, withCredentials);
401
+ const data = await request(`${baseUrl}?${params}`, resolveHeaders(opts), undefined, withCredentials);
402
402
  return { data };
403
403
  },
404
404
 
405
- async createMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables, meta }: CreateManyParams<TVariables>): Promise<CreateManyResult<TData>> {
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<TData>(baseUrl, resolveHeaders(opts), {
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<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, ids, variables, meta }: UpdateManyParams<TVariables>): Promise<UpdateManyResult<TData>> {
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<TData>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
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<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, ids, meta }: DeleteManyParams<TVariables>): Promise<DeleteManyResult<TData>> {
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<TData | undefined>(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
435
+ request(`${baseUrl}/${encodeIdPathSegment(id)}`, resolveHeaders(opts), {
436
436
  method: 'DELETE',
437
- }, withCredentials).then(data => data === undefined ? { id } as unknown as TData : data)
437
+ }, withCredentials).then(data => data === undefined ? { id } : data)
438
438
  )
439
439
  );
440
440
  return { data: results };
441
441
  },
442
442
 
443
- async custom<TData = unknown, TVariables = unknown>({ url, method, payload, query, headers, sorters, filters, signal }: CustomParams<TVariables>): Promise<CustomResult<TData>> {
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<TData>(requestUrl, requestHeaders, {
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
  }