UNPKG

@reflet/express

Version:

Well-defined and well-typed express decorators

274 lines (273 loc) 12.9 kB
"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;