@geekmidas/client 0.0.1

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.
Files changed (51) hide show
  1. package/README.md +518 -0
  2. package/dist/chunk-CUT6urMc.cjs +30 -0
  3. package/dist/fetcher-DLDD_7Sa.mjs +86 -0
  4. package/dist/fetcher-DLDD_7Sa.mjs.map +1 -0
  5. package/dist/fetcher-KdwHgdAl.cjs +98 -0
  6. package/dist/fetcher-KdwHgdAl.cjs.map +1 -0
  7. package/dist/fetcher.cjs +4 -0
  8. package/dist/fetcher.d.cts +18 -0
  9. package/dist/fetcher.d.mts +18 -0
  10. package/dist/fetcher.mjs +3 -0
  11. package/dist/openapi-hooks.cjs +44 -0
  12. package/dist/openapi-hooks.cjs.map +1 -0
  13. package/dist/openapi-hooks.d.cts +99 -0
  14. package/dist/openapi-hooks.d.mts +99 -0
  15. package/dist/openapi-hooks.mjs +43 -0
  16. package/dist/openapi-hooks.mjs.map +1 -0
  17. package/dist/openapi-types.d.cjs +0 -0
  18. package/dist/openapi-types.d.cts +443 -0
  19. package/dist/openapi-types.d.mts +443 -0
  20. package/dist/openapi.cjs +526 -0
  21. package/dist/openapi.cjs.map +1 -0
  22. package/dist/openapi.mjs +501 -0
  23. package/dist/openapi.mjs.map +1 -0
  24. package/dist/react-query.cjs +147 -0
  25. package/dist/react-query.cjs.map +1 -0
  26. package/dist/react-query.d.cts +77 -0
  27. package/dist/react-query.d.mts +77 -0
  28. package/dist/react-query.mjs +141 -0
  29. package/dist/react-query.mjs.map +1 -0
  30. package/dist/types-csACmD6U.d.cts +68 -0
  31. package/dist/types-tMp85Lt_.d.mts +68 -0
  32. package/dist/types.cjs +0 -0
  33. package/dist/types.d.cts +2 -0
  34. package/dist/types.d.mts +2 -0
  35. package/dist/types.mjs +0 -0
  36. package/package.json +59 -0
  37. package/src/__tests__/fetcher.spec.ts +409 -0
  38. package/src/__tests__/method-restrictions.spec.tsx +227 -0
  39. package/src/__tests__/openapi-hooks.spec.tsx +558 -0
  40. package/src/__tests__/react-query-infinite.spec.tsx +1003 -0
  41. package/src/__tests__/react-query-invalidation.spec.tsx +238 -0
  42. package/src/__tests__/react-query.spec.tsx +582 -0
  43. package/src/__tests__/setup.ts +281 -0
  44. package/src/__tests__/types.spec.ts +134 -0
  45. package/src/__tests__/url-parsing.spec.ts +196 -0
  46. package/src/fetcher.ts +173 -0
  47. package/src/openapi-hooks.ts +193 -0
  48. package/src/openapi-types.d.ts +440 -0
  49. package/src/openapi.json +595 -0
  50. package/src/react-query.ts +341 -0
  51. package/src/types.ts +149 -0
