@geekmidas/client 0.3.0 → 0.5.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 (80) hide show
  1. package/dist/auth-fetcher.cjs +2 -3
  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 -3
  8. package/dist/auth-fetcher.mjs.map +1 -1
  9. package/dist/endpoint-hooks.cjs +10 -5
  10. package/dist/endpoint-hooks.cjs.map +1 -1
  11. package/dist/endpoint-hooks.d.cts +2 -2
  12. package/dist/endpoint-hooks.d.cts.map +1 -0
  13. package/dist/endpoint-hooks.d.mts +2 -2
  14. package/dist/endpoint-hooks.d.mts.map +1 -0
  15. package/dist/endpoint-hooks.mjs +10 -5
  16. package/dist/endpoint-hooks.mjs.map +1 -1
  17. package/dist/{fetcher-DLDD_7Sa.mjs → fetcher-5fnBE7Dk.mjs} +2 -2
  18. package/dist/fetcher-5fnBE7Dk.mjs.map +1 -0
  19. package/dist/{fetcher-KdwHgdAl.cjs → fetcher-CgLEaIAC.cjs} +2 -2
  20. package/dist/fetcher-CgLEaIAC.cjs.map +1 -0
  21. package/dist/fetcher.cjs +1 -1
  22. package/dist/fetcher.d.cts +3 -1
  23. package/dist/fetcher.d.cts.map +1 -0
  24. package/dist/fetcher.d.mts +3 -1
  25. package/dist/fetcher.d.mts.map +1 -0
  26. package/dist/fetcher.mjs +1 -1
  27. package/dist/infer.d.cts.map +1 -0
  28. package/dist/infer.d.mts.map +1 -0
  29. package/dist/openapi-hooks.cjs +3 -5
  30. package/dist/openapi-hooks.cjs.map +1 -1
  31. package/dist/openapi-hooks.d.cts +3 -3
  32. package/dist/openapi-hooks.d.cts.map +1 -0
  33. package/dist/openapi-hooks.d.mts +6 -6
  34. package/dist/openapi-hooks.d.mts.map +1 -0
  35. package/dist/openapi-hooks.mjs +3 -5
  36. package/dist/openapi-hooks.mjs.map +1 -1
  37. package/dist/openapi-types.d.cts.map +1 -0
  38. package/dist/openapi-types.d.mts.map +1 -0
  39. package/dist/openapi.cjs.map +1 -1
  40. package/dist/openapi.mjs.map +1 -1
  41. package/dist/react-query.cjs +1 -1
  42. package/dist/react-query.cjs.map +1 -1
  43. package/dist/react-query.d.cts +3 -1
  44. package/dist/react-query.d.cts.map +1 -0
  45. package/dist/react-query.d.mts +10 -8
  46. package/dist/react-query.d.mts.map +1 -0
  47. package/dist/react-query.mjs +1 -1
  48. package/dist/react-query.mjs.map +1 -1
  49. package/dist/{types-D4OSWveN.d.cts → types-4K4N-Fl1.d.cts} +8 -6
  50. package/dist/types-4K4N-Fl1.d.cts.map +1 -0
  51. package/dist/{types-Cdv1XAWr.d.mts → types-hhkZWjQn.d.mts} +8 -6
  52. package/dist/types-hhkZWjQn.d.mts.map +1 -0
  53. package/dist/types.d.cts +1 -1
  54. package/dist/types.d.mts +1 -1
  55. package/package.json +6 -5
  56. package/src/__tests__/auth-fetcher.spec.ts +505 -0
  57. package/src/__tests__/endpoint-hooks.spec.tsx +348 -348
  58. package/src/__tests__/fetcher.spec.ts +403 -403
  59. package/src/__tests__/infer.integration.spec.ts +171 -171
  60. package/src/__tests__/infer.spec.ts +182 -182
  61. package/src/__tests__/method-restrictions.spec.tsx +165 -165
  62. package/src/__tests__/openapi-hooks.spec.tsx +536 -537
  63. package/src/__tests__/react-query-infinite.spec.tsx +989 -989
  64. package/src/__tests__/react-query-invalidation.spec.tsx +223 -222
  65. package/src/__tests__/react-query.spec.tsx +567 -567
  66. package/src/__tests__/setup.ts +260 -260
  67. package/src/__tests__/types.spec.ts +127 -127
  68. package/src/__tests__/url-parsing.spec.ts +191 -191
  69. package/src/auth-fetcher.ts +160 -162
  70. package/src/endpoint-hooks.ts +151 -150
  71. package/src/fetcher.ts +163 -163
  72. package/src/infer.ts +64 -63
  73. package/src/openapi-hooks.ts +146 -146
  74. package/src/openapi-types.d.ts +428 -428
  75. package/src/openapi.json +593 -593
  76. package/src/react-query.ts +281 -281
  77. package/src/types.ts +124 -125
  78. package/tsconfig.json +12 -0
  79. package/dist/fetcher-DLDD_7Sa.mjs.map +0 -1
  80. package/dist/fetcher-KdwHgdAl.cjs.map +0 -1
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
5
5
  import { renderHook, waitFor } from '@testing-library/react';
