import ts from 'typescript' ; import * as path from 'path' ; import { camelCase } from 'lodash' ; import ApiGenerator , { getOperationName as _getOperationName , getReferenceName , isReference , supportDeepObjects , } from 'oazapfts/lib/codegen/generate' ; import { createQuestionToken , keywordType , createPropertyAssignment , isValidIdentifier , } from 'oazapfts/lib/codegen/tscodegen' ; import type { OpenAPIV3 } from 'openapi-types' ; import { generateReactHooks } from './generators/react-hooks' ; import type { EndpointMatcher , EndpointOverrides , GenerationOptions , OperationDefinition , TextMatcher } from './types' ; import { capitalize , getOperationDefinitions , getV3Doc , isQuery as testIsQuery , removeUndefined } from './utils' ; import type { ObjectPropertyDefinitions } from './codegen' ; import { generateCreateApiCall , generateEndpointDefinition , generateImportNode } from './codegen' ; import { factory } from './utils/factory' ; const generatedApiName = 'injectedRtkApi' ; function defaultIsDataResponse ( code ) { const parsedCode = Number ( code ) ; return ! Number . isNaN ( parsedCode ) && parsedCode >= 200 && parsedCode < 300 ; } function getOperationName ( { verb , path , operation } ) { return _getOperationName ( verb , path , operation . operationId ) ; } function patternMatches ( pattern ? ) { const filters = Array . isArray ( pattern ) ? pattern : [ pattern ] ; return function matcher ( operationName ) { if ( ! pattern ) return true ; return filters . some ( ( filter ) => typeof filter === 'string' ? filter === operationName : filter ?. test ( operationName ) ) ; } ; } function operationMatches ( pattern ? ) { const checkMatch = typeof pattern === 'function' ? pattern : patternMatches ( pattern ) ; return function matcher ( operationDefinition ) { if ( ! pattern ) return true ; const operationName = getOperationName ( operationDefinition ) ; return checkMatch ( operationName , operationDefinition ) ; } ; } export function getOverrides ( operation , endpointOverrides ? ) { return endpointOverrides ?. find ( ( override ) => operationMatches ( override . pattern ) ( operation ) ) ; } export async function generateApi ( spec , { apiFile , apiImport = 'api' , exportName = 'enhancedApi' , argSuffix = 'ApiArg' , responseSuffix = 'ApiResponse' , hooks = false , outputFile , isDataResponse = defaultIsDataResponse , filterEndpoints , endpointOverrides , } ) { const v3Doc = await getV3Doc ( spec ) ; const apiGen = new ApiGenerator ( v3Doc , { } ) ; const operationDefinitions = getOperationDefinitions ( v3Doc ) . filter ( operationMatches ( filterEndpoints ) ) ; const resultFile = ts . createSourceFile ( 'someFileName.ts' , '' , ts . ScriptTarget . Latest , false , ts . ScriptKind . TS ) ; const printer = ts . createPrinter ( { newLine : ts . NewLineKind . LineFeed } ) ; const interfaces = { } ; function registerInterface ( declaration ) { const name = declaration . name . escapedText . toString ( ) ; if ( name in interfaces ) { throw new Error ( `interface/type alias ${ name } already registered` ) ; } interfaces [ name ] = declaration ; return declaration ; } if ( outputFile ) { outputFile = path . resolve ( process . cwd ( ) , outputFile ) ; if ( apiFile . startsWith ( '.' ) ) { apiFile = path . relative ( path . dirname ( outputFile ) , apiFile ) ; if ( ! apiFile . startsWith ( '.' ) ) apiFile = './' + apiFile ; } } apiFile = apiFile . replace ( /\.[jt]sx?$/ , '' ) ; const sourceCode = printer . printNode ( ts . EmitHint . Unspecified , factory . createSourceFile ( [ generateImportNode ( apiFile , { [ apiImport ] : 'api' } ) , generateCreateApiCall ( { endpointDefinitions : factory . createObjectLiteralExpression ( operationDefinitions . map ( ( operationDefinition ) => generateEndpoint ( { operationDefinition , overrides : getOverrides ( operationDefinition , endpointOverrides ) , } ) ) , true ) , } ) , factory . createExportDeclaration ( undefined , undefined , false , factory . createNamedExports ( [ factory . createExportSpecifier ( factory . createIdentifier ( generatedApiName ) , factory . createIdentifier ( exportName ) ) , ] ) , undefined ) , ... Object . values ( interfaces ) , ... apiGen [ 'aliases' ] , ... ( hooks ? [ generateReactHooks ( { exportName : generatedApiName , operationDefinitions , endpointOverrides } ) ] : [ ] ) , ] , factory . createToken ( ts . SyntaxKind . EndOfFileToken ) , ts . NodeFlags . None ) , resultFile ) ; return sourceCode ; function generateEndpoint ( { operationDefinition , overrides , } ) { const { verb , path , pathItem , operation , operation : { responses , requestBody } , } = operationDefinition ; const operationName = getOperationName ( { verb , path , operation } ) ; const isQuery = testIsQuery ( verb , overrides ) ; const returnsJson = apiGen . getResponseType ( responses ) === 'json' ; let ResponseType = factory . createKeywordTypeNode ( ts . SyntaxKind . UnknownKeyword ) ; if ( returnsJson ) { const returnTypes = Object . entries ( responses || { } ) . map ( ( [ code , response ] ) => [ code , apiGen . resolve ( response ) , apiGen . getTypeFromResponse ( response ) || factory . createKeywordTypeNode ( ts . SyntaxKind . UndefinedKeyword ) , ] as const ) . filter ( ( [ status , response ] ) => isDataResponse ( status , apiGen . resolve ( response ) , responses || { } ) ) . map ( ( [ code , response , type ] ) => ts . addSyntheticLeadingComment ( { ... type } , ts . SyntaxKind . MultiLineCommentTrivia , `* status ${ code } ${ response . description } ` , false ) ) . filter ( ( type ) => type !== keywordType . void ) ; if ( returnTypes . length > 0 ) { ResponseType = factory . createUnionTypeNode ( returnTypes ) ; } } const ResponseTypeName = factory . createTypeReferenceNode ( registerInterface ( factory . createTypeAliasDeclaration ( undefined , [ factory . createModifier ( ts . SyntaxKind . ExportKeyword ) ] , capitalize ( operationName + responseSuffix ) , undefined , ResponseType ) ) . name ) ; const parameters = supportDeepObjects ( [ ... apiGen . resolveArray ( pathItem . parameters ) , ... apiGen . resolveArray ( operation . parameters ) , ] ) ; const allNames = parameters . map ( ( p ) => p . name ) ; const queryArg = { } ; for ( const param of parameters ) { const isPureSnakeCase = /^[a-zA-Z][a-zA-Z0-9_]*$/ . test ( param . name ) ; const camelCaseName = camelCase ( param . name ) ; const name = isPureSnakeCase && ! allNames . includes ( camelCaseName ) ? camelCaseName : param . name ; queryArg [ name ] = { origin : 'param' , name , originalName : param . name , type : apiGen . getTypeFromSchema ( isReference ( param ) ? param : param . schema ) , required : param . required , param , } ; } if ( requestBody ) { const body = apiGen . resolve ( requestBody ) ; const schema = apiGen . getSchemaFromContent ( body . content ) ; const type = apiGen . getTypeFromSchema ( schema ) ; const schemaName = camelCase ( ( type as any ) . name || getReferenceName ( schema ) || 'body' ) ; let name = schemaName in queryArg ? 'body' : schemaName ; while ( name in queryArg ) { name = '_' + name ; } queryArg [ name ] = { origin : 'body' , name , originalName : schemaName , type : apiGen . getTypeFromSchema ( schema ) , required : true , body , } ; } const propertyName = ( name ) : ts . PropertyName => { if ( typeof name === 'string' ) { return isValidIdentifier ( name ) ? factory . createIdentifier ( name ) : factory . createStringLiteral ( name ) ; } return name ; } ; const queryArgValues = Object . values ( queryArg ) ; const QueryArg = factory . createTypeReferenceNode ( registerInterface ( factory . createTypeAliasDeclaration ( undefined , [ factory . createModifier ( ts . SyntaxKind . ExportKeyword ) ] , capitalize ( operationName + argSuffix ) , undefined , queryArgValues . length > 0 ? factory . createTypeLiteralNode ( queryArgValues . map ( ( def ) => { const comment = def . origin === 'param' ? def . param . description : def . body . description ; const node = factory . createPropertySignature ( undefined , propertyName ( def . name ) , createQuestionToken ( ! def . required ) , def . type ) ; if ( comment ) { return ts . addSyntheticLeadingComment ( node , ts . SyntaxKind . MultiLineCommentTrivia , `* ${ comment } ` , true ) ; } return node ; } ) ) : factory . createKeywordTypeNode ( ts . SyntaxKind . VoidKeyword ) ) ) . name ) ; return generateEndpointDefinition ( { operationName , type : isQuery ? 'query' : 'mutation' , Response : ResponseTypeName , QueryArg , queryFn : generateQueryFn ( { operationDefinition , queryArg , isQuery } ) , extraEndpointsProps : isQuery ? generateQueryEndpointProps ( { operationDefinition } ) : generateMutationEndpointProps ( { operationDefinition } ) , } ) ; } function generateQueryFn ( { operationDefinition , queryArg , isQuery , } ) { const { path , verb } = operationDefinition ; const pathParameters = Object . values ( queryArg ) . filter ( ( def ) => def . origin === 'param' && def . param . in === 'path' ) ; const queryParameters = Object . values ( queryArg ) . filter ( ( def ) => def . origin === 'param' && def . param . in === 'query' ) ; const headerParameters = Object . values ( queryArg ) . filter ( ( def ) => def . origin === 'param' && def . param . in === 'header' ) ; const cookieParameters = Object . values ( queryArg ) . filter ( ( def ) => def . origin === 'param' && def . param . in === 'cookie' ) ; const bodyParameter = Object . values ( queryArg ) . find ( ( def ) => def . origin === 'body' ) ; const rootObject = factory . createIdentifier ( 'queryArg' ) ; return factory . createArrowFunction ( undefined , undefined , Object . keys ( queryArg ) . length ? [ factory . createParameterDeclaration ( undefined , undefined , undefined , rootObject , undefined , undefined , undefined ) , ] : [ ] , undefined , factory . createToken ( ts . SyntaxKind . EqualsGreaterThanToken ) , factory . createParenthesizedExpression ( factory . createObjectLiteralExpression ( [ factory . createPropertyAssignment ( factory . createIdentifier ( 'url' ) , generatePathExpression ( path , pathParameters , rootObject ) ) , isQuery && verb . toUpperCase ( ) === 'GET' ? undefined : factory . createPropertyAssignment ( factory . createIdentifier ( 'method' ) , factory . createStringLiteral ( verb . toUpperCase ( ) ) ) , bodyParameter === undefined ? undefined : factory . createPropertyAssignment ( factory . createIdentifier ( 'body' ) , factory . createPropertyAccessExpression ( rootObject , factory . createIdentifier ( bodyParameter . name ) ) ) , cookieParameters . length === 0 ? undefined : factory . createPropertyAssignment ( factory . createIdentifier ( 'cookies' ) , generateQuerArgObjectLiteralExpression ( cookieParameters , rootObject ) ) , headerParameters . length === 0 ? undefined : factory . createPropertyAssignment ( factory . createIdentifier ( 'headers' ) , generateQuerArgObjectLiteralExpression ( headerParameters , rootObject ) ) , queryParameters . length === 0 ? undefined : factory . createPropertyAssignment ( factory . createIdentifier ( 'params' ) , generateQuerArgObjectLiteralExpression ( queryParameters , rootObject ) ) , ] . filter ( removeUndefined ) , false ) ) ) ; } function generateQueryEndpointProps ( { } ) { return { } ; } function generateMutationEndpointProps ( { } ) { return { } ; } } function accessProperty ( rootObject , propertyName ) { return isValidIdentifier ( propertyName ) ? factory . createPropertyAccessExpression ( rootObject , factory . createIdentifier ( propertyName ) ) : factory . createElementAccessExpression ( rootObject , factory . createStringLiteral ( propertyName ) ) ; } function generatePathExpression ( path , pathParameters , rootObject ) { const expressions = [ ] ; const head = path . replace ( /\{(.*?)\}(.*?)(?=\{|$)/g , ( _ , expression , literal ) => { const param = pathParameters . find ( ( p ) => p . originalName === expression ) ; if ( ! param ) { throw new Error ( `path parameter ${ expression } does not seem to be defined in '${ path }'!` ) ; } expressions . push ( [ param . name , literal ] ) ; return '' ; } ) ; return expressions . length ? factory . createTemplateExpression ( factory . createTemplateHead ( head ) , expressions . map ( ( [ prop , literal ] , index ) => factory . createTemplateSpan ( accessProperty ( rootObject , prop ) , index === expressions . length - 1 ? factory . createTemplateTail ( literal ) : factory . createTemplateMiddle ( literal ) ) ) ) : factory . createNoSubstitutionTemplateLiteral ( head ) ; } function generateQuerArgObjectLiteralExpression ( queryArgs , rootObject ) { return factory . createObjectLiteralExpression ( queryArgs . map ( ( param ) => createPropertyAssignment ( param . originalName , accessProperty ( rootObject , param . name ) ) , true ) ) ; } type QueryArgDefinition = { name : string ; originalName : string ; type : ts . TypeNode ; required ? : boolean ; } & ( { origin : 'param' ; param : OpenAPIV3 . ParameterObject ; } | { origin : 'body' ; body : OpenAPIV3 . RequestBodyObject ; } ) ; type QueryArgDefinitions = Record < string , QueryArgDefinition > ;