UNPKG

@foal/core

Version:

Full-featured Node.js framework, with no complexity

150 lines (149 loc) 6.74 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OPENAPI_SERVICE_ID = void 0; exports.getHttpLogParamsDefault = getHttpLogParamsDefault; exports.createApp = createApp; // std const node_crypto_1 = require("node:crypto"); // 3p const cookieParser = require("cookie-parser"); const express = require("express"); const morgan = require("morgan"); // FoalTS const core_1 = require("../core"); const send_response_1 = require("./send-response"); exports.OPENAPI_SERVICE_ID = 'OPENAPI_SERVICE_ID_a5NWKbBNBxVVZ'; function handleJsonErrors(err, req, res, next) { if (err.type !== 'entity.parse.failed') { next(err); return; } res.status(err.status).send({ body: err.body, message: err.message }); } function protectionHeaders(req, res, next) { res.removeHeader('X-Powered-By'); res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); next(); } function getHttpLogParamsDefault(tokens, req, res) { const statusCode = tokens.status(req, res); const contentLength = tokens.res(req, res, 'content-length'); const responseTime = tokens['response-time'](req, res); return { method: tokens.method(req, res), url: tokens.url(req, res).split('?')[0], statusCode: statusCode === undefined ? null : parseInt(statusCode, 10), contentLength: contentLength === undefined ? null : contentLength, responseTime: responseTime === undefined ? null : parseFloat(responseTime), }; } /** * Create an Express application from the root controller. * * @export * @param {Class<IAppController>} AppController - The root controller, usually called `AppController` * and located in `src/app`. * @param {CreateAppOptions} [options] - Options containaining Express middlewares or other settings. * @param {any} [options.expressInstance] - Express instance to be used as base for the * returned application. * @param {boolean} [options.methods.handleError] - Specifies if AppController.handleError should be * used to handle errors. * @param {ServiceManager} [options.serviceManager] - Prebuilt and configured Service Manager for * optionally overriding the mapped identities. * @param {(RequestHandler | ErrorRequestHandler)[]} [options.preMiddlewares] Express * middlewares to be executed before the controllers and hooks. * @param {(RequestHandler | ErrorRequestHandler)[]} [options.postMiddlewares] Express * middlewares to be executed after the controllers and hooks, but before the 500 or 404 handler get called. * @returns {Promise<any>} The express application. */ async function createApp(AppController, options = {}) { const app = options.expressInstance || express(); // Add optional pre-middlewares. for (const middleware of options.preMiddlewares || []) { app.use(middleware); } // Create the service and controller manager. const services = options.serviceManager || new core_1.ServiceManager(); app.foal = { services }; // Retrieve the logger. const logger = services.get(core_1.Logger); // Allow to add log context. app.use((req, res, next) => { logger.initLogContext(next); }); // Generate a unique ID for each request. app.use((req, res, next) => { const requestId = req.get('x-request-id') || (0, node_crypto_1.randomUUID)(); req.id = requestId; logger.addLogContext({ requestId }); next(); }); // Log requests. const shouldLogHttpRequests = core_1.Config.get('settings.logger.logHttpRequests', 'boolean', true); if (shouldLogHttpRequests) { const getHttpLogParams = options.getHttpLogParams || getHttpLogParamsDefault; app.use(morgan((tokens, req, res) => JSON.stringify(getHttpLogParams(tokens, req, res)), { stream: { write: (message) => { const data = JSON.parse(message); logger.info(`${core_1.httpRequestMessagePrefix}${data.method} ${data.url}`, data); }, }, })); } app.use(protectionHeaders); // Serve static files. app.use(core_1.Config.get('settings.staticPathPrefix', 'string', ''), express.static(core_1.Config.get('settings.staticPath', 'string', 'public'), { cacheControl: core_1.Config.get('settings.staticFiles.cacheControl', 'boolean') })); // Parse request body. const limit = core_1.Config.get('settings.bodyParser.limit', 'number|string'); app.use(express.json({ limit })); app.use(handleJsonErrors); app.use(express.urlencoded({ extended: false, limit })); app.use(express.text({ type: ['text/*', 'application/graphql', 'application/xml'], limit })); // Parse cookies. app.use(cookieParser(core_1.Config.get('settings.cookieParser.secret', 'string'))); // Add optional after pre-middlewares. for (const middleware of options.afterPreMiddlewares || []) { app.use(middleware); } // Inject the OpenAPI service with an ID string to avoid duplicated singletons // across several npm packages. services.set(exports.OPENAPI_SERVICE_ID, services.get(core_1.OpenApi)); // Retrieve the AppController instance. const appController = services.get(AppController); // Resolve the controllers and hooks and add them to the express instance. const routes = (0, core_1.makeControllerRoutes)(AppController, services); for (const { route } of routes) { app[route.httpMethod.toLowerCase()](route.path, async (req, res, next) => { try { const ctx = new core_1.Context(req, route.controller.constructor.name, route.propertyKey); // TODO: better test this line. const response = await (0, core_1.getResponse)(route, ctx, services, appController); (0, send_response_1.sendResponse)(response, res, logger); } catch (error) { // This try/catch will never be called: the `getResponse` function catches any errors // thrown or rejected in the application and converts it into a response. // However, for more security, this line has been added to avoid crashing the server // in case the function is badly implemented. next(error); } }); } // Add optional post-middlewares. for (const middleware of options.postMiddlewares || []) { app.use(middleware); } await services.boot(); if (appController.init) { await appController.init(); } return app; }