@codewithagents/openapi-react-query 3.3.1 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +40 -21
- package/__fixtures__/specs/task-hooks.json +18 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +27 -7
- package/dist/config.js.map +1 -1
- package/dist/plugins/hooks.d.ts.map +1 -1
- package/dist/plugins/hooks.js +37 -4
- package/dist/plugins/hooks.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/compat-matrix.test.ts +5 -2
- package/src/__tests__/config.test.ts +43 -11
- package/src/__tests__/hooks.property.test.ts +40 -19
- package/src/__tests__/hooks.test.ts +400 -55
- package/src/__tests__/prettier-output.test.ts +1 -1
- package/src/__tests__/test-utils.test.ts +1 -1
- package/src/config.ts +37 -11
- package/src/plugins/hooks.ts +98 -28
- package/stryker.config.json +1 -5
|
@@ -17,7 +17,7 @@ describe('generateHooks — task-hooks.json fixture', () => {
|
|
|
17
17
|
|
|
18
18
|
it('contains auto-generated header comment', () => {
|
|
19
19
|
expect(content).toContain(
|
|
20
|
-
'// This file is auto-generated by @codewithagents/openapi-react-query — do not edit'
|
|
20
|
+
'// This file is auto-generated by @codewithagents/openapi-react-query — do not edit'
|
|
21
21
|
)
|
|
22
22
|
})
|
|
23
23
|
|
|
@@ -40,8 +40,8 @@ describe('generateHooks — task-hooks.json fixture', () => {
|
|
|
40
40
|
it('generates taskKeys factory with all(), list(), and detail() entries', () => {
|
|
41
41
|
expect(content).toContain('export const taskKeys')
|
|
42
42
|
expect(content).toContain("all: () => ['tasks'] as const")
|
|
43
|
-
expect(content).toContain(
|
|
44
|
-
expect(content).toContain(
|
|
43
|
+
expect(content).toContain('list:')
|
|
44
|
+
expect(content).toContain('detail:')
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
it('useListTasks uses taskKeys.list(params) as queryKey', () => {
|
|
@@ -269,13 +269,19 @@ describe('generateHooks — Bug #53: key factory includes query params when oper
|
|
|
269
269
|
})
|
|
270
270
|
|
|
271
271
|
it('key factory detail entry includes required params when path + required query params', () => {
|
|
272
|
-
const { content } = generateHooks(specWithPathAndRequiredQueryParams, {
|
|
272
|
+
const { content } = generateHooks(specWithPathAndRequiredQueryParams, {
|
|
273
|
+
staleTime: 0,
|
|
274
|
+
gcTime: 0,
|
|
275
|
+
})
|
|
273
276
|
// Key factory should accept required params (no ?)
|
|
274
277
|
expect(content).toContain('(id: string, params: Parameters<typeof getReport>[1])')
|
|
275
278
|
})
|
|
276
279
|
|
|
277
280
|
it('hook marks params as required when path param + required query param', () => {
|
|
278
|
-
const { content } = generateHooks(specWithPathAndRequiredQueryParams, {
|
|
281
|
+
const { content } = generateHooks(specWithPathAndRequiredQueryParams, {
|
|
282
|
+
staleTime: 0,
|
|
283
|
+
gcTime: 0,
|
|
284
|
+
})
|
|
279
285
|
expect(content).toContain('params: Parameters<typeof getReport>[1]')
|
|
280
286
|
expect(content).not.toContain('params?: Parameters<typeof getReport>[1]')
|
|
281
287
|
})
|
|
@@ -284,25 +290,43 @@ describe('generateHooks — Bug #53: key factory includes query params when oper
|
|
|
284
290
|
// ── Bug #3: Mutation hook argument shapes ───────────────────────────────────────
|
|
285
291
|
|
|
286
292
|
describe('generateHooks — Bug #54: mutation hook handles all argument shapes', () => {
|
|
287
|
-
const makeSpec = (
|
|
293
|
+
const makeSpec = (
|
|
294
|
+
pathSegment: string,
|
|
295
|
+
method: string,
|
|
296
|
+
hasBody: boolean,
|
|
297
|
+
queryParams: Array<{ name: string; required?: boolean }>
|
|
298
|
+
): OpenAPIV3_1.Document => ({
|
|
288
299
|
openapi: '3.1.0',
|
|
289
300
|
info: { title: 'Test', version: '1.0.0' },
|
|
290
301
|
paths: {
|
|
291
302
|
[pathSegment]: {
|
|
292
303
|
[method]: {
|
|
293
304
|
operationId: `${method}Op`,
|
|
294
|
-
parameters: queryParams.map((p) => ({
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
305
|
+
parameters: queryParams.map((p) => ({
|
|
306
|
+
name: p.name,
|
|
307
|
+
in: 'query' as const,
|
|
308
|
+
required: p.required,
|
|
309
|
+
schema: { type: 'string' as const },
|
|
310
|
+
})),
|
|
311
|
+
...(hasBody
|
|
312
|
+
? {
|
|
313
|
+
requestBody: {
|
|
314
|
+
required: true,
|
|
315
|
+
content: {
|
|
316
|
+
'application/json': { schema: { $ref: '#/components/schemas/Body' } },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
: {}),
|
|
301
321
|
responses: { '200': { description: 'ok' } },
|
|
302
322
|
},
|
|
303
323
|
},
|
|
304
324
|
},
|
|
305
|
-
components: {
|
|
325
|
+
components: {
|
|
326
|
+
schemas: {
|
|
327
|
+
Body: { type: 'object' as const, properties: { name: { type: 'string' as const } } },
|
|
328
|
+
},
|
|
329
|
+
},
|
|
306
330
|
})
|
|
307
331
|
|
|
308
332
|
it('mutation with query params only (no body, no path): variables type is Parameters[0], mutationFn is (vars) => fn(vars)', () => {
|
|
@@ -344,7 +368,9 @@ describe('generateHooks — Bug #54: mutation hook handles all argument shapes',
|
|
|
344
368
|
},
|
|
345
369
|
},
|
|
346
370
|
},
|
|
347
|
-
components: {
|
|
371
|
+
components: {
|
|
372
|
+
schemas: { Body: { type: 'object', properties: { name: { type: 'string' } } } },
|
|
373
|
+
},
|
|
348
374
|
}
|
|
349
375
|
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0 })
|
|
350
376
|
// Variables includes id, body, and params
|
|
@@ -391,13 +417,17 @@ describe('generateHooks — Bug #54: mutation hook handles all argument shapes',
|
|
|
391
417
|
operationId: 'createTask',
|
|
392
418
|
requestBody: {
|
|
393
419
|
required: true,
|
|
394
|
-
content: {
|
|
420
|
+
content: {
|
|
421
|
+
'application/json': { schema: { $ref: '#/components/schemas/CreateTask' } },
|
|
422
|
+
},
|
|
395
423
|
},
|
|
396
424
|
responses: { '201': { description: 'created' } },
|
|
397
425
|
},
|
|
398
426
|
},
|
|
399
427
|
},
|
|
400
|
-
components: {
|
|
428
|
+
components: {
|
|
429
|
+
schemas: { CreateTask: { type: 'object', properties: { title: { type: 'string' } } } },
|
|
430
|
+
},
|
|
401
431
|
}
|
|
402
432
|
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0 })
|
|
403
433
|
expect(content).toContain('Parameters<typeof createTask>[0]')
|
|
@@ -414,7 +444,14 @@ describe('generateHooks — tag name to camelCase key factory', () => {
|
|
|
414
444
|
[`/${pathSegment}`]: {
|
|
415
445
|
get: {
|
|
416
446
|
operationId: `list${pathSegment}`,
|
|
417
|
-
responses: {
|
|
447
|
+
responses: {
|
|
448
|
+
'200': {
|
|
449
|
+
description: 'ok',
|
|
450
|
+
content: {
|
|
451
|
+
'application/json': { schema: { type: 'array', items: { type: 'object' } } },
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
},
|
|
418
455
|
},
|
|
419
456
|
},
|
|
420
457
|
},
|
|
@@ -514,6 +551,130 @@ describe('generateHooks — Feature #60: suspense query variants', () => {
|
|
|
514
551
|
const { content } = generateHooks(suspenseSpec, { staleTime: 0, gcTime: 0, suspense: false })
|
|
515
552
|
expect(content).not.toContain('useSuspenseQuery')
|
|
516
553
|
})
|
|
554
|
+
|
|
555
|
+
it('suspense hook with 1 path param + optional query param generates params? signature', () => {
|
|
556
|
+
// Covers the `pathParams.length === 1 && hasQueryParams` branch (hooks.ts line 309-310)
|
|
557
|
+
// and the `paramsRequired === false` (false) branch at line 296 → paramsToken = 'params?'
|
|
558
|
+
const spec: OpenAPIV3_1.Document = {
|
|
559
|
+
openapi: '3.1.0',
|
|
560
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
561
|
+
paths: {
|
|
562
|
+
'/repos/{repoId}/commits': {
|
|
563
|
+
get: {
|
|
564
|
+
operationId: 'listRepoCommits',
|
|
565
|
+
parameters: [
|
|
566
|
+
{ name: 'repoId', in: 'path', required: true, schema: { type: 'string' } },
|
|
567
|
+
{ name: 'branch', in: 'query', required: false, schema: { type: 'string' } },
|
|
568
|
+
],
|
|
569
|
+
responses: { '200': { description: 'ok' } },
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
}
|
|
574
|
+
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
|
|
575
|
+
expect(content).toContain('export function useSuspenseListRepoCommits')
|
|
576
|
+
expect(content).toContain('repoId: string')
|
|
577
|
+
expect(content).toContain('params?:')
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it('suspense hook with 1 path param + required query param generates params (non-optional) signature', () => {
|
|
581
|
+
// Covers the `paramsRequired === true` (true) branch at line 296 → paramsToken = 'params'
|
|
582
|
+
const spec: OpenAPIV3_1.Document = {
|
|
583
|
+
openapi: '3.1.0',
|
|
584
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
585
|
+
paths: {
|
|
586
|
+
'/search/{category}': {
|
|
587
|
+
get: {
|
|
588
|
+
operationId: 'searchCategory',
|
|
589
|
+
parameters: [
|
|
590
|
+
{ name: 'category', in: 'path', required: true, schema: { type: 'string' } },
|
|
591
|
+
{ name: 'q', in: 'query', required: true, schema: { type: 'string' } },
|
|
592
|
+
],
|
|
593
|
+
responses: { '200': { description: 'ok' } },
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
}
|
|
598
|
+
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
|
|
599
|
+
expect(content).toContain('export function useSuspenseSearchCategory')
|
|
600
|
+
expect(content).toContain('category: string')
|
|
601
|
+
// Required query param → non-optional params token (no ?)
|
|
602
|
+
expect(content).toContain('params: Parameters')
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it('suspense hook with 2 path params builds multi-param queryKey call', () => {
|
|
606
|
+
// Covers the multi-path-param (pathParams.length > 1) queryKey branch in suspense generation
|
|
607
|
+
const spec: OpenAPIV3_1.Document = {
|
|
608
|
+
openapi: '3.1.0',
|
|
609
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
610
|
+
paths: {
|
|
611
|
+
'/orgs/{orgId}/repos/{repoId}': {
|
|
612
|
+
get: {
|
|
613
|
+
operationId: 'getOrgRepo',
|
|
614
|
+
parameters: [
|
|
615
|
+
{ name: 'orgId', in: 'path', required: true, schema: { type: 'string' } },
|
|
616
|
+
{ name: 'repoId', in: 'path', required: true, schema: { type: 'string' } },
|
|
617
|
+
],
|
|
618
|
+
responses: { '200': { description: 'ok' } },
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
}
|
|
623
|
+
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
|
|
624
|
+
expect(content).toContain('export function useSuspenseGetOrgRepo')
|
|
625
|
+
// Both path params appear in the function signature
|
|
626
|
+
expect(content).toContain('orgId: string')
|
|
627
|
+
expect(content).toContain('repoId: string')
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
it('suspense hook with 2 path params + query params builds correct queryKey', () => {
|
|
631
|
+
// Covers the else branch: pathParams.length > 1 && hasQueryParams
|
|
632
|
+
const spec: OpenAPIV3_1.Document = {
|
|
633
|
+
openapi: '3.1.0',
|
|
634
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
635
|
+
paths: {
|
|
636
|
+
'/orgs/{orgId}/repos/{repoId}/commits': {
|
|
637
|
+
get: {
|
|
638
|
+
operationId: 'listOrgRepoCommits',
|
|
639
|
+
parameters: [
|
|
640
|
+
{ name: 'orgId', in: 'path', required: true, schema: { type: 'string' } },
|
|
641
|
+
{ name: 'repoId', in: 'path', required: true, schema: { type: 'string' } },
|
|
642
|
+
{ name: 'branch', in: 'query', required: false, schema: { type: 'string' } },
|
|
643
|
+
],
|
|
644
|
+
responses: { '200': { description: 'ok' } },
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
}
|
|
649
|
+
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
|
|
650
|
+
expect(content).toContain('export function useSuspenseListOrgRepoCommits')
|
|
651
|
+
expect(content).toContain('orgId: string')
|
|
652
|
+
expect(content).toContain('repoId: string')
|
|
653
|
+
expect(content).toContain('params?:')
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
it('deprecated GET with suspense: true emits @deprecated JSDoc on the suspense hook', () => {
|
|
657
|
+
// Covers the `if (op.deprecated)` branch in suspense hook generation
|
|
658
|
+
const spec: OpenAPIV3_1.Document = {
|
|
659
|
+
openapi: '3.1.0',
|
|
660
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
661
|
+
paths: {
|
|
662
|
+
'/legacy': {
|
|
663
|
+
get: {
|
|
664
|
+
operationId: 'listLegacy',
|
|
665
|
+
deprecated: true,
|
|
666
|
+
responses: { '200': { description: 'ok' } },
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
}
|
|
671
|
+
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
|
|
672
|
+
const suspenseIdx = content.indexOf('export function useSuspenseListLegacy')
|
|
673
|
+
expect(suspenseIdx).toBeGreaterThan(-1)
|
|
674
|
+
// @deprecated comment appears immediately before the function declaration
|
|
675
|
+
const before = content.slice(0, suspenseIdx)
|
|
676
|
+
expect(before.slice(before.lastIndexOf('/**'))).toContain('@deprecated')
|
|
677
|
+
})
|
|
517
678
|
})
|
|
518
679
|
|
|
519
680
|
// ── Feature #59: Per-resource staleTime and gcTime overrides ───────────────────
|
|
@@ -622,7 +783,11 @@ describe('generateHooks — Bug #75: auto-invalidation skips detail when multipl
|
|
|
622
783
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
623
784
|
requestBody: {
|
|
624
785
|
required: true,
|
|
625
|
-
content: {
|
|
786
|
+
content: {
|
|
787
|
+
'application/json': {
|
|
788
|
+
schema: { type: 'object', properties: { name: { type: 'string' } } },
|
|
789
|
+
},
|
|
790
|
+
},
|
|
626
791
|
},
|
|
627
792
|
responses: { '200': { description: 'ok' } },
|
|
628
793
|
},
|
|
@@ -638,13 +803,21 @@ describe('generateHooks — Bug #75: auto-invalidation skips detail when multipl
|
|
|
638
803
|
}
|
|
639
804
|
|
|
640
805
|
it('key factory does NOT use "detail:" when there are multiple detail ops', () => {
|
|
641
|
-
const { content } = generateHooks(multiDetailSpec, {
|
|
806
|
+
const { content } = generateHooks(multiDetailSpec, {
|
|
807
|
+
staleTime: 0,
|
|
808
|
+
gcTime: 0,
|
|
809
|
+
autoInvalidate: true,
|
|
810
|
+
})
|
|
642
811
|
// With 2 detail ops (getItem + getItemSummary), the key factory uses operation-derived names
|
|
643
812
|
expect(content).not.toContain('detail:')
|
|
644
813
|
})
|
|
645
814
|
|
|
646
815
|
it('mutation hook does NOT reference itemKeys.detail( when there are multiple detail ops', () => {
|
|
647
|
-
const { content } = generateHooks(multiDetailSpec, {
|
|
816
|
+
const { content } = generateHooks(multiDetailSpec, {
|
|
817
|
+
staleTime: 0,
|
|
818
|
+
gcTime: 0,
|
|
819
|
+
autoInvalidate: true,
|
|
820
|
+
})
|
|
648
821
|
const updateStart = content.indexOf('export function useUpdateItem')
|
|
649
822
|
const updateEnd = content.indexOf('\n}', updateStart) + 2
|
|
650
823
|
const updateContent = content.slice(updateStart, updateEnd)
|
|
@@ -652,7 +825,11 @@ describe('generateHooks — Bug #75: auto-invalidation skips detail when multipl
|
|
|
652
825
|
})
|
|
653
826
|
|
|
654
827
|
it('mutation hook DOES contain itemKeys.all() for broad invalidation', () => {
|
|
655
|
-
const { content } = generateHooks(multiDetailSpec, {
|
|
828
|
+
const { content } = generateHooks(multiDetailSpec, {
|
|
829
|
+
staleTime: 0,
|
|
830
|
+
gcTime: 0,
|
|
831
|
+
autoInvalidate: true,
|
|
832
|
+
})
|
|
656
833
|
const updateStart = content.indexOf('export function useUpdateItem')
|
|
657
834
|
const updateEnd = content.indexOf('\n}', updateStart) + 2
|
|
658
835
|
const updateContent = content.slice(updateStart, updateEnd)
|
|
@@ -768,7 +945,11 @@ describe('feature interactions', () => {
|
|
|
768
945
|
|
|
769
946
|
describe('auto-invalidate mutations (#58)', () => {
|
|
770
947
|
it('autoInvalidate: false — no useQueryClient or invalidateQueries in output', () => {
|
|
771
|
-
const { content } = generateHooks(spec, {
|
|
948
|
+
const { content } = generateHooks(spec, {
|
|
949
|
+
staleTime: 0,
|
|
950
|
+
gcTime: 300_000,
|
|
951
|
+
autoInvalidate: false,
|
|
952
|
+
})
|
|
772
953
|
expect(content).not.toContain('useQueryClient')
|
|
773
954
|
expect(content).not.toContain('invalidateQueries')
|
|
774
955
|
})
|
|
@@ -851,7 +1032,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
851
1032
|
operationId: 'postC',
|
|
852
1033
|
requestBody: {
|
|
853
1034
|
required: true,
|
|
854
|
-
content: {
|
|
1035
|
+
content: {
|
|
1036
|
+
'application/json': {
|
|
1037
|
+
schema: {
|
|
1038
|
+
type: 'object' as const,
|
|
1039
|
+
properties: { name: { type: 'string' as const } },
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
855
1043
|
},
|
|
856
1044
|
responses: { '201': { description: 'created' } },
|
|
857
1045
|
},
|
|
@@ -863,7 +1051,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
863
1051
|
parameters: [{ name: 'dryRun', in: 'query', schema: { type: 'string' } }],
|
|
864
1052
|
requestBody: {
|
|
865
1053
|
required: true,
|
|
866
|
-
content: {
|
|
1054
|
+
content: {
|
|
1055
|
+
'application/json': {
|
|
1056
|
+
schema: {
|
|
1057
|
+
type: 'object' as const,
|
|
1058
|
+
properties: { name: { type: 'string' as const } },
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
867
1062
|
},
|
|
868
1063
|
responses: { '200': { description: 'ok' } },
|
|
869
1064
|
},
|
|
@@ -894,7 +1089,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
894
1089
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
895
1090
|
requestBody: {
|
|
896
1091
|
required: true,
|
|
897
|
-
content: {
|
|
1092
|
+
content: {
|
|
1093
|
+
'application/json': {
|
|
1094
|
+
schema: {
|
|
1095
|
+
type: 'object' as const,
|
|
1096
|
+
properties: { name: { type: 'string' as const } },
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
898
1100
|
},
|
|
899
1101
|
responses: { '200': { description: 'ok' } },
|
|
900
1102
|
},
|
|
@@ -909,7 +1111,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
909
1111
|
],
|
|
910
1112
|
requestBody: {
|
|
911
1113
|
required: true,
|
|
912
|
-
content: {
|
|
1114
|
+
content: {
|
|
1115
|
+
'application/json': {
|
|
1116
|
+
schema: {
|
|
1117
|
+
type: 'object' as const,
|
|
1118
|
+
properties: { name: { type: 'string' as const } },
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
913
1122
|
},
|
|
914
1123
|
responses: { '200': { description: 'ok' } },
|
|
915
1124
|
},
|
|
@@ -947,7 +1156,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
947
1156
|
],
|
|
948
1157
|
requestBody: {
|
|
949
1158
|
required: true,
|
|
950
|
-
content: {
|
|
1159
|
+
content: {
|
|
1160
|
+
'application/json': {
|
|
1161
|
+
schema: {
|
|
1162
|
+
type: 'object' as const,
|
|
1163
|
+
properties: { name: { type: 'string' as const } },
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
951
1167
|
},
|
|
952
1168
|
responses: { '200': { description: 'ok' } },
|
|
953
1169
|
},
|
|
@@ -963,7 +1179,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
963
1179
|
],
|
|
964
1180
|
requestBody: {
|
|
965
1181
|
required: true,
|
|
966
|
-
content: {
|
|
1182
|
+
content: {
|
|
1183
|
+
'application/json': {
|
|
1184
|
+
schema: {
|
|
1185
|
+
type: 'object' as const,
|
|
1186
|
+
properties: { name: { type: 'string' as const } },
|
|
1187
|
+
},
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
967
1190
|
},
|
|
968
1191
|
responses: { '200': { description: 'ok' } },
|
|
969
1192
|
},
|
|
@@ -975,28 +1198,38 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
975
1198
|
|
|
976
1199
|
it('case 1 (0 params, no body, no query): variablesType is void, mutationFn is () => fn()', () => {
|
|
977
1200
|
// UseMutationOptions<..., ApiError, void> — void is the variables type
|
|
978
|
-
expect(shapesContent).toContain(
|
|
1201
|
+
expect(shapesContent).toContain(
|
|
1202
|
+
'UseMutationOptions<Awaited<ReturnType<typeof deleteA>>, ApiError, void>'
|
|
1203
|
+
)
|
|
979
1204
|
expect(shapesContent).toContain('mutationFn: () => deleteA()')
|
|
980
1205
|
})
|
|
981
1206
|
|
|
982
1207
|
it('case 2 (0 params, no body, has query): variablesType is Parameters<typeof fn>[0], mutationFn is (vars) => fn(vars)', () => {
|
|
983
|
-
expect(shapesContent).toContain(
|
|
1208
|
+
expect(shapesContent).toContain(
|
|
1209
|
+
'UseMutationOptions<Awaited<ReturnType<typeof postB>>, ApiError, Parameters<typeof postB>[0]>'
|
|
1210
|
+
)
|
|
984
1211
|
expect(shapesContent).toContain('mutationFn: (vars) => postB(vars)')
|
|
985
1212
|
})
|
|
986
1213
|
|
|
987
1214
|
it('case 3 (0 params, has body, no query): variablesType is Parameters<typeof fn>[0], mutationFn is (vars) => fn(vars)', () => {
|
|
988
|
-
expect(shapesContent).toContain(
|
|
1215
|
+
expect(shapesContent).toContain(
|
|
1216
|
+
'UseMutationOptions<Awaited<ReturnType<typeof postC>>, ApiError, Parameters<typeof postC>[0]>'
|
|
1217
|
+
)
|
|
989
1218
|
expect(shapesContent).toContain('mutationFn: (vars) => postC(vars)')
|
|
990
1219
|
})
|
|
991
1220
|
|
|
992
1221
|
it('case 4 (0 params, has body, has query): variablesType is { body: ...[0]; params: ...[1] }, mutationFn destructures both', () => {
|
|
993
|
-
expect(shapesContent).toContain(
|
|
1222
|
+
expect(shapesContent).toContain(
|
|
1223
|
+
'body: Parameters<typeof postD>[0]; params: Parameters<typeof postD>[1]'
|
|
1224
|
+
)
|
|
994
1225
|
expect(shapesContent).toContain('mutationFn: ({ body, params }) => postD(body, params)')
|
|
995
1226
|
})
|
|
996
1227
|
|
|
997
1228
|
it('case 5 (1 path param, no body, no query): variablesType is string, mutationFn is (id) => fn(id)', () => {
|
|
998
1229
|
// string is the entire variables type — only case that uses literal 'string'
|
|
999
|
-
expect(shapesContent).toContain(
|
|
1230
|
+
expect(shapesContent).toContain(
|
|
1231
|
+
'UseMutationOptions<Awaited<ReturnType<typeof deleteE>>, ApiError, string>'
|
|
1232
|
+
)
|
|
1000
1233
|
expect(shapesContent).toContain('mutationFn: (id) => deleteE(id)')
|
|
1001
1234
|
})
|
|
1002
1235
|
|
|
@@ -1011,28 +1244,44 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
1011
1244
|
})
|
|
1012
1245
|
|
|
1013
1246
|
it('case 8 (1 path param, has body, has query): variablesType is { id: string; body: ...[1]; params: ...[2] }, mutationFn destructures all', () => {
|
|
1014
|
-
expect(shapesContent).toContain(
|
|
1247
|
+
expect(shapesContent).toContain(
|
|
1248
|
+
'id: string; body: Parameters<typeof putH>[1]; params: Parameters<typeof putH>[2]'
|
|
1249
|
+
)
|
|
1015
1250
|
expect(shapesContent).toContain('mutationFn: ({ id, body, params }) => putH(id, body, params)')
|
|
1016
1251
|
})
|
|
1017
1252
|
|
|
1018
1253
|
it('case 9 (2+ params, no body, no query): variablesType is { projectId: string; taskId: string }, mutationFn destructures both path params', () => {
|
|
1019
1254
|
expect(shapesContent).toContain('projectId: string; taskId: string }')
|
|
1020
|
-
expect(shapesContent).toContain(
|
|
1255
|
+
expect(shapesContent).toContain(
|
|
1256
|
+
'mutationFn: ({ projectId, taskId }) => deleteI(projectId, taskId)'
|
|
1257
|
+
)
|
|
1021
1258
|
})
|
|
1022
1259
|
|
|
1023
1260
|
it('case 10 (2+ params, no body, has query): variablesType includes both path params and params, mutationFn passes all', () => {
|
|
1024
|
-
expect(shapesContent).toContain(
|
|
1025
|
-
|
|
1261
|
+
expect(shapesContent).toContain(
|
|
1262
|
+
'projectId: string; taskId: string; params: Parameters<typeof deleteJ>[2]'
|
|
1263
|
+
)
|
|
1264
|
+
expect(shapesContent).toContain(
|
|
1265
|
+
'mutationFn: ({ projectId, taskId, params }) => deleteJ(projectId, taskId, params)'
|
|
1266
|
+
)
|
|
1026
1267
|
})
|
|
1027
1268
|
|
|
1028
1269
|
it('case 11 (2+ params, has body, no query): variablesType includes both path params and body at index [2]', () => {
|
|
1029
|
-
expect(shapesContent).toContain(
|
|
1030
|
-
|
|
1270
|
+
expect(shapesContent).toContain(
|
|
1271
|
+
'projectId: string; taskId: string; body: Parameters<typeof putK>[2]'
|
|
1272
|
+
)
|
|
1273
|
+
expect(shapesContent).toContain(
|
|
1274
|
+
'mutationFn: ({ projectId, taskId, body }) => putK(projectId, taskId, body)'
|
|
1275
|
+
)
|
|
1031
1276
|
})
|
|
1032
1277
|
|
|
1033
1278
|
it('case 12 (2+ params, has body, has query): variablesType includes both path params, body at [2], params at [3]', () => {
|
|
1034
|
-
expect(shapesContent).toContain(
|
|
1035
|
-
|
|
1279
|
+
expect(shapesContent).toContain(
|
|
1280
|
+
'projectId: string; taskId: string; body: Parameters<typeof putL>[2]; params: Parameters<typeof putL>[3]'
|
|
1281
|
+
)
|
|
1282
|
+
expect(shapesContent).toContain(
|
|
1283
|
+
'mutationFn: ({ projectId, taskId, body, params }) => putL(projectId, taskId, body, params)'
|
|
1284
|
+
)
|
|
1036
1285
|
})
|
|
1037
1286
|
})
|
|
1038
1287
|
|
|
@@ -1063,7 +1312,14 @@ describe('auto-invalidate: method-based detail invalidation', () => {
|
|
|
1063
1312
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
1064
1313
|
requestBody: {
|
|
1065
1314
|
required: true,
|
|
1066
|
-
content: {
|
|
1315
|
+
content: {
|
|
1316
|
+
'application/json': {
|
|
1317
|
+
schema: {
|
|
1318
|
+
type: 'object' as const,
|
|
1319
|
+
properties: { name: { type: 'string' as const } },
|
|
1320
|
+
},
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1067
1323
|
},
|
|
1068
1324
|
responses: { '200': { description: 'ok' } },
|
|
1069
1325
|
},
|
|
@@ -1073,7 +1329,14 @@ describe('auto-invalidate: method-based detail invalidation', () => {
|
|
|
1073
1329
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
1074
1330
|
requestBody: {
|
|
1075
1331
|
required: true,
|
|
1076
|
-
content: {
|
|
1332
|
+
content: {
|
|
1333
|
+
'application/json': {
|
|
1334
|
+
schema: {
|
|
1335
|
+
type: 'object' as const,
|
|
1336
|
+
properties: { name: { type: 'string' as const } },
|
|
1337
|
+
},
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1077
1340
|
},
|
|
1078
1341
|
responses: { '200': { description: 'ok' } },
|
|
1079
1342
|
},
|
|
@@ -1087,7 +1350,11 @@ describe('auto-invalidate: method-based detail invalidation', () => {
|
|
|
1087
1350
|
},
|
|
1088
1351
|
}
|
|
1089
1352
|
|
|
1090
|
-
const { content: thingsContent } = generateHooks(thingsSpec, {
|
|
1353
|
+
const { content: thingsContent } = generateHooks(thingsSpec, {
|
|
1354
|
+
staleTime: 0,
|
|
1355
|
+
gcTime: 0,
|
|
1356
|
+
autoInvalidate: true,
|
|
1357
|
+
})
|
|
1091
1358
|
|
|
1092
1359
|
it('thingKeys key factory has a detail entry (exactly one detail GET op)', () => {
|
|
1093
1360
|
expect(thingsContent).toContain('export const thingKeys')
|
|
@@ -1161,7 +1428,10 @@ describe('query hook key builder — 2+ path params with query params', () => {
|
|
|
1161
1428
|
},
|
|
1162
1429
|
}
|
|
1163
1430
|
|
|
1164
|
-
const { content: multiParamContent } = generateHooks(multiParamQuerySpec, {
|
|
1431
|
+
const { content: multiParamContent } = generateHooks(multiParamQuerySpec, {
|
|
1432
|
+
staleTime: 0,
|
|
1433
|
+
gcTime: 0,
|
|
1434
|
+
})
|
|
1165
1435
|
|
|
1166
1436
|
it('key factory detail entry accepts both path params as typed args', () => {
|
|
1167
1437
|
expect(multiParamContent).toContain('(projectId: string, taskId: string,')
|
|
@@ -1173,7 +1443,9 @@ describe('query hook key builder — 2+ path params with query params', () => {
|
|
|
1173
1443
|
})
|
|
1174
1444
|
|
|
1175
1445
|
it('enabled guard ANDs both path params', () => {
|
|
1176
|
-
expect(multiParamContent).toContain(
|
|
1446
|
+
expect(multiParamContent).toContain(
|
|
1447
|
+
'enabled: projectId != null && taskId != null && (options?.enabled ?? true)'
|
|
1448
|
+
)
|
|
1177
1449
|
})
|
|
1178
1450
|
|
|
1179
1451
|
it('queryKey call includes both path params with non-null assertions and query params', () => {
|
|
@@ -1193,7 +1465,10 @@ describe('generateHooks — edge cases', () => {
|
|
|
1193
1465
|
post: {
|
|
1194
1466
|
operationId: 'createPet',
|
|
1195
1467
|
deprecated: true,
|
|
1196
|
-
requestBody: {
|
|
1468
|
+
requestBody: {
|
|
1469
|
+
required: true,
|
|
1470
|
+
content: { 'application/json': { schema: { type: 'object' } } },
|
|
1471
|
+
},
|
|
1197
1472
|
responses: { '201': { description: 'Created' } },
|
|
1198
1473
|
},
|
|
1199
1474
|
},
|
|
@@ -1243,14 +1518,28 @@ describe('generateHooks — edge cases', () => {
|
|
|
1243
1518
|
'/items': {
|
|
1244
1519
|
get: {
|
|
1245
1520
|
operationId: 'listItems',
|
|
1246
|
-
responses: {
|
|
1521
|
+
responses: {
|
|
1522
|
+
'200': {
|
|
1523
|
+
description: 'ok',
|
|
1524
|
+
content: {
|
|
1525
|
+
'application/json': { schema: { type: 'array', items: { type: 'object' } } },
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1528
|
+
},
|
|
1247
1529
|
},
|
|
1248
1530
|
},
|
|
1249
1531
|
'/items/search': {
|
|
1250
1532
|
get: {
|
|
1251
1533
|
operationId: 'searchItems',
|
|
1252
1534
|
parameters: [{ name: 'q', in: 'query', schema: { type: 'string' } }],
|
|
1253
|
-
responses: {
|
|
1535
|
+
responses: {
|
|
1536
|
+
'200': {
|
|
1537
|
+
description: 'ok',
|
|
1538
|
+
content: {
|
|
1539
|
+
'application/json': { schema: { type: 'array', items: { type: 'object' } } },
|
|
1540
|
+
},
|
|
1541
|
+
},
|
|
1542
|
+
},
|
|
1254
1543
|
},
|
|
1255
1544
|
},
|
|
1256
1545
|
},
|
|
@@ -1289,7 +1578,10 @@ describe('Bug #105: cache key collision when multiple GET ops share same path pa
|
|
|
1289
1578
|
},
|
|
1290
1579
|
}
|
|
1291
1580
|
|
|
1292
|
-
const { content: multiDetailResult } = generateHooks(specWithMultipleDetailOps, {
|
|
1581
|
+
const { content: multiDetailResult } = generateHooks(specWithMultipleDetailOps, {
|
|
1582
|
+
staleTime: 0,
|
|
1583
|
+
gcTime: 0,
|
|
1584
|
+
})
|
|
1293
1585
|
|
|
1294
1586
|
it('both operations have distinct named entries in the key factory', () => {
|
|
1295
1587
|
expect(multiDetailResult).toContain('getItemById:')
|
|
@@ -1380,7 +1672,9 @@ describe('generateHooks — path segment sanitization in deriveOperationName', (
|
|
|
1380
1672
|
paths: {
|
|
1381
1673
|
'/api-keys/{api-key-id}': {
|
|
1382
1674
|
get: {
|
|
1383
|
-
parameters: [
|
|
1675
|
+
parameters: [
|
|
1676
|
+
{ name: 'api-key-id', in: 'path', required: true, schema: { type: 'string' } },
|
|
1677
|
+
],
|
|
1384
1678
|
responses: { '200': { description: 'ok' } },
|
|
1385
1679
|
},
|
|
1386
1680
|
},
|
|
@@ -1423,7 +1717,14 @@ describe('generateHooks — toKeyFactoryName sanitizes special chars', () => {
|
|
|
1423
1717
|
'/attachments/{attachment-publicUUID}': {
|
|
1424
1718
|
get: {
|
|
1425
1719
|
operationId: 'getAttachment',
|
|
1426
|
-
parameters: [
|
|
1720
|
+
parameters: [
|
|
1721
|
+
{
|
|
1722
|
+
name: 'attachment-publicUUID',
|
|
1723
|
+
in: 'path',
|
|
1724
|
+
required: true,
|
|
1725
|
+
schema: { type: 'string' },
|
|
1726
|
+
},
|
|
1727
|
+
],
|
|
1427
1728
|
responses: { '200': { description: 'ok' } },
|
|
1428
1729
|
},
|
|
1429
1730
|
},
|
|
@@ -1452,7 +1753,10 @@ describe('Bug #106: ...options spread must not overwrite auto-invalidation onSuc
|
|
|
1452
1753
|
},
|
|
1453
1754
|
post: {
|
|
1454
1755
|
operationId: 'createItem',
|
|
1455
|
-
requestBody: {
|
|
1756
|
+
requestBody: {
|
|
1757
|
+
required: true,
|
|
1758
|
+
content: { 'application/json': { schema: { $ref: '#/components/schemas/Item' } } },
|
|
1759
|
+
},
|
|
1456
1760
|
responses: { '201': { description: 'ok' } },
|
|
1457
1761
|
},
|
|
1458
1762
|
},
|
|
@@ -1491,10 +1795,51 @@ describe('Bug #106: ...options spread must not overwrite auto-invalidation onSuc
|
|
|
1491
1795
|
})
|
|
1492
1796
|
|
|
1493
1797
|
it('mutation hook without auto-invalidation is unaffected (no onSuccess generated)', () => {
|
|
1494
|
-
const { content: plain } = generateHooks(specWithAutoInvalidate, {
|
|
1798
|
+
const { content: plain } = generateHooks(specWithAutoInvalidate, {
|
|
1799
|
+
staleTime: 0,
|
|
1800
|
+
gcTime: 0,
|
|
1801
|
+
autoInvalidate: false,
|
|
1802
|
+
})
|
|
1495
1803
|
const hookStart = plain.indexOf('export function useCreateItem')
|
|
1496
1804
|
const hookEnd = plain.indexOf('\n}', hookStart) + 2
|
|
1497
1805
|
const hookContent = plain.slice(hookStart, hookEnd)
|
|
1498
1806
|
expect(hookContent).not.toContain('onSuccess:')
|
|
1499
1807
|
})
|
|
1500
1808
|
})
|
|
1809
|
+
|
|
1810
|
+
describe('coverage: spec without paths property — generates empty output', () => {
|
|
1811
|
+
it('spec with paths=undefined produces a valid but empty hooks file', () => {
|
|
1812
|
+
// Covers the `if (paths !== undefined)` false branch in generateHooks (line 542)
|
|
1813
|
+
const spec = {
|
|
1814
|
+
openapi: '3.1.0',
|
|
1815
|
+
info: { title: 'Empty', version: '1.0.0' },
|
|
1816
|
+
// no paths property
|
|
1817
|
+
} as OpenAPIV3_1.Document
|
|
1818
|
+
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0 })
|
|
1819
|
+
expect(content).toBeDefined()
|
|
1820
|
+
// No hooks generated — just the header boilerplate
|
|
1821
|
+
expect(content).not.toContain('export function use')
|
|
1822
|
+
})
|
|
1823
|
+
})
|
|
1824
|
+
|
|
1825
|
+
describe('coverage: requestBody with no content — hasBody but no bodyTypeName', () => {
|
|
1826
|
+
it('POST with requestBody missing content property produces mutation without typed body', () => {
|
|
1827
|
+
// Covers `if (content === undefined) return { hasBody: true, bodyTypeName: undefined }` (line 112)
|
|
1828
|
+
const spec: OpenAPIV3_1.Document = {
|
|
1829
|
+
openapi: '3.1.0',
|
|
1830
|
+
info: { title: 'Test', version: '1.0.0' },
|
|
1831
|
+
paths: {
|
|
1832
|
+
'/items': {
|
|
1833
|
+
post: {
|
|
1834
|
+
operationId: 'createItem',
|
|
1835
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1836
|
+
requestBody: { required: true } as any, // no content property
|
|
1837
|
+
responses: { '201': { description: 'created' } },
|
|
1838
|
+
},
|
|
1839
|
+
},
|
|
1840
|
+
},
|
|
1841
|
+
}
|
|
1842
|
+
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0 })
|
|
1843
|
+
expect(content).toContain('export function useCreateItem')
|
|
1844
|
+
})
|
|
1845
|
+
})
|