@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.
- package/README.md +518 -0
- package/dist/chunk-CUT6urMc.cjs +30 -0
- package/dist/fetcher-DLDD_7Sa.mjs +86 -0
- package/dist/fetcher-DLDD_7Sa.mjs.map +1 -0
- package/dist/fetcher-KdwHgdAl.cjs +98 -0
- package/dist/fetcher-KdwHgdAl.cjs.map +1 -0
- package/dist/fetcher.cjs +4 -0
- package/dist/fetcher.d.cts +18 -0
- package/dist/fetcher.d.mts +18 -0
- package/dist/fetcher.mjs +3 -0
- package/dist/openapi-hooks.cjs +44 -0
- package/dist/openapi-hooks.cjs.map +1 -0
- package/dist/openapi-hooks.d.cts +99 -0
- package/dist/openapi-hooks.d.mts +99 -0
- package/dist/openapi-hooks.mjs +43 -0
- package/dist/openapi-hooks.mjs.map +1 -0
- package/dist/openapi-types.d.cjs +0 -0
- package/dist/openapi-types.d.cts +443 -0
- package/dist/openapi-types.d.mts +443 -0
- package/dist/openapi.cjs +526 -0
- package/dist/openapi.cjs.map +1 -0
- package/dist/openapi.mjs +501 -0
- package/dist/openapi.mjs.map +1 -0
- package/dist/react-query.cjs +147 -0
- package/dist/react-query.cjs.map +1 -0
- package/dist/react-query.d.cts +77 -0
- package/dist/react-query.d.mts +77 -0
- package/dist/react-query.mjs +141 -0
- package/dist/react-query.mjs.map +1 -0
- package/dist/types-csACmD6U.d.cts +68 -0
- package/dist/types-tMp85Lt_.d.mts +68 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +2 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +0 -0
- package/package.json +59 -0
- package/src/__tests__/fetcher.spec.ts +409 -0
- package/src/__tests__/method-restrictions.spec.tsx +227 -0
- package/src/__tests__/openapi-hooks.spec.tsx +558 -0
- package/src/__tests__/react-query-infinite.spec.tsx +1003 -0
- package/src/__tests__/react-query-invalidation.spec.tsx +238 -0
- package/src/__tests__/react-query.spec.tsx +582 -0
- package/src/__tests__/setup.ts +281 -0
- package/src/__tests__/types.spec.ts +134 -0
- package/src/__tests__/url-parsing.spec.ts +196 -0
- package/src/fetcher.ts +173 -0
- package/src/openapi-hooks.ts +193 -0
- package/src/openapi-types.d.ts +440 -0
- package/src/openapi.json +595 -0
- package/src/react-query.ts +341 -0
- package/src/types.ts +149 -0
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint } from "./types-csACmD6U.cjs";
|
|
2
|
+
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint };
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint } from "./types-tMp85Lt_.mjs";
|
|
2
|
+
export { EndpointString, ExtractEndpointConfig, ExtractEndpointResponse, ExtractMethod, ExtractPathParams, ExtractQueryParams, ExtractRequestBody, ExtractResponse, FetcherOptions, FilterEndpointByMethod, FilteredRequestConfig, MutationEndpoint, OpenAPIRoutes, ParseEndpoint, QueryEndpoint, RequestConfig, TypedEndpoint, ValidEndpoint };
|
package/dist/types.mjs
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@geekmidas/client",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"require": "./dist/index.cjs"
|
|
11
|
+
},
|
|
12
|
+
"./fetcher": {
|
|
13
|
+
"types": "./dist/fetcher.d.ts",
|
|
14
|
+
"import": "./dist/fetcher.mjs",
|
|
15
|
+
"require": "./dist/fetcher.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./react-query": {
|
|
18
|
+
"types": "./dist/react-query.d.ts",
|
|
19
|
+
"import": "./dist/react-query.mjs",
|
|
20
|
+
"require": "./dist/react-query.cjs"
|
|
21
|
+
},
|
|
22
|
+
"./openapi": {
|
|
23
|
+
"types": "./dist/openapi-hooks.d.ts",
|
|
24
|
+
"import": "./dist/openapi-hooks.mjs",
|
|
25
|
+
"require": "./dist/openapi-hooks.cjs"
|
|
26
|
+
},
|
|
27
|
+
"./types": {
|
|
28
|
+
"types": "./dist/types.d.ts",
|
|
29
|
+
"import": "./dist/types.mjs",
|
|
30
|
+
"require": "./dist/types.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/geekmidas/toolbox.git"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"registry": "https://registry.npmjs.org/",
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@testing-library/jest-dom": "~6.6.3",
|
|
43
|
+
"@testing-library/dom": "~10.4.0",
|
|
44
|
+
"@testing-library/react": "~16.3.0",
|
|
45
|
+
"@testing-library/react-hooks": "~8.0.1",
|
|
46
|
+
"@types/react": "~19.1.8",
|
|
47
|
+
"@types/react-dom": "~19.1.6",
|
|
48
|
+
"jsdom": "~26.1.0",
|
|
49
|
+
"msw": "~2.10.3"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@tanstack/react-query": ">=5.0.0",
|
|
53
|
+
"react": ">=18.0.0",
|
|
54
|
+
"react-dom": ">=18.0.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"ts": "tsc --noEmit --skipLibCheck src/**/*.ts"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { type FetchFn, createTypedFetcher } from '../fetcher';
|
|
3
|
+
import type { paths } from '../openapi-types';
|
|
4
|
+
import './setup';
|
|
5
|
+
|
|
6
|
+
describe('TypedFetcher', () => {
|
|
7
|
+
it('should make GET request to fetch users', async () => {
|
|
8
|
+
const client = createTypedFetcher<paths>({
|
|
9
|
+
baseURL: 'https://api.example.com',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const result = await client('GET /users');
|
|
13
|
+
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
users: [
|
|
16
|
+
{ id: '1', name: 'John Doe', email: 'john@example.com' },
|
|
17
|
+
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' },
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should make GET request with path params', async () => {
|
|
23
|
+
const client = createTypedFetcher<paths>({
|
|
24
|
+
baseURL: 'https://api.example.com',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = await client('GET /users/{id}', { params: { id: '123' } });
|
|
28
|
+
|
|
29
|
+
expect(result).toEqual({
|
|
30
|
+
id: '123',
|
|
31
|
+
name: 'John Doe',
|
|
32
|
+
email: 'john@example.com',
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should make POST request with body', async () => {
|
|
37
|
+
const client = createTypedFetcher<paths>({
|
|
38
|
+
baseURL: 'https://api.example.com',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const requestBody = { name: 'Jane Doe', email: 'jane@example.com' };
|
|
42
|
+
const result = await client('POST /users', { body: requestBody });
|
|
43
|
+
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
id: '123',
|
|
46
|
+
name: 'Jane Doe',
|
|
47
|
+
email: 'jane@example.com',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should make PUT request with path params and body', async () => {
|
|
52
|
+
const client = createTypedFetcher<paths>({
|
|
53
|
+
baseURL: 'https://api.example.com',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await client('PUT /users/{id}', {
|
|
57
|
+
params: { id: '456' },
|
|
58
|
+
body: { name: 'Updated Name' },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(result).toEqual({
|
|
62
|
+
id: '456',
|
|
63
|
+
name: 'Updated Name',
|
|
64
|
+
email: 'john@example.com',
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should handle DELETE requests', async () => {
|
|
69
|
+
const client = createTypedFetcher<paths>({
|
|
70
|
+
baseURL: 'https://api.example.com',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// DELETE returns no content (204)
|
|
74
|
+
const result = await client('DELETE /users/{id}', {
|
|
75
|
+
params: { id: '123' },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// For 204 responses, result should be undefined or empty
|
|
79
|
+
expect(result).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle query parameters', async () => {
|
|
83
|
+
const client = createTypedFetcher<paths>({
|
|
84
|
+
baseURL: 'https://api.example.com',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await client('GET /posts', {
|
|
88
|
+
query: { page: 2, limit: 5, sort: 'desc' },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(result.posts).toHaveLength(5);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle array query parameters', async () => {
|
|
95
|
+
// Mock fetch to capture the request URL
|
|
96
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
97
|
+
ok: true,
|
|
98
|
+
status: 200,
|
|
99
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
100
|
+
json: async () => ({ items: ['a', 'b', 'c'] }),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const client = createTypedFetcher<any>({
|
|
104
|
+
baseURL: 'https://api.example.com',
|
|
105
|
+
fetch: mockFetch,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await client('GET /search', {
|
|
109
|
+
query: { tags: ['nodejs', 'typescript', 'javascript'] as any },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
113
|
+
'https://api.example.com/search?tags=nodejs&tags=typescript&tags=javascript',
|
|
114
|
+
expect.any(Object),
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle object query parameters with dot notation', async () => {
|
|
119
|
+
// Mock fetch to capture the request URL
|
|
120
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
121
|
+
ok: true,
|
|
122
|
+
status: 200,
|
|
123
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
124
|
+
json: async () => ({ results: [] }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const client = createTypedFetcher<any>({
|
|
128
|
+
baseURL: 'https://api.example.com',
|
|
129
|
+
fetch: mockFetch,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await client('GET /products', {
|
|
133
|
+
query: {
|
|
134
|
+
filter: {
|
|
135
|
+
category: 'electronics',
|
|
136
|
+
minPrice: 100,
|
|
137
|
+
maxPrice: 500,
|
|
138
|
+
},
|
|
139
|
+
sort: 'price',
|
|
140
|
+
} as any,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
144
|
+
'https://api.example.com/products?filter.category=electronics&filter.minPrice=100&filter.maxPrice=500&sort=price',
|
|
145
|
+
expect.any(Object),
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should handle arrays within nested objects', async () => {
|
|
150
|
+
// Mock fetch to capture the request URL
|
|
151
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
152
|
+
ok: true,
|
|
153
|
+
status: 200,
|
|
154
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
155
|
+
json: async () => ({ results: [] }),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const client = createTypedFetcher<any>({
|
|
159
|
+
baseURL: 'https://api.example.com',
|
|
160
|
+
fetch: mockFetch,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await client('GET /advanced-search', {
|
|
164
|
+
query: {
|
|
165
|
+
user: {
|
|
166
|
+
roles: ['admin', 'moderator', 'user'],
|
|
167
|
+
status: 'active',
|
|
168
|
+
},
|
|
169
|
+
settings: {
|
|
170
|
+
notifications: {
|
|
171
|
+
types: ['email', 'sms', 'push'],
|
|
172
|
+
enabled: true,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
} as any,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
179
|
+
'https://api.example.com/advanced-search?user.roles=admin&user.roles=moderator&user.roles=user&user.status=active&settings.notifications.types=email&settings.notifications.types=sms&settings.notifications.types=push&settings.notifications.enabled=true',
|
|
180
|
+
expect.any(Object),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should handle 404 errors', async () => {
|
|
185
|
+
const client = createTypedFetcher<paths>({
|
|
186
|
+
baseURL: 'https://api.example.com',
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await client('GET /users/{id}', {
|
|
191
|
+
params: { id: '404' },
|
|
192
|
+
});
|
|
193
|
+
expect.fail('Should have thrown an error');
|
|
194
|
+
} catch (error) {
|
|
195
|
+
expect(error).toBeInstanceOf(Response);
|
|
196
|
+
const response = error as Response;
|
|
197
|
+
expect(response.status).toBe(404);
|
|
198
|
+
const data = await response.json();
|
|
199
|
+
expect(data.message).toBe('User not found');
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should handle 500 errors', async () => {
|
|
204
|
+
const client = createTypedFetcher<paths>({
|
|
205
|
+
baseURL: 'https://api.example.com',
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
await client('GET /error');
|
|
210
|
+
expect.fail('Should have thrown an error');
|
|
211
|
+
} catch (error) {
|
|
212
|
+
expect(error).toBeInstanceOf(Response);
|
|
213
|
+
const response = error as Response;
|
|
214
|
+
expect(response.status).toBe(500);
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
expect(data.message).toBe('Internal server error');
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should apply request interceptor', async () => {
|
|
221
|
+
const onRequest = vi.fn((config) => ({
|
|
222
|
+
...config,
|
|
223
|
+
headers: { ...config.headers, 'X-Custom-Header': 'test-value' },
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
const client = createTypedFetcher<paths>({
|
|
227
|
+
baseURL: 'https://api.example.com',
|
|
228
|
+
onRequest,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await client('GET /users/{id}', { params: { id: '123' } });
|
|
232
|
+
|
|
233
|
+
expect(onRequest).toHaveBeenCalledWith(
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
method: 'GET',
|
|
236
|
+
headers: expect.any(Object),
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('should apply response interceptor', async () => {
|
|
242
|
+
const onResponse = vi.fn((response) => response);
|
|
243
|
+
|
|
244
|
+
const client = createTypedFetcher<paths>({
|
|
245
|
+
baseURL: 'https://api.example.com',
|
|
246
|
+
onResponse,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await client('GET /users/{id}', { params: { id: '123' } });
|
|
250
|
+
|
|
251
|
+
expect(onResponse).toHaveBeenCalledWith(expect.any(Response));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should apply error handler', async () => {
|
|
255
|
+
const onError = vi.fn();
|
|
256
|
+
|
|
257
|
+
const client = createTypedFetcher<paths>({
|
|
258
|
+
baseURL: 'https://api.example.com',
|
|
259
|
+
onError,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
await expect(
|
|
263
|
+
client('GET /users/{id}', { params: { id: '404' } }),
|
|
264
|
+
).rejects.toThrow();
|
|
265
|
+
|
|
266
|
+
expect(onError).toHaveBeenCalledWith(expect.any(Response));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should merge default headers with request headers', async () => {
|
|
270
|
+
const client = createTypedFetcher<paths>({
|
|
271
|
+
baseURL: 'https://api.example.com',
|
|
272
|
+
headers: { Authorization: 'Bearer valid-token' },
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const result = await client('GET /protected', {
|
|
276
|
+
headers: { 'X-Custom': 'value' },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result).toEqual({ message: 'Protected data' });
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should handle unauthorized requests', async () => {
|
|
283
|
+
const client = createTypedFetcher<paths>({
|
|
284
|
+
baseURL: 'https://api.example.com',
|
|
285
|
+
headers: { Authorization: 'Bearer broken-token' },
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
await client('GET /protected');
|
|
290
|
+
expect.fail('Should have thrown an error');
|
|
291
|
+
} catch (error) {
|
|
292
|
+
expect(error).toBeInstanceOf(Response);
|
|
293
|
+
const response = error as Response;
|
|
294
|
+
expect(response.status).toBe(401);
|
|
295
|
+
const data = await response.json();
|
|
296
|
+
expect(data.message).toBe('Unauthorized');
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should handle empty query parameters', async () => {
|
|
301
|
+
const client = createTypedFetcher<paths>({
|
|
302
|
+
baseURL: 'https://api.example.com',
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const result = await client('GET /posts', {
|
|
306
|
+
query: { page: 1, sort: undefined as any },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expect(result.pagination.page).toBe(1);
|
|
310
|
+
expect(result.sort).toBe('asc'); // Default value when sort is undefined
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should properly encode path parameters', async () => {
|
|
314
|
+
const client = createTypedFetcher<paths>({
|
|
315
|
+
baseURL: 'https://api.example.com',
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const result = await client('GET /users/{id}', {
|
|
319
|
+
params: { id: 'user with spaces' },
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// MSW decodes the URL parameters, so we get the original value back
|
|
323
|
+
expect(result).toEqual({
|
|
324
|
+
id: 'user with spaces',
|
|
325
|
+
name: 'John Doe',
|
|
326
|
+
email: 'john@example.com',
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should correctly substitute multiple path parameters', async () => {
|
|
331
|
+
// Add a mock handler for this test
|
|
332
|
+
const mockFetch = vi.fn(async (url: string) => {
|
|
333
|
+
if (url === 'https://api.example.com/posts/123/comments/456') {
|
|
334
|
+
return new Response(
|
|
335
|
+
JSON.stringify({
|
|
336
|
+
postId: '123',
|
|
337
|
+
commentId: '456',
|
|
338
|
+
content: 'Test comment',
|
|
339
|
+
}),
|
|
340
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return new Response('Not found', { status: 404 });
|
|
344
|
+
}) as FetchFn;
|
|
345
|
+
|
|
346
|
+
const client = createTypedFetcher<paths>({
|
|
347
|
+
baseURL: 'https://api.example.com',
|
|
348
|
+
fetch: mockFetch,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const result = await client(
|
|
352
|
+
'GET /posts/{postId}/comments/{commentId}' as any,
|
|
353
|
+
{
|
|
354
|
+
params: { postId: '123', commentId: '456' },
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
359
|
+
'https://api.example.com/posts/123/comments/456',
|
|
360
|
+
expect.any(Object),
|
|
361
|
+
);
|
|
362
|
+
expect(result).toEqual({
|
|
363
|
+
postId: '123',
|
|
364
|
+
commentId: '456',
|
|
365
|
+
content: 'Test comment',
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should URL encode special characters in path parameters', async () => {
|
|
370
|
+
// Mock fetch to verify the actual URL being called
|
|
371
|
+
const mockFetch = vi.fn(async (url: string) => {
|
|
372
|
+
if (
|
|
373
|
+
url ===
|
|
374
|
+
'https://api.example.com/users/user%20with%20spaces%2Fand%2Fslashes'
|
|
375
|
+
) {
|
|
376
|
+
return new Response(
|
|
377
|
+
JSON.stringify({
|
|
378
|
+
id: 'user with spaces/and/slashes',
|
|
379
|
+
name: 'Test User',
|
|
380
|
+
email: 'test@example.com',
|
|
381
|
+
}),
|
|
382
|
+
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return new Response('Not found', { status: 404 });
|
|
386
|
+
}) as typeof fetch;
|
|
387
|
+
|
|
388
|
+
const client = createTypedFetcher<paths>({
|
|
389
|
+
baseURL: 'https://api.example.com',
|
|
390
|
+
fetch: mockFetch,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const result = await client('GET /users/{id}', {
|
|
394
|
+
params: { id: 'user with spaces/and/slashes' },
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Verify the URL was properly encoded
|
|
398
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
399
|
+
'https://api.example.com/users/user%20with%20spaces%2Fand%2Fslashes',
|
|
400
|
+
expect.any(Object),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(result).toEqual({
|
|
404
|
+
id: 'user with spaces/and/slashes',
|
|
405
|
+
name: 'Test User',
|
|
406
|
+
email: 'test@example.com',
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
+
import { renderHook } from '@testing-library/react';
|
|
6
|
+
// biome-ignore lint/style/useImportType: <explanation>
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import type { paths } from '../openapi-types';
|
|
10
|
+
import { createTypedQueryClient } from '../react-query';
|
|
11
|
+
import './setup';
|
|
12
|
+
|
|
13
|
+
describe('Method Restrictions', () => {
|
|
14
|
+
const queryClient = new QueryClient({
|
|
15
|
+
defaultOptions: {
|
|
16
|
+
queries: {
|
|
17
|
+
retry: false,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
23
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
describe('useQuery', () => {
|
|
27
|
+
it('should accept GET endpoints', () => {
|
|
28
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
29
|
+
baseURL: 'https://api.example.com',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// This should compile - GET endpoints are allowed
|
|
33
|
+
const { result } = renderHook(() => typedClient.useQuery('GET /users'), {
|
|
34
|
+
wrapper,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(result.current).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should accept GET endpoints with parameters', () => {
|
|
41
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
42
|
+
baseURL: 'https://api.example.com',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// This should compile - GET endpoints with params are allowed
|
|
46
|
+
const { result } = renderHook(
|
|
47
|
+
() =>
|
|
48
|
+
typedClient.useQuery('GET /users/{id}', {
|
|
49
|
+
params: { id: '123' },
|
|
50
|
+
}),
|
|
51
|
+
{ wrapper },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(result.current).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should accept GET endpoints with query parameters', () => {
|
|
58
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
59
|
+
baseURL: 'https://api.example.com',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// This should compile - GET endpoints with query params are allowed
|
|
63
|
+
const { result } = renderHook(
|
|
64
|
+
() =>
|
|
65
|
+
typedClient.useQuery('GET /posts', {
|
|
66
|
+
query: { page: 1, limit: 10 },
|
|
67
|
+
}),
|
|
68
|
+
{ wrapper },
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(result.current).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// The following tests would fail TypeScript compilation if uncommented
|
|
75
|
+
// They are left here as documentation of what should NOT work
|
|
76
|
+
|
|
77
|
+
/*
|
|
78
|
+
it('should NOT accept POST endpoints', () => {
|
|
79
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
80
|
+
baseURL: 'https://api.example.com',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// @ts-expect-error - POST endpoints should not be allowed in useQuery
|
|
84
|
+
const { result } = renderHook(
|
|
85
|
+
() => typedClient.useQuery('POST /users'),
|
|
86
|
+
{ wrapper },
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should NOT accept PUT endpoints', () => {
|
|
91
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
92
|
+
baseURL: 'https://api.example.com',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// @ts-expect-error - PUT endpoints should not be allowed in useQuery
|
|
96
|
+
const { result } = renderHook(
|
|
97
|
+
() => typedClient.useQuery('PUT /users/{id}', { params: { id: '123' } }),
|
|
98
|
+
{ wrapper },
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should NOT accept DELETE endpoints', () => {
|
|
103
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
104
|
+
baseURL: 'https://api.example.com',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// @ts-expect-error - DELETE endpoints should not be allowed in useQuery
|
|
108
|
+
const { result } = renderHook(
|
|
109
|
+
() => typedClient.useQuery('DELETE /users/{id}', { params: { id: '123' } }),
|
|
110
|
+
{ wrapper },
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
*/
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('useMutation', () => {
|
|
117
|
+
it('should accept POST endpoints', () => {
|
|
118
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
119
|
+
baseURL: 'https://api.example.com',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// This should compile - POST endpoints are allowed
|
|
123
|
+
const { result } = renderHook(
|
|
124
|
+
() => typedClient.useMutation('POST /users'),
|
|
125
|
+
{ wrapper },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(result.current).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should accept PUT endpoints', () => {
|
|
132
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
133
|
+
baseURL: 'https://api.example.com',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// This should compile - PUT endpoints are allowed
|
|
137
|
+
const { result } = renderHook(
|
|
138
|
+
() => typedClient.useMutation('PUT /users/{id}'),
|
|
139
|
+
{ wrapper },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(result.current).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should accept DELETE endpoints', () => {
|
|
146
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
147
|
+
baseURL: 'https://api.example.com',
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// This should compile - DELETE endpoints are allowed
|
|
151
|
+
const { result } = renderHook(
|
|
152
|
+
() => typedClient.useMutation('DELETE /users/{id}'),
|
|
153
|
+
{ wrapper },
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(result.current).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// The following test would fail TypeScript compilation if uncommented
|
|
160
|
+
// It is left here as documentation of what should NOT work
|
|
161
|
+
|
|
162
|
+
/*
|
|
163
|
+
it('should NOT accept GET endpoints', () => {
|
|
164
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
165
|
+
baseURL: 'https://api.example.com',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// @ts-expect-error - GET endpoints should not be allowed in useMutation
|
|
169
|
+
const { result } = renderHook(
|
|
170
|
+
() => typedClient.useMutation('GET /users'),
|
|
171
|
+
{ wrapper },
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
*/
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Type inference', () => {
|
|
178
|
+
it('should correctly infer response types for GET requests', () => {
|
|
179
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
180
|
+
baseURL: 'https://api.example.com',
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const { result } = renderHook(() => typedClient.useQuery('GET /users'), {
|
|
184
|
+
wrapper,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// TypeScript should infer this type correctly
|
|
188
|
+
type ResponseType = typeof result.current.data;
|
|
189
|
+
type ExpectedType =
|
|
190
|
+
| { users: Array<{ id: string; name: string; email: string }> }
|
|
191
|
+
| undefined;
|
|
192
|
+
|
|
193
|
+
// This assertion validates that the types match at compile time
|
|
194
|
+
const _typeCheck: ResponseType extends ExpectedType ? true : false = true;
|
|
195
|
+
const _typeCheck2: ExpectedType extends ResponseType ? true : false =
|
|
196
|
+
true;
|
|
197
|
+
|
|
198
|
+
expect(_typeCheck).toBe(true);
|
|
199
|
+
expect(_typeCheck2).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should correctly infer response types for POST requests', () => {
|
|
203
|
+
const typedClient = createTypedQueryClient<paths>({
|
|
204
|
+
baseURL: 'https://api.example.com',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const { result } = renderHook(
|
|
208
|
+
() => typedClient.useMutation('POST /users'),
|
|
209
|
+
{ wrapper },
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// TypeScript should infer this type correctly
|
|
213
|
+
type ResponseType = typeof result.current.data;
|
|
214
|
+
type ExpectedType =
|
|
215
|
+
| { id: string; name: string; email: string }
|
|
216
|
+
| undefined;
|
|
217
|
+
|
|
218
|
+
// This assertion validates that the types match at compile time
|
|
219
|
+
const _typeCheck: ResponseType extends ExpectedType ? true : false = true;
|
|
220
|
+
const _typeCheck2: ExpectedType extends ResponseType ? true : false =
|
|
221
|
+
true;
|
|
222
|
+
|
|
223
|
+
expect(_typeCheck).toBe(true);
|
|
224
|
+
expect(_typeCheck2).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|