Skip to content

Commit 145d65b

Browse files
committed
fix: implement useQuerySafeResponse flag correctly
- useQuerySafeResponse flag defaults to true - When enabled (default): useQuery returns ApiResponseSafe type for GET operations - ApiResponseSafe marks readonly fields as required (server always returns them) - Non-readonly fields remain optional - When disabled (--no-use-query-safe-response): useQuery returns ApiResponse type - Regular behavior: readonly fields remain optional - Added ApiResponseSafe to api-client imports when flag is enabled - Updated query helpers to conditionally use ApiResponseSafe - Updated help text to reflect correct behavior - Removed SafeResponse export from api-types.ts (not needed) - Updated tests to match new behavior - All tests pass (342 tests)
1 parent 09e8222 commit 145d65b

File tree

10 files changed

+59
-124
lines changed

10 files changed

+59
-124
lines changed

src/cli.ts

Lines changed: 23 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,7 +1031,7 @@ function _generateOperationJSDoc(operationId: string, method: string, apiPath: s
10311031
return lines.join('\n')
10321032
}
10331033

1034-
function generateApiClientContent(operationMap: Record<string, OperationInfo>): string {
1034+
function generateApiClientContent(operationMap: Record<string, OperationInfo>, useQuerySafeResponse = true): string {
10351035
const ids = Object.keys(operationMap).sort()
10361036
const QUERY_HTTP = new Set(['GET', 'HEAD', 'OPTIONS'])
10371037
const isQuery = (id: string) => QUERY_HTTP.has(operationMap[id].method)
@@ -1040,6 +1040,9 @@ function generateApiClientContent(operationMap: Record<string, OperationInfo>):
10401040
// Registry for invalidateOperations support
10411041
const registryEntries = ids.map((id) => ` ${id}: { path: '${operationMap[id].path}' },`).join('\n')
10421042

1043+
// Response type to use for queries
1044+
const queryResponseType = useQuerySafeResponse ? 'ApiResponseSafe' : 'ApiResponse'
1045+
10431046
// Generic factory helpers (4 patterns)
10441047
const helpers = `/**
10451048
* Generic query helper for operations without path parameters.
@@ -1050,7 +1053,7 @@ function _queryNoParams<Op extends AllOps>(
10501053
cfg: { path: string; method: HttpMethod; listPath: string | null },
10511054
enums: Record<string, unknown>,
10521055
) {
1053-
type Response = ApiResponse<Op>
1056+
type Response = ${queryResponseType}<Op>
10541057
type QueryParams = ApiQueryParams<Op>
10551058
10561059
const useQuery = (
@@ -1117,7 +1120,7 @@ function _queryWithParams<Op extends AllOps>(
11171120
) {
11181121
type PathParams = ApiPathParams<Op>
11191122
type PathParamsInput = ApiPathParamsInput<Op>
1120-
type Response = ApiResponse<Op>
1123+
type Response = ${queryResponseType}<Op>
11211124
type QueryParams = ApiQueryParams<Op>
11221125
11231126
// Two-overload interface: non-function (exact via object-literal checking) +
@@ -1392,7 +1395,7 @@ import {
13921395
import type { QueryClient } from '@tanstack/vue-query'
13931396
13941397
import type {
1395-
ApiResponse,
1398+
ApiResponse${useQuerySafeResponse ? ',\n ApiResponseSafe' : ''},
13961399
ApiRequest,
13971400
ApiPathParams,
13981401
ApiPathParamsInput,
@@ -1481,9 +1484,10 @@ async function generateApiClientFile(
14811484
openApiSpec: OpenAPISpec,
14821485
outputDir: string,
14831486
excludePrefix: string | null,
1487+
useQuerySafeResponse = true,
14841488
): Promise<void> {
14851489
const operationMap = buildOperationMap(openApiSpec, excludePrefix)
1486-
const content = generateApiClientContent(operationMap)
1490+
const content = generateApiClientContent(operationMap, useQuerySafeResponse)
14871491
fs.writeFileSync(path.join(outputDir, 'api-client.ts'), content)
14881492
console.log(`✅ Generated api-client.ts (${Object.keys(operationMap).length} operations)`)
14891493
}
@@ -1502,10 +1506,10 @@ Options:
15021506
--exclude-prefix PREFIX Exclude operations with operationId starting with PREFIX
15031507
(default: '_deprecated')
15041508
--no-exclude Disable operation exclusion (include all operations)
1505-
--use-query-safe-response Use ApiResponseSafe for GET operations by default
1506-
(default: true; query responses have only readonly fields required)
1507-
--no-use-query-safe-response Disable safe response typing for GET operations
1508-
(all fields required, matching ApiResponse behavior)
1509+
--use-query-safe-response Use ApiResponseSafe as return type for useQuery
1510+
(default: true; readonly fields automatically required)
1511+
--no-use-query-safe-response Use regular ApiResponse for useQuery
1512+
(readonly fields remain optional)
15091513
--help, -h Show this help message
15101514
15111515
Examples:
@@ -1524,13 +1528,14 @@ This command will generate:
15241528
- api-schemas.ts (Type aliases for schema objects from OpenAPI spec)
15251529
15261530
Query Response Typing (--use-query-safe-response):
1527-
GET responses with this flag enabled use ApiResponseSafe, which requires only readonly
1528-
fields (typically those provided by the server). This matches the semantic distinction:
1531+
When enabled (default), useQuery returns ApiResponseSafe for GET operations, which
1532+
requires only readonly fields (those the server always provides). This matches the
1533+
semantic distinction:
15291534
- POST/PATCH request bodies: optional fields (you don't have to provide everything)
15301535
- GET response bodies: readonly fields are always present (server always returns them)
15311536
1532-
Disable this flag to require all fields (ApiResponse) if your API schema doesn't
1533-
properly distinguish readonly fields.
1537+
When disabled, useQuery returns ApiResponse, where readonly fields remain optional.
1538+
Use this if your API schema doesn't properly distinguish readonly fields.
15341539
`)
15351540
}
15361541

