@svadmin/elysia 0.10.4 → 0.10.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,11 +1,15 @@
1
1
  {
2
2
  "name": "@svadmin/elysia",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
4
4
  "description": "Elysia DataProvider for svadmin — CRUD convention + InferResourceMap type utility",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "files": [
8
- "src/**/*"
8
+ "src/**/*.ts",
9
+ "!src/**/*.test.*",
10
+ "!src/**/*.spec.*",
11
+ "!src/**/vitest.config.*",
12
+ "!src/**/setupTest.*"
9
13
  ],
10
14
  "main": "src/index.ts",
11
15
  "types": "src/index.ts",
@@ -16,7 +20,7 @@
16
20
  }
17
21
  },
18
22
  "peerDependencies": {
19
- "@svadmin/core": "^0.23.0",
23
+ "@svadmin/core": "^0.25.3",
20
24
  "elysia": ">=1.0.0",
21
25
  "@elysiajs/eden": ">=1.0.0"
22
26
  },
@@ -26,5 +30,16 @@
26
30
  "type": "git",
27
31
  "url": "https://github.com/zuohuadong/svadmin.git",
28
32
  "directory": "packages/elysia"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "@svadmin/core": {
36
+ "optional": true
37
+ },
38
+ "elysia": {
39
+ "optional": true
40
+ },
41
+ "@elysiajs/eden": {
42
+ "optional": true
43
+ }
29
44
  }
30
45
  }
