NestJS как лучше всего инициализировать и передавать контекст запроса

#typescript #nestjs

#typescript #nestjs

Вопрос:

У меня есть глобальный перехватчик, который должен инициализировать мой собственный контекст запроса DTO, и я хочу, чтобы этот DTO был доступен в контроллере, который обрабатывает текущий запрос.

Решение, которое я нашел до сих пор, заключается в создании вводимого класса RequestContext с областью действия запроса:

 import {
    Injectable,
    Scope
} from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';

@Injectable({ scope: Scope.REQUEST })
export class RequestContext {
    public headers: IncomingHttpHeaders;
    ....

    initialize(request: Request) {
        this.headers = request.headers;
        .....
    }
}
 

И внедрить этот класс в перехватчик:

 import {
    NestInterceptor,
    ExecutionContext,
    CallHandler,
    Injectable,
    Inject
} from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RequestContext } from '../dto';

@Injectable()
export class RequestContextInterceptor implements NestInterceptor {
    constructor(
        @Inject(RequestContext)
        protected requestContext: RequestContext
    ) { }

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const request = context.switchToHttp().getRequest<Request>();
        this.requestContext.initialize(request);

        return next.handle()
            .pipe(
                tap(() => {
                    // decorate response
                }));
    }
}
 

А затем внедрить этот RequestContext в каждый контроллер…

 import {
    Controller,
    UseInterceptors,
    Inject,
    Get
} from '@nestjs/common';
import { BaseMicroserviceController } from '../core/base/base-microservice.controller';
import { RequestContext } from '../dto';
import { DispatchService } from '../services';

@Controller('api/v1/example')
export class ExampleController extends BaseMicroserviceController {

    constructor (
        @Inject(RequestContext)
        protected requestContext: RequestContext,
        protected dispatcheService: DispatchService
    ) {
        super(dispatcheService);
    }

    @Get()
    test() {
        return 'test';
    }
}
 

Существует огромное обходное решение для достижения этой простой функциональности, IMHO
Кроме того, у меня есть эта статья, в которой описывается, почему использовать инъекцию на основе области действия нехорошо: https://guxi.me/posts/why-you-should-avoid-using-request-scope-injection-in-nest-js /

Мой сервис будет огромным, с огромным количеством контроллеров и огромным количеством вводимых сервисов. Согласно этой статье — мой сервис не будет масштабируемым с точки зрения производительности и использования памяти.

Мой вопрос в том, как достичь нужной мне функциональности в NestJS и какова наилучшая практика? Еще один «бонусный вопрос» — класс RequestContext имеет initialize метод, который получает экспресс-запрос и анализирует его. Мне это не нравится, я хочу, чтобы каждое свойство этого класса было доступно только для чтения, и инициализирую этот класс традиционным способом, вызывая конструктор с request помощью object … Как я могу достичь этого с @Inject помощью стратегии?

Ответ №1:

Если вы хотите сделать это без использования поставщиков с областью запроса, вы можете многое упростить, просто обогатив объект запроса дополнительными данными. Объект запроса технически всегда доступен для входящего HTTP-взаимодействия независимо от того, какую область внедрения вы используете. Вы можете RequestContext полностью отказаться от него и просто добавить любые дополнительные данные, которые вы хотите, к объекту запроса внутри вашего перехватчика.

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest<Request>();
    const customRequestContext = initialize(request); // whatever you need to do to build this

    request.customRequestContext = customRequestContext;

    return next.handle();
}
 

Легко получить доступ к этому значению в любом из ваших контроллеров, используя пользовательский декоратор:

 export const RequestContext = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.customRequestContext;
  },
);
 

А затем в любом из ваших контроллеров вы можете использовать это, чтобы получить доступ к значению:

 @Get()
async findOne(@RequestContext() requestContext: RequestContextInterface) {
  // do whatever you need to do with it in your controllers
}
 

Ответ №2:

Подход к совместному использованию контекста запроса заключается в использовании пакета Continuation-local storage (CLS), такого как express-http-context. Затем вы можете

  • используйте промежуточное программное обеспечение для установки соответствующих контекстных данных
  • определите поставщика, который обертывает логику CLS
  • внедрите этого поставщика в свои контроллеры
 // request-context.ts
