@codewithagents/openapi-react-query 3.3.1 → 3.4.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.4.0](https://github.com/codewithagents/glue/compare/openapi-react-query-v3.3.1...openapi-react-query-v3.4.0) (2026-05-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * **smoke:** expand to 9 real API requests, run on every push to main ([#144](https://github.com/codewithagents/glue/issues/144)) ([ba0c7c3](https://github.com/codewithagents/glue/commit/ba0c7c39c97f4f0a152fed67676346c035de90fd))
9
+
3
10
  ## [3.3.1](https://github.com/codewithagents/glue/compare/openapi-react-query-v3.3.0...openapi-react-query-v3.3.1) (2026-05-29)
4
11
 
5
12
 
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@codewithagents/openapi-react-query.svg)](https://npmjs.com/package/@codewithagents/openapi-react-query)
4
4
  [![codecov](https://codecov.io/gh/codewithagents/glue/graph/badge.svg?flag=openapi-react-query)](https://codecov.io/gh/codewithagents/glue)
5
5
 
6
- Generate typed [React Query v5](https://tanstack.com/query/v5) hooks from an OpenAPI 3.1 spec. Run once, get a fully typed `useQuery` hook per GET endpoint and a `useMutation` hook per write operation — no hand-written boilerplate.
6
+ Generate typed [React Query v5](https://tanstack.com/query/v5) hooks from an OpenAPI 3.x spec. Run once, get a fully typed `useQuery` hook per GET endpoint and a `useMutation` hook per write operation — no hand-written boilerplate.
7
7
 
8
8
  Works alongside [`@codewithagents/openapi-gen`](https://www.npmjs.com/package/@codewithagents/openapi-gen) which generates the underlying typed fetch client. All generated files are **Prettier-clean** out of the box — commit them without running a formatter.
9
9
 
@@ -33,7 +33,7 @@ Create `openapi-react-query.config.json` in your project root:
33
33
 
34
34
  | Field | Required | Default | Description |
35
35
  |---|---|---|---|
36
- | `input_openapi` | ✅ | — | Path to OpenAPI 3.1 spec (JSON or YAML) |
36
+ | `input_openapi` | ✅ | — | OpenAPI 3.x spec (JSON or YAML) |
37
37
  | `output` | ✅ | — | Directory to write generated files (same as openapi-gen output) |
38
38
  | `stale_time` | — | `0` | `staleTime` in ms applied to all `useQuery` hooks |
39
39
  | `gc_time` | — | `300000` | `gcTime` in ms applied to all `useQuery` hooks |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codewithagents/openapi-react-query",
3
- "version": "3.3.1",
3
+ "version": "3.4.0",
4
4
  "description": "Generate typed React Query v5 hooks from OpenAPI 3.1 specs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "dependencies": {
14
14
  "openapi-types": "^12.1.3",
15
15
  "prettier": "^3.5.3",
16
- "@codewithagents/openapi-gen": "4.1.0"
16
+ "@codewithagents/openapi-gen": "4.2.0"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "@tanstack/react-query": "^5.0.0"
@@ -1,3 +1,4 @@
1
+ // fallow-ignore-file code-duplication
1
2
  import { describe, it } from 'vitest'
2
3
  import { readdirSync, readFileSync } from 'node:fs'
3
4
  import { resolve } from 'node:path'
@@ -7,7 +8,7 @@ import { generateHooks } from '../plugins/hooks.js'
7
8
  const configsDir = resolve(import.meta.dirname, '../../../../examples/configs')
8
9
 
9
10
  const cases = readdirSync(configsDir)
10
- .filter((f) => f.endsWith('.json') && !f.startsWith('_tmp'))
11
+ .filter((f) => f.endsWith('.json') && !f.startsWith('_'))
11
12
  .map((f) => {
12
13
  const config = JSON.parse(readFileSync(resolve(configsDir, f), 'utf-8')) as { input_openapi: string }
13
14
  const specPath = resolve(configsDir, config.input_openapi)
@@ -514,6 +514,130 @@ describe('generateHooks — Feature #60: suspense query variants', () => {
514
514
  const { content } = generateHooks(suspenseSpec, { staleTime: 0, gcTime: 0, suspense: false })
515
515
  expect(content).not.toContain('useSuspenseQuery')
516
516
  })
517
+
518
+ it('suspense hook with 1 path param + optional query param generates params? signature', () => {
519
+ // Covers the `pathParams.length === 1 && hasQueryParams` branch (hooks.ts line 309-310)
520
+ // and the `paramsRequired === false` (false) branch at line 296 → paramsToken = 'params?'
521
+ const spec: OpenAPIV3_1.Document = {
522
+ openapi: '3.1.0',
523
+ info: { title: 'Test', version: '1.0.0' },
524
+ paths: {
525
+ '/repos/{repoId}/commits': {
526
+ get: {
527
+ operationId: 'listRepoCommits',
528
+ parameters: [
529
+ { name: 'repoId', in: 'path', required: true, schema: { type: 'string' } },
530
+ { name: 'branch', in: 'query', required: false, schema: { type: 'string' } },
531
+ ],
532
+ responses: { '200': { description: 'ok' } },
533
+ },
534
+ },
535
+ },
536
+ }
537
+ const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
538
+ expect(content).toContain('export function useSuspenseListRepoCommits')
539
+ expect(content).toContain('repoId: string')
540
+ expect(content).toContain('params?:')
541
+ })
542
+
543
+ it('suspense hook with 1 path param + required query param generates params (non-optional) signature', () => {
544
+ // Covers the `paramsRequired === true` (true) branch at line 296 → paramsToken = 'params'
545
+ const spec: OpenAPIV3_1.Document = {
546
+ openapi: '3.1.0',
547
+ info: { title: 'Test', version: '1.0.0' },
548
+ paths: {
549
+ '/search/{category}': {
550
+ get: {
551
+ operationId: 'searchCategory',
552
+ parameters: [
553
+ { name: 'category', in: 'path', required: true, schema: { type: 'string' } },
554
+ { name: 'q', in: 'query', required: true, schema: { type: 'string' } },
555
+ ],
556
+ responses: { '200': { description: 'ok' } },
557
+ },
558
+ },
559
+ },
560
+ }
561
+ const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
562
+ expect(content).toContain('export function useSuspenseSearchCategory')
563
+ expect(content).toContain('category: string')
564
+ // Required query param → non-optional params token (no ?)
565
+ expect(content).toContain('params: Parameters')
566
+ })
567
+
568
+ it('suspense hook with 2 path params builds multi-param queryKey call', () => {
569
+ // Covers the multi-path-param (pathParams.length > 1) queryKey branch in suspense generation
570
+ const spec: OpenAPIV3_1.Document = {
571
+ openapi: '3.1.0',
572
+ info: { title: 'Test', version: '1.0.0' },
573
+ paths: {
574
+ '/orgs/{orgId}/repos/{repoId}': {
575
+ get: {
576
+ operationId: 'getOrgRepo',
577
+ parameters: [
578
+ { name: 'orgId', in: 'path', required: true, schema: { type: 'string' } },
579
+ { name: 'repoId', in: 'path', required: true, schema: { type: 'string' } },
580
+ ],
581
+ responses: { '200': { description: 'ok' } },
582
+ },
583
+ },
584
+ },
585
+ }
586
+ const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
587
+ expect(content).toContain('export function useSuspenseGetOrgRepo')
588
+ // Both path params appear in the function signature
589
+ expect(content).toContain('orgId: string')
590
+ expect(content).toContain('repoId: string')
591
+ })
592
+
593
+ it('suspense hook with 2 path params + query params builds correct queryKey', () => {
594
+ // Covers the else branch: pathParams.length > 1 && hasQueryParams
595
+ const spec: OpenAPIV3_1.Document = {
596
+ openapi: '3.1.0',
597
+ info: { title: 'Test', version: '1.0.0' },
598
+ paths: {
599
+ '/orgs/{orgId}/repos/{repoId}/commits': {
600
+ get: {
601
+ operationId: 'listOrgRepoCommits',
602
+ parameters: [
603
+ { name: 'orgId', in: 'path', required: true, schema: { type: 'string' } },
604
+ { name: 'repoId', in: 'path', required: true, schema: { type: 'string' } },
605
+ { name: 'branch', in: 'query', required: false, schema: { type: 'string' } },
606
+ ],
607
+ responses: { '200': { description: 'ok' } },
608
+ },
609
+ },
610
+ },
611
+ }
612
+ const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
613
+ expect(content).toContain('export function useSuspenseListOrgRepoCommits')
614
+ expect(content).toContain('orgId: string')
615
+ expect(content).toContain('repoId: string')
616
+ expect(content).toContain('params?:')
617
+ })
618
+
619
+ it('deprecated GET with suspense: true emits @deprecated JSDoc on the suspense hook', () => {
620
+ // Covers the `if (op.deprecated)` branch in suspense hook generation
621
+ const spec: OpenAPIV3_1.Document = {
622
+ openapi: '3.1.0',
623
+ info: { title: 'Test', version: '1.0.0' },
624
+ paths: {
625
+ '/legacy': {
626
+ get: {
627
+ operationId: 'listLegacy',
628
+ deprecated: true,
629
+ responses: { '200': { description: 'ok' } },
630
+ },
631
+ },
632
+ },
633
+ }
634
+ const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0, suspense: true })
635
+ const suspenseIdx = content.indexOf('export function useSuspenseListLegacy')
636
+ expect(suspenseIdx).toBeGreaterThan(-1)
637
+ // @deprecated comment appears immediately before the function declaration
638
+ const before = content.slice(0, suspenseIdx)
639
+ expect(before.slice(before.lastIndexOf('/**'))).toContain('@deprecated')
640
+ })
517
641
  })
518
642
 
519
643
  // ── Feature #59: Per-resource staleTime and gcTime overrides ───────────────────
@@ -1498,3 +1622,40 @@ describe('Bug #106: ...options spread must not overwrite auto-invalidation onSuc
1498
1622
  expect(hookContent).not.toContain('onSuccess:')
1499
1623
  })
1500
1624
  })
1625
+
1626
+ describe('coverage: spec without paths property — generates empty output', () => {
1627
+ it('spec with paths=undefined produces a valid but empty hooks file', () => {
1628
+ // Covers the `if (paths !== undefined)` false branch in generateHooks (line 542)
1629
+ const spec = {
1630
+ openapi: '3.1.0',
1631
+ info: { title: 'Empty', version: '1.0.0' },
1632
+ // no paths property
1633
+ } as OpenAPIV3_1.Document
1634
+ const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0 })
1635
+ expect(content).toBeDefined()
1636
+ // No hooks generated — just the header boilerplate
1637
+ expect(content).not.toContain('export function use')
1638
+ })
1639
+ })
1640
+
1641
+ describe('coverage: requestBody with no content — hasBody but no bodyTypeName', () => {
1642
+ it('POST with requestBody missing content property produces mutation without typed body', () => {
1643
+ // Covers `if (content === undefined) return { hasBody: true, bodyTypeName: undefined }` (line 112)
1644
+ const spec: OpenAPIV3_1.Document = {
1645
+ openapi: '3.1.0',
1646
+ info: { title: 'Test', version: '1.0.0' },
1647
+ paths: {
1648
+ '/items': {
1649
+ post: {
1650
+ operationId: 'createItem',
1651
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1652
+ requestBody: { required: true } as any, // no content property
1653
+ responses: { '201': { description: 'created' } },
1654
+ },
1655
+ },
1656
+ },
1657
+ }
1658
+ const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0 })
1659
+ expect(content).toContain('export function useCreateItem')
1660
+ })
1661
+ })