UNPKG

fastify-openapi-connector-generator

Version:

Complimentary CLI tool for fastify-openapi-connector package. It generates prefabricates for handlers from OpenAPI specification.

262 lines (257 loc) 9.37 kB
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { glob } from 'glob'; import camelCase from 'just-camel-case'; /** * Function to generate handler files * @param imp operationId * @param typesPath path to types file */ export const routeTemplateTyped = (imp, operationId, typesPath) => `import type { TypedHandler, TypedResponseAsync } from '${typesPath}'; export const ${imp}: TypedHandler<'${operationId}'> = async (req, reply): TypedResponseAsync<'${operationId}'> => { return reply.code(501).send("Not Implemented"); } `; /** * Function to generate untyped handler files * @param imp operationId * @param typesPath not used */ export const routeTemplateUntyped = (imp, _operationId, _typesPath) => `import type { FastifyReply, FastifyRequest } from 'fastify'; export const ${imp} = (req: FastifyRequest, reply: FastifyReply): any => { return reply.code(501).send("Not Implemented"); } `; /** * Function to generate security files * @param name security name */ export const securityTemplate = (name) => `import type { FastifyRequest } from 'fastify'; export const ${name} = (req: FastifyRequest, scopes?: string[]): boolean | Promise<boolean> => { return false; } `; /** * Function to generate handler files * @param args setup arguments * @returns operationIds */ export const parseAndGenerateOperationHandlers = async (args) => { const operationIds = []; for (const pathObj of Object.values(args.paths)) { if (typeof pathObj !== 'object' || pathObj === null) { continue; } for (const operationObj of Object.values(pathObj)) { if (typeof operationObj !== 'object' || operationObj === null) { continue; } const operationId = operationObj.operationId; if (!operationId) { continue; } operationIds.push(operationId); } } await generateHandlerFiles({ handlerNames: operationIds, path: args.filesPath, typesPath: args.typesPath.replace(/.ts$/, '.js').replace(/^(?!\.\.?\/)/, './'), templateFunction: args.typed ? routeTemplateTyped : routeTemplateUntyped, }); return operationIds; }; /** * Function to generate security files * @param args setup arguments * @returns security hander names */ export const parseAndGenerateSecurity = async (args) => { const securityNames = []; for (const security of Object.keys(args.security)) { if (security.startsWith('x-')) { continue; } securityNames.push(security); } await generateHandlerFiles({ handlerNames: securityNames, path: args.filesPath, typesPath: '', templateFunction: securityTemplate, }); return securityNames; }; /** * Entry point function that generates the service and handler files * @param args Setup arguments */ export const generate = async (args) => { let pathOperationIds; let webhookOperationIds; let securityNames; if (args.spec.paths && args.routesPath) { pathOperationIds = await parseAndGenerateOperationHandlers({ filesPath: args.routesPath, typesPath: args.typesPath, paths: args.spec.paths, typed: args.typed, }); } if (args.spec.webhooks && args.webhooksPath) { webhookOperationIds = await parseAndGenerateOperationHandlers({ filesPath: args.webhooksPath, typesPath: args.typesPath, paths: args.spec.webhooks, typed: args.typed, }); } if (args.spec.components?.securitySchemes && args.securityPath) { securityNames = await parseAndGenerateSecurity({ filesPath: args.securityPath, security: args.spec.components.securitySchemes, }); } generateTypesFile(args.typesPath, args.schemaFilePath, args.overrideTypesFile); generateServiceFile({ servicePath: args.servicePath, pathHandlerNames: pathOperationIds, webhookHandlerNames: webhookOperationIds, securityHandlerNames: securityNames, routesPath: args.routesPath, webhooksPath: args.webhooksPath, securityPath: args.securityPath, }); console.log('DONE!'); }; /** * Function to generate handler files * @param args setup arguments */ export const generateHandlerFiles = async (args) => { if (!existsSync(args.path)) { mkdirSync(args.path, { recursive: true }); } const files = (await glob(path.resolve(args.path, '*.ts'), { windowsPathsNoEscape: true, })).map((file) => path.basename(file, '.ts')); // All operation ids must have an implementation const missingImplementations = args.handlerNames.filter((name) => !files.includes(name)); if (missingImplementations.length) { console.log('These operations are missing an implementation:'); console.log(missingImplementations); const relative = path.relative(path.resolve(args.path), path.resolve(args.typesPath)).replace(/\\/g, '/'); for (const operationId of missingImplementations) { writeFileSync(path.join(args.path, `${operationId}.ts`), args.templateFunction(camelCase(operationId), operationId, relative)); } } }; /** * Function to generate handler imports * @param args setup arguments * @returns handler imports */ export const generateHandlerImports = (args) => { return args.handlerNames.map((name) => { const handlerPath = path.relative(path.resolve(args.servicePath, '..'), path.resolve(args.path)); const importPath = path .join(handlerPath, `${name}.js'`) .replace(/\\/g, '/') .replace(/^(?!\.\.?\/)/, './'); return `import { ${camelCase(name)} } from '${importPath};`; }); }; /** * Sort handlers function, sorting based on path * @param a * @param b * @returns -1/0/1 */ export const handlersSort = (a, b) => { if (!a.path) { return -1; } if (!b.path) { return 1; } return a.path > b.path ? 1 : b.path > a.path ? -1 : 0; }; /** * Generets types file * @param typesFilePath Path where to generate * @param schemaPath Path to schema file * @param overrideTypesFile Indicates that types file should be overrided if exists * @returns */ export const generateTypesFile = (typesFilePath, schemaPath, overrideTypesFile) => { if (existsSync(typesFilePath) && !overrideTypesFile) { console.log('Types file already exists. Skipping generation.'); return; } const relative = schemaPath.startsWith('@') ? schemaPath : path .relative(path.resolve(typesFilePath, '..'), path.resolve(schemaPath.replace(/.ts$/, '.js'))) .replace(/\\/g, '/') .replace(/^(?!\.\.?\/)/, './'); const content = `import type { TypedRequestBase, TypedHandlerBase, TypedResponseBase, TypedResponseBaseSync, TypedResponseBaseAsync} from 'fastify-openapi-connector'; import type { operations } from '${relative}'; export type TypedRequest<T extends keyof operations> = TypedRequestBase<operations, T>; export type TypedResponse<T extends keyof operations> = TypedResponseBase<operations, T>; export type TypedResponseSync<T extends keyof operations> = TypedResponseBaseSync<operations, T>; export type TypedResponseAsync<T extends keyof operations> = TypedResponseBaseAsync<operations, T>; export type TypedHandler<T extends keyof operations> = TypedHandlerBase<operations, T>; `; writeFileSync(typesFilePath, content); }; /** * Function to generate service file * @param args setup arguments */ export const generateServiceFile = (args) => { let serviceTs = '// THIS FILE IS AUTO GENERATED - DO NOT MANUALLY ALTER!!\n'; const organizedHandlers = [ { path: args.routesPath, handlers: args.pathHandlerNames, exportName: 'pathHandlers', typeName: 'OperationHandlers', }, { path: args.webhooksPath, handlers: args.webhookHandlerNames, exportName: 'webhookHandlers', typeName: 'OperationHandlers', }, { path: args.securityPath, handlers: args.securityHandlerNames, exportName: 'securityHandlers', typeName: 'SecurityHandlers', }, ].sort(handlersSort); let imports = []; const typeImports = new Set(); const exports = []; for (const { path, handlers, exportName, typeName } of organizedHandlers) { if (!path || !handlers) { continue; } imports = imports.concat(generateHandlerImports({ handlerNames: handlers, path: path, servicePath: args.servicePath, })); typeImports.add(typeName); exports.push(`export const ${exportName}: ${typeName} = { ${handlers.map((name) => ` '${name}': ${camelCase(name)}`).join(',\n')}, };`); } if (typeImports.size > 0) { imports.unshift(`import type { ${Array.from(typeImports).join(', ')} } from 'fastify-openapi-connector';`); } serviceTs += `${imports.join('\n')}\n\n`; serviceTs += exports.join('\n\n'); serviceTs += '\n'; writeFileSync(args.servicePath, serviceTs); };