/* eslint-disable no-template-curly-in-string */ /* eslint-disable @typescript-eslint/no-unused-expressions */ import * as fs from 'fs'; import * as path from 'path'; import { stringify } from 'qs'; // eslint-disable-next-line import/no-extraneous-dependencies import * as morph from 'ts-morph'; import { API_DIR as API_DIR_CONST, GENERATOR_ENTITY_ALLIAS, } from '../../consts'; import { toCamel, capitalize, schemaParamParser } from './utils'; const API_DIR = path.resolve(API_DIR_CONST); if (!fs.existsSync(API_DIR)) { fs.mkdirSync(API_DIR); } const { Project, QuoteKind } = morph; class ApiGenerator { project = new Project({ tsConfigFilePath: './tsconfig.json', addFilesFromTsConfig: false, manipulationSettings: { quoteKind: QuoteKind.Single, usePrefixAndSuffixTextForRename: false, useTrailingCommas: true, }, }); openapi: Record; serverUrl: string; paths: any; /* interface Controllers { [controller: string]: { [operationId: string]: { parameters - from opneApi, responses - from opneApi, method } } } */ controllers: Record = {}; apis: morph.SourceFile[] = []; constructor(openapi: Record) { this.openapi = openapi; this.paths = openapi.paths; this.serverUrl = openapi.servers[0].url; Object.keys(this.paths).forEach((pathKey) => { Object.keys(this.paths[pathKey]).forEach((method) => { const { tags, operationId, parameters, responses, requestBody, security, } = this.paths[pathKey][method]; const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', '')); if (this.controllers[controller]) { this.controllers[controller][operationId] = { parameters, responses, method, requestBody, security, pathKey: pathKey.replace(/{/g, '${'), }; } else { this.controllers[controller] = { [operationId]: { parameters, responses, method, requestBody, security, pathKey: pathKey.replace(/{/g, '${'), } }; } }); }); this.generateApiFiles(); } generateApiFiles = () => { Object.keys(this.controllers).forEach(this.generateApiFile); }; generateApiFile = (cName: string) => { const apiFile = this.project.createSourceFile(`${API_DIR}/${cName}.ts`); apiFile.addStatements([ '// This file was autogenerated. Please do not change.', '// All changes will be overwrited on commit.', '', ]); // const schemaProperties = schemas[schemaName].properties; const importEntities: any[] = []; // add api class to file const apiClass = apiFile.addClass({ name: `${capitalize(cName)}Api`, isDefaultExport: true, }); // get operations of controller const controllerOperations = this.controllers[cName]; const operationList = Object.keys(controllerOperations).sort(); // for each operation add fetcher operationList.forEach((operation) => { const { requestBody, responses, parameters, method, pathKey, security, } = controllerOperations[operation]; const queryParams: any[] = []; // { name, type } const bodyParam: any[] = []; // { name, type } let hasResponseBodyType: /* boolean | ReturnType */ false | [string, boolean, boolean, boolean, boolean] = false; let contentType = ''; if (parameters) { parameters.forEach((p: any) => { const [ pType, isArray, isClass, isImport, ] = schemaParamParser(p.schema, this.openapi); if (isImport) { importEntities.push({ type: pType, isClass }); } if (p.in === 'query') { queryParams.push({ name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required }); } }); } if (queryParams.length > 0) { const imp = apiFile.getImportDeclaration((i) => { return i.getModuleSpecifierValue() === 'qs'; }); if (!imp) { apiFile.addImportDeclaration({ moduleSpecifier: 'qs', defaultImport: 'qs', }); } } if (requestBody) { let content = requestBody.content; const { $ref }: { $ref: string } = requestBody; if (!content && $ref) { const name = $ref.split('/').pop() as string; content = this.openapi.components.requestBodies[name].content; } [contentType] = Object.keys(content); const data = content[contentType]; const [ pType, isArray, isClass, isImport, ] = schemaParamParser(data.schema, this.openapi); if (isImport) { importEntities.push({ type: pType, isClass }); bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType }); } else { bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` }); } } if (responses['200']) { const { content, headers } = responses['200']; if (content && (content['*/*'] || content['application/json'])) { const { schema, examples } = content['*/*'] || content['application/json']; if (!schema) { process.exit(0); } const propType = schemaParamParser(schema, this.openapi); const [pType, , isClass, isImport] = propType; if (isImport) { importEntities.push({ type: pType, isClass }); } hasResponseBodyType = propType; } } let returnType = ''; if (hasResponseBodyType) { const [pType, isArray, isClass] = hasResponseBodyType as any; let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`; returnType = data; } else { returnType = 'Promise b.isClass); if (shouldValidate.length > 0) { returnType += ' | string[]'; } // append Error to default type return; returnType += ' | Error>'; const fetcher = apiClass.addMethod({ isAsync: true, isStatic: true, name: operation, returnType, }); const params = [...queryParams, ...bodyParam].sort((a, b) => (Number(!!a.hasQuestionToken) - Number(!!b.hasQuestionToken))); fetcher.addParameters(params); fetcher.setBodyText((w) => { // Add data to URLSearchParams if (contentType === 'text/plain') { bodyParam.forEach((b) => { w.writeLine(`const params = String(${b.name});`); }); } else { if (shouldValidate.length > 0) { w.writeLine(`const haveError: string[] = [];`); shouldValidate.forEach((b) => { w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`); w.writeLine(`haveError.push(...${b.name}Valid.validate());`); }); w.writeLine(`if (haveError.length > 0) {`); w.writeLine(` return Promise.resolve(haveError);`) w.writeLine(`}`); } } // Switch return of fetch in case on queryParams if (queryParams.length > 0) { w.writeLine('const queryParams = {'); queryParams.forEach((q) => { w.writeLine(` ${q.name}: ${q.name},`); }); w.writeLine('}'); w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}?\${qs.stringify(queryParams, { arrayFormat: 'comma' })}\`, {`); } else { w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}\`, {`); } // Add method w.writeLine(` method: '${method.toUpperCase()}',`); // add Fetch options if (contentType && contentType !== 'multipart/form-data') { w.writeLine(' headers: {'); w.writeLine(` 'Content-Type': '${contentType}',`); w.writeLine(' },'); } if (contentType) { switch (contentType) { case 'text/plain': w.writeLine(' body: params,'); break; default: w.writeLine(` body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`); break; } } // Handle response if (hasResponseBodyType) { w.writeLine('}).then(async (res) => {'); w.writeLine(' if (res.status === 200) {'); w.writeLine(' return res.json();'); } else { w.writeLine('}).then(async (res) => {'); w.writeLine(' if (res.status === 200) {'); w.writeLine(' return res.status;'); } // Handle Error w.writeLine(' } else {'); w.writeLine(' return new Error(String(res.status));'); w.writeLine(' }'); w.writeLine('})'); }); }); const imports: any[] = []; const types: string[] = []; importEntities.forEach((i) => { const { type } = i; if (!types.includes(type)) { imports.push(i); types.push(type); } }); imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => { const { type: pType, isClass } = ie; if (isClass) { apiFile.addImportDeclaration({ moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`, defaultImport: pType, namedImports: [`I${pType}`], }); } else { apiFile.addImportDeclaration({ moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`, namedImports: [pType], }); } }); this.apis.push(apiFile); }; save = () => { this.apis.forEach(async (e) => { await e.saveSync(); }); }; } export default ApiGenerator;