Files
nest/packages/core/router/router-explorer.ts
2024-11-26 12:46:59 +01:00

439 lines
14 KiB
TypeScript

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<T extends HttpServer = any>(
instanceWrapper: InstanceWrapper,
moduleKey: string,
applicationRef: T,
host: string | RegExp | Array<string | RegExp>,
routePathMetadata: RoutePathMetadata,
) {
const { instance } = instanceWrapper;
const routerPaths = this.pathsExplorer.scanForPaths(instance);
this.applyPathsToRouterProxy(
applicationRef,
routerPaths,
instanceWrapper,
moduleKey,
routePathMetadata,
host,
);
}
public extractRouterPath(metatype: Type<Controller>): 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<T extends HttpServer>(
router: T,
routeDefinitions: RouteDefinition[],
instanceWrapper: InstanceWrapper,
moduleKey: string,
routePathMetadata: RoutePathMetadata,
host: string | RegExp | Array<string | RegExp>,
) {
(routeDefinitions || []).forEach(routeDefinition => {
const { version: methodVersion } = routeDefinition;
routePathMetadata.methodVersion = methodVersion;
this.applyCallbackToRouter(
router,
routeDefinition,
instanceWrapper,
moduleKey,
routePathMetadata,
host,
);
});
}
private applyCallbackToRouter<T extends HttpServer>(
router: T,
routeDefinition: RouteDefinition,
instanceWrapper: InstanceWrapper,
moduleKey: string,
routePathMetadata: RoutePathMetadata,
host: string | RegExp | Array<string | RegExp>,
) {
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<HttpEntrypointMetadata> = {
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<HttpEntrypointMetadata>(
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<string | RegExp>,
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 <TRequest extends Record<string, any> = any, TResponse = any>(
req: TRequest,
res: TResponse,
next: () => void,
) => {
(req as Record<string, any>).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<T extends HttpServer>(
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 <TRequest extends Record<any, any>, 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<T extends Record<any, unknown> = 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,
);
}
}
}