UNPKG

sipp

Version:

An Opinionated, High-Productivity MVC Web Framework in TypeScript

367 lines 16.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.App = void 0; const dotenv_1 = require("dotenv"); const module_alias_1 = __importDefault(require("module-alias")); const express_1 = __importDefault(require("express")); const express_session_1 = __importDefault(require("express-session")); const connect_flash_1 = __importDefault(require("connect-flash")); const csurf_1 = __importDefault(require("csurf")); const cookie_parser_1 = __importDefault(require("cookie-parser")); const method_override_1 = __importDefault(require("method-override")); const constants_1 = require("./constants"); const Connection_1 = require("./db/Connection"); const exceptions_1 = require("./exceptions"); const http_1 = require("./http"); const RouteMapper_1 = require("./routing/RouteMapper"); const logger_1 = require("./logger"); const async_store_1 = require("./utils/async-store"); const services_1 = require("./services"); const ServiceRegistry_1 = require("./framework/services/ServiceRegistry"); const utils_1 = require("./utils"); dotenv_1.config(); module_alias_1.default(process.cwd()); const defaultConfig = { basePath: '', serviceName: 'app', port: process.env.PORT ? parseInt(process.env.PORT) : 3000, }; const normalizeMiddlewareOptions = (opt) => { return Array.isArray(opt) ? [opt[0], opt[1]] : ['', opt == null ? {} : opt || false]; }; class App { constructor(app, config, controllers) { this.controllers = []; this.providers = []; this.globalMiddleware = []; this.middleware = []; this.app = app; this.controllers = controllers || []; this.logger = config.logger || logger_1.Logger.new(config.mode || 'production'); if (config.serviceName) { this.logger.setServiceLabel(config.serviceName); } this.config = Object.assign({}, defaultConfig, config); this.exceptionHandler = new exceptions_1.ExceptionHandler(this.logger, config.mode); this.routeMapper = new RouteMapper_1.RouteMapper(); this.connection = new Connection_1.Connection(this.config.mode, this.config.knexPath); } static bootstrap(config, controllers) { return new App(express_1.default(), config, controllers).init(); } init() { var _a, _b, _c, _d, _e, _f; this.logger.debug('App:init'); this.withGlobalMiddleware(http_1.reqInfoLoggingMiddleware(this.logger)); const [jsonTest, jsonOpt] = normalizeMiddlewareOptions((_a = this.config.middleware) === null || _a === void 0 ? void 0 : _a.json); if (jsonOpt) { this.withGlobalMiddleware(jsonTest, express_1.default.json(jsonOpt)); } const [bodyTest, bodyOpt] = normalizeMiddlewareOptions((_b = this.config.middleware) === null || _b === void 0 ? void 0 : _b.body); if (bodyOpt) { this.withGlobalMiddleware(bodyTest, express_1.default.urlencoded(Object.assign({ extended: true }, bodyOpt))); } this.withGlobalMiddleware(method_override_1.default('_method')); const [staticTest, staticOpt] = normalizeMiddlewareOptions((_c = this.config.middleware) === null || _c === void 0 ? void 0 : _c.static); if (staticOpt) { this.withGlobalMiddleware(staticTest, express_1.default.static(staticOpt.path)); } const [sessionTest, sessionOpt] = normalizeMiddlewareOptions((_d = this.config.middleware) === null || _d === void 0 ? void 0 : _d.session); if (sessionOpt !== false) { this.withGlobalMiddleware(sessionTest, express_session_1.default(sessionOpt), connect_flash_1.default()); } const [test, csrfOpt] = normalizeMiddlewareOptions((_e = this.config.middleware) === null || _e === void 0 ? void 0 : _e.csrf); if (csrfOpt !== false) { if (csrfOpt.cookie) { const [secret, opt] = ((_f = this.config.middleware) === null || _f === void 0 ? void 0 : _f.cookieParser) || []; this.withGlobalMiddleware(cookie_parser_1.default(secret, opt)); } this.withMiddleware(test, csurf_1.default(csrfOpt), (req, _, next) => { if (req.body && req.body._csrf) { req.headers['csrf-token'] = req.body._csrf; delete req.body._csrf; } next(); }); } this.withMiddleware((req) => { const store = async_store_1.getStore(); store.set("__REQ_KEY__", req); }); this.withProviders(new services_1.ParamResolutionProvider(), new services_1.ModelResolutionProvider(), new services_1.RouteMappingProvider(this.routeMapper), new services_1.UrlProvider(staticOpt.path, this.routeMapper), new services_1.LoggerProvider(this.logger)); return this; } getLogger() { return this.logger; } getExceptionHandler() { return this.exceptionHandler; } withMiddleware(route, ...middleware) { this.logger.debug('adding middleware'); let realPath = ''; if (typeof route !== 'string' && !(route instanceof RegExp)) { this.middleware.push([ '', route instanceof http_1.Middleware ? route.bind() : route, ]); } else { realPath = route; } const middlewareTuples = middleware.map((m) => [realPath, m instanceof http_1.Middleware ? m.bind() : m]); this.middleware.push(...middlewareTuples); return this; } withGlobalMiddleware(route, ...middleware) { this.logger.debug('adding global middleware'); let realPath = ''; if (typeof route !== 'string' && !(route instanceof RegExp)) { this.globalMiddleware.push([ '', route instanceof http_1.Middleware ? route.bind() : route, ]); } else { realPath = route; } const middlewareTuples = middleware.map((m) => [realPath, m instanceof http_1.Middleware ? m.bind() : m]); this.globalMiddleware.push(...middlewareTuples); return this; } withControllers(...controllers) { this.logger.debug('adding controllers'); this.controllers.push(...controllers); return this; } withProviders(...providers) { this.logger.debug('adding controllers'); this.providers.push(...providers); return this; } withExceptionHandler(handler) { this.logger.debug(`adding exception handler ${handler.constructor.name}`); this.exceptionHandler = handler; return this; } async wire() { await Promise.all(this.providers.map((provider) => provider.init())); ServiceRegistry_1.registry.registerProviders(this.providers); this.connection.connect(); this.globalMiddleware.forEach(([test, fn]) => { if (!test) { this.app.use(...this.wrapMiddleware(fn)); } else { this.app.use(test, ...this.wrapMiddleware(fn)); } }); this.registerControllers(); this.app.use((req, _, next) => { next(new exceptions_1.NotFoundException(`${req.method}: ${req.path} not found`)); }); this.app.use((err, req, res, next) => { this.onException(err, req, res, next); }); return this; } listen() { return this.app.listen(this.config.port, () => { this.logger.debug(`${this.config.serviceName} listening on ${this.config.port}`); }); } express() { return this.app; } registerControllers() { this.controllers.forEach((controller) => { const routes = Reflect.getMetadata(constants_1.ROUTES_METADATA, controller); for (let method in routes) { const pathMetadata = Reflect.getMetadata(constants_1.PATH_METADATA, controller, method); const pathOptionMetadata = Reflect.getMetadata(constants_1.PATH_OPTION_METADATA, controller, method); const methodMetadata = Reflect.getMetadata(constants_1.METHOD_METADATA, controller, method); if (methodMetadata && pathMetadata) { const fullPath = this.constructPath(pathMetadata, controller.getBasePath()); if (pathOptionMetadata && pathOptionMetadata.name) { this.routeMapper.register(pathOptionMetadata.name, fullPath, methodMetadata); } const controllerMiddleware = Reflect.getMetadata(constants_1.CONTROLLER_MIDDLEWARE_METADATA, controller.constructor); const methodMiddleware = Reflect.getMetadata(constants_1.MIDDLEWARE_METADATA, controller, method); const middlewaresToApply = (this.middleware || []) .map(([test, fn]) => { if (!test) { return fn; } if (test instanceof RegExp) { return test.test(fullPath) && fn; } return fullPath.startsWith(test) && fn; }) .filter((fn) => fn); this.app[methodMetadata].apply(this.app, [ fullPath, ...this.wrapMiddleware(...middlewaresToApply, ...(controllerMiddleware || []), ...(methodMiddleware || []), async (req, res) => { req.logger.debug('start controller handling'); const controllerReponse = await Promise.resolve(controller[method](req, res)) .then(http_1.toResponse) .then(async (response) => { const store = async_store_1.getStore(); const trx = store.get("__TRANSACTION_KEY__"); if (trx && !trx.isCompleted()) { req.logger.info('transaction commit'); await trx.commit(); } return response; }) .catch((err) => { req.logger.debug(`controller response threw an error, ${err.message}`); throw err; }); req.logger.debug('end controller handling'); if (!res.headersSent) { this.handleResponse(controllerReponse, req, res); } }), async (err, req, res, next) => { try { const trx = async_store_1.getStore().get("__TRANSACTION_KEY__"); if (trx && !trx.isCompleted()) { req.logger.info('rolling back transaction'); await trx.rollback().catch((rollbackError) => { req.logger.critical(`failed to rollback transaction: ${rollbackError.message}`); }); } await this.onException(err, req, res, next, controller); } catch (err) { next(err); } }, ]); } } }); } constructPath(path, controllerBase) { const replaceSlashes = (str) => str.replace(/^\/?/, '').replace(/\/?$/, ''); return ('/' + [ replaceSlashes(this.config.basePath), replaceSlashes(controllerBase), replaceSlashes(path), ] .filter(Boolean) .join('/')); } async onException(err, req, res, next, controller) { const exception = exceptions_1.BaseException.toException(err); const logger = req.logger || this.logger; let handled = false; if (controller) { try { const controllerReponse = controller && (await controller.onException(exception, req, res)); if (controllerReponse !== false) { this.handleResponse(await http_1.toResponse(controllerReponse), req, res); handled = true; } } catch (err) { logger.error(`${controller.constructor.name} threw handling error, ${err.message}`); } } if (handled) { try { this.exceptionHandler.reportHandledException(exception); } catch (err) { logger.error(`exception handler threw reporting handled exception, ${err.message}`); } } else { handled = await Promise.resolve(this.exceptionHandler.handle(exception, req, res)).then((rawResponse) => { if (rawResponse !== false) { return http_1.toResponse(rawResponse).then((exceptionHandlerResponse) => { this.handleResponse(exceptionHandlerResponse, req, res); return true; }); } }); if (!handled) { Promise.resolve(this.exceptionHandler.reportUnhandledException(exception)).catch((reportError) => { logger.error(`exception handler threw reporting unhandled exception, ${reportError.message}`); }); } else { Promise.resolve(this.exceptionHandler.reportHandledException(exception)).catch((reportError) => { logger.error(`exception handler threw reporting handled exception, ${reportError.message}`); }); } if (!res.headersSent) { next(exception); } } } wrapMiddleware(...middleware) { return middleware .filter((fn) => fn) .map((fn) => { return (req, res, next) => { let isResolved = false; new Promise((resolve, reject) => { const expectsNext = fn.length === 3; const maybePromise = fn(req, res, (err, response) => { isResolved = true; return err ? reject(err) : resolve(response); }); if (maybePromise && maybePromise instanceof Promise) { maybePromise .then((resolvedValue) => { if (!isResolved) { resolve(resolvedValue); } }) .catch((err) => { if (!isResolved) { reject(err); } }); } else if (!expectsNext) { resolve(void 0); } }) .then((resolvedValue) => { if (utils_1.isInstanceOf(http_1.HTTPResponse, resolvedValue)) { this.handleResponse(resolvedValue, req, res); } if (!res.headersSent) { next(); } }) .catch((err) => { if (!res.headersSent) { next(err); } }); }; }); } handleResponse(response, req, res) { response.handle(req, res); req.logger.debug('response sent'); this.afterResponse(req, res); } afterResponse(req, res) { req.logger.addScope({ status: res.statusCode, }); req.logger.info(`duration ${Date.now() - req.received.getTime()}ms`); } } exports.App = App; //# sourceMappingURL=App.js.map