UNPKG

@nestjs/core

Version:

Nest - modern, fast, powerful node.js web framework (@core)

278 lines (277 loc) 14.5 kB
import { pathToRegexp } from 'path-to-regexp'; import { UnknownRequestMappingException } from '../errors/exceptions/unknown-request-mapping.exception.js'; import { GuardsConsumer, GuardsContextCreator } from '../guards/index.js'; import { ContextIdFactory } from '../helpers/context-id-factory.js'; import { ExecutionContextHost } from '../helpers/execution-context-host.js'; import { ROUTE_MAPPED_MESSAGE, VERSIONED_ROUTE_MAPPED_MESSAGE, } from '../helpers/messages.js'; import { RouterMethodFactory } from '../helpers/router-method-factory.js'; import { STATIC_CONTEXT } from '../injector/constants.js'; import { InterceptorsConsumer, InterceptorsContextCreator, } from '../interceptors/index.js'; import { PipesConsumer, PipesContextCreator } from '../pipes/index.js'; import { PathsExplorer } from './paths-explorer.js'; import { REQUEST_CONTEXT_ID } from './request/request-constants.js'; import { RouteParamsFactory } from './route-params-factory.js'; import { RouterExecutionContext } from './router-execution-context.js'; import { PATH_METADATA, addLeadingSlash, isUndefined, } from '@nestjs/common/internal'; import { RequestMethod, VersioningType, InternalServerErrorException, Logger, } from '@nestjs/common'; export class RouterExplorer { container; injector; routerProxy; exceptionsFilter; routePathFactory; graphInspector; executionContextCreator; pathsExplorer; routerMethodFactory = new RouterMethodFactory(); logger = new Logger(RouterExplorer.name, { timestamp: true, }); exceptionFiltersCache = new WeakMap(); constructor(metadataScanner, container, injector, routerProxy, exceptionsFilter, config, routePathFactory, graphInspector) { this.container = container; this.injector = injector; this.routerProxy = routerProxy; this.exceptionsFilter = exceptionsFilter; this.routePathFactory = routePathFactory; this.graphInspector = graphInspector; this.pathsExplorer = new PathsExplorer(metadataScanner); const routeParamsFactory = new RouteParamsFactory(); const pipesContextCreator = new PipesContextCreator(container, config); const pipesConsumer = new PipesConsumer(); const guardsContextCreator = new GuardsContextCreator(container, config); const guardsConsumer = new GuardsConsumer(); const interceptorsContextCreator = new InterceptorsContextCreator(container, config); const interceptorsConsumer = new InterceptorsConsumer(); this.executionContextCreator = new RouterExecutionContext(routeParamsFactory, pipesContextCreator, pipesConsumer, guardsContextCreator, guardsConsumer, interceptorsContextCreator, interceptorsConsumer, container.getHttpAdapterRef()); } explore(instanceWrapper, moduleKey, httpAdapterRef, host, routePathMetadata, options = {}) { const { instance } = instanceWrapper; const routerPaths = this.pathsExplorer.scanForPaths(instance); this.applyPathsToRouterProxy(httpAdapterRef, routerPaths, instanceWrapper, moduleKey, routePathMetadata, host, options); } extractRouterPath(metatype) { const path = Reflect.getMetadata(PATH_METADATA, metatype); if (isUndefined(path)) { throw new UnknownRequestMappingException(metatype); } if (Array.isArray(path)) { return path.map(p => addLeadingSlash(p)); } return [addLeadingSlash(path)]; } applyPathsToRouterProxy(router, routeDefinitions, instanceWrapper, moduleKey, routePathMetadata, host, options = {}) { (routeDefinitions || []).forEach(routeDefinition => { const { version: methodVersion } = routeDefinition; routePathMetadata.methodVersion = methodVersion; this.applyCallbackToRouter(router, routeDefinition, instanceWrapper, moduleKey, routePathMetadata, host, options); }); } applyCallbackToRouter(router, routeDefinition, instanceWrapper, moduleKey, routePathMetadata, host, options = {}) { const { onRouteResolved, deferRegistration = false } = options; const { path: paths, requestMethod, targetCallback, methodName, } = routeDefinition; const { instance } = instanceWrapper; const routerMethodRef = this.routerMethodFactory .get(router, requestMethod) .bind(router); const isRequestScoped = !instanceWrapper.isDependencyTreeStatic(); const proxy = isRequestScoped ? this.createRequestScopedHandler(instanceWrapper, requestMethod, this.container.getModuleByKey(moduleKey), moduleKey, methodName) : this.createCallbackProxy(instance, targetCallback, methodName, moduleKey, requestMethod); const isVersioned = (routePathMetadata.methodVersion || routePathMetadata.controllerVersion) && routePathMetadata.versioningOptions; let routeHandler = this.applyHostFilter(host, proxy); paths.forEach(path => { if (isVersioned && routePathMetadata.versioningOptions.type !== VersioningType.URI) { // All versioning (except for URI Versioning) is done via the "Version Filter" routeHandler = this.applyVersionFilter(router, routePathMetadata, routeHandler); } routePathMetadata.methodPath = path; const pathsToRegister = this.routePathFactory.create(routePathMetadata, requestMethod); pathsToRegister.forEach(path => { const normalizedPath = router.normalizePath ? router.normalizePath(path) : path; const entrypointDefinition = { type: 'http-endpoint', methodName, className: instanceWrapper.name, classNodeId: instanceWrapper.id, metadata: { key: path, path, requestMethod: RequestMethod[requestMethod], methodVersion: routePathMetadata.methodVersion, controllerVersion: routePathMetadata.controllerVersion, }, }; if (!deferRegistration) { this.copyMetadataToCallback(targetCallback, routeHandler); const httpAdapter = this.container.getHttpAdapterRef(); const onRouteTriggered = httpAdapter.getOnRouteTriggered?.(); if (onRouteTriggered) { routerMethodRef(normalizedPath, (...args) => { onRouteTriggered(requestMethod, path); return routeHandler(...args); }); } else { routerMethodRef(normalizedPath, routeHandler); } } onRouteResolved?.({ method: requestMethod, path: normalizedPath, rawPath: path, host, version: routePathMetadata.methodVersion ?? routePathMetadata.controllerVersion, methodVersion: routePathMetadata.methodVersion, controllerVersion: routePathMetadata.controllerVersion, handler: routeHandler, targetCallback, methodName, instanceWrapper, }); this.graphInspector.insertEntrypointDefinition(entrypointDefinition, instanceWrapper.id); }); const pathsToLog = this.routePathFactory.create({ ...routePathMetadata, versioningOptions: undefined, }, requestMethod); pathsToLog.forEach(path => { if (isVersioned) { const version = this.routePathFactory.getVersion(routePathMetadata); this.logger.log(VERSIONED_ROUTE_MAPPED_MESSAGE(path, requestMethod, version)); } else { this.logger.log(ROUTE_MAPPED_MESSAGE(path, requestMethod)); } }); }); } /** * Registers a previously resolved route on the underlying HTTP adapter. * Used when route registration has been deferred (e.g. when sorting * routes by specificity) so the caller can choose the order in which * routes are installed on the adapter. */ registerResolvedRoute(router, route) { const routerMethodRef = this.routerMethodFactory .get(router, route.method) .bind(router); this.copyMetadataToCallback(route.targetCallback, route.handler); const normalizedPath = route.path; const rawPath = route.rawPath ?? route.path; const httpAdapter = this.container.getHttpAdapterRef(); const onRouteTriggered = httpAdapter.getOnRouteTriggered?.(); if (onRouteTriggered) { routerMethodRef(normalizedPath, (...args) => { onRouteTriggered(route.method, rawPath); return route.handler(...args); }); } else { routerMethodRef(normalizedPath, route.handler); } } applyHostFilter(host, handler) { if (!host) { return handler; } const httpAdapterRef = this.container.getHttpAdapterRef(); const hosts = Array.isArray(host) ? host : [host]; const hostRegExps = hosts.map((host) => { if (typeof host === 'string') { try { return pathToRegexp(host); } catch (e) { if (e instanceof TypeError) { this.logger.error(`Unsupported host "${host}" syntax. In past releases, ?, *, and + were used to denote optional or repeating path parameters. The latest version of "path-to-regexp" now requires the use of named parameters. For example, instead of using a route like /users/* to capture all routes starting with "/users", you should use /users/*path. Please see the migration guide for more information.`); } throw e; } } return { regexp: host, keys: [] }; }); const unsupportedFilteringErrorMessage = Array.isArray(host) ? `HTTP adapter does not support filtering on hosts: ["${host.join('", "')}"]` : `HTTP adapter does not support filtering on host: "${host}"`; return (req, res, next) => { req.hosts = {}; const hostname = httpAdapterRef.getRequestHostname(req) || ''; for (const exp of hostRegExps) { const match = hostname.match(exp.regexp); if (match) { if (exp.keys.length > 0) { exp.keys.forEach((key, i) => (req.hosts[key.name] = match[i + 1])); } else if (exp.regexp && match.groups) { for (const groupName in match.groups) { req.hosts[groupName] = match.groups[groupName]; } } return handler(req, res, next); } } if (!next) { throw new InternalServerErrorException(unsupportedFilteringErrorMessage); } return next(); }; } applyVersionFilter(router, routePathMetadata, handler) { const version = this.routePathFactory.getVersion(routePathMetadata); return router.applyVersionFilter(handler, version, routePathMetadata.versioningOptions); } createCallbackProxy(instance, callback, methodName, moduleRef, requestMethod, contextId = STATIC_CONTEXT, inquirerId) { const executionContext = this.executionContextCreator.create(instance, callback, methodName, moduleRef, requestMethod, contextId, inquirerId); const exceptionFilter = this.exceptionsFilter.create(instance, callback, moduleRef, contextId, inquirerId); return this.routerProxy.createProxy(executionContext, exceptionFilter); } createRequestScopedHandler(instanceWrapper, requestMethod, moduleRef, moduleKey, methodName) { const { instance } = instanceWrapper; const collection = moduleRef.controllers; const isTreeDurable = instanceWrapper.isDependencyTreeDurable(); return async (req, res, next) => { try { const contextId = this.getContextId(req, isTreeDurable); const contextInstance = await this.injector.loadPerContext(instance, moduleRef, collection, contextId); await this.createCallbackProxy(contextInstance, contextInstance[methodName], methodName, moduleKey, requestMethod, contextId, instanceWrapper.id)(req, res, next); } catch (err) { let exceptionFilter = this.exceptionFiltersCache.get(instance[methodName]); if (!exceptionFilter) { exceptionFilter = this.exceptionsFilter.create(instance, instance[methodName], moduleKey); this.exceptionFiltersCache.set(instance[methodName], exceptionFilter); } const host = new ExecutionContextHost([req, res, next]); exceptionFilter.next(err, host); } }; } getContextId(request, isTreeDurable) { const contextId = ContextIdFactory.getByRequest(request); if (!request[REQUEST_CONTEXT_ID]) { Object.defineProperty(request, REQUEST_CONTEXT_ID, { value: contextId, enumerable: false, writable: false, configurable: false, }); const requestProviderValue = isTreeDurable ? contextId.payload : Object.assign(request, contextId.payload); this.container.registerRequestProvider(requestProviderValue, contextId); } return contextId; } copyMetadataToCallback(originalCallback, targetCallback) { for (const key of Reflect.getMetadataKeys(originalCallback)) { Reflect.defineMetadata(key, Reflect.getMetadata(key, originalCallback), targetCallback); } } }