@@ -1,440 +0,0 @@
1
- // Tests for enhanced Elysia DataProvider
2
- import { describe, it, expect, beforeEach, mock, afterEach } from 'bun:test';
3
- import { createElysiaDataProvider } from './data-provider';
4
- import type { DataProvider } from '@svadmin/core';
5
-
6
- // ─── Mock fetch ──────────────────────────────────────────────
7
-
8
- const originalFetch = globalThis.fetch;
9
- let mockFetchFn: ReturnType<typeof mock>;
10
-
11
- function setupMockFetch(response: unknown, status = 200, statusText = 'OK') {
12
- mockFetchFn = mock(() =>
13
- Promise.resolve({
14
- ok: status >= 200 && status < 300,
15
- status,
16
- statusText,
17
- json: () => Promise.resolve(response),
18
- text: () => Promise.resolve(JSON.stringify(response)),
19
- } as Response)
20
- );
21
- globalThis.fetch = mockFetchFn as unknown as typeof fetch;
22
- }
23
-
24
- afterEach(() => {
25
- globalThis.fetch = originalFetch;
26
- });
27
-
28
- // ─── Basic CRUD ──────────────────────────────────────────────
29
-
30
- describe('createElysiaDataProvider', () => {
31
- let provider: DataProvider;
32
-
33
- beforeEach(() => {
34
- provider = createElysiaDataProvider({ apiUrl: 'http://localhost:3000' });
35
- });
36
-
37
- it('should return the API URL', () => {
38
- expect(provider.getApiUrl()).toBe('http://localhost:3000');
39
- });
40
-
41
- // ─── getList ───────────────────────────────────────────────
42
-
43
- describe('getList', () => {
44
- it('should fetch list with { items, total } format', async () => {
45
- const mockData = { items: [{ id: 1, name: 'Test' }], total: 1 };
46
- setupMockFetch(mockData);
47
-
48
- const result = await provider.getList({ resource: 'posts' });
49
-
50
- expect(result.data).toEqual([{ id: 1, name: 'Test' }]);
51
- expect(result.total).toBe(1);
52
- expect(mockFetchFn).toHaveBeenCalledTimes(1);
53
- const [url] = mockFetchFn.mock.calls[0] as [string, RequestInit];
54
- expect(url).toContain('http://localhost:3000/posts?');
55
- expect(url).toContain('_page=1');
56
- expect(url).toContain('_limit=10');
57
- });
58
-
59
- it('should fetch list with { data, total } format', async () => {
60
- const mockData = { data: [{ id: 1 }, { id: 2 }], total: 42 };
61
- setupMockFetch(mockData);
62
-
63
- const result = await provider.getList({ resource: 'users' });
64
- expect(result.data).toEqual([{ id: 1 }, { id: 2 }]);
65
- expect(result.total).toBe(42);
66
- });
67
-
68
- it('should fetch list with raw array format', async () => {
69
- const mockData = [{ id: 1 }, { id: 2 }, { id: 3 }];
70
- setupMockFetch(mockData);
71
-
72
- const result = await provider.getList({ resource: 'channels' });
73
- expect(result.data).toEqual(mockData);
74
- expect(result.total).toBe(3);
75
- });
76
-
77
- it('should apply pagination params', async () => {
78
- setupMockFetch({ items: [], total: 0 });
79
-
80
- await provider.getList({
81
- resource: 'posts',
82
- pagination: { current: 3, pageSize: 25 },
83
- });
84
-
85
- const [url] = mockFetchFn.mock.calls[0] as [string, RequestInit];
86
- expect(url).toContain('_page=3');
87
- expect(url).toContain('_limit=25');
88
- });
89
-
90
- it('should apply sorters', async () => {
91
- setupMockFetch({ items: [], total: 0 });
92
-
93
- await provider.getList({
94
- resource: 'posts',
95
- sorters: [{ field: 'name', order: 'asc' }],
96
- });
97
-
98
- const [url] = mockFetchFn.mock.calls[0] as [string, RequestInit];
99
- expect(url).toContain('_sort=name');
100
- expect(url).toContain('_order=asc');
101
- });
102
-
103
- it('should apply filters', async () => {
104
- setupMockFetch({ items: [], total: 0 });
105
-
106
- await provider.getList({
107
- resource: 'posts',
108
- filters: [
109
- { field: 'status', operator: 'eq', value: 'active' },
110
- { field: 'name', operator: 'contains', value: 'test' },
111
- { field: 'age', operator: 'gte', value: 18 },
112
- ],
113
- });
114
-
115
- const [url] = mockFetchFn.mock.calls[0] as [string, RequestInit];
116
- expect(url).toContain('status=active');
117
- expect(url).toContain('name_like=test');
118
- expect(url).toContain('age_gte=18');
119
- });
120
- });
121
-
122
- // ─── getOne ────────────────────────────────────────────────
123
-
124
- describe('getOne', () => {
125
- it('should fetch a single record', async () => {
126
- const mockData = { id: 1, name: 'Test' };
127
- setupMockFetch(mockData);
128
-
129
- const result = await provider.getOne({ resource: 'posts', id: 1 });
130
- expect(result.data).toEqual(mockData);
131
-
132
- const [url] = mockFetchFn.mock.calls[0] as [string, RequestInit];
133
- expect(url).toBe('http://localhost:3000/posts/1');
134
- });
135
- });
136
-
137
- // ─── create ────────────────────────────────────────────────
138
-
139
- describe('create', () => {
140
- it('should create a record with POST', async () => {
141
- const mockData = { id: 1, name: 'New' };
142
- setupMockFetch(mockData);
143
-
144
- const result = await provider.create({
145
- resource: 'posts',
146
- variables: { name: 'New' },
147
- });
148
-
149
- expect(result.data).toEqual(mockData);
150
- const [url, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
151
- expect(url).toBe('http://localhost:3000/posts');
152
- expect(init.method).toBe('POST');
153
- expect(init.body).toBe('{"name":"New"}');
154
- });
155
- });
156
-
157
- // ─── update ────────────────────────────────────────────────
158
-
159
- describe('update', () => {
160
- it('should default to PATCH method', async () => {
161
- const mockData = { id: 1, name: 'Updated' };
162
- setupMockFetch(mockData);
163
-
164
- await provider.update({
165
- resource: 'posts',
166
- id: 1,
167
- variables: { name: 'Updated' },
168
- });
169
-
170
- const [, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
171
- expect(init.method).toBe('PATCH');
172
- });
173
- });
174
-
175
- // ─── deleteOne ─────────────────────────────────────────────
176
-
177
- describe('deleteOne', () => {
178
- it('should delete a record', async () => {
179
- setupMockFetch({ id: 1 });
180
-
181
- const result = await provider.deleteOne({ resource: 'posts', id: 1 });
182
- expect(result.data).toEqual({ id: 1 });
183
-
184
- const [url, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
185
- expect(url).toBe('http://localhost:3000/posts/1');
186
- expect(init.method).toBe('DELETE');
187
- });
188
- });
189
-
190
- // ─── error handling ────────────────────────────────────────
191
-
192
- describe('error handling', () => {
193
- it('should throw on non-OK response', async () => {
194
- setupMockFetch({ error: 'Not found' }, 404, 'Not Found');
195
-
196
- await expect(
197
- provider.getOne({ resource: 'posts', id: 999 })
198
- ).rejects.toThrow('HTTP 404');
199
- });
200
- });
201
- });
202
-
203
- // ─── updateMethod option ─────────────────────────────────────
204
-
205
- describe('updateMethod: PUT', () => {
206
- it('should use PUT for updates when configured', async () => {
207
- const provider = createElysiaDataProvider({
208
- apiUrl: 'http://localhost:3000',
209
- updateMethod: 'PUT',
210
- });
211
-
212
- setupMockFetch({ id: 1, name: 'Updated' });
213
-
214
- await provider.update({
215
- resource: 'channels',
216
- id: 1,
217
- variables: { name: 'Updated' },
218
- });
219
-
220
- const [url, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
221
- expect(url).toBe('http://localhost:3000/channels/1');
222
- expect(init.method).toBe('PUT');
223
- });
224
-
225
- it('should use PUT for updateMany when configured', async () => {
226
- const provider = createElysiaDataProvider({
227
- apiUrl: 'http://localhost:3000',
228
- updateMethod: 'PUT',
229
- });
230
-
231
- setupMockFetch({ id: 1 });
232
-
233
- await provider.updateMany!({
234
- resource: 'channels',
235
- ids: [1, 2],
236
- variables: { status: 'active' },
237
- });
238
-
239
- for (const call of mockFetchFn.mock.calls) {
240
- const [, init] = call as [string, RequestInit];
241
- expect(init.method).toBe('PUT');
242
- }
243
- });
244
- });
245
-
246
- // ─── withCredentials option ──────────────────────────────────
247
-
248
- describe('withCredentials', () => {
249
- it('should include credentials when enabled', async () => {
250
- const provider = createElysiaDataProvider({
251
- apiUrl: 'http://localhost:3000',
252
- withCredentials: true,
253
- });
254
-
255
- setupMockFetch({ items: [], total: 0 });
256
-
257
- await provider.getList({ resource: 'posts' });
258
-
259
- const [, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
260
- expect(init.credentials).toBe('include');
261
- });
262
-
263
- it('should not include credentials when disabled', async () => {
264
- const provider = createElysiaDataProvider({
265
- apiUrl: 'http://localhost:3000',
266
- });
267
-
268
- setupMockFetch({ items: [], total: 0 });
269
-
270
- await provider.getList({ resource: 'posts' });
271
-
272
- const [, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
273
- expect(init.credentials).toBeUndefined();
274
- });
275
- });
276
-
277
- // ─── resourceUrlMap option ───────────────────────────────────
278
-
279
- describe('resourceUrlMap', () => {
280
- it('should map resource names to custom URL segments', async () => {
281
- const provider = createElysiaDataProvider({
282
- apiUrl: 'http://localhost:3000/admin',
283
- resourceUrlMap: {
284
- user_groups: 'user-groups',
285
- rateLimits: 'rate-limits',
286
- },
287
- });
288
-
289
- setupMockFetch([{ id: 1 }]);
290
-
291
- await provider.getList({ resource: 'user_groups' });
292
- const [url1] = mockFetchFn.mock.calls[0] as [string, RequestInit];
293
- expect(url1).toContain('http://localhost:3000/admin/user-groups?');
294
-
295
- setupMockFetch({ id: 1 });
296
-
297
- await provider.getOne({ resource: 'rateLimits', id: 1 });
298
- const [url2] = mockFetchFn.mock.calls[0] as [string, RequestInit];
299
- expect(url2).toBe('http://localhost:3000/admin/rate-limits/1');
300
- });
301
-
302
- it('should use resource name as-is when no mapping exists', async () => {
303
- const provider = createElysiaDataProvider({
304
- apiUrl: 'http://localhost:3000',
305
- resourceUrlMap: { mapped: 'mapped-url' },
306
- });
307
-
308
- setupMockFetch({ items: [], total: 0 });
309
-
310
- await provider.getList({ resource: 'unmapped' });
311
- const [url] = mockFetchFn.mock.calls[0] as [string, RequestInit];
312
- expect(url).toContain('http://localhost:3000/unmapped?');
313
- });
314
- });
315
-
316
- // ─── parseListResponse option ────────────────────────────────
317
-
318
- describe('parseListResponse', () => {
319
- it('should use custom parser when provided', async () => {
320
- const provider = createElysiaDataProvider({
321
- apiUrl: 'http://localhost:3000',
322
- parseListResponse: <T>(json: unknown) => {
323
- const obj = json as { results: T[]; count: number };
324
- return { data: obj.results, total: obj.count };
325
- },
326
- });
327
-
328
- setupMockFetch({ results: [{ id: 1 }], count: 100 });
329
-
330
- const result = await provider.getList({ resource: 'posts' });
331
- expect(result.data).toEqual([{ id: 1 }]);
332
- expect(result.total).toBe(100);
333
- });
334
- });
335
-
336
- // ─── headers option ──────────────────────────────────────────
337
-
338
- describe('headers', () => {
339
- it('should support static headers', async () => {
340
- const provider = createElysiaDataProvider({
341
- apiUrl: 'http://localhost:3000',
342
- headers: { 'Authorization': 'Bearer token123' },
343
- });
344
-
345
- setupMockFetch({ items: [], total: 0 });
346
-
347
- await provider.getList({ resource: 'posts' });
348
-
349
- const [, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
350
- const headers = init.headers as Record<string, string>;
351
- expect(headers['Authorization']).toBe('Bearer token123');
352
- expect(headers['Content-Type']).toBe('application/json');
353
- });
354
-
355
- it('should support dynamic headers function', async () => {
356
- let token = 'token1';
357
- const provider = createElysiaDataProvider({
358
- apiUrl: 'http://localhost:3000',
359
- headers: () => ({ 'Authorization': `Bearer ${token}` }),
360
- });
361
-
362
- setupMockFetch({ items: [], total: 0 });
363
-
364
- await provider.getList({ resource: 'posts' });
365
- let [, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
366
- expect((init.headers as Record<string, string>)['Authorization']).toBe('Bearer token1');
367
-
368
- // Change token
369
- token = 'token2';
370
- setupMockFetch({ items: [], total: 0 });
371
-
372
- await provider.getList({ resource: 'posts' });
373
- [, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
374
- expect((init.headers as Record<string, string>)['Authorization']).toBe('Bearer token2');
375
- });
376
- });
377
-
378
- // ─── custom method ───────────────────────────────────────────
379
-
380
- describe('custom', () => {
381
- it('should support custom API calls', async () => {
382
- const provider = createElysiaDataProvider({
383
- apiUrl: 'http://localhost:3000',
384
- withCredentials: true,
385
- });
386
-
387
- setupMockFetch({ success: true, modelsCount: 42 });
388
-
389
- const result = await provider.custom!({
390
- url: 'http://localhost:3000/channels/1/sync-models',
391
- method: 'post',
392
- });
393
-
394
- expect(result.data).toEqual({ success: true, modelsCount: 42 });
395
- const [url, init] = mockFetchFn.mock.calls[0] as [string, RequestInit];
396
- expect(url).toBe('http://localhost:3000/channels/1/sync-models');
397
- expect(init.method).toBe('POST');
398
- expect(init.credentials).toBe('include');
399
- });
400
- });
401
-
402
- // ─── bulk operations ─────────────────────────────────────────
403
-
404
- describe('bulk operations', () => {
405
- it('should support getMany', async () => {
406
- const provider = createElysiaDataProvider({ apiUrl: 'http://localhost:3000' });
407
- setupMockFetch([{ id: 1 }, { id: 2 }]);
408
-
409
- const result = await provider.getMany!({ resource: 'posts', ids: [1, 2] });
410
- expect(result.data).toEqual([{ id: 1 }, { id: 2 }]);
411
-
412
- const [url] = mockFetchFn.mock.calls[0] as [string, RequestInit];
413
- expect(url).toContain('id=1&id=2');
414
- });
415
-
416
- it('should support createMany', async () => {
417
- const provider = createElysiaDataProvider({ apiUrl: 'http://localhost:3000' });
418
- setupMockFetch({ id: 1, name: 'A' });
419
-
420
- const result = await provider.createMany!({
421
- resource: 'posts',
422
- variables: [{ name: 'A' }, { name: 'B' }],
423
- });
424
-
425
- expect(mockFetchFn).toHaveBeenCalledTimes(2);
426
- });
427
-
428
- it('should support deleteMany', async () => {
429
- const provider = createElysiaDataProvider({ apiUrl: 'http://localhost:3000' });
430
- setupMockFetch({ success: true });
431
-
432
- await provider.deleteMany!({ resource: 'posts', ids: [1, 2, 3] });
433
-
434
- expect(mockFetchFn).toHaveBeenCalledTimes(3);
435
- for (const call of mockFetchFn.mock.calls) {
436
- const [, init] = call as [string, RequestInit];
437
- expect(init.method).toBe('DELETE');
438
- }
439
- });
440
- });