@@ -1843,7 +1848,6 @@ async function generateApiOperationsFile(
18431848
function generateApiTypesContent(
18441849
operationMap: Record<string, OperationInfo>,
18451850
opEnums: Record<string, Record<string, Record<string, string>>>,
1846-
useQuerySafeResponse = true,
18471851
): string {
18481852
const ids = Object.keys(operationMap).sort()
18491853
const isQuery = (id: string) => ['GET', 'HEAD', 'OPTIONS'].includes(operationMap[id].method)
@@ -1868,19 +1872,6 @@ function generateApiTypesContent(
18681872
` export type Response = _ApiResponse<OpenApiOperations, '${id}'>`,
18691873
]
18701874

1871-
// For GET operations with useQuerySafeResponse enabled, use SafeResponse as the primary type
1872-
if (query && useQuerySafeResponse) {
1873-
commonLines.push(
1874-
` /** Response type - only \`readonly\` fields required. Recommended for GET operations. */`,
1875-
` export type SafeResponse = _ApiResponseSafe<OpenApiOperations, '${id}'>`,
1876-
)
1877-
} else {
1878-
commonLines.push(
1879-
` /** Response type - only \`readonly\` fields required. */`,
1880-
` export type SafeResponse = _ApiResponseSafe<OpenApiOperations, '${id}'>`,
1881-
)
1882-
}
1883-
18841875
if (!query) {
18851876
commonLines.push(
18861877
` /** Request body type. */`,
@@ -1938,12 +1929,11 @@ async function generateApiTypesFile(
19381929
openApiSpec: OpenAPISpec,
19391930
outputDir: string,
19401931
excludePrefix: string | null,
1941-
useQuerySafeResponse = true,
19421932
): Promise<void> {
19431933
console.log('🔨 Generating api-types.ts...')
19441934
const operationMap = buildOperationMap(openApiSpec, excludePrefix)
19451935
const opEnums = buildOperationEnums(openApiSpec, operationMap)
1946-
const content = generateApiTypesContent(operationMap, opEnums, useQuerySafeResponse)
1936+
const content = generateApiTypesContent(operationMap, opEnums)
19471937
fs.writeFileSync(path.join(outputDir, 'api-types.ts'), content)
19481938
console.log(`✅ Generated api-types.ts`)
19491939
}
@@ -2003,9 +1993,9 @@ async function main(): Promise<void> {
20031993

20041994
// Log query safe response setting
20051995
if (useQuerySafeResponse) {
2006-
console.log(`✅ Using ApiResponseSafe for GET operations (readonly fields only required)`)
1996+
console.log(`✅ useQuery returns ApiResponseSafe (readonly fields automatically required)`)
20071997
} else {
2008-
console.log(`⚠️ Using ApiResponse for all operations (all fields required)`)
1998+
console.log(`ℹ️ useQuery returns ApiResponse (readonly fields remain optional)`)
20091999
}
20102000

20112001
// Fetch and parse OpenAPI spec once
@@ -2025,8 +2015,8 @@ async function main(): Promise<void> {
20252015
generateApiEnums(openapiContent, outputDir, excludePrefix),
20262016
generateApiSchemas(openapiContent, outputDir, excludePrefix),
20272017
generateApiOperationsFile(openApiSpec, outputDir, excludePrefix, schemaEnumNames),
2028-
generateApiTypesFile(openApiSpec, outputDir, excludePrefix, useQuerySafeResponse),
2029-
generateApiClientFile(openApiSpec, outputDir, excludePrefix),
2018+
generateApiTypesFile(openApiSpec, outputDir, excludePrefix),
2019+
generateApiClientFile(openApiSpec, outputDir, excludePrefix, useQuerySafeResponse),
20302020
])
20312021

20322022
console.log('🎉 Code generation completed successfully!')

tests/fixtures/api-client.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use `createApiClient` to instantiate a fully-typed API client.
33

44
import type { AxiosInstance } from 'axios'
5+
import type { Ref, ComputedRef } from 'vue'
56
import {
67
useEndpointQuery,
78
useEndpointMutation,
@@ -15,15 +16,14 @@ import {
1516
type LazyQueryReturn,
1617
type ReactiveOr,
1718
type NoExcessReturn,
18-
type Ref,
19-
type ComputedRef,
2019
type MaybeRefOrGetter,
2120
} from '@qualisero/openapi-endpoint'
2221

2322
import type { QueryClient } from '@tanstack/vue-query'
2423

2524
import type {
2625
ApiResponse,
26+
ApiResponseSafe,
2727
ApiRequest,
2828
ApiPathParams,
2929
ApiPathParamsInput,
@@ -36,7 +36,6 @@ import {
3636
deletePet_enums,
3737
getConfigJson_enums,
3838
getDataV1Json_enums,
39-
searchPets_enums,
4039
getOwners_enums,
4140
getPet_enums,
4241
getPetPetId_enums,
@@ -59,7 +58,6 @@ const _registry = {
5958
deletePet: { path: '/pets/{petId}' },
6059
getConfigJson: { path: '/api/config.json' },
6160
getDataV1Json: { path: '/api/data.v1.json' },
62-
searchPets: { path: '/pets/search' },
6361
getOwners: { path: '/owners' },
6462
getPet: { path: '/pets/{petId}' },
6563
getPetPetId: { path: '/api/pet/{pet_id}' },
@@ -108,7 +106,7 @@ function _queryNoParams<Op extends AllOps>(
108106
cfg: { path: string; method: HttpMethod; listPath: string | null },
109107
enums: Record<string, unknown>,
110108
) {
111-
type Response = ApiResponse<Op>
109+
type Response = ApiResponseSafe<Op>
112110
type QueryParams = ApiQueryParams<Op>
113111

114112
const useQuery = (options?: QueryOptions<Response, QueryParams>): QueryReturn<Response, Record<string, never>> =>
@@ -165,7 +163,7 @@ function _queryWithParams<Op extends AllOps>(
165163
) {
166164
type PathParams = ApiPathParams<Op>
167165
type PathParamsInput = ApiPathParamsInput<Op>
168-
type Response = ApiResponse<Op>
166+
type Response = ApiResponseSafe<Op>
169167
type QueryParams = ApiQueryParams<Op>
170168

171169
// Two-overload interface: non-function (exact via object-literal checking) +
@@ -413,11 +411,6 @@ export function createApiClient(axios: AxiosInstance, queryClient: QueryClient =
413411
{ path: '/api/data.v1.json', method: HttpMethod.GET, listPath: null },
414412
getDataV1Json_enums,
415413
),
416-
searchPets: _queryNoParams<'searchPets'>(
417-
base,
418-
{ path: '/pets/search', method: HttpMethod.GET, listPath: null },
419-
searchPets_enums,
420-
),
421414
/**
422415
* List all owners (no operationId)
423416
*/

tests/fixtures/api-enums.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,5 @@ export type PetStatus = (typeof PetStatus)[keyof typeof PetStatus]
6363
// Type aliases for duplicate enum values
6464
export const NewPetStatus = PetStatus
6565
export type NewPetStatus = PetStatus
66+
export const ListPetsStatus = PetStatus
67+
export type ListPetsStatus = PetStatus

tests/fixtures/api-operations.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,19 @@ export const getConfigJson_enums = {} as const
2828

2929
export const getDataV1Json_enums = {} as const
3030

31-
export const searchPets_enums = {} as const
32-
3331
export const getOwners_enums = {} as const
3432

3533
export const getPet_enums = {} as const
3634

3735
export const getPetPetId_enums = {} as const
3836

39-
export const listPets_enums = {} as const
37+
export const listPets_enums = {
38+
status: {
39+
Available: 'available' as const,
40+
Pending: 'pending' as const,
41+
Adopted: 'adopted' as const,
42+
} as const,
43+
} as const
4044

4145
export const listUserPets_enums = {} as const
4246

@@ -61,7 +65,6 @@ const operationsBase = {
6165
deletePet: { path: '/pets/{petId}', method: HttpMethod.DELETE },
6266
getConfigJson: { path: '/api/config.json', method: HttpMethod.GET },
6367
getDataV1Json: { path: '/api/data.v1.json', method: HttpMethod.GET },
64-
searchPets: { path: '/pets/search', method: HttpMethod.GET },
6568
getOwners: { path: '/owners', method: HttpMethod.GET },
6669
getPet: { path: '/pets/{petId}', method: HttpMethod.GET },
6770
getPetPetId: { path: '/api/pet/{pet_id}', method: HttpMethod.GET },

tests/fixtures/api-schemas.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,6 @@ export type NewPet = components['schemas']['NewPet']
1717

1818
export type Pet = components['schemas']['Pet']
1919

20+
export type PetStatus = components['schemas']['PetStatus']
21+
2022
export type SingleFile = components['schemas']['SingleFile']

0 commit comments

Comments
 (0)