123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- import express, {Express} from 'express';
- import { Middleware, Route, HttpMethod, HttpHandler } from '../base/http';
- import { RouteNotSetException } from '../base/exceptions';
- import { IncorrectMethodException } from '../base/exceptions';
- import { Logger, Message, MessageTypes } from '../base/logger';
- import { ServerProperties } from './ServerProperties';
- import { i18nLoader, $$ } from '../base/i18n';
- import path from 'path';
- import fs from 'fs';
- import http from 'http';
- import { Server as WebSocketServer, WebSocket as WS } from 'ws';
- import { InvalidMiddlewareException } from '../base/exceptions';
- import { InvalidRouteException } from '../base/exceptions';
- import { ServerNotInitializedException } from '../base/exceptions';
- import { WebSocketHandler } from '../base/websocket';
- import swaggerJsDoc, { OAS3Options, Options, SecurityRequirement } from 'swagger-jsdoc';
- import swaggerUi from 'swagger-ui-express';
- /** @sealed */
- class Server {
- private instance: Express;
- private httpServer: http.Server;
- private readonly port: number;
- private readonly host: string;
- private readonly logger: Logger;
- private i18n: i18nLoader;
- public static readonly i18nDefaultPath = '../resources/i18n.json';
- public static readonly defaultMiddlewaresPath = '../middlewares';
- public static readonly defaultRoutesPath = '../routes';
- private readonly httpHandlers: {[key: string]: HttpHandler};
- private readonly wsHandlers: {[url: string]: WebSocketHandler};
- private readonly wsServers: {[url: string]: WebSocketServer};
- private initialized: boolean;
- private readonly i18nPath?: string;
- private readonly middlewaresPath?: string;
- private readonly routesPath?: string;
- private readonly viewEngine?: string;
- private readonly viewsPath?: string;
- private readonly options?: {[key: string]: any};
- private readonly swaggerComponents? : object;
- private readonly swaggerSecurity? : object;
- private readonly swaggerDocsPath? : string;
- private readonly swaggerTitle?: string;
- private readonly swaggerDescription?: string;
- private readonly swaggerApiVersion?: string;
- private readonly swaggerRoute?: string;
- public constructor(properties: ServerProperties) {
- this.instance = express();
- this.httpServer = http.createServer(this.instance);
- this.port = properties.port;
- this.host = properties.host || `http://localhost:${this.port}`;
- this.i18n = i18nLoader.getInstance();
- if(properties.locale)
- this.i18n.setLocale(properties.locale);
- this.logger = new Logger();
- this.httpHandlers = {};
- this.wsHandlers = properties.wsHandlers || {};
- this.wsServers = {};
- this.i18nPath = properties.i18nPath;
- this.middlewaresPath = properties.middlewaresPath;
- this.routesPath = properties.routesPath;
- this.viewEngine = properties.viewEngine;
- this.viewsPath = properties.viewsPath;
- this.options = properties.options;
- if(properties.swagger) {
- this.swaggerDocsPath = properties.swagger?.docsPath;
- this.swaggerTitle = properties.swagger?.title || 'API Documentation';
- this.swaggerDescription = properties.swagger?.description || 'API Documentation';
- this.swaggerApiVersion = properties.swagger?.version ||'1.0.0';
- this.swaggerRoute = properties.swagger?.route || '/api-docs';
- this.swaggerComponents = properties.swagger?.components;
- this.swaggerSecurity = properties.swagger?.security;
- }
- this.initialized = false;
- }
- public async init(): Promise<Server> {
- if(this.viewEngine)
- this.instance.set('view engine', this.viewEngine);
- if(this.viewsPath)
- this.instance.set('views', this.viewsPath);
- this.i18n.load(path.resolve(__dirname, Server.i18nDefaultPath));
- await this.registerMiddlewares(path.resolve(__dirname, Server.defaultMiddlewaresPath));
- await this.registerRoutes(path.resolve(__dirname, Server.defaultRoutesPath));
- await this.postInit();
- this.initialized = true;
- return this;
- }
- private async postInit(): Promise<void> {
- if(this.i18nPath)
- this.i18n.load(this.i18nPath);
- if(this.middlewaresPath)
- await this.registerMiddlewares(this.middlewaresPath);
- if(this.routesPath)
- await this.registerRoutes(this.routesPath);
- if(this.swaggerDocsPath)
- fs.writeFileSync(this.swaggerDocsPath, '');
- if(Object.keys(this.wsHandlers).length > 0) {
- this.registerWsServers();
- this.applyWsHandlers();
- }
- }
- private processHttpHandlers(): void {
- const handlers: HttpHandler[] = [];
- for(const key in this.httpHandlers)
- handlers.push(this.httpHandlers[key]);
- handlers.sort((a, b) => a.getOrder() - b.getOrder());
- for(const handler of handlers) {
- if(handler instanceof Middleware)
- this.addMiddleware(handler);
- else if (handler instanceof Route)
- this.addRoute(handler);
- }
- }
- public addMiddleware(middleware: Middleware): Server {
- if(middleware.getRoute() != null)
- this.instance.use(<string>middleware.getRoute(), middleware.getAction());
- else
- this.instance.use(middleware.getAction());
- return this;
- }
- public addRoute(route: Route): Server {
- if(route.getRoute() == null)
- throw new RouteNotSetException();
- switch(route.getMethod()) {
- case HttpMethod.GET:
- return this.get(route);
- case HttpMethod.POST:
- return this.post(route);
- default:
- throw new IncorrectMethodException();
- }
- }
- private registerRoutesDocumentation(): Server {
- for(const routeName in this.httpHandlers) {
- const route = this.httpHandlers[routeName];
- if(!(route instanceof Route))
- continue;
- if(route.getRoute() == null)
- throw new RouteNotSetException();
-
- if(![HttpMethod.GET, HttpMethod.POST].includes(route.getMethod()))
- throw new IncorrectMethodException();
-
- const docs = route.getDocumentation();
-
- if(this.swaggerDocsPath && docs.length > 0) {
- fs.appendFileSync(this.swaggerDocsPath, `${docs}\n`);
- this.logInfo(`Swagger documentation for route '${route.getRoute()}' generated!`);
- }
- }
- return this;
- }
- public registerWsServers(): Server {
- for(const url of Object.keys(this.wsHandlers)) {
- const wsServer = this.wsServers[url] = new WebSocketServer({ noServer: true });
- const wsHandler = this.wsHandlers[url];
- wsServer.on(WebSocketHandler.Event.CONNECTION, (ws: WS) => {
- wsHandler.onConnect(ws);
- ws.on(WebSocketHandler.Event.MESSAGE, wsHandler.onMessage);
- ws.on(WebSocketHandler.Event.ERROR, wsHandler.onError);
- ws.on(WebSocketHandler.Event.CLOSE, wsHandler.onClose);
- });
- }
- return this;
- }
- public applyWsHandlers(): Server {
- this.httpServer.on('upgrade', (request, socket, head) => {
- const url = request.url?.split('?')[0];
- if(url && this.wsHandlers[url] && this.wsServers[url]) {
- const wsServer = this.wsServers[url];
- wsServer.handleUpgrade(request, socket, head, (ws) => {
- wsServer.emit(WebSocketHandler.Event.CONNECTION, ws, request);
- });
- } else {
- socket.destroy();
- }
- });
- return this;
- }
- public getWsConnections(url: string): Set<WS> | null {
- const wsServer = this.wsServers[url];
- return wsServer ? wsServer.clients : null;
- }
- public logInfo(message: string): void {
- this.logger.getLogger().info(message);
- }
- public logError(message: string): void {
- this.logger.getLogger().error(message);
- }
- public logWarn(message: string): void {
- this.logger.getLogger().warn(message);
- }
- public log(message: Message): void {
- switch(message.type) {
- case MessageTypes.WARNING:
- return this.logWarn(message.text);
- case MessageTypes.ERROR:
- return this.logError(message.text);
- default:
- return this.logInfo(message.text);
- }
- }
- private get(route: Route): Server {
- this.instance.get(<string>route.getRoute(), route.getAction());
- return this;
- }
- private post(route: Route): Server {
- this.instance.post(<string>route.getRoute(), route.getAction());
- return this;
- }
- private registerSwaggerMiddleware(): Server {
- if(!this.swaggerDocsPath)
- return this;
- const swaggerOptions : OAS3Options = {
- swaggerDefinition: {
- openapi: '3.0.1',
- info: {
- title: this.swaggerTitle!,
- version: this.swaggerApiVersion!,
- description: this.swaggerDescription!,
- },
- servers: [
- { url: this.host }
- ],
- },
- apis: [this.swaggerDocsPath],
- };
- if(this.swaggerComponents)
- swaggerOptions.swaggerDefinition!.components = this.swaggerComponents;
- if(this.swaggerSecurity)
- swaggerOptions.swaggerDefinition!.security = this.swaggerSecurity as SecurityRequirement[];
- const swaggerDocs = swaggerJsDoc(swaggerOptions as Options);
- this.instance.use(this.swaggerRoute!, swaggerUi.serve, swaggerUi.setup(swaggerDocs));
- return this;
- }
- public async registerRoutes(dir: string): Promise<Server> {
- const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' });
- for(const file of files) {
- if(/\.js$/.test(file)) {
- const {default: RouteClass} = await import(path.join(dir, file));
- if(RouteClass.prototype instanceof Route) {
- this.httpHandlers[RouteClass.name] = <HttpHandler>new RouteClass(this);
- } else throw new InvalidRouteException(file);
- }
- }
- return this;
- }
- public async registerMiddlewares(dir: string): Promise<Server> {
- const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' });
- for(const file of files) {
- if(/\.js$/.test(file)) {
- const {default: MiddlewareClass} = await import(path.join(dir, file));
- if(MiddlewareClass.prototype instanceof Middleware) {
- this.httpHandlers[MiddlewareClass.name] = <HttpHandler>new MiddlewareClass(this);
- } else throw new InvalidMiddlewareException(file);
- }
- }
- return this;
- }
- public getLogger(): Logger {
- return this.logger;
- }
- public i18nLoad(path: string): Server {
- this.i18n.load(path);
- return this;
- }
- public getHost(): string {
- return this.host;
- }
- public getOption(key: string): any {
- return this.options ? this.options[key] || null : null;
- }
- public start(callback?: () => any): void {
- if(!this.initialized)
- throw new ServerNotInitializedException();
- this.registerRoutesDocumentation();
- this.registerSwaggerMiddleware();
- this.processHttpHandlers();
- const cb = (): void => {
- this.logInfo($$('org.crazydoctor.expressts.start', { 'port': this.port }));
- if(callback) callback();
- };
- this.httpServer.listen(this.port, cb);
- }
- }
- export { Server };
|