@geekmidas/client 0.2.0 → 0.4.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.
Files changed (57) hide show
  1. package/dist/auth-fetcher.cjs +2 -2
  2. package/dist/auth-fetcher.cjs.map +1 -1
  3. package/dist/auth-fetcher.d.cts +2 -1
  4. package/dist/auth-fetcher.d.cts.map +1 -0
  5. package/dist/auth-fetcher.d.mts +2 -1
  6. package/dist/auth-fetcher.d.mts.map +1 -0
  7. package/dist/auth-fetcher.mjs +2 -2
  8. package/dist/auth-fetcher.mjs.map +1 -1
  9. package/dist/endpoint-hooks.cjs.map +1 -1
  10. package/dist/endpoint-hooks.d.cts +1 -1
  11. package/dist/endpoint-hooks.d.cts.map +1 -0
  12. package/dist/endpoint-hooks.d.mts +1 -1
  13. package/dist/endpoint-hooks.d.mts.map +1 -0
  14. package/dist/endpoint-hooks.mjs.map +1 -1
  15. package/dist/{fetcher-DLDD_7Sa.mjs → fetcher-5fnBE7Dk.mjs} +2 -2
  16. package/dist/{fetcher-DLDD_7Sa.mjs.map → fetcher-5fnBE7Dk.mjs.map} +1 -1
  17. package/dist/{fetcher-KdwHgdAl.cjs → fetcher-CgLEaIAC.cjs} +2 -2
  18. package/dist/{fetcher-KdwHgdAl.cjs.map → fetcher-CgLEaIAC.cjs.map} +1 -1
  19. package/dist/fetcher.cjs +1 -1
  20. package/dist/fetcher.d.cts +3 -1
  21. package/dist/fetcher.d.cts.map +1 -0
  22. package/dist/fetcher.d.mts +3 -1
  23. package/dist/fetcher.d.mts.map +1 -0
  24. package/dist/fetcher.mjs +1 -1
  25. package/dist/infer.d.cts.map +1 -0
  26. package/dist/infer.d.mts.map +1 -0
  27. package/dist/openapi-hooks.cjs +3 -5
  28. package/dist/openapi-hooks.cjs.map +1 -1
  29. package/dist/openapi-hooks.d.cts +2 -2
  30. package/dist/openapi-hooks.d.cts.map +1 -0
  31. package/dist/openapi-hooks.d.mts +2 -2
  32. package/dist/openapi-hooks.d.mts.map +1 -0
  33. package/dist/openapi-hooks.mjs +3 -5
  34. package/dist/openapi-hooks.mjs.map +1 -1
  35. package/dist/openapi-types.d.cts.map +1 -0
  36. package/dist/openapi-types.d.mts.map +1 -0
  37. package/dist/react-query.cjs +1 -1
  38. package/dist/react-query.d.cts +3 -1
  39. package/dist/react-query.d.cts.map +1 -0
  40. package/dist/react-query.d.mts +3 -1
  41. package/dist/react-query.d.mts.map +1 -0
  42. package/dist/react-query.mjs +1 -1
  43. package/dist/{types-D4OSWveN.d.cts → types-FxummKaL.d.mts} +7 -5
  44. package/dist/types-FxummKaL.d.mts.map +1 -0
  45. package/dist/{types-Cdv1XAWr.d.mts → types-vutATyWv.d.cts} +7 -5
  46. package/dist/types-vutATyWv.d.cts.map +1 -0
  47. package/dist/types.d.cts +1 -1
  48. package/dist/types.d.mts +1 -1
  49. package/package.json +12 -6
  50. package/src/__tests__/auth-fetcher.spec.ts +505 -0
  51. package/src/__tests__/openapi-hooks.spec.tsx +7 -8
  52. package/src/auth-fetcher.ts +1 -1
  53. package/src/endpoint-hooks.ts +14 -8
  54. package/src/fetcher.ts +1 -1
  55. package/src/openapi-hooks.ts +3 -3
  56. package/src/types.ts +6 -4
  57. package/tsconfig.json +12 -0
