UNPKG

@micro.ts/core

Version:

Microservice framework with Typescript

852 lines (851 loc) 39.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseServer = void 0; const GlobalMetadata_1 = require("../decorators/GlobalMetadata"); const BaseHelpers_1 = require("../helpers/BaseHelpers"); const Logger_1 = require("./Logger"); const types_1 = require("./types"); const SpecBuilder_1 = require("../openapi/SpecBuilder"); const MainAppErrror_1 = require("../errors/MainAppErrror"); const ParamMetadataTypes_1 = require("../decorators/types/ParamMetadataTypes"); const BaseContainer_1 = require("../di/BaseContainer"); class BaseServer { constructor(options) { this.options = options; this._serverInfo = new Map(); } get logger() { return BaseContainer_1.Container.get(Logger_1.LoggerKey); } static get controllersMetadata() { return (0, GlobalMetadata_1.getGlobalMetadata)(); } /** * Execute a single middleware and return its result * @param middleware Middleware function or IMiddleware instance to execute * @param def Route definition on which the middleware is being called from * @param action The action object, with the state of the action just before this middleware is being executed * @param controller The controller instance * @param broker The broker instance */ static executeMiddleware(middleware, def, action, controller, broker, requestModule, send) { return __awaiter(this, void 0, void 0, function* () { if (middleware.prototype && 'do' in middleware.prototype) { const casted = requestModule.get(middleware.prototype.constructor); return casted.do(action, def, controller, broker, send); } return middleware(action, def, controller, broker, send); }); } /** * Execute the error handler and return its result as a boolean * @param handler Error handler function or IErrorHandler instance * @param error Error object * @param action Action object up to the state before the error * @param def Route definition where the error was thrown * @param controller The controller instance * @param broker The broker instance */ static executeErrorHandler(handler, error, action, def, controller, broker, requestModule) { return __awaiter(this, void 0, void 0, function* () { if (handler.prototype && 'do' in handler.prototype) { const casted = requestModule.get(handler.prototype.constructor); return casted.do(error, action, def, controller, broker); } return handler(error, action, def, controller, broker); }); } /** * If an error was thrown, the error object will go through all the error handlers sequentially until on of the handlers returns true * @param handlers List of handlers to execute * @param error Error object * @param action Action state on the moment the error was thrown * @param def Route definition where the error was thrown * @param controllerInstance The controller instance * @param broker The broker instance */ static handleError(handlers, error, action, def, controllerInstance, broker, requestModule) { return __awaiter(this, void 0, void 0, function* () { for (let i = 0; i < handlers.length; i++) { const result = yield BaseServer.executeErrorHandler(handlers[i], error, action, def, controllerInstance, broker, requestModule); if (result) { return true; } } return false; }); } executeWithTimeout(def, action, broker, controller, methodControllerMetadata, requestModule) { return __awaiter(this, void 0, void 0, function* () { /** * Get configured timeouts */ const baseTimeout = this.options.timeout || 0; const brokerTimeout = broker.getDefaultTimeout() || 0; const requestTimeout = def.timeout || 0; /** * Get non-zero timeouts and get the minimum timeout */ const timeout = (0, BaseHelpers_1.minNonZero)(baseTimeout, brokerTimeout, requestTimeout); const promises = [ this.handleRequest(def, action, broker, controller, methodControllerMetadata, requestModule), ]; if (timeout) { promises.push((0, BaseHelpers_1.sleep)(timeout).then(() => { throw new MainAppErrror_1.TimeoutError('Request timed out'); })); } return Promise.race(promises); }); } /** * Execute the request (passing through all the middlewares) and handle errors if thrown any * @param def Route definition * @param action Action from the broker * @param broker Broker instance */ executeRequest(def, action, broker) { return __awaiter(this, void 0, void 0, function* () { const start = new Date().getTime(); const requestModule = BaseContainer_1.Container.newModule(); requestModule.set(types_1.Action, action); action.container_module = requestModule; const controllerInstance = requestModule.get(def.controllerCtor); const methodControllerMetadata = (0, GlobalMetadata_1.getHandlerMetadata)(def.controllerCtor, def.handlerName); try { action = yield this.executeWithTimeout(def, action, broker, controllerInstance, methodControllerMetadata, requestModule); } catch (err) { const error = err; const errorHandlers = this.getErrorHandlers(methodControllerMetadata); const handled = yield BaseServer.handleError(errorHandlers, err, action, def, controllerInstance, broker, requestModule); if (!handled) { action.response = action.response || {}; action.response.statusCode = Number(error.statusCode) || 500; action.response.is_error = true; action.response.error = err; if (this.options.logErrors && action.response.statusCode === 500) { console.log('Error: ', { error: err, route: def }); } } } /* * Log the request info if enabled */ if (this.options.logRequests) { // let end = process.hrtime(start); const end = new Date().getTime() - start; const seconds = Math.floor(end / 1000); const millis = end % 1000; const response = action.response || {}; const statusCode = response.statusCode || 200; console.log(`[${broker.name}]`, `[${def.method.toUpperCase()}]`, `[${def.controller}]`, `[${def.handlerName}]`, `${action.request.path}`, statusCode === 200 ? `[${statusCode}]` : `[${statusCode}]`, `[${seconds}s ${millis}ms]`); // console.log( // chalk.greenBright(`[${broker.name}]`), // chalk.blueBright(`[${def.method.toUpperCase()}]`), // chalk.green(`[${def.controller}]`), // chalk.yellow(`[${def.handlerName}]`), // `${action.request.path}`, // statusCode === 200 // ? chalk.blue(`[${statusCode}]`) // : chalk.red(`[${statusCode}]`), // chalk.greenBright(`[${seconds}s ${millis}ms]`) // ); } return action; }); } /** * Checks if the action for the handler handler needs to pass through authorizationChecker function, * And executes the the authorizationChecker function, with the corresponding arguments * @param action Action object at the time of invocation * @param methodMetadata Metadata for the handler to check if the request should get filtered by authorization checker */ checkAuthorization(action, methodMetadata) { return __awaiter(this, void 0, void 0, function* () { let shouldCheck = false; if (methodMetadata.controller.authorize) { shouldCheck = true; } if (methodMetadata.method.authorize === false) { shouldCheck = false; } else if (methodMetadata.method.authorize === true) { shouldCheck = true; } if (shouldCheck && this.options.authorizationChecker) { const options = methodMetadata.method.authorization || methodMetadata.controller.authorization || {}; const authorized = yield this.options.authorizationChecker(action, options); if (!authorized) { if (this.options.getNotAuthorizedError) { const error = yield this.options.getNotAuthorizedError(action, options); throw error; } throw new MainAppErrror_1.NotAuthorized('You are not authorized to make this request'); } } }); } /** * Group an array of middleware options , with the before flag, into to groups, before middlewares and after middlewares * @param middlewares List of middleware options */ groupMiddlewares(middlewares) { const result = { before: [], after: [], }; middlewares.forEach((m) => { if (m.before) { result.before.push(m.middleware); } else { result.after.push(m.middleware); } }); return result; } /** * Get all middlewares for a specific handler, * The sorting of middlewares in this method determines the sequence of the middleware executions * Before the handler is executed middlewares are executed on this order: * 1. App before middlewares, * 2. Controller before middlewares, * 3. Handler before middlewares * After the handler is executed after middlewares are executed in this order * 1. Handler after middlewares * 2. Controller's after middlewares * 3. App's after middlewares * @param methodMetadata */ getMiddlewares(methodMetadata) { const middlewares = { before: [], after: [], }; let afterMiddlewares = []; /** * App level before middlewares */ if (this.options.beforeMiddlewares && this.options.beforeMiddlewares.length > 0) { middlewares.before.push(...this.options.beforeMiddlewares); } /** * App level after middlwares */ if (this.options.afterMiddlewares && this.options.afterMiddlewares.length > 0) { afterMiddlewares.push(this.options.afterMiddlewares); } /** * Controller level middlewares */ if (methodMetadata.controller.middlewares && methodMetadata.controller.middlewares.length > 0) { const groupedControllerMiddlewares = this.groupMiddlewares(methodMetadata.controller.middlewares); /** * Insert each item to the before middlewares */ middlewares.before.push(...groupedControllerMiddlewares.before); /** * Insert the whole group to to the after middlwares */ afterMiddlewares.push(groupedControllerMiddlewares.after); } /** * Handler level middlwares */ if (methodMetadata.method.middlewares && methodMetadata.method.middlewares.length > 0) { const groupedMethodMiddleware = this.groupMiddlewares(methodMetadata.method.middlewares); /** * Insert each item to the before middlewares */ middlewares.before.push(...groupedMethodMiddleware.before); /** * Insert the whole group to to the after middlwares */ afterMiddlewares.push(groupedMethodMiddleware.after); } // Reverse after middlewares so they go in the order of 1. Handler Middlewares, 2. Controller Middlewares, 3. Global Middlewares afterMiddlewares = afterMiddlewares.reverse(); afterMiddlewares.forEach((a) => { middlewares.after.push(...a); }); return middlewares; } /** * Build error handlers for the route, using first the method error handlers, then controller error handlers, and app-level error handlers * @param methodMetadata Metadata for the handler */ getErrorHandlers(methodMetadata) { const result = []; /** * Handler level error handlers */ result.push(...(methodMetadata.method.errorHandlers || [])); /** * Controller level error handlers */ result.push(...(methodMetadata.controller.errorHandlers || [])); /** * App level error handlers */ result.push(...(this.options.errorHandlers || [])); return result; } /** * Handle the before middlewares execution, handler execution, and after middlewares execution * @param def Route definition * @param action The action object from the broker * @param broker The broker instance * @param controllerInstance The controller instance (needs to be passed into the middlewares) * @param methodControllerMetadata Handler metadata */ handleRequest(def, action, broker, controllerInstance, methodControllerMetadata, requestModule) { return __awaiter(this, void 0, void 0, function* () { /** * If route requires authorization, check it with the authorization function */ yield this.checkAuthorization(action, methodControllerMetadata); /** * Build middlewares */ const middlewares = this.getMiddlewares(methodControllerMetadata); let broken = false; const send = (result) => { broken = true; action.response = action.response || {}; action.response.headers = action.response.headers || {}; action.response.statusCode = 200; action.response.body = result; return action; }; /** * Execute before middlewares */ if (middlewares.before.length) { for (let i = 0; i < middlewares.before.length; i++) { action = yield BaseServer.executeMiddleware(middlewares.before[i], def, action, controllerInstance, broker, requestModule, send); if (broken) { break; } } } if (!broken) { /** * Build handler parameters */ const args = yield this.buildParams(action, methodControllerMetadata.method, broker, requestModule, def); /** * Execute the handler */ const result = yield controllerInstance[def.handlerName](...args); /** * Build response */ action.response = action.response || {}; action.response.headers = action.response.headers || {}; action.response.statusCode = 200; action.response.body = result; /** * Execute after middlewares */ if (middlewares.after.length) { for (let i = 0; i < middlewares.after.length; i++) { action = yield BaseServer.executeMiddleware(middlewares.after[i], def, action, controllerInstance, broker, requestModule, send); if (broken) { break; } } } /** * If all are successful execute redirect */ if (methodControllerMetadata.method.redirect && !broken) { action.response = action.response || {}; action.response.headers = action.response.headers || {}; action.response.statusCode = 301; if (typeof action.response.body === 'string') { action.response.headers.Location = action.response.body; } else if (typeof action.response.body === 'object') { let unparsedUrl = methodControllerMetadata.method.redirect; const keys = Object.keys(action.response.body); keys.forEach((key) => { const match = new RegExp(`:${key}`, 'gi'); unparsedUrl = unparsedUrl.replace(match, action.response.body[key]); }); action.response.headers.Location = unparsedUrl; } else { action.response.headers.Location = methodControllerMetadata.method.redirect; } } } return action; }); } /** * Build the arguments list for the handler * @param action Action object * @param metadata * @param broker */ buildParams(action, metadata, broker, requestModule, def) { return __awaiter(this, void 0, void 0, function* () { return Promise.all(metadata.params.map((p) => __awaiter(this, void 0, void 0, function* () { return this.buildSingleParam(action, p, broker, requestModule, def); }))); }); } /** * Execute currentUserChecker function, to get the user from a request, and inject it if required int the handlers arguments * @param action Action object with the request * @param broker Broker instance */ getUser(action, broker) { return __awaiter(this, void 0, void 0, function* () { if (!this.options.currentUserChecker) { return null; } return this.options.currentUserChecker(action, broker); }); } transform(value, description, def, action) { if (value && this.options.transformerFunction && description.type) { return this.options.transformerFunction(description.type, value, description, def, action); } return value; } /** * Validate a single method argument * @param value Value of the argument * @param required If true and value is empty value it throws bad request * @param validate If true, and the validate function throws it throws bad request * @param name Key of the value in case is a single key option * @param type Type of parameter to use in validation * @param isObject if the value is a key-value object * @param notEmpty if the value should not be empty */ validateParam({ value, required, validate, name, type, isObject, notEmpty, }) { return __awaiter(this, void 0, void 0, function* () { if (required && !value) { throw new MainAppErrror_1.BadRequest(`${name} is required`); } if (isObject && notEmpty) { if (!!value && Object.keys(value).length === 0) { throw new MainAppErrror_1.BadRequest(`${name} must not be empty`); } } if (validate && !!value && this.options.validateFunction) { try { const result = yield this.options.validateFunction(value, type); return result || value; } catch (err) { const error = err; throw new MainAppErrror_1.BadRequest('One or more errors with your request', error.details || error.message || error); } } return value; }); } /** * Switches through all the cases of param types and maps the correct information * @param action Action object after all before middlewares executed * @param metadata Metadata for the handler * @param broker Broker instance */ buildSingleParam(action, metadata, broker, requestModule, def) { return __awaiter(this, void 0, void 0, function* () { if (!metadata.options) { return action.request.body || action.request.qs || {}; } else { const options = metadata.options; switch (options.decoratorType) { /** * Inject the request body */ case ParamMetadataTypes_1.ParamDecoratorType.Body: const body = this.transform(action.request.body, metadata, def, action); return this.validateParam({ value: body, required: options.bodyOptions.required || false, validate: options.bodyOptions.validate || false, isObject: true, notEmpty: options.bodyOptions.notEmpty || false, name: 'body', type: metadata.type, }); /** * Inject only a named field of the body */ case ParamMetadataTypes_1.ParamDecoratorType.BodyField: const bodyField = this.transform(action.request.body[options.name], metadata, def, action); return this.validateParam({ value: bodyField, isObject: false, required: options.bodyParamOptions.required || false, validate: false, name: options.name, }); /** * Inject the request parameters */ case ParamMetadataTypes_1.ParamDecoratorType.Params: const params = this.transform(action.request.params, metadata, def, action); return this.validateParam({ value: params, isObject: true, required: true, validate: options.paramOptions.validate || false, name: 'parameters', type: metadata.type, }); /** * Inject only a named parameter */ case ParamMetadataTypes_1.ParamDecoratorType.ParamField: const paramField = this.transform(action.request.params[options.name], metadata, def, action); return this.validateParam({ value: paramField || '', isObject: false, required: false, validate: false, name: options.name, }); /** * Inject the request method */ case ParamMetadataTypes_1.ParamDecoratorType.Method: return action.request.method; /** * Inject the broker connection */ case ParamMetadataTypes_1.ParamDecoratorType.Connection: return action.connection; /** * Inject the full action */ case ParamMetadataTypes_1.ParamDecoratorType.Request: return action; /** * Inject the broker's raw request */ case ParamMetadataTypes_1.ParamDecoratorType.RawRequest: return action.request.raw; /** * Inject a specified instance from the container */ case ParamMetadataTypes_1.ParamDecoratorType.ContainerInject: return requestModule.get(options.name); /** * Inject the broker */ case ParamMetadataTypes_1.ParamDecoratorType.Broker: return broker; /** * Inject all the request headers */ case ParamMetadataTypes_1.ParamDecoratorType.Header: const headers = this.transform(action.request.headers, metadata, def, action); return this.validateParam({ value: headers, isObject: true, notEmpty: options.headerOptions.notEmpty || false, required: options.headerOptions.validate || false, validate: false, name: 'headers', type: metadata.type, }); /** * Inject only a named header */ case ParamMetadataTypes_1.ParamDecoratorType.HeaderField: const headerParam = this.transform(action.request.headers[options.name], metadata, def, action); return this.validateParam({ value: headerParam, isObject: false, required: options.headerParamOptions.required || false, validate: false, name: options.name, }); /** * Inject the request query */ case ParamMetadataTypes_1.ParamDecoratorType.Query: const query = this.transform(action.request.qs, metadata, def, action); return this.validateParam({ value: query, isObject: true, notEmpty: options.queryOptions.notEmpty || false, required: options.queryOptions.required || false, validate: options.queryOptions.validate || false, name: 'query', type: metadata.type, }); /** * Inject only a named query field */ case ParamMetadataTypes_1.ParamDecoratorType.QueryField: const queryParam = this.transform(action.request.qs[options.name], metadata, def, action); return this.validateParam({ value: queryParam, isObject: false, required: options.queryParamOptions.required || false, validate: false, name: options.name, }); /** * Inject the user object */ case ParamMetadataTypes_1.ParamDecoratorType.User: const user = this.transform(yield this.getUser(action, broker), metadata, def, action); const required = options.currentUserOptions.required || false; if (required && !user) { throw new MainAppErrror_1.NotAuthorized('You are not authorized to access this resource'); } return user; } } }); } /** * Adds route to its corresponding brokers * @param def Route definition * @param brokers List of brokers enabled for this handler * @param params List of parameters required for this handler (to be used when generating API specifications) */ addRoute(def, brokers, params) { return __awaiter(this, void 0, void 0, function* () { if (this.options.onRouteListeners && this.options.onRouteListeners.length) { this.options.onRouteListeners.forEach((listener) => { try { listener(def, brokers, params); } catch (_e) { // ignore } }); } const result = {}; if (brokers && brokers.length) { for (let i = 0; i < brokers.length; i++) { const broker = brokers[i]; const name = broker.name; const route = yield broker.addRoute(def, (action) => { return this.executeRequest(def, action, broker); }); result[name] = route; const brokerServerInfo = this._serverInfo.get(broker) || []; brokerServerInfo.push({ route, def, params }); this._serverInfo.set(broker, brokerServerInfo); } if (this.options.generateSwagger) { try { const schemaBuilder = BaseContainer_1.Container.get(SpecBuilder_1.SpecBuilder); schemaBuilder.registerRoute(def, brokers, params); } catch (err) { console.log(`Error registering route schema!`, { route: def, error: err, }); } } } return result; }); } /** * Registers all routes to the brokers * Initializes all brokers */ start() { return __awaiter(this, void 0, void 0, function* () { yield this.buildRoutes(); if (this.options.brokers) { yield Promise.all(this.options.brokers.map((x) => __awaiter(this, void 0, void 0, function* () { if (this.options.onBrokerConnnectionError) { x.setConnectionErrorHandler(this.options.onBrokerConnnectionError); } try { yield x.start(); } catch (brokerConnectionError) { if (this.options.onBrokerConnnectionError) { this.options.onBrokerConnnectionError(x, brokerConnectionError); } else { throw brokerConnectionError; } } }))); this.logger.info('Base Server Started'); } }); } get serverInfo() { return this._serverInfo; } /** * Build route for a single handler * @param methodName Name of the method * @param desc Method metadata * @param basePath Base path of the app * @param controllerPath Path of the controller * @param ctor Controller constructor function * @param isJson Is JsonController * @param brokers List of brokers enabled for the controller * @param routes Routes to append to the result * @param controllerName Name of the controller */ buildSingleMethodRoute({ methodName, desc, basePath, controllerPath, ctor, isJson, brokers, routes, controllerName, methodDescriptor, timeout, brokerRouteOptions, }) { return __awaiter(this, void 0, void 0, function* () { const metadata = desc.metadata || {}; const methodPath = metadata.path; let path = methodPath || methodName; if (methodPath === '') { path = methodPath; } const reqMethod = metadata.method; const handlerBrokersFilter = metadata.brokers; let methodBrokers = [...brokers]; if (handlerBrokersFilter) { methodBrokers = methodBrokers.filter(handlerBrokersFilter); } const routeDefinition = { base: basePath, controller: controllerPath, controllerCtor: ctor, handler: path, handlerName: methodName, method: reqMethod || 'get', queueOptions: metadata.queueOptions, json: isJson, timeout: timeout, methodDescription: methodDescriptor, brokerRouteOptions, }; const results = yield this.addRoute(routeDefinition, methodBrokers, desc.params || []); const brokerNames = methodBrokers .map((x) => { return x.name; }) .join(', '); routes.push(Object.assign({ brokers: brokerNames, method: (reqMethod || 'get').toUpperCase(), handler: `${controllerName}.${methodName}` }, results)); }); } /** * Build routes for a single controller * @param controllerMetadata Controller metadata * @param basePath Base path of the app * @param routes All the routes of the controller * @param brokers All the brokers filtered for this controller */ buildSingleControllerRoute(controllerMetadata, basePath, routes, brokers) { var _a; return __awaiter(this, void 0, void 0, function* () { if (this.options.controllers.includes(controllerMetadata.ctor)) { const name = controllerMetadata.name; let options = controllerMetadata.options; options = options || {}; let controllerBrokers = [...brokers]; /** * Filter brokers for the controller if annotated with broker filter */ if (options.brokersFilter) { controllerBrokers = controllerBrokers.filter(options.brokersFilter); } const cPath = options.path; const isJson = !!options.json; const controllerPath = cPath || ''; const handlers = controllerMetadata.handlers; const controllerTimeout = (controllerMetadata.options && controllerMetadata.options.timeout) || 0; const controllerBrokerRouteOptions = (_a = controllerMetadata.options) === null || _a === void 0 ? void 0 : _a.brokerRouteOptions; /** * Build method handlers */ yield Promise.all(Object.keys(handlers).map((key) => __awaiter(this, void 0, void 0, function* () { var _b; /** * Get timeout from controller or method */ let timeout = controllerTimeout; const methodDescriptor = (controllerMetadata.handlers || {})[key]; /** * If it has method timeout configured get the minimum nonzero */ if (!!methodDescriptor) { methodDescriptor.metadata = methodDescriptor.metadata || {}; const methodTimeout = methodDescriptor.metadata.timeout || 0; timeout = (0, BaseHelpers_1.minNonZero)(controllerTimeout, methodTimeout); } yield this.buildSingleMethodRoute({ methodName: key, timeout, desc: (controllerMetadata.handlers || {})[key], basePath, controllerPath, ctor: controllerMetadata.ctor, isJson, brokers: controllerBrokers, routes, controllerName: name, methodDescriptor, brokerRouteOptions: ((_b = methodDescriptor.metadata) === null || _b === void 0 ? void 0 : _b.brokerRouteOptions) || controllerBrokerRouteOptions, }); }))); } }); } /** * Build all the routes for all the app's controllers * @param controllers Metadata for all registered controllers */ buildAllControllers(controllers) { return __awaiter(this, void 0, void 0, function* () { const routes = []; const basePath = this.options.basePath || ''; const brokers = this.options.brokers || []; yield Promise.all(controllers.map((c) => __awaiter(this, void 0, void 0, function* () { yield this.buildSingleControllerRoute(c, basePath, routes, brokers); }))); return routes; }); } /** * Gets all the controllers metadata and build all the routes * Displays route table on the screen */ buildRoutes() { return __awaiter(this, void 0, void 0, function* () { const controllers = BaseServer.controllersMetadata.controllers; const routes = yield this.buildAllControllers(Array.from(controllers.values())); console.table(routes); }); } } exports.BaseServer = BaseServer;