Server.ts 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import fs from 'fs';
  2. import http from 'http';
  3. import express, { Express } from 'express';
  4. import { Middleware, Route, HttpMethod } from '../base/http';
  5. import { RouteNotSetException } from '../base/exceptions';
  6. import { IncorrectMethodException } from '../base/exceptions';
  7. import { Logger, Message } from '../base/logger';
  8. import { ServerProperties } from './ServerProperties';
  9. import { i18nLoader, $ } from '../base/i18n';
  10. import { WebSocketServer, WebSocket as WS } from 'ws';
  11. import { ServerNotInitializedException } from '../base/exceptions';
  12. import { WebSocketHandler } from '../base/websocket';
  13. import Registry from '../base/registry/Registry';
  14. import SystemRegistry from '../base/registry/SystemRegistry';
  15. import DynamicRegistry from '../base/registry/DynamicRegistry';
  16. /** @sealed */
  17. class Server {
  18. private instance: Express;
  19. private httpServer: http.Server;
  20. private readonly port: number;
  21. private readonly host: string;
  22. private readonly logger: Logger;
  23. private i18n: i18nLoader;
  24. private readonly wsHandlers: {[url: string]: WebSocketHandler};
  25. private readonly wsServers: {[url: string]: WebSocketServer};
  26. private initialized: boolean;
  27. private readonly i18nPath?: string;
  28. private readonly middlewaresPath?: string;
  29. private readonly routesPath?: string;
  30. private readonly viewEngine?: string;
  31. private readonly viewsPath?: string;
  32. private readonly options?: {[key: string]: any};
  33. private readonly swaggerComponents? : object;
  34. private readonly swaggerSecurity? : object;
  35. private readonly swaggerDocsPath? : string;
  36. private readonly swaggerTitle?: string;
  37. private readonly swaggerDescription?: string;
  38. private readonly swaggerApiVersion?: string;
  39. private readonly swaggerRoute?: string;
  40. public static registry: Registry;
  41. private readonly systemRegistry: SystemRegistry;
  42. public constructor(properties: ServerProperties) {
  43. this.instance = express();
  44. this.httpServer = http.createServer(this.instance);
  45. this.port = properties.port;
  46. this.host = properties.host || `http://localhost:${this.port}`;
  47. this.i18n = i18nLoader.getInstance();
  48. if(properties.locale)
  49. this.i18n.setLocale(properties.locale);
  50. this.logger = new Logger();
  51. this.wsHandlers = properties.wsHandlers || {};
  52. this.wsServers = {};
  53. this.i18nPath = properties.i18nPath;
  54. this.middlewaresPath = properties.middlewaresPath;
  55. this.routesPath = properties.routesPath;
  56. this.viewEngine = properties.viewEngine;
  57. this.viewsPath = properties.viewsPath;
  58. this.options = properties.options;
  59. if(properties.swagger) {
  60. this.swaggerDocsPath = properties.swagger?.docsPath;
  61. this.swaggerTitle = properties.swagger?.title || 'API Documentation';
  62. this.swaggerDescription = properties.swagger?.description || 'API Documentation';
  63. this.swaggerApiVersion = properties.swagger?.version ||'1.0.0';
  64. this.swaggerRoute = properties.swagger?.route || '/api-docs';
  65. this.swaggerComponents = properties.swagger?.components;
  66. this.swaggerSecurity = properties.swagger?.security;
  67. }
  68. this.systemRegistry = new SystemRegistry({
  69. json: properties.json || false,
  70. urlencoded: properties.urlencoded || false,
  71. sessions: properties.sessions || false,
  72. swagger: properties.swagger != null
  73. }).setServer(this) as SystemRegistry;
  74. Server.registry = Server.registry == null ? new DynamicRegistry(this.routesPath, this.middlewaresPath) : Server.registry;
  75. Server.registry.setServer(this);
  76. this.initialized = false;
  77. }
  78. private async init(): Promise<Server> {
  79. if(this.viewEngine)
  80. this.instance.set('view engine', this.viewEngine);
  81. if(this.viewsPath)
  82. this.instance.set('views', this.viewsPath);
  83. await this.systemRegistry.registerHttpHandlers();
  84. await this.postInit();
  85. this.initialized = true;
  86. return this;
  87. }
  88. private async postInit(): Promise<void> {
  89. if(this.i18nPath)
  90. this.i18n.load(this.i18nPath);
  91. await Server.registry.registerHttpHandlers();
  92. if(this.swaggerDocsPath)
  93. fs.writeFileSync(this.swaggerDocsPath, '');
  94. if(Object.keys(this.wsHandlers).length > 0) {
  95. this.registerWsServers();
  96. this.applyWsHandlers();
  97. }
  98. }
  99. private processHttpHandlers(): void {
  100. for(const handler of Registry.getHttpHandlers()) {
  101. if(handler instanceof Middleware)
  102. this.addMiddleware(handler);
  103. else if (handler instanceof Route)
  104. this.addRoute(handler);
  105. }
  106. }
  107. public addMiddleware(middleware: Middleware): Server {
  108. if(middleware.getRoute() != null)
  109. this.instance.use(<string>middleware.getRoute(), middleware.getAction());
  110. else
  111. this.instance.use(middleware.getAction());
  112. return this;
  113. }
  114. public addRoute(route: Route): Server {
  115. if(route.getRoute() == null)
  116. throw new RouteNotSetException();
  117. switch(route.getMethod()) {
  118. case HttpMethod.GET:
  119. return this.get(route);
  120. case HttpMethod.POST:
  121. return this.post(route);
  122. default:
  123. throw new IncorrectMethodException();
  124. }
  125. }
  126. private registerRoutesDocumentation(): Server {
  127. if(!this.swaggerDocsPath)
  128. return this;
  129. for(const route of Registry.getHttpHandlers()) {
  130. if(!(route instanceof Route))
  131. continue;
  132. if(route.getRoute() == null)
  133. throw new RouteNotSetException();
  134. if(![HttpMethod.GET, HttpMethod.POST].includes(route.getMethod()))
  135. throw new IncorrectMethodException();
  136. const docs = route.getDocumentation();
  137. if(docs.length > 0) {
  138. fs.appendFileSync(this.swaggerDocsPath, `${docs}\n`);
  139. this.logInfo($('org.crazydoctor.extress.swagger.routeGenerated', { route: route.getRoute() }));
  140. }
  141. }
  142. return this;
  143. }
  144. public registerWsServers(): Server {
  145. for(const url of Object.keys(this.wsHandlers)) {
  146. const wsServer = this.wsServers[url] = new WebSocketServer({ noServer: true });
  147. const wsHandler = this.wsHandlers[url];
  148. wsServer.on(WebSocketHandler.Event.CONNECTION, (ws: WS) => {
  149. wsHandler.onConnect(ws);
  150. ws.on(WebSocketHandler.Event.MESSAGE, wsHandler.onMessage);
  151. ws.on(WebSocketHandler.Event.ERROR, wsHandler.onError);
  152. ws.on(WebSocketHandler.Event.CLOSE, wsHandler.onClose);
  153. });
  154. }
  155. return this;
  156. }
  157. public applyWsHandlers(): Server {
  158. this.httpServer.on('upgrade', (request, socket, head) => {
  159. const url = request.url?.split('?')[0];
  160. if(url && this.wsHandlers[url] && this.wsServers[url]) {
  161. const wsServer = this.wsServers[url];
  162. wsServer.handleUpgrade(request, socket, head, (ws) => {
  163. wsServer.emit(WebSocketHandler.Event.CONNECTION, ws, request);
  164. });
  165. } else {
  166. socket.destroy();
  167. }
  168. });
  169. return this;
  170. }
  171. public getWsConnections(url: string): Set<WS> | null {
  172. const wsServer = this.wsServers[url];
  173. return wsServer ? wsServer.clients : null;
  174. }
  175. public logInfo(message: string): void {
  176. this.logger.info(message);
  177. }
  178. public logError(message: string): void {
  179. this.logger.error(message);
  180. }
  181. public logWarn(message: string): void {
  182. this.logger.warn(message);
  183. }
  184. public log(message: Message): void {
  185. switch(message.type) {
  186. case 'warn':
  187. return this.logWarn(message.text);
  188. case 'error':
  189. return this.logError(message.text);
  190. default:
  191. return this.logInfo(message.text);
  192. }
  193. }
  194. private get(route: Route): Server {
  195. this.instance.get(<string>route.getRoute(), route.getAction());
  196. return this;
  197. }
  198. private post(route: Route): Server {
  199. this.instance.post(<string>route.getRoute(), route.getAction());
  200. return this;
  201. }
  202. public getLogger(): Logger {
  203. return this.logger;
  204. }
  205. public i18nLoad(path: string): Server {
  206. this.i18n.load(path);
  207. return this;
  208. }
  209. public getHost(): string {
  210. return this.host;
  211. }
  212. public getOption(key: string): any {
  213. return this.options ? this.options[key] || null : null;
  214. }
  215. public async start(callback?: () => any): Promise<Server> {
  216. return this.init().then((server) => {
  217. if(!this.initialized)
  218. throw new ServerNotInitializedException();
  219. this.registerRoutesDocumentation();
  220. this.processHttpHandlers();
  221. const cb = (): void => {
  222. this.logInfo($('org.crazydoctor.extress.start', { 'port': this.port }));
  223. if(callback) callback();
  224. };
  225. this.httpServer.listen(this.port, cb);
  226. return server;
  227. });
  228. }
  229. }
  230. export { Server };