@@ -0,0 +1,505 @@
1
+ import { http, HttpResponse } from 'msw';
2
+ import { setupServer } from 'msw/node';
3
+ import {
4
+ afterAll,
5
+ afterEach,
6
+ beforeAll,
7
+ describe,
8
+ expect,
9
+ it,
10
+ vi,
11
+ } from 'vitest';
12
+ import {
13
+ type ApiKeyProvider,
14
+ type AwsSigner,
15
+ type SecuritySchemeObject,
16
+ type TokenProvider,
17
+ createAuthAwareFetcher,
18
+ } from '../auth-fetcher';
19
+
20
+ // Test types - structured to match OpenAPI spec format
21
+ interface TestPaths {
22
+ '/users': {
23
+ get: {
24
+ responses: {
25
+ 200: {
26
+ content: {
27
+ 'application/json': { users: Array<{ id: string; name: string }> };
28
+ };
29
+ };
30
+ };
31
+ };
32
+ };
33
+ '/protected': {
34
+ get: {
35
+ responses: {
36
+ 200: {
37
+ content: {
38
+ 'application/json': { data: string };
39
+ };
40
+ };
41
+ };
42
+ };
43
+ };
44
+ '/api-key-protected': {
45
+ get: {
46
+ responses: {
47
+ 200: {
48
+ content: {
49
+ 'application/json': { data: string };
50
+ };
51
+ };
52
+ };
53
+ };
54
+ };
55
+ '/iam-protected': {
56
+ get: {
57
+ responses: {
58
+ 200: {
59
+ content: {
60
+ 'application/json': { data: string };
61
+ };
62
+ };
63
+ };
64
+ };
65
+ };
66
+ '/data': {
67
+ post: {
68
+ requestBody: {
69
+ content: {
70
+ 'application/json': { value: string };
71
+ };
72
+ };
73
+ responses: {
74
+ 201: {
75
+ content: {
76
+ 'application/json': { id: string };
77
+ };
78
+ };
79
+ };
80
+ };
81
+ };
82
+ }
83
+
84
+ // Track headers received by handlers
85
+ let lastReceivedHeaders: Record<string, string> = {};
86
+
87
+ const handlers = [
88
+ http.get('https://api.example.com/users', ({ request }) => {
89
+ lastReceivedHeaders = Object.fromEntries(request.headers.entries());
90
+ return HttpResponse.json({
91
+ users: [{ id: '1', name: 'John' }],
92
+ });
93
+ }),
94
+
95
+ http.get('https://api.example.com/protected', ({ request }) => {
96
+ lastReceivedHeaders = Object.fromEntries(request.headers.entries());
97
+ const auth = request.headers.get('Authorization');
98
+ if (!auth || auth !== 'Bearer valid-token') {
99
+ return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 });
100
+ }
101
+ return HttpResponse.json({ data: 'secret' });
102
+ }),
103
+
104
+ http.get('https://api.example.com/api-key-protected', ({ request }) => {
105
+ lastReceivedHeaders = Object.fromEntries(request.headers.entries());
106
+ const apiKey = request.headers.get('X-API-Key');
107
+ if (!apiKey || apiKey !== 'my-api-key') {
108
+ return HttpResponse.json({ message: 'Unauthorized' }, { status: 401 });
109
+ }
110
+ return HttpResponse.json({ data: 'api-key-data' });
111
+ }),
112
+
113
+ http.get('https://api.example.com/iam-protected', ({ request }) => {
114
+ lastReceivedHeaders = Object.fromEntries(request.headers.entries());
115
+ return HttpResponse.json({ data: 'iam-data' });
116
+ }),
117
+
118
+ http.post('https://api.example.com/data', async ({ request }) => {
119
+ lastReceivedHeaders = Object.fromEntries(request.headers.entries());
120
+ return HttpResponse.json({ id: '123' }, { status: 201 });
121
+ }),
122
+ ];
123
+
124
+ const server = setupServer(...handlers);
125
+
126
+ beforeAll(() => {
127
+ server.listen({ onUnhandledRequest: 'error' });
128
+ });
129
+
130
+ afterEach(() => {
131
+ server.resetHandlers();
132
+ lastReceivedHeaders = {};
133
+ vi.clearAllMocks();
134
+ });
135
+
136
+ afterAll(() => {
137
+ server.close();
138
+ });
139
+
140
+ describe('createAuthAwareFetcher', () => {
141
+ const securitySchemes: Record<string, SecuritySchemeObject> = {
142
+ bearer: {
143
+ type: 'http',
144
+ scheme: 'bearer',
145
+ bearerFormat: 'JWT',
146
+ },
147
+ apiKey: {
148
+ type: 'apiKey',
149
+ in: 'header',
150
+ name: 'X-API-Key',
151
+ },
152
+ iam: {
153
+ type: 'http',
154
+ scheme: 'aws4-hmac-sha256',
155
+ },
156
+ };
157
+
158
+ describe('bearer auth', () => {
159
+ it('should add Authorization header for bearer auth endpoints', async () => {
160
+ const tokenProvider: TokenProvider = {
161
+ getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
162
+ createValidAuthHeaders: vi.fn().mockResolvedValue({
163
+ Authorization: 'Bearer valid-token',
164
+ }),
165
+ };
166
+
167
+ const endpointAuth = {
168
+ 'GET /users': null,
169
+ 'GET /protected': 'bearer',
170
+ } as const;
171
+
172
+ const api = createAuthAwareFetcher<TestPaths>({
173
+ baseURL: 'https://api.example.com',
174
+ endpointAuth,
175
+ securitySchemes,
176
+ authStrategies: {
177
+ bearer: { type: 'bearer', tokenProvider },
178
+ },
179
+ });
180
+
181
+ const result = await api('GET /protected');
182
+
183
+ expect(tokenProvider.createValidAuthHeaders).toHaveBeenCalled();
184
+ expect(result).toEqual({ data: 'secret' });
185
+ expect(lastReceivedHeaders.authorization).toBe('Bearer valid-token');
186
+ });
187
+
188
+ it('should not add auth headers for public endpoints', async () => {
189
+ const tokenProvider: TokenProvider = {
190
+ getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
191
+ createValidAuthHeaders: vi.fn().mockResolvedValue({
192
+ Authorization: 'Bearer valid-token',
193
+ }),
194
+ };
195
+
196
+ const endpointAuth = {
197
+ 'GET /users': null,
198
+ 'GET /protected': 'bearer',
199
+ } as const;
200
+
201
+ const api = createAuthAwareFetcher<TestPaths>({
202
+ baseURL: 'https://api.example.com',
203
+ endpointAuth,
204
+ securitySchemes,
205
+ authStrategies: {
206
+ bearer: { type: 'bearer', tokenProvider },
207
+ },
208
+ });
209
+
210
+ const result = await api('GET /users');
211
+
212
+ expect(tokenProvider.createValidAuthHeaders).not.toHaveBeenCalled();
213
+ expect(result).toEqual({ users: [{ id: '1', name: 'John' }] });
214
+ expect(lastReceivedHeaders.authorization).toBeUndefined();
215
+ });
216
+ });
217
+
218
+ describe('apiKey auth', () => {
219
+ it('should add API key header for apiKey auth endpoints', async () => {
220
+ const apiKeyProvider: ApiKeyProvider = {
221
+ getApiKey: vi.fn().mockResolvedValue('my-api-key'),
222
+ };
223
+
224
+ const endpointAuth = {
225
+ 'GET /api-key-protected': 'apiKey',
226
+ } as const;
227
+
228
+ const api = createAuthAwareFetcher<TestPaths>({
229
+ baseURL: 'https://api.example.com',
230
+ endpointAuth,
231
+ securitySchemes,
232
+ authStrategies: {
233
+ apiKey: { type: 'apiKey', apiKeyProvider },
234
+ },
235
+ });
236
+
237
+ const result = await api('GET /api-key-protected');
238
+
239
+ expect(apiKeyProvider.getApiKey).toHaveBeenCalled();
240
+ expect(result).toEqual({ data: 'api-key-data' });
241
+ expect(lastReceivedHeaders['x-api-key']).toBe('my-api-key');
242
+ });
243
+
244
+ it('should use custom header name if provided', async () => {
245
+ const apiKeyProvider: ApiKeyProvider = {
246
+ getApiKey: vi.fn().mockResolvedValue('custom-key'),
247
+ };
248
+
249
+ const customSchemes: Record<string, SecuritySchemeObject> = {
250
+ customApiKey: {
251
+ type: 'apiKey',
252
+ in: 'header',
253
+ name: 'X-Custom-Key',
254
+ },
255
+ };
256
+
257
+ const endpointAuth = {
258
+ 'GET /api-key-protected': 'customApiKey',
259
+ } as const;
260
+
261
+ server.use(
262
+ http.get('https://api.example.com/api-key-protected', ({ request }) => {
263
+ lastReceivedHeaders = Object.fromEntries(request.headers.entries());
264
+ return HttpResponse.json({ data: 'custom-key-data' });
265
+ }),
266
+ );
267
+
268
+ const api = createAuthAwareFetcher<TestPaths>({
269
+ baseURL: 'https://api.example.com',
270
+ endpointAuth,
271
+ securitySchemes: customSchemes,
272
+ authStrategies: {
273
+ customApiKey: {
274
+ type: 'apiKey',
275
+ apiKeyProvider,
276
+ headerName: 'X-Custom-Key',
277
+ },
278
+ },
279
+ });
280
+
281
+ await api('GET /api-key-protected');
282
+
283
+ expect(lastReceivedHeaders['x-custom-key']).toBe('custom-key');
284
+ });
285
+
286
+ it('should handle synchronous getApiKey', async () => {
287
+ const apiKeyProvider: ApiKeyProvider = {
288
+ getApiKey: vi.fn().mockReturnValue('sync-api-key'),
289
+ };
290
+
291
+ server.use(
292
+ http.get('https://api.example.com/api-key-protected', ({ request }) => {
293
+ lastReceivedHeaders = Object.fromEntries(request.headers.entries());
294
+ return HttpResponse.json({ data: 'sync-data' });
295
+ }),
296
+ );
297
+
298
+ const endpointAuth = {
299
+ 'GET /api-key-protected': 'apiKey',
300
+ } as const;
301
+
302
+ const api = createAuthAwareFetcher<TestPaths>({
303
+ baseURL: 'https://api.example.com',
304
+ endpointAuth,
305
+ securitySchemes,
306
+ authStrategies: {
307
+ apiKey: { type: 'apiKey', apiKeyProvider },
308
+ },
309
+ });
310
+
311
+ await api('GET /api-key-protected');
312
+
313
+ expect(lastReceivedHeaders['x-api-key']).toBe('sync-api-key');
314
+ });
315
+ });
316
+
317
+ describe('IAM auth', () => {
318
+ it('should call signer for IAM auth endpoints', async () => {
319
+ const signer: AwsSigner = {
320
+ sign: vi.fn().mockResolvedValue({}),
321
+ };
322
+
323
+ const endpointAuth = {
324
+ 'GET /iam-protected': 'iam',
325
+ } as const;
326
+
327
+ const api = createAuthAwareFetcher<TestPaths>({
328
+ baseURL: 'https://api.example.com',
329
+ endpointAuth,
330
+ securitySchemes,
331
+ authStrategies: {
332
+ iam: { type: 'iam', signer },
333
+ },
334
+ });
335
+
336
+ const result = await api('GET /iam-protected');
337
+
338
+ // IAM returns empty headers in current implementation
339
+ expect(result).toEqual({ data: 'iam-data' });
340
+ });
341
+ });
342
+
343
+ describe('header merging', () => {
344
+ it('should merge auth headers with user-provided headers', async () => {
345
+ const tokenProvider: TokenProvider = {
346
+ getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
347
+ createValidAuthHeaders: vi.fn().mockResolvedValue({
348
+ Authorization: 'Bearer valid-token',
349
+ }),
350
+ };
351
+
352
+ const endpointAuth = {
353
+ 'GET /protected': 'bearer',
354
+ } as const;
355
+
356
+ const api = createAuthAwareFetcher<TestPaths>({
357
+ baseURL: 'https://api.example.com',
358
+ endpointAuth,
359
+ securitySchemes,
360
+ authStrategies: {
361
+ bearer: { type: 'bearer', tokenProvider },
362
+ },
363
+ });
364
+
365
+ await api('GET /protected', {
366
+ headers: { 'X-Custom-Header': 'custom-value' },
367
+ });
368
+
369
+ expect(lastReceivedHeaders.authorization).toBe('Bearer valid-token');
370
+ expect(lastReceivedHeaders['x-custom-header']).toBe('custom-value');
371
+ });
372
+
373
+ it('should allow user headers to override auth headers', async () => {
374
+ const tokenProvider: TokenProvider = {
375
+ getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
376
+ createValidAuthHeaders: vi.fn().mockResolvedValue({
377
+ Authorization: 'Bearer valid-token',
378
+ }),
379
+ };
380
+
381
+ // Override handler to accept any valid Bearer token
382
+ server.use(
383
+ http.get('https://api.example.com/protected', ({ request }) => {
384
+ lastReceivedHeaders = Object.fromEntries(request.headers.entries());
385
+ const auth = request.headers.get('Authorization');
386
+ if (!auth || !auth.startsWith('Bearer ')) {
387
+ return HttpResponse.json(
388
+ { message: 'Unauthorized' },
389
+ { status: 401 },
390
+ );
391
+ }
392
+ return HttpResponse.json({ data: 'secret' });
393
+ }),
394
+ );
395
+
396
+ const endpointAuth = {
397
+ 'GET /protected': 'bearer',
398
+ } as const;
399
+
400
+ const api = createAuthAwareFetcher<TestPaths>({
401
+ baseURL: 'https://api.example.com',
402
+ endpointAuth,
403
+ securitySchemes,
404
+ authStrategies: {
405
+ bearer: { type: 'bearer', tokenProvider },
406
+ },
407
+ });
408
+
409
+ await api('GET /protected', {
410
+ headers: { Authorization: 'Bearer override-token' },
411
+ });
412
+
413
+ expect(lastReceivedHeaders.authorization).toBe('Bearer override-token');
414
+ });
415
+ });
416
+
417
+ describe('onRequest interceptor', () => {
418
+ it('should call user onRequest interceptor after auth headers', async () => {
419
+ const onRequest = vi.fn((config: RequestInit) => ({
420
+ ...config,
421
+ headers: {
422
+ ...config.headers,
423
+ 'X-Intercepted': 'true',
424
+ },
425
+ }));
426
+
427
+ const tokenProvider: TokenProvider = {
428
+ getValidAccessToken: vi.fn().mockResolvedValue('valid-token'),
429
+ createValidAuthHeaders: vi.fn().mockResolvedValue({
430
+ Authorization: 'Bearer valid-token',
431
+ }),
432
+ };
433
+
434
+ const endpointAuth = {
435
+ 'GET /protected': 'bearer',
436
+ } as const;
437
+
438
+ const api = createAuthAwareFetcher<TestPaths>({
439
+ baseURL: 'https://api.example.com',
440
+ endpointAuth,
441
+ securitySchemes,
442
+ authStrategies: {
443
+ bearer: { type: 'bearer', tokenProvider },
444
+ },
445
+ onRequest,
446
+ });
447
+
448
+ await api('GET /protected');
449
+
450
+ expect(onRequest).toHaveBeenCalled();
451
+ expect(lastReceivedHeaders['x-intercepted']).toBe('true');
452
+ });
453
+ });
454
+
455
+ describe('none auth strategy', () => {
456
+ it('should not add any headers for none strategy', async () => {
457
+ const endpointAuth = {
458
+ 'GET /users': 'noAuth',
459
+ } as const;
460
+
461
+ const customSchemes: Record<string, SecuritySchemeObject> = {
462
+ noAuth: {
463
+ type: 'http',
464
+ scheme: 'none',
465
+ },
466
+ };
467
+
468
+ const api = createAuthAwareFetcher<TestPaths>({
469
+ baseURL: 'https://api.example.com',
470
+ endpointAuth,
471
+ securitySchemes: customSchemes,
472
+ authStrategies: {
473
+ noAuth: { type: 'none' },
474
+ },
475
+ });
476
+
477
+ const result = await api('GET /users');
478
+
479
+ expect(result).toEqual({ users: [{ id: '1', name: 'John' }] });
480
+ expect(lastReceivedHeaders.authorization).toBeUndefined();
481
+ });
482
+ });
483
+
484
+ describe('missing strategy handling', () => {
485
+ it('should not add headers when scheme exists but strategy is missing', async () => {
486
+ const endpointAuth = {
487
+ 'GET /users': 'unknownScheme',
488
+ } as const;
489
+
490
+ const api = createAuthAwareFetcher<TestPaths>({
491
+ baseURL: 'https://api.example.com',
492
+ endpointAuth,
493
+ securitySchemes: {
494
+ unknownScheme: { type: 'http', scheme: 'custom' },
495
+ },
496
+ // No strategy for unknownScheme
497
+ authStrategies: {} as any,
498
+ });
499
+
500
+ const result = await api('GET /users');
501
+
502
+ expect(result).toEqual({ users: [{ id: '1', name: 'John' }] });
503
+ });
504
+ });
505
+ });
@@ -3,8 +3,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
3
  * @vitest-environment jsdom
