UNPKG

@sleekify/sleekify-fastify

Version:

A TypeScript decorator driven approach for developing Fastify web applications.

352 lines (351 loc) 13.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Sleekify = void 0; const fastify_plugin_1 = require("fastify-plugin"); const sleekify_1 = require("@sleekify/sleekify"); const tsimportlib_1 = require("tsimportlib"); const lodash_1 = __importDefault(require("lodash")); const methodMapArray = [ { decorator: sleekify_1.DELETE, key: 'delete' }, { decorator: sleekify_1.GET, key: 'get' }, { decorator: sleekify_1.HEAD, key: 'head' }, { decorator: sleekify_1.OPTIONS, key: 'options' }, { decorator: sleekify_1.PATCH, key: 'patch' }, { decorator: sleekify_1.POST, key: 'post' }, { decorator: sleekify_1.PUT, key: 'put' }, { decorator: sleekify_1.TRACE, key: 'trace' } ]; /** * This class provides the Sleekify integration for Fastify. */ class Sleekify { classArray; specification; serviceHandlers; plugin; /** * Creates a new Sleekify instance. * * @param specification The OpenAPI base specification * @param classArray The resource classes to include in the OpenAPI specification */ constructor(specification, classArray) { this.classArray = classArray; this.specification = JSON.parse(JSON.stringify(specification)); } /** * Get a Fastify plugin which you can register. */ getPlugin() { this.initialize(); if (this.plugin !== undefined) { return this.plugin; } const plugin = async (fastify, options) => { /* This is an ES module so we must import it dynamically */ const { fastifyOpenapiGlue } = (await (0, tsimportlib_1.dynamicImport)('fastify-openapi-glue', module)); await fastifyOpenapiGlue(fastify, { ...options, specification: this.getSpecification(), serviceHandlers: this.serviceHandlers }); }; this.plugin = (0, fastify_plugin_1.fastifyPlugin)(plugin, { fastify: '>=4.0.0', name: '@sleekify/sleekify-fastify' }); return this.plugin; } /** * Get the OpenAPI specification which may be passed to Fastify OpenAPI glue. * @param options The options * @param options.pretty Sort the specification path's etc. */ getSpecification(options) { this.initialize(); const result = JSON.parse(JSON.stringify(this.specification)); if (options?.pretty === true) { this.sortObject(result, 'paths'); this.sortObject(result, 'webhooks'); this.sortObject(result, 'components'); const components = [ 'callbacks', 'examples', 'links', 'parameters', 'pathItems', 'requestBodies', 'responses', 'schemas', 'securitySchemes' ]; for (const key of components) { this.sortObject(result, `components.${key}`); } this.sortOperationObjects(result); if (result.servers !== undefined) { result.servers.sort((a, b) => a.url.localeCompare(b.url)); } if (result.tags !== undefined) { result.tags.sort((a, b) => a.name.localeCompare(b.name)); } } return result; } /** * Get the service handlers which may be passed to Fastify OpenAPI glue. */ getServiceHandlers() { this.initialize(); return this.serviceHandlers; } initialize() { if (this.serviceHandlers !== undefined) { /* already initialized */ return; } this.serviceHandlers = {}; for (const clazz of this.classArray) { const Clazz = clazz; const classInstance = new Clazz(); const componentsObject = sleekify_1.Annotation.get(clazz, undefined, sleekify_1.Components); const pathItemObject = sleekify_1.Annotation.get(clazz, undefined, sleekify_1.Path); if (componentsObject !== undefined) { this.addComponents(clazz, componentsObject); } if (pathItemObject !== undefined) { this.addServiceHandlers(clazz, classInstance, pathItemObject); } } } addComponents(clazz, componentsObject) { if (this.specification.components === undefined) { this.specification.components = {}; } const propertyNameArray = [ 'schemas', 'responses', 'parameters', 'examples', 'requestBodies', 'headers', 'securitySchemes', 'links', 'callbacks', 'pathItems' ]; for (const propertyName of propertyNameArray) { if (componentsObject[propertyName] === undefined) { continue; } let componentsSection = this.specification.components[propertyName]; if (componentsSection === undefined) { componentsSection = this.specification.components[propertyName] = {}; } Object.assign(componentsSection, componentsObject[propertyName]); } } addPath(path, pathItemObject, operationMap) { if (lodash_1.default.isEmpty(operationMap)) { return; } if (this.specification.paths === undefined) { this.specification.paths = {}; } if (this.specification.paths[path] === undefined) { this.specification.paths[path] = { ...lodash_1.default.omit(pathItemObject, 'path'), ...operationMap }; } } addRequestBody(operationObject, resolvedConsumes, resolvedSchema) { if (operationObject.requestBody === undefined) { operationObject.requestBody = { content: resolvedConsumes.reduce((content, mediaType) => { content[mediaType] = { schema: resolvedSchema }; return content; }, {}) }; } const requestBody = operationObject.requestBody; if (lodash_1.default.isEmpty(requestBody) || (requestBody.content !== undefined && lodash_1.default.isEmpty(requestBody.content) && Object.keys(requestBody).length === 1)) { delete operationObject.requestBody; } } addResponses(operationObject, resolvedProduces, resolvedSchema, resolvedStatusCodes) { if (resolvedStatusCodes.length === 0) { return; } if (operationObject.responses === undefined) { operationObject.responses = {}; } for (const resolvedStatusCode of resolvedStatusCodes) { if (operationObject.responses[resolvedStatusCode] === undefined) { /* add a new response object */ operationObject.responses[resolvedStatusCode] = { description: 'Successful response', content: resolvedProduces.reduce((content, mediaType) => { content[mediaType] = { schema: resolvedSchema }; return content; }, {}) }; } else { const referenceOrResponse = operationObject.responses[resolvedStatusCode]; if (referenceOrResponse.$ref === undefined) { if (referenceOrResponse.content === undefined) { const responseObject = operationObject.responses[resolvedStatusCode]; /* add content to an existing response object */ responseObject.content = resolvedProduces.reduce((content, mediaType) => { content[mediaType] = { schema: resolvedSchema }; return content; }, {}); } if (lodash_1.default.isEmpty(referenceOrResponse.content)) { delete referenceOrResponse.content; } } } } } addSchemas(clazz, propertyName, operationObject, methodKey) { const classConsumes = sleekify_1.Annotation.get(clazz, undefined, sleekify_1.Consumes); const classProduces = sleekify_1.Annotation.get(clazz, undefined, sleekify_1.Produces); const classSchema = sleekify_1.Annotation.get(clazz, undefined, sleekify_1.Schema); const methodConsumes = sleekify_1.Annotation.get(clazz, propertyName, sleekify_1.Consumes); const methodProduces = sleekify_1.Annotation.get(clazz, propertyName, sleekify_1.Produces); const methodSchema = sleekify_1.Annotation.get(clazz, propertyName, sleekify_1.Schema); const resolvedConsumes = methodConsumes ?? classConsumes ?? ['application/json']; const resolvedProduces = methodProduces ?? classProduces ?? ['application/json']; const resolvedSchema = methodSchema ?? classSchema; if (resolvedSchema === undefined) { return; } if (['patch', 'post', 'put'].includes(methodKey)) { this.addRequestBody(operationObject, resolvedConsumes, resolvedSchema); } const resolvedStatusCodes = this.getSuccessStatusCodes(operationObject); this.addResponses(operationObject, resolvedProduces, resolvedSchema, resolvedStatusCodes); } addServiceHandlers(clazz, classInstance, pathItemObject) { const path = pathItemObject.path; const operationMap = {}; const propertyNameMap = {}; for (let prototype = Object.getPrototypeOf(classInstance); prototype !== Object.prototype; prototype = Object.getPrototypeOf(prototype)) { for (const propertyName of Object.getOwnPropertyNames(prototype)) { propertyNameMap[propertyName] = null; } } for (const propertyName in propertyNameMap) { const propertyValue = classInstance[propertyName]; if (['constructor', 'function'].includes(propertyName) || !lodash_1.default.isFunction(propertyValue)) { continue; } for (const methodMap of methodMapArray) { const decorator = methodMap.decorator; if (!sleekify_1.Annotation.exists(clazz, propertyName, decorator)) { continue; } const methodKey = methodMap.key; const operationObject = lodash_1.default.cloneDeep(sleekify_1.Annotation.get(clazz, propertyName, decorator) ?? {}); operationMap[methodKey] = operationObject; if (operationObject.operationId === undefined) { operationObject.operationId = lodash_1.default.camelCase(`${methodKey}${path}`.replace(/\//g, '_').replace(/{/g, '').replace(/}/g, '')); } this.addSchemas(clazz, propertyName, operationObject, methodKey); this.serviceHandlers[operationObject.operationId] = async (request, reply) => { const result = classInstance[propertyName](request, reply); return result instanceof Promise ? await result : result; }; } } this.addPath(path, pathItemObject, operationMap); } getSuccessStatusCodes(operationObject) { const successStatusCodes = []; if (operationObject.responses !== undefined) { for (const responseKey in operationObject.responses) { const statusCode = Number.parseInt(responseKey); if (!Number.isNaN(statusCode) && statusCode >= 200 && statusCode < 300) { successStatusCodes.push(statusCode); } } } const resolvedStatusCodes = []; if (successStatusCodes.length > 0) { for (const statusCode of [200, 201]) { if (successStatusCodes.includes(statusCode)) { resolvedStatusCodes.push(statusCode); } } } else { resolvedStatusCodes.push(200); } return resolvedStatusCodes; } sortObject(root, path) { const object = lodash_1.default.get(root, path); if (object === undefined) { return; } const result = {}; const sortedKeys = Object.keys(object).sort(); for (const key of sortedKeys) { result[key] = object[key]; } lodash_1.default.set(root, path, result); } sortOperationObjects(result) { if (result.paths !== undefined) { for (const path in result.paths) { const pathItemObject = result.paths[path]; for (const methodMap of methodMapArray) { const operationObject = pathItemObject[methodMap.key]; if (operationObject !== undefined) { this.sortObject(operationObject, 'callbacks'); this.sortObject(operationObject, 'responses'); if (operationObject.tags !== undefined) { operationObject.tags.sort(); } } } } } } } exports.Sleekify = Sleekify;