fix(microservices): fix request scoped enhancers #2489

This commit is contained in:
Kamil Myśliwiec
2019-07-02 13:51:33 +02:00
parent 791b1ceaa6
commit 7a93b0d91f
18 changed files with 299 additions and 13 deletions

View File

@@ -0,0 +1,81 @@
import { INestApplication } from '@nestjs/common';
import { Transport } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import * as request from 'supertest';
import { Guard } from '../src/msvc/guards/request-scoped.guard';
import { HelloController } from '../src/msvc/hello.controller';
import { HelloModule } from '../src/msvc/hello.module';
import { Interceptor } from '../src/msvc/interceptors/logging.interceptor';
import { UsersService } from '../src/msvc/users/users.service';
class Meta {
static COUNTER = 0;
constructor() {
Meta.COUNTER++;
}
}
describe('Request scope (microservices)', () => {
let server;
let app: INestApplication;
before(async () => {
const module = await Test.createTestingModule({
imports: [
HelloModule.forRoot({
provide: 'META',
useClass: Meta,
}),
],
}).compile();
app = module.createNestApplication();
app.connectMicroservice({ transport: Transport.TCP });
server = app.getHttpServer();
await app.init();
await app.startAllMicroservicesAsync();
});
describe('when one service is request scoped', () => {
before(async () => {
const performHttpCall = end =>
request(server)
.get('/hello')
.end((err, res) => {
if (err) return end(err);
end();
});
await new Promise(resolve => performHttpCall(resolve));
await new Promise(resolve => performHttpCall(resolve));
await new Promise(resolve => performHttpCall(resolve));
});
it(`should create controller for each request`, async () => {
expect(HelloController.COUNTER).to.be.eql(3);
});
it(`should create service for each request`, async () => {
expect(UsersService.COUNTER).to.be.eql(3);
});
it(`should share static provider across requests`, async () => {
expect(Meta.COUNTER).to.be.eql(1);
});
it(`should create request scoped interceptor for each request`, async () => {
expect(Interceptor.COUNTER).to.be.eql(3);
expect(Interceptor.REQUEST_SCOPED_DATA).to.deep.equal([1, 1, 1]);
});
it(`should create request scoped guard for each request`, async () => {
expect(Guard.COUNTER).to.be.eql(3);
expect(Guard.REQUEST_SCOPED_DATA).to.deep.equal([1, 1, 1]);
});
});
after(async () => {
await app.close();
});
});

View File

@@ -63,14 +63,17 @@ describe('Request scope', () => {
it(`should create request scoped pipe for each request`, async () => {
expect(UserByIdPipe.COUNTER).to.be.eql(3);
expect(UserByIdPipe.REQUEST_SCOPED_DATA).to.deep.equal([1, 1, 1]);
});
it(`should create request scoped interceptor for each request`, async () => {
expect(Interceptor.COUNTER).to.be.eql(3);
expect(Interceptor.REQUEST_SCOPED_DATA).to.deep.equal([1, 1, 1]);
});
it(`should create request scoped guard for each request`, async () => {
expect(Guard.COUNTER).to.be.eql(3);
expect(Guard.REQUEST_SCOPED_DATA).to.deep.equal([1, 1, 1]);
});
});

View File