import { createParamDecorator, HttpException, HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import httpContext from 'express-http-context';

@Injectable()
export class RequestContextProvider {
    get(key) {
        return httpContext.get(key)
    }

    set(key, value) {
        return httpContext.set(key, value)
    }
}


@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
    constructor(private requestContextProvider: RequestContextProvider) { }

    use(req: Request, res: Response, next: NextFunction) {

        // first run express-http-context middleware
        httpContext.middleware(req, res, () => {
            // set context data
            // for example extract user data from JWT
            const [, token] = req.headers.authorization.split(' ')
            const decoded: Record<string, unknown> = jwt_decode(token)
            this.requestContextProvider.set('userId', decoded.userId)
            next();
        })
    }
}

 

Промежуточное программное обеспечение Appy в приложении

 //app.module.ts

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { RequestContextMiddleware, RequestContextProvider } from './common/request-context';
import { MyModule } from './my/my.module';

@Module({
    imports: [MyModule],
    providers: [RequestContextProvider]
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(RequestContextMiddleware)
            .forRoutes('*');
    }
}
 

Внедряйте контроллеры, подобные этому

 //my.controller.ts

import { Controller, Post } from '@nestjs/common';

@Controller('my')
export class MyController {
    constructor(private requestContextProvider: RequestContextProvider) { }
    @Post()
    doSomething(): string {
        const userId = this.requestContextProvider.get('userId')
        //  do something with user ID
        //...
    }
}
 

Вы также можете использовать декораторы

 
// request-context.ts

export const UserId = createParamDecorator(() => {
    const userId = httpContext.get('userId')
    if (!userId) {
        throw new HttpException(
            'Missing authorisation credentials',
            HttpStatus.FORBIDDEN
        )
    }
    return userId;
})

 

Полный пример общего контекста запроса NestJS

Ответ №3:

Насколько я понял, вам нужно промежуточное программное обеспечение, которое управляет запросом. Для этого вы можете создать пользовательский перехватчик.

 import {
  UseInterceptors,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';

//  nest will take dto and turn it to the json and send it back as response

// with implement, this class will satisfy either an abstract class or an interface
export class MyCustomInterceptor implements NestInterceptor {
    // passing dto will make this class reusable
    constructor(private dto:any){

    }
  intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {

    // this is where  you run code before request is handled
    return handler.handle().pipe(
      
      map((data: any) => {
         // data is the response
         // here you can manipulate the response
        });
      }),
    );
  }
}
 

вы используете это в контроллере следующим образом:

  import {UseInterceptors} from '@nestjs/common';


// U can manipulate the request for this route
@UseInterceptors(new MyCustomInterceptor(YourDto))
  // each part of the request is string. thats why param is string
  @Get('/:id')
  async findUser(@Param('id') id: string) {
   // logic...
  }
 

Ответ №4:

Я думаю, вы можете использовать пакет nj-request-scope, который я написал. В нем нет проблем с цепочкой впрыскивания и проблемами с производительностью:

https://github.com/kugacz/nj-request-scope

Во-первых, вы должны добавить импорт RequestScopeModule в декоратор класса модуля:

 import { RequestScopeModule } from 'nj-request-scope';

@Module({
    imports: [RequestScopeModule],
})
 

и тогда ваш RequestContext класс может быть написан следующим образом:

 import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';
import { RequestScope } from 'nj-request-scope';
import { NJRS_REQUEST } from 'nj-request-scope';

@Injectable()
@RequestScope()
export class RequestContext {
    public headers: IncomingHttpHeaders;
    ....

    constructor(@Inject(NJRS_REQUEST) private readonly request: Request) {
        this.headers = request.headers;
    }
}
 

В этом случае RequestContext для каждого запроса будет создан новый экземпляр class . Таким образом, вы можете хранить любую информацию о области запроса внутри этого класса. Внедрение этого класса в любую часть вашего приложения не приведет к всплывающей цепочке инъекций.

Другой подход выглядит следующим образом:

 import { Injectable } from '@nestjs/common';
import { Request } from 'express';
import { IncomingHttpHeaders } from 'http';
import { NJRS_REQUEST } from 'nj-request-scope';

@Injectable()
export class RequestContext {

    constructor(@Inject(NJRS_REQUEST) private readonly request: Request) {
    }

    public get headers(): IncomingHttpHeaders {
        return this.request.headers;
    }
}
 

В этом случае RequestContext класс является одноэлементным, а единственным request полем является область запроса. Итак, вы не можете хранить какую-либо информацию, относящуюся к области запроса, непосредственно в RequestContext классе, скорее вам нужно считывать данные, относящиеся к области запроса, непосредственно из объекта запроса с помощью геттеров. Кроме того, внедрение этого класса в любую часть вашего приложения не приведет к всплывающей цепочке инъекций.

Простые примеры использования пакета nj-request-scope вы можете найти здесь: https://github.com/kugacz/nj-request-scope-example