@aws/nx-plugin 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/open-api/ts-client/__snapshots__/generator.additional-properties.spec.ts.snap +2236 -0
- package/src/open-api/ts-client/__snapshots__/generator.complex-types.spec.ts.snap +2307 -0
- package/src/open-api/ts-client/__snapshots__/generator.composite-types.spec.ts.snap +1495 -0
- package/src/open-api/ts-client/__snapshots__/generator.primitive-types.spec.ts.snap +1470 -0
- package/src/open-api/ts-client/__snapshots__/generator.request.spec.ts.snap +1138 -0
- package/src/open-api/ts-client/__snapshots__/generator.response.spec.ts.snap +732 -0
- package/src/open-api/ts-client/__snapshots__/generator.tags.spec.ts.snap +743 -0
- package/src/open-api/ts-client/files/client.gen.ts.template +52 -15
- package/src/open-api/ts-client/files/types.gen.ts.template +5 -0
- package/src/open-api/ts-hooks/__snapshots__/generator.spec.tsx.snap +1092 -0
- package/src/open-api/ts-hooks/files/options-proxy.gen.ts.template +210 -0
- package/src/open-api/ts-hooks/generator.d.ts +5 -0
- package/src/open-api/ts-hooks/generator.js +15 -2
- package/src/open-api/ts-hooks/generator.js.map +1 -1
- package/src/open-api/ts-hooks/generator.spec.tsx +1787 -0
- package/src/open-api/utils/codegen-data/types.d.ts +25 -0
- package/src/open-api/utils/codegen-data/types.js +26 -1
- package/src/open-api/utils/codegen-data/types.js.map +1 -1
- package/src/open-api/utils/codegen-data.js +187 -79
- package/src/open-api/utils/codegen-data.js.map +1 -1
- package/src/open-api/utils/normalise.js +11 -1
- package/src/open-api/utils/normalise.js.map +1 -1
- package/src/py/fast-api/react/__snapshots__/generator.spec.ts.snap +120 -10
- package/src/py/fast-api/react/files/website/components/__apiNameClassName__Provider.tsx.template +40 -0
- package/src/py/fast-api/react/files/website/hooks/use__apiNameClassName__.tsx.template +13 -18
- package/src/py/fast-api/react/files/website/hooks/use__apiNameClassName__Client.tsx.template +13 -0
- package/src/py/fast-api/react/generator.js +35 -9
- package/src/py/fast-api/react/generator.js.map +1 -1
- package/src/py/project/generator.js +5 -0
- package/src/py/project/generator.js.map +1 -1
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +7 -9
- package/src/utils/files/http-api/common/constructs/src/core/http-api.ts.template +7 -9
- package/src/open-api/ts-client/__snapshots__/generator.spec.ts.snap +0 -7880
|
@@ -0,0 +1,1787 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { Tree } from '@nx/devkit';
|
|
6
|
+
import { createTreeUsingTsSolutionSetup } from '../../utils/test';
|
|
7
|
+
import { Spec } from '../utils/types';
|
|
8
|
+
import { openApiTsHooksGenerator } from './generator';
|
|
9
|
+
import {
|
|
10
|
+
expectTypeScriptToCompile,
|
|
11
|
+
TypeScriptVerifier,
|
|
12
|
+
} from '../ts-client/generator.utils.spec';
|
|
13
|
+
import { importTypeScriptModule } from '../../utils/js';
|
|
14
|
+
import { waitFor, render, fireEvent } from '@testing-library/react';
|
|
15
|
+
import {
|
|
16
|
+
QueryClient,
|
|
17
|
+
QueryClientProvider,
|
|
18
|
+
useQuery,
|
|
19
|
+
useMutation,
|
|
20
|
+
useInfiniteQuery,
|
|
21
|
+
UseQueryResult,
|
|
22
|
+
UseInfiniteQueryResult,
|
|
23
|
+
UseMutationResult,
|
|
24
|
+
} from '@tanstack/react-query';
|
|
25
|
+
import React from 'react';
|
|
26
|
+
import { Mock } from 'vitest';
|
|
27
|
+
|
|
28
|
+
describe('openApiTsHooksGenerator', () => {
|
|
29
|
+
let tree: Tree;
|
|
30
|
+
const title = 'TestApi';
|
|
31
|
+
const baseUrl = 'https://example.com';
|
|
32
|
+
const verifier = new TypeScriptVerifier(['@tanstack/react-query']);
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
tree = createTreeUsingTsSolutionSetup();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const validateTypeScript = (paths: string[]) => {
|
|
39
|
+
verifier.expectTypeScriptToCompile(tree, paths);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Helper function to create a wrapper component with QueryClientProvider
|
|
43
|
+
const createWrapper = () => {
|
|
44
|
+
// Create a new QueryClient for testing
|
|
45
|
+
const queryClient = new QueryClient({
|
|
46
|
+
defaultOptions: {
|
|
47
|
+
queries: {
|
|
48
|
+
retry: false,
|
|
49
|
+
},
|
|
50
|
+
mutations: {
|
|
51
|
+
retry: false,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return ({ children }: { children: React.ReactNode }) => (
|
|
57
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Helper function to configure the options proxy
|
|
62
|
+
const configureOptionsProxy = async (
|
|
63
|
+
clientModule: string,
|
|
64
|
+
optionsProxyModule: string,
|
|
65
|
+
mockFetch: Mock<any, any>,
|
|
66
|
+
) => {
|
|
67
|
+
// Dynamically import the generated modules
|
|
68
|
+
const { TestApi } = await importTypeScriptModule<any>(clientModule);
|
|
69
|
+
const { TestApiOptionsProxy } =
|
|
70
|
+
await importTypeScriptModule<any>(optionsProxyModule);
|
|
71
|
+
|
|
72
|
+
// Create client instance with mock fetch
|
|
73
|
+
const apiClient = new TestApi({ url: baseUrl, fetch: mockFetch });
|
|
74
|
+
|
|
75
|
+
// Create options proxy with the client
|
|
76
|
+
const optionsProxyInstance = new TestApiOptionsProxy({ client: apiClient });
|
|
77
|
+
|
|
78
|
+
return optionsProxyInstance;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Helper function to test a query hook
|
|
82
|
+
const renderQueryHook = async (
|
|
83
|
+
hookOptions: any,
|
|
84
|
+
): Promise<{
|
|
85
|
+
getLatestHookState: () => UseQueryResult<any>;
|
|
86
|
+
getHookStates: () => UseQueryResult<any>[];
|
|
87
|
+
}> => {
|
|
88
|
+
const Wrapper = createWrapper();
|
|
89
|
+
|
|
90
|
+
// Track the state of the hook on every render
|
|
91
|
+
const states: UseQueryResult<any>[] = [];
|
|
92
|
+
|
|
93
|
+
// Component that uses the query hook
|
|
94
|
+
const Component = () => {
|
|
95
|
+
const query = useQuery(hookOptions);
|
|
96
|
+
states.push(query);
|
|
97
|
+
return <div>Query Component</div>;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
render(
|
|
101
|
+
<Wrapper>
|
|
102
|
+
<Component />
|
|
103
|
+
</Wrapper>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Wait for the query to reach a terminal state (success or error)
|
|
107
|
+
await waitFor(() =>
|
|
108
|
+
expect(states[states.length - 1].isLoading).toBe(false),
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
// Return the latest state
|
|
113
|
+
getLatestHookState: () => states[states.length - 1],
|
|
114
|
+
getHookStates: () => states,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Helper function to test a mutation hook
|
|
119
|
+
const renderMutationHook = async (
|
|
120
|
+
hookOptions: any,
|
|
121
|
+
inputData: any,
|
|
122
|
+
): Promise<{
|
|
123
|
+
getLatestHookState: () => UseMutationResult<any>;
|
|
124
|
+
}> => {
|
|
125
|
+
const Wrapper = createWrapper();
|
|
126
|
+
|
|
127
|
+
// Track the state of the hook on every render
|
|
128
|
+
const states: UseMutationResult<any>[] = [];
|
|
129
|
+
|
|
130
|
+
// Component that uses the mutation hook
|
|
131
|
+
const Component = () => {
|
|
132
|
+
const mutation = useMutation(hookOptions);
|
|
133
|
+
states.push(mutation);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div>
|
|
137
|
+
<button onClick={() => mutation.mutate(inputData)}>Mutate</button>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const rendered = render(
|
|
143
|
+
<Wrapper>
|
|
144
|
+
<Component />
|
|
145
|
+
</Wrapper>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Execute the mutation
|
|
149
|
+
fireEvent.click(rendered.getByText('Mutate'));
|
|
150
|
+
|
|
151
|
+
// Wait for the mutation to reach a terminal state (success or error)
|
|
152
|
+
await waitFor(() =>
|
|
153
|
+
expect(states[states.length - 1].isPending).toBe(false),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
// Return the latest state
|
|
158
|
+
getLatestHookState: () => states[states.length - 1],
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Helper function to test an infinite query hook
|
|
163
|
+
const renderInfiniteQueryHook = async (
|
|
164
|
+
hookOptions: any,
|
|
165
|
+
): Promise<{
|
|
166
|
+
getLatestHookState: () => UseInfiniteQueryResult<any>;
|
|
167
|
+
fetchNextPage: () => void;
|
|
168
|
+
}> => {
|
|
169
|
+
const Wrapper = createWrapper();
|
|
170
|
+
|
|
171
|
+
// Track the state of the hook on every render
|
|
172
|
+
const states: UseInfiniteQueryResult<any>[] = [];
|
|
173
|
+
|
|
174
|
+
const Component = () => {
|
|
175
|
+
const query = useInfiniteQuery(hookOptions);
|
|
176
|
+
states.push(query);
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div>
|
|
180
|
+
<button onClick={() => query.fetchNextPage()}>Next Page</button>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const rendered = render(
|
|
186
|
+
<Wrapper>
|
|
187
|
+
<Component />
|
|
188
|
+
</Wrapper>,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
await waitFor(() =>
|
|
192
|
+
expect(states[states.length - 1].isLoading).toBe(false),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
// Return the latest state
|
|
197
|
+
getLatestHookState: () => states[states.length - 1],
|
|
198
|
+
fetchNextPage: () => {
|
|
199
|
+
fireEvent.click(rendered.getByText('Next Page'));
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
it('should generate an options proxy for a query operation', async () => {
|
|
205
|
+
const spec: Spec = {
|
|
206
|
+
openapi: '3.0.0',
|
|
207
|
+
info: { title, version: '1.0.0' },
|
|
208
|
+
paths: {
|
|
209
|
+
'/test': {
|
|
210
|
+
get: {
|
|
211
|
+
operationId: 'getTest',
|
|
212
|
+
description: 'Sends a test request!',
|
|
213
|
+
responses: {
|
|
214
|
+
'200': {
|
|
215
|
+
description: 'getTest',
|
|
216
|
+
content: {
|
|
217
|
+
'application/json': {
|
|
218
|
+
schema: {
|
|
219
|
+
type: 'object',
|
|
220
|
+
properties: {
|
|
221
|
+
string: { type: 'string' },
|
|
222
|
+
number: { type: 'number' },
|
|
223
|
+
integer: { type: 'integer' },
|
|
224
|
+
boolean: { type: 'boolean' },
|
|
225
|
+
'nullable-string': { type: 'string', nullable: true },
|
|
226
|
+
optionalNumber: { type: 'number' },
|
|
227
|
+
},
|
|
228
|
+
required: ['string', 'number', 'integer', 'boolean'],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
240
|
+
|
|
241
|
+
await openApiTsHooksGenerator(tree, {
|
|
242
|
+
openApiSpecPath: 'openapi.json',
|
|
243
|
+
outputPath: 'src/generated',
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
validateTypeScript([
|
|
247
|
+
'src/generated/client.gen.ts',
|
|
248
|
+
'src/generated/types.gen.ts',
|
|
249
|
+
'src/generated/options-proxy.gen.ts',
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
253
|
+
const optionsProxy = tree.read(
|
|
254
|
+
'src/generated/options-proxy.gen.ts',
|
|
255
|
+
'utf-8',
|
|
256
|
+
);
|
|
257
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
258
|
+
|
|
259
|
+
// Create mock fetch function
|
|
260
|
+
const mockFetch = vi.fn();
|
|
261
|
+
mockFetch.mockResolvedValue({
|
|
262
|
+
status: 200,
|
|
263
|
+
json: vi.fn().mockResolvedValue({
|
|
264
|
+
string: 'str',
|
|
265
|
+
number: 42.3,
|
|
266
|
+
integer: 33,
|
|
267
|
+
boolean: true,
|
|
268
|
+
'nullable-string': null,
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Configure the options proxy
|
|
273
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
274
|
+
client,
|
|
275
|
+
optionsProxy,
|
|
276
|
+
mockFetch,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Test the root queryKey method
|
|
280
|
+
const rootQueryKey = optionsProxyInstance.queryKey();
|
|
281
|
+
expect(rootQueryKey).toEqual(['TestApi']);
|
|
282
|
+
|
|
283
|
+
// Test the operation-specific queryKey method
|
|
284
|
+
const operationQueryKey = optionsProxyInstance.getTest.queryKey();
|
|
285
|
+
expect(operationQueryKey).toEqual(['TestApi', 'getTest']);
|
|
286
|
+
|
|
287
|
+
// Test the queryFilter method
|
|
288
|
+
const queryFilter = optionsProxyInstance.getTest.queryFilter();
|
|
289
|
+
expect(queryFilter.queryKey).toEqual(['TestApi', 'getTest']);
|
|
290
|
+
|
|
291
|
+
// Test queryFilter with additional options
|
|
292
|
+
const extendedFilter = optionsProxyInstance.getTest.queryFilter({
|
|
293
|
+
exact: true,
|
|
294
|
+
});
|
|
295
|
+
expect(extendedFilter).toEqual({
|
|
296
|
+
queryKey: ['TestApi', 'getTest'],
|
|
297
|
+
exact: true,
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Test the query hook
|
|
301
|
+
const { getLatestHookState } = await renderQueryHook(
|
|
302
|
+
optionsProxyInstance.getTest.queryOptions(),
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
// Verify the data is correct
|
|
306
|
+
expect(getLatestHookState().data).toEqual({
|
|
307
|
+
string: 'str',
|
|
308
|
+
number: 42.3,
|
|
309
|
+
integer: 33,
|
|
310
|
+
boolean: true,
|
|
311
|
+
nullableString: null,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Verify the fetch was called correctly
|
|
315
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
316
|
+
`${baseUrl}/test`,
|
|
317
|
+
expect.objectContaining({
|
|
318
|
+
method: 'GET',
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should generate an options proxy for a mutation operation', async () => {
|
|
324
|
+
const spec: Spec = {
|
|
325
|
+
openapi: '3.0.0',
|
|
326
|
+
info: { title, version: '1.0.0' },
|
|
327
|
+
paths: {
|
|
328
|
+
'/users': {
|
|
329
|
+
post: {
|
|
330
|
+
operationId: 'createUser',
|
|
331
|
+
description: 'Creates a new user',
|
|
332
|
+
requestBody: {
|
|
333
|
+
required: true,
|
|
334
|
+
content: {
|
|
335
|
+
'application/json': {
|
|
336
|
+
schema: {
|
|
337
|
+
type: 'object',
|
|
338
|
+
properties: {
|
|
339
|
+
name: { type: 'string' },
|
|
340
|
+
email: { type: 'string' },
|
|
341
|
+
age: { type: 'integer' },
|
|
342
|
+
},
|
|
343
|
+
required: ['name', 'email'],
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
responses: {
|
|
349
|
+
'201': {
|
|
350
|
+
description: 'User created successfully',
|
|
351
|
+
content: {
|
|
352
|
+
'application/json': {
|
|
353
|
+
schema: {
|
|
354
|
+
type: 'object',
|
|
355
|
+
properties: {
|
|
356
|
+
id: { type: 'string' },
|
|
357
|
+
name: { type: 'string' },
|
|
358
|
+
email: { type: 'string' },
|
|
359
|
+
age: { type: 'integer' },
|
|
360
|
+
createdAt: { type: 'string', format: 'date-time' },
|
|
361
|
+
},
|
|
362
|
+
required: ['id', 'name', 'email', 'createdAt'],
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
'400': {
|
|
368
|
+
description: 'Bad request',
|
|
369
|
+
content: {
|
|
370
|
+
'application/json': {
|
|
371
|
+
schema: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
properties: {
|
|
374
|
+
error: { type: 'string' },
|
|
375
|
+
},
|
|
376
|
+
required: ['error'],
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
388
|
+
|
|
389
|
+
await openApiTsHooksGenerator(tree, {
|
|
390
|
+
openApiSpecPath: 'openapi.json',
|
|
391
|
+
outputPath: 'src/generated',
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
validateTypeScript([
|
|
395
|
+
'src/generated/client.gen.ts',
|
|
396
|
+
'src/generated/types.gen.ts',
|
|
397
|
+
'src/generated/options-proxy.gen.ts',
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
401
|
+
const optionsProxy = tree.read(
|
|
402
|
+
'src/generated/options-proxy.gen.ts',
|
|
403
|
+
'utf-8',
|
|
404
|
+
);
|
|
405
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
406
|
+
|
|
407
|
+
// Create mock fetch function
|
|
408
|
+
const mockFetch = vi.fn();
|
|
409
|
+
mockFetch.mockResolvedValue({
|
|
410
|
+
status: 201,
|
|
411
|
+
json: vi.fn().mockResolvedValue({
|
|
412
|
+
id: '123',
|
|
413
|
+
name: 'John Doe',
|
|
414
|
+
email: 'john@example.com',
|
|
415
|
+
age: 30,
|
|
416
|
+
createdAt: '2023-01-01T12:00:00Z',
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Prepare test data
|
|
421
|
+
const userData = {
|
|
422
|
+
name: 'John Doe',
|
|
423
|
+
email: 'john@example.com',
|
|
424
|
+
age: 30,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// Configure the options proxy
|
|
428
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
429
|
+
client,
|
|
430
|
+
optionsProxy,
|
|
431
|
+
mockFetch,
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
// Test the mutation hook
|
|
435
|
+
const { getLatestHookState } = await renderMutationHook(
|
|
436
|
+
optionsProxyInstance.createUser.mutationOptions(),
|
|
437
|
+
userData,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
// Verify the data is correct
|
|
441
|
+
expect(getLatestHookState().data).toEqual({
|
|
442
|
+
id: '123',
|
|
443
|
+
name: 'John Doe',
|
|
444
|
+
email: 'john@example.com',
|
|
445
|
+
age: 30,
|
|
446
|
+
createdAt: new Date('2023-01-01T12:00:00Z'),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// Verify the fetch was called correctly
|
|
450
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
451
|
+
`${baseUrl}/users`,
|
|
452
|
+
expect.objectContaining({
|
|
453
|
+
method: 'POST',
|
|
454
|
+
body: JSON.stringify(userData),
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should handle query errors correctly', async () => {
|
|
460
|
+
const spec: Spec = {
|
|
461
|
+
openapi: '3.0.0',
|
|
462
|
+
info: { title, version: '1.0.0' },
|
|
463
|
+
paths: {
|
|
464
|
+
'/error': {
|
|
465
|
+
get: {
|
|
466
|
+
operationId: 'getError',
|
|
467
|
+
description: 'Returns an error',
|
|
468
|
+
responses: {
|
|
469
|
+
'200': {
|
|
470
|
+
description: 'Success response',
|
|
471
|
+
content: {
|
|
472
|
+
'application/json': {
|
|
473
|
+
schema: {
|
|
474
|
+
type: 'object',
|
|
475
|
+
properties: {
|
|
476
|
+
message: { type: 'string' },
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
'400': {
|
|
483
|
+
description: 'Error response',
|
|
484
|
+
content: {
|
|
485
|
+
'application/json': {
|
|
486
|
+
schema: {
|
|
487
|
+
type: 'object',
|
|
488
|
+
properties: {
|
|
489
|
+
error: { type: 'string' },
|
|
490
|
+
},
|
|
491
|
+
required: ['error'],
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
503
|
+
|
|
504
|
+
await openApiTsHooksGenerator(tree, {
|
|
505
|
+
openApiSpecPath: 'openapi.json',
|
|
506
|
+
outputPath: 'src/generated',
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
validateTypeScript([
|
|
510
|
+
'src/generated/client.gen.ts',
|
|
511
|
+
'src/generated/types.gen.ts',
|
|
512
|
+
'src/generated/options-proxy.gen.ts',
|
|
513
|
+
]);
|
|
514
|
+
|
|
515
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
516
|
+
const optionsProxy = tree.read(
|
|
517
|
+
'src/generated/options-proxy.gen.ts',
|
|
518
|
+
'utf-8',
|
|
519
|
+
);
|
|
520
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
521
|
+
|
|
522
|
+
// Create mock fetch function that returns an error
|
|
523
|
+
const mockFetch = vi.fn();
|
|
524
|
+
mockFetch.mockResolvedValue({
|
|
525
|
+
status: 400,
|
|
526
|
+
json: vi.fn().mockResolvedValue({
|
|
527
|
+
error: 'Bad request',
|
|
528
|
+
}),
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Configure the options proxy
|
|
532
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
533
|
+
client,
|
|
534
|
+
optionsProxy,
|
|
535
|
+
mockFetch,
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
// Test the query hook
|
|
539
|
+
const { getLatestHookState } = await renderQueryHook(
|
|
540
|
+
optionsProxyInstance.getError.queryOptions(),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// Verify the error state
|
|
544
|
+
expect(getLatestHookState().isError).toBe(true);
|
|
545
|
+
expect(getLatestHookState().error).toBeDefined();
|
|
546
|
+
expect(getLatestHookState().error).toMatchObject({
|
|
547
|
+
status: 400,
|
|
548
|
+
error: { error: 'Bad request' },
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Verify the fetch was called correctly
|
|
552
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
553
|
+
`${baseUrl}/error`,
|
|
554
|
+
expect.objectContaining({
|
|
555
|
+
method: 'GET',
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('should handle mutation errors correctly', async () => {
|
|
561
|
+
const spec: Spec = {
|
|
562
|
+
openapi: '3.0.0',
|
|
563
|
+
info: { title, version: '1.0.0' },
|
|
564
|
+
paths: {
|
|
565
|
+
'/users': {
|
|
566
|
+
post: {
|
|
567
|
+
operationId: 'createUser',
|
|
568
|
+
description: 'Creates a new user',
|
|
569
|
+
requestBody: {
|
|
570
|
+
required: true,
|
|
571
|
+
content: {
|
|
572
|
+
'application/json': {
|
|
573
|
+
schema: {
|
|
574
|
+
type: 'object',
|
|
575
|
+
properties: {
|
|
576
|
+
name: { type: 'string' },
|
|
577
|
+
email: { type: 'string' },
|
|
578
|
+
},
|
|
579
|
+
required: ['name', 'email'],
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
responses: {
|
|
585
|
+
'201': {
|
|
586
|
+
description: 'User created successfully',
|
|
587
|
+
content: {
|
|
588
|
+
'application/json': {
|
|
589
|
+
schema: {
|
|
590
|
+
type: 'object',
|
|
591
|
+
properties: {
|
|
592
|
+
id: { type: 'string' },
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
'400': {
|
|
599
|
+
description: 'Bad request',
|
|
600
|
+
content: {
|
|
601
|
+
'application/json': {
|
|
602
|
+
schema: {
|
|
603
|
+
type: 'object',
|
|
604
|
+
properties: {
|
|
605
|
+
error: { type: 'string' },
|
|
606
|
+
},
|
|
607
|
+
required: ['error'],
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
619
|
+
|
|
620
|
+
await openApiTsHooksGenerator(tree, {
|
|
621
|
+
openApiSpecPath: 'openapi.json',
|
|
622
|
+
outputPath: 'src/generated',
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
validateTypeScript([
|
|
626
|
+
'src/generated/client.gen.ts',
|
|
627
|
+
'src/generated/types.gen.ts',
|
|
628
|
+
'src/generated/options-proxy.gen.ts',
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
632
|
+
const optionsProxy = tree.read(
|
|
633
|
+
'src/generated/options-proxy.gen.ts',
|
|
634
|
+
'utf-8',
|
|
635
|
+
);
|
|
636
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
637
|
+
|
|
638
|
+
// Create mock fetch function that returns an error
|
|
639
|
+
const mockFetch = vi.fn();
|
|
640
|
+
mockFetch.mockResolvedValue({
|
|
641
|
+
status: 400,
|
|
642
|
+
json: vi.fn().mockResolvedValue({
|
|
643
|
+
error: 'Invalid email format',
|
|
644
|
+
}),
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Prepare test data
|
|
648
|
+
const userData = {
|
|
649
|
+
name: 'John Doe',
|
|
650
|
+
email: 'invalid-email',
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// Configure the options proxy
|
|
654
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
655
|
+
client,
|
|
656
|
+
optionsProxy,
|
|
657
|
+
mockFetch,
|
|
658
|
+
);
|
|
659
|
+
|
|
660
|
+
// Test the mutation hook
|
|
661
|
+
const { getLatestHookState } = await renderMutationHook(
|
|
662
|
+
optionsProxyInstance.createUser.mutationOptions(),
|
|
663
|
+
userData,
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// Verify the error state
|
|
667
|
+
expect(getLatestHookState().isError).toBe(true);
|
|
668
|
+
expect(getLatestHookState().error).toBeDefined();
|
|
669
|
+
expect(getLatestHookState().error).toMatchObject({
|
|
670
|
+
status: 400,
|
|
671
|
+
error: { error: 'Invalid email format' },
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Verify the fetch was called correctly
|
|
675
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
676
|
+
`${baseUrl}/users`,
|
|
677
|
+
expect.objectContaining({
|
|
678
|
+
method: 'POST',
|
|
679
|
+
body: JSON.stringify(userData),
|
|
680
|
+
}),
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
it('should generate an options proxy for a successful infinite query operation', async () => {
|
|
685
|
+
const spec: Spec = {
|
|
686
|
+
openapi: '3.0.0',
|
|
687
|
+
info: { title, version: '1.0.0' },
|
|
688
|
+
paths: {
|
|
689
|
+
'/items': {
|
|
690
|
+
get: {
|
|
691
|
+
operationId: 'getItems',
|
|
692
|
+
description: 'Gets a paginated list of items',
|
|
693
|
+
parameters: [
|
|
694
|
+
{
|
|
695
|
+
name: 'cursor',
|
|
696
|
+
in: 'query',
|
|
697
|
+
description: 'Pagination cursor',
|
|
698
|
+
required: false,
|
|
699
|
+
schema: {
|
|
700
|
+
type: 'string',
|
|
701
|
+
},
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
name: 'limit',
|
|
705
|
+
in: 'query',
|
|
706
|
+
description: 'Number of items to return',
|
|
707
|
+
required: false,
|
|
708
|
+
schema: {
|
|
709
|
+
type: 'integer',
|
|
710
|
+
default: 10,
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
],
|
|
714
|
+
responses: {
|
|
715
|
+
'200': {
|
|
716
|
+
description: 'List of items',
|
|
717
|
+
content: {
|
|
718
|
+
'application/json': {
|
|
719
|
+
schema: {
|
|
720
|
+
type: 'object',
|
|
721
|
+
properties: {
|
|
722
|
+
items: {
|
|
723
|
+
type: 'array',
|
|
724
|
+
items: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: {
|
|
727
|
+
id: { type: 'string' },
|
|
728
|
+
name: { type: 'string' },
|
|
729
|
+
description: { type: 'string' },
|
|
730
|
+
},
|
|
731
|
+
required: ['id', 'name'],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
nextCursor: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
required: ['items'],
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
750
|
+
|
|
751
|
+
await openApiTsHooksGenerator(tree, {
|
|
752
|
+
openApiSpecPath: 'openapi.json',
|
|
753
|
+
outputPath: 'src/generated',
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
validateTypeScript([
|
|
757
|
+
'src/generated/client.gen.ts',
|
|
758
|
+
'src/generated/types.gen.ts',
|
|
759
|
+
'src/generated/options-proxy.gen.ts',
|
|
760
|
+
]);
|
|
761
|
+
|
|
762
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
763
|
+
const optionsProxy = tree.read(
|
|
764
|
+
'src/generated/options-proxy.gen.ts',
|
|
765
|
+
'utf-8',
|
|
766
|
+
);
|
|
767
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
768
|
+
|
|
769
|
+
// Create mock fetch function for first page
|
|
770
|
+
const mockFetch = vi.fn();
|
|
771
|
+
|
|
772
|
+
// First call returns first page
|
|
773
|
+
mockFetch.mockResolvedValueOnce({
|
|
774
|
+
status: 200,
|
|
775
|
+
json: vi.fn().mockResolvedValue({
|
|
776
|
+
items: [
|
|
777
|
+
{ id: '1', name: 'Item 1', description: 'First item' },
|
|
778
|
+
{ id: '2', name: 'Item 2', description: 'Second item' },
|
|
779
|
+
],
|
|
780
|
+
nextCursor: 'next-page-cursor',
|
|
781
|
+
}),
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// Second call returns second page
|
|
785
|
+
mockFetch.mockResolvedValue({
|
|
786
|
+
status: 200,
|
|
787
|
+
json: vi.fn().mockResolvedValue({
|
|
788
|
+
items: [
|
|
789
|
+
{ id: '3', name: 'Item 3', description: 'Third item' },
|
|
790
|
+
{ id: '4', name: 'Item 4', description: 'Fourth item' },
|
|
791
|
+
],
|
|
792
|
+
}),
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Configure the options proxy
|
|
796
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
797
|
+
client,
|
|
798
|
+
optionsProxy,
|
|
799
|
+
mockFetch,
|
|
800
|
+
);
|
|
801
|
+
|
|
802
|
+
// Test the root queryKey method
|
|
803
|
+
const rootQueryKey = optionsProxyInstance.queryKey();
|
|
804
|
+
expect(rootQueryKey).toEqual(['TestApi']);
|
|
805
|
+
|
|
806
|
+
// Test the operation-specific queryKey method
|
|
807
|
+
const operationQueryKey = optionsProxyInstance.getItems.queryKey({});
|
|
808
|
+
expect(operationQueryKey).toEqual(['TestApi', 'getItems', {}]);
|
|
809
|
+
|
|
810
|
+
// Test the queryFilter method
|
|
811
|
+
const queryFilter = optionsProxyInstance.getItems.queryFilter({});
|
|
812
|
+
expect(queryFilter.queryKey).toEqual(['TestApi', 'getItems', {}]);
|
|
813
|
+
|
|
814
|
+
const extendedFilter = optionsProxyInstance.getItems.queryFilter(
|
|
815
|
+
{},
|
|
816
|
+
{ exact: true },
|
|
817
|
+
);
|
|
818
|
+
expect(extendedFilter).toEqual({
|
|
819
|
+
queryKey: ['TestApi', 'getItems', {}],
|
|
820
|
+
exact: true,
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// Test with parameters
|
|
824
|
+
const withParams = optionsProxyInstance.getItems.queryKey({
|
|
825
|
+
cursor: 'test-cursor',
|
|
826
|
+
limit: 20,
|
|
827
|
+
});
|
|
828
|
+
expect(withParams).toEqual([
|
|
829
|
+
'TestApi',
|
|
830
|
+
'getItems',
|
|
831
|
+
{ cursor: 'test-cursor', limit: 20 },
|
|
832
|
+
]);
|
|
833
|
+
|
|
834
|
+
const { getLatestHookState: infiniteQuery, fetchNextPage } =
|
|
835
|
+
await renderInfiniteQueryHook(
|
|
836
|
+
optionsProxyInstance.getItems.infiniteQueryOptions(
|
|
837
|
+
{},
|
|
838
|
+
{
|
|
839
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
840
|
+
},
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
// Verify the first page data is correct
|
|
845
|
+
expect(infiniteQuery().data.pages).toHaveLength(1);
|
|
846
|
+
expect(infiniteQuery().data.pages[0]).toEqual({
|
|
847
|
+
items: [
|
|
848
|
+
{ id: '1', name: 'Item 1', description: 'First item' },
|
|
849
|
+
{ id: '2', name: 'Item 2', description: 'Second item' },
|
|
850
|
+
],
|
|
851
|
+
nextCursor: 'next-page-cursor',
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
// Verify the fetch was called correctly for the first page
|
|
855
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
856
|
+
`${baseUrl}/items`,
|
|
857
|
+
expect.objectContaining({
|
|
858
|
+
method: 'GET',
|
|
859
|
+
}),
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
fetchNextPage();
|
|
863
|
+
|
|
864
|
+
// Verify both pages are now available
|
|
865
|
+
await waitFor(() => expect(infiniteQuery().data.pages).toHaveLength(2));
|
|
866
|
+
expect(infiniteQuery().data.pages[1]).toEqual({
|
|
867
|
+
items: [
|
|
868
|
+
{ id: '3', name: 'Item 3', description: 'Third item' },
|
|
869
|
+
{ id: '4', name: 'Item 4', description: 'Fourth item' },
|
|
870
|
+
],
|
|
871
|
+
});
|
|
872
|
+
// Verify there are no more pages as nextCursor is undefined
|
|
873
|
+
expect(infiniteQuery().hasNextPage).toBe(false);
|
|
874
|
+
|
|
875
|
+
// Verify the fetch was called correctly for the second page with the cursor
|
|
876
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
877
|
+
`${baseUrl}/items?cursor=next-page-cursor`,
|
|
878
|
+
expect.objectContaining({
|
|
879
|
+
method: 'GET',
|
|
880
|
+
}),
|
|
881
|
+
);
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it('should handle GET operation with x-mutation: true correctly', async () => {
|
|
885
|
+
const spec: Spec = {
|
|
886
|
+
openapi: '3.0.0',
|
|
887
|
+
info: { title, version: '1.0.0' },
|
|
888
|
+
paths: {
|
|
889
|
+
'/actions/trigger': {
|
|
890
|
+
get: {
|
|
891
|
+
...{
|
|
892
|
+
'x-mutation': true,
|
|
893
|
+
},
|
|
894
|
+
operationId: 'triggerAction',
|
|
895
|
+
description: 'Triggers an action via GET',
|
|
896
|
+
parameters: [
|
|
897
|
+
{
|
|
898
|
+
name: 'actionId',
|
|
899
|
+
in: 'query',
|
|
900
|
+
required: true,
|
|
901
|
+
schema: { type: 'string' },
|
|
902
|
+
},
|
|
903
|
+
],
|
|
904
|
+
responses: {
|
|
905
|
+
'200': {
|
|
906
|
+
description: 'Action triggered successfully',
|
|
907
|
+
content: {
|
|
908
|
+
'application/json': {
|
|
909
|
+
schema: {
|
|
910
|
+
type: 'object',
|
|
911
|
+
properties: {
|
|
912
|
+
success: { type: 'boolean' },
|
|
913
|
+
actionId: { type: 'string' },
|
|
914
|
+
},
|
|
915
|
+
required: ['success', 'actionId'],
|
|
916
|
+
},
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
927
|
+
|
|
928
|
+
await openApiTsHooksGenerator(tree, {
|
|
929
|
+
openApiSpecPath: 'openapi.json',
|
|
930
|
+
outputPath: 'src/generated',
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
validateTypeScript([
|
|
934
|
+
'src/generated/client.gen.ts',
|
|
935
|
+
'src/generated/types.gen.ts',
|
|
936
|
+
'src/generated/options-proxy.gen.ts',
|
|
937
|
+
]);
|
|
938
|
+
|
|
939
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
940
|
+
const optionsProxy = tree.read(
|
|
941
|
+
'src/generated/options-proxy.gen.ts',
|
|
942
|
+
'utf-8',
|
|
943
|
+
);
|
|
944
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
945
|
+
|
|
946
|
+
// Configure the options proxy
|
|
947
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
948
|
+
client,
|
|
949
|
+
optionsProxy,
|
|
950
|
+
vi.fn(),
|
|
951
|
+
);
|
|
952
|
+
|
|
953
|
+
// Verify that the operation has mutationKey and mutationOptions methods (not queryKey/queryOptions)
|
|
954
|
+
expect(optionsProxyInstance.triggerAction.mutationKey).toBeDefined();
|
|
955
|
+
expect(optionsProxyInstance.triggerAction.mutationOptions).toBeDefined();
|
|
956
|
+
expect(optionsProxyInstance.triggerAction.queryKey).toBeUndefined();
|
|
957
|
+
expect(optionsProxyInstance.triggerAction.queryOptions).toBeUndefined();
|
|
958
|
+
|
|
959
|
+
// Test the mutation key
|
|
960
|
+
const mutationKey = optionsProxyInstance.triggerAction.mutationKey();
|
|
961
|
+
expect(mutationKey).toEqual(['TestApi', 'triggerAction']);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('should handle POST operation with x-query: true correctly', async () => {
|
|
965
|
+
const spec: Spec = {
|
|
966
|
+
openapi: '3.0.0',
|
|
967
|
+
info: { title, version: '1.0.0' },
|
|
968
|
+
paths: {
|
|
969
|
+
'/data/search': {
|
|
970
|
+
post: {
|
|
971
|
+
...{
|
|
972
|
+
'x-query': true,
|
|
973
|
+
},
|
|
974
|
+
operationId: 'searchData',
|
|
975
|
+
description: 'Search data via POST',
|
|
976
|
+
requestBody: {
|
|
977
|
+
required: true,
|
|
978
|
+
content: {
|
|
979
|
+
'application/json': {
|
|
980
|
+
schema: {
|
|
981
|
+
type: 'object',
|
|
982
|
+
properties: {
|
|
983
|
+
query: { type: 'string' },
|
|
984
|
+
},
|
|
985
|
+
required: ['query'],
|
|
986
|
+
},
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
responses: {
|
|
991
|
+
'200': {
|
|
992
|
+
description: 'Search results',
|
|
993
|
+
content: {
|
|
994
|
+
'application/json': {
|
|
995
|
+
schema: {
|
|
996
|
+
type: 'object',
|
|
997
|
+
properties: {
|
|
998
|
+
results: {
|
|
999
|
+
type: 'array',
|
|
1000
|
+
items: {
|
|
1001
|
+
type: 'object',
|
|
1002
|
+
properties: {
|
|
1003
|
+
id: { type: 'string' },
|
|
1004
|
+
title: { type: 'string' },
|
|
1005
|
+
},
|
|
1006
|
+
required: ['id', 'title'],
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
total: { type: 'integer' },
|
|
1010
|
+
},
|
|
1011
|
+
required: ['results', 'total'],
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
1023
|
+
|
|
1024
|
+
await openApiTsHooksGenerator(tree, {
|
|
1025
|
+
openApiSpecPath: 'openapi.json',
|
|
1026
|
+
outputPath: 'src/generated',
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
validateTypeScript([
|
|
1030
|
+
'src/generated/client.gen.ts',
|
|
1031
|
+
'src/generated/types.gen.ts',
|
|
1032
|
+
'src/generated/options-proxy.gen.ts',
|
|
1033
|
+
]);
|
|
1034
|
+
|
|
1035
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
1036
|
+
const optionsProxy = tree.read(
|
|
1037
|
+
'src/generated/options-proxy.gen.ts',
|
|
1038
|
+
'utf-8',
|
|
1039
|
+
);
|
|
1040
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
1041
|
+
|
|
1042
|
+
// Configure the options proxy
|
|
1043
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
1044
|
+
client,
|
|
1045
|
+
optionsProxy,
|
|
1046
|
+
vi.fn(),
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
// Verify that the operation has queryKey and queryOptions methods (not mutationKey/mutationOptions)
|
|
1050
|
+
expect(optionsProxyInstance.searchData.queryKey).toBeDefined();
|
|
1051
|
+
expect(optionsProxyInstance.searchData.queryOptions).toBeDefined();
|
|
1052
|
+
expect(optionsProxyInstance.searchData.queryFilter).toBeDefined();
|
|
1053
|
+
expect(optionsProxyInstance.searchData.mutationKey).toBeUndefined();
|
|
1054
|
+
expect(optionsProxyInstance.searchData.mutationOptions).toBeUndefined();
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it('should handle infinite query with custom cursor parameter', async () => {
|
|
1058
|
+
const spec: Spec = {
|
|
1059
|
+
openapi: '3.0.0',
|
|
1060
|
+
info: { title, version: '1.0.0' },
|
|
1061
|
+
paths: {
|
|
1062
|
+
'/records': {
|
|
1063
|
+
get: {
|
|
1064
|
+
...{
|
|
1065
|
+
'x-cursor': 'nextToken',
|
|
1066
|
+
},
|
|
1067
|
+
operationId: 'listRecords',
|
|
1068
|
+
description: 'Lists records with nextToken pagination',
|
|
1069
|
+
parameters: [
|
|
1070
|
+
{
|
|
1071
|
+
name: 'limit',
|
|
1072
|
+
in: 'query',
|
|
1073
|
+
description: 'Number of records to return',
|
|
1074
|
+
required: false,
|
|
1075
|
+
schema: {
|
|
1076
|
+
type: 'integer',
|
|
1077
|
+
default: 10,
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
name: 'nextToken',
|
|
1082
|
+
in: 'query',
|
|
1083
|
+
description: 'Pagination token',
|
|
1084
|
+
required: false,
|
|
1085
|
+
schema: {
|
|
1086
|
+
type: 'string',
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
],
|
|
1090
|
+
responses: {
|
|
1091
|
+
'200': {
|
|
1092
|
+
description: 'List of records',
|
|
1093
|
+
content: {
|
|
1094
|
+
'application/json': {
|
|
1095
|
+
schema: {
|
|
1096
|
+
type: 'object',
|
|
1097
|
+
properties: {
|
|
1098
|
+
records: {
|
|
1099
|
+
type: 'array',
|
|
1100
|
+
items: {
|
|
1101
|
+
type: 'object',
|
|
1102
|
+
properties: {
|
|
1103
|
+
id: { type: 'string' },
|
|
1104
|
+
name: { type: 'string' },
|
|
1105
|
+
value: { type: 'number' },
|
|
1106
|
+
},
|
|
1107
|
+
required: ['id', 'name'],
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
nextToken: {
|
|
1111
|
+
type: 'string',
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
required: ['records'],
|
|
1115
|
+
},
|
|
1116
|
+
},
|
|
1117
|
+
},
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
},
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
1126
|
+
|
|
1127
|
+
await openApiTsHooksGenerator(tree, {
|
|
1128
|
+
openApiSpecPath: 'openapi.json',
|
|
1129
|
+
outputPath: 'src/generated',
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
validateTypeScript([
|
|
1133
|
+
'src/generated/client.gen.ts',
|
|
1134
|
+
'src/generated/types.gen.ts',
|
|
1135
|
+
'src/generated/options-proxy.gen.ts',
|
|
1136
|
+
]);
|
|
1137
|
+
|
|
1138
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
1139
|
+
const optionsProxy = tree.read(
|
|
1140
|
+
'src/generated/options-proxy.gen.ts',
|
|
1141
|
+
'utf-8',
|
|
1142
|
+
);
|
|
1143
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
1144
|
+
|
|
1145
|
+
// Create mock fetch function for first page
|
|
1146
|
+
const mockFetch = vi.fn();
|
|
1147
|
+
|
|
1148
|
+
// First call returns first page
|
|
1149
|
+
mockFetch.mockResolvedValueOnce({
|
|
1150
|
+
status: 200,
|
|
1151
|
+
json: vi.fn().mockResolvedValue({
|
|
1152
|
+
records: [
|
|
1153
|
+
{ id: '1', name: 'Record 1', value: 100 },
|
|
1154
|
+
{ id: '2', name: 'Record 2', value: 200 },
|
|
1155
|
+
],
|
|
1156
|
+
nextToken: 'next-page-token',
|
|
1157
|
+
}),
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
// Second call returns second page
|
|
1161
|
+
mockFetch.mockResolvedValue({
|
|
1162
|
+
status: 200,
|
|
1163
|
+
json: vi.fn().mockResolvedValue({
|
|
1164
|
+
records: [
|
|
1165
|
+
{ id: '3', name: 'Record 3', value: 300 },
|
|
1166
|
+
{ id: '4', name: 'Record 4', value: 400 },
|
|
1167
|
+
],
|
|
1168
|
+
// No nextToken in the response means end of pagination
|
|
1169
|
+
}),
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// Configure the options proxy
|
|
1173
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
1174
|
+
client,
|
|
1175
|
+
optionsProxy,
|
|
1176
|
+
mockFetch,
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
// Test the infinite query hook
|
|
1180
|
+
const { getLatestHookState: infiniteQuery, fetchNextPage } =
|
|
1181
|
+
await renderInfiniteQueryHook(
|
|
1182
|
+
optionsProxyInstance.listRecords.infiniteQueryOptions(
|
|
1183
|
+
{},
|
|
1184
|
+
{
|
|
1185
|
+
getNextPageParam: (lastPage) => lastPage.nextToken,
|
|
1186
|
+
},
|
|
1187
|
+
),
|
|
1188
|
+
);
|
|
1189
|
+
|
|
1190
|
+
// Verify the first page data is correct
|
|
1191
|
+
expect(infiniteQuery().data.pages).toHaveLength(1);
|
|
1192
|
+
expect(infiniteQuery().data.pages[0]).toEqual({
|
|
1193
|
+
records: [
|
|
1194
|
+
{ id: '1', name: 'Record 1', value: 100 },
|
|
1195
|
+
{ id: '2', name: 'Record 2', value: 200 },
|
|
1196
|
+
],
|
|
1197
|
+
nextToken: 'next-page-token',
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
// Verify the fetch was called correctly for the first page
|
|
1201
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
1202
|
+
`${baseUrl}/records`,
|
|
1203
|
+
expect.objectContaining({
|
|
1204
|
+
method: 'GET',
|
|
1205
|
+
}),
|
|
1206
|
+
);
|
|
1207
|
+
|
|
1208
|
+
fetchNextPage();
|
|
1209
|
+
|
|
1210
|
+
// Verify both pages are now available
|
|
1211
|
+
await waitFor(() => expect(infiniteQuery().data.pages).toHaveLength(2));
|
|
1212
|
+
expect(infiniteQuery().data.pages[1]).toEqual({
|
|
1213
|
+
records: [
|
|
1214
|
+
{ id: '3', name: 'Record 3', value: 300 },
|
|
1215
|
+
{ id: '4', name: 'Record 4', value: 400 },
|
|
1216
|
+
],
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
// Verify there are no more pages as nextToken is undefined
|
|
1220
|
+
expect(infiniteQuery().hasNextPage).toBe(false);
|
|
1221
|
+
|
|
1222
|
+
// Verify the fetch was called correctly for the second page with the nextToken
|
|
1223
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
1224
|
+
`${baseUrl}/records?nextToken=next-page-token`,
|
|
1225
|
+
expect.objectContaining({
|
|
1226
|
+
method: 'GET',
|
|
1227
|
+
}),
|
|
1228
|
+
);
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it('should handle streaming query operation correctly', async () => {
|
|
1232
|
+
const spec: Spec = {
|
|
1233
|
+
openapi: '3.0.0',
|
|
1234
|
+
info: { title, version: '1.0.0' },
|
|
1235
|
+
paths: {
|
|
1236
|
+
'/events/stream': {
|
|
1237
|
+
get: {
|
|
1238
|
+
...{
|
|
1239
|
+
'x-streaming': true,
|
|
1240
|
+
},
|
|
1241
|
+
operationId: 'streamEvents',
|
|
1242
|
+
description: 'Streams events as they occur',
|
|
1243
|
+
parameters: [
|
|
1244
|
+
{
|
|
1245
|
+
name: 'type',
|
|
1246
|
+
in: 'query',
|
|
1247
|
+
description: 'Event type filter',
|
|
1248
|
+
required: false,
|
|
1249
|
+
schema: {
|
|
1250
|
+
type: 'string',
|
|
1251
|
+
},
|
|
1252
|
+
},
|
|
1253
|
+
],
|
|
1254
|
+
responses: {
|
|
1255
|
+
'200': {
|
|
1256
|
+
description: 'Stream of events',
|
|
1257
|
+
content: {
|
|
1258
|
+
'application/json': {
|
|
1259
|
+
schema: {
|
|
1260
|
+
type: 'object',
|
|
1261
|
+
properties: {
|
|
1262
|
+
id: { type: 'string' },
|
|
1263
|
+
type: { type: 'string' },
|
|
1264
|
+
timestamp: { type: 'string', format: 'date-time' },
|
|
1265
|
+
data: {
|
|
1266
|
+
type: 'object',
|
|
1267
|
+
properties: {
|
|
1268
|
+
value: {
|
|
1269
|
+
type: 'integer',
|
|
1270
|
+
},
|
|
1271
|
+
},
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
required: ['id', 'type', 'timestamp'],
|
|
1275
|
+
},
|
|
1276
|
+
},
|
|
1277
|
+
},
|
|
1278
|
+
},
|
|
1279
|
+
},
|
|
1280
|
+
},
|
|
1281
|
+
},
|
|
1282
|
+
},
|
|
1283
|
+
};
|
|
1284
|
+
|
|
1285
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
1286
|
+
|
|
1287
|
+
await openApiTsHooksGenerator(tree, {
|
|
1288
|
+
openApiSpecPath: 'openapi.json',
|
|
1289
|
+
outputPath: 'src/generated',
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
validateTypeScript([
|
|
1293
|
+
'src/generated/client.gen.ts',
|
|
1294
|
+
'src/generated/types.gen.ts',
|
|
1295
|
+
'src/generated/options-proxy.gen.ts',
|
|
1296
|
+
]);
|
|
1297
|
+
|
|
1298
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
1299
|
+
const optionsProxy = tree.read(
|
|
1300
|
+
'src/generated/options-proxy.gen.ts',
|
|
1301
|
+
'utf-8',
|
|
1302
|
+
);
|
|
1303
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
1304
|
+
|
|
1305
|
+
// Create a mock async iterable for streaming events
|
|
1306
|
+
const mockEvents = [
|
|
1307
|
+
{
|
|
1308
|
+
id: '1',
|
|
1309
|
+
type: 'update',
|
|
1310
|
+
timestamp: '2023-01-01T12:00:00Z',
|
|
1311
|
+
data: { value: 10 },
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
id: '2',
|
|
1315
|
+
type: 'update',
|
|
1316
|
+
timestamp: '2023-01-01T12:01:00Z',
|
|
1317
|
+
data: { value: 20 },
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
id: '3',
|
|
1321
|
+
type: 'update',
|
|
1322
|
+
timestamp: '2023-01-01T12:02:00Z',
|
|
1323
|
+
data: { value: 30 },
|
|
1324
|
+
},
|
|
1325
|
+
];
|
|
1326
|
+
|
|
1327
|
+
// Create mock fetch function that returns an async iterable
|
|
1328
|
+
const mockFetch = vi.fn();
|
|
1329
|
+
const mockClient = {
|
|
1330
|
+
streamEvents: vi.fn().mockImplementation(async function* () {
|
|
1331
|
+
for (const event of mockEvents) {
|
|
1332
|
+
// Add a delay to ensure there's time for a rerender after each event
|
|
1333
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1334
|
+
yield event;
|
|
1335
|
+
}
|
|
1336
|
+
}),
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
// Configure the options proxy with our mock client
|
|
1340
|
+
const { TestApiOptionsProxy } =
|
|
1341
|
+
await importTypeScriptModule<any>(optionsProxy);
|
|
1342
|
+
const optionsProxyInstance = new TestApiOptionsProxy({
|
|
1343
|
+
client: mockClient,
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
// Create a mock QueryClient for testing
|
|
1347
|
+
const queryClient = new QueryClient({
|
|
1348
|
+
defaultOptions: {
|
|
1349
|
+
queries: { retry: false },
|
|
1350
|
+
},
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
// Test the query hook
|
|
1354
|
+
const { getLatestHookState, getHookStates: getStates } =
|
|
1355
|
+
await renderQueryHook(
|
|
1356
|
+
optionsProxyInstance.streamEvents.queryOptions(
|
|
1357
|
+
{ type: 'update' },
|
|
1358
|
+
{ client: queryClient },
|
|
1359
|
+
),
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
// Verify the data contains all streamed events
|
|
1363
|
+
await waitFor(() => expect(getLatestHookState().data).toEqual(mockEvents));
|
|
1364
|
+
|
|
1365
|
+
// Check that we had individual state updates for each streamed element
|
|
1366
|
+
const states = getStates();
|
|
1367
|
+
|
|
1368
|
+
// We should have at least initial loading state + one state per event
|
|
1369
|
+
expect(states.length).toBeGreaterThan(mockEvents.length);
|
|
1370
|
+
|
|
1371
|
+
// Find the first success state (after loading)
|
|
1372
|
+
const successStates = states.filter((state) => state.isSuccess);
|
|
1373
|
+
expect(successStates.length).toBeGreaterThanOrEqual(mockEvents.length);
|
|
1374
|
+
|
|
1375
|
+
// Verify that we got incremental updates
|
|
1376
|
+
for (
|
|
1377
|
+
let i = 0;
|
|
1378
|
+
i < Math.min(mockEvents.length, successStates.length);
|
|
1379
|
+
i++
|
|
1380
|
+
) {
|
|
1381
|
+
// Each state should have the events up to that point
|
|
1382
|
+
expect(successStates[i].data).toEqual(mockEvents.slice(0, i + 1));
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Verify the client method was called correctly
|
|
1386
|
+
expect(mockClient.streamEvents).toHaveBeenCalledWith({ type: 'update' });
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('should handle streaming mutation operation correctly', async () => {
|
|
1390
|
+
const spec: Spec = {
|
|
1391
|
+
openapi: '3.0.0',
|
|
1392
|
+
info: { title, version: '1.0.0' },
|
|
1393
|
+
paths: {
|
|
1394
|
+
'/uploads/stream': {
|
|
1395
|
+
post: {
|
|
1396
|
+
...{
|
|
1397
|
+
'x-streaming': true,
|
|
1398
|
+
},
|
|
1399
|
+
operationId: 'uploadStream',
|
|
1400
|
+
description: 'Upload a file with streaming progress',
|
|
1401
|
+
requestBody: {
|
|
1402
|
+
required: true,
|
|
1403
|
+
content: {
|
|
1404
|
+
'application/octet-stream': {
|
|
1405
|
+
schema: {
|
|
1406
|
+
type: 'string',
|
|
1407
|
+
format: 'binary',
|
|
1408
|
+
},
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
},
|
|
1412
|
+
responses: {
|
|
1413
|
+
'200': {
|
|
1414
|
+
description: 'Upload progress and result',
|
|
1415
|
+
content: {
|
|
1416
|
+
'application/json': {
|
|
1417
|
+
schema: {
|
|
1418
|
+
type: 'object',
|
|
1419
|
+
properties: {
|
|
1420
|
+
progress: { type: 'number' },
|
|
1421
|
+
bytesUploaded: { type: 'integer' },
|
|
1422
|
+
status: { type: 'string' },
|
|
1423
|
+
},
|
|
1424
|
+
required: ['progress', 'bytesUploaded', 'status'],
|
|
1425
|
+
},
|
|
1426
|
+
},
|
|
1427
|
+
},
|
|
1428
|
+
},
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
};
|
|
1434
|
+
|
|
1435
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
1436
|
+
|
|
1437
|
+
await openApiTsHooksGenerator(tree, {
|
|
1438
|
+
openApiSpecPath: 'openapi.json',
|
|
1439
|
+
outputPath: 'src/generated',
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
validateTypeScript([
|
|
1443
|
+
'src/generated/client.gen.ts',
|
|
1444
|
+
'src/generated/types.gen.ts',
|
|
1445
|
+
'src/generated/options-proxy.gen.ts',
|
|
1446
|
+
]);
|
|
1447
|
+
|
|
1448
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
1449
|
+
const optionsProxy = tree.read(
|
|
1450
|
+
'src/generated/options-proxy.gen.ts',
|
|
1451
|
+
'utf-8',
|
|
1452
|
+
);
|
|
1453
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
1454
|
+
|
|
1455
|
+
// Create a mock async iterable for streaming upload progress
|
|
1456
|
+
const mockProgress = [
|
|
1457
|
+
{ progress: 0.25, bytesUploaded: 256000, status: 'uploading' },
|
|
1458
|
+
{ progress: 0.5, bytesUploaded: 512000, status: 'uploading' },
|
|
1459
|
+
{ progress: 0.75, bytesUploaded: 768000, status: 'uploading' },
|
|
1460
|
+
{ progress: 1.0, bytesUploaded: 1024000, status: 'completed' },
|
|
1461
|
+
];
|
|
1462
|
+
|
|
1463
|
+
// Create mock client with streaming upload method
|
|
1464
|
+
const mockClient = {
|
|
1465
|
+
uploadStream: vi.fn().mockImplementation(async function* () {
|
|
1466
|
+
for (const progress of mockProgress) {
|
|
1467
|
+
yield progress;
|
|
1468
|
+
}
|
|
1469
|
+
}),
|
|
1470
|
+
};
|
|
1471
|
+
|
|
1472
|
+
// Configure the options proxy with our mock client
|
|
1473
|
+
const { TestApiOptionsProxy } =
|
|
1474
|
+
await importTypeScriptModule<any>(optionsProxy);
|
|
1475
|
+
const optionsProxyInstance = new TestApiOptionsProxy({
|
|
1476
|
+
client: mockClient,
|
|
1477
|
+
});
|
|
1478
|
+
|
|
1479
|
+
// Test the mutation hook
|
|
1480
|
+
const fileData = new Blob([new Uint8Array(1024000)]);
|
|
1481
|
+
const { getLatestHookState } = await renderMutationHook(
|
|
1482
|
+
optionsProxyInstance.uploadStream.mutationOptions(),
|
|
1483
|
+
fileData,
|
|
1484
|
+
);
|
|
1485
|
+
|
|
1486
|
+
// Verify the mutation was called with the file data
|
|
1487
|
+
expect(mockClient.uploadStream).toHaveBeenCalledWith(fileData);
|
|
1488
|
+
|
|
1489
|
+
// Verify the mutation returns an AsyncIterableIterator
|
|
1490
|
+
expect(getLatestHookState().data).toBeDefined();
|
|
1491
|
+
expect(typeof getLatestHookState().data[Symbol.asyncIterator]).toBe(
|
|
1492
|
+
'function',
|
|
1493
|
+
);
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
it('should handle streaming infinite query operation correctly', async () => {
|
|
1497
|
+
const spec: Spec = {
|
|
1498
|
+
openapi: '3.0.0',
|
|
1499
|
+
info: { title, version: '1.0.0' },
|
|
1500
|
+
paths: {
|
|
1501
|
+
'/logs': {
|
|
1502
|
+
get: {
|
|
1503
|
+
...{
|
|
1504
|
+
'x-streaming': true,
|
|
1505
|
+
},
|
|
1506
|
+
operationId: 'streamLogs',
|
|
1507
|
+
description: 'Stream logs with pagination',
|
|
1508
|
+
parameters: [
|
|
1509
|
+
{
|
|
1510
|
+
name: 'limit',
|
|
1511
|
+
in: 'query',
|
|
1512
|
+
description: 'Number of logs to return per page',
|
|
1513
|
+
required: false,
|
|
1514
|
+
schema: {
|
|
1515
|
+
type: 'integer',
|
|
1516
|
+
default: 10,
|
|
1517
|
+
},
|
|
1518
|
+
},
|
|
1519
|
+
{
|
|
1520
|
+
name: 'cursor',
|
|
1521
|
+
in: 'query',
|
|
1522
|
+
description: 'Pagination token',
|
|
1523
|
+
required: false,
|
|
1524
|
+
schema: {
|
|
1525
|
+
type: 'string',
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1528
|
+
],
|
|
1529
|
+
responses: {
|
|
1530
|
+
'200': {
|
|
1531
|
+
description: 'Stream of logs',
|
|
1532
|
+
content: {
|
|
1533
|
+
'application/json': {
|
|
1534
|
+
schema: {
|
|
1535
|
+
type: 'object',
|
|
1536
|
+
properties: {
|
|
1537
|
+
id: { type: 'string' },
|
|
1538
|
+
message: { type: 'string' },
|
|
1539
|
+
level: { type: 'string' },
|
|
1540
|
+
timestamp: { type: 'string', format: 'date-time' },
|
|
1541
|
+
},
|
|
1542
|
+
required: ['id', 'message', 'level', 'timestamp'],
|
|
1543
|
+
},
|
|
1544
|
+
},
|
|
1545
|
+
},
|
|
1546
|
+
},
|
|
1547
|
+
},
|
|
1548
|
+
},
|
|
1549
|
+
},
|
|
1550
|
+
},
|
|
1551
|
+
};
|
|
1552
|
+
|
|
1553
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
1554
|
+
|
|
1555
|
+
await openApiTsHooksGenerator(tree, {
|
|
1556
|
+
openApiSpecPath: 'openapi.json',
|
|
1557
|
+
outputPath: 'src/generated',
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
validateTypeScript([
|
|
1561
|
+
'src/generated/client.gen.ts',
|
|
1562
|
+
'src/generated/types.gen.ts',
|
|
1563
|
+
'src/generated/options-proxy.gen.ts',
|
|
1564
|
+
]);
|
|
1565
|
+
|
|
1566
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
1567
|
+
const optionsProxy = tree.read(
|
|
1568
|
+
'src/generated/options-proxy.gen.ts',
|
|
1569
|
+
'utf-8',
|
|
1570
|
+
);
|
|
1571
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
1572
|
+
|
|
1573
|
+
// Create mock streaming data for first page
|
|
1574
|
+
const mockFirstPageLogs = [
|
|
1575
|
+
{
|
|
1576
|
+
id: '1',
|
|
1577
|
+
message: 'Starting application',
|
|
1578
|
+
level: 'info',
|
|
1579
|
+
timestamp: '2023-01-01T12:00:00Z',
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
id: '2',
|
|
1583
|
+
message: 'Connected to database',
|
|
1584
|
+
level: 'info',
|
|
1585
|
+
timestamp: '2023-01-01T12:00:01Z',
|
|
1586
|
+
},
|
|
1587
|
+
];
|
|
1588
|
+
|
|
1589
|
+
// Create mock streaming data for second page
|
|
1590
|
+
const mockSecondPageLogs = [
|
|
1591
|
+
{
|
|
1592
|
+
id: '3',
|
|
1593
|
+
message: 'User login',
|
|
1594
|
+
level: 'info',
|
|
1595
|
+
timestamp: '2023-01-01T12:00:02Z',
|
|
1596
|
+
},
|
|
1597
|
+
{
|
|
1598
|
+
id: '4',
|
|
1599
|
+
message: 'API request received',
|
|
1600
|
+
level: 'debug',
|
|
1601
|
+
timestamp: '2023-01-01T12:00:03Z',
|
|
1602
|
+
},
|
|
1603
|
+
];
|
|
1604
|
+
|
|
1605
|
+
// Create mock client with streaming methods
|
|
1606
|
+
const mockClient = {
|
|
1607
|
+
streamLogs: vi
|
|
1608
|
+
.fn()
|
|
1609
|
+
// First call returns first page with nextToken
|
|
1610
|
+
.mockImplementationOnce(async function* () {
|
|
1611
|
+
for (const log of mockFirstPageLogs) {
|
|
1612
|
+
yield log;
|
|
1613
|
+
}
|
|
1614
|
+
})
|
|
1615
|
+
// Second call returns second page without nextToken
|
|
1616
|
+
.mockImplementationOnce(async function* () {
|
|
1617
|
+
for (const log of mockSecondPageLogs) {
|
|
1618
|
+
yield log;
|
|
1619
|
+
}
|
|
1620
|
+
}),
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
// Configure the options proxy with our mock client
|
|
1624
|
+
const { TestApiOptionsProxy } =
|
|
1625
|
+
await importTypeScriptModule<any>(optionsProxy);
|
|
1626
|
+
const optionsProxyInstance = new TestApiOptionsProxy({
|
|
1627
|
+
client: mockClient,
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
// Test the infinite query hook
|
|
1631
|
+
const { getLatestHookState: infiniteQuery, fetchNextPage } =
|
|
1632
|
+
await renderInfiniteQueryHook(
|
|
1633
|
+
optionsProxyInstance.streamLogs.infiniteQueryOptions(
|
|
1634
|
+
{},
|
|
1635
|
+
{
|
|
1636
|
+
getNextPageParam: (lastPage) => lastPage[lastPage.length - 1].id,
|
|
1637
|
+
},
|
|
1638
|
+
),
|
|
1639
|
+
);
|
|
1640
|
+
|
|
1641
|
+
// Verify the first page data is correct
|
|
1642
|
+
expect(infiniteQuery().data.pages).toHaveLength(1);
|
|
1643
|
+
expect(infiniteQuery().data.pages[0]).toEqual(mockFirstPageLogs);
|
|
1644
|
+
|
|
1645
|
+
// Verify the client method was called correctly for the first page
|
|
1646
|
+
expect(mockClient.streamLogs).toHaveBeenCalledWith({});
|
|
1647
|
+
|
|
1648
|
+
// Fetch the next page
|
|
1649
|
+
fetchNextPage();
|
|
1650
|
+
|
|
1651
|
+
// Verify both pages are now available
|
|
1652
|
+
await waitFor(() => expect(infiniteQuery().data.pages).toHaveLength(2));
|
|
1653
|
+
expect(infiniteQuery().data.pages[1]).toEqual(mockSecondPageLogs);
|
|
1654
|
+
|
|
1655
|
+
// Verify the client method was called correctly for the second page
|
|
1656
|
+
expect(mockClient.streamLogs).toHaveBeenCalledWith({ cursor: '2' });
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
it('should handle infinite query errors correctly', async () => {
|
|
1660
|
+
const spec: Spec = {
|
|
1661
|
+
openapi: '3.0.0',
|
|
1662
|
+
info: { title, version: '1.0.0' },
|
|
1663
|
+
paths: {
|
|
1664
|
+
'/items': {
|
|
1665
|
+
get: {
|
|
1666
|
+
operationId: 'getItems',
|
|
1667
|
+
description: 'Gets a paginated list of items',
|
|
1668
|
+
parameters: [
|
|
1669
|
+
{
|
|
1670
|
+
name: 'cursor',
|
|
1671
|
+
in: 'query',
|
|
1672
|
+
description: 'Pagination cursor',
|
|
1673
|
+
required: false,
|
|
1674
|
+
schema: {
|
|
1675
|
+
type: 'string',
|
|
1676
|
+
},
|
|
1677
|
+
},
|
|
1678
|
+
],
|
|
1679
|
+
responses: {
|
|
1680
|
+
'200': {
|
|
1681
|
+
description: 'List of items',
|
|
1682
|
+
content: {
|
|
1683
|
+
'application/json': {
|
|
1684
|
+
schema: {
|
|
1685
|
+
type: 'object',
|
|
1686
|
+
properties: {
|
|
1687
|
+
items: {
|
|
1688
|
+
type: 'array',
|
|
1689
|
+
items: {
|
|
1690
|
+
type: 'object',
|
|
1691
|
+
properties: {
|
|
1692
|
+
id: { type: 'string' },
|
|
1693
|
+
name: { type: 'string' },
|
|
1694
|
+
},
|
|
1695
|
+
required: ['id', 'name'],
|
|
1696
|
+
},
|
|
1697
|
+
},
|
|
1698
|
+
nextCursor: { type: 'string', nullable: true },
|
|
1699
|
+
},
|
|
1700
|
+
required: ['items'],
|
|
1701
|
+
},
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
},
|
|
1705
|
+
'400': {
|
|
1706
|
+
description: 'Bad request',
|
|
1707
|
+
content: {
|
|
1708
|
+
'application/json': {
|
|
1709
|
+
schema: {
|
|
1710
|
+
type: 'object',
|
|
1711
|
+
properties: {
|
|
1712
|
+
error: { type: 'string' },
|
|
1713
|
+
},
|
|
1714
|
+
required: ['error'],
|
|
1715
|
+
},
|
|
1716
|
+
},
|
|
1717
|
+
},
|
|
1718
|
+
},
|
|
1719
|
+
},
|
|
1720
|
+
},
|
|
1721
|
+
},
|
|
1722
|
+
},
|
|
1723
|
+
};
|
|
1724
|
+
|
|
1725
|
+
tree.write('openapi.json', JSON.stringify(spec));
|
|
1726
|
+
|
|
1727
|
+
await openApiTsHooksGenerator(tree, {
|
|
1728
|
+
openApiSpecPath: 'openapi.json',
|
|
1729
|
+
outputPath: 'src/generated',
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
validateTypeScript([
|
|
1733
|
+
'src/generated/client.gen.ts',
|
|
1734
|
+
'src/generated/types.gen.ts',
|
|
1735
|
+
'src/generated/options-proxy.gen.ts',
|
|
1736
|
+
]);
|
|
1737
|
+
|
|
1738
|
+
const client = tree.read('src/generated/client.gen.ts', 'utf-8');
|
|
1739
|
+
const optionsProxy = tree.read(
|
|
1740
|
+
'src/generated/options-proxy.gen.ts',
|
|
1741
|
+
'utf-8',
|
|
1742
|
+
);
|
|
1743
|
+
expect(optionsProxy).toMatchSnapshot();
|
|
1744
|
+
|
|
1745
|
+
// Create mock fetch function that returns an error
|
|
1746
|
+
const mockFetch = vi.fn();
|
|
1747
|
+
mockFetch.mockResolvedValue({
|
|
1748
|
+
status: 400,
|
|
1749
|
+
json: vi.fn().mockResolvedValue({
|
|
1750
|
+
error: 'Invalid cursor format',
|
|
1751
|
+
}),
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
// Configure the options proxy
|
|
1755
|
+
const optionsProxyInstance = await configureOptionsProxy(
|
|
1756
|
+
client,
|
|
1757
|
+
optionsProxy,
|
|
1758
|
+
mockFetch,
|
|
1759
|
+
);
|
|
1760
|
+
|
|
1761
|
+
// Test the infinite query hook with an invalid cursor
|
|
1762
|
+
const { getLatestHookState: infiniteQuery } = await renderInfiniteQueryHook(
|
|
1763
|
+
optionsProxyInstance.getItems.infiniteQueryOptions(
|
|
1764
|
+
{},
|
|
1765
|
+
{
|
|
1766
|
+
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
|
1767
|
+
},
|
|
1768
|
+
),
|
|
1769
|
+
);
|
|
1770
|
+
|
|
1771
|
+
// Verify the error state
|
|
1772
|
+
expect(infiniteQuery().isError).toBe(true);
|
|
1773
|
+
expect(infiniteQuery().error).toBeDefined();
|
|
1774
|
+
expect(infiniteQuery().error).toMatchObject({
|
|
1775
|
+
status: 400,
|
|
1776
|
+
error: { error: 'Invalid cursor format' },
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
// Verify the fetch was called correctly
|
|
1780
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
1781
|
+
`${baseUrl}/items`,
|
|
1782
|
+
expect.objectContaining({
|
|
1783
|
+
method: 'GET',
|
|
1784
|
+
}),
|
|
1785
|
+
);
|
|
1786
|
+
});
|
|
1787
|
+
});
|