sipp
Version:
An Opinionated, High-Productivity MVC Web Framework in TypeScript
367 lines • 16.6 kB
JavaScript
"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