@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 +7 -0
- package/README.md +2 -2
- package/package.json +2 -2
- package/src/__tests__/compat-matrix.test.ts +2 -1
- package/src/__tests__/hooks.test.ts +161 -0
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
|
[](https://npmjs.com/package/@codewithagents/openapi-react-query)
|
|
4
4
|
[](https://codecov.io/gh/codewithagents/glue)
|
|
5
5
|
|
|
6
|
-
Generate typed [React Query v5](https://tanstack.com/query/v5) hooks from an OpenAPI 3.
|
|
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` | ✅ | — |
|
|
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
|
+
"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.
|
|
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('
|
|
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
|
+
})
|