4
4
  */
5
5
  import { renderHook, waitFor } from '@testing-library/react';
6
- // biome-ignore lint/style/useImportType: required for React provider
7
- import React from 'react';
8
6
  import { afterEach, describe, expect, it, vi } from 'vitest';
9
7
  import { createOpenAPIHooks } from '../openapi-hooks';
10
8
  import './setup';
@@ -152,7 +150,7 @@ function createWrapper() {
152
150
  },
153
151
  });
154
152
 
155
- return ({ children }: { children: React.ReactNode }) => (
153
+ return ({ children }: { children: any }) => (
156
154
  <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
157
155
  );
158
156
  }
@@ -426,11 +424,12 @@ describe('createOpenAPIHooks', () => {
426
424
  expect(result.current.isSuccess).toBe(true);
427
425
  });
428
426
 
429
- expect(onSuccess).toHaveBeenCalledWith(
430
- { id: '123', name: 'Test', email: 'test@test.com' },
431
- expect.any(Object),
432
- undefined,
433
- );
427
+ expect(onSuccess).toHaveBeenCalled();
428
+ expect(onSuccess.mock.calls[0]?.[0]).toEqual({
429
+ id: '123',
430
+ name: 'Test',
431
+ email: 'test@test.com',
432
+ });
434
433
  expect(onError).not.toHaveBeenCalled();
