@svadmin/elysia 0.6.0 → 0.7.0

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.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Elysia DataProvider for svadmin — CRUD convention + InferResourceMap type utility",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -0,0 +1,440 @@
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
+ });
@@ -1,6 +1,6 @@
1
1
  // Elysia DataProvider — CRUD convention compatible
2
2
  // Expects backend routes following: GET /resource, GET /resource/:id, POST /resource, PATCH /resource/:id, DELETE /resource/:id
3
- // Response format for lists: { items: T[], total: number }
3
+ // Response format for lists: { items: T[], total: number } (also supports raw arrays)
4
4
 
5
5
  import type {
6
6
  DataProvider, GetListParams, GetListResult, GetOneParams, GetOneResult,
@@ -15,16 +15,51 @@ export interface ElysiaDataProviderOptions {
15
15
  apiUrl: string;
16
16
  /** Static headers or a function returning headers (useful for auth tokens) */
17
17
  headers?: Record<string, string> | (() => Record<string, string>);
18
+ /**
19
+ * HTTP method to use for update operations.
20
+ * @default 'PATCH'
21
+ */
22
+ updateMethod?: 'PATCH' | 'PUT';
23
+ /**
24
+ * Whether to include credentials (cookies) in requests.
25
+ * Set to `true` for cookie-based authentication.
26
+ * @default false
27
+ */
28
+ withCredentials?: boolean;
29
+ /**
30
+ * Custom resource-to-URL segment mapping.
31
+ * Maps a resource name to a URL segment, e.g. `{ user_groups: 'user-groups' }`.
32
+ * When not provided, the resource name is used as-is.
33
+ */
34
+ resourceUrlMap?: Record<string, string>;
35
+ /**
36
+ * Custom response parser for list endpoints.
37
+ * When provided, this function extracts `{ data, total }` from the raw JSON response.
38
+ * Use the `resource` parameter to apply different parsers per resource.
39
+ * Useful when the backend response format differs from `{ items, total }`.
40
+ *
41
+ * @default Handles `{ items, total }` and raw arrays automatically
42
+ */
43
+ parseListResponse?: <T>(json: unknown, resource: string) => { data: T[]; total: number };
18
44
  }
19
45
 
20
46
  function resolveHeaders(opts: ElysiaDataProviderOptions): Record<string, string> {
21
- const base = { 'Content-Type': 'application/json' };
47
+ const base: Record<string, string> = { 'Content-Type': 'application/json' };
22
48
  const extra = typeof opts.headers === 'function' ? opts.headers() : (opts.headers ?? {});
23
49
  return { ...base, ...extra };
24
50
  }
25
51
 
26
- async function request<T>(url: string, headers: Record<string, string>, init?: RequestInit): Promise<T> {
27
- const response = await fetch(url, { ...init, headers: { ...headers, ...init?.headers } });
52
+ function resolveResourceUrl(opts: ElysiaDataProviderOptions, resource: string): string {
53
+ const segment = opts.resourceUrlMap?.[resource] ?? resource;
54
+ return `${opts.apiUrl}/${segment}`;
55
+ }
56
+
57
+ async function request<T>(url: string, headers: Record<string, string>, init?: RequestInit, withCredentials?: boolean): Promise<T> {
58
+ const fetchInit: RequestInit = { ...init, headers: { ...headers, ...init?.headers } };
59
+ if (withCredentials) {
60
+ fetchInit.credentials = 'include';
61
+ }
62
+ const response = await fetch(url, fetchInit);
28
63
  if (!response.ok) {
29
64
  const body = await response.text().catch(() => '');
30
65
  throw new Error(`HTTP ${response.status}: ${response.statusText}${body ? ` — ${body}` : ''}`);
@@ -32,14 +67,49 @@ async function request<T>(url: string, headers: Record<string, string>, init?: R
32
67
  return response.json();
33
68
  }
34
69
 
70
+ /**
71
+ * Default list response parser.
72
+ * Supports:
73
+ * - `{ items: T[], total: number }` (standard)
74
+ * - `{ data: T[], total: number }` (common alternative)
75
+ * - `T[]` (raw array — total is inferred from array length)
76
+ */
77
+ function defaultParseListResponse<T>(json: unknown): { data: T[]; total: number } {
78
+ if (Array.isArray(json)) {
79
+ return { data: json as T[], total: json.length };
80
+ }
81
+ const obj = json as Record<string, unknown>;
82
+ if (Array.isArray(obj.items)) {
83
+ return { data: obj.items as T[], total: obj.total !== undefined ? Number(obj.total) : obj.items.length };
84
+ }
85
+ if (Array.isArray(obj.data)) {
86
+ return { data: obj.data as T[], total: obj.total !== undefined ? Number(obj.total) : obj.data.length };
87
+ }
88
+ throw new Error('Unrecognized list response format. Expected { items, total }, { data, total }, or an array.');
89
+ }
90
+
35
91
  /**
36
92
  * Creates a DataProvider for Elysia backends using the CRUD plugin convention.
37
93
  *
38
- * List responses are expected to return `{ items: T[], total: number }`.
39
- * Single-record responses return the record directly.
94
+ * List responses support multiple formats:
95
+ * - `{ items: T[], total: number }` (standard CRUD convention)
96
+ * - `{ data: T[], total: number }` (common alternative)
97
+ * - `T[]` (raw array — total is inferred from length)
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const dataProvider = createElysiaDataProvider({
102
+ * apiUrl: 'http://localhost:3000/api',
103
+ * withCredentials: true, // cookie auth
104
+ * updateMethod: 'PUT', // use PUT instead of PATCH
105
+ * resourceUrlMap: {
106
+ * user_groups: 'user-groups',
107
+ * },
108
+ * });
109
+ * ```
40
110
  */
41
111
  export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataProvider {
42
- const { apiUrl } = opts;
112
+ const { apiUrl, updateMethod = 'PATCH', withCredentials = false } = opts;
43
113
 
44
114
  return {
45
115
  getApiUrl: () => apiUrl,
@@ -63,78 +133,91 @@ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataP
63
133
  }
64
134
  }
65
135
 
66
- const url = `${apiUrl}/${resource}?${params.toString()}`;
136
+ const baseUrl = resolveResourceUrl(opts, resource);
137
+ const url = `${baseUrl}?${params.toString()}`;
67
138
  const headers = resolveHeaders(opts);
68
- const json = await request<{ items: TData[]; total: number }>(url, headers);
69
- return { data: json.items, total: json.total };
139
+ const json = await request<unknown>(url, headers, undefined, withCredentials);
140
+
141
+ if (opts.parseListResponse) {
142
+ return opts.parseListResponse<TData>(json, resource);
143
+ }
144
+ return defaultParseListResponse<TData>(json);
70
145
  },
71
146
 
72
147
  async getOne<TData extends BaseRecord = BaseRecord>({ resource, id }: GetOneParams): Promise<GetOneResult<TData>> {
73
- const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts));
148
+ const baseUrl = resolveResourceUrl(opts, resource);
149
+ const data = await request<TData>(`${baseUrl}/${id}`, resolveHeaders(opts), undefined, withCredentials);
74
150
  return { data };
75
151
  },
76
152
 
77
153
  async create<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables }: CreateParams<TVariables>): Promise<CreateResult<TData>> {
78
- const data = await request<TData>(`${apiUrl}/${resource}`, resolveHeaders(opts), {
154
+ const baseUrl = resolveResourceUrl(opts, resource);
155
+ const data = await request<TData>(baseUrl, resolveHeaders(opts), {
79
156
  method: 'POST',
80
157
  body: JSON.stringify(variables),
81
- });
158
+ }, withCredentials);
82
159
  return { data };
83
160
  },
