@foal/core
Version:
Full-featured Node.js framework, with no complexity
150 lines (149 loc) • 6.74 kB
JavaScript
;
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;
}