package/README.md ADDED
@@ -0,0 +1,518 @@
1
+ # @geekmidas/client
2
+
3
+ Type-safe client library for consuming HTTP APIs with full TypeScript support, React Query integration, and automatic code generation from OpenAPI specifications.
4
+
5
+ ## Features
6
+
7
+ - **Type-Safe Fetcher**: Fully typed HTTP client with automatic type inference
8
+ - **React Query Integration**: Pre-built hooks with TypeScript support
9
+ - **OpenAPI Code Generation**: Generate React Query hooks from OpenAPI specs
10
+ - **Infinite Queries**: Built-in support for pagination and infinite scroll
11
+ - **Automatic Validation**: Request/response validation with StandardSchema
12
+ - **Error Handling**: Type-safe error handling with HTTP status codes
13
+ - **Query Invalidation**: Type-safe cache invalidation
14
+ - **Method Restrictions**: Type-level enforcement of HTTP methods per endpoint
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm add @geekmidas/client
20
+ ```
21
+
22
+ For React Query integration:
23
+
24
+ ```bash
25
+ pnpm add @geekmidas/client @tanstack/react-query
26
+ ```
27
+
28
+ ## Package Exports
29
+
30
+ ```typescript
31
+ // Type-safe fetcher
32
+ import { createTypedFetcher } from '@geekmidas/client';
33
+
34
+ // React Query client
35
+ import { createTypedQueryClient } from '@geekmidas/client/react-query';
36
+
37
+ // OpenAPI hooks generation
38
+ import { generateReactQueryHooks } from '@geekmidas/client/openapi';
39
+
40
+ // Type utilities
41
+ import type { TypedFetcherOptions } from '@geekmidas/client/types';
42
+ ```
43
+
44
+ ## Quick Start
45
+
46
+ ### Type-Safe Fetcher
47
+
48
+ Create a typed fetcher for your API:
49
+
50
+ ```typescript
51
+ import { createTypedFetcher } from '@geekmidas/client';
52
+
53
+ // Define your API types
54
+ interface API {
55
+ 'GET /users': {
56
+ response: {
57
+ id: string;
58
+ name: string;
59
+ email: string;
60
+ }[];
61
+ };
62
+ 'POST /users': {
63
+ body: {
64
+ name: string;
65
+ email: string;
66
+ };
67
+ response: {
68
+ id: string;
69
+ name: string;
70
+ email: string;
71
+ };
72
+ };
73
+ 'GET /users/:id': {
74
+ params: {
75
+ id: string;
76
+ };
77
+ response: {
78
+ id: string;
79
+ name: string;
80
+ email: string;
81
+ };
82
+ };
83
+ }
84
+
85
+ // Create typed fetcher
86
+ const api = createTypedFetcher<API>({
87
+ baseUrl: 'https://api.example.com'
88
+ });
89
+
90
+ // Use with full type safety
91
+ const users = await api('GET /users');
92
+ // users is typed as Array<{ id: string; name: string; email: string }>
93
+
94
+ const user = await api('POST /users', {
95
+ body: { name: 'John Doe', email: 'john@example.com' }
96
+ });
97
+ // user is typed as { id: string; name: string; email: string }
98
+
99
+ const singleUser = await api('GET /users/:id', {
100
+ params: { id: '123' }
101
+ });
102
+ // singleUser is typed as { id: string; name: string; email: string }
103
+ ```
104
+
105
+ ### React Query Integration
106
+
107
+ Use with React Query for automatic caching and state management:
108
+
109
+ ```typescript
110
+ import { createTypedQueryClient } from '@geekmidas/client/react-query';
111
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
112
+
113
+ // Create query client
114
+ const queryClient = new QueryClient();
115
+ const api = createTypedQueryClient<API>({
116
+ baseUrl: 'https://api.example.com'
117
+ });
118
+
119
+ // In your component
120
+ function UsersList() {
121
+ const { data, isLoading, error } = api.useQuery('GET /users');
122
+
123
+ if (isLoading) return <div>Loading...</div>;
124
+ if (error) return <div>Error: {error.message}</div>;
125
+
126
+ return (
127
+ <ul>
128
+ {data.map(user => (
129
+ <li key={user.id}>{user.name}</li>
130
+ ))}
131
+ </ul>
132
+ );
133
+ }
134
+
135
+ // Mutations
136
+ function CreateUser() {
137
+ const createUser = api.useMutation('POST /users');
138
+
139
+ const handleSubmit = async (e: React.FormEvent) => {
140
+ e.preventDefault();
141
+ await createUser.mutateAsync({
142
+ body: { name: 'Jane Doe', email: 'jane@example.com' }
143
+ });
144
+ };
145
+
146
+ return (
147
+ <form onSubmit={handleSubmit}>
148
+ <button disabled={createUser.isPending}>
149
+ Create User
150
+ </button>
151
+ </form>
152
+ );
153
+ }
154
+ ```
155
+
156
+ ### Query Parameters
157
+
158
+ Handle query parameters with full type safety:
159
+
160
+ ```typescript
161
+ interface API {
162
+ 'GET /users/search': {
163
+ query: {
164
+ q: string;
165
+ limit?: number;
166
+ offset?: number;
167
+ };
168
+ response: {
169
+ users: Array<{ id: string; name: string }>;
170
+ total: number;
171
+ };
172
+ };
173
+ }
174
+
175
+ // Usage
176
+ const result = await api('GET /users/search', {
177
+ query: { q: 'john', limit: 10 }
178
+ });
179
+ ```
180
+
181
+ ### Infinite Queries
182
+
183
+ Implement infinite scroll pagination:
184
+
185
+ ```typescript
186
+ function InfiniteUsersList() {
187
+ const {
188
+ data,
189
+ fetchNextPage,
190
+ hasNextPage,
191
+ isFetchingNextPage
192
+ } = api.useInfiniteQuery(
193
+ 'GET /users',
194
+ {
195
+ query: { limit: 20 }
196
+ },
197
+ {
198
+ getNextPageParam: (lastPage, pages) => {
199
+ if (pages.length * 20 < lastPage.total) {
200
+ return { offset: pages.length * 20 };
201
+ }
202
+ return undefined;
203
+ }
204
+ }
205
+ );
206
+
207
+ return (
208
+ <div>
209
+ {data?.pages.map((page, i) => (
210
+ <div key={i}>
211
+ {page.users.map(user => (
212
+ <div key={user.id}>{user.name}</div>
213
+ ))}
214
+ </div>
215
+ ))}
216
+ {hasNextPage && (
217
+ <button
218
+ onClick={() => fetchNextPage()}
219
+ disabled={isFetchingNextPage}
220
+ >
221
+ Load More
222
+ </button>
223
+ )}
224
+ </div>
225
+ );
226
+ }
227
+ ```
228
+
229
+ ## OpenAPI Code Generation
230
+
231
+ Generate React Query hooks from OpenAPI specifications:
232
+
233
+ ```bash
234
+ # Using CLI
235
+ pnpm gkm generate:react-query --input api-docs.json --output ./src/api
236
+
237
+ # Programmatic usage
238
+ import { generateReactQueryHooks } from '@geekmidas/client/openapi';
239
+ import fs from 'fs/promises';
240
+
241
+ const spec = JSON.parse(await fs.readFile('api-docs.json', 'utf-8'));
242
+ const code = await generateReactQueryHooks(spec);
243
+ await fs.writeFile('./src/api/generated.ts', code);
244
+ ```
245
+
246
+ Generated hooks example:
247
+
248
+ ```typescript
249
+ // Generated from OpenAPI spec
250
+ export const api = createTypedQueryClient<{
251
+ 'GET /users': {
252
+ response: User[];
253
+ };
254
+ 'POST /users': {
255
+ body: CreateUserRequest;
256
+ response: User;
257
+ };
258
+ // ... all your endpoints
259
+ }>({
260
+ baseUrl: process.env.REACT_APP_API_URL
261
+ });
262
+
263
+ // Use generated hooks
264
+ function MyComponent() {
265
+ const { data: users } = api.useQuery('GET /users');
266
+ const createUser = api.useMutation('POST /users');
267
+
268
+ return (
269
+ // Your component
270
+ );
271
+ }
272
+ ```
273
+
274
+ ## Advanced Features
275
+
276
+ ### Query Invalidation
277
+
278
+ Type-safe cache invalidation:
279
+
280
+ ```typescript
281
+ // Invalidate specific query
282
+ await api.invalidateQueries('GET /users');
283
+
284
+ // Invalidate with params
285
+ await api.invalidateQueries('GET /users/:id', {
286
+ params: { id: '123' }
287
+ });
288
+
289
+ // Invalidate multiple queries
290
+ await Promise.all([
291
+ api.invalidateQueries('GET /users'),
292
+ api.invalidateQueries('GET /users/:id')
293
+ ]);
294
+ ```
295
+
296
+ ### Optimistic Updates
297
+
298
+ Implement optimistic UI updates:
299
+
300
+ ```typescript
301
+ const updateUser = api.useMutation('PUT /users/:id', {
302
+ onMutate: async (variables) => {
303
+ // Cancel outgoing refetches
304
+ await api.cancelQueries('GET /users/:id', {
305
+ params: { id: variables.params.id }
306
+ });
307
+
308
+ // Snapshot previous value
309
+ const previousUser = api.getQueryData('GET /users/:id', {
310
+ params: { id: variables.params.id }
311
+ });
312
+
313
+ // Optimistically update
314
+ api.setQueryData('GET /users/:id', {
315
+ params: { id: variables.params.id }
316
+ }, variables.body);
317
+
318
+ return { previousUser };
319
+ },
320
+ onError: (err, variables, context) => {
321
+ // Rollback on error
322
+ if (context?.previousUser) {
323
+ api.setQueryData('GET /users/:id', {
324
+ params: { id: variables.params.id }
325
+ }, context.previousUser);
326
+ }
327
+ },
328
+ onSettled: (data, error, variables) => {
329
+ // Refetch after mutation
330
+ api.invalidateQueries('GET /users/:id', {
331
+ params: { id: variables.params.id }
332
+ });
333
+ }
334
+ });
335
+ ```
336
+
337
+ ### Custom Headers
338
+
339
+ Add custom headers to requests:
340
+
341
+ ```typescript
342
+ const api = createTypedFetcher<API>({
343
+ baseUrl: 'https://api.example.com',
344
+ headers: {
345
+ 'Authorization': `Bearer ${token}`,
346
+ 'X-Api-Key': apiKey
347
+ }
348
+ });
349
+
350
+ // Per-request headers
351
+ const user = await api('GET /users/:id', {
352
+ params: { id: '123' },
353
+ headers: {
354
+ 'X-Request-ID': requestId
355
+ }
356
+ });
357
+ ```
358
+
359
+ ### Error Handling
360
+
361
+ Handle errors with full type safety:
362
+
363
+ ```typescript
364
+ import { HttpError } from '@geekmidas/errors';
365
+
366
+ try {
367
+ const user = await api('GET /users/:id', {
368
+ params: { id: '123' }
369
+ });
370
+ } catch (error) {
371
+ if (error instanceof HttpError) {
372
+ if (error.statusCode === 404) {
373
+ console.log('User not found');
374
+ } else if (error.statusCode === 403) {
375
+ console.log('Access denied');
376
+ } else {
377
+ console.error('API error:', error.message);
378
+ }
379
+ } else {
380
+ console.error('Unexpected error:', error);
381
+ }
382
+ }
383
+ ```
384
+
385
+ ### Request/Response Interceptors
386
+
387
+ Add interceptors for logging, auth, etc:
388
+
389
+ ```typescript
390
+ const api = createTypedFetcher<API>({
391
+ baseUrl: 'https://api.example.com',
392
+ beforeRequest: async (url, options) => {
393
+ // Add auth token
394
+ const token = await getAuthToken();
395
+ options.headers = {
396
+ ...options.headers,
397
+ 'Authorization': `Bearer ${token}`
398
+ };
399
+ return { url, options };
400
+ },
401
+ afterResponse: async (response) => {
402
+ // Log response
403
+ console.log(`${response.status} ${response.url}`);
404
+ return response;
405
+ },
406
+ onError: async (error) => {
407
+ // Handle auth errors
408
+ if (error instanceof HttpError && error.statusCode === 401) {
409
+ await refreshAuthToken();
410
+ // Retry request
411
+ }
412
+ throw error;
413
+ }
414
+ });
415
+ ```
416
+
417
+ ### Prefetching
418
+
419
+ Prefetch queries for better UX:
420
+
421
+ ```typescript
422
+ function UsersList() {
423
+ const { data: users } = api.useQuery('GET /users');
424
+
425
+ const handleUserHover = (userId: string) => {
426
+ // Prefetch user details on hover
427
+ api.prefetchQuery('GET /users/:id', {
428
+ params: { id: userId }
429
+ });
430
+ };
431
+
432
+ return (
433
+ <ul>
434
+ {users?.map(user => (
435
+ <li
436
+ key={user.id}
437
+ onMouseEnter={() => handleUserHover(user.id)}
438
+ >
439
+ {user.name}
440
+ </li>
441
+ ))}
442
+ </ul>
443
+ );
444
+ }
445
+ ```
446
+
447
+ ## Type Utilities
448
+
449
+ ### Infer API Types
450
+
451
+ ```typescript
452
+ import type { InferAPIResponse, InferAPIRequest } from '@geekmidas/client/types';
453
+
454
+ type UsersResponse = InferAPIResponse<API, 'GET /users'>;
455
+ // type UsersResponse = Array<{ id: string; name: string; email: string }>
456
+
457
+ type CreateUserRequest = InferAPIRequest<API, 'POST /users'>;
458
+ // type CreateUserRequest = { body: { name: string; email: string } }
459
+ ```
460
+
461
+ ### Method Restrictions
462
+
463
+ Enforce correct HTTP methods at type level:
464
+
465
+ ```typescript
466
+ // ✅ Correct - POST endpoint with body
467
+ await api('POST /users', {
468
+ body: { name: 'John' }
469
+ });
470
+
471
+ // ❌ Type error - GET endpoint can't have body
472
+ await api('GET /users', {
473
+ body: { name: 'John' } // Type error!
474
+ });
475
+
476
+ // ❌ Type error - Wrong method
477
+ await api('DELETE /users', {}); // Type error if endpoint not defined!
478
+ ```
479
+
480
+ ## Testing
481
+
482
+ Mock API calls in tests:
483
+
484
+ ```typescript
485
+ import { createTypedFetcher } from '@geekmidas/client';
486
+ import { vi } from 'vitest';
487
+
488
+ const mockFetch = vi.fn();
489
+ global.fetch = mockFetch;
490
+
491
+ const api = createTypedFetcher<API>({
492
+ baseUrl: 'https://api.example.com'
493
+ });
494
+
495
+ // Mock response
496
+ mockFetch.mockResolvedValueOnce({
497
+ ok: true,
498
+ json: async () => [{ id: '1', name: 'John', email: 'john@example.com' }]
499
+ });
500
+
501
+ const users = await api('GET /users');
502
+ expect(users).toHaveLength(1);
503
+ expect(mockFetch).toHaveBeenCalledWith(
504
+ 'https://api.example.com/users',
505
+ expect.objectContaining({ method: 'GET' })
506
+ );
507
+ ```
508
+
509
+ ## Related Packages
510
+
511
+ - [@geekmidas/constructs](../constructs) - Build type-safe endpoints that this client consumes
512
+ - [@geekmidas/errors](../errors) - HTTP error classes for error handling
513
+ - [@geekmidas/cli](../cli) - Generate OpenAPI specs and React Query hooks
514
+ - [@tanstack/react-query](https://tanstack.com/query) - React Query library
515
+
516
+ ## License
517
+
518
+ MIT
@@ -0,0 +1,30 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+
25
+ Object.defineProperty(exports, '__toESM', {
26
+ enumerable: true,
27
+ get: function () {
28
+ return __toESM;
29
+ }
30
+ });
@@ -0,0 +1,86 @@
1
+ //#region src/fetcher.ts
2
+ var TypedFetcher = class TypedFetcher {
3
+ baseURL;
4
+ defaultHeaders;
5
+ options;
6
+ fetchFn;
7
+ static getFetchFn(fn) {
8
+ if (fn) return fn;
9
+ if (typeof window !== "undefined" && typeof window.fetch === "function") return window.fetch.bind(window);
10
+ if (typeof globalThis !== "undefined" && typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
11
+ throw new Error("No fetch implementation found");
12
+ }
13
+ constructor(options = {}) {
14
+ this.baseURL = options.baseURL || "";
15
+ this.defaultHeaders = options.headers || {};
16
+ this.options = options;
17
+ this.fetchFn = TypedFetcher.getFetchFn(options.fetch);
18
+ }
19
+ async request(endpoint, config) {
20
+ const { method, route } = this.parseEndpoint(endpoint);
21
+ let url = route;
22
+ if (config && "params" in config && config.params) Object.entries(config.params).forEach(([key, value]) => {
23
+ url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
24
+ });
25
+ if (config && "query" in config && config.query) {
26
+ const queryParams = new URLSearchParams();
27
+ const appendQueryParam = (prefix, value) => {
28
+ if (value === void 0 || value === null) return;
29
+ if (Array.isArray(value)) value.forEach((item) => {
30
+ queryParams.append(prefix, String(item));
31
+ });
32
+ else if (typeof value === "object") Object.entries(value).forEach(([subKey, subValue]) => {
33
+ appendQueryParam(`${prefix}.${subKey}`, subValue);
34
+ });
35
+ else queryParams.append(prefix, String(value));
36
+ };
37
+ Object.entries(config.query).forEach(([key, value]) => {
38
+ appendQueryParam(key, value);
39
+ });
40
+ const queryString = queryParams.toString();
41
+ if (queryString) url += `?${queryString}`;
42
+ }
43
+ let requestConfig = {
44
+ method: method.toUpperCase(),
45
+ headers: {
46
+ ...this.defaultHeaders,
47
+ ...config && "headers" in config && config.headers || {}
48
+ }
49
+ };
50
+ if (config && "body" in config && config.body) {
51
+ requestConfig.body = JSON.stringify(config.body);
52
+ requestConfig.headers = {
53
+ ...requestConfig.headers,
54
+ "Content-Type": "application/json"
55
+ };
56
+ }
57
+ if (this.options.onRequest) requestConfig = await this.options.onRequest(requestConfig);
58
+ try {
59
+ let response = await this.fetchFn(`${this.baseURL}${url}`, requestConfig);
60
+ if (this.options.onResponse) response = await this.options.onResponse(response);
61
+ if (!response.ok) throw response;
62
+ if (response.status === 204 || response.headers.get("content-length") === "0") return void 0;
63
+ const data = await response.json();
64
+ return data;
65
+ } catch (error) {
66
+ if (this.options.onError) await this.options.onError(error);
67
+ throw error;
68
+ }
69
+ }
70
+ parseEndpoint(endpoint) {
71
+ const [method, ...routeParts] = endpoint.split(" ");
72
+ const route = routeParts.join(" ");
73
+ return {
74
+ method: method.toLowerCase(),
75
+ route
76
+ };
77
+ }
78
+ };
79
+ function createTypedFetcher(options) {
80
+ const fetcher = new TypedFetcher(options);
81
+ return (endpoint, config) => fetcher.request(endpoint, config);
82
+ }
83
+
84
+ //#endregion
85
+ export { TypedFetcher, createTypedFetcher };
86
+ //# sourceMappingURL=fetcher-DLDD_7Sa.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetcher-DLDD_7Sa.mjs","names":["fn?: FetchFn","options: FetcherOptions","endpoint: T","config?: FilteredRequestConfig<Paths, T>","prefix: string","value: unknown","requestConfig: RequestInit","options?: FetcherOptions"],"sources":["../src/fetcher.ts"],"sourcesContent":["import type {\n EndpointString,\n ExtractEndpointResponse,\n FetcherOptions,\n FilteredRequestConfig,\n ParseEndpoint,\n TypedEndpoint,\n} from './types';\n\nexport class TypedFetcher<Paths> {\n private baseURL: string;\n private defaultHeaders: Record<string, string>;\n private options: FetcherOptions;\n private fetchFn: FetchFn;\n\n static getFetchFn(fn?: FetchFn): FetchFn {\n if (fn) {\n return fn;\n }\n\n if (typeof window !== 'undefined' && typeof window.fetch === 'function') {\n return window.fetch.bind(window);\n }\n\n if (\n typeof globalThis !== 'undefined' &&\n typeof globalThis.fetch === 'function'\n ) {\n return globalThis.fetch.bind(globalThis);\n }\n\n throw new Error('No fetch implementation found');\n }\n\n constructor(options: FetcherOptions = {}) {\n this.baseURL = options.baseURL || '';\n this.defaultHeaders = options.headers || {};\n this.options = options;\n this.fetchFn = TypedFetcher.getFetchFn(options.fetch);\n }\n\n async request<T extends TypedEndpoint<Paths>>(\n endpoint: T,\n config?: FilteredRequestConfig<Paths, T>,\n ): Promise<ExtractEndpointResponse<Paths, T>> {\n const { method, route } = this.parseEndpoint(endpoint);\n\n // Replace path parameters\n let url = route;\n if (config && 'params' in config && config.params) {\n Object.entries(config.params as Record<string, unknown>).forEach(\n ([key, value]) => {\n url = url.replace(`{${key}}`, encodeURIComponent(String(value)));\n },\n );\n }\n\n // Add query parameters\n if (config && 'query' in config && config.query) {\n const queryParams = new URLSearchParams();\n\n // Recursive function to handle nested objects and arrays\n const appendQueryParam = (prefix: string, value: unknown) => {\n if (value === undefined || value === null) {\n return;\n }\n\n if (Array.isArray(value)) {\n // Handle arrays by appending multiple values with the same key\n value.forEach((item) => {\n queryParams.append(prefix, String(item));\n });\n } else if (typeof value === 'object') {\n // For objects, recursively flatten into dot notation\n Object.entries(value as Record<string, unknown>).forEach(\n ([subKey, subValue]) => {\n appendQueryParam(`${prefix}.${subKey}`, subValue);\n },\n );\n } else {\n queryParams.append(prefix, String(value));\n }\n };\n\n // Process all query parameters\n Object.entries(config.query as Record<string, unknown>).forEach(\n ([key, value]) => {\n appendQueryParam(key, value);\n },\n );\n\n const queryString = queryParams.toString();\n if (queryString) {\n url += `?${queryString}`;\n }\n }\n\n // Build request configuration\n let requestConfig: RequestInit = {\n method: method.toUpperCase(),\n headers: {\n ...this.defaultHeaders,\n ...((config && 'headers' in config && config.headers) || {}),\n },\n };\n\n // Add body if present\n if (config && 'body' in config && config.body) {\n requestConfig.body = JSON.stringify(config.body);\n requestConfig.headers = {\n ...requestConfig.headers,\n 'Content-Type': 'application/json',\n };\n }\n\n // Apply request interceptor\n if (this.options.onRequest) {\n requestConfig = await this.options.onRequest(requestConfig);\n }\n\n try {\n // Make the request\n let response = await this.fetchFn(`${this.baseURL}${url}`, requestConfig);\n\n // Apply response interceptor\n if (this.options.onResponse) {\n response = await this.options.onResponse(response);\n }\n\n // Handle errors\n if (!response.ok) {\n throw response;\n }\n\n // Handle empty responses (204 No Content, etc.)\n if (\n response.status === 204 ||\n response.headers.get('content-length') === '0'\n ) {\n return undefined as ExtractEndpointResponse<Paths, T>;\n }\n\n // Parse JSON response\n const data = await response.json();\n return data as ExtractEndpointResponse<Paths, T>;\n } catch (error) {\n // Apply error handler\n if (this.options.onError) {\n // @ts-ignore\n await this.options.onError(error);\n }\n throw error;\n }\n }\n\n private parseEndpoint<T extends EndpointString>(\n endpoint: T,\n ): ParseEndpoint<T> {\n const [method, ...routeParts] = endpoint.split(' ');\n const route = routeParts.join(' ');\n return { method: method.toLowerCase(), route } as ParseEndpoint<T>;\n }\n}\n\nexport function createTypedFetcher<Paths>(options?: FetcherOptions) {\n const fetcher = new TypedFetcher<Paths>(options);\n return <T extends TypedEndpoint<Paths>>(\n endpoint: T,\n config?: FilteredRequestConfig<Paths, T>,\n ) => fetcher.request(endpoint, config);\n}\n\nexport type FetchFn = typeof fetch;\n"],"mappings":";AASA,IAAa,eAAb,MAAa,aAAoB;CAC/B,AAAQ;CACR,AAAQ;CACR,AAAQ;CACR,AAAQ;CAER,OAAO,WAAWA,IAAuB;AACvC,MAAI,GACF,QAAO;AAGT,aAAW,WAAW,sBAAsB,OAAO,UAAU,WAC3D,QAAO,OAAO,MAAM,KAAK,OAAO;AAGlC,aACS,eAAe,sBACf,WAAW,UAAU,WAE5B,QAAO,WAAW,MAAM,KAAK,WAAW;AAG1C,QAAM,IAAI,MAAM;CACjB;CAED,YAAYC,UAA0B,CAAE,GAAE;AACxC,OAAK,UAAU,QAAQ,WAAW;AAClC,OAAK,iBAAiB,QAAQ,WAAW,CAAE;AAC3C,OAAK,UAAU;AACf,OAAK,UAAU,aAAa,WAAW,QAAQ,MAAM;CACtD;CAED,MAAM,QACJC,UACAC,QAC4C;EAC5C,MAAM,EAAE,QAAQ,OAAO,GAAG,KAAK,cAAc,SAAS;EAGtD,IAAI,MAAM;AACV,MAAI,UAAU,YAAY,UAAU,OAAO,OACzC,QAAO,QAAQ,OAAO,OAAkC,CAAC,QACvD,CAAC,CAAC,KAAK,MAAM,KAAK;AAChB,SAAM,IAAI,SAAS,GAAG,IAAI,IAAI,mBAAmB,OAAO,MAAM,CAAC,CAAC;EACjE,EACF;AAIH,MAAI,UAAU,WAAW,UAAU,OAAO,OAAO;GAC/C,MAAM,cAAc,IAAI;GAGxB,MAAM,mBAAmB,CAACC,QAAgBC,UAAmB;AAC3D,QAAI,oBAAuB,UAAU,KACnC;AAGF,QAAI,MAAM,QAAQ,MAAM,CAEtB,OAAM,QAAQ,CAAC,SAAS;AACtB,iBAAY,OAAO,QAAQ,OAAO,KAAK,CAAC;IACzC,EAAC;oBACc,UAAU,SAE1B,QAAO,QAAQ,MAAiC,CAAC,QAC/C,CAAC,CAAC,QAAQ,SAAS,KAAK;AACtB,uBAAkB,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS;IAClD,EACF;QAED,aAAY,OAAO,QAAQ,OAAO,MAAM,CAAC;GAE5C;AAGD,UAAO,QAAQ,OAAO,MAAiC,CAAC,QACtD,CAAC,CAAC,KAAK,MAAM,KAAK;AAChB,qBAAiB,KAAK,MAAM;GAC7B,EACF;GAED,MAAM,cAAc,YAAY,UAAU;AAC1C,OAAI,YACF,SAAQ,GAAG,YAAY;EAE1B;EAGD,IAAIC,gBAA6B;GAC/B,QAAQ,OAAO,aAAa;GAC5B,SAAS;IACP,GAAG,KAAK;IACR,GAAK,UAAU,aAAa,UAAU,OAAO,WAAY,CAAE;GAC5D;EACF;AAGD,MAAI,UAAU,UAAU,UAAU,OAAO,MAAM;AAC7C,iBAAc,OAAO,KAAK,UAAU,OAAO,KAAK;AAChD,iBAAc,UAAU;IACtB,GAAG,cAAc;IACjB,gBAAgB;GACjB;EACF;AAGD,MAAI,KAAK,QAAQ,UACf,iBAAgB,MAAM,KAAK,QAAQ,UAAU,cAAc;AAG7D,MAAI;GAEF,IAAI,WAAW,MAAM,KAAK,SAAS,EAAE,KAAK,QAAQ,EAAE,IAAI,GAAG,cAAc;AAGzE,OAAI,KAAK,QAAQ,WACf,YAAW,MAAM,KAAK,QAAQ,WAAW,SAAS;AAIpD,QAAK,SAAS,GACZ,OAAM;AAIR,OACE,SAAS,WAAW,OACpB,SAAS,QAAQ,IAAI,iBAAiB,KAAK,IAE3C;GAIF,MAAM,OAAO,MAAM,SAAS,MAAM;AAClC,UAAO;EACR,SAAQ,OAAO;AAEd,OAAI,KAAK,QAAQ,QAEf,OAAM,KAAK,QAAQ,QAAQ,MAAM;AAEnC,SAAM;EACP;CACF;CAED,AAAQ,cACNJ,UACkB;EAClB,MAAM,CAAC,QAAQ,GAAG,WAAW,GAAG,SAAS,MAAM,IAAI;EACnD,MAAM,QAAQ,WAAW,KAAK,IAAI;AAClC,SAAO;GAAE,QAAQ,OAAO,aAAa;GAAE;EAAO;CAC/C;AACF;AAED,SAAgB,mBAA0BK,SAA0B;CAClE,MAAM,UAAU,IAAI,aAAoB;AACxC,QAAO,CACLL,UACAC,WACG,QAAQ,QAAQ,UAAU,OAAO;AACvC"}