84
161
 
85
162
  async update<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id, variables }: UpdateParams<TVariables>): Promise<UpdateResult<TData>> {
86
- const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts), {
87
- method: 'PATCH',
163
+ const baseUrl = resolveResourceUrl(opts, resource);
164
+ const data = await request<TData>(`${baseUrl}/${id}`, resolveHeaders(opts), {
165
+ method: updateMethod,
88
166
  body: JSON.stringify(variables),
89
- });
167
+ }, withCredentials);
90
168
  return { data };
91
169
  },
92
170
 
93
171
  async deleteOne<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, id }: DeleteParams<TVariables>): Promise<DeleteResult<TData>> {
94
- const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts), {
172
+ const baseUrl = resolveResourceUrl(opts, resource);
173
+ const data = await request<TData>(`${baseUrl}/${id}`, resolveHeaders(opts), {
95
174
  method: 'DELETE',
96
- });
175
+ }, withCredentials);
97
176
  return { data };
98
177
  },
99
178
 
100
179
  async getMany<TData extends BaseRecord = BaseRecord>({ resource, ids }: GetManyParams): Promise<GetManyResult<TData>> {
101
- const params = ids.map(id => `id=${id}`).join('&');
102
- const data = await request<TData[]>(`${apiUrl}/${resource}?${params}`, resolveHeaders(opts));
180
+ const baseUrl = resolveResourceUrl(opts, resource);
181
+ const params = ids.map(id => `id=${encodeURIComponent(String(id))}`).join('&');
182
+ const data = await request<TData[]>(`${baseUrl}?${params}`, resolveHeaders(opts), undefined, withCredentials);
103
183
  return { data };
104
184
  },
