From d3486b03c0f0e50b7342589c9e719e9eba3b4e06 Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Thu, 19 Feb 2026 13:35:58 +0900 Subject: [PATCH 01/10] chore: update oazapfts to v7.3.0 and adjust compatibility layer - Updated oazapfts dependency from v6.4.0 to v7.3.0 in package.json and yarn.lock. - Introduced oazapfts-compat.ts to adapt to breaking changes in oazapfts v7, replacing the ApiGenerator with OazapftsAdapter. - Refactored generate.ts to utilize the new compatibility layer. - Updated TypeScript version handling in index.ts for compatibility with oazapfts v7. - Adjusted type generation logic to ensure backward compatibility with existing code. - Fixed optional properties in generated types to be required where necessary in test snapshots. --- .../rtk-query-codegen-openapi/package.json | 2 +- .../rtk-query-codegen-openapi/src/generate.ts | 35 +- .../src/generators/react-hooks.ts | 2 +- .../rtk-query-codegen-openapi/src/index.ts | 15 +- .../src/oazapfts-compat.ts | 921 ++++++++++++++++++ .../rtk-query-codegen-openapi/src/types.ts | 10 + .../generateEndpoints.test.ts.snap | 4 +- yarn.lock | 24 +- 8 files changed, 982 insertions(+), 31 deletions(-) create mode 100644 packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts diff --git a/packages/rtk-query-codegen-openapi/package.json b/packages/rtk-query-codegen-openapi/package.json index 2ba3f77f32..1f77c76331 100644 --- a/packages/rtk-query-codegen-openapi/package.json +++ b/packages/rtk-query-codegen-openapi/package.json @@ -78,7 +78,7 @@ "@apidevtools/swagger-parser": "^10.1.1", "commander": "^6.2.0", "lodash.camelcase": "^4.3.0", - "oazapfts": "^6.4.0", + "oazapfts": "^7.3.0", "prettier": "^3.2.5", "semver": "^7.3.5", "swagger2openapi": "^7.0.4", diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 0cf52b6130..f56c9e030d 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -1,20 +1,22 @@ import camelCase from 'lodash.camelcase'; import path from 'node:path'; -import ApiGenerator, { - getOperationName as _getOperationName, +import type { OpenAPIV3 } from 'openapi-types'; +import ts from 'typescript'; +import type { ObjectPropertyDefinitions } from './codegen'; +import { generateCreateApiCall, generateEndpointDefinition, generateImportNode, generateTagTypes } from './codegen'; +import { generateReactHooks } from './generators/react-hooks'; +import { + OazapftsAdapter, createPropertyAssignment, createQuestionToken, + getOperationName as _getOperationName, getReferenceName, + getSchemaFromContent, isReference, isValidIdentifier, keywordType, supportDeepObjects, -} from 'oazapfts/generate'; -import type { OpenAPIV3 } from 'openapi-types'; -import ts from 'typescript'; -import type { ObjectPropertyDefinitions } from './codegen'; -import { generateCreateApiCall, generateEndpointDefinition, generateImportNode, generateTagTypes } from './codegen'; -import { generateReactHooks } from './generators/react-hooks'; +} from './oazapfts-compat'; import type { EndpointMatcher, EndpointOverrides, @@ -89,7 +91,7 @@ function withQueryComment(node: T, def: QueryArgDefinition, h function getPatternFromProperty( property: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, - apiGen: ApiGenerator + apiGen: OazapftsAdapter ): string | null { const resolved = apiGen.resolve(property); if (!resolved || typeof resolved !== 'object' || !('pattern' in resolved)) return null; @@ -101,7 +103,7 @@ function getPatternFromProperty( function generateRegexConstantsForType( typeName: string, schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, - apiGen: ApiGenerator + apiGen: OazapftsAdapter ): ts.VariableStatement[] { const resolvedSchema = apiGen.resolve(schema); if (!resolvedSchema || !('properties' in resolvedSchema) || !resolvedSchema.properties) return []; @@ -165,6 +167,7 @@ export async function generateApi( flattenArg = false, includeDefault = false, useEnumType = false, + enumStyle, mergeReadWriteOnly = false, httpResolverOptions, useUnknown = false, @@ -174,17 +177,15 @@ export async function generateApi( ) { const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions)); - const apiGen = new ApiGenerator(v3Doc, { - unionUndefined, + const apiGen = new OazapftsAdapter(v3Doc, { + enumStyle, useEnumType, + unionUndefined, mergeReadWriteOnly, useUnknown, }); - // temporary workaround for https://github.com/oazapfts/oazapfts/issues/491 - if (apiGen.spec.components?.schemas) { - apiGen.preprocessComponents(apiGen.spec.components.schemas); - } + await apiGen.init(); const operationDefinitions = getOperationDefinitions(v3Doc).filter(operationMatches(filterEndpoints)); @@ -403,7 +404,7 @@ export async function generateApi( if (requestBody) { const body = apiGen.resolve(requestBody); - const schema = apiGen.getSchemaFromContent(body.content); + const schema = getSchemaFromContent(body.content); const type = apiGen.getTypeFromSchema(schema); const schemaName = camelCase( (type as any).name || diff --git a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts index 91b5baef81..a1d28fdfd0 100644 --- a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts +++ b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts @@ -1,5 +1,5 @@ import ts from 'typescript'; -import { getOperationName } from 'oazapfts/generate'; +import { getOperationName } from '../oazapfts-compat'; import { capitalize, isQuery } from '../utils'; import type { OperationDefinition, EndpointOverrides, ConfigFile } from '../types'; import { getOverrides } from '../generate'; diff --git a/packages/rtk-query-codegen-openapi/src/index.ts b/packages/rtk-query-codegen-openapi/src/index.ts index 41a251d63d..bfb4499a26 100644 --- a/packages/rtk-query-codegen-openapi/src/index.ts +++ b/packages/rtk-query-codegen-openapi/src/index.ts @@ -50,10 +50,23 @@ export function parseConfig(fullConfig: ConfigFile) { /** * Enforces `oazapfts` to use the same TypeScript version as this module itself uses. * That should prevent enums from running out of sync if both libraries use different TS versions. + * + * In oazapfts v7, TypeScript is a peerDependency so this is typically a no-op, + * but we keep it for safety in environments with complex dependency resolution. */ function enforceOazapftsTsVersion(cb: () => T): T { - const ozTsPath = require.resolve('typescript', { paths: [require.resolve('oazapfts')] }); + let ozTsPath: string; + try { + ozTsPath = require.resolve('typescript', { paths: [require.resolve('oazapfts')] }); + } catch { + // In oazapfts v7+, TypeScript is a peerDependency and may resolve to the + // same path. If resolution fails, just run the callback directly. + return cb(); + } const tsPath = require.resolve('typescript'); + if (ozTsPath === tsPath) { + return cb(); + } const originalEntry = require.cache[ozTsPath]; try { require.cache[ozTsPath] = require.cache[tsPath]; diff --git a/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts b/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts new file mode 100644 index 0000000000..f71fe520f1 --- /dev/null +++ b/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts @@ -0,0 +1,921 @@ +/** + * Compatibility adapter for oazapfts v7. + * + * oazapfts v7 removed the `ApiGenerator` class and `oazapfts/generate` export. + * This module provides equivalent functionality using v7's public API + * (`createContext`, `UNSTABLE_cg`) combined with local implementations of + * helper functions that are no longer publicly exported. + * + * Type generation is done lazily (on demand) to match v6's behavior, + * ensuring that only types referenced by the generated endpoints are included. + */ +import { createContext, type OazapftsContext } from 'oazapfts/context'; +import { UNSTABLE_cg as cg } from 'oazapfts'; +import type { OpenAPIV3 } from 'openapi-types'; +import lodashCamelCase from 'lodash.camelcase'; +import ts from 'typescript'; + +const factory = ts.factory; + +export const createPropertyAssignment: typeof cg.createPropertyAssignment = + cg.createPropertyAssignment; +export const createQuestionToken: typeof cg.createQuestionToken = + cg.createQuestionToken; +export const keywordType: typeof cg.keywordType = cg.keywordType; +export const isValidIdentifier: typeof cg.isValidIdentifier = + cg.isValidIdentifier; + +export function isReference( + obj: unknown +): obj is OpenAPIV3.ReferenceObject { + return typeof obj === 'object' && obj !== null && '$ref' in (obj as any); +} + +export function getReferenceName( + obj: unknown +): string | undefined { + if (isReference(obj)) { + return obj.$ref.split('/').pop(); + } +} + +export function getOperationName( + verb: string, + path: string, + operationId?: string +): string { + if (operationId) { + const normalized = operationId.replace(/[^\w\s]/g, ' '); + let camelCased = lodashCamelCase(normalized); + if (camelCased) { + camelCased = camelCased.replace(/^[^a-zA-Z_$]+/, ''); + if (camelCased && isValidIdentifier(camelCased)) { + return camelCased; + } + } + } + const pathStr = path + .replace(/\{(.+?)\}/, 'by $1') + .replace(/\{(.+?)\}/, 'and $1'); + return lodashCamelCase(`${verb} ${pathStr}`); +} + +export function supportDeepObjects( + params: OpenAPIV3.ParameterObject[] +): OpenAPIV3.ParameterObject[] { + const res: OpenAPIV3.ParameterObject[] = []; + const merged: Record = {}; + for (const p of params) { + const m = /^(.+?)\[(.*?)\]/.exec(p.name); + if (!m) { + res.push(p); + continue; + } + const [, name, prop] = m; + let obj = merged[name]; + if (!obj) { + obj = merged[name] = { + name, + in: p.in, + style: 'deepObject', + schema: { + type: 'object', + properties: {} as Record, + }, + }; + res.push(obj); + } + obj.schema.properties[prop] = p.schema; + } + return res; +} + +const jsonMimeTypes: Record = { + '*/*': true, + 'application/json': true, +}; + +function isJsonMimeType(mime: string): boolean { + return !!jsonMimeTypes[mime] || /\bjson\b/i.test(mime); +} + +function isMimeType(s: string): boolean { + return /^[^/]+\/[^/]+$/.test(s); +} + +export function getSchemaFromContent( + content: Record +): OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject { + const contentType = Object.keys(content).find(isMimeType); + if (contentType) { + const { schema } = content[contentType]; + if (schema) { + return schema; + } + } + if ( + Object.keys(content).length === 0 || + Object.keys(content).some((type) => type.startsWith('text/')) + ) { + return { type: 'string' }; + } + return { type: 'string', format: 'binary' }; +} + +function toIdentifier(s: string, isTypeName = false): string { + let result = lodashCamelCase(s); + if (isTypeName && result) { + result = result.charAt(0).toUpperCase() + result.slice(1); + } + return result || s; +} + +type OnlyMode = 'readOnly' | 'writeOnly'; + +type EnumStyle = 'union' | 'enum' | 'as-const'; + +export interface OazapftsAdapterOptions { + enumStyle?: EnumStyle; + useEnumType?: boolean; + unionUndefined?: boolean; + mergeReadWriteOnly?: boolean; + useUnknown?: boolean; +} + +function resolveEnumStyle(opts: OazapftsAdapterOptions): EnumStyle { + if (opts.enumStyle !== undefined) return opts.enumStyle; + if (opts.useEnumType) return 'enum'; + return 'union'; +} + +export class OazapftsAdapter { + public ctx: OazapftsContext; + private opts: OazapftsAdapterOptions; + private effectiveEnumStyle: EnumStyle; + + constructor(doc: OpenAPIV3.Document, opts: OazapftsAdapterOptions) { + this.opts = opts; + this.effectiveEnumStyle = resolveEnumStyle(opts); + this.ctx = createContext(doc as any, { + enumStyle: this.effectiveEnumStyle, + useEnumType: opts.useEnumType, + unionUndefined: opts.unionUndefined, + mergeReadWriteOnly: opts.mergeReadWriteOnly, + useUnknown: opts.useUnknown, + }); + } + + get spec(): OpenAPIV3.Document { + return this.ctx.spec as unknown as OpenAPIV3.Document; + } + + get aliases() { + return this.ctx.aliases; + } + + get enumAliases(): ts.Statement[] { + return this.ctx.enumAliases; + } + + /** + * Initialize the adapter. Preprocesses components for discriminated union support. + */ + async init(): Promise { + this.preprocessComponents(); + this.makeDiscriminatorPropertiesRequired(); + } + + resolve(obj: T | OpenAPIV3.ReferenceObject): T { + if (!isReference(obj)) return obj; + const pathParts = obj.$ref.replace(/^#\//, '').split('/'); + const resolved = pathParts.reduce( + (current, part) => current?.[part], + this.ctx.spec + ); + if (resolved === undefined) { + throw new Error(`Can't resolve ${obj.$ref}`); + } + return resolved as T; + } + + resolveArray( + array?: Array + ): T[] { + return array ? array.map((el) => this.resolve(el)) : []; + } + + /** + * Preprocess components for discriminated union support. + */ + private preprocessComponents(): void { + const schemas = (this.ctx.spec as any).components?.schemas; + if (!schemas) return; + + const prefix = '#/components/schemas/'; + + // First pass: identify discriminating schemas + for (const name of Object.keys(schemas)) { + const schema = schemas[name]; + if (isReference(schema) || typeof schema === 'boolean') continue; + if (schema.discriminator && !schema.oneOf && !schema.anyOf) { + this.ctx.discriminatingSchemas.add(schema); + } + } + + // Second pass: make mappings explicit + for (const name of Object.keys(schemas)) { + const schema = schemas[name]; + if (isReference(schema) || typeof schema === 'boolean' || !schema.allOf) continue; + + for (const childSchema of schema.allOf) { + if (!isReference(childSchema)) continue; + const resolved = this.resolve(childSchema); + if (!this.ctx.discriminatingSchemas.has(resolved as any)) continue; + + const refBasename = childSchema.$ref.split('/').pop()!; + const discriminatingSchema = schemas[refBasename]; + if (isReference(discriminatingSchema)) continue; + + const discriminator = discriminatingSchema.discriminator; + if (!discriminator) continue; + + const refs = Object.values(discriminator.mapping || {}); + if (refs.includes(prefix + name)) continue; + + if (!discriminator.mapping) { + discriminator.mapping = {}; + } + discriminator.mapping[name] = prefix + name; + } + } + } + + getResponseType( + responses?: OpenAPIV3.ResponsesObject + ): 'json' | 'text' | 'blob' { + if (!responses) return 'text'; + + const resolvedResponses = Object.values(responses).map((response) => + this.resolve(response) + ); + + if ( + !resolvedResponses.some( + (res) => Object.keys(res.content ?? {}).length > 0 + ) + ) { + return 'text'; + } + + const hasJson = resolvedResponses.some((response) => { + const responseMimeTypes = Object.keys(response.content ?? {}); + return responseMimeTypes.some(isJsonMimeType); + }); + + if (hasJson) return 'json'; + + if ( + resolvedResponses.some((res) => + Object.keys(res.content ?? {}).some((type) => type.startsWith('text/')) + ) + ) { + return 'text'; + } + + return 'blob'; + } + + getTypeFromResponse( + response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject, + onlyMode?: OnlyMode + ): ts.TypeNode { + const resolved = this.resolve(response); + if (!resolved.content) return keywordType.void; + const schema = getSchemaFromContent(resolved.content); + return this.getTypeFromSchema(schema, undefined, onlyMode); + } + + getTypeFromSchema( + schema?: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, + name?: string, + onlyMode?: OnlyMode + ): ts.TypeNode { + if (schema === undefined) { + return this.opts.useUnknown ? keywordType.unknown : keywordType.any; + } + + if (isReference(schema)) { + return this.getRefAlias(schema, onlyMode); + } + + const type = this.resolveBaseSchema(schema, name, onlyMode); + if (schema.nullable) { + return factory.createUnionTypeNode([type, keywordType.null]); + } + return type; + } + + /** + * Create or look up a type alias for a $ref schema. + * Lazily creates type aliases and stores them in ctx.aliases/ctx.refs. + */ + private getRefAlias( + obj: OpenAPIV3.ReferenceObject, + onlyMode?: OnlyMode + ): ts.TypeNode { + const $ref = obj.$ref; + + if (!this.ctx.refs[$ref]) { + const schema = this.resolve(obj); + const name = + (typeof schema === 'object' && schema.title) || + $ref.split('/').pop()!; + const identifier = toIdentifier(name, true); + + if ( + this.effectiveEnumStyle !== 'union' && + this.isNamedEnumSchema(schema) + ) { + if (this.effectiveEnumStyle === 'enum') { + this.generateTrueEnum(schema, identifier); + } else if (this.effectiveEnumStyle === 'as-const') { + this.generateAsConstEnum(schema, identifier); + } + const typeRef = factory.createTypeReferenceNode( + this.getUniqueAlias(identifier), + undefined + ); + this.ctx.refs[$ref] = { + base: typeRef, + readOnly: undefined, + writeOnly: undefined, + }; + return typeRef; + } + + const isDiscriminating = this.ctx.discriminatingSchemas.has(schema as any); + const alias = this.getUniqueAlias( + isDiscriminating ? identifier + 'Base' : identifier + ); + + this.ctx.refs[$ref] = { + base: factory.createTypeReferenceNode(alias, undefined), + readOnly: undefined, + writeOnly: undefined, + }; + + const type = this.resolveBaseSchema(schema, undefined, undefined, $ref); + const nullable = schema.nullable + ? factory.createUnionTypeNode([type, keywordType.null]) + : type; + + this.ctx.aliases.push( + factory.createTypeAliasDeclaration( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + alias, + undefined, + nullable + ) + ); + + if (!this.opts.mergeReadWriteOnly) { + const { hasReadOnly, hasWriteOnly } = this.checkSchemaOnlyMode(schema); + + if (hasReadOnly) { + const readOnlyAlias = this.getUniqueAlias( + toIdentifier(name + 'Read', true) + ); + const readOnlyType = this.resolveBaseSchema(schema, undefined, 'readOnly'); + const readOnlyNullable = schema.nullable + ? factory.createUnionTypeNode([readOnlyType, keywordType.null]) + : readOnlyType; + this.ctx.refs[$ref].readOnly = factory.createTypeReferenceNode( + readOnlyAlias, + undefined + ); + this.ctx.aliases.push( + factory.createTypeAliasDeclaration( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + readOnlyAlias, + undefined, + readOnlyNullable + ) + ); + } + + if (hasWriteOnly) { + const writeOnlyAlias = this.getUniqueAlias( + toIdentifier(name + 'Write', true) + ); + const writeOnlyType = this.resolveBaseSchema(schema, undefined, 'writeOnly'); + const writeOnlyNullable = schema.nullable + ? factory.createUnionTypeNode([writeOnlyType, keywordType.null]) + : writeOnlyType; + this.ctx.refs[$ref].writeOnly = factory.createTypeReferenceNode( + writeOnlyAlias, + undefined + ); + this.ctx.aliases.push( + factory.createTypeAliasDeclaration( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + writeOnlyAlias, + undefined, + writeOnlyNullable + ) + ); + } + } + } + + const entry = this.ctx.refs[$ref]; + return entry[onlyMode || 'base'] ?? entry.base; + } + + private isNamedEnumSchema(schema: OpenAPIV3.SchemaObject): boolean { + if (typeof schema !== 'object') return false; + if (schema.enum && schema.enum.length > 0) return true; + if (schema.type === 'array' && schema.items && !isReference(schema.items) && schema.items.enum) return true; + return false; + } + + private generateTrueEnum( + schema: OpenAPIV3.SchemaObject, + name: string + ): void { + const values = schema.enum || (schema.items && !isReference(schema.items) ? schema.items.enum : undefined); + if (!values) return; + + const customNames = this.getCustomNames(schema); + const alias = this.getUniqueAlias(name); + + const members = values.map((value: any, index: number) => { + const memberName = customNames?.[index] ?? String(value); + return factory.createEnumMember( + isValidIdentifier(memberName) + ? factory.createIdentifier(memberName) + : factory.createStringLiteral(memberName), + typeof value === 'string' + ? factory.createStringLiteral(value) + : factory.createNumericLiteral(String(value)) + ); + }); + + this.ctx.enumAliases.push( + factory.createEnumDeclaration( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + alias, + members + ) + ); + } + + private generateAsConstEnum( + schema: OpenAPIV3.SchemaObject, + name: string + ): void { + const values = schema.enum || (schema.items && !isReference(schema.items) ? schema.items.enum : undefined); + if (!values) return; + + const customNames = this.getCustomNames(schema); + const alias = this.getUniqueAlias(name); + + // Generate: export const Foo = { Bar: 'bar', Baz: 'baz' } as const; + const properties = values.map((value: any, index: number) => { + const memberName = customNames?.[index] ?? toIdentifier(String(value), true); + const memberKey = isValidIdentifier(memberName) + ? factory.createIdentifier(memberName) + : factory.createStringLiteral(memberName); + const memberValue = + typeof value === 'string' + ? factory.createStringLiteral(value) + : factory.createNumericLiteral(String(value)); + return factory.createPropertyAssignment(memberKey, memberValue); + }); + + const objectLiteral = factory.createObjectLiteralExpression(properties, true); + const asConst = factory.createAsExpression( + objectLiteral, + factory.createTypeReferenceNode('const') + ); + + this.ctx.enumAliases.push( + factory.createVariableStatement( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + factory.createVariableDeclarationList( + [factory.createVariableDeclaration(alias, undefined, undefined, asConst)], + ts.NodeFlags.Const + ) + ) + ); + + // Generate: export type Foo = typeof Foo[keyof typeof Foo]; + const typeofExpr = factory.createTypeQueryNode(factory.createIdentifier(alias)); + const keyofTypeof = factory.createTypeOperatorNode( + ts.SyntaxKind.KeyofKeyword, + typeofExpr + ); + const indexedAccess = factory.createIndexedAccessTypeNode(typeofExpr, keyofTypeof); + + this.ctx.enumAliases.push( + factory.createTypeAliasDeclaration( + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + alias, + undefined, + indexedAccess + ) + ); + } + + private getCustomNames(schema: OpenAPIV3.SchemaObject): string[] | undefined { + const ext = schema as any; + return ext['x-enumNames'] || ext['x-enum-varnames'] || undefined; + } + + private getUniqueAlias(name: string): string { + const count = this.ctx.typeAliases[name]; + if (count === undefined) { + this.ctx.typeAliases[name] = 1; + return name; + } + this.ctx.typeAliases[name] = count + 1; + return `${name}${count + 1}`; + } + + private checkSchemaOnlyMode(schema: OpenAPIV3.SchemaObject): { + hasReadOnly: boolean; + hasWriteOnly: boolean; + } { + if (this.opts.mergeReadWriteOnly) { + return { hasReadOnly: false, hasWriteOnly: false }; + } + + let hasReadOnly = false; + let hasWriteOnly = false; + + if (schema.properties) { + for (const prop of Object.values(schema.properties)) { + if (isReference(prop)) continue; + if (prop.readOnly) hasReadOnly = true; + if (prop.writeOnly) hasWriteOnly = true; + } + } + + return { hasReadOnly, hasWriteOnly }; + } + + /** + * For schemas with discriminator + oneOf/anyOf: + * 1. Populate implicit discriminator mappings (using enum values from child schemas) + * 2. Make the discriminator property required in child schemas + */ + private makeDiscriminatorPropertiesRequired(): void { + const schemas = (this.ctx.spec as any).components?.schemas; + if (!schemas) return; + const prefix = '#/components/schemas/'; + + for (const name of Object.keys(schemas)) { + const schema = schemas[name]; + if (isReference(schema) || typeof schema === 'boolean') continue; + if (!schema.discriminator) continue; + + const discriminator = schema.discriminator; + const propName = discriminator.propertyName; + const refs = schema.oneOf || schema.anyOf || []; + + for (const ref of refs) { + if (!isReference(ref)) continue; + const childName = ref.$ref.split('/').pop(); + if (!childName) continue; + const childSchema = schemas[childName]; + if (!childSchema || isReference(childSchema) || typeof childSchema === 'boolean') continue; + + // Make discriminator property required + if (!childSchema.required) childSchema.required = []; + if (!childSchema.required.includes(propName)) { + childSchema.required.push(propName); + } + + // Populate implicit mapping if not already present + if (!discriminator.mapping) { + discriminator.mapping = {}; + } + const alreadyMapped = Object.values(discriminator.mapping).some( + (v) => (v as string).split('/').pop() === childName + ); + if (!alreadyMapped) { + // Use the enum value from the child's discriminator property if available + const discProp = childSchema.properties?.[propName]; + if ( + discProp && + !isReference(discProp) && + discProp.enum?.length === 1 + ) { + discriminator.mapping[String(discProp.enum[0])] = + prefix + childName; + } else { + discriminator.mapping[childName] = prefix + childName; + } + } + } + } + } + + private resolveBaseSchema( + schema: OpenAPIV3.SchemaObject, + _name?: string, + onlyMode?: OnlyMode, + currentRef?: string + ): ts.TypeNode { + if (schema.oneOf) { + return this.resolveCompositionSchema(schema.oneOf, 'oneOf', schema, onlyMode); + } + + if (schema.anyOf) { + return this.resolveCompositionSchema(schema.anyOf, 'anyOf', schema, onlyMode); + } + + if (schema.allOf) { + return this.resolveAllOfSchema(schema, onlyMode, currentRef); + } + + if (schema.enum) { + return this.createEnumTypeNode(schema.enum); + } + + switch (schema.type) { + case 'string': + if (schema.format === 'binary') { + return factory.createTypeReferenceNode('Blob'); + } + return keywordType.string; + case 'number': + case 'integer': + return keywordType.number; + case 'boolean': + return keywordType.boolean; + case 'array': + if (schema.items) { + const itemType = this.getTypeFromSchema( + schema.items, + undefined, + onlyMode + ); + return factory.createArrayTypeNode(itemType); + } + return factory.createArrayTypeNode(keywordType.any); + case 'object': + return this.resolveObjectSchema(schema, onlyMode, true); + default: + return this.resolveObjectSchema(schema, onlyMode, false); + } + } + + /** + * Handle oneOf/anyOf composition, with discriminator support. + * When a discriminator is present, each variant becomes + * `{ discriminatorProp: "value" } & ChildType`. + */ + private resolveCompositionSchema( + variants: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[], + _kind: 'oneOf' | 'anyOf', + parentSchema: OpenAPIV3.SchemaObject, + onlyMode?: OnlyMode + ): ts.TypeNode { + const discriminator = parentSchema.discriminator; + + const types = variants.map((s) => { + const childType = this.getTypeFromSchema(s, undefined, onlyMode); + + if (discriminator && isReference(s)) { + const value = this.getDiscriminatorValueForRef(discriminator, s.$ref); + if (value !== undefined) { + const discLiteral = factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier(discriminator.propertyName), + undefined, + factory.createLiteralTypeNode(factory.createStringLiteral(value)) + ), + ]); + return factory.createIntersectionTypeNode([discLiteral, childType]); + } + } + + return childType; + }); + + return types.length === 1 ? types[0] : factory.createUnionTypeNode(types); + } + + /** + * Handle allOf composition, with discriminator + own properties support. + * When a schema has both allOf and its own properties, the result is + * an intersection of all allOf items plus a type literal for own properties. + * When an allOf item references a discriminating schema, a discriminator + * literal type is prepended. + */ + private resolveAllOfSchema( + schema: OpenAPIV3.SchemaObject, + onlyMode?: OnlyMode, + currentRef?: string + ): ts.TypeNode { + const types: ts.TypeNode[] = []; + + for (const s of schema.allOf!) { + // Check if this allOf item references a discriminating schema + if (isReference(s)) { + const resolved = this.resolve(s); + if ( + this.ctx.discriminatingSchemas.has(resolved as any) && + resolved.discriminator + ) { + const value = currentRef + ? this.getDiscriminatorValueForRef( + resolved.discriminator, + currentRef + ) + : undefined; + if (value !== undefined) { + types.push( + factory.createTypeLiteralNode([ + factory.createPropertySignature( + undefined, + factory.createIdentifier( + resolved.discriminator.propertyName + ), + undefined, + factory.createLiteralTypeNode( + factory.createStringLiteral(value) + ) + ), + ]) + ); + } + } + } + types.push(this.getTypeFromSchema(s, undefined, onlyMode)); + } + + if (schema.properties) { + types.push(this.resolveObjectSchema(schema, onlyMode)); + } + + return types.length === 1 + ? types[0] + : factory.createIntersectionTypeNode(types); + } + + /** + * Look up the discriminator value for a given $ref in a discriminator mapping. + */ + private getDiscriminatorValueForRef( + discriminator: OpenAPIV3.DiscriminatorObject, + targetRef: string + ): string | undefined { + const mapping = discriminator.mapping || {}; + const targetName = targetRef.split('/').pop(); + for (const [key, ref] of Object.entries(mapping)) { + const refName = ref.split('/').pop(); + if (refName === targetName) return key; + } + return undefined; + } + + private resolveObjectSchema( + schema: OpenAPIV3.SchemaObject, + onlyMode?: OnlyMode, + isExplicitObject = false + ): ts.TypeNode { + if (schema.properties) { + const members: ts.TypeElement[] = Object.entries(schema.properties) + .filter(([, prop]) => { + if (isReference(prop)) return true; + if (this.opts.mergeReadWriteOnly) return true; + switch (onlyMode) { + case 'readOnly': + return !prop.writeOnly; + case 'writeOnly': + return !prop.readOnly; + default: + // Base type: exclude both readOnly and writeOnly props + return !prop.readOnly && !prop.writeOnly; + } + }) + .map(([name, prop]) => { + const propType = this.getTypeFromSchema(prop, undefined, onlyMode); + const isRequired = schema.required?.includes(name) ?? false; + const optionalType = + !isRequired && this.opts.unionUndefined + ? factory.createUnionTypeNode([propType, keywordType.undefined]) + : propType; + const signature = factory.createPropertySignature( + undefined, + isValidIdentifier(name) + ? factory.createIdentifier(name) + : factory.createStringLiteral(name), + isRequired + ? undefined + : factory.createToken(ts.SyntaxKind.QuestionToken), + optionalType + ); + const resolvedProp = isReference(prop) ? this.resolve(prop) : prop; + if ( + typeof resolvedProp === 'object' && + 'description' in resolvedProp && + resolvedProp.description + ) { + const description = resolvedProp.description.replace('*/', '*\\/'); + return ts.addSyntheticLeadingComment( + signature, + ts.SyntaxKind.MultiLineCommentTrivia, + `* ${description} `, + true + ); + } + return signature; + }); + + if (schema.additionalProperties) { + const valueType = + schema.additionalProperties === true + ? keywordType.any + : this.getTypeFromSchema( + schema.additionalProperties, + undefined, + onlyMode + ); + members.push( + factory.createIndexSignature( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + 'key', + undefined, + keywordType.string + ), + ], + valueType + ) + ); + } + + return factory.createTypeLiteralNode(members); + } + + if (schema.additionalProperties) { + const valueType = + schema.additionalProperties === true + ? keywordType.any + : this.getTypeFromSchema( + schema.additionalProperties, + undefined, + onlyMode + ); + return factory.createTypeLiteralNode([ + factory.createIndexSignature( + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + 'key', + undefined, + keywordType.string + ), + ], + valueType + ), + ]); + } + + if (isExplicitObject) { + return factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword); + } + return this.opts.useUnknown ? keywordType.unknown : keywordType.any; + } + + private createEnumTypeNode( + values: Array + ): ts.TypeNode { + const types = values.map((v) => { + if (v === null) return keywordType.null; + switch (typeof v) { + case 'string': + return factory.createLiteralTypeNode(factory.createStringLiteral(v)); + case 'number': + return factory.createLiteralTypeNode( + factory.createNumericLiteral(String(v)) + ); + case 'boolean': + return factory.createLiteralTypeNode( + v ? factory.createTrue() : factory.createFalse() + ); + default: + return keywordType.string; + } + }); + return types.length === 1 ? types[0] : factory.createUnionTypeNode(types); + } +} diff --git a/packages/rtk-query-codegen-openapi/src/types.ts b/packages/rtk-query-codegen-openapi/src/types.ts index bdff603282..030845d34a 100644 --- a/packages/rtk-query-codegen-openapi/src/types.ts +++ b/packages/rtk-query-codegen-openapi/src/types.ts @@ -150,8 +150,18 @@ export interface OutputFileOptions extends Partial { /** * If passed as true it will generate TS enums instead of union of strings * @default false + * @deprecated Use `enumStyle: "enum"` instead. */ useEnumType?: boolean; + /** + * Controls how enums are generated in TypeScript. + * Takes precedence over `useEnumType` if both are specified. + * - `'union'` (default): Union types, e.g. `type Status = 'active' | 'inactive'` + * - `'enum'`: TypeScript enums, e.g. `enum Status { Active = 'active' }` + * - `'as-const'`: Const objects with companion types, e.g. `const Status = { ... } as const` + * @default 'union' + */ + enumStyle?: 'union' | 'enum' | 'as-const'; } /** diff --git a/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap b/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap index 6e7a7c29d7..0b42c1081c 100644 --- a/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap +++ b/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap @@ -3234,12 +3234,12 @@ export type CreateAllowanceApiArg = { allowance: Allowance; }; export type EngineeringAllowance = { - allowance_type?: "engineering"; + allowance_type: "engineering"; distribution?: "MARECO" | "LINEAR"; capacity_speed_limit?: number; }; export type StandardAllowance = { - allowance_type?: "standard"; + allowance_type: "standard"; default_value?: number; ranges?: { min?: number; diff --git a/yarn.lock b/yarn.lock index 98e3fb4c30..d91483dd51 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7824,7 +7824,7 @@ __metadata: lodash.camelcase: "npm:^4.3.0" msw: "npm:^2.1.5" node-fetch: "npm:^3.3.2" - oazapfts: "npm:^6.4.0" + oazapfts: "npm:^7.3.0" openapi-types: "npm:^9.1.0" prettier: "npm:^3.2.5" pretty-quick: "npm:^4.0.0" @@ -21296,6 +21296,13 @@ __metadata: languageName: node linkType: hard +"lodash@npm:^4.17.23": + version: 4.17.23 + resolution: "lodash@npm:4.17.23" + checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 + languageName: node + linkType: hard + "log-symbols@npm:^1.0.2": version: 1.0.2 resolution: "log-symbols@npm:1.0.2" @@ -23726,21 +23733,20 @@ __metadata: languageName: node linkType: hard -"oazapfts@npm:^6.4.0": - version: 6.4.0 - resolution: "oazapfts@npm:6.4.0" +"oazapfts@npm:^7.3.0": + version: 7.3.0 + resolution: "oazapfts@npm:7.3.0" dependencies: "@apidevtools/swagger-parser": "npm:^12.1.0" - lodash: "npm:^4.17.21" + lodash: "npm:^4.17.23" minimist: "npm:^1.2.8" - swagger2openapi: "npm:^7.0.8" tapable: "npm:^2.3.0" typescript: "npm:^5.9.3" peerDependencies: "@oazapfts/runtime": "*" bin: - oazapfts: cli.js - checksum: 10/c52d1a8d786e41b5c228d6e2a52923ac58fde86eef44c1fb31dc65171788ae7c36d6b23ab53acea91fcc20a3b6121051bc7e90a6ebb633db54380562a8a16664 + oazapfts: dist/cli.js + checksum: 10/a174f0ea118a6ed5b8a29d30d093b11a8a775cecabefcf95e31aca3219780864cc5a76cb749ec9d352bf778c7f84d789a16443ba19aac4fe2bd4031ca6cab0ef languageName: node linkType: hard @@ -30499,7 +30505,7 @@ __metadata: languageName: node linkType: hard -"swagger2openapi@npm:^7.0.4, swagger2openapi@npm:^7.0.8": +"swagger2openapi@npm:^7.0.4": version: 7.0.8 resolution: "swagger2openapi@npm:7.0.8" dependencies: From 6f8e661e0bbd21c894bbd81a39a002a4b0f3280c Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Thu, 19 Feb 2026 13:59:55 +0900 Subject: [PATCH 02/10] delete enumStyle and focus migrate v7 --- .../rtk-query-codegen-openapi/src/generate.ts | 2 - .../src/oazapfts-compat.ts | 133 ------------------ .../rtk-query-codegen-openapi/src/types.ts | 10 -- 3 files changed, 145 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index f56c9e030d..1795cde85b 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -167,7 +167,6 @@ export async function generateApi( flattenArg = false, includeDefault = false, useEnumType = false, - enumStyle, mergeReadWriteOnly = false, httpResolverOptions, useUnknown = false, @@ -178,7 +177,6 @@ export async function generateApi( const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions)); const apiGen = new OazapftsAdapter(v3Doc, { - enumStyle, useEnumType, unionUndefined, mergeReadWriteOnly, diff --git a/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts b/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts index f71fe520f1..a48d6b0558 100644 --- a/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts +++ b/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts @@ -132,32 +132,20 @@ function toIdentifier(s: string, isTypeName = false): string { type OnlyMode = 'readOnly' | 'writeOnly'; -type EnumStyle = 'union' | 'enum' | 'as-const'; - export interface OazapftsAdapterOptions { - enumStyle?: EnumStyle; useEnumType?: boolean; unionUndefined?: boolean; mergeReadWriteOnly?: boolean; useUnknown?: boolean; } -function resolveEnumStyle(opts: OazapftsAdapterOptions): EnumStyle { - if (opts.enumStyle !== undefined) return opts.enumStyle; - if (opts.useEnumType) return 'enum'; - return 'union'; -} - export class OazapftsAdapter { public ctx: OazapftsContext; private opts: OazapftsAdapterOptions; - private effectiveEnumStyle: EnumStyle; constructor(doc: OpenAPIV3.Document, opts: OazapftsAdapterOptions) { this.opts = opts; - this.effectiveEnumStyle = resolveEnumStyle(opts); this.ctx = createContext(doc as any, { - enumStyle: this.effectiveEnumStyle, useEnumType: opts.useEnumType, unionUndefined: opts.unionUndefined, mergeReadWriteOnly: opts.mergeReadWriteOnly, @@ -332,27 +320,6 @@ export class OazapftsAdapter { $ref.split('/').pop()!; const identifier = toIdentifier(name, true); - if ( - this.effectiveEnumStyle !== 'union' && - this.isNamedEnumSchema(schema) - ) { - if (this.effectiveEnumStyle === 'enum') { - this.generateTrueEnum(schema, identifier); - } else if (this.effectiveEnumStyle === 'as-const') { - this.generateAsConstEnum(schema, identifier); - } - const typeRef = factory.createTypeReferenceNode( - this.getUniqueAlias(identifier), - undefined - ); - this.ctx.refs[$ref] = { - base: typeRef, - readOnly: undefined, - writeOnly: undefined, - }; - return typeRef; - } - const isDiscriminating = this.ctx.discriminatingSchemas.has(schema as any); const alias = this.getUniqueAlias( isDiscriminating ? identifier + 'Base' : identifier @@ -431,106 +398,6 @@ export class OazapftsAdapter { return entry[onlyMode || 'base'] ?? entry.base; } - private isNamedEnumSchema(schema: OpenAPIV3.SchemaObject): boolean { - if (typeof schema !== 'object') return false; - if (schema.enum && schema.enum.length > 0) return true; - if (schema.type === 'array' && schema.items && !isReference(schema.items) && schema.items.enum) return true; - return false; - } - - private generateTrueEnum( - schema: OpenAPIV3.SchemaObject, - name: string - ): void { - const values = schema.enum || (schema.items && !isReference(schema.items) ? schema.items.enum : undefined); - if (!values) return; - - const customNames = this.getCustomNames(schema); - const alias = this.getUniqueAlias(name); - - const members = values.map((value: any, index: number) => { - const memberName = customNames?.[index] ?? String(value); - return factory.createEnumMember( - isValidIdentifier(memberName) - ? factory.createIdentifier(memberName) - : factory.createStringLiteral(memberName), - typeof value === 'string' - ? factory.createStringLiteral(value) - : factory.createNumericLiteral(String(value)) - ); - }); - - this.ctx.enumAliases.push( - factory.createEnumDeclaration( - [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - alias, - members - ) - ); - } - - private generateAsConstEnum( - schema: OpenAPIV3.SchemaObject, - name: string - ): void { - const values = schema.enum || (schema.items && !isReference(schema.items) ? schema.items.enum : undefined); - if (!values) return; - - const customNames = this.getCustomNames(schema); - const alias = this.getUniqueAlias(name); - - // Generate: export const Foo = { Bar: 'bar', Baz: 'baz' } as const; - const properties = values.map((value: any, index: number) => { - const memberName = customNames?.[index] ?? toIdentifier(String(value), true); - const memberKey = isValidIdentifier(memberName) - ? factory.createIdentifier(memberName) - : factory.createStringLiteral(memberName); - const memberValue = - typeof value === 'string' - ? factory.createStringLiteral(value) - : factory.createNumericLiteral(String(value)); - return factory.createPropertyAssignment(memberKey, memberValue); - }); - - const objectLiteral = factory.createObjectLiteralExpression(properties, true); - const asConst = factory.createAsExpression( - objectLiteral, - factory.createTypeReferenceNode('const') - ); - - this.ctx.enumAliases.push( - factory.createVariableStatement( - [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - factory.createVariableDeclarationList( - [factory.createVariableDeclaration(alias, undefined, undefined, asConst)], - ts.NodeFlags.Const - ) - ) - ); - - // Generate: export type Foo = typeof Foo[keyof typeof Foo]; - const typeofExpr = factory.createTypeQueryNode(factory.createIdentifier(alias)); - const keyofTypeof = factory.createTypeOperatorNode( - ts.SyntaxKind.KeyofKeyword, - typeofExpr - ); - const indexedAccess = factory.createIndexedAccessTypeNode(typeofExpr, keyofTypeof); - - this.ctx.enumAliases.push( - factory.createTypeAliasDeclaration( - [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - alias, - undefined, - indexedAccess - ) - ); - } - - private getCustomNames(schema: OpenAPIV3.SchemaObject): string[] | undefined { - const ext = schema as any; - return ext['x-enumNames'] || ext['x-enum-varnames'] || undefined; - } - private getUniqueAlias(name: string): string { const count = this.ctx.typeAliases[name]; if (count === undefined) { diff --git a/packages/rtk-query-codegen-openapi/src/types.ts b/packages/rtk-query-codegen-openapi/src/types.ts index 030845d34a..bdff603282 100644 --- a/packages/rtk-query-codegen-openapi/src/types.ts +++ b/packages/rtk-query-codegen-openapi/src/types.ts @@ -150,18 +150,8 @@ export interface OutputFileOptions extends Partial { /** * If passed as true it will generate TS enums instead of union of strings * @default false - * @deprecated Use `enumStyle: "enum"` instead. */ useEnumType?: boolean; - /** - * Controls how enums are generated in TypeScript. - * Takes precedence over `useEnumType` if both are specified. - * - `'union'` (default): Union types, e.g. `type Status = 'active' | 'inactive'` - * - `'enum'`: TypeScript enums, e.g. `enum Status { Active = 'active' }` - * - `'as-const'`: Const objects with companion types, e.g. `const Status = { ... } as const` - * @default 'union' - */ - enumStyle?: 'union' | 'enum' | 'as-const'; } /** From fbef0b9de52564bbe65191285239bceaf06dc8bf Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Thu, 19 Feb 2026 14:05:12 +0900 Subject: [PATCH 03/10] chore: downgrade oazapfts to v7.0.0 in package.json and yarn.lock - Updated oazapfts dependency from v7.3.0 to v7.0.0 in both package.json and yarn.lock to ensure compatibility with existing code. - Adjusted related versioning and checksums accordingly. --- packages/rtk-query-codegen-openapi/package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/package.json b/packages/rtk-query-codegen-openapi/package.json index 1f77c76331..9f12774aa5 100644 --- a/packages/rtk-query-codegen-openapi/package.json +++ b/packages/rtk-query-codegen-openapi/package.json @@ -78,7 +78,7 @@ "@apidevtools/swagger-parser": "^10.1.1", "commander": "^6.2.0", "lodash.camelcase": "^4.3.0", - "oazapfts": "^7.3.0", + "oazapfts": "^7.0.0", "prettier": "^3.2.5", "semver": "^7.3.5", "swagger2openapi": "^7.0.4", diff --git a/yarn.lock b/yarn.lock index d91483dd51..747dc2188b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7824,7 +7824,7 @@ __metadata: lodash.camelcase: "npm:^4.3.0" msw: "npm:^2.1.5" node-fetch: "npm:^3.3.2" - oazapfts: "npm:^7.3.0" + oazapfts: "npm:^7.0.0" openapi-types: "npm:^9.1.0" prettier: "npm:^3.2.5" pretty-quick: "npm:^4.0.0" @@ -23733,9 +23733,9 @@ __metadata: languageName: node linkType: hard -"oazapfts@npm:^7.3.0": - version: 7.3.0 - resolution: "oazapfts@npm:7.3.0" +"oazapfts@npm:^7.0.0": + version: 7.4.0 + resolution: "oazapfts@npm:7.4.0" dependencies: "@apidevtools/swagger-parser": "npm:^12.1.0" lodash: "npm:^4.17.23" @@ -23746,7 +23746,7 @@ __metadata: "@oazapfts/runtime": "*" bin: oazapfts: dist/cli.js - checksum: 10/a174f0ea118a6ed5b8a29d30d093b11a8a775cecabefcf95e31aca3219780864cc5a76cb749ec9d352bf778c7f84d789a16443ba19aac4fe2bd4031ca6cab0ef + checksum: 10/354a38ce64f26936d7053e1b9c9a2a373d1ca21187c5e683e578c8be558ea391c6b9f1c390af54930120e19376fb58fdd3000036b02342aa0d747e6047c05a79 languageName: node linkType: hard From 903e461b1e7f53d8f577cf63f121ebcaa31720a6 Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Fri, 27 Feb 2026 13:31:09 +0900 Subject: [PATCH 04/10] chore: update oazapfts and refactor code generation - Updated oazapfts dependency to v7.5.0-alpha.3 in package.json and yarn.lock. - Introduced new utility functions for operation name generation and deep object support. - Removed the deprecated oazapfts-compat.ts file and refactored generate.ts to utilize the new public API. - Enhanced type generation logic to support TypeScript enums based on the useEnumType option. - Updated tests to reflect changes in type generation and added new tests for enum generation. --- .../rtk-query-codegen-openapi/package.json | 3 +- .../rtk-query-codegen-openapi/src/generate.ts | 202 ++++- .../src/generators/react-hooks.ts | 2 +- .../src/oazapfts-compat.ts | 788 ------------------ .../src/utils/getOperationName.ts | 25 + .../src/utils/index.ts | 1 + .../generateEndpoints.test.ts.snap | 50 ++ .../test/generateEndpoints.test.ts | 16 + yarn.lock | 30 +- 9 files changed, 286 insertions(+), 831 deletions(-) delete mode 100644 packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts create mode 100644 packages/rtk-query-codegen-openapi/src/utils/getOperationName.ts diff --git a/packages/rtk-query-codegen-openapi/package.json b/packages/rtk-query-codegen-openapi/package.json index 9f12774aa5..d7191f10da 100644 --- a/packages/rtk-query-codegen-openapi/package.json +++ b/packages/rtk-query-codegen-openapi/package.json @@ -76,9 +76,10 @@ }, "dependencies": { "@apidevtools/swagger-parser": "^10.1.1", + "@oazapfts/resolve": "^1.0.0", "commander": "^6.2.0", "lodash.camelcase": "^4.3.0", - "oazapfts": "^7.0.0", + "oazapfts": "7.5.0-alpha.3", "prettier": "^3.2.5", "semver": "^7.3.5", "swagger2openapi": "^7.0.4", diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 1795cde85b..b5018e3522 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -2,21 +2,13 @@ import camelCase from 'lodash.camelcase'; import path from 'node:path'; import type { OpenAPIV3 } from 'openapi-types'; import ts from 'typescript'; +import { UNSTABLE_cg as cg } from 'oazapfts'; +import { createContext, withMode, type OazapftsContext, type OnlyMode } from 'oazapfts/context'; +import { getTypeFromSchema, getResponseType, getSchemaFromContent } from 'oazapfts/generate'; +import { resolve, resolveArray, isReference, getReferenceName } from '@oazapfts/resolve'; import type { ObjectPropertyDefinitions } from './codegen'; import { generateCreateApiCall, generateEndpointDefinition, generateImportNode, generateTagTypes } from './codegen'; import { generateReactHooks } from './generators/react-hooks'; -import { - OazapftsAdapter, - createPropertyAssignment, - createQuestionToken, - getOperationName as _getOperationName, - getReferenceName, - getSchemaFromContent, - isReference, - isValidIdentifier, - keywordType, - supportDeepObjects, -} from './oazapfts-compat'; import type { EndpointMatcher, EndpointOverrides, @@ -26,12 +18,152 @@ import type { ParameterMatcher, TextMatcher, } from './types'; -import { capitalize, getOperationDefinitions, getV3Doc, removeUndefined, isQuery as testIsQuery } from './utils'; +import { + capitalize, + getOperationDefinitions, + getOperationName as _getOperationName, + getV3Doc, + removeUndefined, + isQuery as testIsQuery, +} from './utils'; import { factory } from './utils/factory'; +const { createPropertyAssignment, createQuestionToken, keywordType, isValidIdentifier } = cg; + const generatedApiName = 'injectedRtkApi'; const v3DocCache: Record = {}; +function supportDeepObjects( + params: OpenAPIV3.ParameterObject[] +): OpenAPIV3.ParameterObject[] { + const res: OpenAPIV3.ParameterObject[] = []; + const merged: Record = {}; + for (const p of params) { + const m = /^(.+?)\[(.*?)\]/.exec(p.name); + if (!m) { + res.push(p); + continue; + } + const [, name, prop] = m; + let obj = merged[name]; + if (!obj) { + obj = merged[name] = { + name, + in: p.in, + style: 'deepObject', + schema: { + type: 'object', + properties: {} as Record, + }, + }; + res.push(obj); + } + obj.schema.properties[prop] = p.schema; + } + return res; +} + +function preprocessComponents(ctx: OazapftsContext): void { + const schemas = (ctx.spec as any).components?.schemas; + if (!schemas) return; + + const prefix = '#/components/schemas/'; + + for (const name of Object.keys(schemas)) { + const schema = schemas[name]; + if (isReference(schema) || typeof schema === 'boolean') continue; + if (schema.discriminator && !schema.oneOf && !schema.anyOf) { + ctx.discriminatingSchemas.add(schema); + } + } + + for (const name of Object.keys(schemas)) { + const schema = schemas[name]; + if (isReference(schema) || typeof schema === 'boolean' || !schema.allOf) continue; + + for (const childSchema of schema.allOf) { + if (!isReference(childSchema)) continue; + const resolved = resolve(childSchema, ctx); + if (!ctx.discriminatingSchemas.has(resolved as any)) continue; + + const refBasename = childSchema.$ref.split('/').pop()!; + const discriminatingSchema = schemas[refBasename]; + if (isReference(discriminatingSchema)) continue; + + const discriminator = discriminatingSchema.discriminator; + if (!discriminator) continue; + + const refs = Object.values(discriminator.mapping || {}); + if (refs.includes(prefix + name)) continue; + + if (!discriminator.mapping) { + discriminator.mapping = {}; + } + discriminator.mapping[name] = prefix + name; + } + } +} + +function makeDiscriminatorPropertiesRequired(ctx: OazapftsContext): void { + const schemas = (ctx.spec as any).components?.schemas; + if (!schemas) return; + const prefix = '#/components/schemas/'; + + for (const name of Object.keys(schemas)) { + const schema = schemas[name]; + if (isReference(schema) || typeof schema === 'boolean') continue; + if (!schema.discriminator) continue; + + const discriminator = schema.discriminator; + const propName = discriminator.propertyName; + const refs = schema.oneOf || schema.anyOf || []; + + for (const ref of refs) { + if (!isReference(ref)) continue; + const childName = ref.$ref.split('/').pop(); + if (!childName) continue; + const childSchema = schemas[childName]; + if (!childSchema || isReference(childSchema) || typeof childSchema === 'boolean') continue; + + if (!childSchema.required) childSchema.required = []; + if (!childSchema.required.includes(propName)) { + childSchema.required.push(propName); + } + + if (!discriminator.mapping) { + discriminator.mapping = {}; + } + const alreadyMapped = Object.values(discriminator.mapping).some( + (v) => (v as string).split('/').pop() === childName + ); + if (!alreadyMapped) { + const discProp = childSchema.properties?.[propName]; + if ( + discProp && + !isReference(discProp) && + discProp.enum?.length === 1 + ) { + discriminator.mapping[String(discProp.enum[0])] = + prefix + childName; + } else { + discriminator.mapping[childName] = prefix + childName; + } + } + } + } +} + +function getTypeFromResponse( + ctx: OazapftsContext, + response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject, + onlyMode?: OnlyMode +): ts.TypeNode { + const resolved = resolve(response, ctx); + if (!resolved.content) return keywordType.void; + const schema = getSchemaFromContent(resolved.content); + return getTypeFromSchema(onlyMode ? withMode(ctx, onlyMode) : ctx, schema); +} + function defaultIsDataResponse(code: string, includeDefault: boolean) { if (includeDefault && code === 'default') { return true; @@ -91,9 +223,9 @@ function withQueryComment(node: T, def: QueryArgDefinition, h function getPatternFromProperty( property: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, - apiGen: OazapftsAdapter + ctx: OazapftsContext ): string | null { - const resolved = apiGen.resolve(property); + const resolved = resolve(property, ctx); if (!resolved || typeof resolved !== 'object' || !('pattern' in resolved)) return null; if (resolved.type !== 'string') return null; const pattern = resolved.pattern; @@ -103,15 +235,15 @@ function getPatternFromProperty( function generateRegexConstantsForType( typeName: string, schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, - apiGen: OazapftsAdapter + ctx: OazapftsContext ): ts.VariableStatement[] { - const resolvedSchema = apiGen.resolve(schema); + const resolvedSchema = resolve(schema, ctx); if (!resolvedSchema || !('properties' in resolvedSchema) || !resolvedSchema.properties) return []; const constants: ts.VariableStatement[] = []; for (const [propertyName, property] of Object.entries(resolvedSchema.properties)) { - const pattern = getPatternFromProperty(property, apiGen); + const pattern = getPatternFromProperty(property, ctx); if (!pattern) continue; const constantName = camelCase(`${typeName} ${propertyName} Pattern`); @@ -176,14 +308,15 @@ export async function generateApi( ) { const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions)); - const apiGen = new OazapftsAdapter(v3Doc, { + const ctx = createContext(v3Doc as any, { useEnumType, unionUndefined, mergeReadWriteOnly, useUnknown, }); - await apiGen.init(); + preprocessComponents(ctx); + makeDiscriminatorPropertiesRequired(ctx); const operationDefinitions = getOperationDefinitions(v3Doc).filter(operationMatches(filterEndpoints)); @@ -257,18 +390,18 @@ export async function generateApi( ), ...Object.values(interfaces), ...(outputRegexConstants - ? apiGen.aliases.flatMap((alias) => { + ? ctx.aliases.flatMap((alias) => { if (!ts.isInterfaceDeclaration(alias) && !ts.isTypeAliasDeclaration(alias)) return [alias]; const typeName = alias.name.escapedText.toString(); const schema = v3Doc.components?.schemas?.[typeName]; if (!schema) return [alias]; - const regexConstants = generateRegexConstantsForType(typeName, schema, apiGen); + const regexConstants = generateRegexConstantsForType(typeName, schema, ctx); return regexConstants.length > 0 ? [alias, ...regexConstants] : [alias]; }) - : apiGen.aliases), - ...apiGen.enumAliases, + : ctx.aliases), + ...ctx.enumAliases, ...(hooks ? [ generateReactHooks({ @@ -317,7 +450,7 @@ export async function generateApi( const tags = tag ? getTags({ verb, pathItem }) : undefined; const isQuery = testIsQuery(verb, overrides); - const returnsJson = apiGen.getResponseType(responses) === 'json'; + const returnsJson = getResponseType(ctx, responses) === 'json'; let ResponseType: ts.TypeNode = factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); if (returnsJson) { const returnTypes = Object.entries(responses || {}) @@ -325,13 +458,13 @@ export async function generateApi( ([code, response]) => [ code, - apiGen.resolve(response), - apiGen.getTypeFromResponse(response, 'readOnly') || + resolve(response, ctx), + getTypeFromResponse(ctx, response, 'readOnly') || factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword), ] as const ) .filter(([status, response]) => - isDataResponse(status, includeDefault, apiGen.resolve(response), responses || {}) + isDataResponse(status, includeDefault, resolve(response, ctx), responses || {}) ) .filter(([_1, _2, type]) => type !== keywordType.void) .map(([code, response, type]) => @@ -358,9 +491,8 @@ export async function generateApi( ).name ); - const operationParameters = apiGen.resolveArray(operation.parameters); - const pathItemParameters = apiGen - .resolveArray(pathItem.parameters) + const operationParameters = resolveArray(ctx, operation.parameters); + const pathItemParameters = resolveArray(ctx, pathItem.parameters) .filter((pp) => !operationParameters.some((op) => op.name === pp.name && op.in === pp.in)); const parameters = supportDeepObjects([...pathItemParameters, ...operationParameters]).filter( @@ -394,16 +526,16 @@ export async function generateApi( origin: 'param', name, originalName: param.name, - type: apiGen.getTypeFromSchema(isReference(param) ? param : param.schema, undefined, 'writeOnly'), + type: getTypeFromSchema(withMode(ctx, 'writeOnly'), isReference(param) ? param : param.schema), required: param.required, param, }; } if (requestBody) { - const body = apiGen.resolve(requestBody); + const body = resolve(requestBody, ctx); const schema = getSchemaFromContent(body.content); - const type = apiGen.getTypeFromSchema(schema); + const type = getTypeFromSchema(ctx, schema); const schemaName = camelCase( (type as any).name || getReferenceName(schema) || @@ -416,7 +548,7 @@ export async function generateApi( origin: 'body', name, originalName: schemaName, - type: apiGen.getTypeFromSchema(schema, undefined, 'writeOnly'), + type: getTypeFromSchema(withMode(ctx, 'writeOnly'), schema), required: true, body, }; diff --git a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts index a1d28fdfd0..d47b73e008 100644 --- a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts +++ b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts @@ -1,5 +1,5 @@ import ts from 'typescript'; -import { getOperationName } from '../oazapfts-compat'; +import { getOperationName } from '../utils'; import { capitalize, isQuery } from '../utils'; import type { OperationDefinition, EndpointOverrides, ConfigFile } from '../types'; import { getOverrides } from '../generate'; diff --git a/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts b/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts deleted file mode 100644 index a48d6b0558..0000000000 --- a/packages/rtk-query-codegen-openapi/src/oazapfts-compat.ts +++ /dev/null @@ -1,788 +0,0 @@ -/** - * Compatibility adapter for oazapfts v7. - * - * oazapfts v7 removed the `ApiGenerator` class and `oazapfts/generate` export. - * This module provides equivalent functionality using v7's public API - * (`createContext`, `UNSTABLE_cg`) combined with local implementations of - * helper functions that are no longer publicly exported. - * - * Type generation is done lazily (on demand) to match v6's behavior, - * ensuring that only types referenced by the generated endpoints are included. - */ -import { createContext, type OazapftsContext } from 'oazapfts/context'; -import { UNSTABLE_cg as cg } from 'oazapfts'; -import type { OpenAPIV3 } from 'openapi-types'; -import lodashCamelCase from 'lodash.camelcase'; -import ts from 'typescript'; - -const factory = ts.factory; - -export const createPropertyAssignment: typeof cg.createPropertyAssignment = - cg.createPropertyAssignment; -export const createQuestionToken: typeof cg.createQuestionToken = - cg.createQuestionToken; -export const keywordType: typeof cg.keywordType = cg.keywordType; -export const isValidIdentifier: typeof cg.isValidIdentifier = - cg.isValidIdentifier; - -export function isReference( - obj: unknown -): obj is OpenAPIV3.ReferenceObject { - return typeof obj === 'object' && obj !== null && '$ref' in (obj as any); -} - -export function getReferenceName( - obj: unknown -): string | undefined { - if (isReference(obj)) { - return obj.$ref.split('/').pop(); - } -} - -export function getOperationName( - verb: string, - path: string, - operationId?: string -): string { - if (operationId) { - const normalized = operationId.replace(/[^\w\s]/g, ' '); - let camelCased = lodashCamelCase(normalized); - if (camelCased) { - camelCased = camelCased.replace(/^[^a-zA-Z_$]+/, ''); - if (camelCased && isValidIdentifier(camelCased)) { - return camelCased; - } - } - } - const pathStr = path - .replace(/\{(.+?)\}/, 'by $1') - .replace(/\{(.+?)\}/, 'and $1'); - return lodashCamelCase(`${verb} ${pathStr}`); -} - -export function supportDeepObjects( - params: OpenAPIV3.ParameterObject[] -): OpenAPIV3.ParameterObject[] { - const res: OpenAPIV3.ParameterObject[] = []; - const merged: Record = {}; - for (const p of params) { - const m = /^(.+?)\[(.*?)\]/.exec(p.name); - if (!m) { - res.push(p); - continue; - } - const [, name, prop] = m; - let obj = merged[name]; - if (!obj) { - obj = merged[name] = { - name, - in: p.in, - style: 'deepObject', - schema: { - type: 'object', - properties: {} as Record, - }, - }; - res.push(obj); - } - obj.schema.properties[prop] = p.schema; - } - return res; -} - -const jsonMimeTypes: Record = { - '*/*': true, - 'application/json': true, -}; - -function isJsonMimeType(mime: string): boolean { - return !!jsonMimeTypes[mime] || /\bjson\b/i.test(mime); -} - -function isMimeType(s: string): boolean { - return /^[^/]+\/[^/]+$/.test(s); -} - -export function getSchemaFromContent( - content: Record -): OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject { - const contentType = Object.keys(content).find(isMimeType); - if (contentType) { - const { schema } = content[contentType]; - if (schema) { - return schema; - } - } - if ( - Object.keys(content).length === 0 || - Object.keys(content).some((type) => type.startsWith('text/')) - ) { - return { type: 'string' }; - } - return { type: 'string', format: 'binary' }; -} - -function toIdentifier(s: string, isTypeName = false): string { - let result = lodashCamelCase(s); - if (isTypeName && result) { - result = result.charAt(0).toUpperCase() + result.slice(1); - } - return result || s; -} - -type OnlyMode = 'readOnly' | 'writeOnly'; - -export interface OazapftsAdapterOptions { - useEnumType?: boolean; - unionUndefined?: boolean; - mergeReadWriteOnly?: boolean; - useUnknown?: boolean; -} - -export class OazapftsAdapter { - public ctx: OazapftsContext; - private opts: OazapftsAdapterOptions; - - constructor(doc: OpenAPIV3.Document, opts: OazapftsAdapterOptions) { - this.opts = opts; - this.ctx = createContext(doc as any, { - useEnumType: opts.useEnumType, - unionUndefined: opts.unionUndefined, - mergeReadWriteOnly: opts.mergeReadWriteOnly, - useUnknown: opts.useUnknown, - }); - } - - get spec(): OpenAPIV3.Document { - return this.ctx.spec as unknown as OpenAPIV3.Document; - } - - get aliases() { - return this.ctx.aliases; - } - - get enumAliases(): ts.Statement[] { - return this.ctx.enumAliases; - } - - /** - * Initialize the adapter. Preprocesses components for discriminated union support. - */ - async init(): Promise { - this.preprocessComponents(); - this.makeDiscriminatorPropertiesRequired(); - } - - resolve(obj: T | OpenAPIV3.ReferenceObject): T { - if (!isReference(obj)) return obj; - const pathParts = obj.$ref.replace(/^#\//, '').split('/'); - const resolved = pathParts.reduce( - (current, part) => current?.[part], - this.ctx.spec - ); - if (resolved === undefined) { - throw new Error(`Can't resolve ${obj.$ref}`); - } - return resolved as T; - } - - resolveArray( - array?: Array - ): T[] { - return array ? array.map((el) => this.resolve(el)) : []; - } - - /** - * Preprocess components for discriminated union support. - */ - private preprocessComponents(): void { - const schemas = (this.ctx.spec as any).components?.schemas; - if (!schemas) return; - - const prefix = '#/components/schemas/'; - - // First pass: identify discriminating schemas - for (const name of Object.keys(schemas)) { - const schema = schemas[name]; - if (isReference(schema) || typeof schema === 'boolean') continue; - if (schema.discriminator && !schema.oneOf && !schema.anyOf) { - this.ctx.discriminatingSchemas.add(schema); - } - } - - // Second pass: make mappings explicit - for (const name of Object.keys(schemas)) { - const schema = schemas[name]; - if (isReference(schema) || typeof schema === 'boolean' || !schema.allOf) continue; - - for (const childSchema of schema.allOf) { - if (!isReference(childSchema)) continue; - const resolved = this.resolve(childSchema); - if (!this.ctx.discriminatingSchemas.has(resolved as any)) continue; - - const refBasename = childSchema.$ref.split('/').pop()!; - const discriminatingSchema = schemas[refBasename]; - if (isReference(discriminatingSchema)) continue; - - const discriminator = discriminatingSchema.discriminator; - if (!discriminator) continue; - - const refs = Object.values(discriminator.mapping || {}); - if (refs.includes(prefix + name)) continue; - - if (!discriminator.mapping) { - discriminator.mapping = {}; - } - discriminator.mapping[name] = prefix + name; - } - } - } - - getResponseType( - responses?: OpenAPIV3.ResponsesObject - ): 'json' | 'text' | 'blob' { - if (!responses) return 'text'; - - const resolvedResponses = Object.values(responses).map((response) => - this.resolve(response) - ); - - if ( - !resolvedResponses.some( - (res) => Object.keys(res.content ?? {}).length > 0 - ) - ) { - return 'text'; - } - - const hasJson = resolvedResponses.some((response) => { - const responseMimeTypes = Object.keys(response.content ?? {}); - return responseMimeTypes.some(isJsonMimeType); - }); - - if (hasJson) return 'json'; - - if ( - resolvedResponses.some((res) => - Object.keys(res.content ?? {}).some((type) => type.startsWith('text/')) - ) - ) { - return 'text'; - } - - return 'blob'; - } - - getTypeFromResponse( - response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject, - onlyMode?: OnlyMode - ): ts.TypeNode { - const resolved = this.resolve(response); - if (!resolved.content) return keywordType.void; - const schema = getSchemaFromContent(resolved.content); - return this.getTypeFromSchema(schema, undefined, onlyMode); - } - - getTypeFromSchema( - schema?: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, - name?: string, - onlyMode?: OnlyMode - ): ts.TypeNode { - if (schema === undefined) { - return this.opts.useUnknown ? keywordType.unknown : keywordType.any; - } - - if (isReference(schema)) { - return this.getRefAlias(schema, onlyMode); - } - - const type = this.resolveBaseSchema(schema, name, onlyMode); - if (schema.nullable) { - return factory.createUnionTypeNode([type, keywordType.null]); - } - return type; - } - - /** - * Create or look up a type alias for a $ref schema. - * Lazily creates type aliases and stores them in ctx.aliases/ctx.refs. - */ - private getRefAlias( - obj: OpenAPIV3.ReferenceObject, - onlyMode?: OnlyMode - ): ts.TypeNode { - const $ref = obj.$ref; - - if (!this.ctx.refs[$ref]) { - const schema = this.resolve(obj); - const name = - (typeof schema === 'object' && schema.title) || - $ref.split('/').pop()!; - const identifier = toIdentifier(name, true); - - const isDiscriminating = this.ctx.discriminatingSchemas.has(schema as any); - const alias = this.getUniqueAlias( - isDiscriminating ? identifier + 'Base' : identifier - ); - - this.ctx.refs[$ref] = { - base: factory.createTypeReferenceNode(alias, undefined), - readOnly: undefined, - writeOnly: undefined, - }; - - const type = this.resolveBaseSchema(schema, undefined, undefined, $ref); - const nullable = schema.nullable - ? factory.createUnionTypeNode([type, keywordType.null]) - : type; - - this.ctx.aliases.push( - factory.createTypeAliasDeclaration( - [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - alias, - undefined, - nullable - ) - ); - - if (!this.opts.mergeReadWriteOnly) { - const { hasReadOnly, hasWriteOnly } = this.checkSchemaOnlyMode(schema); - - if (hasReadOnly) { - const readOnlyAlias = this.getUniqueAlias( - toIdentifier(name + 'Read', true) - ); - const readOnlyType = this.resolveBaseSchema(schema, undefined, 'readOnly'); - const readOnlyNullable = schema.nullable - ? factory.createUnionTypeNode([readOnlyType, keywordType.null]) - : readOnlyType; - this.ctx.refs[$ref].readOnly = factory.createTypeReferenceNode( - readOnlyAlias, - undefined - ); - this.ctx.aliases.push( - factory.createTypeAliasDeclaration( - [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - readOnlyAlias, - undefined, - readOnlyNullable - ) - ); - } - - if (hasWriteOnly) { - const writeOnlyAlias = this.getUniqueAlias( - toIdentifier(name + 'Write', true) - ); - const writeOnlyType = this.resolveBaseSchema(schema, undefined, 'writeOnly'); - const writeOnlyNullable = schema.nullable - ? factory.createUnionTypeNode([writeOnlyType, keywordType.null]) - : writeOnlyType; - this.ctx.refs[$ref].writeOnly = factory.createTypeReferenceNode( - writeOnlyAlias, - undefined - ); - this.ctx.aliases.push( - factory.createTypeAliasDeclaration( - [factory.createModifier(ts.SyntaxKind.ExportKeyword)], - writeOnlyAlias, - undefined, - writeOnlyNullable - ) - ); - } - } - } - - const entry = this.ctx.refs[$ref]; - return entry[onlyMode || 'base'] ?? entry.base; - } - - private getUniqueAlias(name: string): string { - const count = this.ctx.typeAliases[name]; - if (count === undefined) { - this.ctx.typeAliases[name] = 1; - return name; - } - this.ctx.typeAliases[name] = count + 1; - return `${name}${count + 1}`; - } - - private checkSchemaOnlyMode(schema: OpenAPIV3.SchemaObject): { - hasReadOnly: boolean; - hasWriteOnly: boolean; - } { - if (this.opts.mergeReadWriteOnly) { - return { hasReadOnly: false, hasWriteOnly: false }; - } - - let hasReadOnly = false; - let hasWriteOnly = false; - - if (schema.properties) { - for (const prop of Object.values(schema.properties)) { - if (isReference(prop)) continue; - if (prop.readOnly) hasReadOnly = true; - if (prop.writeOnly) hasWriteOnly = true; - } - } - - return { hasReadOnly, hasWriteOnly }; - } - - /** - * For schemas with discriminator + oneOf/anyOf: - * 1. Populate implicit discriminator mappings (using enum values from child schemas) - * 2. Make the discriminator property required in child schemas - */ - private makeDiscriminatorPropertiesRequired(): void { - const schemas = (this.ctx.spec as any).components?.schemas; - if (!schemas) return; - const prefix = '#/components/schemas/'; - - for (const name of Object.keys(schemas)) { - const schema = schemas[name]; - if (isReference(schema) || typeof schema === 'boolean') continue; - if (!schema.discriminator) continue; - - const discriminator = schema.discriminator; - const propName = discriminator.propertyName; - const refs = schema.oneOf || schema.anyOf || []; - - for (const ref of refs) { - if (!isReference(ref)) continue; - const childName = ref.$ref.split('/').pop(); - if (!childName) continue; - const childSchema = schemas[childName]; - if (!childSchema || isReference(childSchema) || typeof childSchema === 'boolean') continue; - - // Make discriminator property required - if (!childSchema.required) childSchema.required = []; - if (!childSchema.required.includes(propName)) { - childSchema.required.push(propName); - } - - // Populate implicit mapping if not already present - if (!discriminator.mapping) { - discriminator.mapping = {}; - } - const alreadyMapped = Object.values(discriminator.mapping).some( - (v) => (v as string).split('/').pop() === childName - ); - if (!alreadyMapped) { - // Use the enum value from the child's discriminator property if available - const discProp = childSchema.properties?.[propName]; - if ( - discProp && - !isReference(discProp) && - discProp.enum?.length === 1 - ) { - discriminator.mapping[String(discProp.enum[0])] = - prefix + childName; - } else { - discriminator.mapping[childName] = prefix + childName; - } - } - } - } - } - - private resolveBaseSchema( - schema: OpenAPIV3.SchemaObject, - _name?: string, - onlyMode?: OnlyMode, - currentRef?: string - ): ts.TypeNode { - if (schema.oneOf) { - return this.resolveCompositionSchema(schema.oneOf, 'oneOf', schema, onlyMode); - } - - if (schema.anyOf) { - return this.resolveCompositionSchema(schema.anyOf, 'anyOf', schema, onlyMode); - } - - if (schema.allOf) { - return this.resolveAllOfSchema(schema, onlyMode, currentRef); - } - - if (schema.enum) { - return this.createEnumTypeNode(schema.enum); - } - - switch (schema.type) { - case 'string': - if (schema.format === 'binary') { - return factory.createTypeReferenceNode('Blob'); - } - return keywordType.string; - case 'number': - case 'integer': - return keywordType.number; - case 'boolean': - return keywordType.boolean; - case 'array': - if (schema.items) { - const itemType = this.getTypeFromSchema( - schema.items, - undefined, - onlyMode - ); - return factory.createArrayTypeNode(itemType); - } - return factory.createArrayTypeNode(keywordType.any); - case 'object': - return this.resolveObjectSchema(schema, onlyMode, true); - default: - return this.resolveObjectSchema(schema, onlyMode, false); - } - } - - /** - * Handle oneOf/anyOf composition, with discriminator support. - * When a discriminator is present, each variant becomes - * `{ discriminatorProp: "value" } & ChildType`. - */ - private resolveCompositionSchema( - variants: (OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject)[], - _kind: 'oneOf' | 'anyOf', - parentSchema: OpenAPIV3.SchemaObject, - onlyMode?: OnlyMode - ): ts.TypeNode { - const discriminator = parentSchema.discriminator; - - const types = variants.map((s) => { - const childType = this.getTypeFromSchema(s, undefined, onlyMode); - - if (discriminator && isReference(s)) { - const value = this.getDiscriminatorValueForRef(discriminator, s.$ref); - if (value !== undefined) { - const discLiteral = factory.createTypeLiteralNode([ - factory.createPropertySignature( - undefined, - factory.createIdentifier(discriminator.propertyName), - undefined, - factory.createLiteralTypeNode(factory.createStringLiteral(value)) - ), - ]); - return factory.createIntersectionTypeNode([discLiteral, childType]); - } - } - - return childType; - }); - - return types.length === 1 ? types[0] : factory.createUnionTypeNode(types); - } - - /** - * Handle allOf composition, with discriminator + own properties support. - * When a schema has both allOf and its own properties, the result is - * an intersection of all allOf items plus a type literal for own properties. - * When an allOf item references a discriminating schema, a discriminator - * literal type is prepended. - */ - private resolveAllOfSchema( - schema: OpenAPIV3.SchemaObject, - onlyMode?: OnlyMode, - currentRef?: string - ): ts.TypeNode { - const types: ts.TypeNode[] = []; - - for (const s of schema.allOf!) { - // Check if this allOf item references a discriminating schema - if (isReference(s)) { - const resolved = this.resolve(s); - if ( - this.ctx.discriminatingSchemas.has(resolved as any) && - resolved.discriminator - ) { - const value = currentRef - ? this.getDiscriminatorValueForRef( - resolved.discriminator, - currentRef - ) - : undefined; - if (value !== undefined) { - types.push( - factory.createTypeLiteralNode([ - factory.createPropertySignature( - undefined, - factory.createIdentifier( - resolved.discriminator.propertyName - ), - undefined, - factory.createLiteralTypeNode( - factory.createStringLiteral(value) - ) - ), - ]) - ); - } - } - } - types.push(this.getTypeFromSchema(s, undefined, onlyMode)); - } - - if (schema.properties) { - types.push(this.resolveObjectSchema(schema, onlyMode)); - } - - return types.length === 1 - ? types[0] - : factory.createIntersectionTypeNode(types); - } - - /** - * Look up the discriminator value for a given $ref in a discriminator mapping. - */ - private getDiscriminatorValueForRef( - discriminator: OpenAPIV3.DiscriminatorObject, - targetRef: string - ): string | undefined { - const mapping = discriminator.mapping || {}; - const targetName = targetRef.split('/').pop(); - for (const [key, ref] of Object.entries(mapping)) { - const refName = ref.split('/').pop(); - if (refName === targetName) return key; - } - return undefined; - } - - private resolveObjectSchema( - schema: OpenAPIV3.SchemaObject, - onlyMode?: OnlyMode, - isExplicitObject = false - ): ts.TypeNode { - if (schema.properties) { - const members: ts.TypeElement[] = Object.entries(schema.properties) - .filter(([, prop]) => { - if (isReference(prop)) return true; - if (this.opts.mergeReadWriteOnly) return true; - switch (onlyMode) { - case 'readOnly': - return !prop.writeOnly; - case 'writeOnly': - return !prop.readOnly; - default: - // Base type: exclude both readOnly and writeOnly props - return !prop.readOnly && !prop.writeOnly; - } - }) - .map(([name, prop]) => { - const propType = this.getTypeFromSchema(prop, undefined, onlyMode); - const isRequired = schema.required?.includes(name) ?? false; - const optionalType = - !isRequired && this.opts.unionUndefined - ? factory.createUnionTypeNode([propType, keywordType.undefined]) - : propType; - const signature = factory.createPropertySignature( - undefined, - isValidIdentifier(name) - ? factory.createIdentifier(name) - : factory.createStringLiteral(name), - isRequired - ? undefined - : factory.createToken(ts.SyntaxKind.QuestionToken), - optionalType - ); - const resolvedProp = isReference(prop) ? this.resolve(prop) : prop; - if ( - typeof resolvedProp === 'object' && - 'description' in resolvedProp && - resolvedProp.description - ) { - const description = resolvedProp.description.replace('*/', '*\\/'); - return ts.addSyntheticLeadingComment( - signature, - ts.SyntaxKind.MultiLineCommentTrivia, - `* ${description} `, - true - ); - } - return signature; - }); - - if (schema.additionalProperties) { - const valueType = - schema.additionalProperties === true - ? keywordType.any - : this.getTypeFromSchema( - schema.additionalProperties, - undefined, - onlyMode - ); - members.push( - factory.createIndexSignature( - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - 'key', - undefined, - keywordType.string - ), - ], - valueType - ) - ); - } - - return factory.createTypeLiteralNode(members); - } - - if (schema.additionalProperties) { - const valueType = - schema.additionalProperties === true - ? keywordType.any - : this.getTypeFromSchema( - schema.additionalProperties, - undefined, - onlyMode - ); - return factory.createTypeLiteralNode([ - factory.createIndexSignature( - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - 'key', - undefined, - keywordType.string - ), - ], - valueType - ), - ]); - } - - if (isExplicitObject) { - return factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword); - } - return this.opts.useUnknown ? keywordType.unknown : keywordType.any; - } - - private createEnumTypeNode( - values: Array - ): ts.TypeNode { - const types = values.map((v) => { - if (v === null) return keywordType.null; - switch (typeof v) { - case 'string': - return factory.createLiteralTypeNode(factory.createStringLiteral(v)); - case 'number': - return factory.createLiteralTypeNode( - factory.createNumericLiteral(String(v)) - ); - case 'boolean': - return factory.createLiteralTypeNode( - v ? factory.createTrue() : factory.createFalse() - ); - default: - return keywordType.string; - } - }); - return types.length === 1 ? types[0] : factory.createUnionTypeNode(types); - } -} diff --git a/packages/rtk-query-codegen-openapi/src/utils/getOperationName.ts b/packages/rtk-query-codegen-openapi/src/utils/getOperationName.ts new file mode 100644 index 0000000000..a3aa94a244 --- /dev/null +++ b/packages/rtk-query-codegen-openapi/src/utils/getOperationName.ts @@ -0,0 +1,25 @@ +import lodashCamelCase from 'lodash.camelcase'; +import { UNSTABLE_cg as cg } from 'oazapfts'; + +const { isValidIdentifier } = cg; + +export function getOperationName( + verb: string, + path: string, + operationId?: string +): string { + if (operationId) { + const normalized = operationId.replace(/[^\w\s]/g, ' '); + let camelCased = lodashCamelCase(normalized); + if (camelCased) { + camelCased = camelCased.replace(/^[^a-zA-Z_$]+/, ''); + if (camelCased && isValidIdentifier(camelCased)) { + return camelCased; + } + } + } + const pathStr = path + .replace(/\{(.+?)\}/, 'by $1') + .replace(/\{(.+?)\}/, 'and $1'); + return lodashCamelCase(`${verb} ${pathStr}`); +} diff --git a/packages/rtk-query-codegen-openapi/src/utils/index.ts b/packages/rtk-query-codegen-openapi/src/utils/index.ts index c7d39e046a..8ef01255b8 100644 --- a/packages/rtk-query-codegen-openapi/src/utils/index.ts +++ b/packages/rtk-query-codegen-openapi/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './capitalize'; export * from './getOperationDefinitions'; +export * from './getOperationName'; export * from './getV3Doc'; export * from './isQuery'; export * from './isValidUrl'; diff --git a/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap b/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap index 0b42c1081c..976ebf30d8 100644 --- a/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap +++ b/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap @@ -3256,6 +3256,56 @@ export type Allowance = " `; +exports[`useEnumType option > generates TypeScript enums when useEnumType is true 1`] = ` +"import { api } from "./fixtures/emptyApi"; +const injectedRtkApi = api.injectEndpoints({ + endpoints: (build) => ({ + findPetsByStatus: build.query< + FindPetsByStatusApiResponse, + FindPetsByStatusApiArg + >({ + query: (queryArg) => ({ + url: \`/pet/findByStatus\`, + params: { + status: queryArg.status, + }, + }), + }), + }), + overrideExisting: false, +}); +export { injectedRtkApi as enhancedApi }; +export type FindPetsByStatusApiResponse = + /** status 200 successful operation */ Pet[]; +export type FindPetsByStatusApiArg = { + /** Status values that need to be considered for filter */ + status?: "available" | "pending" | "sold"; +}; +export type Category = { + id?: number | undefined; + name?: string | undefined; +}; +export type Tag = { + id?: number | undefined; + name?: string | undefined; +}; +export type Pet = { + id?: number | undefined; + name: string; + category?: Category | undefined; + photoUrls: string[]; + tags?: Tag[] | undefined; + /** pet status in the store */ + status?: Status | undefined; +}; +export enum Status { + Available = "available", + Pending = "pending", + Sold = "sold", +} +" +`; + exports[`yaml parsing > should be able to use read a yaml file 1`] = ` "import { api } from "./tmp/emptyApi"; export const addTagTypes = ["pet", "store", "user"] as const; diff --git a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts index 20c895219d..116f1ce7b2 100644 --- a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts +++ b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts @@ -768,6 +768,22 @@ describe('openapi spec', () => { }); }); +describe('useEnumType option', () => { + it('generates TypeScript enums when useEnumType is true', async () => { + const api = await generateEndpoints({ + unionUndefined: true, + schemaFile: resolve(__dirname, 'fixtures/petstore.json'), + apiFile: './fixtures/emptyApi.ts', + useEnumType: true, + filterEndpoints: ['findPetsByStatus'], + }); + + expect(api).toMatchSnapshot(); + + expect(api).toMatch(/enum\s+\w+/); + }); +}); + describe('query parameters', () => { it('parameters overridden in swagger should also be overridden in the code', async () => { const api = await generateEndpoints({ diff --git a/yarn.lock b/yarn.lock index 747dc2188b..9ca58327ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6692,6 +6692,15 @@ __metadata: languageName: node linkType: hard +"@oazapfts/resolve@npm:0.0.0-development || ^1.0.0, @oazapfts/resolve@npm:^1.0.0": + version: 1.0.0 + resolution: "@oazapfts/resolve@npm:1.0.0" + dependencies: + openapi-types: "npm:^12.1.3" + checksum: 10/2dfc1af464172fa3b987ed83cbd33159638bc219f6fa96e017c9c007ca02c9ce861a048475353e5bb62166074994934c519b493959b1b18f8fe49c6d5a012d4c + languageName: node + linkType: hard + "@oazapfts/runtime@npm:^1.0.3": version: 1.0.4 resolution: "@oazapfts/runtime@npm:1.0.4" @@ -7808,6 +7817,7 @@ __metadata: "@babel/core": "npm:^7.12.10" "@babel/preset-env": "npm:^7.12.11" "@babel/preset-typescript": "npm:^7.12.7" + "@oazapfts/resolve": "npm:^1.0.0" "@oazapfts/runtime": "npm:^1.0.3" "@reduxjs/toolkit": "npm:^1.6.0" "@types/commander": "npm:^2.12.2" @@ -7824,7 +7834,7 @@ __metadata: lodash.camelcase: "npm:^4.3.0" msw: "npm:^2.1.5" node-fetch: "npm:^3.3.2" - oazapfts: "npm:^7.0.0" + oazapfts: "npm:7.5.0-alpha.3" openapi-types: "npm:^9.1.0" prettier: "npm:^3.2.5" pretty-quick: "npm:^4.0.0" @@ -23733,20 +23743,21 @@ __metadata: languageName: node linkType: hard -"oazapfts@npm:^7.0.0": - version: 7.4.0 - resolution: "oazapfts@npm:7.4.0" +"oazapfts@npm:7.5.0-alpha.3": + version: 7.5.0-alpha.3 + resolution: "oazapfts@npm:7.5.0-alpha.3" dependencies: "@apidevtools/swagger-parser": "npm:^12.1.0" + "@oazapfts/resolve": "npm:0.0.0-development || ^1.0.0" lodash: "npm:^4.17.23" minimist: "npm:^1.2.8" tapable: "npm:^2.3.0" typescript: "npm:^5.9.3" peerDependencies: - "@oazapfts/runtime": "*" + "@oazapfts/runtime": 0.0.0-development || ^1.2.0 bin: oazapfts: dist/cli.js - checksum: 10/354a38ce64f26936d7053e1b9c9a2a373d1ca21187c5e683e578c8be558ea391c6b9f1c390af54930120e19376fb58fdd3000036b02342aa0d747e6047c05a79 + checksum: 10/843e10fb617038d560d065885f52a65c78be567e8f3992d9746b8654f2c5d98757df9f4d478b9bdd960e9a963a2e9c476aa3a9443bde68ac0e96121c3d024a4f languageName: node linkType: hard @@ -23962,6 +23973,13 @@ __metadata: languageName: node linkType: hard +"openapi-types@npm:^12.1.3": + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 10/9d1d7ed848622b63d0a4c3f881689161b99427133054e46b8e3241e137f1c78bb0031c5d80b420ee79ac2e91d2e727ffd6fc13c553d1b0488ddc8ad389dcbef8 + languageName: node + linkType: hard + "openapi-types@npm:^9.1.0": version: 9.3.1 resolution: "openapi-types@npm:9.3.1" From 5ef98673592e5f09bc482f6be4a767c28e55fb78 Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Fri, 27 Feb 2026 14:08:02 +0900 Subject: [PATCH 05/10] refactor: simplify discriminator handling in schema processing - Removed unnecessary mapping logic for discriminator properties in makeDiscriminatorPropertiesRequired function. - Streamlined the process of making discriminator properties required for child schemas. - Improved code readability by eliminating redundant checks and variables. --- .../rtk-query-codegen-openapi/src/generate.ts | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index b5018e3522..7135d476dc 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -107,15 +107,13 @@ function preprocessComponents(ctx: OazapftsContext): void { function makeDiscriminatorPropertiesRequired(ctx: OazapftsContext): void { const schemas = (ctx.spec as any).components?.schemas; if (!schemas) return; - const prefix = '#/components/schemas/'; for (const name of Object.keys(schemas)) { const schema = schemas[name]; if (isReference(schema) || typeof schema === 'boolean') continue; if (!schema.discriminator) continue; - const discriminator = schema.discriminator; - const propName = discriminator.propertyName; + const propName = schema.discriminator.propertyName; const refs = schema.oneOf || schema.anyOf || []; for (const ref of refs) { @@ -129,26 +127,6 @@ function makeDiscriminatorPropertiesRequired(ctx: OazapftsContext): void { if (!childSchema.required.includes(propName)) { childSchema.required.push(propName); } - - if (!discriminator.mapping) { - discriminator.mapping = {}; - } - const alreadyMapped = Object.values(discriminator.mapping).some( - (v) => (v as string).split('/').pop() === childName - ); - if (!alreadyMapped) { - const discProp = childSchema.properties?.[propName]; - if ( - discProp && - !isReference(discProp) && - discProp.enum?.length === 1 - ) { - discriminator.mapping[String(discProp.enum[0])] = - prefix + childName; - } else { - discriminator.mapping[childName] = prefix + childName; - } - } } } } From 4f32e53e85beba9f261edbd6526c1420411a863f Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Fri, 27 Feb 2026 14:14:00 +0900 Subject: [PATCH 06/10] refactor: remove makeDiscriminatorPropertiesRequired function and clean up tests - Eliminated the makeDiscriminatorPropertiesRequired function to simplify schema processing. - Updated tests to remove unnecessary snapshot expectations and adjusted generated types to make allowance_type properties optional. - Improved overall code clarity and maintainability. --- .../rtk-query-codegen-openapi/src/generate.ts | 28 ---------- .../generateEndpoints.test.ts.snap | 54 +------------------ .../test/generateEndpoints.test.ts | 2 - 3 files changed, 2 insertions(+), 82 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 7135d476dc..fb8305f5b5 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -104,33 +104,6 @@ function preprocessComponents(ctx: OazapftsContext): void { } } -function makeDiscriminatorPropertiesRequired(ctx: OazapftsContext): void { - const schemas = (ctx.spec as any).components?.schemas; - if (!schemas) return; - - for (const name of Object.keys(schemas)) { - const schema = schemas[name]; - if (isReference(schema) || typeof schema === 'boolean') continue; - if (!schema.discriminator) continue; - - const propName = schema.discriminator.propertyName; - const refs = schema.oneOf || schema.anyOf || []; - - for (const ref of refs) { - if (!isReference(ref)) continue; - const childName = ref.$ref.split('/').pop(); - if (!childName) continue; - const childSchema = schemas[childName]; - if (!childSchema || isReference(childSchema) || typeof childSchema === 'boolean') continue; - - if (!childSchema.required) childSchema.required = []; - if (!childSchema.required.includes(propName)) { - childSchema.required.push(propName); - } - } - } -} - function getTypeFromResponse( ctx: OazapftsContext, response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject, @@ -294,7 +267,6 @@ export async function generateApi( }); preprocessComponents(ctx); - makeDiscriminatorPropertiesRequired(ctx); const operationDefinitions = getOperationDefinitions(v3Doc).filter(operationMatches(filterEndpoints)); diff --git a/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap b/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap index 976ebf30d8..6e7a7c29d7 100644 --- a/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap +++ b/packages/rtk-query-codegen-openapi/test/__snapshots__/generateEndpoints.test.ts.snap @@ -3234,12 +3234,12 @@ export type CreateAllowanceApiArg = { allowance: Allowance; }; export type EngineeringAllowance = { - allowance_type: "engineering"; + allowance_type?: "engineering"; distribution?: "MARECO" | "LINEAR"; capacity_speed_limit?: number; }; export type StandardAllowance = { - allowance_type: "standard"; + allowance_type?: "standard"; default_value?: number; ranges?: { min?: number; @@ -3256,56 +3256,6 @@ export type Allowance = " `; -exports[`useEnumType option > generates TypeScript enums when useEnumType is true 1`] = ` -"import { api } from "./fixtures/emptyApi"; -const injectedRtkApi = api.injectEndpoints({ - endpoints: (build) => ({ - findPetsByStatus: build.query< - FindPetsByStatusApiResponse, - FindPetsByStatusApiArg - >({ - query: (queryArg) => ({ - url: \`/pet/findByStatus\`, - params: { - status: queryArg.status, - }, - }), - }), - }), - overrideExisting: false, -}); -export { injectedRtkApi as enhancedApi }; -export type FindPetsByStatusApiResponse = - /** status 200 successful operation */ Pet[]; -export type FindPetsByStatusApiArg = { - /** Status values that need to be considered for filter */ - status?: "available" | "pending" | "sold"; -}; -export type Category = { - id?: number | undefined; - name?: string | undefined; -}; -export type Tag = { - id?: number | undefined; - name?: string | undefined; -}; -export type Pet = { - id?: number | undefined; - name: string; - category?: Category | undefined; - photoUrls: string[]; - tags?: Tag[] | undefined; - /** pet status in the store */ - status?: Status | undefined; -}; -export enum Status { - Available = "available", - Pending = "pending", - Sold = "sold", -} -" -`; - exports[`yaml parsing > should be able to use read a yaml file 1`] = ` "import { api } from "./tmp/emptyApi"; export const addTagTypes = ["pet", "store", "user"] as const; diff --git a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts index 116f1ce7b2..b926959040 100644 --- a/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts +++ b/packages/rtk-query-codegen-openapi/test/generateEndpoints.test.ts @@ -778,8 +778,6 @@ describe('useEnumType option', () => { filterEndpoints: ['findPetsByStatus'], }); - expect(api).toMatchSnapshot(); - expect(api).toMatch(/enum\s+\w+/); }); }); From 68a3625ff010ed4bc10ba0a31c0eea690267bbf8 Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Sat, 28 Feb 2026 16:42:53 +0900 Subject: [PATCH 07/10] chore: update oazapfts to v7.5.0-alpha.5 and refine code generation - Updated oazapfts dependency to v7.5.0-alpha.5 in package.json and yarn.lock. - Refactored generate.ts to improve type resolution logic and removed unused preprocessing functions. - Enhanced import statements for better clarity and maintainability. --- .../rtk-query-codegen-openapi/package.json | 2 +- .../rtk-query-codegen-openapi/src/generate.ts | 58 +------------------ yarn.lock | 16 ++--- 3 files changed, 11 insertions(+), 65 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/package.json b/packages/rtk-query-codegen-openapi/package.json index d7191f10da..c9e0e2f7a4 100644 --- a/packages/rtk-query-codegen-openapi/package.json +++ b/packages/rtk-query-codegen-openapi/package.json @@ -79,7 +79,7 @@ "@oazapfts/resolve": "^1.0.0", "commander": "^6.2.0", "lodash.camelcase": "^4.3.0", - "oazapfts": "7.5.0-alpha.3", + "oazapfts": "7.5.0-alpha.5", "prettier": "^3.2.5", "semver": "^7.3.5", "swagger2openapi": "^7.0.4", diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index fb8305f5b5..87c95b4008 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -4,7 +4,7 @@ import type { OpenAPIV3 } from 'openapi-types'; import ts from 'typescript'; import { UNSTABLE_cg as cg } from 'oazapfts'; import { createContext, withMode, type OazapftsContext, type OnlyMode } from 'oazapfts/context'; -import { getTypeFromSchema, getResponseType, getSchemaFromContent } from 'oazapfts/generate'; +import { getTypeFromSchema, getTypeFromResponse, getResponseType, getSchemaFromContent } from 'oazapfts/generate'; import { resolve, resolveArray, isReference, getReferenceName } from '@oazapfts/resolve'; import type { ObjectPropertyDefinitions } from './codegen'; import { generateCreateApiCall, generateEndpointDefinition, generateImportNode, generateTagTypes } from './codegen'; @@ -63,58 +63,6 @@ function supportDeepObjects( return res; } -function preprocessComponents(ctx: OazapftsContext): void { - const schemas = (ctx.spec as any).components?.schemas; - if (!schemas) return; - - const prefix = '#/components/schemas/'; - - for (const name of Object.keys(schemas)) { - const schema = schemas[name]; - if (isReference(schema) || typeof schema === 'boolean') continue; - if (schema.discriminator && !schema.oneOf && !schema.anyOf) { - ctx.discriminatingSchemas.add(schema); - } - } - - for (const name of Object.keys(schemas)) { - const schema = schemas[name]; - if (isReference(schema) || typeof schema === 'boolean' || !schema.allOf) continue; - - for (const childSchema of schema.allOf) { - if (!isReference(childSchema)) continue; - const resolved = resolve(childSchema, ctx); - if (!ctx.discriminatingSchemas.has(resolved as any)) continue; - - const refBasename = childSchema.$ref.split('/').pop()!; - const discriminatingSchema = schemas[refBasename]; - if (isReference(discriminatingSchema)) continue; - - const discriminator = discriminatingSchema.discriminator; - if (!discriminator) continue; - - const refs = Object.values(discriminator.mapping || {}); - if (refs.includes(prefix + name)) continue; - - if (!discriminator.mapping) { - discriminator.mapping = {}; - } - discriminator.mapping[name] = prefix + name; - } - } -} - -function getTypeFromResponse( - ctx: OazapftsContext, - response: OpenAPIV3.ResponseObject | OpenAPIV3.ReferenceObject, - onlyMode?: OnlyMode -): ts.TypeNode { - const resolved = resolve(response, ctx); - if (!resolved.content) return keywordType.void; - const schema = getSchemaFromContent(resolved.content); - return getTypeFromSchema(onlyMode ? withMode(ctx, onlyMode) : ctx, schema); -} - function defaultIsDataResponse(code: string, includeDefault: boolean) { if (includeDefault && code === 'default') { return true; @@ -266,8 +214,6 @@ export async function generateApi( useUnknown, }); - preprocessComponents(ctx); - const operationDefinitions = getOperationDefinitions(v3Doc).filter(operationMatches(filterEndpoints)); const resultFile = ts.createSourceFile( @@ -409,7 +355,7 @@ export async function generateApi( [ code, resolve(response, ctx), - getTypeFromResponse(ctx, response, 'readOnly') || + getTypeFromResponse(response, withMode(ctx, 'readOnly')) || factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword), ] as const ) diff --git a/yarn.lock b/yarn.lock index 9ca58327ba..670b93bb7d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6692,7 +6692,7 @@ __metadata: languageName: node linkType: hard -"@oazapfts/resolve@npm:0.0.0-development || ^1.0.0, @oazapfts/resolve@npm:^1.0.0": +"@oazapfts/resolve@npm:^1.0.0": version: 1.0.0 resolution: "@oazapfts/resolve@npm:1.0.0" dependencies: @@ -7834,7 +7834,7 @@ __metadata: lodash.camelcase: "npm:^4.3.0" msw: "npm:^2.1.5" node-fetch: "npm:^3.3.2" - oazapfts: "npm:7.5.0-alpha.3" + oazapfts: "npm:7.5.0-alpha.5" openapi-types: "npm:^9.1.0" prettier: "npm:^3.2.5" pretty-quick: "npm:^4.0.0" @@ -23743,21 +23743,21 @@ __metadata: languageName: node linkType: hard -"oazapfts@npm:7.5.0-alpha.3": - version: 7.5.0-alpha.3 - resolution: "oazapfts@npm:7.5.0-alpha.3" +"oazapfts@npm:7.5.0-alpha.5": + version: 7.5.0-alpha.5 + resolution: "oazapfts@npm:7.5.0-alpha.5" dependencies: "@apidevtools/swagger-parser": "npm:^12.1.0" - "@oazapfts/resolve": "npm:0.0.0-development || ^1.0.0" + "@oazapfts/resolve": "npm:^1.0.0" lodash: "npm:^4.17.23" minimist: "npm:^1.2.8" tapable: "npm:^2.3.0" typescript: "npm:^5.9.3" peerDependencies: - "@oazapfts/runtime": 0.0.0-development || ^1.2.0 + "@oazapfts/runtime": ^1.2.0 bin: oazapfts: dist/cli.js - checksum: 10/843e10fb617038d560d065885f52a65c78be567e8f3992d9746b8654f2c5d98757df9f4d478b9bdd960e9a963a2e9c476aa3a9443bde68ac0e96121c3d024a4f + checksum: 10/3a05040038b0f137d3ecda9323824f9a5bfcf73260305d5d8d2a3434ccc08c049c7be70f6db1ccacad2f924d4c8410ea6d942947ff30e7844bcac575e5d5f167 languageName: node linkType: hard From bfabf8fca3a8c8a0232b64e935aa3614105db84a Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Sat, 28 Feb 2026 16:48:26 +0900 Subject: [PATCH 08/10] refactor: streamline operation name handling and improve TypeScript version enforcement - Removed the getOperationName utility function and updated imports to use the version from oazapfts/generate. - Simplified TypeScript version enforcement logic in index.ts for better clarity and reliability. - Enhanced overall code organization by removing unused files and improving import statements. --- .../rtk-query-codegen-openapi/src/generate.ts | 3 +-- .../src/generators/react-hooks.ts | 2 +- .../rtk-query-codegen-openapi/src/index.ts | 15 +---------- .../src/utils/getOperationName.ts | 25 ------------------- .../src/utils/index.ts | 1 - 5 files changed, 3 insertions(+), 43 deletions(-) delete mode 100644 packages/rtk-query-codegen-openapi/src/utils/getOperationName.ts diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 87c95b4008..9ca22946f1 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -4,7 +4,7 @@ import type { OpenAPIV3 } from 'openapi-types'; import ts from 'typescript'; import { UNSTABLE_cg as cg } from 'oazapfts'; import { createContext, withMode, type OazapftsContext, type OnlyMode } from 'oazapfts/context'; -import { getTypeFromSchema, getTypeFromResponse, getResponseType, getSchemaFromContent } from 'oazapfts/generate'; +import { getTypeFromSchema, getTypeFromResponse, getResponseType, getSchemaFromContent, getOperationName as _getOperationName } from 'oazapfts/generate'; import { resolve, resolveArray, isReference, getReferenceName } from '@oazapfts/resolve'; import type { ObjectPropertyDefinitions } from './codegen'; import { generateCreateApiCall, generateEndpointDefinition, generateImportNode, generateTagTypes } from './codegen'; @@ -21,7 +21,6 @@ import type { import { capitalize, getOperationDefinitions, - getOperationName as _getOperationName, getV3Doc, removeUndefined, isQuery as testIsQuery, diff --git a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts index d47b73e008..91b5baef81 100644 --- a/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts +++ b/packages/rtk-query-codegen-openapi/src/generators/react-hooks.ts @@ -1,5 +1,5 @@ import ts from 'typescript'; -import { getOperationName } from '../utils'; +import { getOperationName } from 'oazapfts/generate'; import { capitalize, isQuery } from '../utils'; import type { OperationDefinition, EndpointOverrides, ConfigFile } from '../types'; import { getOverrides } from '../generate'; diff --git a/packages/rtk-query-codegen-openapi/src/index.ts b/packages/rtk-query-codegen-openapi/src/index.ts index bfb4499a26..41a251d63d 100644 --- a/packages/rtk-query-codegen-openapi/src/index.ts +++ b/packages/rtk-query-codegen-openapi/src/index.ts @@ -50,23 +50,10 @@ export function parseConfig(fullConfig: ConfigFile) { /** * Enforces `oazapfts` to use the same TypeScript version as this module itself uses. * That should prevent enums from running out of sync if both libraries use different TS versions. - * - * In oazapfts v7, TypeScript is a peerDependency so this is typically a no-op, - * but we keep it for safety in environments with complex dependency resolution. */ function enforceOazapftsTsVersion(cb: () => T): T { - let ozTsPath: string; - try { - ozTsPath = require.resolve('typescript', { paths: [require.resolve('oazapfts')] }); - } catch { - // In oazapfts v7+, TypeScript is a peerDependency and may resolve to the - // same path. If resolution fails, just run the callback directly. - return cb(); - } + const ozTsPath = require.resolve('typescript', { paths: [require.resolve('oazapfts')] }); const tsPath = require.resolve('typescript'); - if (ozTsPath === tsPath) { - return cb(); - } const originalEntry = require.cache[ozTsPath]; try { require.cache[ozTsPath] = require.cache[tsPath]; diff --git a/packages/rtk-query-codegen-openapi/src/utils/getOperationName.ts b/packages/rtk-query-codegen-openapi/src/utils/getOperationName.ts deleted file mode 100644 index a3aa94a244..0000000000 --- a/packages/rtk-query-codegen-openapi/src/utils/getOperationName.ts +++ /dev/null @@ -1,25 +0,0 @@ -import lodashCamelCase from 'lodash.camelcase'; -import { UNSTABLE_cg as cg } from 'oazapfts'; - -const { isValidIdentifier } = cg; - -export function getOperationName( - verb: string, - path: string, - operationId?: string -): string { - if (operationId) { - const normalized = operationId.replace(/[^\w\s]/g, ' '); - let camelCased = lodashCamelCase(normalized); - if (camelCased) { - camelCased = camelCased.replace(/^[^a-zA-Z_$]+/, ''); - if (camelCased && isValidIdentifier(camelCased)) { - return camelCased; - } - } - } - const pathStr = path - .replace(/\{(.+?)\}/, 'by $1') - .replace(/\{(.+?)\}/, 'and $1'); - return lodashCamelCase(`${verb} ${pathStr}`); -} diff --git a/packages/rtk-query-codegen-openapi/src/utils/index.ts b/packages/rtk-query-codegen-openapi/src/utils/index.ts index 8ef01255b8..c7d39e046a 100644 --- a/packages/rtk-query-codegen-openapi/src/utils/index.ts +++ b/packages/rtk-query-codegen-openapi/src/utils/index.ts @@ -1,6 +1,5 @@ export * from './capitalize'; export * from './getOperationDefinitions'; -export * from './getOperationName'; export * from './getV3Doc'; export * from './isQuery'; export * from './isValidUrl'; From f2149596d6dbbb8091f91fe8c1e76972689fb3e1 Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Sat, 28 Feb 2026 18:28:04 +0900 Subject: [PATCH 09/10] refactor: improve context creation in API generation - Updated the context creation in generate.ts to ensure proper handling of the useEnumType option. - Enhanced code clarity by restructuring the parameters passed to createContext. --- packages/rtk-query-codegen-openapi/src/generate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 9ca22946f1..75ce0878fc 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -206,9 +206,9 @@ export async function generateApi( ) { const v3Doc = (v3DocCache[spec] ??= await getV3Doc(spec, httpResolverOptions)); - const ctx = createContext(v3Doc as any, { - useEnumType, + const ctx = createContext(v3Doc, { unionUndefined, + useEnumType, mergeReadWriteOnly, useUnknown, }); From 973b27a65a3f0320eea0b3f2cb6bc78397b0e3ce Mon Sep 17 00:00:00 2001 From: michimasa_suto Date: Thu, 19 Mar 2026 16:28:40 +0900 Subject: [PATCH 10/10] chore: update oazapfts to v7.5.0-alpha.6 and enhance code generation - Updated oazapfts dependency to v7.5.0-alpha.6 in package.json and yarn.lock. - Added preprocessComponents function call in generate.ts to improve component processing during API generation. - Refined import statements for better clarity and maintainability. --- packages/rtk-query-codegen-openapi/package.json | 2 +- packages/rtk-query-codegen-openapi/src/generate.ts | 5 +++-- yarn.lock | 10 +++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/rtk-query-codegen-openapi/package.json b/packages/rtk-query-codegen-openapi/package.json index c9e0e2f7a4..512168d890 100644 --- a/packages/rtk-query-codegen-openapi/package.json +++ b/packages/rtk-query-codegen-openapi/package.json @@ -79,7 +79,7 @@ "@oazapfts/resolve": "^1.0.0", "commander": "^6.2.0", "lodash.camelcase": "^4.3.0", - "oazapfts": "7.5.0-alpha.5", + "oazapfts": "7.5.0-alpha.6", "prettier": "^3.2.5", "semver": "^7.3.5", "swagger2openapi": "^7.0.4", diff --git a/packages/rtk-query-codegen-openapi/src/generate.ts b/packages/rtk-query-codegen-openapi/src/generate.ts index 75ce0878fc..880e484a78 100644 --- a/packages/rtk-query-codegen-openapi/src/generate.ts +++ b/packages/rtk-query-codegen-openapi/src/generate.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import type { OpenAPIV3 } from 'openapi-types'; import ts from 'typescript'; import { UNSTABLE_cg as cg } from 'oazapfts'; -import { createContext, withMode, type OazapftsContext, type OnlyMode } from 'oazapfts/context'; -import { getTypeFromSchema, getTypeFromResponse, getResponseType, getSchemaFromContent, getOperationName as _getOperationName } from 'oazapfts/generate'; +import { createContext, withMode, type OazapftsContext } from 'oazapfts/context'; +import { getTypeFromSchema, getTypeFromResponse, getResponseType, getSchemaFromContent, getOperationName as _getOperationName, preprocessComponents } from 'oazapfts/generate'; import { resolve, resolveArray, isReference, getReferenceName } from '@oazapfts/resolve'; import type { ObjectPropertyDefinitions } from './codegen'; import { generateCreateApiCall, generateEndpointDefinition, generateImportNode, generateTagTypes } from './codegen'; @@ -212,6 +212,7 @@ export async function generateApi( mergeReadWriteOnly, useUnknown, }); + preprocessComponents(ctx); const operationDefinitions = getOperationDefinitions(v3Doc).filter(operationMatches(filterEndpoints)); diff --git a/yarn.lock b/yarn.lock index 670b93bb7d..0d1afe7620 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7834,7 +7834,7 @@ __metadata: lodash.camelcase: "npm:^4.3.0" msw: "npm:^2.1.5" node-fetch: "npm:^3.3.2" - oazapfts: "npm:7.5.0-alpha.5" + oazapfts: "npm:7.5.0-alpha.6" openapi-types: "npm:^9.1.0" prettier: "npm:^3.2.5" pretty-quick: "npm:^4.0.0" @@ -23743,9 +23743,9 @@ __metadata: languageName: node linkType: hard -"oazapfts@npm:7.5.0-alpha.5": - version: 7.5.0-alpha.5 - resolution: "oazapfts@npm:7.5.0-alpha.5" +"oazapfts@npm:7.5.0-alpha.6": + version: 7.5.0-alpha.6 + resolution: "oazapfts@npm:7.5.0-alpha.6" dependencies: "@apidevtools/swagger-parser": "npm:^12.1.0" "@oazapfts/resolve": "npm:^1.0.0" @@ -23757,7 +23757,7 @@ __metadata: "@oazapfts/runtime": ^1.2.0 bin: oazapfts: dist/cli.js - checksum: 10/3a05040038b0f137d3ecda9323824f9a5bfcf73260305d5d8d2a3434ccc08c049c7be70f6db1ccacad2f924d4c8410ea6d942947ff30e7844bcac575e5d5f167 + checksum: 10/b82ac3a7d5a833cf36e061d732d4fbc4ebcc7b2c4dec9c13bbb5b0c4ff7b5972ae0cab1e44be0a2116878ca4fb5d66ea908776c80658194994e253bffab60452 languageName: node linkType: hard