typescript-scaffolder
Version:
 ### Unit Test Coverage: 97.53%
136 lines (135 loc) • 6.27 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");
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();
(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);
// 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: `
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;
}
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);
generateApiClientFunction(config.baseUrl, fileName, functionName, endpoint, {
authType: config.authType,
credentials: config.credentials,
}, interfacesDir, outputDir, 'append');
}
}
/**
* 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.findDirectoryContainingAllSchemas)(requiredSchemas, interfaceNameToDirs, configPath, funcName);
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.');
}