import fs from 'fs'; import http from 'http'; import express, { Express } from 'express'; import { Middleware, Route, HttpMethod } from '../base/http'; import { RouteNotSetException } from '../base/exceptions'; import { IncorrectMethodException } from '../base/exceptions'; import { Logger, Message } from '../base/logger'; import { ServerProperties } from './ServerProperties'; import { i18nLoader, $ } from '../base/i18n'; import { WebSocketServer, WebSocket as WS } from 'ws'; import { ServerNotInitializedException } from '../base/exceptions'; import { WebSocketHandler } from '../base/websocket'; import Registry from '../base/registry/Registry'; import SystemRegistry from '../base/registry/SystemRegistry'; import DynamicRegistry from '../base/registry/DynamicRegistry'; /** @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; 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 static registry: Registry; private readonly systemRegistry: SystemRegistry; 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.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.systemRegistry = new SystemRegistry({ json: properties.json || false, urlencoded: properties.urlencoded || false, sessions: properties.sessions || false, swagger: properties.swagger != null }).setServer(this) as SystemRegistry; Server.registry = Server.registry == null ? new DynamicRegistry(this.routesPath, this.middlewaresPath) : Server.registry; Server.registry.setServer(this); this.initialized = false; } private async init(): Promise { if(this.viewEngine) this.instance.set('view engine', this.viewEngine); if(this.viewsPath) this.instance.set('views', this.viewsPath); await this.systemRegistry.registerHttpHandlers(); await this.postInit(); this.initialized = true; return this; } private async postInit(): Promise { if(this.i18nPath) this.i18n.load(this.i18nPath); await Server.registry.registerHttpHandlers(); if(this.swaggerDocsPath) fs.writeFileSync(this.swaggerDocsPath, ''); if(Object.keys(this.wsHandlers).length > 0) { this.registerWsServers(); this.applyWsHandlers(); } } private processHttpHandlers(): void { for(const handler of Registry.getHttpHandlers()) { 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(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 { if(!this.swaggerDocsPath) return this; for(const route of Registry.getHttpHandlers()) { 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(docs.length > 0) { fs.appendFileSync(this.swaggerDocsPath, `${docs}\n`); this.logInfo($('org.crazydoctor.extress.swagger.routeGenerated', { route: route.getRoute() })); } } 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 | null { const wsServer = this.wsServers[url]; return wsServer ? wsServer.clients : null; } public logInfo(message: string): void { this.logger.info(message); } public logError(message: string): void { this.logger.error(message); } public logWarn(message: string): void { this.logger.warn(message); } public log(message: Message): void { switch(message.type) { case 'warn': return this.logWarn(message.text); case 'error': return this.logError(message.text); default: return this.logInfo(message.text); } } private get(route: Route): Server { this.instance.get(route.getRoute(), route.getAction()); return this; } private post(route: Route): Server { this.instance.post(route.getRoute(), route.getAction()); 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 async start(callback?: () => any): Promise { return this.init().then((server) => { if(!this.initialized) throw new ServerNotInitializedException(); this.registerRoutesDocumentation(); this.processHttpHandlers(); const cb = (): void => { this.logInfo($('org.crazydoctor.extress.start', { 'port': this.port })); if(callback) callback(); }; this.httpServer.listen(this.port, cb); return server; }); } } export { Server };