105
185
 
106
186
  async createMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, variables }: CreateManyParams<TVariables>): Promise<CreateManyResult<TData>> {
107
- const results: TData[] = [];
108
- for (const vars of variables) {
109
- const data = await request<TData>(`${apiUrl}/${resource}`, resolveHeaders(opts), {
110
- method: 'POST',
111
- body: JSON.stringify(vars),
112
- });
113
- results.push(data);
114
- }
187
+ const baseUrl = resolveResourceUrl(opts, resource);
188
+ const results = await Promise.all(
189
+ variables.map(vars =>
190
+ request<TData>(baseUrl, resolveHeaders(opts), {
191
+ method: 'POST',
192
+ body: JSON.stringify(vars),
193
+ }, withCredentials)
194
+ )
195
+ );
115
196
  return { data: results };
116
197
  },
117
198
 
118
199
  async updateMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, ids, variables }: UpdateManyParams<TVariables>): Promise<UpdateManyResult<TData>> {
119
- const results: TData[] = [];
120
- for (const id of ids) {
121
- const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts), {
122
- method: 'PATCH',
123
- body: JSON.stringify(variables),
124
- });
125
- results.push(data);
126
- }
200
+ const baseUrl = resolveResourceUrl(opts, resource);
201
+ const results = await Promise.all(
202
+ ids.map(id =>
203
+ request<TData>(`${baseUrl}/${id}`, resolveHeaders(opts), {
204
+ method: updateMethod,
205
+ body: JSON.stringify(variables),
206
+ }, withCredentials)
207
+ )
208
+ );
127
209
  return { data: results };
128
210
  },
129
211
 
130
212
  async deleteMany<TData extends BaseRecord = BaseRecord, TVariables = unknown>({ resource, ids }: DeleteManyParams<TVariables>): Promise<DeleteManyResult<TData>> {
131
- const results: TData[] = [];
132
- for (const id of ids) {
133
- const data = await request<TData>(`${apiUrl}/${resource}/${id}`, resolveHeaders(opts), {
134
- method: 'DELETE',
135
- });
136
- results.push(data);
137
- }
213
+ const baseUrl = resolveResourceUrl(opts, resource);
214
+ const results = await Promise.all(
215
+ ids.map(id =>
216
+ request<TData>(`${baseUrl}/${id}`, resolveHeaders(opts), {
217
+ method: 'DELETE',
218
+ }, withCredentials)
219
+ )
220
+ );
138
221
  return { data: results };
139
222
  },
140
223
 
@@ -142,7 +225,7 @@ export function createElysiaDataProvider(opts: ElysiaDataProviderOptions): DataP
142
225
  const data = await request<TData>(url, { ...resolveHeaders(opts), ...headers }, {
143
226
  method: method.toUpperCase(),
144
227
  body: payload ? JSON.stringify(payload) : undefined,
145
- });
228
+ }, withCredentials);
146
229
  return { data };
147
230
  },
148
231
  };