@7nohe/openapi-react-query-codegen
Version:
OpenAPI React Query Codegen
165 lines (164 loc) • 6.86 kB
JavaScript
import { Node, } from "ts-morph";
import ts from "typescript";
import { capitalizeFirstLetter, extractPropertiesFromObjectParam, getNameFromVariable, getShortType, getVariableArrowFunctionParameters, } from "./common.mjs";
import { modelsFileName, serviceFileName } from "./constants.mjs";
import { getServices } from "./service.mjs";
function getPageParamTypeKind(type) {
const types = type.isUnion()
? type.types.filter((item) => !(item.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)))
: [type];
if (types.length > 0 &&
types.every((item) => item.flags & ts.TypeFlags.StringLike)) {
return "string";
}
if (types.length > 0 &&
types.every((item) => item.flags & ts.TypeFlags.NumberLike)) {
return "number";
}
return "other";
}
/**
* Extract parameter information from a method's variable declaration.
*/
function extractParameters(method, pageParam) {
const arrowParams = getVariableArrowFunctionParameters(method);
if (!arrowParams.length) {
return [];
}
return arrowParams.flatMap((param) => {
const paramNodes = extractPropertiesFromObjectParam(param);
return paramNodes
.filter((p) => p.name !== pageParam)
.map((refParam) => ({
name: refParam.name,
typeName: getShortType(refParam.type?.getText() ?? ""),
optional: refParam.optional,
}));
});
}
/**
* Read the operation's Data type name from the first type argument of the
* SDK options parameter (`Options<XData, ThrowOnError>`).
* See OperationInfo.dataTypeName for why the name is read from the signature
* instead of derived from the method name (#213).
*/
function getDataTypeNameFromSignature(optionsParam) {
const typeNode = optionsParam?.getTypeNode();
if (!typeNode || !Node.isTypeReference(typeNode))
return undefined;
if (typeNode.getTypeName().getText() !== "Options")
return undefined;
const [dataTypeArg] = typeNode.getTypeArguments();
return dataTypeArg?.getText();
}
/**
* Get paginatable Data types by checking if they have the pageParam in their query property.
* Uses TypeScript compiler API for accurate AST traversal.
* The map is keyed by the Data type name (e.g., "FindPetsData") so callers can
* look it up with the name read from the SDK signature.
*/
function getPaginatableMethods(project, pageParam) {
const modelsFile = project
.getSourceFiles()
.find((sf) => sf.getFilePath().includes(modelsFileName));
if (!modelsFile)
return new Map();
const paginatableMethods = new Map();
const typeChecker = project.getTypeChecker().compilerObject;
const modelDeclarations = modelsFile.getExportedDeclarations();
const entries = modelDeclarations.entries();
for (const [key, value] of entries) {
// Check if this is a *Data type (e.g., FindPetsData)
if (!key.endsWith("Data"))
continue;
const node = value[0].compilerNode;
if (!ts.isTypeAliasDeclaration(node))
continue;
const typeAliasDeclaration = node.type;
if (typeAliasDeclaration.kind !== ts.SyntaxKind.TypeLiteral)
continue;
// Look for 'query' property in the type literal
const query = typeAliasDeclaration.members.find((m) => m.kind === ts.SyntaxKind.PropertySignature &&
m.name?.getText() === "query");
if (!query)
continue;
// Check if query type has the pageParam
const queryType = query.type;
if (!queryType || queryType.kind !== ts.SyntaxKind.TypeLiteral)
continue;
const pageParamNode = queryType.members.find((m) => ts.isPropertySignature(m) && m.name?.getText() === pageParam);
if (pageParamNode) {
const pageParamType = pageParamNode.type?.getText(modelsFile.compilerNode);
const resolvedType = typeChecker.getTypeAtLocation(pageParamNode.type ?? pageParamNode);
paginatableMethods.set(key, {
type: pageParamType ?? "unknown",
typeKind: getPageParamTypeKind(resolvedType),
});
}
}
return paginatableMethods;
}
/**
* Parse operations from the OpenAPI-generated service file and return normalized DTOs.
*/
export async function parseOperations(project, pageParam) {
const service = await getServices(project);
const { methods } = service;
const paginatableMethods = getPaginatableMethods(project, pageParam);
return methods.map((desc) => {
const methodName = getNameFromVariable(desc.method);
const httpMethod = desc.httpMethodName.toUpperCase();
const parameters = extractParameters(desc.method);
// Use the SDK function's parameter optionality as the authoritative check.
// Generic types like Options<XData, ThrowOnError> may not resolve correctly
// via extractPropertiesFromObjectParam for type alias properties (path, url).
const sdkParams = getVariableArrowFunctionParameters(desc.method);
const allParamsOptional = sdkParams.length === 0 || sdkParams[0].isOptional();
const dataTypeName = getDataTypeNameFromSignature(sdkParams[0]);
const pageParamInfo = dataTypeName
? paginatableMethods.get(dataTypeName)
: undefined;
const isPaginatable = httpMethod === "GET" && pageParamInfo !== undefined;
return {
methodName,
capitalizedMethodName: capitalizeFirstLetter(methodName),
dataTypeName,
httpMethod,
jsDoc: desc.jsDoc,
isDeprecated: desc.isDeprecated,
parameters,
allParamsOptional,
isPaginatable,
pageParamType: isPaginatable ? pageParamInfo.type : undefined,
pageParamTypeKind: isPaginatable ? pageParamInfo.typeKind : undefined,
};
});
}
/**
* Build generation context from project configuration.
*/
export function buildGenerationContext(project, client, pageParam, nextPageParam, initialPageParam, omitInitialPageParam, version) {
const modelsFile = project
.getSourceFiles()
.find((sf) => sf.getFilePath().includes(modelsFileName));
const serviceFile = project
.getSourceFiles()
.find((sf) => sf.getFilePath().includes(serviceFileName));
if (!serviceFile) {
throw new Error("No service node found");
}
const modelNames = modelsFile
? Array.from(modelsFile.getExportedDeclarations().keys())
: [];
const serviceNames = Array.from(serviceFile.getExportedDeclarations().keys());
return {
client,
modelNames,
serviceNames,
pageParam,
nextPageParam,
initialPageParam,
omitInitialPageParam,
version,
};
}