@reflet/express
Version:
Well-defined and well-typed express decorators
274 lines (273 loc) • 12.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getGlobalMiddlewares = exports.register = void 0;
const express = require("express");
const async_wrapper_1 = require("./async-wrapper");
const type_guards_1 = require("./type-guards");
// Extractors
const router_decorator_1 = require("./router-decorator");
const route_decorators_1 = require("./route-decorators");
const middleware_decorator_1 = require("./middleware-decorator");
const error_handler_decorator_1 = require("./error-handler-decorator");
const param_decorators_1 = require("./param-decorators");
const send_decorator_1 = require("./send-decorator");
const application_class_1 = require("./application-class");
const reflet_error_1 = require("./reflet-error");
/**
* Main method to register routers into an express application.
* @param app - express application.
* @param routers - decorated classes or instances.
*
* @example
* ```ts
* class Foo {
* @Get('/some')
* get() {}
* }
* const app = express()
* register(app, [Foo])
* app.listen(3000)
*
* // ------
* // or with instantiation:
* register(app, [new Foo()])
* ```
* ------
* @public
*/
function register(app, routers) {
if (!(0, type_guards_1.isExpressApp)(app)) {
throw new reflet_error_1.RefletExpressError('INVALID_EXPRESS_APP', 'This is not an Express application.');
}
const appMeta = (0, application_class_1.extractApplicationClass)(app);
registerRootHandlers(app, appMeta);
// Retrieve all global middlewares from plain `use` and from application class decorators if any.
const globalMwares = getGlobalMiddlewares(app);
for (const router of routers) {
registerRouter(router, app, globalMwares, [], appMeta === null || appMeta === void 0 ? void 0 : appMeta.class);
}
registerRootErrorHandlers(app, appMeta);
if (appMeta && !appMeta.registered) {
appMeta.registered = true;
}
return app;
}
exports.register = register;
/**
* @internal
*/
function registerRouter(registration, app, globalMwares, parentSharedMwares = [], appClass) {
const [constrainedPath, router] = isPathRouterTuple(registration) ? registration : [null, registration];
// Attach plain express routers.
if (constrainedPath && (0, type_guards_1.isExpressRouter)(router)) {
app.use(constrainedPath, router);
return;
}
const routerInstance = (0, type_guards_1.isClass)(router) ? new router() : router;
const routerClass = (0, type_guards_1.isClass)(router) ? router : routerInstance.constructor;
// Must be after instanciation to properly retrieve child routers from metadata.
const routerMeta = (0, router_decorator_1.extractRouterMeta)(routerClass, appClass);
if (!routerMeta || routerMeta.path == null) {
throw new reflet_error_1.RefletExpressError('ROUTER_DECORATOR_MISSING', `"${routerClass.name}" must be decorated with @Router.`);
}
checkPathConstraint(constrainedPath, routerMeta, routerClass);
// Either attach middlewares/handlers to an intermediary router or directly to the app.
const appInstance = routerMeta ? express.Router(routerMeta.options) : app;
const routes = (0, route_decorators_1.extractRoutes)(routerClass);
const sharedMwares = (0, middleware_decorator_1.extractMiddlewares)(routerClass);
const sharedErrHandlers = (0, error_handler_decorator_1.extractErrorHandlers)(routerClass);
// Apply shared middlewares to the router instance
// or to each of the routes if the class is attached on the base app.
if (!routerMeta.scopedMiddlewares) {
for (const mware of sharedMwares) {
appInstance.use((0, async_wrapper_1.wrapAsync)(mware));
}
}
for (const { path, method, key } of routes) {
const routeMwares = (0, middleware_decorator_1.extractMiddlewares)(routerClass, key);
const routeErrHandlers = (0, error_handler_decorator_1.extractErrorHandlers)(routerClass, key);
const paramsMwares = (0, param_decorators_1.extractParamsMiddlewares)(routerClass, key, [
globalMwares,
parentSharedMwares,
sharedMwares,
routeMwares,
]);
const handler = createHandler(routerClass, routerInstance, key, appClass);
appInstance[method](path, routerMeta.scopedMiddlewares ? sharedMwares.map(async_wrapper_1.wrapAsync) : [], routeMwares.map(async_wrapper_1.wrapAsync), paramsMwares.map(async_wrapper_1.wrapAsync), handler, routeErrHandlers.map(async_wrapper_1.wrapAsyncError), routerMeta.scopedMiddlewares ? sharedErrHandlers.map(async_wrapper_1.wrapAsyncError) : []);
}
// Recursively attach children routers
if (routerMeta.children) {
// Keep track of all shared middlewares for dedupe.
const parentSharedMwares_ = parentSharedMwares.concat(sharedMwares);
const children = typeof routerMeta.children === 'function'
? routerMeta.children(...routerMeta.childrenDeps)
: routerMeta.children;
for (const child of children) {
registerRouter(child, appInstance, globalMwares, parentSharedMwares_, appClass);
}
}
if (!routerMeta.scopedMiddlewares) {
for (const errHandler of sharedErrHandlers) {
appInstance.use((0, async_wrapper_1.wrapAsyncError)(errHandler));
}
}
const routerPath = routerMeta.path === router_decorator_1.DYNAMIC_PATH ? constrainedPath : routerMeta.path;
// Finally attach the router to the app
app.use(routerPath, appInstance);
}
/**
* Retrieves and attaches global middlewares and routes from an inherited `Application` class.
* Only executed once, if the user calls `register` multiple times on the same app.
* @internal
*/
function registerRootHandlers(app, appMeta) {
if (!appMeta || appMeta.registered) {
return;
}
// Probability that the user has already attached middlewares before calling `register`.
const preexistingGlobalMwares = getGlobalMiddlewares(app);
const newGlobalMwares = (0, middleware_decorator_1.extractMiddlewares)(appMeta.class);
for (const globalMware of newGlobalMwares) {
app.use((0, async_wrapper_1.wrapAsync)(globalMware));
}
const routes = (0, route_decorators_1.extractRoutes)(appMeta.class);
for (const { key, method, path } of routes) {
const routeMwares = (0, middleware_decorator_1.extractMiddlewares)(appMeta.class, key);
const routeErrHandlers = (0, error_handler_decorator_1.extractErrorHandlers)(appMeta.class, key);
const paramsMwares = (0, param_decorators_1.extractParamsMiddlewares)(appMeta.class, key, [
preexistingGlobalMwares,
newGlobalMwares,
routeMwares,
]);
const handler = createHandler(appMeta.class, app, key);
app[method](path, routeMwares.map(async_wrapper_1.wrapAsync), paramsMwares.map(async_wrapper_1.wrapAsync), handler, routeErrHandlers.map(async_wrapper_1.wrapAsyncError));
}
}
/**
* Retrieves and attaches global error handlers from an inherited `Application` class.
* @internal
*/
function registerRootErrorHandlers(app, appMeta) {
var _a, _b;
if (!appMeta) {
return;
}
const globalErrorHandlers = (0, error_handler_decorator_1.extractErrorHandlers)(appMeta.class);
if (!globalErrorHandlers.length) {
return;
}
// Error handlers added at the end of the stack during the first `register` call,
// are moved to the last position for subsequent calls, to keep their behavior global.
if (appMeta.registered) {
for (const errHandler of globalErrorHandlers) {
const layerIndex = (_a = app._router) === null || _a === void 0 ? void 0 : _a.stack.findIndex((layer) => layer.handle === errHandler);
// optimisation, not sure about side effects.
// if (layerIndex === app._router.stack.length - 1) continue
/* istanbul ignore else - user has removed error handler before calling register again */
if (layerIndex >= 0) {
(_b = app._router) === null || _b === void 0 ? void 0 : _b.stack.splice(layerIndex, 1);
app.use((0, async_wrapper_1.wrapAsyncError)(errHandler));
}
}
}
else {
for (const errHandler of globalErrorHandlers) {
app.use((0, async_wrapper_1.wrapAsyncError)(errHandler));
}
}
}
/**
* @internal
*/
function isPathRouterTuple(registration) {
return Array.isArray(registration) && registration.length === 2;
}
/**
* @internal
*/
function checkPathConstraint(constrainedPath, router, routerClass) {
if (router.path === router_decorator_1.DYNAMIC_PATH) {
if (constrainedPath === null) {
throw new reflet_error_1.RefletExpressError('DYNAMIC_ROUTER_PATH_UNDEFINED', `"${routerClass.name}" is dynamic and must be registered with a path.`);
}
// Stop there if dynamic path
return;
}
// Stop there if no constraint on path
if (constrainedPath === null) {
return;
}
const notSameString = typeof router.path === 'string' && typeof constrainedPath === 'string' && router.path !== constrainedPath;
if (notSameString) {
throw new reflet_error_1.RefletExpressError('ROUTER_PATH_CONSTRAINED', `"${routerClass.name}" expects "${constrainedPath}" as root path. Actual: "${router.path}".`);
}
const notSameRegex = router.path instanceof RegExp &&
constrainedPath instanceof RegExp &&
router.path.source !== constrainedPath.source;
if (notSameRegex) {
throw new reflet_error_1.RefletExpressError('ROUTER_PATH_CONSTRAINED', `"${routerClass.name}" expects "${constrainedPath}" as root path. Actual: "${router.path}".`);
}
const shouldBeString = router.path instanceof RegExp && typeof constrainedPath === 'string';
if (shouldBeString) {
throw new reflet_error_1.RefletExpressError('ROUTER_PATH_CONSTRAINED', `"${routerClass.name}" expects string "${constrainedPath}" as root path. Actual: "${router.path}" (regex).`);
}
const shouldBeRegex = typeof router.path === 'string' && constrainedPath instanceof RegExp;
if (shouldBeRegex) {
throw new reflet_error_1.RefletExpressError('ROUTER_PATH_CONSTRAINED', `"${routerClass.name}" expects regex "${constrainedPath}" as root path. Actual: "${router.path}" (string).`);
}
}
/**
* @internal
*/
function createHandler(routerClass, // different from `routerInstance.constructor` in case of decorated Application class.
routerInstance, key, appClass) {
const sendHandler = (0, send_decorator_1.extractSendHandler)(routerClass, key, appClass);
// get from the instance instead of the prototype, so this can be a function property and not only a method.
const fn = routerInstance[key];
const codePath = `${routerClass.name}.${String(key)}`;
if (typeof fn !== 'function') {
throw new reflet_error_1.RefletExpressError('INVALID_ROUTE_TYPE', `"${codePath}" should be a function.`);
}
const isAsync = (0, type_guards_1.isAsyncFunction)(fn);
return (req, res, next) => {
const args = (0, param_decorators_1.extractParams)(routerClass, key, { req, res, next });
const result = fn.apply(routerInstance, args);
// Handle or bypass sending the method's result according to @Send decorator,
// if the response has already been sent to the client, we also bypass.
if (!sendHandler || res.headersSent) {
// We only use the async modifier in this case to decide to catch async errors, for performance.
// If the user ever decides to return a promise in a non-async method while not using @Send,
// it won't catch its errors, but that's an unlikely edge case.
return isAsync ? result.catch(next) : result;
}
if ((0, type_guards_1.isPromise)(result)) {
return result.then((value) => sendHandler(value, { req, res, next })).catch(next);
}
try {
const sendResult = sendHandler(result, { req, res, next });
return (0, type_guards_1.isPromise)(sendResult) ? sendResult.catch(next) : sendResult;
}
catch (error) {
next(error);
}
};
}
/**
* Exported for tests only.
* @internal
*/
function getGlobalMiddlewares(app) {
var _a;
const globalMwares = [];
// `_router` might be undefined before express app is properly initialized.
for (const layer of ((_a = app._router) === null || _a === void 0 ? void 0 : _a.stack) || []) {
if (layer.name !== 'query' &&
layer.name !== 'expressInit' &&
layer.name !== 'router' &&
layer.name !== 'bound dispatch') {
globalMwares.push(layer.handle);
}
}
return globalMwares;
}
exports.getGlobalMiddlewares = getGlobalMiddlewares;