6
- import { http, HttpResponse } from 'msw';
6
+ import { HttpResponse, http } from 'msw';
7
7
  // biome-ignore lint/style/useImportType: needed for JSX
8
8
  import React, { createElement } from 'react';
9
9
  import { beforeEach, describe, expect, it } from 'vitest';
@@ -13,362 +13,362 @@ import { server } from './setup';
13
13
 
14
14
  // Test API paths type
15
15
  interface TestPaths {
16
- '/health': {
17
- get: {
18
- responses: {
19
- 200: {
20
- content: {
21
- 'application/json': { status: string };
22
- };
23
- };
24
- };
25
- };
26
- };
27
- '/users/{id}': {
28
- parameters: {
29
- path: { id: string };
30
- };
31
- get: {
32
- responses: {
33
- 200: {
34
- content: {
35
- 'application/json': { id: string; name: string };
36
- };
37
- };
38
- };
39
- };
40
- };
41
- '/users': {
42
- get: {
43
- parameters: {
44
- query: { limit?: number; offset?: number };
45
- };
46
- responses: {
47
- 200: {
48
- content: {
49
- 'application/json': Array<{ id: string; name: string }>;
50
- };
51
- };
52
- };
53
- };
54
- post: {
55
- requestBody: {
56
- content: {
57
- 'application/json': { name: string; email: string };
58
- };
59
- };
60
- responses: {
61
- 201: {
62
- content: {
63
- 'application/json': { id: string; name: string; email: string };
64
- };
65
- };
66
- };
67
- };
68
- };
69
- '/posts/{postId}/comments': {
70
- parameters: {
71
- path: { postId: string };
72
- };
73
- post: {
74
- requestBody: {
75
- content: {
76
- 'application/json': { content: string };
77
- };
78
- };
79
- responses: {
80
- 201: {
81
- content: {
82
- 'application/json': { id: string; postId: string; content: string };
83
- };
84
- };
85
- };
86
- };
87
- };
16
+ '/health': {
17
+ get: {
18
+ responses: {
19
+ 200: {
20
+ content: {
21
+ 'application/json': { status: string };
22
+ };
23
+ };
24
+ };
25
+ };
26
+ };
27
+ '/users/{id}': {
28
+ parameters: {
29
+ path: { id: string };
30
+ };
31
+ get: {
32
+ responses: {
33
+ 200: {
34
+ content: {
35
+ 'application/json': { id: string; name: string };
36
+ };
37
+ };
38
+ };
39
+ };
40
+ };
41
+ '/users': {
42
+ get: {
43
+ parameters: {
44
+ query: { limit?: number; offset?: number };
45
+ };
46
+ responses: {
47
+ 200: {
48
+ content: {
49
+ 'application/json': Array<{ id: string; name: string }>;
50
+ };
51
+ };
52
+ };
53
+ };
54
+ post: {
55
+ requestBody: {
56
+ content: {
57
+ 'application/json': { name: string; email: string };
58
+ };
59
+ };
60
+ responses: {
61
+ 201: {
62
+ content: {
63
+ 'application/json': { id: string; name: string; email: string };
64
+ };
65
+ };
66
+ };
67
+ };
68
+ };
69
+ '/posts/{postId}/comments': {
70
+ parameters: {
71
+ path: { postId: string };
72
+ };
73
+ post: {
74
+ requestBody: {
75
+ content: {
76
+ 'application/json': { content: string };
77
+ };
78
+ };
79
+ responses: {
80
+ 201: {
81
+ content: {
82
+ 'application/json': { id: string; postId: string; content: string };
83
+ };
84
+ };
85
+ };
86
+ };
87
+ };
88
88
  }
