import { HttpServer } from '@nestjs/common'; import { PATH_METADATA } from '@nestjs/common/constants'; import { RequestMethod, VersioningType } from '@nestjs/common/enums'; import { InternalServerErrorException } from '@nestjs/common/exceptions'; import { Controller } from '@nestjs/common/interfaces/controllers/controller.interface'; import { Type } from '@nestjs/common/interfaces/type.interface'; import { VersionValue } from '@nestjs/common/interfaces/version-options.interface'; import { Logger } from '@nestjs/common/services/logger.service'; import { addLeadingSlash, isUndefined, } from '@nestjs/common/utils/shared.utils'; import * as pathToRegexp from 'path-to-regexp'; import { ApplicationConfig } from '../application-config'; import { UnknownRequestMappingException } from '../errors/exceptions/unknown-request-mapping.exception'; import { GuardsConsumer, GuardsContextCreator } from '../guards'; import { ContextIdFactory } from '../helpers/context-id-factory'; import { ExecutionContextHost } from '../helpers/execution-context-host'; import { ROUTE_MAPPED_MESSAGE, VERSIONED_ROUTE_MAPPED_MESSAGE, } from '../helpers/messages'; import { RouterMethodFactory } from '../helpers/router-method-factory'; import { STATIC_CONTEXT } from '../injector/constants'; import { NestContainer } from '../injector/container'; import { Injector } from '../injector/injector'; import { ContextId, InstanceWrapper } from '../injector/instance-wrapper'; import { Module } from '../injector/module'; import { GraphInspector } from '../inspector/graph-inspector'; import { Entrypoint, HttpEntrypointMetadata, } from '../inspector/interfaces/entrypoint.interface'; import { InterceptorsConsumer, InterceptorsContextCreator, } from '../interceptors'; import { MetadataScanner } from '../metadata-scanner'; import { PipesConsumer, PipesContextCreator } from '../pipes'; import { ExceptionsFilter } from './interfaces/exceptions-filter.interface'; import { RoutePathMetadata } from './interfaces/route-path-metadata.interface'; import { PathsExplorer } from './paths-explorer'; import { REQUEST_CONTEXT_ID } from './request/request-constants'; import { RouteParamsFactory } from './route-params-factory'; import { RoutePathFactory } from './route-path-factory'; import { RouterExecutionContext } from './router-execution-context'; import { RouterProxy, RouterProxyCallback } from './router-proxy'; export interface RouteDefinition { path: string[]; requestMethod: RequestMethod; targetCallback: RouterProxyCallback; methodName: string; version?: VersionValue; } export class RouterExplorer { private readonly executionContextCreator: RouterExecutionContext; private readonly pathsExplorer: PathsExplorer; private readonly routerMethodFactory = new RouterMethodFactory(); private readonly logger = new Logger(RouterExplorer.name, { timestamp: true, }); private readonly exceptionFiltersCache = new WeakMap(); constructor( metadataScanner: MetadataScanner, private readonly container: NestContainer, private readonly injector: Injector, private readonly routerProxy: RouterProxy, private readonly exceptionsFilter: ExceptionsFilter, config: ApplicationConfig, private readonly routePathFactory: RoutePathFactory, private readonly 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(), ); } public explore( instanceWrapper: InstanceWrapper, moduleKey: string, applicationRef: T, host: string | RegExp | Array, routePathMetadata: RoutePathMetadata, ) { const { instance } = instanceWrapper; const routerPaths = this.pathsExplorer.scanForPaths(instance); this.applyPathsToRouterProxy( applicationRef, routerPaths, instanceWrapper, moduleKey, routePathMetadata, host, ); } public extractRouterPath(metatype: Type): string[] { 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)]; } public applyPathsToRouterProxy( router: T, routeDefinitions: RouteDefinition[], instanceWrapper: InstanceWrapper, moduleKey: string, routePathMetadata: RoutePathMetadata, host: string | RegExp | Array, ) { (routeDefinitions || []).forEach(routeDefinition => { const { version: methodVersion } = routeDefinition; routePathMetadata.methodVersion = methodVersion; this.applyCallbackToRouter( router, routeDefinition, instanceWrapper, moduleKey, routePathMetadata, host, ); }); } private applyCallbackToRouter( router: T, routeDefinition: RouteDefinition, instanceWrapper: InstanceWrapper, moduleKey: string, routePathMetadata: RoutePathMetadata, host: string | RegExp | Array, ) { 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 entrypointDefinition: Entrypoint = { type: 'http-endpoint', methodName, className: instanceWrapper.name, classNodeId: instanceWrapper.id, metadata: { key: path, path, requestMethod: RequestMethod[ requestMethod ] as keyof typeof RequestMethod, methodVersion: routePathMetadata.methodVersion, controllerVersion: routePathMetadata.controllerVersion, }, }; this.copyMetadataToCallback(targetCallback, routeHandler); routerMethodRef(path, routeHandler); 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)); } }); }); } private applyHostFilter( host: string | RegExp | Array, handler: Function, ) { if (!host) { return handler; } const httpAdapterRef = this.container.getHttpAdapterRef(); const hosts = Array.isArray(host) ? host : [host]; const hostRegExps = hosts.map((host: string | RegExp) => { const keys = []; const regexp = pathToRegexp(host, keys); return { regexp, 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 = any, TResponse = any>( req: TRequest, res: TResponse, next: () => void, ) => { (req as Record).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(); }; } private applyVersionFilter( router: T, routePathMetadata: RoutePathMetadata, handler: Function, ) { const version = this.routePathFactory.getVersion(routePathMetadata); return router.applyVersionFilter( handler, version, routePathMetadata.versioningOptions, ); } private createCallbackProxy( instance: Controller, callback: RouterProxyCallback, methodName: string, moduleRef: string, requestMethod: RequestMethod, contextId = STATIC_CONTEXT, inquirerId?: string, ) { 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); } public createRequestScopedHandler( instanceWrapper: InstanceWrapper, requestMethod: RequestMethod, moduleRef: Module, moduleKey: string, methodName: string, ) { const { instance } = instanceWrapper; const collection = moduleRef.controllers; const isTreeDurable = instanceWrapper.isDependencyTreeDurable(); return async , TResponse>( req: TRequest, res: TResponse, next: () => void, ) => { 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); } }; } private getContextId = any>( request: T, isTreeDurable: boolean, ): ContextId { const contextId = ContextIdFactory.getByRequest(request); if (!request[REQUEST_CONTEXT_ID as any]) { Object.defineProperty(request, REQUEST_CONTEXT_ID, { value: contextId, enumerable: false, writable: false, configurable: false, }); const requestProviderValue = isTreeDurable ? contextId.payload : request; this.container.registerRequestProvider(requestProviderValue, contextId); } return contextId; } private copyMetadataToCallback( originalCallback: RouterProxyCallback, targetCallback: Function, ) { for (const key of Reflect.getMetadataKeys(originalCallback)) { Reflect.defineMetadata( key, Reflect.getMetadata(key, originalCallback), targetCallback, ); } } }