Skip to content

Commit

Permalink
feat(query): generate query return type along each client endpoints (#…
Browse files Browse the repository at this point in the history
…318)

* feat(query): generate query return type for along with hooks

* fix(query): use NonNullable for QueryData type generation

* fix(query): export QueryError type for muation query

* fix(query): prefer MutationData and MutationError suffix for mutations

* fix(query): prefer Result over Data for result data type suffix

* feat(angular): add return type for angular client

* feat(swr): add return type for swr client

* feat(axios): add return type for swr client

* feat(axios): correctly handle return tyep for axios

* fix(footer): warn about params change if footer function call fail

Use it to avoid a breaking change
  • Loading branch information
CPatchane authored Mar 14, 2022
1 parent 8e99ff9 commit d7cb2d4
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 37 deletions.
27 changes: 20 additions & 7 deletions src/core/generators/angular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ const ANGULAR_DEPENDENCIES: GeneratorDependency[] = [
},
];

const returnTypesToWrite: string[] = [];

export const getAngularDependencies = () => ANGULAR_DEPENDENCIES;

export const generateAngularTitle = (title: string) => {
Expand Down Expand Up @@ -98,7 +100,8 @@ export class ${title} {
private http: HttpClient,
) {}`;

export const generateAngularFooter = () => '};\n';
export const generateAngularFooter = () =>
`};\n\n${returnTypesToWrite.join('\n')}`;

const generateImplementation = (
{
Expand Down Expand Up @@ -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,
Expand All @@ -145,9 +156,10 @@ const generateImplementation = (
)
: '';

return ` ${operationName}<TData = ${
response.definition.success || 'unknown'
}>(\n ${toObjectString(props, 'implementation')}\n ${
return ` ${operationName}<TData = ${dataType}>(\n ${toObjectString(
props,
'implementation',
)}\n ${
isRequestOptions && mutator.hasThirdArg
? `options?: ThirdParameter<typeof ${mutator.name}>`
: ''
Expand All @@ -172,9 +184,10 @@ const generateImplementation = (
isAngular: true,
});

return ` ${operationName}<TData = ${
response.definition.success || 'unknown'
}>(\n ${toObjectString(props, 'implementation')} ${
return ` ${operationName}<TData = ${dataType}>(\n ${toObjectString(
props,
'implementation',
)} ${
isRequestOptions ? `options?: HttpClientOptions\n` : ''
} ): Observable<TData> {${bodyForm}
return this.http.${verb}<TData>(${options});
Expand Down
50 changes: 48 additions & 2 deletions src/core/generators/axios.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ClientFooterBuilder,
GeneratorClient,
GeneratorDependency,
GeneratorOptions,
Expand Down Expand Up @@ -31,6 +32,8 @@ const AXIOS_DEPENDENCIES: GeneratorDependency[] = [
},
];

const returnTypesToWrite: Map<string, (title?: string) => string> = new Map();

export const getAxiosDependencies = (hasGlobalMutator: boolean) => [
...(!hasGlobalMutator ? AXIOS_DEPENDENCIES : []),
];
Expand Down Expand Up @@ -84,6 +87,18 @@ const generateAxiosImplementation = (
)
: '';

returnTypesToWrite.set(
operationName,
(title?: string) =>
`export type ${pascal(
operationName,
)}Result = NonNullable<AsyncReturnType<${
title
? `ReturnType<typeof ${title}>['${operationName}']`
: `typeof ${operationName}`
}>>`,
);

return `const ${operationName} = (\n ${toObjectString(
props,
'implementation',
Expand All @@ -110,6 +125,16 @@ const generateAxiosImplementation = (
isFormUrlEncoded,
});

returnTypesToWrite.set(
operationName,
(title?: string) =>
`export type ${pascal(operationName)}Result = AsyncReturnType<${
title
? `ReturnType<typeof ${title}>['${operationName}']`
: `typeof ${operationName}`
}>`,
);

return `const ${operationName} = <TData = AxiosResponse<${
response.definition.success || 'unknown'
}>>(\n ${toObjectString(props, 'implementation')} ${
Expand Down Expand Up @@ -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<any>
> = T extends (...args: any) => Promise<infer R> ? R : any;
\n${returnTypesArr.join('\n')}`
: '';

return noFunction ? returnTypes : functionFooter + returnTypes;
};

export const generateAxios = (
verbOptions: GeneratorVerbOptions,
Expand Down
35 changes: 29 additions & 6 deletions src/core/generators/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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`,
};
};
Expand Down
48 changes: 30 additions & 18 deletions src/core/generators/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,18 +376,25 @@ const generateQueryImplementation = ({
: response.definition.errors || 'unknown';
}

const dataType = mutator?.isHook
? `ReturnType<typeof use${pascal(operationName)}Hook>`
: `typeof ${operationName}`;

return `
export const ${camel(`use-${name}`)} = <TData = AsyncReturnType<${
mutator?.isHook
? `ReturnType<typeof use${pascal(operationName)}Hook>`
: `typeof ${operationName}`
}>, TError = ${errorType}>(\n ${queryProps} ${generateQueryArguments({
operationName,
definitions: '',
mutator,
isRequestOptions,
type,
})}\n ): ${returnType} & { queryKey: QueryKey } => {
export type ${pascal(name)}QueryResult = NonNullable<AsyncReturnType<${dataType}>>
export type ${pascal(name)}QueryError = ${errorType}
export const ${camel(
`use-${name}`,
)} = <TData = AsyncReturnType<${dataType}>, TError = ${errorType}>(\n ${queryProps} ${generateQueryArguments(
{
operationName,
definitions: '',
mutator,
isRequestOptions,
type,
},
)}\n ): ${returnType} & { queryKey: QueryKey } => {
${
isRequestOptions
Expand Down Expand Up @@ -545,7 +552,16 @@ const generateQueryHook = (
: response.definition.errors || 'unknown';
}

const dataType = mutator?.isHook
? `ReturnType<typeof use${pascal(operationName)}Hook>`
: `typeof ${operationName}`;

return `
export type ${pascal(
operationName,
)}MutationResult = NonNullable<AsyncReturnType<${dataType}>>
export type ${pascal(operationName)}MutationError = ${errorType}
export const ${camel(`use-${operationName}`)} = <TError = ${errorType},
${!definitions ? `TVariables = void,` : ''}
TContext = unknown>(${generateQueryArguments({
Expand Down Expand Up @@ -573,13 +589,9 @@ const generateQueryHook = (
}
const mutationFn: MutationFunction<AsyncReturnType<${
mutator?.isHook
? `ReturnType<typeof use${pascal(operationName)}Hook>`
: `typeof ${operationName}`
}>, ${definitions ? `{${definitions}}` : 'TVariables'}> = (${
properties ? 'props' : ''
}) => {
const mutationFn: MutationFunction<AsyncReturnType<${dataType}>, ${
definitions ? `{${definitions}}` : 'TVariables'
}> = (${properties ? 'props' : ''}) => {
${properties ? `const {${properties}} = props || {}` : ''};
return ${operationName}(${properties}${properties ? ',' : ''}${
Expand Down
7 changes: 6 additions & 1 deletion src/core/generators/swr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -205,6 +205,11 @@ const generateSwrImplementation = ({
}

return `
export type ${pascal(
operationName,
)}QueryResult = NonNullable<AsyncReturnType<typeof ${operationName}>>
export type ${pascal(operationName)}QueryError = ${errorType}
export const ${camel(
`use-${operationName}`,
)} = <TError = ${errorType}>(\n ${swrProps} ${generateSwrArguments({
Expand Down
6 changes: 5 additions & 1 deletion src/core/writers/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
6 changes: 5 additions & 1 deletion src/core/writers/targetTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion src/types/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down

1 comment on commit d7cb2d4

@vercel
Copy link

@vercel vercel bot commented on d7cb2d4 Mar 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.