@7nohe/openapi-react-query-codegen
Version:
OpenAPI React Query Codegen
257 lines (256 loc) • 15.6 kB
JavaScript
import ts from "typescript";
import { BuildCommonTypeName, EqualsOrGreaterThanToken, TData, TError, capitalizeFirstLetter, createQueryKeyExport, createQueryKeyFnExport, getNameFromVariable, getQueryKeyFnName, getRequestParamFromMethod, getVariableArrowFunctionParameters, queryKeyConstraint, queryKeyGenericType, } from "./common.mjs";
import { addJSDocToNode } from "./util.mjs";
const createApiResponseType = ({ methodName, client, modelNames, }) => {
/** Awaited<ReturnType<typeof myClass.myMethod>> */
const awaitedResponseDataType = ts.factory.createIndexedAccessTypeNode(ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Awaited"), [
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("ReturnType"), [
ts.factory.createTypeQueryNode(ts.factory.createIdentifier(methodName), undefined),
]),
]), ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("data")));
/** DefaultResponseDataType
* export type MyClassMethodDefaultResponse = Awaited<ReturnType<typeof myClass.myMethod>>
*/
const apiResponse = ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(`${capitalizeFirstLetter(methodName)}DefaultResponse`), undefined, awaitedResponseDataType);
const responseDataType = ts.factory.createTypeParameterDeclaration(undefined, TData.text, undefined, ts.factory.createTypeReferenceNode(BuildCommonTypeName(apiResponse.name)));
// Response data type for suspense - wrap with NonNullable to exclude undefined
const suspenseResponseDataType = ts.factory.createTypeParameterDeclaration(undefined, TData.text, undefined, ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("NonNullable"), [
ts.factory.createTypeReferenceNode(BuildCommonTypeName(apiResponse.name)),
]));
const errorTypeName = `${capitalizeFirstLetter(methodName)}Error`;
const hasErrorType = modelNames.includes(errorTypeName);
const responseErrorType = ts.factory.createTypeParameterDeclaration(undefined, TError.text, undefined, hasErrorType
? client === "@hey-api/client-axios"
? ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("AxiosError"), [
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(errorTypeName)),
])
: ts.factory.createTypeReferenceNode(errorTypeName)
: client === "@hey-api/client-axios"
? ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("AxiosError"), [ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)])
: ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword));
return {
/**
* DefaultResponseDataType
*
* export type MyClassMethodDefaultResponse = Awaited<ReturnType<typeof myClass.myMethod>>
*/
apiResponse,
/**
* This will be the name of the type of the response type of the method
*
* MyClassMethodDefaultResponse
*/
responseDataType,
/**
* ResponseDataType for suspense - wrap with NonNullable to exclude undefined
*
* NonNullable<MyClassMethodDefaultResponse>
*/
suspenseResponseDataType,
/**
* ErrorDataType
*
* MyClassMethodError
*/
responseErrorType,
};
};
/**
* Return Type
*
* export const classNameMethodNameQueryResult<TData = MyClassMethodDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
*/
function createReturnTypeExport({ methodName, defaultApiResponse, }) {
return ts.factory.createTypeAliasDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier(`${capitalizeFirstLetter(methodName)}QueryResult`), [
ts.factory.createTypeParameterDeclaration(undefined, TData, undefined, ts.factory.createTypeReferenceNode(defaultApiResponse.name)),
ts.factory.createTypeParameterDeclaration(undefined, TError, undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword)),
], ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("UseQueryResult"), [
ts.factory.createTypeReferenceNode(TData),
ts.factory.createTypeReferenceNode(TError),
]));
}
export function hookNameFromMethod({ method, }) {
const methodName = getNameFromVariable(method);
return `use${capitalizeFirstLetter(methodName)}`;
}
export function createQueryKeyFromMethod({ method, }) {
const customHookName = hookNameFromMethod({ method });
const queryKey = `${customHookName}Key`;
return queryKey;
}
/**
* Creates a custom hook for a query
* @param queryString The type of query to use from react-query
* @param suffix The suffix to append to the hook name
*/
function createQueryHook({ queryString, suffix, responseDataType, responseErrorType, requestParams, method, pageParam, nextPageParam, initialPageParam, }) {
const methodName = getNameFromVariable(method);
const customHookName = hookNameFromMethod({ method });
const queryKey = createQueryKeyFromMethod({ method });
if (queryString === "useInfiniteQuery" &&
(pageParam === undefined || nextPageParam === undefined)) {
throw new Error("pageParam and nextPageParam are required for infinite queries");
}
const isInfiniteQuery = queryString === "useInfiniteQuery";
const isSuspenseQuery = queryString === "useSuspenseQuery";
// ts.TypeParameterDeclaration.default is ts.TypeNode | undefined.
// We know it's a TypeReferenceNode with an Identifier typeName because we created it
// via ts.factory in createApiResponseType, but TypeScript cannot infer the specific subtype.
const responseDataTypeRef = responseDataType.default;
const responseDataTypeIdentifier = responseDataTypeRef.typeName;
const hookExport = ts.factory.createVariableStatement([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
ts.factory.createVariableDeclaration(ts.factory.createIdentifier(`${customHookName}${suffix}`), undefined, undefined, ts.factory.createArrowFunction(undefined, ts.factory.createNodeArray([
isInfiniteQuery
? ts.factory.createTypeParameterDeclaration(undefined, TData, undefined, ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("InfiniteData"), [
ts.factory.createTypeReferenceNode(responseDataTypeIdentifier),
]))
: responseDataType,
responseErrorType,
ts.factory.createTypeParameterDeclaration(undefined, "TQueryKey", queryKeyConstraint, ts.factory.createArrayTypeNode(ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword))),
]), [
...requestParams,
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier("queryKey"), ts.factory.createToken(ts.SyntaxKind.QuestionToken), queryKeyGenericType),
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier("options"), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Omit"), [
ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(isInfiniteQuery
? "UseInfiniteQueryOptions"
: isSuspenseQuery
? "UseSuspenseQueryOptions"
: "UseQueryOptions"), [
ts.factory.createTypeReferenceNode(TData),
ts.factory.createTypeReferenceNode(TError),
]),
ts.factory.createUnionTypeNode([
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("queryKey")),
ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("queryFn")),
]),
])),
], undefined, EqualsOrGreaterThanToken, ts.factory.createCallExpression(ts.factory.createIdentifier(queryString), isInfiniteQuery
? []
: [
ts.factory.createTypeReferenceNode(TData),
ts.factory.createTypeReferenceNode(TError),
], [
ts.factory.createObjectLiteralExpression([
ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryKey"), ts.factory.createCallExpression(BuildCommonTypeName(getQueryKeyFnName(queryKey)), undefined, [
ts.factory.createIdentifier("clientOptions"),
ts.factory.createIdentifier("queryKey"),
])),
ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryFn"), ts.factory.createArrowFunction(undefined, undefined, isInfiniteQuery
? [
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createObjectBindingPattern([
ts.factory.createBindingElement(undefined, undefined, ts.factory.createIdentifier("pageParam"), undefined),
]), undefined, undefined),
]
: [], undefined, EqualsOrGreaterThanToken, ts.factory.createAsExpression(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createCallExpression(ts.factory.createIdentifier(methodName), undefined, pageParam && isInfiniteQuery
? [
// { ...clientOptions, query: { ...clientOptions.query, page: pageParam as number } }
ts.factory.createObjectLiteralExpression([
ts.factory.createSpreadAssignment(ts.factory.createIdentifier("clientOptions")),
ts.factory.createPropertyAssignment(ts.factory.createIdentifier("query"), ts.factory.createObjectLiteralExpression([
ts.factory.createSpreadAssignment(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("clientOptions"), ts.factory.createIdentifier("query"))),
ts.factory.createPropertyAssignment(ts.factory.createIdentifier(pageParam), ts.factory.createAsExpression(ts.factory.createIdentifier("pageParam"), ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword))),
])),
]),
]
: // { ...clientOptions }
getVariableArrowFunctionParameters(method)
.length > 0
? [
ts.factory.createObjectLiteralExpression([
ts.factory.createSpreadAssignment(ts.factory.createIdentifier("clientOptions")),
]),
]
: undefined), ts.factory.createIdentifier("then")), undefined, [
ts.factory.createArrowFunction(undefined, undefined, [
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier("response"), undefined, undefined, undefined),
], undefined, EqualsOrGreaterThanToken, ts.factory.createAsExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("response"), ts.factory.createIdentifier("data")), ts.factory.createTypeReferenceNode(TData))),
]), ts.factory.createTypeReferenceNode(TData)))),
...createInfiniteQueryParams(pageParam, nextPageParam, initialPageParam),
ts.factory.createSpreadAssignment(ts.factory.createIdentifier("options")),
]),
]))),
], ts.NodeFlags.Const));
return hookExport;
}
export const createUseQuery = ({ functionDescription: { method, jsDoc }, client, pageParam, nextPageParam, initialPageParam, paginatableMethods, modelNames, }) => {
const methodName = getNameFromVariable(method);
const queryKey = createQueryKeyFromMethod({ method });
const { apiResponse: defaultApiResponse, responseDataType, suspenseResponseDataType, responseErrorType, } = createApiResponseType({
methodName,
client,
modelNames,
});
const requestParam = getRequestParamFromMethod(method, undefined, modelNames);
const infiniteRequestParam = getRequestParamFromMethod(method, pageParam, modelNames);
const requestParams = requestParam ? [requestParam] : [];
const queryHook = createQueryHook({
queryString: "useQuery",
suffix: "",
responseDataType,
responseErrorType,
requestParams,
method,
});
const suspenseQueryHook = createQueryHook({
queryString: "useSuspenseQuery",
suffix: "Suspense",
responseDataType: suspenseResponseDataType,
responseErrorType,
requestParams,
method,
});
const isInfiniteQuery = paginatableMethods.includes(methodName);
const infiniteQueryHook = isInfiniteQuery
? createQueryHook({
queryString: "useInfiniteQuery",
suffix: "Infinite",
responseDataType,
responseErrorType,
requestParams: infiniteRequestParam ? [infiniteRequestParam] : [],
method,
pageParam,
nextPageParam,
initialPageParam,
})
: undefined;
const hookWithJsDoc = addJSDocToNode(queryHook, jsDoc);
const suspenseHookWithJsDoc = addJSDocToNode(suspenseQueryHook, jsDoc);
const infiniteHookWithJsDoc = infiniteQueryHook
? addJSDocToNode(infiniteQueryHook, jsDoc)
: undefined;
const returnTypeExport = createReturnTypeExport({
methodName,
defaultApiResponse,
});
const queryKeyExport = createQueryKeyExport({
methodName,
queryKey,
});
const queryKeyFn = createQueryKeyFnExport(queryKey, method, "query", modelNames);
return {
apiResponse: defaultApiResponse,
returnType: returnTypeExport,
key: queryKeyExport,
queryHook: hookWithJsDoc,
suspenseQueryHook: suspenseHookWithJsDoc,
infiniteQueryHook: infiniteHookWithJsDoc,
queryKeyFn,
};
};
function createInfiniteQueryParams(pageParam, nextPageParam, initialPageParam = "1") {
if (pageParam === undefined || nextPageParam === undefined) {
return [];
}
return [
ts.factory.createPropertyAssignment(ts.factory.createIdentifier("initialPageParam"), ts.factory.createStringLiteral(initialPageParam)),
ts.factory.createPropertyAssignment(ts.factory.createIdentifier("getNextPageParam"),
// (response) => (response as { nextPage: number }).nextPage,
ts.factory.createArrowFunction(undefined, undefined, [
ts.factory.createParameterDeclaration(undefined, undefined, ts.factory.createIdentifier("response"), undefined, undefined),
], undefined, EqualsOrGreaterThanToken, ts.factory.createPropertyAccessExpression(ts.factory.createParenthesizedExpression(ts.factory.createAsExpression(ts.factory.createIdentifier("response"), nextPageParam.split(".").reduceRight((acc, segment) => {
return ts.factory.createTypeLiteralNode([
ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(segment), undefined, acc),
]);
}, ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword)))), ts.factory.createIdentifier(nextPageParam)))),
];
}