UNPKG

@nestjs/core

Version:

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

223 lines (222 loc) 11.7 kB
import { ForbiddenException, SSE_ABORT_CONTROLLER, } from '@nestjs/common'; import { CUSTOM_ROUTE_ARGS_METADATA, HEADERS_METADATA, HTTP_CODE_METADATA, isEmptyArray, isString, REDIRECT_METADATA, RENDER_METADATA, ROUTE_ARGS_METADATA, RouteParamtypes, SSE_METADATA, } from '@nestjs/common/internal'; import { FORBIDDEN_MESSAGE, } from '../guards/index.js'; import { ContextUtils } from '../helpers/context-utils.js'; import { HandlerMetadataStorage, } from '../helpers/handler-metadata-storage.js'; import { STATIC_CONTEXT } from '../injector/constants.js'; import { RouterResponseController, } from './router-response-controller.js'; export class RouterExecutionContext { paramsFactory; pipesContextCreator; pipesConsumer; guardsContextCreator; guardsConsumer; interceptorsContextCreator; interceptorsConsumer; applicationRef; handlerMetadataStorage = new HandlerMetadataStorage(); contextUtils = new ContextUtils(); responseController; constructor(paramsFactory, pipesContextCreator, pipesConsumer, guardsContextCreator, guardsConsumer, interceptorsContextCreator, interceptorsConsumer, applicationRef) { this.paramsFactory = paramsFactory; this.pipesContextCreator = pipesContextCreator; this.pipesConsumer = pipesConsumer; this.guardsContextCreator = guardsContextCreator; this.guardsConsumer = guardsConsumer; this.interceptorsContextCreator = interceptorsContextCreator; this.interceptorsConsumer = interceptorsConsumer; this.applicationRef = applicationRef; this.responseController = new RouterResponseController(applicationRef); } create(instance, callback, methodName, moduleKey, requestMethod, contextId = STATIC_CONTEXT, inquirerId) { const contextType = 'http'; const { argsLength, fnHandleResponse, isSseHandler, paramtypes, getParamsMetadata, httpStatusCode, responseHeaders, hasCustomHeaders, } = this.getMetadata(instance, callback, methodName, moduleKey, requestMethod, contextType); const paramsOptions = this.contextUtils.mergeParamsMetatypes(getParamsMetadata(moduleKey, contextId, inquirerId), paramtypes); const pipes = this.pipesContextCreator.create(instance, callback, moduleKey, contextId, inquirerId); const guards = this.guardsContextCreator.create(instance, callback, moduleKey, contextId, inquirerId); const interceptors = this.interceptorsContextCreator.create(instance, callback, moduleKey, contextId, inquirerId); const fnCanActivate = this.createGuardsFn(guards, instance, callback, contextType); const fnApplyPipes = this.createPipesFn(pipes, paramsOptions); const handler = (args, req, res, next) => async () => { fnApplyPipes && (await fnApplyPipes(args, req, res, next)); return callback.apply(instance, args); }; return async (req, res, next) => { const args = this.contextUtils.createNullArray(argsLength); fnCanActivate && (await fnCanActivate([req, res, next])); this.responseController.setStatus(res, httpStatusCode); hasCustomHeaders && this.responseController.setHeaders(res, responseHeaders); if (isSseHandler) { // Attach a per-request AbortController before the handler runs so async // @Sse() handlers can observe client disconnects via @SseSignal() during // their setup. The controller is aborted in RouterResponseController.sse() // when the underlying connection closes. this.attachSseAbortSignal(req); } const resultOrDeferred = this.interceptorsConsumer.intercept(interceptors, [req, res, next], instance, callback, handler(args, req, res, next), contextType); const result = isSseHandler ? resultOrDeferred : await resultOrDeferred; await fnHandleResponse(result, res, req); }; } getMetadata(instance, callback, methodName, moduleKey, requestMethod, contextType) { const cacheMetadata = this.handlerMetadataStorage.get(instance, methodName); if (cacheMetadata) { return cacheMetadata; } const metadata = this.contextUtils.reflectCallbackMetadata(instance, methodName, ROUTE_ARGS_METADATA) || {}; const keys = Object.keys(metadata); const argsLength = this.contextUtils.getArgumentsLength(keys, metadata); const paramtypes = this.contextUtils.reflectCallbackParamtypes(instance, methodName); const contextFactory = this.contextUtils.getContextFactory(contextType, instance, callback); const getParamsMetadata = (moduleKey, contextId = STATIC_CONTEXT, inquirerId) => this.exchangeKeysForValues(keys, metadata, moduleKey, contextId, inquirerId, contextFactory); const paramsMetadata = getParamsMetadata(moduleKey); const isResponseHandled = this.isResponseHandled(instance, methodName, paramsMetadata); const httpRedirectResponse = this.reflectRedirect(callback); const fnHandleResponse = this.createHandleResponseFn(callback, isResponseHandled, httpRedirectResponse); const isSseHandler = !!this.reflectSse(callback); const httpCode = this.reflectHttpStatusCode(callback); const httpStatusCode = httpCode ?? this.responseController.getStatusByMethod(requestMethod); const responseHeaders = this.reflectResponseHeaders(callback); const hasCustomHeaders = !isEmptyArray(responseHeaders); const handlerMetadata = { argsLength, fnHandleResponse, isSseHandler, paramtypes, getParamsMetadata, httpStatusCode, hasCustomHeaders, responseHeaders, }; this.handlerMetadataStorage.set(instance, methodName, handlerMetadata); return handlerMetadata; } reflectRedirect(callback) { return Reflect.getMetadata(REDIRECT_METADATA, callback); } reflectHttpStatusCode(callback) { return Reflect.getMetadata(HTTP_CODE_METADATA, callback); } reflectRenderTemplate(callback) { return Reflect.getMetadata(RENDER_METADATA, callback); } reflectResponseHeaders(callback) { return Reflect.getMetadata(HEADERS_METADATA, callback) || []; } reflectSse(callback) { return Reflect.getMetadata(SSE_METADATA, callback); } exchangeKeysForValues(keys, metadata, moduleContext, contextId = STATIC_CONTEXT, inquirerId, contextFactory) { this.pipesContextCreator.setModuleContext(moduleContext); return keys.map(key => { const { index, data, pipes: pipesCollection, schema } = metadata[key]; const pipes = this.pipesContextCreator.createConcreteContext(pipesCollection, contextId, inquirerId); const type = this.contextUtils.mapParamType(key); if (key.includes(CUSTOM_ROUTE_ARGS_METADATA)) { const { factory } = metadata[key]; const customExtractValue = this.contextUtils.getCustomFactory(factory, data, contextFactory); return { index, extractValue: customExtractValue, type, data, pipes, schema, }; } const numericType = Number(type); const extractValue = (req, res, next) => this.paramsFactory.exchangeKeyForValue(numericType, data, { req: req, res, next, }); return { index, extractValue, type: numericType, data, pipes, schema }; }); } async getParamValue(value, metadata, pipes) { if (!isEmptyArray(pipes)) { return this.pipesConsumer.apply(value, metadata, pipes); } return value; } isPipeable(type) { return (type === RouteParamtypes.BODY || type === RouteParamtypes.RAW_BODY || type === RouteParamtypes.QUERY || type === RouteParamtypes.PARAM || type === RouteParamtypes.FILE || type === RouteParamtypes.FILES || isString(type)); } createGuardsFn(guards, instance, callback, contextType) { const canActivateFn = async (args) => { const canActivate = await this.guardsConsumer.tryActivate(guards, args, instance, callback, contextType); if (!canActivate) { throw new ForbiddenException(FORBIDDEN_MESSAGE); } }; return guards.length ? canActivateFn : null; } createPipesFn(pipes, paramsOptions) { const pipesFn = async (args, req, res, next) => { const resolveParamValue = async (param) => { const { index, extractValue, type, data, metatype, pipes: paramPipes, schema, } = param; const value = extractValue(req, res, next); args[index] = this.isPipeable(type) ? await this.getParamValue(value, { metatype, type, data, schema }, pipes.concat(paramPipes)) : value; }; await Promise.all(paramsOptions.map(resolveParamValue)); }; return paramsOptions.length ? pipesFn : null; } createHandleResponseFn(callback, isResponseHandled, redirectResponse, httpStatusCode) { const renderTemplate = this.reflectRenderTemplate(callback); if (renderTemplate) { return async (result, res) => { return await this.responseController.render(result, res, renderTemplate); }; } if (redirectResponse && isString(redirectResponse.url)) { return async (result, res) => { await this.responseController.redirect(result, res, redirectResponse); }; } const isSseHandler = !!this.reflectSse(callback); if (isSseHandler) { return async (result, res, req) => { const rawResponse = res.raw ?? res; await this.responseController.sse(result, rawResponse, req.raw || req, { additionalHeaders: res.getHeaders?.(), statusCode: res.statusCode ?? rawResponse.statusCode, }); }; } return async (result, res) => { result = await this.responseController.transformToResult(result); !isResponseHandled && (await this.responseController.apply(result, res, httpStatusCode)); return res; }; } isResponseHandled(instance, methodName, paramsMetadata) { const hasResponseOrNextDecorator = paramsMetadata.some(({ type }) => type === RouteParamtypes.RESPONSE || type === RouteParamtypes.NEXT); const isPassthroughEnabled = this.contextUtils.reflectPassthrough(instance, methodName); return hasResponseOrNextDecorator && !isPassthroughEnabled; } attachSseAbortSignal(req) { const carrier = req; // Attach to both the framework request and its raw form (when present), since // @SseSignal() reads from the execution-context request while // RouterResponseController.sse() operates on the raw request. if (!carrier[SSE_ABORT_CONTROLLER]) { carrier[SSE_ABORT_CONTROLLER] = new AbortController(); } if (carrier.raw && !carrier.raw[SSE_ABORT_CONTROLLER]) { carrier.raw[SSE_ABORT_CONTROLLER] = carrier[SSE_ABORT_CONTROLLER]; } } }