@@ -1,6 +1,7 @@
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
Scope,
} from '@nestjs/common';
@@ -9,13 +10,16 @@ import { Observable } from 'rxjs';
@Injectable({ scope: Scope.REQUEST })
export class Guard implements CanActivate {
static COUNTER = 0;
constructor() {
static REQUEST_SCOPED_DATA = [];
constructor(@Inject('REQUEST_ID') private requestId: number) {
Guard.COUNTER++;
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
Guard.REQUEST_SCOPED_DATA.push(this.requestId);
return true;
}
}

View File

@@ -1,11 +1,19 @@
import { DynamicModule, Inject, Module, Provider } from '@nestjs/common';
import { DynamicModule, Inject, Module, Provider, Scope } from '@nestjs/common';
import { HelloController } from './hello.controller';
import { HelloService } from './hello.service';
import { UsersService } from './users/users.service';
@Module({
controllers: [HelloController],
providers: [HelloService, UsersService],
providers: [
HelloService,
UsersService,
{
provide: 'REQUEST_ID',
useFactory: () => 1,
scope: Scope.REQUEST,
},
],
})
export class HelloModule {
constructor(@Inject('META') private readonly meta) {}

View File

@@ -1,6 +1,7 @@
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
NestInterceptor,
Scope,
@@ -10,10 +11,14 @@ import { Observable } from 'rxjs';
@Injectable({ scope: Scope.REQUEST })
export class Interceptor implements NestInterceptor {
static COUNTER = 0;
constructor() {
static REQUEST_SCOPED_DATA = [];
constructor(@Inject('REQUEST_ID') private requestId: number) {
Interceptor.COUNTER++;
}
intercept(context: ExecutionContext, call: CallHandler): Observable<any> {
Interceptor.REQUEST_SCOPED_DATA.push(this.requestId);
return call.handle();
}
}

View File

@@ -1,14 +1,25 @@
import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
import {
ArgumentMetadata,
Inject,
Injectable,
PipeTransform,
} from '@nestjs/common';
import { UsersService } from './users.service';
@Injectable()
export class UserByIdPipe implements PipeTransform<string> {
static COUNTER = 0;
constructor(private readonly usersService: UsersService) {
static REQUEST_SCOPED_DATA = [];
constructor(
@Inject('REQUEST_ID') private requestId: number,
private readonly usersService: UsersService,
) {
UserByIdPipe.COUNTER++;
}
transform(value: string, metadata: ArgumentMetadata) {
UserByIdPipe.REQUEST_SCOPED_DATA.push(this.requestId);
return this.usersService.findById(value);
}
}

View File

@@ -0,0 +1,10 @@
import { IsString, IsNotEmpty, IsNumber } from 'class-validator';
export class TestDto {
@IsString()
@IsNotEmpty()
string: string;
@IsNumber()
number: number;
}

View File

@@ -0,0 +1,25 @@
import {
CanActivate,
ExecutionContext,
Inject,
Injectable,
Scope,
} from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable({ scope: Scope.REQUEST })
export class Guard implements CanActivate {
static COUNTER = 0;
static REQUEST_SCOPED_DATA = [];
constructor(@Inject('REQUEST_ID') private requestId: number) {
Guard.COUNTER++;
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
Guard.REQUEST_SCOPED_DATA.push(this.requestId);
return true;
}
}

View File

@@ -0,0 +1,24 @@
import { Controller, UseGuards, UseInterceptors } from '@nestjs/common';
import { MessagePattern } from '@nestjs/microservices';
import { Guard } from './guards/request-scoped.guard';
import { HelloService } from './hello.service';
import { Interceptor } from './interceptors/logging.interceptor';
import { UsersService } from './users/users.service';
@Controller()
export class HelloController {
static COUNTER = 0;
constructor(
private readonly helloService: HelloService,
private readonly usersService: UsersService,
) {
HelloController.COUNTER++;
}
@UseGuards(Guard)
@UseInterceptors(Interceptor)
@MessagePattern('test')
greeting(): string {
return this.helloService.greeting();
}
}

View File

@@ -0,0 +1,28 @@
import { DynamicModule, Inject, Module, Provider, Scope } from '@nestjs/common';
import { HelloController } from './hello.controller';
import { HelloService } from './hello.service';
import { HttpController } from './http.controller';
import { UsersService } from './users/users.service';
@Module({
controllers: [HelloController, HttpController],
providers: [
HelloService,
UsersService,
{
provide: 'REQUEST_ID',
useFactory: () => 1,
scope: Scope.REQUEST,
},
],
})
export class HelloModule {
constructor(@Inject('META') private readonly meta) {}
static forRoot(meta: Provider): DynamicModule {
return {
module: HelloModule,
providers: [meta],
};
}
}

View File

@@ -0,0 +1,10 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class HelloService {
constructor(@Inject('META') private readonly meta) {}
greeting(): string {
return 'Hello world!';
}
}

View File

@@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import { ClientProxyFactory, Transport } from '@nestjs/microservices';
@Controller()
export class HttpController {
@Get('hello')
testMsvc() {
const client = ClientProxyFactory.create({
transport: Transport.TCP,
});
return client.send('test', { test: true });
}
}

View File

@@ -0,0 +1,24 @@
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
NestInterceptor,
Scope,
} from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable({ scope: Scope.REQUEST })
export class Interceptor implements NestInterceptor {
static COUNTER = 0;
static REQUEST_SCOPED_DATA = [];
constructor(@Inject('REQUEST_ID') private requestId: number) {
Interceptor.COUNTER++;
}
intercept(context: ExecutionContext, call: CallHandler): Observable<any> {
Interceptor.REQUEST_SCOPED_DATA.push(this.requestId);
return call.handle();
}
}

View File

@@ -0,0 +1,13 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class UsersService {
static COUNTER = 0;
constructor(@Inject('META') private readonly meta) {
UsersService.COUNTER++;
}
findById(id: string) {
return { id };
}
}

View File

@@ -3,6 +3,7 @@ import { Controller } from '@nestjs/common/interfaces/controllers/controller.int
import { isEmpty } from '@nestjs/common/utils/shared.utils';
import { ApplicationConfig } from '@nestjs/core/application-config';
import { BaseExceptionFilterContext } from '@nestjs/core/exceptions/base-exception-filter-context';
import { STATIC_CONTEXT } from '@nestjs/core/injector/constants';
import { NestContainer } from '@nestjs/core/injector/container';
import { Observable } from 'rxjs';
import { RpcExceptionsHandler } from '../exceptions/rpc-exceptions-handler';
@@ -19,6 +20,8 @@ export class ExceptionFiltersContext extends BaseExceptionFilterContext {
instance: Controller,
callback: <T = any>(data: T) => Observable<any>,
module: string,
contextId = STATIC_CONTEXT,
inquirerId?: string,
): RpcExceptionsHandler {
this.moduleContext = module;
@@ -27,6 +30,8 @@ export class ExceptionFiltersContext extends BaseExceptionFilterContext {
instance,
callback,
EXCEPTION_FILTERS_METADATA,
contextId,
inquirerId,
);
if (isEmpty(filters)) {
return exceptionHandler;

View File

@@ -3,12 +3,13 @@ import { Controller } from '@nestjs/common/interfaces';
import { FORBIDDEN_MESSAGE } from '@nestjs/core/guards/constants';
import { GuardsConsumer } from '@nestjs/core/guards/guards-consumer';
import { GuardsContextCreator } from '@nestjs/core/guards/guards-context-creator';
import { STATIC_CONTEXT } from '@nestjs/core/injector/constants';
import { InterceptorsConsumer } from '@nestjs/core/interceptors/interceptors-consumer';
import { InterceptorsContextCreator } from '@nestjs/core/interceptors/interceptors-context-creator';
import { PipesConsumer } from '@nestjs/core/pipes/pipes-consumer';
import { PipesContextCreator } from '@nestjs/core/pipes/pipes-context-creator';
import { Observable } from 'rxjs';
import { RpcException } from '..';
import { RpcException } from '../exceptions';
import { ExceptionFiltersContext } from './exception-filters-context';
import { RpcProxy } from './rpc-proxy';
@@ -28,19 +29,37 @@ export class RpcContextCreator {
instance: Controller,
callback: (data: any, ...args: any[]) => Observable<any>,
module: string,
contextId = STATIC_CONTEXT,
inquirerId?: string,
): (...args: any[]) => Promise<Observable<any>> {
const exceptionHandler = this.exceptionFiltersContext.create(
instance,
callback,
module,
contextId,
inquirerId,
);
const pipes = this.pipesCreator.create(
instance,
callback,
module,
contextId,
inquirerId,
);
const guards = this.guardsContextCreator.create(
instance,
callback,
module,
contextId,
inquirerId,
);
const pipes = this.pipesCreator.create(instance, callback, module);
const guards = this.guardsContextCreator.create(instance, callback, module);
const metatype = this.getDataMetatype(instance, callback);
const interceptors = this.interceptorsContextCreator.create(
instance,
callback,
module,
contextId,
inquirerId,
);
const fnCanActivate = this.createGuardsFn(guards, instance, callback);
const handler = (args: any[]) => async () => {

View File

@@ -59,7 +59,7 @@ export class ListenersController {
return server.addHandler(pattern, proxy, isEventHandler);
}
const asyncHandler = this.createRequestScopedHandler(
instance,
instanceWrapper,
pattern,
module,
moduleKey,
@@ -91,13 +91,14 @@ export class ListenersController {
}
public createRequestScopedHandler(
instance: Controller,
wrapper: InstanceWrapper,
pattern: PatternMetadata,
module: Module,
moduleKey: string,
methodKey: string,
) {
const collection = module.controllers;
const { instance } = wrapper;
return async (...args: unknown[]) => {
try {
const data = args[0];
@@ -114,6 +115,8 @@ export class ListenersController {
contextInstance,
contextInstance[methodKey],
moduleKey,
contextId,
wrapper.id,
);
return proxy(data);
} catch (err) {

View File

@@ -100,11 +100,11 @@ describe('ListenersController', () => {
controllers: new Map(),
} as any;
const pattern = {};
const controller = { [methodKey]: {} };
const wrapper = new InstanceWrapper({ instance: { [methodKey]: {} } });
it('should delegete error to exception filters', async () => {
const handler = instance.createRequestScopedHandler(
controller,
wrapper,
pattern,
module,
moduleKey,