89
89
 
90
90
  // Create a mock fetcher
91
91
  function createMockFetcher(): TypedApiFunction<TestPaths> {
92
- return async (endpoint: string, config?: any) => {
93
- const [method, path] = endpoint.split(' ');
94
- let url = `https://api.example.com${path}`;
95
-
96
- // Replace path params
97
- if (config?.params) {
98
- for (const [key, value] of Object.entries(config.params)) {
99
- url = url.replace(`{${key}}`, value as string);
100
- }
101
- }
102
-
103
- // Add query params
104
- if (config?.query) {
105
- const searchParams = new URLSearchParams();
106
- for (const [key, value] of Object.entries(config.query)) {
107
- if (value !== undefined) {
108
- searchParams.set(key, String(value));
109
- }
110
- }
111
- if (searchParams.toString()) {
112
- url += `?${searchParams.toString()}`;
113
- }
114
- }
115
-
116
- const response = await fetch(url, {
117
- method,
118
- headers: {
119
- 'Content-Type': 'application/json',
120
- ...config?.headers,
121
- },
122
- body: config?.body ? JSON.stringify(config.body) : undefined,
123
- });
124
-
125
- return response.json();
126
- };
92
+ return async (endpoint: string, config?: any) => {
93
+ const [method, path] = endpoint.split(' ');
94
+ let url = `https://api.example.com${path}`;
95
+
96
+ // Replace path params
97
+ if (config?.params) {
98
+ for (const [key, value] of Object.entries(config.params)) {
99
+ url = url.replace(`{${key}}`, value as string);
100
+ }
101
+ }
102
+
103
+ // Add query params
104
+ if (config?.query) {
105
+ const searchParams = new URLSearchParams();
106
+ for (const [key, value] of Object.entries(config.query)) {
107
+ if (value !== undefined) {
108
+ searchParams.set(key, String(value));
109
+ }
110
+ }
111
+ if (searchParams.toString()) {
112
+ url += `?${searchParams.toString()}`;
113
+ }
114
+ }
115
+
116
+ const response = await fetch(url, {
117
+ method,
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ ...config?.headers,
121
+ },
122
+ body: config?.body ? JSON.stringify(config.body) : undefined,
123
+ });
124
+
125
+ return response.json();
126
+ };
127
127
  }
128
128
 
129
129
  // Test wrapper with QueryClient
130
130
  function createWrapper() {
131
- const queryClient = new QueryClient({
132
- defaultOptions: {
133
- queries: {
134
- retry: false,
135
- },
136
- },
137
- });
138
-
139
- return ({ children }: { children: React.ReactNode }) =>
140
- createElement(QueryClientProvider, { client: queryClient }, children);
131
+ const queryClient = new QueryClient({
132
+ defaultOptions: {
133
+ queries: {
134
+ retry: false,
135
+ },
136
+ },
137
+ });
138
+
139
+ return ({ children }: { children: React.ReactNode }) =>
140
+ createElement(QueryClientProvider, { client: queryClient }, children);
141
141
  }
