typescript-scaffolder
Version:
 ### Unit Test Coverage: 97.12%
196 lines (195 loc) • 9.12 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateApiClientFunction = generateApiClientFunction;
exports.generateApiClientFromFile = generateApiClientFromFile;
exports.generateApiClientsFromPath = generateApiClientsFromPath;
const ts_morph_1 = require("ts-morph");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const file_system_1 = require("../../utils/file-system");
const client_constructors_1 = require("../../utils/client-constructors");
const logger_1 = require("../../utils/logger");
const retry_constructors_1 = require("../../utils/retry-constructors");
const generate_retry_helper_1 = require("./generate-retry-helper");
function generateApiClientFunction(baseUrl, fileName, functionName, endpoint, config, interfaceInputDir, clientOutputDir, writeMode = 'overwrite') {
const funcName = 'generateApiClientFunction';
logger_1.Logger.debug(funcName, 'Generating api client function...');
const project = new ts_morph_1.Project();
const useRetry = !!config?.retry?.enabled;
(0, file_system_1.ensureDir)(clientOutputDir);
const outputFilePath = path_1.default.join(clientOutputDir, `${fileName}.ts`);
let sourceFile;
const fileExists = fs_1.default.existsSync(outputFilePath);
if (writeMode === 'append' && fileExists) {
sourceFile = project.addSourceFileAtPath(outputFilePath);
}
else {
sourceFile = project.createSourceFile(outputFilePath, '', {
overwrite: writeMode === 'overwrite',
});
}
const method = endpoint.method.toLowerCase();
const hasBody = (0, client_constructors_1.determineHasBody)(method);
const requestSchema = endpoint.requestSchema;
const responseSchema = endpoint.responseSchema;
const pathParams = endpoint.pathParams ?? [];
const urlPath = (0, client_constructors_1.constructUrlPath)(endpoint);
// Imports
(0, client_constructors_1.addClientRequiredImports)(sourceFile, outputFilePath, interfaceInputDir, requestSchema, responseSchema, hasBody);
if (useRetry) {
const helperModule = `./${fileName}.requestWithRetry`;
const wrapperSymbol = (0, retry_constructors_1.buildRetryWrapperName)(functionName);
// add import if missing
const existing = sourceFile.getImportDeclarations().find(d => d.getModuleSpecifierValue() === helperModule);
if (!existing) {
sourceFile.addImportDeclaration({
namedImports: [{ name: wrapperSymbol }],
moduleSpecifier: helperModule,
});
}
else if (!existing.getNamedImports().some(n => n.getName() === wrapperSymbol)) {
existing.addNamedImports([{ name: wrapperSymbol }]);
}
}
// Function parameters
const parameters = [
...pathParams.map((param) => ({ name: param, type: 'string' })),
...(hasBody && requestSchema ? [{ name: 'body', type: requestSchema }] : []),
{ name: 'headers', hasQuestionToken: true, type: 'Record<string, string>' },
];
if (writeMode === 'append' &&
sourceFile.getFunction(functionName)) {
logger_1.Logger.info(funcName, `Function "${functionName}" already exists in ${fileName}.ts — skipping.`);
return;
}
// Function
sourceFile.addFunction({
isExported: true,
name: functionName,
parameters,
returnType: `Promise<${responseSchema}>`,
isAsync: true,
statements: useRetry ? `
const authHeaders = ${(0, client_constructors_1.generateInlineAuthHeader)(config.authType, config.credentials)};
const response = await ${(0, retry_constructors_1.buildRetryWrapperName)(functionName)}(
() => axios.${method}(
\`${baseUrl}${urlPath}\`,
${hasBody ? 'body,' : ''}
{
headers: {
...authHeaders,
...(headers ?? {}),
},
} as AxiosRequestConfig
),
{
enabled: true,
maxAttempts: ${config?.retry?.maxAttempts ?? 3},
initialDelayMs: ${config?.retry?.initialDelayMs ?? 250},
multiplier: ${config?.retry?.multiplier ?? 2.0},
method: "${endpoint.method.toUpperCase()}"
}
);
return response.data;
` : `
const authHeaders = ${(0, client_constructors_1.generateInlineAuthHeader)(config.authType, config.credentials)};
const response = await axios.${method}(
\`${baseUrl}${urlPath}\`,
${hasBody ? 'body,' : ''}
{
headers: {
...authHeaders,
...(headers ?? {}),
},
} as AxiosRequestConfig
);
return response.data;
`,
});
// Save to disk
sourceFile.saveSync();
}
/**
* Generates a grouped API client file from a client endpoint config file.
*
* Assumes each endpoint includes a `modelName` (e.g., "person") used for
* determining both the function name and output file name.
*
* @param configPath - Path to the EndpointClientConfigFile JSON
* @param interfacesDir - Path to where the interfaces are stored
* @param outputDir - Output directory
*/
async function generateApiClientFromFile(configPath, interfacesDir, outputDir) {
const funcName = 'generateApiClientFromFile';
const config = (0, file_system_1.readEndpointClientConfigFile)(configPath);
if (!config) {
return;
}
const metasByFile = new Map();
for (const endpoint of config.endpoints) {
const { objectName } = endpoint;
if (!objectName) {
logger_1.Logger.warn(funcName, 'Missing modelName in endpoint:', endpoint);
continue;
}
const { functionName, fileName } = (0, client_constructors_1.generateClientAction)(endpoint);
// Build EndpointMeta for potential retry helper generation
const responseType = endpoint.responseSchema;
// Compute module specifier RELATIVE TO the API output directory,
// pointing to the actual interface file path, then strip ".ts".
const responseFile = path_1.default.join(interfacesDir, `${responseType}.ts`);
let responseModule = path_1.default.relative(outputDir, responseFile).replace(/\\/g, '/');
if (!responseModule.startsWith('.')) {
responseModule = './' + responseModule;
}
responseModule = responseModule.replace(/\.ts$/, '');
const list = metasByFile.get(fileName) ?? [];
list.push({ functionName, responseType, responseModule });
metasByFile.set(fileName, list);
generateApiClientFunction(config.baseUrl, fileName, functionName, endpoint, {
authType: config.authType,
credentials: config.credentials,
retry: config.retry, // surfaced for useRetry
}, interfacesDir, outputDir, 'append');
}
if (config.retry?.enabled) {
for (const [fileBaseName, endpoints] of metasByFile.entries()) {
(0, generate_retry_helper_1.generateRetryHelperForApiFile)(outputDir, fileBaseName, endpoints, /* overwrite */ true);
}
}
}
/**
* Takes in a config directory, a directory of interfaces, and output directories and scaffolds out
* all API clients based on the config and interfaces available
* @param configDir
* @param interfacesRootDir
* @param outputRootDir
*/
async function generateApiClientsFromPath(configDir, interfacesRootDir, outputRootDir) {
const funcName = 'generateApiClientsFromPath';
logger_1.Logger.debug(funcName, 'Starting API client generation from config and interface directories...');
const { configFiles, interfaceNameToDirs } = (0, file_system_1.extractInterfaces)(configDir, interfacesRootDir);
for (const configPath of configFiles) {
const config = (0, file_system_1.readEndpointClientConfigFile)(configPath);
if (!config) {
continue;
}
// Collect all unique schemas used in this config's endpoints
const requiredSchemas = (0, client_constructors_1.collectRequiredSchemas)(config.endpoints);
// Find a directory that contains all required schemas
const foundDir = (0, client_constructors_1.assertDirectoryContainingAllSchemas)(requiredSchemas, interfaceNameToDirs, configPath);
if (!foundDir) {
logger_1.Logger.warn(funcName, `Could not find a directory containing all schemas for config: ${configPath}`);
continue;
}
// Compute relative path of foundDir to interfacesRootDir to preserve structure in outputRootDir
const relativeInterfaceDir = path_1.default.relative(interfacesRootDir, foundDir);
const outputDir = path_1.default.join(outputRootDir, relativeInterfaceDir);
(0, file_system_1.ensureDir)(outputDir);
await generateApiClientFromFile(configPath, foundDir, outputDir);
}
logger_1.Logger.info(funcName, 'API client generation completed.');
}