UNPKG

typescript-scaffolder

Version:

![npm version](https://img.shields.io/npm/v/typescript-scaffolder) ### Unit Test Coverage: 97.12%

196 lines (195 loc) 9.12 kB
"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.'); }