Server.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import express, {Express} from 'express';
  2. import { Middleware, Route, HttpMethod, HttpHandler } from '../base/http';
  3. import { RouteNotSetException } from '../base/exceptions';
  4. import { IncorrectMethodException } from '../base/exceptions';
  5. import { Logger, Message, MessageTypes } from '../base/logger';
  6. import { ServerProperties } from './ServerProperties';
  7. import { i18nLoader, $$ } from '../base/i18n';
  8. import path from 'path';
  9. import fs from 'fs';
  10. import http from 'http';
  11. import { Server as WebSocketServer, WebSocket as WS } from 'ws';
  12. import { InvalidMiddlewareException } from '../base/exceptions';
  13. import { InvalidRouteException } from '../base/exceptions';
  14. import { ServerNotInitializedException } from '../base/exceptions';
  15. import { WebSocketHandler } from '../base/websocket';
  16. import swaggerJsDoc, { OAS3Options, Options, SecurityRequirement } from 'swagger-jsdoc';
  17. import swaggerUi from 'swagger-ui-express';
  18. /** @sealed */
  19. class Server {
  20. private instance: Express;
  21. private httpServer: http.Server;
  22. private readonly port: number;
  23. private readonly host: string;
  24. private readonly logger: Logger;
  25. private i18n: i18nLoader;
  26. public static readonly i18nDefaultPath = '../resources/i18n.json';
  27. public static readonly defaultMiddlewaresPath = '../middlewares';
  28. public static readonly defaultRoutesPath = '../routes';
  29. private readonly httpHandlers: {[key: string]: HttpHandler};
  30. private readonly wsHandlers: {[url: string]: WebSocketHandler};
  31. private readonly wsServers: {[url: string]: WebSocketServer};
  32. private initialized: boolean;
  33. private readonly i18nPath?: string;
  34. private readonly middlewaresPath?: string;
  35. private readonly routesPath?: string;
  36. private readonly viewEngine?: string;
  37. private readonly viewsPath?: string;
  38. private readonly options?: {[key: string]: any};
  39. private readonly swaggerComponents? : object;
  40. private readonly swaggerSecurity? : object;
  41. private readonly swaggerDocsPath? : string;
  42. private readonly swaggerTitle?: string;
  43. private readonly swaggerDescription?: string;
  44. private readonly swaggerApiVersion?: string;
  45. private readonly swaggerRoute?: string;
  46. public constructor(properties: ServerProperties) {
  47. this.instance = express();
  48. this.httpServer = http.createServer(this.instance);
  49. this.port = properties.port;
  50. this.host = properties.host || `http://localhost:${this.port}`;
  51. this.i18n = i18nLoader.getInstance();
  52. if(properties.locale)
  53. this.i18n.setLocale(properties.locale);
  54. this.logger = new Logger();
  55. this.httpHandlers = {};
  56. this.wsHandlers = properties.wsHandlers || {};
  57. this.wsServers = {};
  58. this.i18nPath = properties.i18nPath;
  59. this.middlewaresPath = properties.middlewaresPath;
  60. this.routesPath = properties.routesPath;
  61. this.viewEngine = properties.viewEngine;
  62. this.viewsPath = properties.viewsPath;
  63. this.options = properties.options;
  64. if(properties.swagger) {
  65. this.swaggerDocsPath = properties.swagger?.docsPath;
  66. this.swaggerTitle = properties.swagger?.title || 'API Documentation';
  67. this.swaggerDescription = properties.swagger?.description || 'API Documentation';
  68. this.swaggerApiVersion = properties.swagger?.version ||'1.0.0';
  69. this.swaggerRoute = properties.swagger?.route || '/api-docs';
  70. this.swaggerComponents = properties.swagger?.components;
  71. this.swaggerSecurity = properties.swagger?.security;
  72. }
  73. this.initialized = false;
  74. }
  75. public async init(): Promise<Server> {
  76. if(this.viewEngine)
  77. this.instance.set('view engine', this.viewEngine);
  78. if(this.viewsPath)
  79. this.instance.set('views', this.viewsPath);
  80. this.i18n.load(path.resolve(__dirname, Server.i18nDefaultPath));
  81. await this.registerMiddlewares(path.resolve(__dirname, Server.defaultMiddlewaresPath));
  82. await this.registerRoutes(path.resolve(__dirname, Server.defaultRoutesPath));
  83. await this.postInit();
  84. this.initialized = true;
  85. return this;
  86. }
  87. private async postInit(): Promise<void> {
  88. if(this.i18nPath)
  89. this.i18n.load(this.i18nPath);
  90. if(this.middlewaresPath)
  91. await this.registerMiddlewares(this.middlewaresPath);
  92. if(this.routesPath)
  93. await this.registerRoutes(this.routesPath);
  94. if(this.swaggerDocsPath)
  95. fs.writeFileSync(this.swaggerDocsPath, '');
  96. if(Object.keys(this.wsHandlers).length > 0) {
  97. this.registerWsServers();
  98. this.applyWsHandlers();
  99. }
  100. }
  101. private processHttpHandlers(): void {
  102. const handlers: HttpHandler[] = [];
  103. for(const key in this.httpHandlers)
  104. handlers.push(this.httpHandlers[key]);
  105. handlers.sort((a, b) => a.getOrder() - b.getOrder());
  106. for(const handler of handlers) {
  107. if(handler instanceof Middleware)
  108. this.addMiddleware(handler);
  109. else if (handler instanceof Route)
  110. this.addRoute(handler);
  111. }
  112. }
  113. public addMiddleware(middleware: Middleware): Server {
  114. if(middleware.getRoute() != null)
  115. this.instance.use(<string>middleware.getRoute(), middleware.getAction());
  116. else
  117. this.instance.use(middleware.getAction());
  118. return this;
  119. }
  120. public addRoute(route: Route): Server {
  121. if(route.getRoute() == null)
  122. throw new RouteNotSetException();
  123. switch(route.getMethod()) {
  124. case HttpMethod.GET:
  125. return this.get(route);
  126. case HttpMethod.POST:
  127. return this.post(route);
  128. default:
  129. throw new IncorrectMethodException();
  130. }
  131. }
  132. private registerRoutesDocumentation(): Server {
  133. for(const routeName in this.httpHandlers) {
  134. const route = this.httpHandlers[routeName];
  135. if(!(route instanceof Route))
  136. continue;
  137. if(route.getRoute() == null)
  138. throw new RouteNotSetException();
  139. if(![HttpMethod.GET, HttpMethod.POST].includes(route.getMethod()))
  140. throw new IncorrectMethodException();
  141. const docs = route.getDocumentation();
  142. if(this.swaggerDocsPath && docs.length > 0) {
  143. fs.appendFileSync(this.swaggerDocsPath, `${docs}\n`);
  144. this.logInfo(`Swagger documentation for route '${route.getRoute()}' generated!`);
  145. }
  146. }
  147. return this;
  148. }
  149. public registerWsServers(): Server {
  150. for(const url of Object.keys(this.wsHandlers)) {
  151. const wsServer = this.wsServers[url] = new WebSocketServer({ noServer: true });
  152. const wsHandler = this.wsHandlers[url];
  153. wsServer.on(WebSocketHandler.Event.CONNECTION, (ws: WS) => {
  154. wsHandler.onConnect(ws);
  155. ws.on(WebSocketHandler.Event.MESSAGE, wsHandler.onMessage);
  156. ws.on(WebSocketHandler.Event.ERROR, wsHandler.onError);
  157. ws.on(WebSocketHandler.Event.CLOSE, wsHandler.onClose);
  158. });
  159. }
  160. return this;
  161. }
  162. public applyWsHandlers(): Server {
  163. this.httpServer.on('upgrade', (request, socket, head) => {
  164. const url = request.url?.split('?')[0];
  165. if(url && this.wsHandlers[url] && this.wsServers[url]) {
  166. const wsServer = this.wsServers[url];
  167. wsServer.handleUpgrade(request, socket, head, (ws) => {
  168. wsServer.emit(WebSocketHandler.Event.CONNECTION, ws, request);
  169. });
  170. } else {
  171. socket.destroy();
  172. }
  173. });
  174. return this;
  175. }
  176. public getWsConnections(url: string): Set<WS> | null {
  177. const wsServer = this.wsServers[url];
  178. return wsServer ? wsServer.clients : null;
  179. }
  180. public logInfo(message: string): void {
  181. this.logger.getLogger().info(message);
  182. }
  183. public logError(message: string): void {
  184. this.logger.getLogger().error(message);
  185. }
  186. public logWarn(message: string): void {
  187. this.logger.getLogger().warn(message);
  188. }
  189. public log(message: Message): void {
  190. switch(message.type) {
  191. case MessageTypes.WARNING:
  192. return this.logWarn(message.text);
  193. case MessageTypes.ERROR:
  194. return this.logError(message.text);
  195. default:
  196. return this.logInfo(message.text);
  197. }
  198. }
  199. private get(route: Route): Server {
  200. this.instance.get(<string>route.getRoute(), route.getAction());
  201. return this;
  202. }
  203. private post(route: Route): Server {
  204. this.instance.post(<string>route.getRoute(), route.getAction());
  205. return this;
  206. }
  207. private registerSwaggerMiddleware(): Server {
  208. if(!this.swaggerDocsPath)
  209. return this;
  210. const swaggerOptions : OAS3Options = {
  211. swaggerDefinition: {
  212. openapi: '3.0.1',
  213. info: {
  214. title: this.swaggerTitle!,
  215. version: this.swaggerApiVersion!,
  216. description: this.swaggerDescription!,
  217. },
  218. servers: [
  219. { url: this.host }
  220. ],
  221. },
  222. apis: [this.swaggerDocsPath],
  223. };
  224. if(this.swaggerComponents)
  225. swaggerOptions.swaggerDefinition!.components = this.swaggerComponents;
  226. if(this.swaggerSecurity)
  227. swaggerOptions.swaggerDefinition!.security = this.swaggerSecurity as SecurityRequirement[];
  228. const swaggerDocs = swaggerJsDoc(swaggerOptions as Options);
  229. this.instance.use(this.swaggerRoute!, swaggerUi.serve, swaggerUi.setup(swaggerDocs));
  230. return this;
  231. }
  232. public async registerRoutes(dir: string): Promise<Server> {
  233. const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' });
  234. for(const file of files) {
  235. if(/\.js$/.test(file)) {
  236. const {default: RouteClass} = await import(path.join(dir, file));
  237. if(RouteClass.prototype instanceof Route) {
  238. this.httpHandlers[RouteClass.name] = <HttpHandler>new RouteClass(this);
  239. } else throw new InvalidRouteException(file);
  240. }
  241. }
  242. return this;
  243. }
  244. public async registerMiddlewares(dir: string): Promise<Server> {
  245. const files = fs.readdirSync(dir, { recursive: true, encoding: 'utf8' });
  246. for(const file of files) {
  247. if(/\.js$/.test(file)) {
  248. const {default: MiddlewareClass} = await import(path.join(dir, file));
  249. if(MiddlewareClass.prototype instanceof Middleware) {
  250. this.httpHandlers[MiddlewareClass.name] = <HttpHandler>new MiddlewareClass(this);
  251. } else throw new InvalidMiddlewareException(file);
  252. }
  253. }
  254. return this;
  255. }
  256. public getLogger(): Logger {
  257. return this.logger;
  258. }
  259. public i18nLoad(path: string): Server {
  260. this.i18n.load(path);
  261. return this;
  262. }
  263. public getHost(): string {
  264. return this.host;
  265. }
  266. public getOption(key: string): any {
  267. return this.options ? this.options[key] || null : null;
  268. }
  269. public start(callback?: () => any): void {
  270. if(!this.initialized)
  271. throw new ServerNotInitializedException();
  272. this.registerRoutesDocumentation();
  273. this.registerSwaggerMiddleware();
  274. this.processHttpHandlers();
  275. const cb = (): void => {
  276. this.logInfo($$('org.crazydoctor.expressts.start', { 'port': this.port }));
  277. if(callback) callback();
  278. };
  279. this.httpServer.listen(this.port, cb);
  280. }
  281. }
  282. export { Server };