142
142
 
143
143
  describe('createEndpointHooks', () => {
144
- beforeEach(() => {
145
- // Add test-specific handlers
146
- server.use(
147
- http.get('https://api.example.com/health', () => {
148
- return HttpResponse.json({ status: 'ok' });
149
- }),
150
- http.get('https://api.example.com/posts/:postId/comments', () => {
151
- return HttpResponse.json([{ id: 'c1', content: 'Test comment' }]);
152
- }),
153
- http.post(
154
- 'https://api.example.com/posts/:postId/comments',
155
- async ({ params, request }) => {
156
- const body = (await request.json()) as { content: string };
157
- return HttpResponse.json(
158
- {
159
- id: 'comment-1',
160
- postId: params.postId as string,
161
- content: body.content,
162
- },
163
- { status: 201 },
164
- );
165
- },
166
- ),
167
- );
168
- });
169
-
170
- describe('useQuery', () => {
171
- it('should fetch data for endpoint without params', async () => {
172
- const fetcher = createMockFetcher();
173
- const hooks = createEndpointHooks<TestPaths>(fetcher);
174
-
175
- const { result } = renderHook(() => hooks.useQuery('GET /health'), {
176
- wrapper: createWrapper(),
177
- });
178
-
179
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
180
-
181
- expect(result.current.data).toEqual({ status: 'ok' });
182
- });
183
-
184
- it('should fetch data for endpoint with path params', async () => {
185
- const fetcher = createMockFetcher();
186
- const hooks = createEndpointHooks<TestPaths>(fetcher);
187
-
188
- const { result } = renderHook(
189
- () => hooks.useQuery('GET /users/{id}', { params: { id: '123' } }),
190
- { wrapper: createWrapper() },
191
- );
192
-
193
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
194
-
195
- // Uses mock from setup.ts which returns { id, name: 'John Doe', email: ... }
196
- expect(result.current.data).toMatchObject({
197
- id: '123',
198
- name: 'John Doe',
199
- });
200
- });
201
-
202
- it('should pass query options to useQuery', async () => {
203
- const fetcher = createMockFetcher();
204
- const hooks = createEndpointHooks<TestPaths>(fetcher);
205
-
206
- const { result } = renderHook(
207
- () => hooks.useQuery('GET /health', undefined, { enabled: false }),
208
- { wrapper: createWrapper() },
209
- );
210
-
211
- // Should not fetch because enabled: false
212
- expect(result.current.isLoading).toBe(false);
213
- expect(result.current.isFetching).toBe(false);
214
- expect(result.current.data).toBeUndefined();
215
- });
216
- });
217
-
218
- describe('useMutation', () => {
219
- it('should mutate data for POST endpoint', async () => {
220
- const fetcher = createMockFetcher();
221
- const hooks = createEndpointHooks<TestPaths>(fetcher);
222
-
223
- const { result } = renderHook(() => hooks.useMutation('POST /users'), {
224
- wrapper: createWrapper(),
225
- });
226
-
227
- result.current.mutate({ body: { name: 'John', email: 'john@test.com' } });
228
-
229
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
230
-
231
- // Uses mock from setup.ts
232
- expect(result.current.data).toMatchObject({
233
- id: '123',
234
- name: 'John',
235
- email: 'john@test.com',
236
- });
237
- });
238
-
239
- it('should mutate data for endpoint with params and body', async () => {
240
- const fetcher = createMockFetcher();
241
- const hooks = createEndpointHooks<TestPaths>(fetcher);
242
-
243
- const { result } = renderHook(
244
- () => hooks.useMutation('POST /posts/{postId}/comments'),
245
- { wrapper: createWrapper() },
246
- );
247
-
248
- result.current.mutate({
249
- params: { postId: 'post-123' },
250
- body: { content: 'Great post!' },
251
- });
252
-
253
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
254
-
255
- expect(result.current.data).toEqual({
256
- id: 'comment-1',
257
- postId: 'post-123',
258
- content: 'Great post!',
259
- });
260
- });
261
-
262
- it('should pass mutation options', async () => {
263
- const fetcher = createMockFetcher();
264
- const hooks = createEndpointHooks<TestPaths>(fetcher);
265
-
266
- let onSuccessCalled = false;
267
-
268
- const { result } = renderHook(
269
- () =>
270
- hooks.useMutation('POST /users', {
271
- onSuccess: () => {
272
- onSuccessCalled = true;
273
- },
274
- }),
275
- { wrapper: createWrapper() },
276
- );
277
-
278
- result.current.mutate({ body: { name: 'Jane', email: 'jane@test.com' } });
279
-
280
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
281
-
282
- expect(onSuccessCalled).toBe(true);
283
- });
284
- });
285
-
286
- describe('buildQueryKey', () => {
287
- it('should build key with just endpoint', () => {
288
- const fetcher = createMockFetcher();
289
- const hooks = createEndpointHooks<TestPaths>(fetcher);
290
-
291
- const key = hooks.buildQueryKey('GET /health');
292
-
293
- expect(key).toEqual(['GET /health']);
294
- });
295
-
296
- it('should build key with params', () => {
297
- const fetcher = createMockFetcher();
298
- const hooks = createEndpointHooks<TestPaths>(fetcher);
299
-
300
- const key = hooks.buildQueryKey('GET /users/{id}', {
301
- params: { id: '123' },
302
- });
303
-
304
- expect(key).toEqual(['GET /users/{id}', { params: { id: '123' } }]);
305
- });
306
-
307
- it('should build key with both params and query', () => {
308
- const fetcher = createMockFetcher();
309
- const hooks = createEndpointHooks<TestPaths>(fetcher);
310
-
311
- // Using a hypothetical endpoint that has both
312
- const key = hooks.buildQueryKey(
313
- 'GET /users/{id}' as any,
314
- {
315
- params: { id: '123' },
316
- query: { include: 'posts' },
317
- } as any,
318
- );
319
-
320
- expect(key).toEqual([
321
- 'GET /users/{id}',
322
- { params: { id: '123' } },
323
- { query: { include: 'posts' } },
324
- ]);
325
- });
326
- });
327
-
328
- describe('type enforcement', () => {
329
- it('should allow optional config for endpoints without required fields', () => {
330
- const fetcher = createMockFetcher();
331
- const hooks = createEndpointHooks<TestPaths>(fetcher);
332
-
333
- // This should compile - config is optional for GET /health
334
- renderHook(() => hooks.useQuery('GET /health'), {
335
- wrapper: createWrapper(),
336
- });
337
-
338
- // This should also compile - providing optional config
339
- renderHook(() => hooks.useQuery('GET /health', {}), {
340
- wrapper: createWrapper(),
341
- });
342
- });
343
-
344
- it('should require config for endpoints with path params', async () => {
345
- const fetcher = createMockFetcher();
346
- const hooks = createEndpointHooks<TestPaths>(fetcher);
347
-
348
- // Config with params is required for GET /users/{id}
349
- const { result } = renderHook(
350
- () => hooks.useQuery('GET /users/{id}', { params: { id: '456' } }),
351
- { wrapper: createWrapper() },
352
- );
353
-
354
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
355
- expect(result.current.data?.id).toBe('456');
356
- });
357
-
358
- it('should require body in mutation config for POST endpoints', async () => {
359
- const fetcher = createMockFetcher();
360
- const hooks = createEndpointHooks<TestPaths>(fetcher);
361
-
362
- const { result } = renderHook(() => hooks.useMutation('POST /users'), {
363
- wrapper: createWrapper(),
364
- });
365
-
366
- // Body is required in mutate() for POST /users
367
- result.current.mutate({
368
- body: { name: 'Test', email: 'test@test.com' },
369
- });
370
-
371
- await waitFor(() => expect(result.current.isSuccess).toBe(true));
372
- });
373
- });
144
+ beforeEach(() => {
145
+ // Add test-specific handlers
146
+ server.use(
147
+ http.get('https://api.example.com/health', () => {
148
+ return HttpResponse.json({ status: 'ok' });
149
+ }),
150
+ http.get('https://api.example.com/posts/:postId/comments', () => {
151
+ return HttpResponse.json([{ id: 'c1', content: 'Test comment' }]);
152
+ }),
153
+ http.post(
154
+ 'https://api.example.com/posts/:postId/comments',
155
+ async ({ params, request }) => {
156
+ const body = (await request.json()) as { content: string };
157
+ return HttpResponse.json(
158
+ {
159
+ id: 'comment-1',
160
+ postId: params.postId as string,
161
+ content: body.content,
162
+ },
163
+ { status: 201 },
164
+ );
165
+ },
166
+ ),
167
+ );
168
+ });
169
+
170
+ describe('useQuery', () => {
171
+ it('should fetch data for endpoint without params', async () => {
172
+ const fetcher = createMockFetcher();
173
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
174
+
175
+ const { result } = renderHook(() => hooks.useQuery('GET /health'), {
176
+ wrapper: createWrapper(),
177
+ });
178
+
179
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
180
+
181
+ expect(result.current.data).toEqual({ status: 'ok' });
182
+ });
183
+
184
+ it('should fetch data for endpoint with path params', async () => {
185
+ const fetcher = createMockFetcher();
186
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
187
+
188
+ const { result } = renderHook(
189
+ () => hooks.useQuery('GET /users/{id}', { params: { id: '123' } }),
190
+ { wrapper: createWrapper() },
191
+ );
192
+
193
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
194
+
195
+ // Uses mock from setup.ts which returns { id, name: 'John Doe', email: ... }
196
+ expect(result.current.data).toMatchObject({
197
+ id: '123',
198
+ name: 'John Doe',
199
+ });
200
+ });
201
+
202
+ it('should pass query options to useQuery', async () => {
203
+ const fetcher = createMockFetcher();
204
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
205
+
206
+ const { result } = renderHook(
207
+ () => hooks.useQuery('GET /health', undefined, { enabled: false }),
208
+ { wrapper: createWrapper() },
209
+ );
210
+
211
+ // Should not fetch because enabled: false
212
+ expect(result.current.isLoading).toBe(false);
213
+ expect(result.current.isFetching).toBe(false);
214
+ expect(result.current.data).toBeUndefined();
215
+ });
216
+ });
217
+
218
+ describe('useMutation', () => {
219
+ it('should mutate data for POST endpoint', async () => {
220
+ const fetcher = createMockFetcher();
221
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
222
+
223
+ const { result } = renderHook(() => hooks.useMutation('POST /users'), {
224
+ wrapper: createWrapper(),
225
+ });
226
+
227
+ result.current.mutate({ body: { name: 'John', email: 'john@test.com' } });
228
+
229
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
230
+
231
+ // Uses mock from setup.ts
232
+ expect(result.current.data).toMatchObject({
233
+ id: '123',
234
+ name: 'John',
235
+ email: 'john@test.com',
236
+ });
237
+ });
238
+
239
+ it('should mutate data for endpoint with params and body', async () => {
240
+ const fetcher = createMockFetcher();
241
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
242
+
243
+ const { result } = renderHook(
244
+ () => hooks.useMutation('POST /posts/{postId}/comments'),
245
+ { wrapper: createWrapper() },
246
+ );
247
+
248
+ result.current.mutate({
249
+ params: { postId: 'post-123' },
250
+ body: { content: 'Great post!' },
251
+ });
252
+
253
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
254
+
255
+ expect(result.current.data).toEqual({
256
+ id: 'comment-1',
257
+ postId: 'post-123',
258
+ content: 'Great post!',
259
+ });
260
+ });
261
+
262
+ it('should pass mutation options', async () => {
263
+ const fetcher = createMockFetcher();
264
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
265
+
266
+ let onSuccessCalled = false;
267
+
268
+ const { result } = renderHook(
269
+ () =>
270
+ hooks.useMutation('POST /users', {
271
+ onSuccess: () => {
272
+ onSuccessCalled = true;
273
+ },
274
+ }),
275
+ { wrapper: createWrapper() },
276
+ );
277
+
278
+ result.current.mutate({ body: { name: 'Jane', email: 'jane@test.com' } });
279
+
280
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
281
+
282
+ expect(onSuccessCalled).toBe(true);
283
+ });
284
+ });
285
+
286
+ describe('buildQueryKey', () => {
287
+ it('should build key with just endpoint', () => {
288
+ const fetcher = createMockFetcher();
289
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
290
+
291
+ const key = hooks.buildQueryKey('GET /health');
292
+
293
+ expect(key).toEqual(['GET /health']);
294
+ });
295
+
296
+ it('should build key with params', () => {
297
+ const fetcher = createMockFetcher();
298
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
299
+
300
+ const key = hooks.buildQueryKey('GET /users/{id}', {
301
+ params: { id: '123' },
302
+ });
303
+
304
+ expect(key).toEqual(['GET /users/{id}', { params: { id: '123' } }]);
305
+ });
306
+
307
+ it('should build key with both params and query', () => {
308
+ const fetcher = createMockFetcher();
309
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
310
+
311
+ // Using a hypothetical endpoint that has both
312
+ const key = hooks.buildQueryKey(
313
+ 'GET /users/{id}' as any,
314
+ {
315
+ params: { id: '123' },
316
+ query: { include: 'posts' },
317
+ } as any,
318
+ );
319
+
320
+ expect(key).toEqual([
321
+ 'GET /users/{id}',
322
+ { params: { id: '123' } },
323
+ { query: { include: 'posts' } },
324
+ ]);
325
+ });
326
+ });
327
+
328
+ describe('type enforcement', () => {
329
+ it('should allow optional config for endpoints without required fields', () => {
330
+ const fetcher = createMockFetcher();
331
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
332
+
333
+ // This should compile - config is optional for GET /health
334
+ renderHook(() => hooks.useQuery('GET /health'), {
335
+ wrapper: createWrapper(),
336
+ });
337
+
338
+ // This should also compile - providing optional config
339
+ renderHook(() => hooks.useQuery('GET /health', {}), {
340
+ wrapper: createWrapper(),
341
+ });
342
+ });
343
+
344
+ it('should require config for endpoints with path params', async () => {
345
+ const fetcher = createMockFetcher();
346
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
347
+
348
+ // Config with params is required for GET /users/{id}
349
+ const { result } = renderHook(
350
+ () => hooks.useQuery('GET /users/{id}', { params: { id: '456' } }),
351
+ { wrapper: createWrapper() },
352
+ );
353
+
354
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
355
+ expect(result.current.data?.id).toBe('456');
356
+ });
357
+
358
+ it('should require body in mutation config for POST endpoints', async () => {
359
+ const fetcher = createMockFetcher();
360
+ const hooks = createEndpointHooks<TestPaths>(fetcher);
361
+
362
+ const { result } = renderHook(() => hooks.useMutation('POST /users'), {
363
+ wrapper: createWrapper(),
364
+ });
365
+
366
+ // Body is required in mutate() for POST /users
367
+ result.current.mutate({
368
+ body: { name: 'Test', email: 'test@test.com' },
369
+ });
370
+
371
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
372
+ });
373
+ });
374
374
  });