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
JavaScript
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);
};