diff --git a/src/core/generators/angular.ts b/src/core/generators/angular.ts index c7ec99c49..d6b7000ae 100644 --- a/src/core/generators/angular.ts +++ b/src/core/generators/angular.ts @@ -35,6 +35,8 @@ const ANGULAR_DEPENDENCIES: GeneratorDependency[] = [ }, ]; +const returnTypesToWrite: string[] = []; + export const getAngularDependencies = () => ANGULAR_DEPENDENCIES; export const generateAngularTitle = (title: string) => { @@ -98,7 +100,8 @@ export class ${title} { private http: HttpClient, ) {}`; -export const generateAngularFooter = () => '};\n'; +export const generateAngularFooter = () => + `};\n\n${returnTypesToWrite.join('\n')}`; const generateImplementation = ( { @@ -127,6 +130,14 @@ const generateImplementation = ( isFormUrlEncoded, }); + const dataType = response.definition.success || 'unknown'; + + returnTypesToWrite.push( + `export type ${pascal( + operationName, + )}ClientResult = NonNullable<${dataType}>`, + ); + if (mutator) { const mutatorConfig = generateMutatorConfig({ route, @@ -145,9 +156,10 @@ const generateImplementation = ( ) : ''; - return ` ${operationName}(\n ${toObjectString(props, 'implementation')}\n ${ + return ` ${operationName}(\n ${toObjectString( + props, + 'implementation', + )}\n ${ isRequestOptions && mutator.hasThirdArg ? `options?: ThirdParameter` : '' @@ -172,9 +184,10 @@ const generateImplementation = ( isAngular: true, }); - return ` ${operationName}(\n ${toObjectString(props, 'implementation')} ${ + return ` ${operationName}(\n ${toObjectString( + props, + 'implementation', + )} ${ isRequestOptions ? `options?: HttpClientOptions\n` : '' } ): Observable {${bodyForm} return this.http.${verb}(${options}); diff --git a/src/core/generators/axios.ts b/src/core/generators/axios.ts index a44ce9e83..a31a32d94 100644 --- a/src/core/generators/axios.ts +++ b/src/core/generators/axios.ts @@ -1,4 +1,5 @@ import { + ClientFooterBuilder, GeneratorClient, GeneratorDependency, GeneratorOptions, @@ -31,6 +32,8 @@ const AXIOS_DEPENDENCIES: GeneratorDependency[] = [ }, ]; +const returnTypesToWrite: Map string> = new Map(); + export const getAxiosDependencies = (hasGlobalMutator: boolean) => [ ...(!hasGlobalMutator ? AXIOS_DEPENDENCIES : []), ]; @@ -84,6 +87,18 @@ const generateAxiosImplementation = ( ) : ''; + returnTypesToWrite.set( + operationName, + (title?: string) => + `export type ${pascal( + operationName, + )}Result = NonNullable['${operationName}']` + : `typeof ${operationName}` + }>>`, + ); + return `const ${operationName} = (\n ${toObjectString( props, 'implementation', @@ -110,6 +125,16 @@ const generateAxiosImplementation = ( isFormUrlEncoded, }); + returnTypesToWrite.set( + operationName, + (title?: string) => + `export type ${pascal(operationName)}Result = AsyncReturnType<${ + title + ? `ReturnType['${operationName}']` + : `typeof ${operationName}` + }>`, + ); + return `const ${operationName} = >(\n ${toObjectString(props, 'implementation')} ${ @@ -150,8 +175,29 @@ export const generateAxiosHeader = ({ } ${!noFunction ? `export const ${title} = () => {\n` : ''}`; -export const generateAxiosFooter = (operationNames: string[] = []) => - `return {${operationNames.join(',')}}};\n`; +export const generateAxiosFooter: ClientFooterBuilder = ({ + operationNames, + title, + noFunction, +}) => { + const functionFooter = `return {${operationNames.join(',')}}};\n`; + const returnTypesArr = operationNames + .map((n) => { + return returnTypesToWrite.has(n) + ? returnTypesToWrite.get(n)?.(noFunction || !title ? undefined : title) + : ''; + }) + .filter(Boolean); + const returnTypes = returnTypesArr.length + ? `\n// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AsyncReturnType< +T extends (...args: any) => Promise +> = T extends (...args: any) => Promise ? R : any; +\n${returnTypesArr.join('\n')}` + : ''; + + return noFunction ? returnTypes : functionFooter + returnTypes; +}; export const generateAxios = ( verbOptions: GeneratorVerbOptions, diff --git a/src/core/generators/client.ts b/src/core/generators/client.ts index 58b6d8e99..69940b473 100644 --- a/src/core/generators/client.ts +++ b/src/core/generators/client.ts @@ -69,7 +69,7 @@ export const GENERATOR_CLIENT: GeneratorClients = { isRequestOptions: boolean; }) => generateAxiosHeader({ ...options, noFunction: true }), dependencies: getAxiosDependencies, - footer: () => '', + footer: (options) => generateAxiosFooter({ ...options, noFunction: true }), title: generateAxiosTitle, }, angular: { @@ -177,13 +177,36 @@ export const generateClientHeader = ({ }; }; -export const generateClientFooter = ( - outputClient: OutputClient | OutputClientFunc = DEFAULT_CLIENT, - operationNames: string[], -): GeneratorClientExtra => { +export const generateClientFooter = ({ + outputClient = DEFAULT_CLIENT, + operationNames, + title, + customTitleFunc, +}: { + outputClient: OutputClient | OutputClientFunc; + operationNames: string[]; + title: string; + customTitleFunc?: (title: string) => string; +}): GeneratorClientExtra => { + const titles = generateClientTitle(outputClient, title, customTitleFunc); const { footer } = getGeneratorClient(outputClient); + let implementation: string; + try { + if (isFunction(outputClient)) { + implementation = (footer as (operationNames: any) => string)(operationNames); + // being here means that the previous call worked + console.warn( + '[WARN] Passing an array of strings for operations names to the footer function is deprecated and will be removed in a future major release. Please pass them in an object instead: { operationNames: string[] }.', + ); + } else { + implementation = footer({ operationNames, title: titles.implementation }); + } + } catch (e) { + implementation = footer({ operationNames, title: titles.implementation }); + } + return { - implementation: footer(operationNames), + implementation, implementationMSW: `]\n`, }; }; diff --git a/src/core/generators/query.ts b/src/core/generators/query.ts index d36ba1bd1..7d901f65e 100644 --- a/src/core/generators/query.ts +++ b/src/core/generators/query.ts @@ -376,18 +376,25 @@ const generateQueryImplementation = ({ : response.definition.errors || 'unknown'; } + const dataType = mutator?.isHook + ? `ReturnType` + : `typeof ${operationName}`; + return ` -export const ${camel(`use-${name}`)} = ` - : `typeof ${operationName}` - }>, TError = ${errorType}>(\n ${queryProps} ${generateQueryArguments({ - operationName, - definitions: '', - mutator, - isRequestOptions, - type, - })}\n ): ${returnType} & { queryKey: QueryKey } => { +export type ${pascal(name)}QueryResult = NonNullable> +export type ${pascal(name)}QueryError = ${errorType} + +export const ${camel( + `use-${name}`, + )} = , TError = ${errorType}>(\n ${queryProps} ${generateQueryArguments( + { + operationName, + definitions: '', + mutator, + isRequestOptions, + type, + }, + )}\n ): ${returnType} & { queryKey: QueryKey } => { ${ isRequestOptions @@ -545,7 +552,16 @@ const generateQueryHook = ( : response.definition.errors || 'unknown'; } + const dataType = mutator?.isHook + ? `ReturnType` + : `typeof ${operationName}`; + return ` + export type ${pascal( + operationName, + )}MutationResult = NonNullable> + export type ${pascal(operationName)}MutationError = ${errorType} + export const ${camel(`use-${operationName}`)} = (${generateQueryArguments({ @@ -573,13 +589,9 @@ const generateQueryHook = ( } - const mutationFn: MutationFunction` - : `typeof ${operationName}` - }>, ${definitions ? `{${definitions}}` : 'TVariables'}> = (${ - properties ? 'props' : '' - }) => { + const mutationFn: MutationFunction, ${ + definitions ? `{${definitions}}` : 'TVariables' + }> = (${properties ? 'props' : ''}) => { ${properties ? `const {${properties}} = props || {}` : ''}; return ${operationName}(${properties}${properties ? ',' : ''}${ diff --git a/src/core/generators/swr.ts b/src/core/generators/swr.ts index 8e7416bfe..5949f4a01 100644 --- a/src/core/generators/swr.ts +++ b/src/core/generators/swr.ts @@ -11,7 +11,7 @@ import { GetterPropType, GetterResponse, } from '../../types/getters'; -import { camel } from '../../utils/case'; +import { camel, pascal } from '../../utils/case'; import { toObjectString } from '../../utils/string'; import { isSyntheticDefaultImportsAllow } from '../../utils/tsconfig'; import { generateVerbImports } from './imports'; @@ -205,6 +205,11 @@ const generateSwrImplementation = ({ } return ` + export type ${pascal( + operationName, + )}QueryResult = NonNullable> + export type ${pascal(operationName)}QueryError = ${errorType} + export const ${camel( `use-${operationName}`, )} = (\n ${swrProps} ${generateSwrArguments({ diff --git a/src/core/writers/target.ts b/src/core/writers/target.ts index 4e10ac5a7..8aef08e1a 100644 --- a/src/core/writers/target.ts +++ b/src/core/writers/target.ts @@ -58,7 +58,11 @@ export const generateTarget = ( acc.implementationMSW.handler = header.implementationMSW + acc.implementationMSW.handler; - const footer = generateClientFooter(options?.client, operationNames); + const footer = generateClientFooter({ + outputClient: options?.client, + operationNames, + title: pascal(info.title), + }); acc.implementation += footer.implementation; acc.implementationMSW.handler += footer.implementationMSW; } diff --git a/src/core/writers/targetTags.ts b/src/core/writers/targetTags.ts index 774d9768c..68e67804f 100644 --- a/src/core/writers/targetTags.ts +++ b/src/core/writers/targetTags.ts @@ -91,7 +91,11 @@ export const generateTargetForTags = ( const operationNames = Object.values(operations) .filter(({ tags }) => tags.includes(tag)) .map(({ operationName }) => operationName); - const footer = generateClientFooter(options?.client, operationNames); + const footer = generateClientFooter({ + outputClient: options?.client, + operationNames, + title: pascal(tag), + }); const header = generateClientHeader({ outputClient: options.client, isRequestOptions: options.override.requestOptions !== false, diff --git a/src/types/generator.ts b/src/types/generator.ts index 8ec351225..a235b845a 100644 --- a/src/types/generator.ts +++ b/src/types/generator.ts @@ -147,7 +147,13 @@ export type ClientHeaderBuilder = (params: { provideIn: boolean | 'root' | 'any'; }) => string; -export type ClientFooterBuilder = (operationIds?: string[]) => string; +export type ClientFooterBuilder = ( + params: { + noFunction?: boolean | undefined; + operationNames: string[]; + title?: string; + }, +) => string; export type ClientTitleBuilder = (title: string) => string;