435
434
  });
436
435
  });
@@ -199,7 +199,7 @@ export function createAuthAwareFetcher<
199
199
  const strategy =
200
200
  authStrategies[schemeName as UsedSecuritySchemes<EndpointAuth>];
201
201
 
202
- if (strategy) {
202
+ if (strategy && scheme) {
203
203
  authHeaders = await resolveAuthHeaders(strategy, scheme);
204
204
  }
205
205
  }
@@ -149,10 +149,13 @@ export function createEndpointHooks<Paths>(
149
149
  () => ({
150
150
  queryKey,
151
151
  queryFn: () =>
152
- fetcher(
153
- endpoint as Parameters<typeof fetcher>[0],
154
- config as Parameters<typeof fetcher>[1],
155
- ),
152
+ // Type assertion needed due to complex conditional types
153
+ (
154
+ fetcher as (
155
+ endpoint: T,
156
+ config?: unknown,
157
+ ) => Promise<ExtractEndpointResponse<Paths, T>>
158
+ )(endpoint, config),
156
159
  ...queryOptions,
157
160
  }),
158
161
  [
@@ -182,10 +185,13 @@ export function createEndpointHooks<Paths>(
182
185
  const memoizedOptions = useMemo(
183
186
  () => ({
184
187
  mutationFn: (config: FilteredRequestConfig<Paths, T>) =>
185
- fetcher(
186
- endpoint as Parameters<typeof fetcher>[0],
187
- config as Parameters<typeof fetcher>[1],
188
- ),
188
+ // Type assertion needed due to complex conditional types
189
+ (
190
+ fetcher as (
191
+ endpoint: T,
192
+ config?: unknown,
193
+ ) => Promise<ExtractEndpointResponse<Paths, T>>
194
+ )(endpoint, config),
189
195
  ...mutationOptions,
190
196
  }),
191
197
  [endpoint, JSON.stringify(mutationOptions)],
package/src/fetcher.ts CHANGED
@@ -160,7 +160,7 @@ export class TypedFetcher<Paths> {
160
160
  ): ParseEndpoint<T> {
161
161
  const [method, ...routeParts] = endpoint.split(' ');
162
162
  const route = routeParts.join(' ');
163
- return { method: method.toLowerCase(), route } as ParseEndpoint<T>;
163
+ return { method: method?.toLowerCase() ?? '', route } as ParseEndpoint<T>;
164
164
  }
165
165
  }
166
166
 
@@ -66,7 +66,7 @@ type OperationParams<
66
66
  : Spec extends { parameters: { path: infer P } }
67
67
  ? P
68
68
  : never;
69
- query?: Spec extends { parameters: { query?: infer Q } } ? Q : never;
69
+ query?: Spec extends { parameters?: { query?: infer Q } } ? Q : never;
70
70
  body?: Spec extends {
71
71
  requestBody: { content: { 'application/json': infer Body } };
72
72
  }
@@ -129,8 +129,8 @@ export function createOpenAPIHooks<Paths>(
129
129
  operationId: OpId,
130
130
  ): string {
131
131
  // Runtime lookup from registry
132
- if (operations && operations[operationId as string]) {
133
- const op = operations[operationId as string];
132
+ const op = operations?.[operationId as string];
133
+ if (op) {
134
134
  return `${op.method.toUpperCase()} ${op.path}`;
135
135
  }
136
136
  // Fallback for compile-time only usage
package/src/types.ts CHANGED
@@ -154,15 +154,17 @@ export type FilteredRequestConfig<
154
154
  : never;
155
155
 
156
156
  /**
157
- * Helper to build request config with correct required/optional fields
157
+ * Helper to build request config with correct required/optional fields.
158
+ * Uses [T] extends [never] pattern to prevent distribution over union types,
159
+ * which would cause the entire type to become `never`.
158
160
  */
159
161
  type BuildRequestConfig<TParams, TQuery, TBody> = SimplifyIntersection<
160
162
  // params: required if not never
161
- (TParams extends never ? {} : { params: TParams }) &
163
+ ([TParams] extends [never] ? {} : { params: TParams }) &
162
164
  // body: required if not never
163
- (TBody extends never ? {} : { body: TBody }) &
165
+ ([TBody] extends [never] ? {} : { body: TBody }) &
164
166
  // query: optional if not never
165
- (TQuery extends never ? {} : { query?: TQuery }) & {
167
+ ([TQuery] extends [never] ? {} : { query?: TQuery }) & {
166
168
  // headers: always optional
167
169
  headers?: Record<string, string>;
168
170
  }
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "composite": true,
7
+ "lib": ["ES2023", "DOM"],
8
+ "jsx": "react-jsx"
9
+ },
10
+ "include": ["src/**/*"],
11
+ "exclude": ["src/__tests__/**/*"]
12
+ }