@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.
Files changed (34) hide show
  1. package/package.json +2 -2
  2. package/src/open-api/ts-client/__snapshots__/generator.additional-properties.spec.ts.snap +2236 -0
  3. package/src/open-api/ts-client/__snapshots__/generator.complex-types.spec.ts.snap +2307 -0
  4. package/src/open-api/ts-client/__snapshots__/generator.composite-types.spec.ts.snap +1495 -0
  5. package/src/open-api/ts-client/__snapshots__/generator.primitive-types.spec.ts.snap +1470 -0
  6. package/src/open-api/ts-client/__snapshots__/generator.request.spec.ts.snap +1138 -0
  7. package/src/open-api/ts-client/__snapshots__/generator.response.spec.ts.snap +732 -0
  8. package/src/open-api/ts-client/__snapshots__/generator.tags.spec.ts.snap +743 -0
  9. package/src/open-api/ts-client/files/client.gen.ts.template +52 -15
  10. package/src/open-api/ts-client/files/types.gen.ts.template +5 -0
  11. package/src/open-api/ts-hooks/__snapshots__/generator.spec.tsx.snap +1092 -0
  12. package/src/open-api/ts-hooks/files/options-proxy.gen.ts.template +210 -0
  13. package/src/open-api/ts-hooks/generator.d.ts +5 -0
  14. package/src/open-api/ts-hooks/generator.js +15 -2
  15. package/src/open-api/ts-hooks/generator.js.map +1 -1
  16. package/src/open-api/ts-hooks/generator.spec.tsx +1787 -0
  17. package/src/open-api/utils/codegen-data/types.d.ts +25 -0
  18. package/src/open-api/utils/codegen-data/types.js +26 -1
  19. package/src/open-api/utils/codegen-data/types.js.map +1 -1
  20. package/src/open-api/utils/codegen-data.js +187 -79
  21. package/src/open-api/utils/codegen-data.js.map +1 -1
  22. package/src/open-api/utils/normalise.js +11 -1
  23. package/src/open-api/utils/normalise.js.map +1 -1
  24. package/src/py/fast-api/react/__snapshots__/generator.spec.ts.snap +120 -10
  25. package/src/py/fast-api/react/files/website/components/__apiNameClassName__Provider.tsx.template +40 -0
  26. package/src/py/fast-api/react/files/website/hooks/use__apiNameClassName__.tsx.template +13 -18
  27. package/src/py/fast-api/react/files/website/hooks/use__apiNameClassName__Client.tsx.template +13 -0
  28. package/src/py/fast-api/react/generator.js +35 -9
  29. package/src/py/fast-api/react/generator.js.map +1 -1
  30. package/src/py/project/generator.js +5 -0
  31. package/src/py/project/generator.js.map +1 -1
  32. package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +7 -9
  33. package/src/utils/files/http-api/common/constructs/src/core/http-api.ts.template +7 -9
  34. 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
+ });