@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.
@@ -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("list:")
44
- expect(content).toContain("detail:")
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, { staleTime: 0, gcTime: 0 })
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, { staleTime: 0, gcTime: 0 })
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 = (pathSegment: string, method: string, hasBody: boolean, queryParams: Array<{name: string; required?: boolean}>): OpenAPIV3_1.Document => ({
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) => ({ name: p.name, in: 'query' as const, required: p.required, schema: { type: 'string' as const } })),
295
- ...(hasBody ? {
296
- requestBody: {
297
- required: true,
298
- content: { 'application/json': { schema: { $ref: '#/components/schemas/Body' } } },
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: { schemas: { Body: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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: { schemas: { Body: { type: 'object', properties: { name: { type: 'string' } } } } },
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: { 'application/json': { schema: { $ref: '#/components/schemas/CreateTask' } } },
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: { schemas: { CreateTask: { type: 'object', properties: { title: { type: 'string' } } } } },
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: { '200': { description: 'ok', content: { 'application/json': { schema: { type: 'array', items: { type: 'object' } } } } } },
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: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string' } } } } },
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, { staleTime: 0, gcTime: 0, autoInvalidate: true })
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, { staleTime: 0, gcTime: 0, autoInvalidate: true })
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, { staleTime: 0, gcTime: 0, autoInvalidate: true })
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, { staleTime: 0, gcTime: 300_000, autoInvalidate: false })
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: { 'application/json': { schema: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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: { 'application/json': { schema: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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: { 'application/json': { schema: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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: { 'application/json': { schema: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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: { 'application/json': { schema: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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: { 'application/json': { schema: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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('UseMutationOptions<Awaited<ReturnType<typeof deleteA>>, ApiError, void>')
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('UseMutationOptions<Awaited<ReturnType<typeof postB>>, ApiError, Parameters<typeof postB>[0]>')
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('UseMutationOptions<Awaited<ReturnType<typeof postC>>, ApiError, Parameters<typeof postC>[0]>')
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('body: Parameters<typeof postD>[0]; params: Parameters<typeof postD>[1]')
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('UseMutationOptions<Awaited<ReturnType<typeof deleteE>>, ApiError, string>')
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('id: string; body: Parameters<typeof putH>[1]; params: Parameters<typeof putH>[2]')
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('mutationFn: ({ projectId, taskId }) => deleteI(projectId, taskId)')
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('projectId: string; taskId: string; params: Parameters<typeof deleteJ>[2]')
1025
- expect(shapesContent).toContain('mutationFn: ({ projectId, taskId, params }) => deleteJ(projectId, taskId, params)')
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('projectId: string; taskId: string; body: Parameters<typeof putK>[2]')
1030
- expect(shapesContent).toContain('mutationFn: ({ projectId, taskId, body }) => putK(projectId, taskId, body)')
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('projectId: string; taskId: string; body: Parameters<typeof putL>[2]; params: Parameters<typeof putL>[3]')
1035
- expect(shapesContent).toContain('mutationFn: ({ projectId, taskId, body, params }) => putL(projectId, taskId, body, params)')
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: { 'application/json': { schema: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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: { 'application/json': { schema: { type: 'object' as const, properties: { name: { type: 'string' as const } } } } },
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, { staleTime: 0, gcTime: 0, autoInvalidate: true })
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, { staleTime: 0, gcTime: 0 })
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('enabled: projectId != null && taskId != null && (options?.enabled ?? true)')
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: { required: true, content: { 'application/json': { schema: { type: 'object' } } } },
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: { '200': { description: 'ok', content: { 'application/json': { schema: { type: 'array', items: { type: 'object' } } } } } },
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: { '200': { description: 'ok', content: { 'application/json': { schema: { type: 'array', items: { type: 'object' } } } } } },
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, { staleTime: 0, gcTime: 0 })
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: [{ name: 'api-key-id', in: 'path', required: true, schema: { type: 'string' } }],
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: [{ name: 'attachment-publicUUID', in: 'path', required: true, schema: { type: 'string' } }],
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: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/Item' } } } },
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, { staleTime: 0, gcTime: 0, autoInvalidate: false })
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
+ })