@sleekify/sleekify-fastify
Version:
A TypeScript decorator driven approach for developing Fastify web applications.
352 lines (351 loc) • 13.9 kB
JavaScript
"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;