diff --git a/.circleci/config.yml b/.circleci/config.yml index 8fa94ead55f..ed7d7212934 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -65,11 +65,6 @@ jobs: - store_artifacts: path: coverage - test_node_10: - <<: *unit-tests-template - docker: - - image: circleci/node:10 - test_node_14: <<: *unit-tests-template docker: @@ -174,9 +169,6 @@ workflows: - test_node_12: requires: - build - - test_node_10: - requires: - - build - lint: requires: - build diff --git a/integration/cors/e2e/express.spec.ts b/integration/cors/e2e/express.spec.ts index c13abf4d9a1..8e268c3166e 100644 --- a/integration/cors/e2e/express.spec.ts +++ b/integration/cors/e2e/express.spec.ts @@ -1,10 +1,10 @@ -import { NestFastifyApplication } from '@nestjs/platform-fastify'; +import { NestExpressApplication } from '@nestjs/platform-express'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; describe('Express Cors', () => { - let app: NestFastifyApplication; + let app: NestExpressApplication; const configs = [ { origin: 'example.com', @@ -30,7 +30,7 @@ describe('Express Cors', () => { imports: [AppModule], }).compile(); - app = module.createNestApplication(); + app = module.createNestApplication(); let requestId = 0; const configDelegation = function (req, cb) { @@ -84,7 +84,7 @@ describe('Express Cors', () => { cb(null, config); }; - app = module.createNestApplication(null, { + app = module.createNestApplication({ cors: configDelegation, }); @@ -126,7 +126,7 @@ describe('Express Cors', () => { imports: [AppModule], }).compile(); - app = module.createNestApplication(); + app = module.createNestApplication(); app.enableCors(configs[0]); await app.init(); @@ -153,7 +153,7 @@ describe('Express Cors', () => { imports: [AppModule], }).compile(); - app = module.createNestApplication(null, { + app = module.createNestApplication({ cors: configs[0], }); await app.init(); diff --git a/integration/cors/e2e/fastify.spec.ts b/integration/cors/e2e/fastify.spec.ts index b98d1232848..df8caba4334 100644 --- a/integration/cors/e2e/fastify.spec.ts +++ b/integration/cors/e2e/fastify.spec.ts @@ -1,9 +1,12 @@ -import { NestFastifyApplication } from '@nestjs/platform-fastify'; +import { + FastifyAdapter, + NestFastifyApplication, +} from '@nestjs/platform-fastify'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; -describe('Fastify Cors', () => { +describe.skip('Fastify Cors', () => { let app: NestFastifyApplication; const configs = [ { @@ -30,7 +33,9 @@ describe('Fastify Cors', () => { imports: [AppModule], }).compile(); - app = module.createNestApplication(); + app = module.createNestApplication( + new FastifyAdapter(), + ); let requestId = 0; const configDelegation = function (req, cb) { @@ -84,9 +89,12 @@ describe('Fastify Cors', () => { cb(null, config); }; - app = module.createNestApplication(null, { - cors: configDelegation, - }); + app = module.createNestApplication( + new FastifyAdapter(), + { + cors: configDelegation, + }, + ); await app.init(); }); @@ -127,7 +135,9 @@ describe('Fastify Cors', () => { imports: [AppModule], }).compile(); - app = module.createNestApplication(); + app = module.createNestApplication( + new FastifyAdapter(), + ); app.enableCors(configs[0]); await app.init(); @@ -147,16 +157,19 @@ describe('Fastify Cors', () => { after(async () => { await app.close(); }); - + describe('Application Options', () => { before(async () => { const module = await Test.createTestingModule({ imports: [AppModule], }).compile(); - app = module.createNestApplication(null, { - cors: configs[0], - }); + app = module.createNestApplication( + new FastifyAdapter(), + { + cors: configs[0], + }, + ); await app.init(); }); diff --git a/integration/microservices/e2e/disconnected-client.spec.ts b/integration/microservices/e2e/disconnected-client.spec.ts index 39feaeb3452..35b65dfb6b3 100644 --- a/integration/microservices/e2e/disconnected-client.spec.ts +++ b/integration/microservices/e2e/disconnected-client.spec.ts @@ -34,7 +34,7 @@ describe('Disconnected client', () => { .send({ transport: Transport.REDIS, options: { - url: 'redis://localhost:3333', + port: '3333', }, }) .expect(408); diff --git a/integration/microservices/src/disconnected.controller.ts b/integration/microservices/src/disconnected.controller.ts index 4239fbeb65e..0212ab1ce45 100644 --- a/integration/microservices/src/disconnected.controller.ts +++ b/integration/microservices/src/disconnected.controller.ts @@ -3,7 +3,7 @@ import { Controller, InternalServerErrorException, Post, - RequestTimeoutException, + RequestTimeoutException } from '@nestjs/common'; import { ClientProxyFactory } from '@nestjs/microservices'; import { Observable, throwError } from 'rxjs'; @@ -24,7 +24,8 @@ export class DisconnectedClientController { return throwError(() => code === 'ECONNREFUSED' || code === 'CONN_ERR' || - code === 'CONNECTION_REFUSED' + code === 'CONNECTION_REFUSED' || + error.message === 'Connection is closed.' ? new RequestTimeoutException('ECONNREFUSED') : new InternalServerErrorException(), ); diff --git a/integration/module-utils/src/integration.module-definition.ts b/integration/module-utils/src/integration.module-definition.ts new file mode 100644 index 00000000000..89d7a984241 --- /dev/null +++ b/integration/module-utils/src/integration.module-definition.ts @@ -0,0 +1,17 @@ +import { ConfigurableModuleBuilder } from '@nestjs/common'; +import { IntegrationModuleOptions } from './interfaces/integration-module-options.interface'; + +export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = + new ConfigurableModuleBuilder() + .setClassMethodName('forRoot') + .setFactoryMethodName('construct') + .setExtras( + { + isGlobal: true, + }, + (definition, extras) => ({ + ...definition, + global: extras.isGlobal, + }), + ) + .build(); diff --git a/integration/module-utils/src/integration.module.ts b/integration/module-utils/src/integration.module.ts new file mode 100644 index 00000000000..3b994f4bd4f --- /dev/null +++ b/integration/module-utils/src/integration.module.ts @@ -0,0 +1,16 @@ +import { Inject, Module } from '@nestjs/common'; +import { + ConfigurableModuleClass, + MODULE_OPTIONS_TOKEN, +} from './integration.module-definition'; +import { IntegrationModuleOptions } from './interfaces/integration-module-options.interface'; + +@Module({}) +export class IntegrationModule extends ConfigurableModuleClass { + constructor( + @Inject(MODULE_OPTIONS_TOKEN) + public readonly options: IntegrationModuleOptions, + ) { + super(); + } +} diff --git a/integration/module-utils/src/interfaces/integration-module-options.interface.ts b/integration/module-utils/src/interfaces/integration-module-options.interface.ts new file mode 100644 index 00000000000..802e02076fa --- /dev/null +++ b/integration/module-utils/src/interfaces/integration-module-options.interface.ts @@ -0,0 +1,4 @@ +export interface IntegrationModuleOptions { + url: string; + secure?: boolean; +} diff --git a/integration/module-utils/test/integration-module.spec.ts b/integration/module-utils/test/integration-module.spec.ts new file mode 100644 index 00000000000..ea22a71c571 --- /dev/null +++ b/integration/module-utils/test/integration-module.spec.ts @@ -0,0 +1,47 @@ +import { Test } from '@nestjs/testing'; +import { expect } from 'chai'; +import { IntegrationModule } from '../src/integration.module'; + +describe('Module utils (ConfigurableModuleBuilder)', () => { + it('should auto-generate "forRoot" method', async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + IntegrationModule.forRoot({ + isGlobal: true, + url: 'test_url', + secure: false, + }), + ], + }).compile(); + + const integrationModule = moduleRef.get(IntegrationModule); + + expect(integrationModule.options).to.deep.equal({ + url: 'test_url', + secure: false, + }); + }); + + it('should auto-generate "forRootAsync" method', async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + IntegrationModule.forRootAsync({ + isGlobal: true, + useFactory: () => { + return { + url: 'test_url', + secure: false, + }; + }, + }), + ], + }).compile(); + + const integrationModule = moduleRef.get(IntegrationModule); + + expect(integrationModule.options).to.deep.equal({ + url: 'test_url', + secure: false, + }); + }); +}); diff --git a/integration/module-utils/tsconfig.json b/integration/module-utils/tsconfig.json new file mode 100644 index 00000000000..c6354c56487 --- /dev/null +++ b/integration/module-utils/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es6", + "sourceMap": true, + "allowJs": true, + "outDir": "./dist" + }, + "include": [ + "src/**/*", + "e2e/**/*" + ], + "exclude": [ + "node_modules", + ] +} \ No newline at end of file diff --git a/integration/nest-application/raw-body/e2e/express.spec.ts b/integration/nest-application/raw-body/e2e/express.spec.ts index 910a0785c2b..bc4501f3ed4 100644 --- a/integration/nest-application/raw-body/e2e/express.spec.ts +++ b/integration/nest-application/raw-body/e2e/express.spec.ts @@ -13,10 +13,9 @@ describe('Raw body (Express Application)', () => { imports: [ExpressModule], }).compile(); - app = moduleFixture.createNestApplication( - undefined, - { rawBody: true }, - ); + app = moduleFixture.createNestApplication({ + rawBody: true, + }); }); it('should return exact post body', async () => { diff --git a/integration/repl/e2e/repl.spec.ts b/integration/repl/e2e/repl.spec.ts new file mode 100644 index 00000000000..03693f29600 --- /dev/null +++ b/integration/repl/e2e/repl.spec.ts @@ -0,0 +1,262 @@ +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { repl } from '@nestjs/core'; +import { ReplContext } from '@nestjs/core/repl/repl-context'; +import { + HelpReplFn, + GetReplFn, + ResolveReplFn, + SelectReplFn, + DebugReplFn, + MethodsReplFn, +} from '@nestjs/core/repl/native-functions'; +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { AppModule } from '../src/app.module'; + +const PROMPT = '\u001b[1G\u001b[0J> \u001b[3G'; + +describe('REPL', () => { + beforeEach(() => { + // To avoid coloring the output: + sinon.stub(clc, 'bold').callsFake(text => text); + sinon.stub(clc, 'green').callsFake(text => text); + sinon.stub(clc, 'yellow').callsFake(text => text); + sinon.stub(clc, 'red').callsFake(text => text); + sinon.stub(clc, 'magentaBright').callsFake(text => text); + sinon.stub(clc, 'cyanBright').callsFake(text => text); + }); + afterEach(() => { + sinon.restore(); + }); + + it('get()', async () => { + const server = await repl(AppModule); + server.context; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + server.emit('line', 'get(UsersService)'); + + expect(outputText).to.equal( + `UsersService { usersRepository: UsersRepository {} } +${PROMPT}`, + ); + + outputText = ''; + server.emit('line', 'get(UsersService).findAll()'); + + expect(outputText).to + .equal(`\u001b[32m'This action returns all users'\u001b[39m +${PROMPT}`); + + outputText = ''; + server.emit('line', 'get(UsersRepository)'); + + expect(outputText).to.equal(`UsersRepository {} +${PROMPT}`); + }); + + it('$()', async () => { + const server = await repl(AppModule); + server.context; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + server.emit('line', '$(UsersService)'); + + expect(outputText).to.equal( + `UsersService { usersRepository: UsersRepository {} } +${PROMPT}`, + ); + + outputText = ''; + server.emit('line', '$(UsersService).findAll()'); + + expect(outputText).to + .equal(`\u001b[32m'This action returns all users'\u001b[39m +${PROMPT}`); + + outputText = ''; + server.emit('line', '$(UsersRepository)'); + + expect(outputText).to.equal(`UsersRepository {} +${PROMPT}`); + }); + + it('debug()', async () => { + const server = await repl(AppModule); + + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + server.emit('line', 'debug(UsersModule)'); + + expect(outputText).to.equal( + ` +UsersModule: + - controllers: + ◻ UsersController + - providers: + ◻ UsersService + ◻ UsersRepository + +${PROMPT}`, + ); + }); + + it('methods()', async () => { + const server = await repl(AppModule); + + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + server.emit('line', 'methods(UsersRepository)'); + + expect(outputText).to.equal( + ` +Methods: + ◻ find + +${PROMPT}`, + ); + + outputText = ''; + server.emit('line', 'methods(UsersService)'); + + expect(outputText).to.equal( + ` +Methods: + ◻ create + ◻ findAll + ◻ findOne + ◻ update + ◻ remove + +${PROMPT}`, + ); + }); + + describe('.help', () => { + it(`Typing "help.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new HelpReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'help.help'); + + expect(outputText).to.equal(`${description} +Interface: help${signature} +${PROMPT}`); + }); + + it(`Typing "get.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new GetReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'get.help'); + + expect(outputText).to.equal(`${description} +Interface: get${signature} +${PROMPT}`); + }); + + it(`Typing "resolve.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new ResolveReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'resolve.help'); + + expect(outputText).to.equal(`${description} +Interface: resolve${signature} +${PROMPT}`); + }); + + it(`Typing "select.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new SelectReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'select.help'); + + expect(outputText).to.equal(`${description} +Interface: select${signature} +${PROMPT}`); + }); + + it(`Typing "debug.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new DebugReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'debug.help'); + + expect(outputText).to.equal(`${description} +Interface: debug${signature} +${PROMPT}`); + }); + + it(`Typing "methods.help" should print function's description and interface`, async () => { + const replServer = await repl(AppModule); + + const { description, signature } = new MethodsReplFn( + sinon.stub() as unknown as ReplContext, + ).fnDefinition; + let outputText = ''; + sinon.stub(process.stdout, 'write').callsFake(text => { + outputText += text; + return true; + }); + + replServer.emit('line', 'methods.help'); + + expect(outputText).to.equal(`${description} +Interface: methods${signature} +${PROMPT}`); + }); + }); +}); diff --git a/integration/repl/src/app.module.ts b/integration/repl/src/app.module.ts new file mode 100644 index 00000000000..867c4b1b3c5 --- /dev/null +++ b/integration/repl/src/app.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { UsersModule } from './users/users.module'; + +@Module({ + imports: [UsersModule], +}) +export class AppModule {} diff --git a/integration/repl/src/users/dto/create-user.dto.ts b/integration/repl/src/users/dto/create-user.dto.ts new file mode 100644 index 00000000000..0311be1384d --- /dev/null +++ b/integration/repl/src/users/dto/create-user.dto.ts @@ -0,0 +1 @@ +export class CreateUserDto {} diff --git a/integration/repl/src/users/dto/update-user.dto.ts b/integration/repl/src/users/dto/update-user.dto.ts new file mode 100644 index 00000000000..dfd37fb1edb --- /dev/null +++ b/integration/repl/src/users/dto/update-user.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateUserDto } from './create-user.dto'; + +export class UpdateUserDto extends PartialType(CreateUserDto) {} diff --git a/integration/repl/src/users/entities/user.entity.ts b/integration/repl/src/users/entities/user.entity.ts new file mode 100644 index 00000000000..4f82c14571c --- /dev/null +++ b/integration/repl/src/users/entities/user.entity.ts @@ -0,0 +1 @@ +export class User {} diff --git a/integration/repl/src/users/users.controller.ts b/integration/repl/src/users/users.controller.ts new file mode 100644 index 00000000000..3eca7ebdeed --- /dev/null +++ b/integration/repl/src/users/users.controller.ts @@ -0,0 +1,34 @@ +import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; + +@Controller('users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Post() + create(@Body() createUserDto: CreateUserDto) { + return this.usersService.create(createUserDto); + } + + @Get() + findAll() { + return this.usersService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.usersService.findOne(+id); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { + return this.usersService.update(+id, updateUserDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.usersService.remove(+id); + } +} diff --git a/integration/repl/src/users/users.module.ts b/integration/repl/src/users/users.module.ts new file mode 100644 index 00000000000..86d73a3f0e4 --- /dev/null +++ b/integration/repl/src/users/users.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersRepository } from './users.repository'; +import { UsersService } from './users.service'; + +@Module({ + controllers: [UsersController], + providers: [ + UsersService, + { + provide: UsersRepository.name, + useValue: new UsersRepository(), + }, + ], +}) +export class UsersModule {} diff --git a/integration/repl/src/users/users.repository.ts b/integration/repl/src/users/users.repository.ts new file mode 100644 index 00000000000..4a616658654 --- /dev/null +++ b/integration/repl/src/users/users.repository.ts @@ -0,0 +1,8 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UsersRepository { + async find() { + return [{ id: 1, email: 'test@nestjs.com' }]; + } +} diff --git a/integration/repl/src/users/users.service.ts b/integration/repl/src/users/users.service.ts new file mode 100644 index 00000000000..ab3191152d8 --- /dev/null +++ b/integration/repl/src/users/users.service.ts @@ -0,0 +1,32 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { CreateUserDto } from './dto/create-user.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { UsersRepository } from './users.repository'; + +@Injectable() +export class UsersService { + constructor( + @Inject('UsersRepository') + private readonly usersRepository: UsersRepository, + ) {} + + create(createUserDto: CreateUserDto) { + return 'This action adds a new user'; + } + + findAll() { + return `This action returns all users`; + } + + findOne(id: number) { + return `This action returns a #${id} user`; + } + + update(id: number, updateUserDto: UpdateUserDto) { + return `This action updates a #${id} user`; + } + + remove(id: number) { + return `This action removes a #${id} user`; + } +} diff --git a/integration/repl/tsconfig.json b/integration/repl/tsconfig.json new file mode 100644 index 00000000000..ea15af490a5 --- /dev/null +++ b/integration/repl/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": false, + "noImplicitAny": false, + "removeComments": true, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es6", + "sourceMap": true, + "allowJs": true, + "outDir": "./dist" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "**/*.spec.ts" + ] +} \ No newline at end of file diff --git a/integration/scopes/e2e/durable-providers.spec.ts b/integration/scopes/e2e/durable-providers.spec.ts new file mode 100644 index 00000000000..306d63aecc4 --- /dev/null +++ b/integration/scopes/e2e/durable-providers.spec.ts @@ -0,0 +1,76 @@ +import { INestApplication } from '@nestjs/common'; +import { ContextIdFactory } from '@nestjs/core'; +import { Test } from '@nestjs/testing'; +import { expect } from 'chai'; +import * as request from 'supertest'; +import { DurableContextIdStrategy } from '../src/durable/durable-context-id.strategy'; +import { DurableModule } from '../src/durable/durable.module'; + +describe('Durable providers', () => { + let server: any; + let app: INestApplication; + + before(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [DurableModule], + }).compile(); + + app = moduleRef.createNestApplication(); + server = app.getHttpServer(); + await app.init(); + + ContextIdFactory.apply(new DurableContextIdStrategy()); + }); + + describe('when service is durable', () => { + const performHttpCall = (tenantId: number, end: (err?: any) => void) => + request(server) + .get('/durable') + .set({ ['x-tenant-id']: tenantId }) + .end((err, res) => { + if (err) return end(err); + end(res); + }); + + it(`should share durable providers per tenant`, async () => { + let result: request.Response; + result = await new Promise(resolve => + performHttpCall(1, resolve), + ); + expect(result.text).equal('Hello world! Counter: 1'); + + result = await new Promise(resolve => + performHttpCall(1, resolve), + ); + expect(result.text).equal('Hello world! Counter: 2'); + + result = await new Promise(resolve => + performHttpCall(1, resolve), + ); + expect(result.text).equal('Hello world! Counter: 3'); + }); + + it(`should create per-tenant DI sub-tree`, async () => { + let result: request.Response; + result = await new Promise(resolve => + performHttpCall(4, resolve), + ); + expect(result.text).equal('Hello world! Counter: 1'); + + result = await new Promise(resolve => + performHttpCall(5, resolve), + ); + expect(result.text).equal('Hello world! Counter: 1'); + + result = await new Promise(resolve => + performHttpCall(6, resolve), + ); + expect(result.text).equal('Hello world! Counter: 1'); + }); + }); + + after(async () => { + ContextIdFactory['strategy'] = undefined; + await app.close(); + }); +}); diff --git a/integration/scopes/src/durable/durable-context-id.strategy.ts b/integration/scopes/src/durable/durable-context-id.strategy.ts new file mode 100644 index 00000000000..ad58553b3b1 --- /dev/null +++ b/integration/scopes/src/durable/durable-context-id.strategy.ts @@ -0,0 +1,21 @@ +import { ContextId, ContextIdStrategy, HostComponentInfo } from '@nestjs/core'; +import { Request } from 'express'; + +const tenants = new Map(); + +export class DurableContextIdStrategy implements ContextIdStrategy { + attach(contextId: ContextId, request: Request) { + const tenantId = request.headers['x-tenant-id'] as string; + let tenantSubTreeId: ContextId; + + if (tenants.has(tenantId)) { + tenantSubTreeId = tenants.get(tenantId); + } else { + tenantSubTreeId = { id: +tenantId } as ContextId; + tenants.set(tenantId, tenantSubTreeId); + } + + return (info: HostComponentInfo) => + info.isTreeDurable ? tenantSubTreeId : contextId; + } +} diff --git a/integration/scopes/src/durable/durable.controller.ts b/integration/scopes/src/durable/durable.controller.ts new file mode 100644 index 00000000000..0800381c0de --- /dev/null +++ b/integration/scopes/src/durable/durable.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { DurableService } from './durable.service'; + +@Controller('durable') +export class DurableController { + constructor(private readonly durableService: DurableService) {} + + @Get() + greeting(): string { + return this.durableService.greeting(); + } +} diff --git a/integration/scopes/src/durable/durable.module.ts b/integration/scopes/src/durable/durable.module.ts new file mode 100644 index 00000000000..ed312900f28 --- /dev/null +++ b/integration/scopes/src/durable/durable.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { DurableController } from './durable.controller'; +import { DurableService } from './durable.service'; + +@Module({ + controllers: [DurableController], + providers: [DurableService], +}) +export class DurableModule {} diff --git a/integration/scopes/src/durable/durable.service.ts b/integration/scopes/src/durable/durable.service.ts new file mode 100644 index 00000000000..84639a21539 --- /dev/null +++ b/integration/scopes/src/durable/durable.service.ts @@ -0,0 +1,11 @@ +import { Injectable, Scope } from '@nestjs/common'; + +@Injectable({ scope: Scope.REQUEST, durable: true }) +export class DurableService { + public instanceCounter = 0; + + greeting() { + ++this.instanceCounter; + return `Hello world! Counter: ${this.instanceCounter}`; + } +} diff --git a/integration/websockets/e2e/ws-gateway.spec.ts b/integration/websockets/e2e/ws-gateway.spec.ts index bea1f5c2a82..973e7c8390a 100644 --- a/integration/websockets/e2e/ws-gateway.spec.ts +++ b/integration/websockets/e2e/ws-gateway.spec.ts @@ -72,7 +72,7 @@ describe('WebSocketGateway (WsAdapter)', () => { it(`should handle message on a different path`, async () => { app = await createNestApp(WsPathGateway); - await app.listenAsync(3000); + await app.listen(3000); try { ws = new WebSocket('ws://localhost:3000/ws-path'); await new Promise((resolve, reject) => { @@ -104,7 +104,7 @@ describe('WebSocketGateway (WsAdapter)', () => { this.retries(10); app = await createNestApp(ExamplePathGateway, WsPathGateway2); - await app.listenAsync(3000); + await app.listen(3000); // open websockets delay await new Promise(resolve => setTimeout(resolve, 1000)); diff --git a/package-lock.json b/package-lock.json index 8e063823b25..acc64be5f22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@nestjs/core", - "version": "8.4.4", + "version": "9.0.0-next.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -35,16 +35,6 @@ "tslib": "^2.3.0" } }, - "graphql-tag": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", - "dev": true, - "optional": true, - "requires": { - "tslib": "^2.1.0" - } - }, "symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -127,9 +117,9 @@ }, "dependencies": { "lru-cache": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", - "integrity": "sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.12.0.tgz", + "integrity": "sha512-OIP3DwzRZDfLg9B9VP/huWBlpvbkmbfiBy8xmsXp4RPmE4A3MhwNozc5ZJ3fWnSg8fDcdlE/neRTPG2ycEKliw==", "dev": true } } @@ -182,9 +172,9 @@ } }, "@apollographql/apollo-tools": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.3.tgz", - "integrity": "sha512-VcsXHfTFoCodDAgJZxN04GdFK1kqOhZQnQY/9Fa147P+I8xfvOSz5d+lKAPB+hwSgBNyd7ncAKGIs4+utbL+yA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@apollographql/apollo-tools/-/apollo-tools-0.5.4.tgz", + "integrity": "sha512-shM3q7rUbNyXVVRkQJQseXv6bnYM3BUma/eZhwXR4xsuM+bqWnJKvW7SAfRjP7LuSCocrexa5AXhjjawNHrIlw==", "dev": true }, "@apollographql/graphql-playground-html": { @@ -1392,12 +1382,59 @@ } }, "@fastify/ajv-compiler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-1.1.0.tgz", - "integrity": "sha512-gvCOUNpXsWrIQ3A4aXCLIdblL0tDq42BG/2Xw7oxbil9h11uow10ztS2GuFazNBfjbrsZ5nl+nPl5jDSjj5TSg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.1.0.tgz", + "integrity": "sha512-+hRMMxcUmdqtnCGPwrI2yczFdlgp3IBR88WlPLimXlgRb8vHBTXz38I17R/9ui+hIt9jx0uOdZKOis77VooHfA==", "dev": true, "requires": { - "ajv": "^6.12.6" + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^1.0.1" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "fast-uri": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-1.0.1.tgz", + "integrity": "sha512-dbO/+ny6lX4tt7pvfPMTiHfQVR5igYKFa5BJ2a21TWuOgd2ySp5DYswsEGuMcJZLL3/eJ/MQJ5KNcXyNUvDt8w==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "@fastify/busboy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.0.0.tgz", + "integrity": "sha512-tzTXX1TFEjWCseEsNdIlXXkD+48uJoN+zpqIojUX4pSoMscsbhO/UuVEB5SzJucexqDWOo2ma0ECwdD7hZdrzg==", + "dev": true, + "requires": { + "text-decoding": "^1.0.0" + } + }, + "@fastify/cors": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.0.0.tgz", + "integrity": "sha512-mB2GsA7aVwq7XG6B2OM1FMpcaiXY69ZbM1h/xDJxLEVu5ITGcs5XYrBIYTMNU2dQtzO6mzXhGd2dEKaCnB7UgQ==", + "dev": true, + "requires": { + "fastify-plugin": "^3.0.0", + "vary": "^1.1.2" } }, "@fastify/error": { @@ -1406,6 +1443,262 @@ "integrity": "sha512-wI3fpfDT0t7p8E6dA2eTECzzOd+bZsZCJ2Hcv+Onn2b7ZwK3RwD27uW2QDaMtQhAfWQQP+WNK7nKf0twLsBf9w==", "dev": true }, + "@fastify/fast-json-stringify-compiler": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-3.0.1.tgz", + "integrity": "sha512-X9BL9/N7827M9UTBVsa5G3xOoD3MQ6EqX+D6EyJyF8LdvWTHQJ//BDN4FAEaGZUA2sL+GEMC6+KNjHESnPwQuw==", + "dev": true, + "requires": { + "fast-json-stringify": "^4.2.0" + }, + "dependencies": { + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "fast-json-stringify": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-4.2.0.tgz", + "integrity": "sha512-9RWBl82H7jwnPlkZ/ghi0VD5OFZVdwgwVui0nYzjnXbPQxJ3ES1+SQcWIoeCJOgrY7JkBkY/69UNZSroFPDRdQ==", + "dev": true, + "requires": { + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "deepmerge": "^4.2.2", + "fast-uri": "^2.0.0", + "rfdc": "^1.2.0", + "string-similarity": "^4.0.1" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, + "@fastify/formbody": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-7.0.1.tgz", + "integrity": "sha512-CY6IfzdtidHbZezyyXv7u9dzmb2Lv92HyOZDqANuFb++5ojsqoqIb8bJz11bSgPK0MDoqww/dH6DxZDMM8N4ng==", + "dev": true, + "requires": { + "fastify-plugin": "^3.0.0" + } + }, + "@fastify/middie": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@fastify/middie/-/middie-8.0.0.tgz", + "integrity": "sha512-SsZUzJwRV2IBhko8TNI5gGzUdUp2Xd0XCrU+pBTfsMN8LYGsksDI/Hb3qcUZ2/Kfg6ecbFEeRO4nZmHeFCDpHQ==", + "dev": true, + "requires": { + "fastify-plugin": "^3.0.0", + "path-to-regexp": "^6.1.0", + "reusify": "^1.0.4" + }, + "dependencies": { + "path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", + "dev": true + } + } + }, + "@fastify/multipart": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@fastify/multipart/-/multipart-6.0.0.tgz", + "integrity": "sha512-TwxPH9jE3bEaCdMD1Xqm2YS1aelgJxcNmA/uYAPCzqnVEylDiKCmxCstGulb1W5WdMoyqD5LBGm7AoqDwWTCWQ==", + "dev": true, + "requires": { + "@fastify/busboy": "^1.0.0", + "@fastify/error": "^2.0.0", + "deepmerge": "^4.2.2", + "end-of-stream": "^1.4.4", + "fastify-plugin": "^3.0.0", + "hexoid": "^1.0.0", + "secure-json-parse": "^2.4.0", + "stream-wormhole": "^1.1.0" + }, + "dependencies": { + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + } + } + }, + "@fastify/static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-5.0.0.tgz", + "integrity": "sha512-GGltJkO0idXa7yCZ0PfdTZ6qokWDX/vigCvmRpjOU2A3jc93c9p+oHDvHmwHK60hwWoBEGqHjGofVyn3H1CjZg==", + "dev": true, + "requires": { + "content-disposition": "^0.5.3", + "encoding-negotiator": "^2.0.1", + "fastify-plugin": "^3.0.0", + "glob": "^7.1.4", + "p-limit": "^3.1.0", + "readable-stream": "^3.4.0", + "send": "^0.17.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } + } + }, "@gar/promisify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", @@ -1413,109 +1706,79 @@ "dev": true }, "@graphql-tools/merge": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.6.tgz", - "integrity": "sha512-dkwTm4czMISi/Io47IVvq2Fl9q4TIGKpJ0VZjuXYdEFkECyH6A5uwxZfPVandZG+gQs8ocFFoa6RisiUJLZrJw==", + "version": "8.2.13", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.13.tgz", + "integrity": "sha512-lhzjCa6wCthOYl7B6UzER3SGjU2WjSGnW0WGr8giMYsrtf6G3vIRotMcSVMlhDzyyMIOn7uPULOUt3/kaJ/rIA==", "dev": true, "requires": { - "@graphql-tools/utils": "8.6.5", - "tslib": "~2.3.0" - }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } + "@graphql-tools/utils": "8.6.12", + "tslib": "~2.4.0" } }, "@graphql-tools/mock": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/@graphql-tools/mock/-/mock-8.6.3.tgz", - "integrity": "sha512-Nv095DXWz5Xt6U3SHhkIVtfuWyBNW1yiQk1o+DgM8/4Vj+v6zic1/y3eUpmoiy/qKu3qgagA6mxI8ZU/W6VS8w==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/mock/-/mock-8.7.0.tgz", + "integrity": "sha512-K/hqP442mXAvW36v/3TmqFpNzRw14P86xlsJZod88OXwpDfb97X09z1QsaMcvSe8E7ijcKWLlTRk15/vDQSL2Q==", "dev": true, "requires": { - "@graphql-tools/schema": "8.3.5", - "@graphql-tools/utils": "8.6.5", + "@graphql-tools/schema": "8.5.0", + "@graphql-tools/utils": "8.8.0", "fast-json-stable-stringify": "^2.1.0", - "tslib": "~2.3.0" + "tslib": "^2.4.0" }, "dependencies": { "@graphql-tools/merge": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.6.tgz", - "integrity": "sha512-dkwTm4czMISi/Io47IVvq2Fl9q4TIGKpJ0VZjuXYdEFkECyH6A5uwxZfPVandZG+gQs8ocFFoa6RisiUJLZrJw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.0.tgz", + "integrity": "sha512-xRa7RAQok/0DD2YnjuqikMrr7dUAxTpdGtZ7BkvUUGhYs3B3p7reCAfvOVr1DJAqVToP7hdlMk+S5+Ylk+AaqA==", "dev": true, "requires": { - "@graphql-tools/utils": "8.6.5", - "tslib": "~2.3.0" + "@graphql-tools/utils": "8.8.0", + "tslib": "^2.4.0" } }, "@graphql-tools/schema": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.5.tgz", - "integrity": "sha512-3mJ/K7TdL+fnEUtCUqF4qkh1fcNMzaxgwKgO9fSYSTS7zyT16hbi5XSulSTshygHgaD2u+MO588iR4ZJcbZcIg==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.5.0.tgz", + "integrity": "sha512-VeFtKjM3SA9/hCJJfr95aEdC3G0xIKM9z0Qdz4i+eC1g2fdZYnfWFt2ucW4IME+2TDd0enHlKzaV0qk2SLVUww==", "dev": true, "requires": { - "@graphql-tools/merge": "8.2.6", - "@graphql-tools/utils": "8.6.5", - "tslib": "~2.3.0", + "@graphql-tools/merge": "8.3.0", + "@graphql-tools/utils": "8.8.0", + "tslib": "^2.4.0", "value-or-promise": "1.0.11" } }, "@graphql-tools/utils": { - "version": "8.6.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.5.tgz", - "integrity": "sha512-mjOtaWiS2WIqRz/cq5gaeM3sVrllcu2xbtHROw1su1v3xWa3D3dKgn8Lrl7+tvWs5WUVySsBss/VZ3WdoPkCrA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.8.0.tgz", + "integrity": "sha512-KJrtx05uSM/cPYFdTnGAS1doL5bftJLAiFCDMZ8Vkifztz3BFn3gpFiy/o4wDtM8s39G46mxmt2Km/RmeltfGw==", "dev": true, "requires": { - "tslib": "~2.3.0" + "tslib": "^2.4.0" } - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true } } }, "@graphql-tools/schema": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.5.tgz", - "integrity": "sha512-3mJ/K7TdL+fnEUtCUqF4qkh1fcNMzaxgwKgO9fSYSTS7zyT16hbi5XSulSTshygHgaD2u+MO588iR4ZJcbZcIg==", + "version": "8.3.13", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.13.tgz", + "integrity": "sha512-e+bx1VHj1i5v4HmhCYCar0lqdoLmkRi/CfV07rTqHR6CRDbIb/S/qDCajHLt7FCovQ5ozlI5sRVbBhzfq5H0PQ==", "dev": true, "requires": { - "@graphql-tools/merge": "8.2.6", - "@graphql-tools/utils": "8.6.5", - "tslib": "~2.3.0", + "@graphql-tools/merge": "8.2.13", + "@graphql-tools/utils": "8.6.12", + "tslib": "~2.4.0", "value-or-promise": "1.0.11" - }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } } }, "@graphql-tools/utils": { - "version": "8.6.5", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.5.tgz", - "integrity": "sha512-mjOtaWiS2WIqRz/cq5gaeM3sVrllcu2xbtHROw1su1v3xWa3D3dKgn8Lrl7+tvWs5WUVySsBss/VZ3WdoPkCrA==", + "version": "8.6.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.12.tgz", + "integrity": "sha512-WQ91O40RC+UJgZ9K+IzevSf8oolR1QE+WQ21Oyc2fgDYYiqT0eSf+HVyhZr/8x9rVjn3N9HeqCsywbdmbljg0w==", "dev": true, "requires": { - "tslib": "~2.3.0" - }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } + "tslib": "~2.4.0" } }, "@graphql-typed-document-node/core": { @@ -1679,6 +1942,12 @@ "integrity": "sha512-ePDxG9UuU9Kobk90ZUjtmDW8IT9U7aRb1/Rl9683MRNM+ur0ocHL2v7TPH2ajTiVSBUFbbeW8vKIt9jrb0JIAA==", "dev": true }, + "@ioredis/commands": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.1.1.tgz", + "integrity": "sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg==", + "dev": true + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1822,37 +2091,6 @@ "ws": "8.7.0" }, "dependencies": { - "@graphql-tools/merge": { - "version": "8.2.13", - "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.13.tgz", - "integrity": "sha512-lhzjCa6wCthOYl7B6UzER3SGjU2WjSGnW0WGr8giMYsrtf6G3vIRotMcSVMlhDzyyMIOn7uPULOUt3/kaJ/rIA==", - "dev": true, - "requires": { - "@graphql-tools/utils": "8.6.12", - "tslib": "~2.4.0" - } - }, - "@graphql-tools/schema": { - "version": "8.3.13", - "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.13.tgz", - "integrity": "sha512-e+bx1VHj1i5v4HmhCYCar0lqdoLmkRi/CfV07rTqHR6CRDbIb/S/qDCajHLt7FCovQ5ozlI5sRVbBhzfq5H0PQ==", - "dev": true, - "requires": { - "@graphql-tools/merge": "8.2.13", - "@graphql-tools/utils": "8.6.12", - "tslib": "~2.4.0", - "value-or-promise": "1.0.11" - } - }, - "@graphql-tools/utils": { - "version": "8.6.12", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.12.tgz", - "integrity": "sha512-WQ91O40RC+UJgZ9K+IzevSf8oolR1QE+WQ21Oyc2fgDYYiqT0eSf+HVyhZr/8x9rVjn3N9HeqCsywbdmbljg0w==", - "dev": true, - "requires": { - "tslib": "~2.4.0" - } - }, "anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -1932,15 +2170,6 @@ "is-glob": "^4.0.1" } }, - "graphql-tag": { - "version": "2.12.6", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", - "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", - "dev": true, - "requires": { - "tslib": "^2.1.0" - } - }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2188,7 +2417,7 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "dev": true }, "@protobufjs/base64": { @@ -2206,13 +2435,13 @@ "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", "dev": true }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dev": true, "requires": { "@protobufjs/aspromise": "^1.1.1", @@ -2222,31 +2451,31 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "dev": true }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", "dev": true }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", "dev": true }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", "dev": true }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "dev": true }, "@sindresorhus/is": { @@ -2761,9 +2990,9 @@ } }, "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "dev": true }, "@types/mime": { @@ -2935,9 +3164,9 @@ "dev": true }, "@types/whatwg-url": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.1.tgz", - "integrity": "sha512-2YubE1sjj5ifxievI5Ge1sckb9k/Er66HyR2c+3+I6VDUUg1TLPdYYTEbQ+DjRkS4nTxMJhgWfSfMRD2sl2EYQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", "dev": true, "requires": { "@types/node": "*", @@ -3203,26 +3432,31 @@ "dev": true }, "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "dependencies": { "mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "requires": { - "mime-db": "1.48.0" + "mime-db": "1.52.0" } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" } } }, @@ -3592,39 +3826,6 @@ "dev": true, "requires": { "node-fetch": "^2.6.7" - }, - "dependencies": { - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=", - "dev": true - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - } } }, "apollo-server-errors": { @@ -3672,67 +3873,6 @@ "@types/qs": "*", "@types/range-parser": "*" } - }, - "apollo-datasource": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-3.3.2.tgz", - "integrity": "sha512-L5TiS8E2Hn/Yz7SSnWIVbZw0ZfEIXZCa5VUiVxD9P53JvSrf4aStvsFDlGWPvpIdCR+aly2CfoB79B9/JjKFqg==", - "dev": true, - "requires": { - "@apollo/utils.keyvaluecache": "^1.0.1", - "apollo-server-env": "^4.2.1" - } - }, - "apollo-server-core": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-3.9.0.tgz", - "integrity": "sha512-WS54C33cTriDaBIcj7ijWv/fgeJICcrQKlP1Cn6pnZp/eumpMraezLeJ3gFWAXprOuR2E3fZe64lNlup0fMu8w==", - "dev": true, - "requires": { - "@apollo/utils.keyvaluecache": "^1.0.1", - "@apollo/utils.logger": "^1.0.0", - "@apollo/utils.usagereporting": "^1.0.0", - "@apollographql/apollo-tools": "^0.5.3", - "@apollographql/graphql-playground-html": "1.6.29", - "@graphql-tools/mock": "^8.1.2", - "@graphql-tools/schema": "^8.0.0", - "@josephg/resolvable": "^1.0.0", - "apollo-datasource": "^3.3.2", - "apollo-reporting-protobuf": "^3.3.1", - "apollo-server-env": "^4.2.1", - "apollo-server-errors": "^3.3.1", - "apollo-server-plugin-base": "^3.6.1", - "apollo-server-types": "^3.6.1", - "async-retry": "^1.2.1", - "fast-json-stable-stringify": "^2.1.0", - "graphql-tag": "^2.11.0", - "loglevel": "^1.6.8", - "lru-cache": "^6.0.0", - "sha.js": "^2.4.11", - "uuid": "^8.0.0", - "whatwg-mimetype": "^3.0.0" - } - }, - "apollo-server-plugin-base": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-3.6.1.tgz", - "integrity": "sha512-bFpxzWO0LTTtSAkGVBeaAtnQXJ5ZCi8eaLN/eMSju8RByifmF3Kr6gAqcOZhOH/geQEt3Y6G8n3bR0eHTGxljQ==", - "dev": true, - "requires": { - "apollo-server-types": "^3.6.1" - } - }, - "apollo-server-types": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-3.6.1.tgz", - "integrity": "sha512-XOPlBlRdwP00PrG03OffGGWuzyei+J9t1rAnvyHsSdP0JCgQWigHJfvL1N9Bhgi4UTjl9JadKOJh1znLNlqIFQ==", - "dev": true, - "requires": { - "@apollo/utils.keyvaluecache": "^1.0.1", - "@apollo/utils.logger": "^1.0.0", - "apollo-reporting-protobuf": "^3.3.1", - "apollo-server-env": "^4.2.1" - } } } }, @@ -4319,9 +4459,9 @@ } }, "got": { - "version": "11.8.5", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", - "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", + "version": "11.8.2", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz", + "integrity": "sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ==", "dev": true, "requires": { "@sindresorhus/is": "^4.0.0", @@ -4329,7 +4469,7 @@ "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", + "cacheable-request": "^7.0.1", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", @@ -4579,7 +4719,8 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "atob": { "version": "2.1.2", @@ -4594,9 +4735,9 @@ "dev": true }, "avvio": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-7.2.5.tgz", - "integrity": "sha512-AOhBxyLVdpOad3TujtC9kL/9r3HnTkxwQ5ggOsYrvvZP1cCFvzHWJd5XxZDFuTn+IN8vkKSG5SEJrd27vCSbeA==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.1.3.tgz", + "integrity": "sha512-tl9TC0yDRKzP6gFLkrInqPyx8AkfBC/0QRnwkE9Jo31+OJjLrE/73GJuE0QgSB0Vpv38CTJJZGqU9hczowclWw==", "dev": true, "requires": { "archy": "^1.0.0", @@ -4634,48 +4775,14 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - }, - "dependencies": { - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "follow-redirects": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", - "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "dev": true }, "axios-retry": { "version": "3.2.0", @@ -5922,6 +6029,12 @@ } } }, + "cluster-key-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz", + "integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==", + "dev": true + }, "cmd-shim": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-2.1.0.tgz", @@ -9068,7 +9181,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true }, "delegates": { "version": "1.0.0", @@ -9393,6 +9507,12 @@ } } }, + "encoding-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/encoding-negotiator/-/encoding-negotiator-2.0.1.tgz", + "integrity": "sha512-GSK7qphNR4iPcejfAlZxKDoz3xMhnspwImK+Af5WhePS9jUpK/Oh7rUdyENWu+9rgDflOCTmAojBsgsvM8neAQ==", + "dev": true + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -10714,12 +10834,6 @@ "color-support": "^1.1.3" } }, - "fast-decode-uri-component": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", - "dev": true - }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10851,380 +10965,95 @@ "integrity": "sha512-qKRta6N7BWEFVlyonVY/V+BMLgFqktCUV0QjT259ekAIlbVrMaFnFLxJ4s/JPl4tou56S1BzPufI60bLe29fHA==" }, "fastify": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-3.29.0.tgz", - "integrity": "sha512-zXSiDTdHJCHcmDrSje1f1RfzTmUTjMtHnPhh6cdokgfHhloQ+gy0Du+KlEjwTbcNC3Djj4GAsBzl6KvfI9Ah2g==", - "dev": true, - "requires": { - "@fastify/ajv-compiler": "^1.0.0", - "@fastify/error": "^2.0.0", - "abstract-logging": "^2.0.0", - "avvio": "^7.1.2", - "fast-json-stringify": "^2.5.2", - "find-my-way": "^4.5.0", - "flatstr": "^1.0.12", - "light-my-request": "^4.2.0", - "pino": "^6.13.0", - "process-warning": "^1.0.0", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.0.2.tgz", + "integrity": "sha512-1DYyRyIqdPbvu/c5Xtt5MF7wQRLO+GbCibRWq+5JhntQVdod9oCy7rGsSNDMjvRgvJY5EUm2UYnBtAsDr5NdFA==", + "dev": true, + "requires": { + "@fastify/ajv-compiler": "^3.1.0", + "@fastify/error": "^3.0.0", + "@fastify/fast-json-stringify-compiler": "^3.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.1.3", + "fast-json-stringify": "^4.1.0", + "find-my-way": "^6.3.0", + "light-my-request": "^5.0.0", + "pino": "^8.0.0", + "process-warning": "^2.0.0", "proxy-addr": "^2.0.7", - "rfdc": "^1.1.4", - "secure-json-parse": "^2.0.0", - "semver": "^7.3.2", - "tiny-lru": "^8.0.1" - }, - "dependencies": { - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true - }, - "fast-json-stringify": { - "version": "2.7.13", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-2.7.13.tgz", - "integrity": "sha512-ar+hQ4+OIurUGjSJD1anvYSDcUflywhKjfxnsW4TBTD7+u0tJufv6DKRWoQk3vI6YBOWMoz0TQtfbe7dxbQmvA==", - "dev": true, - "requires": { - "ajv": "^6.11.0", - "deepmerge": "^4.2.2", - "rfdc": "^1.2.0", - "string-similarity": "^4.0.1" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "light-my-request": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.10.1.tgz", - "integrity": "sha512-l+zWk0HXGhGzY7IYTZnYEqIpj3Mpcyk2f8+FkKUyREywvaiWCf2jyQVxpasKRsploY/nVpoqTlxx72CIeQNcIQ==", - "dev": true, - "requires": { - "ajv": "^8.1.0", - "cookie": "^0.5.0", - "process-warning": "^1.0.0", - "set-cookie-parser": "^2.4.1" - }, - "dependencies": { - "ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - } - } - }, - "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "fastify-cors": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/fastify-cors/-/fastify-cors-6.1.0.tgz", - "integrity": "sha512-QBKz32IoY/iuT74CunRY1XOSpjSTIOh9E3FxulXIBhd0D2vdgG0kDvy0eG6HA/88sRfWHeba43LkGEXPz0Rh8g==", - "dev": true, - "requires": { - "fastify-cors-deprecated": "npm:fastify-cors@6.0.3", - "process-warning": "^1.0.0" - }, - "dependencies": { - "fastify-cors-deprecated": { - "version": "npm:fastify-cors@6.0.3", - "resolved": "https://registry.npmjs.org/fastify-cors/-/fastify-cors-6.0.3.tgz", - "integrity": "sha512-fMbXubKKyBHHCfSBtsCi3+7VyVRdhJQmGes5gM+eGKkRErCdm0NaYO0ozd31BQBL1ycoTIjbqOZhJo4RTF/Vlg==", - "dev": true, - "requires": { - "fastify-plugin": "^3.0.0", - "vary": "^1.1.2" - } - } - } - }, - "fastify-formbody": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/fastify-formbody/-/fastify-formbody-5.3.0.tgz", - "integrity": "sha512-7cjFV2HE/doojyfTwCLToIFD6Hmbw2jVTbfqZ2lbUZznQWlSXu+MBQgqBU8T2nHcMfqSi9vx6PyX0LwTehuKkg==", - "dev": true, - "requires": { - "fastify-formbody-deprecated": "npm:fastify-formbody@5.2.0", - "process-warning": "^1.0.0" - }, - "dependencies": { - "fastify-formbody-deprecated": { - "version": "npm:fastify-formbody@5.2.0", - "resolved": "https://registry.npmjs.org/fastify-formbody/-/fastify-formbody-5.2.0.tgz", - "integrity": "sha512-d8Y5hCL82akPyoFiXh2wYOm3es0pV9jqoPo3pO9OV2cNF0cQx39J5WAVXzCh4MSt9Z2qF4Fy5gHlvlyESwjtvg==", - "dev": true, - "requires": { - "fastify-plugin": "^3.0.0" - } - } - } - }, - "fastify-multipart": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/fastify-multipart/-/fastify-multipart-5.4.0.tgz", - "integrity": "sha512-Pafy4mtcuFUnFM/t0kgCdL854KIEoDymNVdv4nD7uBfV7lBCQq/NVEuNnaNXAbuCTpeXzYRzi50lSDa9ZM838A==", - "dev": true, - "requires": { - "fastify-multipart-deprecated": "npm:fastify-multipart@5.3.1", - "process-warning": "^1.0.0" - }, - "dependencies": { - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "fastify-multipart-deprecated": { - "version": "npm:fastify-multipart@5.3.1", - "resolved": "https://registry.npmjs.org/fastify-multipart/-/fastify-multipart-5.3.1.tgz", - "integrity": "sha512-c2pnGfkJmiNpYqzFYT2QfBg/06AxG531O+n1elqc8YUbWPRzufdqn3yfGAIV3RA7J4Vnf7Pfvgx0iaWqaRTOVA==", - "dev": true, - "requires": { - "@fastify/busboy": "^1.0.0", - "deepmerge": "^4.2.2", - "end-of-stream": "^1.4.4", - "fastify-error": "^0.3.0", - "fastify-plugin": "^3.0.0", - "hexoid": "^1.0.0", - "secure-json-parse": "^2.4.0", - "stream-wormhole": "^1.1.0" - }, - "dependencies": { - "@fastify/busboy": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-1.1.0.tgz", - "integrity": "sha512-Fv854f94v0CzIDllbY3i/0NJPNBRNLDawf3BTYVGCe9VrIIs3Wi7AFx24F9NzCxdf0wyx/x0Q9kEVnvDOPnlxA==", - "dev": true, - "requires": { - "text-decoding": "^1.0.0" - } - }, - "fastify-error": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", - "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==", - "dev": true - }, - "stream-wormhole": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz", - "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", - "dev": true - } - } - } - } - }, - "fastify-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", - "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==", - "dev": true - }, - "fastify-static": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/fastify-static/-/fastify-static-4.7.0.tgz", - "integrity": "sha512-zZhCfJv/hkmud2qhWqpU3K9XVAuy3+IV8Tp9BC5J5U+GyA2XwoB6h8lh9GqpEIqdXOw01WyWQllV7dOWVyAlXg==", - "dev": true, - "requires": { - "fastify-static-deprecated": "npm:fastify-static@4.6.1", - "process-warning": "^1.0.0" + "rfdc": "^1.3.0", + "secure-json-parse": "^2.4.0", + "semver": "^7.3.7", + "tiny-lru": "^8.0.2" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg==", - "dev": true - }, - "fastify-static-deprecated": { - "version": "npm:fastify-static@4.6.1", - "resolved": "https://registry.npmjs.org/fastify-static/-/fastify-static-4.6.1.tgz", - "integrity": "sha512-vy7N28U4AMhuOim12ZZWHulEE6OQKtzZbHgiB8Zj4llUuUQXPka0WHAQI3njm1jTCx4W6fixUHfpITxweMtAIA==", - "dev": true, - "requires": { - "content-disposition": "^0.5.3", - "encoding-negotiator": "^2.0.1", - "fastify-plugin": "^3.0.0", - "glob": "^7.1.4", - "p-limit": "^3.1.0", - "readable-stream": "^3.4.0", - "send": "^0.17.1" - }, - "dependencies": { - "encoding-negotiator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/encoding-negotiator/-/encoding-negotiator-2.0.1.tgz", - "integrity": "sha512-GSK7qphNR4iPcejfAlZxKDoz3xMhnspwImK+Af5WhePS9jUpK/Oh7rUdyENWu+9rgDflOCTmAojBsgsvM8neAQ==", - "dev": true - } - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "@fastify/error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.0.0.tgz", + "integrity": "sha512-dPRyT40GiHRzSCll3/Jn2nPe25+E1VXc9tDwRAIKwFCxd5Np5wzgz1tmooWG3sV0qKgrBibihVoCna2ru4SEFg==", "dev": true }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "ajv": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", + "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } }, - "send": { - "version": "0.17.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", - "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "fast-json-stringify": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-4.2.0.tgz", + "integrity": "sha512-9RWBl82H7jwnPlkZ/ghi0VD5OFZVdwgwVui0nYzjnXbPQxJ3ES1+SQcWIoeCJOgrY7JkBkY/69UNZSroFPDRdQ==", "dev": true, "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "1.8.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "deepmerge": "^4.2.2", + "fast-uri": "^2.0.0", + "rfdc": "^1.2.0", + "string-similarity": "^4.0.1" } }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "string_decoder": { + "process-warning": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz", + "integrity": "sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==", + "dev": true + }, + "rfdc": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dev": true, "requires": { - "safe-buffer": "~5.2.0" + "lru-cache": "^6.0.0" } } } }, + "fastify-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", + "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==", + "dev": true + }, "fastq": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.6.0.tgz", @@ -11353,15 +11182,13 @@ } }, "find-my-way": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-4.5.1.tgz", - "integrity": "sha512-kE0u7sGoUFbMXcOG/xpkmz4sRLCklERnBcg7Ftuu1iAxsfEt2S46RLJ3Sq7vshsEy2wJT2hZxE58XZK27qa8kg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-6.3.0.tgz", + "integrity": "sha512-WRtxcItuTCR6X+jaZFMI1aWT4Ih5GzL5faZAOxoHrmZAMneTzHl6AeGs2RN5b6dEMYIykVsRJtGrTk3RYGfJBg==", "dev": true, "requires": { - "fast-decode-uri-component": "^1.0.1", "fast-deep-equal": "^3.1.3", - "safe-regex2": "^2.0.0", - "semver-store": "^0.3.0" + "safe-regex2": "^2.0.0" } }, "find-up": { @@ -11431,12 +11258,6 @@ } } }, - "flatstr": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", - "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==", - "dev": true - }, "flatted": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", @@ -11726,10 +11547,6 @@ "concat-map": "0.0.1" } }, - "chownr": { - "version": "1.1.1", - "bundled": true - }, "code-point-at": { "version": "1.1.0", "bundled": true, @@ -11781,13 +11598,6 @@ "dev": true, "optional": true }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "requires": { - "minipass": "^2.2.1" - } - }, "fs.realpath": { "version": "1.0.0", "bundled": true, @@ -11894,21 +11704,6 @@ "dev": true, "optional": true }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "requires": { - "minipass": "^2.2.1" - } - }, "mkdirp": { "version": "0.5.1", "bundled": true, @@ -12092,7 +11887,9 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true + "bundled": true, + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -12179,10 +11976,6 @@ "bundled": true, "dev": true, "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true } } }, @@ -13030,9 +12823,9 @@ } }, "graphql-tag": { - "version": "2.12.4", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.4.tgz", - "integrity": "sha512-VV1U4O+9x99EkNpNmCUV5RZwq6MnK4+pGbRYWG+lA/m3uo7TSqJF81OkcOP148gFP6fzdl7JWYBrwWVTS9jXww==", + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", "dev": true, "requires": { "tslib": "^2.1.0" @@ -13088,12 +12881,6 @@ "integrity": "sha512-hvyIS71vs4Tu/yUYHPvGXsTgo0t3arU820+lT5VjZS2go0ewp2LqyCgxEN56CzOG7Iys52eRhHBiD1gGRdiQtw==", "dev": true }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "gulp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", @@ -13904,6 +13691,12 @@ } } }, + "hashlru": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/hashlru/-/hashlru-2.3.0.tgz", + "integrity": "sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==", + "dev": true + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -14373,6 +14166,46 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", "dev": true }, + "ioredis": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.0.4.tgz", + "integrity": "sha512-qFJw3MnPNsJF1lcIOP3vztbsasOXK3nDdNAgjQj7t7/Bn/w10PGchTOpqylQNxjzPbLoYDu34LjeJtSWiKBntQ==", + "dev": true, + "requires": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.0.1", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "denque": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -16303,12 +16136,6 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", - "dev": true - }, "slice-ansi": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", @@ -16447,6 +16274,12 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", + "dev": true + }, "lodash.escape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", @@ -17405,12 +17238,14 @@ "mime-db": { "version": "1.38.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", - "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==", + "dev": true }, "mime-types": { "version": "2.1.22", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "dev": true, "requires": { "mime-db": "~1.38.0" } @@ -17581,32 +17416,30 @@ } }, "mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", - "debug": "4.3.3", + "debug": "4.3.4", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", "glob": "7.2.0", - "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", - "minimatch": "4.2.1", + "minimatch": "5.0.1", "ms": "2.1.3", - "nanoid": "3.3.1", + "nanoid": "3.3.3", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", + "workerpool": "6.2.1", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" @@ -17711,9 +17544,9 @@ "dev": true }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -17849,12 +17682,23 @@ } }, "minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + } } }, "ms": { @@ -17926,15 +17770,6 @@ "is-number": "^7.0.0" } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "yargs-parser": { "version": "20.2.4", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", @@ -18336,9 +18171,9 @@ "optional": true }, "nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true }, "nanomatch": { @@ -18384,7 +18219,8 @@ "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true }, "neo-async": { "version": "2.6.0", @@ -19629,6 +19465,12 @@ "es-abstract": "^1.19.1" } }, + "on-exit-leak-free": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-1.0.0.tgz", + "integrity": "sha512-Ve8ubhrXRdnuCJ5bQSQpP3uaV43K1PMcOfSRC1pqHgRZommXCgsXwh08jVC5NpjwScE23BPDwDvVg4cov3mwjw==", + "dev": true + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -20102,24 +19944,92 @@ } }, "pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.0.0.tgz", + "integrity": "sha512-EvZh9ZUoLGkrhqhoF9UBxw2/ZiAhXHUKlGrI4WUT/wLu0sfu8Wr3NJaZ6lxcy/S51W0PMSon5KE7ujPAhc/G6g==", "dev": true, "requires": { + "atomic-sleep": "^1.0.0", "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", + "on-exit-leak-free": "^1.0.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^5.0.0", + "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^3.0.0", + "thread-stream": "^1.0.0" + }, + "dependencies": { + "process-warning": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz", + "integrity": "sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww==", + "dev": true + } + } + }, + "pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "dev": true, + "requires": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dev": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", + "dev": true + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + } } }, "pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-5.6.0.tgz", + "integrity": "sha512-VdUXCw8gO+xhir7sFuoYSjTnzB+TMDGxhAC/ph3YS3sdHnXNdsK0wMtADNUltfeGkn2KDxEM21fnjF3RwXyC8A==", "dev": true }, "pkg-dir": { @@ -20212,14 +20122,6 @@ "requires": { "fastify-plugin": "^3.0.0", "hashlru": "^2.3.0" - }, - "dependencies": { - "hashlru": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/hashlru/-/hashlru-2.3.0.tgz", - "integrity": "sha512-0cMsjjIC8I+D3M44pOQdsy0OHXGLVz6Z0beRuufhKa0KfaD2wGwAev6jILzXsd3/vpnNQJmWyZtIILqM1N+n5A==", - "dev": true - } } } } @@ -20781,6 +20683,12 @@ } } }, + "real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "dev": true + }, "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -21064,7 +20972,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, "resolve": { @@ -21160,9 +21068,9 @@ "dev": true }, "rfdc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.2.0.tgz", - "integrity": "sha512-ijLyszTMmUrXvjSooucVQwimGUk84eRcmCuLV8Xghe3UO85mjUtRAHRyoMM6XtyqbECaXuBWx18La3523sXINA==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, "rimraf": { "version": "2.6.3", @@ -21246,6 +21154,12 @@ } } }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "dev": true + }, "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -21297,12 +21211,6 @@ "sver-compat": "^1.5.0" } }, - "semver-store": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/semver-store/-/semver-store-0.3.0.tgz", - "integrity": "sha512-TcZvGMMy9vodEFSse30lWinkj+JgOBvPn8wRItpQRSayhc+4ssDs335uklkfvQQJgL/WvmHLVj4Ycv2s7QCQMg==", - "dev": true - }, "send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -21853,13 +21761,12 @@ } }, "sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.0.0.tgz", + "integrity": "sha512-p5DiZOZHbJ2ZO5MADczp5qrfOd3W5Vr2vHxfCpe7G4AzPwVOweIjbfgku8wSQUuk+Y5Yuo8W7JqRe6XKmKistg==", "dev": true, "requires": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" + "atomic-sleep": "^1.0.0" } }, "sort-keys": { @@ -22116,6 +22023,12 @@ "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true }, + "standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "dev": true + }, "static-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", @@ -22245,6 +22158,12 @@ "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", "dev": true }, + "stream-wormhole": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stream-wormhole/-/stream-wormhole-1.1.0.tgz", + "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", + "dev": true + }, "streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", @@ -22842,6 +22761,15 @@ "thenify": ">= 3.1.0 < 4" } }, + "thread-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-1.0.0.tgz", + "integrity": "sha512-2Sw29jWubQWOcVa7MhLHJ51wjksUD/GHN4Fy3hP9w9DYTujifoZGSKBl54CMLRXWoD5h2pD707kY3fAdzhcwAg==", + "dev": true, + "requires": { + "real-require": "^0.1.0" + } + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -23015,6 +22943,12 @@ "through2": "^2.0.3" } }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -24237,9 +24171,9 @@ "dev": true }, "workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", "dev": true }, "wrap-ansi": { @@ -24334,9 +24268,9 @@ "dev": true }, "xss": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz", - "integrity": "sha512-qmoqrRksmzqSKvgqzN0055UFWY7OKx1/9JWeRswwEVX9fCG5jcYRxa/A2DHcmZX6VJvjzHRQ2STeeVcQkrmLSw==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.13.tgz", + "integrity": "sha512-clu7dxTm1e8Mo5fz3n/oW3UCXBfV89xZ72jM8yzo1vR/pIS0w3sgB3XV2H8Vm6zfGnHL0FzvLJPJEBhd86/z4Q==", "dev": true, "requires": { "commander": "^2.20.3", diff --git a/package.json b/package.json index 73b75f14a46..3d0e3b915bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nestjs/core", - "version": "8.4.4", + "version": "9.0.0-next.1", "description": "Modern, fast, powerful node.js web framework", "homepage": "https://nestjs.com", "repository": { @@ -53,7 +53,6 @@ }, "dependencies": { "@nuxtjs/opencollective": "0.3.2", - "axios": "0.27.2", "class-transformer": "0.5.1", "class-validator": "0.13.2", "cli-color": "2.0.2", @@ -75,6 +74,11 @@ "@codechecks/client": "0.1.12", "@commitlint/cli": "17.0.3", "@commitlint/config-angular": "17.0.0", + "@fastify/cors": "^8.0.0", + "@fastify/formbody": "7.0.1", + "@fastify/middie": "8.0.0", + "@fastify/multipart": "6.0.0", + "@fastify/static": "5.0.0", "@grpc/grpc-js": "1.6.7", "@grpc/proto-loader": "0.6.13", "@nestjs/apollo": "10.0.16", @@ -123,11 +127,7 @@ "eslint-plugin-import": "2.26.0", "eventsource": "2.0.2", "fancy-log": "2.0.0", - "fastify": "3.29.0", - "fastify-cors": "6.1.0", - "fastify-formbody": "5.3.0", - "fastify-multipart": "5.4.0", - "fastify-static": "4.7.0", + "fastify": "4.0.2", "graphql": "15.8.0", "graphql-tools": "8.2.13", "gulp": "4.0.2", @@ -139,6 +139,7 @@ "http-errors": "2.0.0", "husky": "8.0.1", "imports-loader": "4.0.0", + "ioredis": "5.0.4", "json-loader": "0.5.7", "kafkajs": "2.0.2", "lerna": "2.11.0", @@ -148,7 +149,7 @@ "markdown-table": "2.0.0", "merge-graphql-schemas": "1.7.8", "middie": "6.1.0", - "mocha": "9.2.2", + "mocha": "10.0.0", "mongoose": "6.4.0", "mqtt": "4.3.7", "multer": "1.4.4", @@ -173,7 +174,7 @@ "ws": "8.8.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.9.0" }, "collective": { "type": "opencollective", diff --git a/packages/common/cache/cache.constants.ts b/packages/common/cache/cache.constants.ts index 0ecf5c94e9f..799547a7b27 100644 --- a/packages/common/cache/cache.constants.ts +++ b/packages/common/cache/cache.constants.ts @@ -1,4 +1,3 @@ export const CACHE_MANAGER = 'CACHE_MANAGER'; -export const CACHE_MODULE_OPTIONS = 'CACHE_MODULE_OPTIONS'; export const CACHE_KEY_METADATA = 'cache_module:cache_key'; export const CACHE_TTL_METADATA = 'cache_module:cache_ttl'; diff --git a/packages/common/cache/cache.module-definition.ts b/packages/common/cache/cache.module-definition.ts new file mode 100644 index 00000000000..25d74e98638 --- /dev/null +++ b/packages/common/cache/cache.module-definition.ts @@ -0,0 +1,12 @@ +import { ConfigurableModuleBuilder } from '../module-utils'; +import { + CacheModuleOptions, + CacheOptionsFactory, +} from './interfaces/cache-module.interface'; + +export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = + new ConfigurableModuleBuilder({ + moduleName: 'Cache', + }) + .setFactoryMethodName('createCacheOptions' as keyof CacheOptionsFactory) + .build(); diff --git a/packages/common/cache/cache.module.ts b/packages/common/cache/cache.module.ts index 4e2c63d48eb..992f98f0379 100644 --- a/packages/common/cache/cache.module.ts +++ b/packages/common/cache/cache.module.ts @@ -1,11 +1,11 @@ import { Module } from '../decorators'; -import { DynamicModule, Provider } from '../interfaces'; -import { CACHE_MANAGER, CACHE_MODULE_OPTIONS } from './cache.constants'; +import { DynamicModule } from '../interfaces'; +import { CACHE_MANAGER } from './cache.constants'; +import { ConfigurableModuleClass } from './cache.module-definition'; import { createCacheManager } from './cache.providers'; import { CacheModuleAsyncOptions, CacheModuleOptions, - CacheOptionsFactory, } from './interfaces/cache-module.interface'; /** @@ -19,7 +19,7 @@ import { providers: [createCacheManager()], exports: [CACHE_MANAGER], }) -export class CacheModule { +export class CacheModule extends ConfigurableModuleClass { /** * Configure the cache manager statically. * @@ -31,9 +31,8 @@ export class CacheModule { options: CacheModuleOptions = {} as any, ): DynamicModule { return { - module: CacheModule, global: options.isGlobal, - providers: [{ provide: CACHE_MODULE_OPTIONS, useValue: options }], + ...super.register(options), }; } @@ -48,47 +47,11 @@ export class CacheModule { static registerAsync< StoreConfig extends Record = Record, >(options: CacheModuleAsyncOptions): DynamicModule { + const moduleDefinition = super.registerAsync(options); return { - module: CacheModule, global: options.isGlobal, - imports: options.imports, - providers: [ - ...this.createAsyncProviders(options), - ...(options.extraProviders || []), - ], - }; - } - - private static createAsyncProviders>( - options: CacheModuleAsyncOptions, - ): Provider[] { - if (options.useExisting || options.useFactory) { - return [this.createAsyncOptionsProvider(options)]; - } - return [ - this.createAsyncOptionsProvider(options), - { - provide: options.useClass, - useClass: options.useClass, - }, - ]; - } - - private static createAsyncOptionsProvider< - StoreConfig extends Record, - >(options: CacheModuleAsyncOptions): Provider { - if (options.useFactory) { - return { - provide: CACHE_MODULE_OPTIONS, - useFactory: options.useFactory, - inject: options.inject || [], - }; - } - return { - provide: CACHE_MODULE_OPTIONS, - useFactory: async (optionsFactory: CacheOptionsFactory) => - optionsFactory.createCacheOptions(), - inject: [options.useExisting || options.useClass], + ...moduleDefinition, + providers: moduleDefinition.providers.concat(options.extraProviders), }; } } diff --git a/packages/common/cache/cache.providers.ts b/packages/common/cache/cache.providers.ts index 2e6df398d2c..4aaacf0f541 100644 --- a/packages/common/cache/cache.providers.ts +++ b/packages/common/cache/cache.providers.ts @@ -1,6 +1,7 @@ import { Provider } from '../interfaces'; import { loadPackage } from '../utils/load-package.util'; -import { CACHE_MANAGER, CACHE_MODULE_OPTIONS } from './cache.constants'; +import { CACHE_MANAGER } from './cache.constants'; +import { MODULE_OPTIONS_TOKEN } from './cache.module-definition'; import { defaultCacheOptions } from './default-options'; import { CacheManagerOptions } from './interfaces/cache-manager.interface'; @@ -31,6 +32,6 @@ export function createCacheManager(): Provider { ...(options || {}), }); }, - inject: [CACHE_MODULE_OPTIONS], + inject: [MODULE_OPTIONS_TOKEN], }; } diff --git a/packages/common/cache/interfaces/cache-module.interface.ts b/packages/common/cache/interfaces/cache-module.interface.ts index 21adf8b36ed..bac872a9d7f 100644 --- a/packages/common/cache/interfaces/cache-module.interface.ts +++ b/packages/common/cache/interfaces/cache-module.interface.ts @@ -1,4 +1,5 @@ -import { ModuleMetadata, Provider, Type } from '../../interfaces'; +import { Provider, Type } from '../../interfaces'; +import { ConfigurableModuleAsyncOptions } from '../../module-utils'; import { CacheManagerOptions } from './cache-manager.interface'; export type CacheModuleOptions< @@ -39,7 +40,10 @@ export interface CacheOptionsFactory< */ export interface CacheModuleAsyncOptions< StoreConfig extends Record = Record, -> extends Pick { +> extends ConfigurableModuleAsyncOptions< + CacheModuleOptions, + keyof CacheOptionsFactory + > { /** * Injection token resolving to an existing provider. The provider must implement * the `CacheOptionsFactory` interface. @@ -63,6 +67,9 @@ export interface CacheModuleAsyncOptions< * Dependencies that a Factory may inject. */ inject?: any[]; + /** + * Extra providers to be registered within a scope of this module. + */ extraProviders?: Provider[]; /** * If "true', register `CacheModule` as a global module. diff --git a/packages/common/decorators/core/controller.decorator.ts b/packages/common/decorators/core/controller.decorator.ts index b20c4a74138..ec13f5f7c22 100644 --- a/packages/common/decorators/core/controller.decorator.ts +++ b/packages/common/decorators/core/controller.decorator.ts @@ -163,7 +163,7 @@ export function Controller( : [ prefixOrOptions.path || defaultPath, prefixOrOptions.host, - { scope: prefixOrOptions.scope }, + { scope: prefixOrOptions.scope, durable: prefixOrOptions.durable }, Array.isArray(prefixOrOptions.version) ? Array.from(new Set(prefixOrOptions.version)) : prefixOrOptions.version, diff --git a/packages/common/exceptions/http.exception.ts b/packages/common/exceptions/http.exception.ts index 84ec9361b3c..540844298e1 100644 --- a/packages/common/exceptions/http.exception.ts +++ b/packages/common/exceptions/http.exception.ts @@ -41,6 +41,22 @@ export class HttpException extends Error { super(); this.initMessage(); this.initName(); + this.initCause(); + } + + public cause: Error | undefined; + + /** + * Configures error chaining support + * + * See: + * - https://nodejs.org/en/blog/release/v16.9.0/#error-cause + * - https://github.com/microsoft/TypeScript/issues/45167 + */ + public initCause() { + if (this.response instanceof Error) { + this.cause = this.response; + } } public initMessage() { diff --git a/packages/common/http/http.constants.ts b/packages/common/http/http.constants.ts deleted file mode 100644 index 21f6d849933..00000000000 --- a/packages/common/http/http.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const AXIOS_INSTANCE_TOKEN = 'AXIOS_INSTANCE_TOKEN'; -export const HTTP_MODULE_ID = 'HTTP_MODULE_ID'; -export const HTTP_MODULE_OPTIONS = 'HTTP_MODULE_OPTIONS'; diff --git a/packages/common/http/http.module.ts b/packages/common/http/http.module.ts deleted file mode 100644 index cc036767384..00000000000 --- a/packages/common/http/http.module.ts +++ /dev/null @@ -1,99 +0,0 @@ -import Axios from 'axios'; -import { Module } from '../decorators/modules/module.decorator'; -import { DynamicModule, Provider } from '../interfaces'; -import { randomStringGenerator } from '../utils/random-string-generator.util'; -import { - AXIOS_INSTANCE_TOKEN, - HTTP_MODULE_ID, - HTTP_MODULE_OPTIONS, -} from './http.constants'; -import { HttpService } from './http.service'; -import { - HttpModuleAsyncOptions, - HttpModuleOptions, - HttpModuleOptionsFactory, -} from './interfaces'; - -/** - * @deprecated "HttpModule" (from the "@nestjs/common" package) is deprecated and will be removed in the next major release. Please, use the "@nestjs/axios" package instead. - */ -@Module({ - providers: [ - HttpService, - { - provide: AXIOS_INSTANCE_TOKEN, - useValue: Axios, - }, - ], - exports: [HttpService], -}) -export class HttpModule { - static register(config: HttpModuleOptions): DynamicModule { - return { - module: HttpModule, - providers: [ - { - provide: AXIOS_INSTANCE_TOKEN, - useValue: Axios.create(config), - }, - { - provide: HTTP_MODULE_ID, - useValue: randomStringGenerator(), - }, - ], - }; - } - - static registerAsync(options: HttpModuleAsyncOptions): DynamicModule { - return { - module: HttpModule, - imports: options.imports, - providers: [ - ...this.createAsyncProviders(options), - { - provide: AXIOS_INSTANCE_TOKEN, - useFactory: (config: HttpModuleOptions) => Axios.create(config), - inject: [HTTP_MODULE_OPTIONS], - }, - { - provide: HTTP_MODULE_ID, - useValue: randomStringGenerator(), - }, - ...(options.extraProviders || []), - ], - }; - } - - private static createAsyncProviders( - options: HttpModuleAsyncOptions, - ): Provider[] { - if (options.useExisting || options.useFactory) { - return [this.createAsyncOptionsProvider(options)]; - } - return [ - this.createAsyncOptionsProvider(options), - { - provide: options.useClass, - useClass: options.useClass, - }, - ]; - } - - private static createAsyncOptionsProvider( - options: HttpModuleAsyncOptions, - ): Provider { - if (options.useFactory) { - return { - provide: HTTP_MODULE_OPTIONS, - useFactory: options.useFactory, - inject: options.inject || [], - }; - } - return { - provide: HTTP_MODULE_OPTIONS, - useFactory: async (optionsFactory: HttpModuleOptionsFactory) => - optionsFactory.createHttpOptions(), - inject: [options.useExisting || options.useClass], - }; - } -} diff --git a/packages/common/http/http.service.ts b/packages/common/http/http.service.ts deleted file mode 100644 index 7c6e2ded267..00000000000 --- a/packages/common/http/http.service.ts +++ /dev/null @@ -1,113 +0,0 @@ -import Axios, { - AxiosInstance, - AxiosPromise, - AxiosRequestConfig, - AxiosResponse, - CancelTokenSource, -} from 'axios'; -import { Observable } from 'rxjs'; -import { Inject } from '../decorators'; -import { Logger } from '../services'; -import { AXIOS_INSTANCE_TOKEN } from './http.constants'; - -/** - * @deprecated "HttpModule" (from the "@nestjs/common" package) is deprecated and will be removed in the next major release. Please, use the "@nestjs/axios" package instead. - */ -export class HttpService { - private readonly logger = new Logger(HttpService.name); - - constructor( - @Inject(AXIOS_INSTANCE_TOKEN) - private readonly instance: AxiosInstance = Axios, - ) { - this.logger.warn( - 'DEPRECATED! "HttpModule" (from the "@nestjs/common" package) is deprecated and will be removed in the next major release. Please, use the "@nestjs/axios" package instead.', - ); - } - - request(config: AxiosRequestConfig): Observable> { - return this.makeObservable(this.instance.request, config); - } - - get( - url: string, - config?: AxiosRequestConfig, - ): Observable> { - return this.makeObservable(this.instance.get, url, config); - } - - delete( - url: string, - config?: AxiosRequestConfig, - ): Observable> { - return this.makeObservable(this.instance.delete, url, config); - } - - head( - url: string, - config?: AxiosRequestConfig, - ): Observable> { - return this.makeObservable(this.instance.head, url, config); - } - - post( - url: string, - data?: any, - config?: AxiosRequestConfig, - ): Observable> { - return this.makeObservable(this.instance.post, url, data, config); - } - - put( - url: string, - data?: any, - config?: AxiosRequestConfig, - ): Observable> { - return this.makeObservable(this.instance.put, url, data, config); - } - - patch( - url: string, - data?: any, - config?: AxiosRequestConfig, - ): Observable> { - return this.makeObservable(this.instance.patch, url, data, config); - } - - get axiosRef(): AxiosInstance { - return this.instance; - } - - private makeObservable( - axios: (...args: any[]) => AxiosPromise, - ...args: any[] - ) { - return new Observable>(subscriber => { - const config: AxiosRequestConfig = { ...(args[args.length - 1] || {}) }; - - let cancelSource: CancelTokenSource; - if (!config.cancelToken) { - cancelSource = Axios.CancelToken.source(); - config.cancelToken = cancelSource.token; - } - - axios(...args) - .then(res => { - subscriber.next(res); - subscriber.complete(); - }) - .catch(err => { - subscriber.error(err); - }); - return () => { - if (config.responseType === 'stream') { - return; - } - - if (cancelSource) { - cancelSource.cancel(); - } - }; - }); - } -} diff --git a/packages/common/http/index.ts b/packages/common/http/index.ts deleted file mode 100644 index 2e7a1fead2a..00000000000 --- a/packages/common/http/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './http.module'; -export * from './http.service'; -export * from './interfaces'; diff --git a/packages/common/http/interfaces/http-module.interface.ts b/packages/common/http/interfaces/http-module.interface.ts deleted file mode 100644 index 794f9385ca4..00000000000 --- a/packages/common/http/interfaces/http-module.interface.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AxiosRequestConfig } from 'axios'; -import { ModuleMetadata, Provider, Type } from '../../interfaces'; - -export type HttpModuleOptions = AxiosRequestConfig; - -export interface HttpModuleOptionsFactory { - createHttpOptions(): Promise | HttpModuleOptions; -} - -export interface HttpModuleAsyncOptions - extends Pick { - useExisting?: Type; - useClass?: Type; - useFactory?: ( - ...args: any[] - ) => Promise | HttpModuleOptions; - inject?: any[]; - extraProviders?: Provider[]; -} diff --git a/packages/common/http/interfaces/index.ts b/packages/common/http/interfaces/index.ts deleted file mode 100644 index 2c295e24418..00000000000 --- a/packages/common/http/interfaces/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './http-module.interface'; -export * from './raw-body-request.interface'; diff --git a/packages/common/index.ts b/packages/common/index.ts index 0fe073784c1..c8d81518727 100644 --- a/packages/common/index.ts +++ b/packages/common/index.ts @@ -11,7 +11,6 @@ export * from './decorators'; export * from './enums'; export * from './exceptions'; export * from './file-stream'; -export * from './http'; export { Abstract, ArgumentMetadata, @@ -49,6 +48,7 @@ export { Paramtype, PipeTransform, Provider, + RawBodyRequest, RpcExceptionFilter, Scope, ScopeOptions, @@ -61,6 +61,7 @@ export { WsExceptionFilter, WsMessageHandler, } from './interfaces'; +export * from './module-utils'; export * from './pipes'; export * from './serializer'; export * from './services'; diff --git a/packages/common/interfaces/http/http-server.interface.ts b/packages/common/interfaces/http/http-server.interface.ts index 46a717d44bd..99b7e55eb29 100644 --- a/packages/common/interfaces/http/http-server.interface.ts +++ b/packages/common/interfaces/http/http-server.interface.ts @@ -50,8 +50,10 @@ export interface HttpServer { listen(port: number | string, hostname: string, callback?: () => void): any; reply(response: any, body: any, statusCode?: number): any; status(response: any, statusCode: number): any; + end(response: any, message?: string): any; render(response: any, view: string, options: any): any; redirect(response: any, statusCode: number, url: string): any; + isHeadersSent(response: any): boolean; setHeader(response: any, name: string, value: string): any; setErrorHandler?(handler: Function, prefix?: string): any; setNotFoundHandler?(handler: Function, prefix?: string): any; @@ -74,13 +76,9 @@ export interface HttpServer { close(): any; getType(): string; init?(): Promise; - applyVersionFilter?( + applyVersionFilter( handler: Function, version: VersionValue, versioningOptions: VersioningOptions, - ): = any, TResponse = any>( - req: TRequest, - res: TResponse, - next: () => void, - ) => any; + ): (req: TRequest, res: TResponse, next: () => void) => Function; } diff --git a/packages/common/interfaces/http/index.ts b/packages/common/interfaces/http/index.ts index f57d218dd25..8c9eed1733a 100644 --- a/packages/common/interfaces/http/index.ts +++ b/packages/common/interfaces/http/index.ts @@ -1,2 +1,3 @@ export * from './http-server.interface'; export * from './message-event.interface'; +export * from './raw-body-request.interface'; diff --git a/packages/common/http/interfaces/raw-body-request.interface.ts b/packages/common/interfaces/http/raw-body-request.interface.ts similarity index 100% rename from packages/common/http/interfaces/raw-body-request.interface.ts rename to packages/common/interfaces/http/raw-body-request.interface.ts diff --git a/packages/common/interfaces/modules/provider.interface.ts b/packages/common/interfaces/modules/provider.interface.ts index 39406c1e260..2fd7f85b66a 100644 --- a/packages/common/interfaces/modules/provider.interface.ts +++ b/packages/common/interfaces/modules/provider.interface.ts @@ -46,6 +46,19 @@ export interface ClassProvider { * Optional enum defining lifetime of the provider that is injected. */ scope?: Scope; + /** + * This option is only available on factory providers! + * + * @see [Use factory](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory) + */ + inject?: never; + /** + * Flags provider as durable. This flag can be used in combination with custom context id + * factory strategy to construct lazy DI subtrees. + * + * This flag can be used only in conjunction with scope = Scope.REQUEST. + */ + durable?: boolean; } /** @@ -72,6 +85,12 @@ export interface ValueProvider { * Instance of a provider to be injected. */ useValue: T; + /** + * This option is only available on factory providers! + * + * @see [Use factory](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory) + */ + inject?: never; } /** @@ -102,7 +121,7 @@ export interface FactoryProvider { /** * Factory function that returns an instance of the provider to be injected. */ - useFactory: (...args: any[]) => T; + useFactory: (...args: any[]) => T | Promise; /** * Optional list of providers to be injected into the context of the Factory function. */ @@ -111,6 +130,13 @@ export interface FactoryProvider { * Optional enum defining lifetime of the provider that is returned by the Factory function. */ scope?: Scope; + /** + * Flags provider as durable. This flag can be used in combination with custom context id + * factory strategy to construct lazy DI subtrees. + * + * This flag can be used only in conjunction with scope = Scope.REQUEST. + */ + durable?: boolean; } /** diff --git a/packages/common/interfaces/nest-application.interface.ts b/packages/common/interfaces/nest-application.interface.ts index ec04c518091..82e302dc8aa 100644 --- a/packages/common/interfaces/nest-application.interface.ts +++ b/packages/common/interfaces/nest-application.interface.ts @@ -61,16 +61,6 @@ export interface INestApplication extends INestApplicationContext { callback?: () => void, ): Promise; - /** - * Starts the application (can be awaited). - * @deprecated use "listen" instead. - * - * @param {number|string} port - * @param {string} [hostname] - * @returns {Promise} - */ - listenAsync(port: number | string, hostname?: string): Promise; - /** * Returns the url the application is listening at, based on OS and IP version. Returns as an IP value either in IPv6 or IPv4 * @@ -138,14 +128,6 @@ export interface INestApplication extends INestApplicationContext { */ startAllMicroservices(): Promise; - /** - * Starts all connected microservices and can be awaited. - * @deprecated use "startAllMicroservices" instead. - * - * @returns {Promise} - */ - startAllMicroservicesAsync(): Promise; - /** * Registers exception filters as global filters (will be used within * every HTTP route handler) diff --git a/packages/common/interfaces/nest-microservice.interface.ts b/packages/common/interfaces/nest-microservice.interface.ts index 857f1d66ccc..025c331bc33 100644 --- a/packages/common/interfaces/nest-microservice.interface.ts +++ b/packages/common/interfaces/nest-microservice.interface.ts @@ -18,14 +18,6 @@ export interface INestMicroservice extends INestApplicationContext { */ listen(): Promise; - /** - * Starts the microservice (can be awaited). - * @deprecated use "listen" instead. - * - * @returns {Promise} - */ - listenAsync(): Promise; - /** * Register Ws Adapter which will be used inside Gateways. * Use when you want to override default `socket.io` library. diff --git a/packages/common/interfaces/scope-options.interface.ts b/packages/common/interfaces/scope-options.interface.ts index f958c393b77..4cca7427566 100644 --- a/packages/common/interfaces/scope-options.interface.ts +++ b/packages/common/interfaces/scope-options.interface.ts @@ -28,4 +28,11 @@ export interface ScopeOptions { * Specifies the lifetime of an injected Provider or Controller. */ scope?: Scope; + /** + * Flags provider as durable. This flag can be used in combination with custom context id + * factory strategy to construct lazy DI subtrees. + * + * This flag can be used only in conjunction with scope = Scope.REQUEST. + */ + durable?: boolean; } diff --git a/packages/common/module-utils/configurable-module.builder.ts b/packages/common/module-utils/configurable-module.builder.ts new file mode 100644 index 00000000000..0e3f1318f20 --- /dev/null +++ b/packages/common/module-utils/configurable-module.builder.ts @@ -0,0 +1,330 @@ +import { DynamicModule, Provider } from '../interfaces'; +import { Logger } from '../services/logger.service'; +import { randomStringGenerator } from '../utils/random-string-generator.util'; +import { + ASYNC_METHOD_SUFFIX, + CONFIGURABLE_MODULE_ID, + DEFAULT_FACTORY_CLASS_METHOD_KEY, + DEFAULT_METHOD_KEY, +} from './constants'; +import { + ConfigurableModuleAsyncOptions, + ConfigurableModuleCls, + ConfigurableModuleOptionsFactory, +} from './interfaces'; +import { ConfigurableModuleHost } from './interfaces/configurable-module-host.interface'; +import { generateOptionsInjectionToken } from './utils/generate-options-injection-token.util'; + +/** + * @publicApi + */ +export interface ConfigurableModuleBuilderOptions { + /** + * Specified what injection token should be used for the module options provider. + * By default, an auto-generated UUID will be used. + */ + optionsInjectionToken?: string | symbol; + /** + * By default, an UUID will be used as a module options provider token. + * Explicitly specifying the "moduleName" will instruct the "ConfigurableModuleBuilder" + * to use a more descriptive provider token. + * + * For example, if `moduleName: "Cache"` then auto-generated provider token will be "CACHE_MODULE_OPTIONS". + */ + moduleName?: string; + /** + * Indicates whether module should always be "transient", meaning, + * every time you call the static method to construct a dynamic module, + * regardless of what arguments you pass in, a new "unique" module will be created. + * + * @default false + */ + alwaysTransient?: boolean; +} + +/** + * Factory that lets you create configurable modules and + * provides a way to reduce the majority of dynamic module boilerplate. + * + * @publicApi + */ +export class ConfigurableModuleBuilder< + ModuleOptions, + StaticMethodKey extends string = typeof DEFAULT_METHOD_KEY, + FactoryClassMethodKey extends string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY, + ExtraModuleDefinitionOptions = {}, +> { + protected staticMethodKey: StaticMethodKey; + protected factoryClassMethodKey: FactoryClassMethodKey; + protected extras: ExtraModuleDefinitionOptions; + protected transformModuleDefinition: ( + definition: DynamicModule, + extraOptions: ExtraModuleDefinitionOptions, + ) => DynamicModule; + + protected readonly logger = new Logger(ConfigurableModuleBuilder.name); + + constructor( + protected readonly options: ConfigurableModuleBuilderOptions = {}, + parentBuilder?: ConfigurableModuleBuilder, + ) { + if (parentBuilder) { + this.staticMethodKey = parentBuilder.staticMethodKey as StaticMethodKey; + this.factoryClassMethodKey = + parentBuilder.factoryClassMethodKey as FactoryClassMethodKey; + this.transformModuleDefinition = parentBuilder.transformModuleDefinition; + this.extras = parentBuilder.extras as ExtraModuleDefinitionOptions; + } + } + + /** + * Registers the "extras" object (a set of extra options that can be used to modify the dynamic module definition). + * Values you specify within the "extras" object will be used as default values (that can be overriden by module consumers). + * + * This method also applies the so-called "module definition transform function" that takes the auto-generated + * dynamic module object ("DynamicModule") and the actual consumer "extras" object as input parameters. + * The "extras" object consists of values explicitly specified by module consumers and default values. + * + * @example + * ```typescript + * .setExtras<{ isGlobal?: boolean }>({ isGlobal: false }, (definition, extras) => + * ({ ...definition, global: extras.isGlobal }) + * ) + * ``` + */ + setExtras( + extras: ExtraModuleDefinitionOptions, + transformDefinition: ( + definition: DynamicModule, + extras: ExtraModuleDefinitionOptions, + ) => DynamicModule, + ) { + const builder = new ConfigurableModuleBuilder< + ModuleOptions, + StaticMethodKey, + FactoryClassMethodKey, + ExtraModuleDefinitionOptions + >(this.options, this as any); + builder.extras = extras; + builder.transformModuleDefinition = transformDefinition; + return builder; + } + + /** + * Dynamic modules must expose public static methods that let you pass in + * configuration parameters (control the module's behavior from the outside). + * Some frequently used names that you may have seen in other modules are: + * "forRoot", "forFeature", "register", "configure". + * + * This method "setClassMethodName" lets you specify the name of the + * method that will be auto-generated. + * + * @param key name of the method + */ + setClassMethodName(key: StaticMethodKey) { + const builder = new ConfigurableModuleBuilder< + ModuleOptions, + StaticMethodKey, + FactoryClassMethodKey, + ExtraModuleDefinitionOptions + >(this.options, this as any); + builder.staticMethodKey = key; + return builder; + } + + /** + * Asynchronously configured modules (that rely on other modules, i.e. "ConfigModule") + * let you pass the configuration factory class that will be registered and instantiated as a provider. + * This provider then will be used to retrieve the module's configuration. To provide the configuration, + * the corresponding factory method must be implemented. + * + * This method ("setFactoryMethodName") lets you control what method name will have to be + * implemented by the config factory (default is "create"). + * + * @param key name of the method + */ + setFactoryMethodName( + key: FactoryClassMethodKey, + ) { + const builder = new ConfigurableModuleBuilder< + ModuleOptions, + StaticMethodKey, + FactoryClassMethodKey, + ExtraModuleDefinitionOptions + >(this.options, this as any); + builder.factoryClassMethodKey = key; + return builder; + } + + /** + * Returns an object consisting of multiple properties that lets you + * easily construct dynamic configurable modules. See "ConfigurableModuleHost" interface for more details. + */ + build(): ConfigurableModuleHost< + ModuleOptions, + StaticMethodKey, + FactoryClassMethodKey, + ExtraModuleDefinitionOptions + > { + this.staticMethodKey ??= DEFAULT_METHOD_KEY as StaticMethodKey; + this.factoryClassMethodKey ??= + DEFAULT_FACTORY_CLASS_METHOD_KEY as FactoryClassMethodKey; + this.options.optionsInjectionToken ??= this.options.moduleName + ? this.constructInjectionTokenString() + : generateOptionsInjectionToken(); + this.transformModuleDefinition ??= definition => definition; + + return { + ConfigurableModuleClass: + this.createConfigurableModuleCls(), + MODULE_OPTIONS_TOKEN: this.options.optionsInjectionToken, + ASYNC_OPTIONS_TYPE: this.createTypeProxy('ASYNC_OPTIONS_TYPE'), + OPTIONS_TYPE: this.createTypeProxy('OPTIONS_TYPE'), + }; + } + + private constructInjectionTokenString(): string { + const moduleNameInSnakeCase = this.options.moduleName + .trim() + .split(/(?=[A-Z])/) + .join('_') + .toUpperCase(); + return `${moduleNameInSnakeCase}_MODULE_OPTIONS`; + } + + private createConfigurableModuleCls(): ConfigurableModuleCls< + ModuleOptions, + StaticMethodKey, + FactoryClassMethodKey + > { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const self = this; + const asyncMethodKey = this.staticMethodKey + ASYNC_METHOD_SUFFIX; + + class InternalModuleClass { + static [self.staticMethodKey]( + options: ModuleOptions & ExtraModuleDefinitionOptions, + ): DynamicModule { + const providers = [ + { + provide: self.options.optionsInjectionToken, + useValue: this.omitExtras(options, self.extras), + }, + ]; + if (self.options.alwaysTransient) { + providers.push({ + provide: CONFIGURABLE_MODULE_ID, + useValue: randomStringGenerator(), + }); + } + return self.transformModuleDefinition( + { + module: this, + providers, + }, + options, + ); + } + + static [asyncMethodKey]( + options: ConfigurableModuleAsyncOptions & + ExtraModuleDefinitionOptions, + ): DynamicModule { + const providers = this.createAsyncProviders(options); + if (self.options.alwaysTransient) { + providers.push({ + provide: CONFIGURABLE_MODULE_ID, + useValue: randomStringGenerator(), + }); + } + return self.transformModuleDefinition( + { + module: this, + imports: options.imports || [], + providers, + }, + options, + ); + } + + private static omitExtras( + input: ModuleOptions & ExtraModuleDefinitionOptions, + extras: ExtraModuleDefinitionOptions | undefined, + ): ModuleOptions { + if (!extras) { + return input; + } + const moduleOptions = {}; + const extrasKeys = Object.keys(extras); + + Object.keys(input) + .filter(key => !extrasKeys.includes(key)) + .forEach(key => { + moduleOptions[key] = input[key]; + }); + return moduleOptions as ModuleOptions; + } + + private static createAsyncProviders( + options: ConfigurableModuleAsyncOptions, + ): Provider[] { + if (options.useExisting || options.useFactory) { + return [this.createAsyncOptionsProvider(options)]; + } + return [ + this.createAsyncOptionsProvider(options), + { + provide: options.useClass, + useClass: options.useClass, + }, + ]; + } + + private static createAsyncOptionsProvider( + options: ConfigurableModuleAsyncOptions, + ): Provider { + if (options.useFactory) { + return { + provide: self.options.optionsInjectionToken, + useFactory: options.useFactory, + inject: options.inject || [], + }; + } + return { + provide: self.options.optionsInjectionToken, + useFactory: async ( + optionsFactory: ConfigurableModuleOptionsFactory< + ModuleOptions, + FactoryClassMethodKey + >, + ) => + await optionsFactory[ + self.factoryClassMethodKey as keyof typeof optionsFactory + ](), + inject: [options.useExisting || options.useClass], + }; + } + } + return InternalModuleClass as unknown as ConfigurableModuleCls< + ModuleOptions, + StaticMethodKey, + FactoryClassMethodKey + >; + } + + private createTypeProxy( + typeName: 'OPTIONS_TYPE' | 'ASYNC_OPTIONS_TYPE' | 'OptionsFactoryInterface', + ) { + const proxy = new Proxy( + {}, + { + get: () => { + throw new Error( + `"${typeName}" is not supposed to be used as a value.`, + ); + }, + }, + ); + return proxy as any; + } +} diff --git a/packages/common/module-utils/constants.ts b/packages/common/module-utils/constants.ts new file mode 100644 index 00000000000..2d97c0c8cf7 --- /dev/null +++ b/packages/common/module-utils/constants.ts @@ -0,0 +1,5 @@ +export const DEFAULT_METHOD_KEY = 'register'; +export const DEFAULT_FACTORY_CLASS_METHOD_KEY = 'create'; + +export const ASYNC_METHOD_SUFFIX = 'Async'; +export const CONFIGURABLE_MODULE_ID = 'CONFIGURABLE_MODULE_ID'; diff --git a/packages/common/module-utils/index.ts b/packages/common/module-utils/index.ts new file mode 100644 index 00000000000..4393992bd15 --- /dev/null +++ b/packages/common/module-utils/index.ts @@ -0,0 +1,2 @@ +export * from './configurable-module.builder'; +export * from './interfaces'; diff --git a/packages/common/module-utils/interfaces/configurable-module-async-options.interface.ts b/packages/common/module-utils/interfaces/configurable-module-async-options.interface.ts new file mode 100644 index 00000000000..56f7040e45c --- /dev/null +++ b/packages/common/module-utils/interfaces/configurable-module-async-options.interface.ts @@ -0,0 +1,51 @@ +import { FactoryProvider, ModuleMetadata, Type } from '../../interfaces'; +import { DEFAULT_FACTORY_CLASS_METHOD_KEY } from '../constants'; + +/** + * Interface that must be implemented by the module options factory class. + * Method key varies depending on the "FactoryClassMethodKey" type argument. + * + * @publicApi + */ +export type ConfigurableModuleOptionsFactory< + ModuleOptions, + FactoryClassMethodKey extends string, +> = Record< + `${FactoryClassMethodKey}`, + () => Promise | ModuleOptions +>; + +/** + * Interface that represents the module async options object + * Factory method name varies depending on the "FactoryClassMethodKey" type argument. + * + * @publicApi + */ +export interface ConfigurableModuleAsyncOptions< + ModuleOptions, + FactoryClassMethodKey extends string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY, +> extends Pick { + /** + * Injection token resolving to an existing provider. The provider must implement + * the corresponding interface. + */ + useExisting?: Type< + ConfigurableModuleOptionsFactory + >; + /** + * Injection token resolving to a class that will be instantiated as a provider. + * The class must implement the corresponding interface. + */ + useClass?: Type< + ConfigurableModuleOptionsFactory + >; + /** + * Function returning options (or a Promise resolving to options) to configure the + * cache module. + */ + useFactory?: (...args: unknown[]) => Promise | ModuleOptions; + /** + * Dependencies that a Factory may inject. + */ + inject?: FactoryProvider['inject']; +} diff --git a/packages/common/module-utils/interfaces/configurable-module-cls.interface.ts b/packages/common/module-utils/interfaces/configurable-module-cls.interface.ts new file mode 100644 index 00000000000..e01c706bef9 --- /dev/null +++ b/packages/common/module-utils/interfaces/configurable-module-cls.interface.ts @@ -0,0 +1,35 @@ +import { DynamicModule } from '../../interfaces'; +import { + DEFAULT_FACTORY_CLASS_METHOD_KEY, + DEFAULT_METHOD_KEY, +} from '../constants'; +import { ConfigurableModuleAsyncOptions } from './configurable-module-async-options.interface'; + +/** + * Class that represents a blueprint/prototype for a configurable Nest module. + * This class provides static methods for constructing dynamic modules. Their names + * can be controlled through the "MethodKey" type argument. + * + * @publicApi + */ +export type ConfigurableModuleCls< + ModuleOptions, + MethodKey extends string = typeof DEFAULT_METHOD_KEY, + FactoryClassMethodKey extends string = typeof DEFAULT_FACTORY_CLASS_METHOD_KEY, + ExtraModuleDefinitionOptions = {}, +> = { + new (): any; +} & Record< + `${MethodKey}`, + (options: ModuleOptions & ExtraModuleDefinitionOptions) => DynamicModule +> & + Record< + `${MethodKey}Async`, + ( + options: ConfigurableModuleAsyncOptions< + ModuleOptions, + FactoryClassMethodKey + > & + ExtraModuleDefinitionOptions, + ) => DynamicModule + >; diff --git a/packages/common/module-utils/interfaces/configurable-module-host.interface.ts b/packages/common/module-utils/interfaces/configurable-module-host.interface.ts new file mode 100644 index 00000000000..6a652ecce2f --- /dev/null +++ b/packages/common/module-utils/interfaces/configurable-module-host.interface.ts @@ -0,0 +1,77 @@ +import { ConfigurableModuleAsyncOptions } from './configurable-module-async-options.interface'; +import { ConfigurableModuleCls } from './configurable-module-cls.interface'; + +/** + * Configurable module host. See properties for more details + * + * @publicApi + */ +export interface ConfigurableModuleHost< + ModuleOptions = Record, + MethodKey extends string = string, + FactoryClassMethodKey extends string = string, + ExtraModuleDefinitionOptions = {}, +> { + /** + * Class that represents a blueprint/prototype for a configurable Nest module. + * This class provides static methods for constructing dynamic modules. Their names + * can be controlled through the "MethodKey" type argument. + * + * Your module class should inherit from this class to make the static methods available. + * + * @example + * ```typescript + * @Module({}) + * class IntegrationModule extends ConfigurableModuleCls { + * // ... + * } + * ``` + */ + ConfigurableModuleClass: ConfigurableModuleCls< + ModuleOptions, + MethodKey, + FactoryClassMethodKey, + ExtraModuleDefinitionOptions + >; + /** + * Module options provider token. Can be used to inject the "options object" to + * providers registered within the host module. + */ + MODULE_OPTIONS_TOKEN: string | symbol; + /** + * Can be used to auto-infer the compound "async module options" type. + * Note: this property is not supposed to be used as a value. + * + * @example + * ```typescript + * @Module({}) + * class IntegrationModule extends ConfigurableModuleCls { + * static module = initializer(IntegrationModule); + * + * static registerAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule { + * return super.registerAsync(options); + * } + * ``` + */ + ASYNC_OPTIONS_TYPE: ConfigurableModuleAsyncOptions< + ModuleOptions, + FactoryClassMethodKey + > & + ExtraModuleDefinitionOptions; + /** + * Can be used to auto-infer the compound "module options" type (options interface + extra module definition options). + * Note: this property is not supposed to be used as a value. + * + * @example + * ```typescript + * @Module({}) + * class IntegrationModule extends ConfigurableModuleCls { + * static module = initializer(IntegrationModule); + * + * static register(options: typeof OPTIONS_TYPE): DynamicModule { + * return super.register(options); + * } + * ``` + */ + OPTIONS_TYPE: ModuleOptions & ExtraModuleDefinitionOptions; +} diff --git a/packages/common/module-utils/interfaces/index.ts b/packages/common/module-utils/interfaces/index.ts new file mode 100644 index 00000000000..35c52307638 --- /dev/null +++ b/packages/common/module-utils/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './configurable-module-async-options.interface'; +export * from './configurable-module-cls.interface'; +export * from './configurable-module-host.interface'; diff --git a/packages/common/module-utils/utils/generate-options-injection-token.util.ts b/packages/common/module-utils/utils/generate-options-injection-token.util.ts new file mode 100644 index 00000000000..259a6afa33e --- /dev/null +++ b/packages/common/module-utils/utils/generate-options-injection-token.util.ts @@ -0,0 +1,6 @@ +import { randomStringGenerator } from '../../utils/random-string-generator.util'; + +export function generateOptionsInjectionToken() { + const hash = randomStringGenerator(); + return `CONFIGURABLE_MODULE_OPTIONS[${hash}]`; +} diff --git a/packages/common/package.json b/packages/common/package.json index 7760c4afac6..101a52e9ab2 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -17,7 +17,6 @@ }, "license": "MIT", "dependencies": { - "axios": "0.27.2", "iterare": "1.2.1", "tslib": "2.4.0", "uuid": "8.3.2" diff --git a/packages/common/pipes/file/file-type.validator.ts b/packages/common/pipes/file/file-type.validator.ts new file mode 100644 index 00000000000..240d3efe86a --- /dev/null +++ b/packages/common/pipes/file/file-type.validator.ts @@ -0,0 +1,36 @@ +import { FileValidator } from './file-validator.interface'; + +export type FileTypeValidatorOptions = { + fileType: string | RegExp; +}; + +/** + * Defines the built-in FileType File Validator. It validates incoming files mime-type + * matching a string or a regular expression. Note that this validator uses a naive strategy + * to check the mime-type and could be fooled if the client provided a file with renamed extension. + * (for instance, renaming a 'malicious.bat' to 'malicious.jpeg'). To handle such security issues + * with more reliability, consider checking against the file's [magic-numbers](https://en.wikipedia.org/wiki/Magic_number_%28programming%29) + * + * @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators) + * + * @publicApi + */ +export class FileTypeValidator extends FileValidator { + buildErrorMessage(): string { + return `Validation failed (expected type is ${this.validationOptions.fileType})`; + } + + isValid(file: any): boolean { + if (!this.validationOptions) { + return true; + } + + if (!file.mimetype) { + return false; + } + + return Boolean( + (file.mimetype as string).match(this.validationOptions.fileType), + ); + } +} diff --git a/packages/common/pipes/file/file-validator.interface.ts b/packages/common/pipes/file/file-validator.interface.ts new file mode 100644 index 00000000000..ad453cdae5f --- /dev/null +++ b/packages/common/pipes/file/file-validator.interface.ts @@ -0,0 +1,18 @@ +/** + * Interface describing FileValidators, which can be added to a {@link ParseFilePipe}. + */ +export abstract class FileValidator> { + constructor(protected readonly validationOptions: TValidationOptions) {} + + /** + * Indicates if this file should be considered valid, according to the options passed in the constructor. + * @param file the file from the request object + */ + abstract isValid(file?: any): boolean | Promise; + + /** + * Builds an error message in case the validation fails. + * @param file the file from the request object + */ + abstract buildErrorMessage(file: any): string; +} diff --git a/packages/common/pipes/file/index.ts b/packages/common/pipes/file/index.ts new file mode 100644 index 00000000000..11d19e3188e --- /dev/null +++ b/packages/common/pipes/file/index.ts @@ -0,0 +1,6 @@ +export * from './file-type.validator'; +export * from './file-validator.interface'; +export * from './max-file-size.validator'; +export * from './parse-file-options.interface'; +export * from './parse-file.pipe'; +export * from './parse-file-pipe.builder'; diff --git a/packages/common/pipes/file/max-file-size.validator.ts b/packages/common/pipes/file/max-file-size.validator.ts new file mode 100644 index 00000000000..847ee19f07b --- /dev/null +++ b/packages/common/pipes/file/max-file-size.validator.ts @@ -0,0 +1,26 @@ +import { FileValidator } from './file-validator.interface'; + +export type MaxFileSizeValidatorOptions = { + maxSize: number; +}; + +/** + * Defines the built-in MaxSize File Validator + * + * @see [File Validators](https://docs.nestjs.com/techniques/file-upload#validators) + * + * @publicApi + */ +export class MaxFileSizeValidator extends FileValidator { + buildErrorMessage(): string { + return `Validation failed (expected size is less than ${this.validationOptions.maxSize})`; + } + + public isValid(file: any): boolean { + if (!this.validationOptions) { + return true; + } + + return file.size < this.validationOptions.maxSize; + } +} diff --git a/packages/common/pipes/file/parse-file-options.interface.ts b/packages/common/pipes/file/parse-file-options.interface.ts new file mode 100644 index 00000000000..e74630999a6 --- /dev/null +++ b/packages/common/pipes/file/parse-file-options.interface.ts @@ -0,0 +1,8 @@ +import { ErrorHttpStatusCode } from '../../utils/http-error-by-code.util'; +import { FileValidator } from './file-validator.interface'; + +export interface ParseFileOptions { + validators?: FileValidator[]; + errorHttpStatusCode?: ErrorHttpStatusCode; + exceptionFactory?: (error: string) => any; +} diff --git a/packages/common/pipes/file/parse-file-pipe.builder.ts b/packages/common/pipes/file/parse-file-pipe.builder.ts new file mode 100644 index 00000000000..7835322d972 --- /dev/null +++ b/packages/common/pipes/file/parse-file-pipe.builder.ts @@ -0,0 +1,37 @@ +import { + FileTypeValidator, + FileTypeValidatorOptions, +} from './file-type.validator'; +import { FileValidator } from './file-validator.interface'; +import { + MaxFileSizeValidator, + MaxFileSizeValidatorOptions, +} from './max-file-size.validator'; +import { ParseFileOptions } from './parse-file-options.interface'; +import { ParseFilePipe } from './parse-file.pipe'; + +export class ParseFilePipeBuilder { + private validators: FileValidator[] = []; + + addMaxSizeValidator(options: MaxFileSizeValidatorOptions) { + this.validators.push(new MaxFileSizeValidator(options)); + return this; + } + + addFileTypeValidator(options: FileTypeValidatorOptions) { + this.validators.push(new FileTypeValidator(options)); + return this; + } + + build( + additionalOptions?: Omit, + ): ParseFilePipe { + const parseFilePipe = new ParseFilePipe({ + ...additionalOptions, + validators: this.validators, + }); + + this.validators = []; + return parseFilePipe; + } +} diff --git a/packages/common/pipes/file/parse-file.pipe.ts b/packages/common/pipes/file/parse-file.pipe.ts new file mode 100644 index 00000000000..e952c38f529 --- /dev/null +++ b/packages/common/pipes/file/parse-file.pipe.ts @@ -0,0 +1,68 @@ +import { Injectable, Optional } from '../../decorators/core'; +import { HttpStatus } from '../../enums'; +import { HttpErrorByCode } from '../../utils/http-error-by-code.util'; +import { PipeTransform } from '../../interfaces/features/pipe-transform.interface'; +import { ParseFileOptions } from './parse-file-options.interface'; +import { FileValidator } from './file-validator.interface'; +import { throws } from 'assert'; + +/** + * Defines the built-in ParseFile Pipe. This pipe can be used to validate incoming files + * with `@UploadedFile()` decorator. You can use either other specific built-in validators + * or provide one of your own, simply implementing it through {@link FileValidator} + * interface and adding it to ParseFilePipe's constructor. + * + * @see [Built-in Pipes](https://docs.nestjs.com/pipes#built-in-pipes) + * + * @publicApi + */ +@Injectable() +export class ParseFilePipe implements PipeTransform { + protected exceptionFactory: (error: string) => any; + private readonly validators: FileValidator[]; + + constructor(@Optional() options: ParseFileOptions = {}) { + const { + exceptionFactory, + errorHttpStatusCode = HttpStatus.BAD_REQUEST, + validators = [], + } = options; + + this.exceptionFactory = + exceptionFactory || + (error => new HttpErrorByCode[errorHttpStatusCode](error)); + + this.validators = validators; + } + + async transform(value: any): Promise { + if (this.validators.length) { + await this.validate(value); + } + return value; + } + + protected async validate(file: any): Promise { + for (const validator of this.validators) { + await this.validateOrThrow(file, validator); + } + + return file; + } + + private async validateOrThrow(file: any, validator: FileValidator) { + const isValid = await validator.isValid(file); + + if (!isValid) { + const errorMessage = validator.buildErrorMessage(file); + throw this.exceptionFactory(errorMessage); + } + } + + /** + * @returns list of validators used in this pipe. + */ + getValidators() { + return this.validators; + } +} diff --git a/packages/common/pipes/index.ts b/packages/common/pipes/index.ts index a6936549377..ebbc55f272a 100644 --- a/packages/common/pipes/index.ts +++ b/packages/common/pipes/index.ts @@ -6,3 +6,4 @@ export * from './parse-float.pipe'; export * from './parse-enum.pipe'; export * from './parse-uuid.pipe'; export * from './validation.pipe'; +export * from './file'; diff --git a/packages/common/pipes/parse-uuid.pipe.ts b/packages/common/pipes/parse-uuid.pipe.ts index 2679e8f93ad..81fe2b4c14b 100644 --- a/packages/common/pipes/parse-uuid.pipe.ts +++ b/packages/common/pipes/parse-uuid.pipe.ts @@ -9,7 +9,7 @@ import { ErrorHttpStatusCode, HttpErrorByCode, } from '../utils/http-error-by-code.util'; -import { isUUID } from '../utils/is-uuid'; +import { isString } from '../utils/shared.utils'; export interface ParseUUIDPipeOptions { version?: '3' | '4' | '5'; @@ -19,6 +19,12 @@ export interface ParseUUIDPipeOptions { @Injectable() export class ParseUUIDPipe implements PipeTransform { + protected static uuidRegExps = { + 3: /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i, + 4: /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, + 5: /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, + all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i, + }; private readonly version: '3' | '4' | '5'; protected exceptionFactory: (errors: string) => any; @@ -35,14 +41,23 @@ export class ParseUUIDPipe implements PipeTransform { exceptionFactory || (error => new HttpErrorByCode[errorHttpStatusCode](error)); } + async transform(value: string, metadata: ArgumentMetadata): Promise { - if (!isUUID(value, this.version)) { + if (!this.isUUID(value, this.version)) { throw this.exceptionFactory( - `Validation failed (uuid ${ - this.version ? 'v' + this.version : '' + `Validation failed (uuid${ + this.version ? ` v ${this.version}` : '' } is expected)`, ); } return value; } + + protected isUUID(str: unknown, version = 'all') { + if (!isString(str)) { + throw this.exceptionFactory('The value passed as UUID is not a string'); + } + const pattern = ParseUUIDPipe.uuidRegExps[version]; + return pattern?.test(str); + } } diff --git a/packages/common/services/logger.service.ts b/packages/common/services/logger.service.ts index 962822b13cf..6f7f38712a8 100644 --- a/packages/common/services/logger.service.ts +++ b/packages/common/services/logger.service.ts @@ -270,7 +270,7 @@ export class Logger implements LoggerService { } if (isObject(logger)) { if (logger instanceof Logger && logger.constructor !== Logger) { - const errorMessage = `Using the "extends Logger" instruction is not allowed in Nest v8. Please, use "extends ConsoleLogger" instead.`; + const errorMessage = `Using the "extends Logger" instruction is not allowed in Nest v9. Please, use "extends ConsoleLogger" instead.`; this.staticInstanceRef.error(errorMessage); throw new Error(errorMessage); } diff --git a/packages/common/test/exceptions/http.exception.spec.ts b/packages/common/test/exceptions/http.exception.spec.ts index c9fbab7310d..a880c1bfc53 100644 --- a/packages/common/test/exceptions/http.exception.spec.ts +++ b/packages/common/test/exceptions/http.exception.spec.ts @@ -129,4 +129,15 @@ describe('HttpException', () => { }); }); }); + + describe('initCause', () => { + it('configures a cause when message is an instance of error', () => { + const message = new Error('Some Error'); + const error = new HttpException(message, 400); + expect(`${error}`).to.be.eql(`HttpException: ${message.message}`); + const { cause } = error; + + expect(cause).to.be.eql(message); + }); + }); }); diff --git a/packages/common/test/http/http.service.spec.ts b/packages/common/test/http/http.service.spec.ts deleted file mode 100644 index f8a6d95406a..00000000000 --- a/packages/common/test/http/http.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AxiosRequestConfig } from 'axios'; -import { expect } from 'chai'; -import { lastValueFrom } from 'rxjs'; -import { HttpService } from '../../http/http.service'; - -describe('HttpService', () => { - it('should not mutate user-given axios options object', done => { - const http = new HttpService({ get: () => Promise.resolve() } as any); - const options: AxiosRequestConfig = {}; - - lastValueFrom(http.get('/', options)).then(() => { - expect(options.cancelToken).to.be.undefined; - done(); - }); - }); -}); diff --git a/packages/common/test/module-utils/configurable-module.builder.spec.ts b/packages/common/test/module-utils/configurable-module.builder.spec.ts new file mode 100644 index 00000000000..3d36232718a --- /dev/null +++ b/packages/common/test/module-utils/configurable-module.builder.spec.ts @@ -0,0 +1,112 @@ +import { expect } from 'chai'; +import { Provider } from '../../interfaces'; +import { ConfigurableModuleBuilder } from '../../module-utils'; + +describe('ConfigurableModuleBuilder', () => { + describe('setExtras', () => { + it('should apply module definition transformer function and return typed builder', () => { + const { ConfigurableModuleClass } = new ConfigurableModuleBuilder() + .setExtras( + { isGlobal: false }, + (definition, extras: { isGlobal: boolean }) => ({ + ...definition, + global: extras.isGlobal, + }), + ) + .build(); + + expect( + ConfigurableModuleClass.register({ + // No type error + isGlobal: true, + }), + ).to.deep.include({ + global: true, + }); + }); + }); + describe('setClassMethodName', () => { + it('should set static class method name and return typed builder', () => { + const { ConfigurableModuleClass } = new ConfigurableModuleBuilder() + .setClassMethodName('forRoot') + .build(); + + expect(ConfigurableModuleClass.forRoot).to.not.be.undefined; + expect(ConfigurableModuleClass.forRootAsync).to.not.be.undefined; + expect((ConfigurableModuleClass as any).register).to.be.undefined; + }); + }); + describe('setFactoryMethodName', () => { + it('should set configuration factory class method name and return typed builder', () => { + const { ConfigurableModuleClass } = new ConfigurableModuleBuilder() + .setFactoryMethodName('createOptions') + .build(); + + expect( + ConfigurableModuleClass.registerAsync({ + useClass: class { + // No type error + createOptions() {} + }, + }), + ).to.not.be.undefined; + }); + }); + describe('build', () => { + it('should return a fully typed "ConfigurableModuleClass"', () => { + type ExtraConfig = { isGlobal?: boolean; extraProviders: Provider[] }; + + const { + ConfigurableModuleClass, + OPTIONS_TYPE, + ASYNC_OPTIONS_TYPE, + MODULE_OPTIONS_TOKEN, + } = new ConfigurableModuleBuilder({ + moduleName: 'RandomTest', + alwaysTransient: true, + }) + .setFactoryMethodName('createOptions') + .setClassMethodName('forFeature') + .setExtras( + { isGlobal: false, extraProviders: [] }, + (definition, extras) => ({ + ...definition, + global: extras.isGlobal, + providers: definition.providers?.concat(extras.extraProviders), + }), + ) + .build(); + + const definition = ConfigurableModuleClass.forFeatureAsync({ + useFactory: () => {}, + isGlobal: true, + extraProviders: ['test' as any], + }); + + expect(definition.global).to.equal(true); + expect(definition.providers).to.have.length(3); + expect(definition.providers).to.deep.contain('test'); + expect(MODULE_OPTIONS_TOKEN).to.equal('RANDOM_TEST_MODULE_OPTIONS'); + expect((definition.providers[0] as any).provide).to.equal( + 'RANDOM_TEST_MODULE_OPTIONS', + ); + + try { + expect(ASYNC_OPTIONS_TYPE.imports).to.equal(undefined); + } catch (err) { + expect(err).to.be.instanceOf(Error); + expect(err.message).to.equal( + '"ASYNC_OPTIONS_TYPE" is not supposed to be used as a value.', + ); + } + try { + expect(OPTIONS_TYPE.isGlobal).to.equal(undefined); + } catch (err) { + expect(err).to.be.instanceOf(Error); + expect(err.message).to.equal( + '"OPTIONS_TYPE" is not supposed to be used as a value.', + ); + } + }); + }); +}); diff --git a/packages/common/test/pipes/file/file-type.validator.spec.ts b/packages/common/test/pipes/file/file-type.validator.spec.ts new file mode 100644 index 00000000000..8b58d114033 --- /dev/null +++ b/packages/common/test/pipes/file/file-type.validator.spec.ts @@ -0,0 +1,89 @@ +import { FileTypeValidator } from '../../../pipes'; +import { expect } from 'chai'; + +describe('FileTypeValidator', () => { + describe('isValid', () => { + it('should return true when the file mimetype is the same as the specified', () => { + const fileTypeValidator = new FileTypeValidator({ + fileType: 'image/jpeg', + }); + + const requestFile = { + mimetype: 'image/jpeg', + }; + + expect(fileTypeValidator.isValid(requestFile)).to.equal(true); + }); + + it('should return true when the file mimetype ends with the specified option type', () => { + const fileTypeValidator = new FileTypeValidator({ + fileType: 'jpeg', + }); + + const requestFile = { + mimetype: 'image/jpeg', + }; + + expect(fileTypeValidator.isValid(requestFile)).to.equal(true); + }); + + it('should return true when the file mimetype matches the specified regexp', () => { + const fileTypeValidator = new FileTypeValidator({ + fileType: /word/, + }); + + const requestFile = { + mimetype: 'application/msword', + }; + + expect(fileTypeValidator.isValid(requestFile)).to.equal(true); + }); + + it('should return false when the file mimetype is different from the specified', () => { + const fileTypeValidator = new FileTypeValidator({ + fileType: 'image/jpeg', + }); + + const requestFile = { + mimetype: 'image/png', + }; + + expect(fileTypeValidator.isValid(requestFile)).to.equal(false); + }); + + it('should return false when the file mimetype does not match the provided regexp', () => { + const fileTypeValidator = new FileTypeValidator({ + fileType: /mp4/, + }); + + const requestFile = { + mimetype: 'image/png', + }; + + expect(fileTypeValidator.isValid(requestFile)).to.equal(false); + }); + + it('should return false when the file mimetype was not provided', () => { + const fileTypeValidator = new FileTypeValidator({ + fileType: 'image/jpeg', + }); + + const requestFile = {}; + + expect(fileTypeValidator.isValid(requestFile)).to.equal(false); + }); + }); + + describe('buildErrorMessage', () => { + it('should return a string with the format "Validation failed (expected type is #fileType)"', () => { + const fileType = 'image/jpeg'; + const fileTypeValidator = new FileTypeValidator({ + fileType, + }); + + expect(fileTypeValidator.buildErrorMessage()).to.equal( + `Validation failed (expected type is ${fileType})`, + ); + }); + }); +}); diff --git a/packages/common/test/pipes/file/max-file-size.validator.spec.ts b/packages/common/test/pipes/file/max-file-size.validator.spec.ts new file mode 100644 index 00000000000..9ba8e4283bc --- /dev/null +++ b/packages/common/test/pipes/file/max-file-size.validator.spec.ts @@ -0,0 +1,56 @@ +import { expect } from 'chai'; +import { MaxFileSizeValidator } from '../../../pipes'; + +describe('MaxFileSizeValidator', () => { + const oneKb = 1024; + + describe('isValid', () => { + it('should return true when the file size is less than the maximum size', () => { + const maxFileSizeValidator = new MaxFileSizeValidator({ + maxSize: oneKb, + }); + + const requestFile = { + size: 100, + }; + + expect(maxFileSizeValidator.isValid(requestFile)).to.equal(true); + }); + + it('should return false when the file size is greater than the maximum size', () => { + const maxFileSizeValidator = new MaxFileSizeValidator({ + maxSize: oneKb, + }); + + const requestFile = { + size: oneKb + 1, + }; + + expect(maxFileSizeValidator.isValid(requestFile)).to.equal(false); + }); + + it('should return false when the file size is equal to the maximum size', () => { + const maxFileSizeValidator = new MaxFileSizeValidator({ + maxSize: oneKb, + }); + + const requestFile = { + size: oneKb, + }; + + expect(maxFileSizeValidator.isValid(requestFile)).to.equal(false); + }); + }); + + describe('buildErrorMessage', () => { + it('should return a string with the format "Validation failed (expected size is less than #maxSize")', () => { + const maxFileSizeValidator = new MaxFileSizeValidator({ + maxSize: oneKb, + }); + + expect(maxFileSizeValidator.buildErrorMessage()).to.equal( + `Validation failed (expected size is less than ${oneKb})`, + ); + }); + }); +}); diff --git a/packages/common/test/pipes/file/parse-file-pipe.builder.spec.ts b/packages/common/test/pipes/file/parse-file-pipe.builder.spec.ts new file mode 100644 index 00000000000..8c31904aaaf --- /dev/null +++ b/packages/common/test/pipes/file/parse-file-pipe.builder.spec.ts @@ -0,0 +1,77 @@ +import { expect } from 'chai'; +import { + FileTypeValidator, + MaxFileSizeValidator, + ParseFilePipeBuilder, +} from '../../../pipes'; + +describe('ParseFilePipeBuilder', () => { + let parseFilePipeBuilder: ParseFilePipeBuilder; + + beforeEach(() => { + parseFilePipeBuilder = new ParseFilePipeBuilder(); + }); + + describe('build', () => { + describe('when no validator was passed', () => { + it('should return a ParseFilePipe with no validators', () => { + const parseFilePipe = parseFilePipeBuilder.build(); + expect(parseFilePipe.getValidators()).to.be.empty; + }); + }); + + describe('when addMaxSizeValidator was chained', () => { + it('should return a ParseFilePipe with MaxSizeValidator and given options', () => { + const options = { + maxSize: 1000, + }; + const parseFilePipe = parseFilePipeBuilder + .addMaxSizeValidator(options) + .build(); + + expect(parseFilePipe.getValidators()).to.deep.include( + new MaxFileSizeValidator(options), + ); + }); + }); + + describe('when addFileTypeValidator was chained', () => { + it('should return a ParseFilePipe with FileTypeValidator and given options', () => { + const options = { + fileType: 'image/jpeg', + }; + const parseFilePipe = parseFilePipeBuilder + .addFileTypeValidator(options) + .build(); + + expect(parseFilePipe.getValidators()).to.deep.include( + new FileTypeValidator(options), + ); + }); + }); + + describe('when it is called twice with different validators', () => { + it('should not reuse validators', () => { + const maxSizeValidatorOptions = { + maxSize: 1000, + }; + + const pipeWithMaxSizeValidator = parseFilePipeBuilder + .addMaxSizeValidator(maxSizeValidatorOptions) + .build(); + + const fileTypeValidatorOptions = { + fileType: 'image/jpeg', + }; + + const pipeWithFileTypeValidator = parseFilePipeBuilder + .addFileTypeValidator(fileTypeValidatorOptions) + .build(); + + expect(pipeWithFileTypeValidator.getValidators()).not.to.deep.equal( + pipeWithMaxSizeValidator.getValidators(), + ); + }); + }); + }); +}); diff --git a/packages/common/test/pipes/file/parse-file.pipe.spec.ts b/packages/common/test/pipes/file/parse-file.pipe.spec.ts new file mode 100644 index 00000000000..ee06ef44b8f --- /dev/null +++ b/packages/common/test/pipes/file/parse-file.pipe.spec.ts @@ -0,0 +1,120 @@ +import { HttpStatus } from '../../../enums'; +import { BadRequestException, ConflictException } from '../../../exceptions'; +import { FileValidator, ParseFilePipe } from '../../../pipes'; +import { expect } from 'chai'; + +class AlwaysValidValidator extends FileValidator { + isValid(): boolean { + return true; + } + buildErrorMessage(): string { + return ''; + } +} + +const customErrorMessage = 'Error!'; + +class AlwaysInvalidValidator extends FileValidator { + isValid(): boolean { + return false; + } + buildErrorMessage(): string { + return customErrorMessage; + } +} + +describe('ParseFilePipe', () => { + let parseFilePipe: ParseFilePipe; + describe('transform', () => { + describe('when there are no validators (explicit)', () => { + beforeEach(() => { + parseFilePipe = new ParseFilePipe({ + validators: [], + }); + }); + + it('should return the file object', async () => { + const requestFile = { + path: 'some-path', + }; + + await expect(parseFilePipe.transform(requestFile)).to.eventually.eql( + requestFile, + ); + }); + }); + + describe('when there are no validators (by default constructor)', () => { + beforeEach(() => { + parseFilePipe = new ParseFilePipe(); + }); + + it('should return the file object', async () => { + const requestFile = { + path: 'some-path', + }; + + await expect(parseFilePipe.transform(requestFile)).to.eventually.eql( + requestFile, + ); + }); + }); + + describe('when all the validators validate the file', () => { + beforeEach(() => { + parseFilePipe = new ParseFilePipe({ + validators: [new AlwaysValidValidator({})], + }); + }); + + it('should return the file object', async () => { + const requestFile = { + path: 'some-path', + }; + + await expect(parseFilePipe.transform(requestFile)).to.eventually.eql( + requestFile, + ); + }); + }); + + describe('when some validator invalidates the file', () => { + describe('and the pipe has the default error', () => { + beforeEach(() => { + parseFilePipe = new ParseFilePipe({ + validators: [new AlwaysInvalidValidator({})], + }); + }); + + it('should throw a BadRequestException', async () => { + const requestFile = { + path: 'some-path', + }; + + await expect(parseFilePipe.transform(requestFile)).to.be.rejectedWith( + BadRequestException, + ); + }); + }); + + describe('and the pipe has a custom error code', () => { + beforeEach(() => { + parseFilePipe = new ParseFilePipe({ + validators: [new AlwaysInvalidValidator({})], + errorHttpStatusCode: HttpStatus.CONFLICT, + }); + }); + + it('should throw this custom Error', async () => { + const requestFile = { + path: 'some-path', + }; + + await expect(parseFilePipe.transform(requestFile)).to.be.rejectedWith( + ConflictException, + ); + }); + }); + }); + }); +}); diff --git a/packages/common/test/pipes/parse-int.pipe.spec.ts b/packages/common/test/pipes/parse-int.pipe.spec.ts index 3bb7ab1857d..d9d78de27eb 100644 --- a/packages/common/test/pipes/parse-int.pipe.spec.ts +++ b/packages/common/test/pipes/parse-int.pipe.spec.ts @@ -1,4 +1,3 @@ -import * as sinon from 'sinon'; import { expect } from 'chai'; import { ArgumentMetadata } from '../../interfaces'; import { ParseIntPipe } from '../../pipes/parse-int.pipe'; diff --git a/packages/common/test/pipes/parse-uuid.pipe.spec.ts b/packages/common/test/pipes/parse-uuid.pipe.spec.ts index 3f6cb79fb1b..bcb3a76b39a 100644 --- a/packages/common/test/pipes/parse-uuid.pipe.spec.ts +++ b/packages/common/test/pipes/parse-uuid.pipe.spec.ts @@ -1,9 +1,18 @@ import { expect } from 'chai'; +import { HttpStatus } from '../../enums'; +import { HttpException } from '../../exceptions'; import { ArgumentMetadata } from '../../interfaces'; import { ParseUUIDPipe } from '../../pipes/parse-uuid.pipe'; +class TestException extends HttpException { + constructor() { + super('This is a TestException', HttpStatus.I_AM_A_TEAPOT); + } +} + describe('ParseUUIDPipe', () => { let target: ParseUUIDPipe; + const exceptionFactory = (error: any) => new TestException(); describe('transform', () => { const v3 = 'e8b5a51d-11c8-3310-a6ab-367563f20686'; @@ -12,53 +21,80 @@ describe('ParseUUIDPipe', () => { describe('when validation passes', () => { it('should return string if value is uuid v3, v4 or v5', async () => { - target = new ParseUUIDPipe(); + target = new ParseUUIDPipe({ exceptionFactory }); expect(await target.transform(v3, {} as ArgumentMetadata)).to.equal(v3); expect(await target.transform(v4, {} as ArgumentMetadata)).to.equal(v4); expect(await target.transform(v5, {} as ArgumentMetadata)).to.equal(v5); }); it('should return string if value is uuid v3', async () => { - target = new ParseUUIDPipe({ version: '3' }); + target = new ParseUUIDPipe({ version: '3', exceptionFactory }); expect(await target.transform(v3, {} as ArgumentMetadata)).to.equal(v3); }); it('should return string if value is uuid v4', async () => { - target = new ParseUUIDPipe({ version: '4' }); + target = new ParseUUIDPipe({ version: '4', exceptionFactory }); expect(await target.transform(v4, {} as ArgumentMetadata)).to.equal(v4); }); it('should return string if value is uuid v5', async () => { - target = new ParseUUIDPipe({ version: '5' }); + target = new ParseUUIDPipe({ version: '5', exceptionFactory }); expect(await target.transform(v5, {} as ArgumentMetadata)).to.equal(v5); }); }); describe('when validation fails', () => { it('should throw an error', async () => { - target = new ParseUUIDPipe(); - expect(target.transform('123a', {} as ArgumentMetadata)).to.be.rejected; + target = new ParseUUIDPipe({ exceptionFactory }); + await expect( + target.transform('123a', {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); + }); + + it('should throw an error - not a string', async () => { + target = new ParseUUIDPipe({ exceptionFactory }); + await expect( + target.transform(undefined, {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); }); it('should throw an error - v3', async () => { - target = new ParseUUIDPipe({ version: '3' }); - expect(target.transform('123a', {} as ArgumentMetadata)).to.be.rejected; - expect(target.transform(v4, {} as ArgumentMetadata)).to.be.rejected; - expect(target.transform(v5, {} as ArgumentMetadata)).to.be.rejected; + target = new ParseUUIDPipe({ version: '3', exceptionFactory }); + await expect( + target.transform('123a', {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); + await expect( + target.transform(v4, {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); + await expect( + target.transform(v5, {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); }); it('should throw an error - v4', async () => { - target = new ParseUUIDPipe({ version: '4' }); - expect(target.transform('123a', {} as ArgumentMetadata)).to.be.rejected; - expect(target.transform(v3, {} as ArgumentMetadata)).to.be.rejected; - expect(target.transform(v5, {} as ArgumentMetadata)).to.be.rejected; + target = new ParseUUIDPipe({ version: '4', exceptionFactory }); + await expect( + target.transform('123a', {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); + await expect( + target.transform(v3, {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); + await expect( + target.transform(v5, {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); }); it('should throw an error - v5 ', async () => { - target = new ParseUUIDPipe({ version: '5' }); - expect(target.transform('123a', {} as ArgumentMetadata)).to.be.rejected; - expect(target.transform(v3, {} as ArgumentMetadata)).to.be.rejected; - expect(target.transform(v4, {} as ArgumentMetadata)).to.be.rejected; + target = new ParseUUIDPipe({ version: '5', exceptionFactory }); + await expect( + target.transform('123a', {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); + await expect( + target.transform(v3, {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); + await expect( + target.transform(v4, {} as ArgumentMetadata), + ).to.be.rejectedWith(TestException); }); }); }); diff --git a/packages/common/utils/cli-colors.util.ts b/packages/common/utils/cli-colors.util.ts index 8efd6167054..738fbc6d337 100644 --- a/packages/common/utils/cli-colors.util.ts +++ b/packages/common/utils/cli-colors.util.ts @@ -5,6 +5,7 @@ const colorIfAllowed = (colorFn: ColorTextFn) => (text: string) => isColorAllowed() ? colorFn(text) : text; export const clc = { + bold: colorIfAllowed((text: string) => `\x1B[1m${text}\x1B[0m`), green: colorIfAllowed((text: string) => `\x1B[32m${text}\x1B[39m`), yellow: colorIfAllowed((text: string) => `\x1B[33m${text}\x1B[39m`), red: colorIfAllowed((text: string) => `\x1B[31m${text}\x1B[39m`), diff --git a/packages/common/utils/is-uuid.ts b/packages/common/utils/is-uuid.ts deleted file mode 100644 index 0e9572f4d61..00000000000 --- a/packages/common/utils/is-uuid.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BadRequestException } from '../exceptions'; -import { isString } from './shared.utils'; - -const uuid = { - 3: /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i, - 4: /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, - 5: /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i, - all: /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i, -}; - -export function isUUID(str: any, version = 'all') { - if (!isString(str)) { - throw new BadRequestException('The value passed as UUID is not a string'); - } - const pattern = uuid[version]; - return pattern && pattern.test(str); -} diff --git a/packages/common/utils/shared.utils.ts b/packages/common/utils/shared.utils.ts index cee2e09730e..4f9ff6eeac1 100644 --- a/packages/common/utils/shared.utils.ts +++ b/packages/common/utils/shared.utils.ts @@ -31,12 +31,6 @@ export const addLeadingSlash = (path?: string): string => : path : ''; -/** - * Deprecated. Use the "addLeadingSlash" function instead. - * @deprecated - */ -export const validatePath = addLeadingSlash; - export const normalizePath = (path?: string): string => path ? path.startsWith('/') diff --git a/packages/core/adapters/http-adapter.ts b/packages/core/adapters/http-adapter.ts index 526afab3890..ab94905a264 100644 --- a/packages/core/adapters/http-adapter.ts +++ b/packages/core/adapters/http-adapter.ts @@ -1,5 +1,5 @@ -import { HttpServer, RequestMethod } from '@nestjs/common'; -import { RequestHandler } from '@nestjs/common/interfaces'; +import { HttpServer, RequestMethod, VersioningOptions } from '@nestjs/common'; +import { RequestHandler, VersionValue } from '@nestjs/common/interfaces'; import { CorsOptions, CorsOptionsDelegate, @@ -105,10 +105,12 @@ export abstract class AbstractHttpAdapter< abstract getRequestUrl(request); abstract status(response, statusCode: number); abstract reply(response, body: any, statusCode?: number); + abstract end(response, message?: string); abstract render(response, view: string, options: any); abstract redirect(response, statusCode: number, url: string); abstract setErrorHandler(handler: Function, prefix?: string); abstract setNotFoundHandler(handler: Function, prefix?: string); + abstract isHeadersSent(response); abstract setHeader(response, name: string, value: string); abstract registerParserMiddleware(prefix?: string, rawBody?: boolean); abstract enableCors( @@ -121,4 +123,9 @@ export abstract class AbstractHttpAdapter< | ((path: string, callback: Function) => any) | Promise<(path: string, callback: Function) => any>; abstract getType(): string; + abstract applyVersionFilter( + handler: Function, + version: VersionValue, + versioningOptions: VersioningOptions, + ): (req: TRequest, res: TResponse, next: () => void) => Function; } diff --git a/packages/core/errors/messages.ts b/packages/core/errors/messages.ts index 20926035929..b0a26e8d1d3 100644 --- a/packages/core/errors/messages.ts +++ b/packages/core/errors/messages.ts @@ -121,11 +121,11 @@ export const USING_INVALID_CLASS_AS_A_MODULE_MESSAGE = ( metatypeUsedAsAModule: Type | ForwardReference, scope: any[], ) => { - const metatypeName = getInstanceName(metatypeUsedAsAModule) || 'found'; + const metatypeNameQuote = + `"${getInstanceName(metatypeUsedAsAModule)}"` || 'that class'; - // TODO(v9): Edit the message below: - return `In the next major version, Nest will not allow classes annotated with @Injectable(), @Catch(), and @Controller() decorators to appear in the "imports" array of a module. -Please remove "${metatypeName}" (including forwarded occurrences, if any) from all of the "imports" arrays. + return `Classes annotated with @Injectable(), @Catch(), and @Controller() decorators must not appear in the "imports" array of a module. +Please remove ${metatypeNameQuote} (including forwarded occurrences, if any) from all of the "imports" arrays. Scope [${stringifyScope(scope)}] `; diff --git a/packages/core/exceptions/base-exception-filter.ts b/packages/core/exceptions/base-exception-filter.ts index ea360e84a69..bc378450120 100644 --- a/packages/core/exceptions/base-exception-filter.ts +++ b/packages/core/exceptions/base-exception-filter.ts @@ -38,7 +38,12 @@ export class BaseExceptionFilter implements ExceptionFilter { message: res, }; - applicationRef.reply(host.getArgByIndex(1), message, exception.getStatus()); + const response = host.getArgByIndex(1); + if (!applicationRef.isHeadersSent(response)) { + applicationRef.reply(response, message, exception.getStatus()); + } else { + applicationRef.end(response); + } } public handleUnknownError( @@ -55,7 +60,14 @@ export class BaseExceptionFilter implements ExceptionFilter { statusCode: HttpStatus.INTERNAL_SERVER_ERROR, message: MESSAGES.UNKNOWN_EXCEPTION_MESSAGE, }; - applicationRef.reply(host.getArgByIndex(1), body, body.statusCode); + + const response = host.getArgByIndex(1); + if (!applicationRef.isHeadersSent(response)) { + applicationRef.reply(response, body, body.statusCode); + } else { + applicationRef.end(response); + } + if (this.isExceptionObject(exception)) { return BaseExceptionFilter.logger.error( exception.message, diff --git a/packages/core/helpers/context-id-factory.ts b/packages/core/helpers/context-id-factory.ts index 99506220bfd..4b1119e3b9a 100644 --- a/packages/core/helpers/context-id-factory.ts +++ b/packages/core/helpers/context-id-factory.ts @@ -1,4 +1,4 @@ -import { ContextId } from '../injector/instance-wrapper'; +import { ContextId, HostComponentInfo } from '../injector/instance-wrapper'; import { REQUEST_CONTEXT_ID } from '../router/request/request-constants'; export function createContextId(): ContextId { @@ -13,7 +13,23 @@ export function createContextId(): ContextId { return { id: Math.random() }; } +export interface ContextIdStrategy { + /** + * Allows to attach a parent context id to the existing child context id. + * This lets you construct durable DI sub-trees that can be shared between contexts. + * @param contextId auto-generated child context id + * @param request request object + * @returns a context id resolver function + */ + attach( + contextId: ContextId, + request: T, + ): ((info: HostComponentInfo) => ContextId) | undefined; +} + export class ContextIdFactory { + private static strategy?: ContextIdStrategy; + /** * Generates a context identifier based on the request object. */ @@ -40,6 +56,20 @@ export class ContextIdFactory { return request[key][REQUEST_CONTEXT_ID]; } } - return ContextIdFactory.create(); + if (!this.strategy) { + return ContextIdFactory.create(); + } + const contextId = createContextId(); + contextId.getParent = this.strategy.attach(contextId, request); + return contextId; + } + + /** + * Registers a custom context id strategy that lets you attach + * a parent context id to the existing context id object. + * @param strategy strategy instance + */ + public static apply(strategy: ContextIdStrategy) { + this.strategy = strategy; } } diff --git a/packages/core/helpers/is-durable.ts b/packages/core/helpers/is-durable.ts new file mode 100644 index 00000000000..377c84f180d --- /dev/null +++ b/packages/core/helpers/is-durable.ts @@ -0,0 +1,7 @@ +import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants'; +import { Type } from '@nestjs/common/interfaces/type.interface'; + +export function isDurable(provider: Type): boolean | undefined { + const metadata = Reflect.getMetadata(SCOPE_OPTIONS_METADATA, provider); + return metadata && metadata.durable; +} diff --git a/packages/core/index.ts b/packages/core/index.ts index ebc30ca4ca7..1babf6c15f7 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -18,5 +18,6 @@ export * from './middleware'; export * from './nest-application'; export * from './nest-application-context'; export { NestFactory } from './nest-factory'; +export * from './repl'; export * from './router'; export * from './services'; diff --git a/packages/core/injector/index.ts b/packages/core/injector/index.ts index 6eb81c89c68..4abbe0bb6ef 100644 --- a/packages/core/injector/index.ts +++ b/packages/core/injector/index.ts @@ -1,6 +1,6 @@ export * from './container'; export * from './inquirer'; -export { ContextId } from './instance-wrapper'; +export { ContextId, HostComponentInfo } from './instance-wrapper'; export * from './lazy-module-loader'; export * from './module-ref'; export * from './modules-container'; diff --git a/packages/core/injector/injector.ts b/packages/core/injector/injector.ts index d4e8c232dc4..a0d68f5d62b 100644 --- a/packages/core/injector/injector.ts +++ b/packages/core/injector/injector.ts @@ -106,9 +106,16 @@ export class Injector { inquirer?: InstanceWrapper, ) { const inquirerId = this.getInquirerId(inquirer); - const instanceHost = wrapper.getInstanceByContextId(contextId, inquirerId); + const instanceHost = wrapper.getInstanceByContextId( + this.getContextId(contextId, wrapper), + inquirerId, + ); if (instanceHost.isPending) { - return instanceHost.donePromise; + return instanceHost.donePromise.then((err?: unknown) => { + if (err) { + throw err; + } + }); } const done = this.applyDoneHook(instanceHost); const token = wrapper.token || wrapper.name; @@ -121,34 +128,39 @@ export class Injector { if (instanceHost.isResolved) { return done(); } - const callback = async (instances: unknown[]) => { - const properties = await this.resolveProperties( + try { + const callback = async (instances: unknown[]) => { + const properties = await this.resolveProperties( + wrapper, + moduleRef, + inject as InjectionToken[], + contextId, + wrapper, + inquirer, + ); + const instance = await this.instantiateClass( + instances, + wrapper, + targetWrapper, + contextId, + inquirer, + ); + this.applyProperties(instance, properties); + done(); + }; + await this.resolveConstructorParams( wrapper, moduleRef, inject as InjectionToken[], + callback, contextId, wrapper, inquirer, ); - const instance = await this.instantiateClass( - instances, - wrapper, - targetWrapper, - contextId, - inquirer, - ); - this.applyProperties(instance, properties); - done(); - }; - await this.resolveConstructorParams( - wrapper, - moduleRef, - inject as InjectionToken[], - callback, - contextId, - wrapper, - inquirer, - ); + } catch (err) { + done(err); + throw err; + } } public async loadMiddleware( @@ -222,9 +234,11 @@ export class Injector { await this.loadEnhancersPerContext(wrapper, contextId, wrapper); } - public applyDoneHook(wrapper: InstancePerContext): () => void { - let done: () => void; - wrapper.donePromise = new Promise((resolve, reject) => { + public applyDoneHook( + wrapper: InstancePerContext, + ): (err?: unknown) => void { + let done: (err?: unknown) => void; + wrapper.donePromise = new Promise((resolve, reject) => { done = resolve; }); wrapper.isPending = true; @@ -278,7 +292,7 @@ export class Injector { index, ); const instanceHost = paramWrapper.getInstanceByContextId( - contextId, + this.getContextId(contextId, paramWrapper), inquirerId, ); if (!instanceHost.isResolved && !paramWrapper.forwardRef) { @@ -434,7 +448,7 @@ export class Injector { ): Promise { const inquirerId = this.getInquirerId(inquirer); const instanceHost = instanceWrapper.getInstanceByContextId( - contextId, + this.getContextId(contextId, instanceWrapper), inquirerId, ); if (!instanceHost.isResolved && !instanceWrapper.forwardRef) { @@ -463,7 +477,7 @@ export class Injector { } if (instanceWrapper.async) { const host = instanceWrapper.getInstanceByContextId( - contextId, + this.getContextId(contextId, instanceWrapper), inquirerId, ); host.instance = await host.instance; @@ -584,7 +598,7 @@ export class Injector { const inquirerId = this.getInquirerId(inquirer); const instanceHost = instanceWrapperRef.getInstanceByContextId( - contextId, + this.getContextId(contextId, instanceWrapperRef), inquirerId, ); if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) { @@ -640,7 +654,7 @@ export class Injector { } const inquirerId = this.getInquirerId(inquirer); const instanceHost = paramWrapper.getInstanceByContextId( - contextId, + this.getContextId(contextId, paramWrapper), inquirerId, ); return instanceHost.instance; @@ -692,7 +706,7 @@ export class Injector { const { metatype, inject } = wrapper; const inquirerId = this.getInquirerId(inquirer); const instanceHost = targetMetatype.getInstanceByContextId( - contextId, + this.getContextId(contextId, targetMetatype), inquirerId, ); const isInContext = @@ -732,7 +746,10 @@ export class Injector { await this.loadInstance(wrapper, collection, moduleRef, ctx, wrapper); await this.loadEnhancersPerContext(wrapper, ctx, wrapper); - const host = wrapper.getInstanceByContextId(ctx, wrapper.id); + const host = wrapper.getInstanceByContextId( + this.getContextId(ctx, wrapper), + wrapper.id, + ); return host && (host.instance as T); } @@ -773,7 +790,11 @@ export class Injector { ); const inquirerId = this.getInquirerId(inquirer); return hosts.map( - item => item.getInstanceByContextId(contextId, inquirerId).instance, + item => + item.getInstanceByContextId( + this.getContextId(contextId, item), + inquirerId, + ).instance, ); } @@ -797,7 +818,10 @@ export class Injector { return dependenciesHosts.map(({ key, host }) => ({ key, name: key, - instance: host.getInstanceByContextId(contextId, inquirerId).instance, + instance: host.getInstanceByContextId( + this.getContextId(contextId, host), + inquirerId, + ).instance, })); } @@ -899,4 +923,16 @@ export class Injector { private isDebugMode(): boolean { return !!process.env.NEST_DEBUG; } + + private getContextId( + contextId: ContextId, + instanceWrapper: InstanceWrapper, + ): ContextId { + return contextId.getParent + ? contextId.getParent({ + token: instanceWrapper.token, + isTreeDurable: instanceWrapper.isDependencyTreeDurable(), + }) + : contextId; + } } diff --git a/packages/core/injector/instance-wrapper.ts b/packages/core/injector/instance-wrapper.ts index 239e89dea7b..2357ac1988c 100644 --- a/packages/core/injector/instance-wrapper.ts +++ b/packages/core/injector/instance-wrapper.ts @@ -1,11 +1,16 @@ -import { Provider, Scope, Type } from '@nestjs/common'; +import { Logger, LoggerService, Provider, Scope, Type } from '@nestjs/common'; import { ClassProvider, FactoryProvider, ValueProvider, } from '@nestjs/common/interfaces'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; -import { isNil, isUndefined } from '@nestjs/common/utils/shared.utils'; +import { + isNil, + isString, + isUndefined, +} from '@nestjs/common/utils/shared.utils'; import { iterate } from 'iterare'; import { STATIC_CONTEXT } from './constants'; import { InstanceToken, Module } from './module'; @@ -13,16 +18,29 @@ import { InstanceToken, Module } from './module'; export const INSTANCE_METADATA_SYMBOL = Symbol.for('instance_metadata:cache'); export const INSTANCE_ID_SYMBOL = Symbol.for('instance_metadata:id'); +export interface HostComponentInfo { + /** + * Injection token (or class reference) + */ + token: InstanceToken; + /** + * Flag that indicates whether DI subtree is durable + */ + isTreeDurable: boolean; +} + export interface ContextId { readonly id: number; + getParent?(info: HostComponentInfo): ContextId; } export interface InstancePerContext { instance: T; isResolved?: boolean; isPending?: boolean; - donePromise?: Promise; + donePromise?: Promise; } + export interface PropertyMetadata { key: symbol | string; wrapper: InstanceWrapper; @@ -45,6 +63,9 @@ export class InstanceWrapper { public metatype: Type | Function; public inject?: FactoryProvider['inject']; public forwardRef?: boolean; + public durable?: boolean; + + private static logger: LoggerService = new Logger(InstanceWrapper.name); private readonly values = new WeakMap>(); private readonly [INSTANCE_METADATA_SYMBOL]: InstanceMetadataStore = {}; @@ -53,6 +74,7 @@ export class InstanceWrapper { | Map>> | undefined; private isTreeStatic: boolean | undefined; + private isTreeDurable: boolean | undefined; constructor( metadata: Partial> & Partial> = {}, @@ -171,14 +193,40 @@ export class InstanceWrapper { return this[INSTANCE_METADATA_SYMBOL].enhancers; } - public isDependencyTreeStatic(lookupRegistry: string[] = []): boolean { - if (!isUndefined(this.isTreeStatic)) { - return this.isTreeStatic; + public isDependencyTreeDurable(lookupRegistry: string[] = []): boolean { + if (!isUndefined(this.isTreeDurable)) { + return this.isTreeDurable; } - if (this.scope === Scope.REQUEST) { - this.isTreeStatic = false; - return this.isTreeStatic; + if (this.durable === true) { + this.isTreeDurable = true; + this.printIntrospectedAsDurable(); + return this.isTreeDurable; + } + const isStatic = this.isDependencyTreeStatic(); + if (isStatic) { + return false; } + const isTreeNonDurable = this.introspectDepsAttribute( + (collection, registry) => + collection.every( + (item: InstanceWrapper) => !item.isDependencyTreeDurable(registry), + ), + lookupRegistry, + ); + this.isTreeDurable = !isTreeNonDurable && this.durable !== false; + if (this.isTreeDurable) { + this.printIntrospectedAsDurable(); + } + return this.isTreeDurable; + } + + public introspectDepsAttribute( + callback: ( + collection: InstanceWrapper[], + lookupRegistry: string[], + ) => boolean, + lookupRegistry: string[] = [], + ): boolean { if (lookupRegistry.includes(this[INSTANCE_ID_SYMBOL])) { return true; } @@ -186,23 +234,41 @@ export class InstanceWrapper { const { dependencies, properties, enhancers } = this[INSTANCE_METADATA_SYMBOL]; - let isStatic = - (dependencies && - this.isWrapperListStatic(dependencies, lookupRegistry)) || - !dependencies; - if (!isStatic || !(properties || enhancers)) { - this.isTreeStatic = isStatic; - return this.isTreeStatic; + let introspectionResult = + (dependencies && callback(dependencies, lookupRegistry)) || !dependencies; + + if (!introspectionResult || !(properties || enhancers)) { + return introspectionResult; } const propertiesHosts = (properties || []).map(item => item.wrapper); - isStatic = - isStatic && this.isWrapperListStatic(propertiesHosts, lookupRegistry); - if (!isStatic || !enhancers) { - this.isTreeStatic = isStatic; + introspectionResult = + introspectionResult && callback(propertiesHosts, lookupRegistry); + if (!introspectionResult || !enhancers) { + return introspectionResult; + } + return callback(enhancers, lookupRegistry); + } + + public isDependencyTreeStatic(lookupRegistry: string[] = []): boolean { + if (!isUndefined(this.isTreeStatic)) { + return this.isTreeStatic; + } + if (this.scope === Scope.REQUEST) { + this.isTreeStatic = false; + this.printIntrospectedAsRequestScoped(); return this.isTreeStatic; } - this.isTreeStatic = this.isWrapperListStatic(enhancers, lookupRegistry); + this.isTreeStatic = this.introspectDepsAttribute( + (collection, registry) => + collection.every((item: InstanceWrapper) => + item.isDependencyTreeStatic(registry), + ), + lookupRegistry, + ); + if (!this.isTreeStatic) { + this.printIntrospectedAsRequestScoped(); + } return this.isTreeStatic; } @@ -341,15 +407,6 @@ export class InstanceWrapper { return isNil(this.inject) && this.metatype && this.metatype.prototype; } - private isWrapperListStatic( - tree: InstanceWrapper[], - lookupRegistry: string[], - ): boolean { - return tree.every((item: InstanceWrapper) => - item.isDependencyTreeStatic(lookupRegistry), - ); - } - private initialize( metadata: Partial> & Partial>, ) { @@ -362,4 +419,34 @@ export class InstanceWrapper { }); this.scope === Scope.TRANSIENT && (this.transientMap = new Map()); } + + private printIntrospectedAsRequestScoped() { + if (!this.isDebugMode() || this.name === 'REQUEST') { + return; + } + if (isString(this.name)) { + InstanceWrapper.logger.log( + `${clc.cyanBright(this.name)}${clc.green( + ' introspected as ', + )}${clc.magentaBright('request-scoped')}`, + ); + } + } + + private printIntrospectedAsDurable() { + if (!this.isDebugMode()) { + return; + } + if (isString(this.name)) { + InstanceWrapper.logger.log( + `${clc.cyanBright(this.name)}${clc.green( + ' introspected as ', + )}${clc.magentaBright('durable')}`, + ); + } + } + + private isDebugMode(): boolean { + return !!process.env.NEST_DEBUG; + } } diff --git a/packages/core/injector/module-ref.ts b/packages/core/injector/module-ref.ts index 22fe0f38357..7051a7b3410 100644 --- a/packages/core/injector/module-ref.ts +++ b/packages/core/injector/module-ref.ts @@ -2,6 +2,7 @@ import { IntrospectionResult, Scope, Type } from '@nestjs/common'; import { InvalidClassScopeException } from '../errors/exceptions/invalid-class-scope.exception'; import { UnknownElementException } from '../errors/exceptions/unknown-element.exception'; import { getClassScope } from '../helpers/get-class-scope'; +import { isDurable } from '../helpers/is-durable'; import { NestContainer } from './container'; import { Injector } from './injector'; import { InstanceLinksHost } from './instance-links-host'; @@ -107,6 +108,7 @@ export abstract class ModuleRef { metatype: type, isResolved: false, scope: getClassScope(type), + durable: isDurable(type), host: moduleRef, }); return new Promise(async (resolve, reject) => { diff --git a/packages/core/injector/module.ts b/packages/core/injector/module.ts index 461fd089956..15b025d4a50 100644 --- a/packages/core/injector/module.ts +++ b/packages/core/injector/module.ts @@ -26,6 +26,7 @@ import { RuntimeException } from '../errors/exceptions/runtime.exception'; import { UnknownExportException } from '../errors/exceptions/unknown-export.exception'; import { createContextId } from '../helpers/context-id-factory'; import { getClassScope } from '../helpers/get-class-scope'; +import { isDurable } from '../helpers/is-durable'; import { CONTROLLER_ID_KEY } from './constants'; import { NestContainer } from './container'; import { InstanceWrapper } from './instance-wrapper'; @@ -209,6 +210,7 @@ export class Module { instance: null, isResolved: false, scope: getClassScope(injectable), + durable: isDurable(injectable), host: this, }); this._injectables.set(injectable, instanceWrapper); @@ -233,6 +235,7 @@ export class Module { instance: null, isResolved: false, scope: getClassScope(provider), + durable: isDurable(provider), host: this, }), ); @@ -301,12 +304,15 @@ export class Module { provider: ClassProvider, collection: Map, ) { - let { scope } = provider; + let { scope, durable } = provider; const { useClass } = provider; if (isUndefined(scope)) { scope = getClassScope(useClass); } + if (isUndefined(durable)) { + durable = isDurable(useClass); + } collection.set( provider.provide, new InstanceWrapper({ @@ -316,6 +322,7 @@ export class Module { instance: null, isResolved: false, scope, + durable, host: this, }), ); @@ -348,6 +355,7 @@ export class Module { useFactory: factory, inject, scope, + durable, provide: providerToken, } = provider; @@ -361,6 +369,7 @@ export class Module { isResolved: false, inject: inject || [], scope, + durable, host: this, }), ); @@ -445,6 +454,7 @@ export class Module { instance: null, isResolved: false, scope: getClassScope(controller), + durable: isDurable(controller), host: this, }), ); diff --git a/packages/core/middleware/container.ts b/packages/core/middleware/container.ts index 330914a1982..de8dc214b00 100644 --- a/packages/core/middleware/container.ts +++ b/packages/core/middleware/container.ts @@ -1,9 +1,10 @@ import { Type } from '@nestjs/common'; import { MiddlewareConfiguration } from '@nestjs/common/interfaces/middleware/middleware-configuration.interface'; +import { getClassScope } from '../helpers/get-class-scope'; +import { isDurable } from '../helpers/is-durable'; import { NestContainer } from '../injector/container'; import { InstanceWrapper } from '../injector/instance-wrapper'; import { InstanceToken } from '../injector/module'; -import { getClassScope } from '../helpers/get-class-scope'; export class MiddlewareContainer { private readonly middleware = new Map< @@ -45,6 +46,7 @@ export class MiddlewareContainer { token, new InstanceWrapper({ scope: getClassScope(metatype), + durable: isDurable(metatype), name: token, metatype, token, diff --git a/packages/core/nest-application.ts b/packages/core/nest-application.ts index 89cb096c045..ed7484c4d56 100644 --- a/packages/core/nest-application.ts +++ b/packages/core/nest-application.ts @@ -236,13 +236,6 @@ export class NestApplication return this; } - public startAllMicroservicesAsync(): Promise { - this.logger.warn( - 'DEPRECATED! "startAllMicroservicesAsync" method is deprecated and will be removed in the next major release. Please, use "startAllMicroservices" instead.', - ); - return this.startAllMicroservices(); - } - public use(...args: [any, any?]): this { this.httpAdapter.use(...args); return this; @@ -301,13 +294,6 @@ export class NestApplication }); } - public listenAsync(port: number | string, ...args: any[]): Promise { - this.logger.warn( - 'DEPRECATED! "listenAsync" method is deprecated and will be removed in the next major release. Please, use "listen" instead.', - ); - return this.listen(port, ...(args as [any])); - } - public async getUrl(): Promise { return new Promise((resolve, reject) => { if (!this.isListening) { diff --git a/packages/core/repl/assign-to-object.util.ts b/packages/core/repl/assign-to-object.util.ts new file mode 100644 index 00000000000..16f9379992f --- /dev/null +++ b/packages/core/repl/assign-to-object.util.ts @@ -0,0 +1,14 @@ +/** + * Similar to `Object.assign` but copying properties descriptors from `source` + * as well. + */ +export function assignToObject(target: T, source: U): T & U { + Object.defineProperties( + target, + Object.keys(source).reduce((descriptors, key) => { + descriptors[key] = Object.getOwnPropertyDescriptor(source, key); + return descriptors; + }, Object.create(null)), + ); + return target as T & U; +} diff --git a/packages/core/repl/constants.ts b/packages/core/repl/constants.ts new file mode 100644 index 00000000000..870e2ec5190 --- /dev/null +++ b/packages/core/repl/constants.ts @@ -0,0 +1 @@ +export const REPL_INITIALIZED_MESSAGE = 'REPL initialized'; diff --git a/packages/core/repl/index.ts b/packages/core/repl/index.ts new file mode 100644 index 00000000000..8a399bd09a5 --- /dev/null +++ b/packages/core/repl/index.ts @@ -0,0 +1 @@ +export * from './repl'; diff --git a/packages/core/repl/native-functions/debug-repl-fn.ts b/packages/core/repl/native-functions/debug-repl-fn.ts new file mode 100644 index 00000000000..910941584e1 --- /dev/null +++ b/packages/core/repl/native-functions/debug-repl-fn.ts @@ -0,0 +1,62 @@ +import type { Type, InjectionToken } from '@nestjs/common'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { ReplFunction } from '../repl-function'; +import type { ModuleDebugEntry } from '../repl-context'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class DebugReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'debug', + description: + 'Print all registered modules as a list together with their controllers and providers. If the argument is passed in, for example, "debug(MyModule)" then it will only print components of this specific module.', + signature: '(moduleCls?: ClassRef | string) => void', + }; + + action(moduleCls?: Type | string): void { + this.ctx.writeToStdout('\n'); + + if (moduleCls) { + const token = + typeof moduleCls === 'function' ? moduleCls.name : moduleCls; + const moduleEntry = this.ctx.debugRegistry[token]; + if (!moduleEntry) { + return this.logger.error( + `"${token}" has not been found in the modules registry`, + ); + } + this.printCtrlsAndProviders(token, moduleEntry); + } else { + Object.keys(this.ctx.debugRegistry).forEach(moduleKey => { + this.printCtrlsAndProviders( + moduleKey, + this.ctx.debugRegistry[moduleKey], + ); + }); + } + this.ctx.writeToStdout('\n'); + } + + private printCtrlsAndProviders( + moduleName: string, + moduleDebugEntry: ModuleDebugEntry, + ) { + this.ctx.writeToStdout(`${clc.green(moduleName)}:\n`); + this.printCollection('controllers', moduleDebugEntry['controllers']); + this.printCollection('providers', moduleDebugEntry['providers']); + } + + private printCollection( + title: string, + collectionValue: Record, + ) { + const collectionEntries = Object.keys(collectionValue); + if (collectionEntries.length <= 0) { + return; + } + + this.ctx.writeToStdout(` ${clc.yellow(`- ${title}`)}:\n`); + collectionEntries.forEach(provider => + this.ctx.writeToStdout(` ${clc.green('◻')} ${provider}\n`), + ); + } +} diff --git a/packages/core/repl/native-functions/get-relp-fn.ts b/packages/core/repl/native-functions/get-relp-fn.ts new file mode 100644 index 00000000000..6de27691896 --- /dev/null +++ b/packages/core/repl/native-functions/get-relp-fn.ts @@ -0,0 +1,17 @@ +import type { Type } from '@nestjs/common'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class GetReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'get', + signature: '(token: InjectionToken) => any', + description: + 'Retrieves an instance of either injectable or controller, otherwise, throws exception.', + aliases: ['$'], + }; + + action(token: string | symbol | Function | Type): any { + return this.ctx.app.get(token); + } +} diff --git a/packages/core/repl/native-functions/help-repl-fn.ts b/packages/core/repl/native-functions/help-repl-fn.ts new file mode 100644 index 00000000000..913ced7ee91 --- /dev/null +++ b/packages/core/repl/native-functions/help-repl-fn.ts @@ -0,0 +1,32 @@ +import { iterate } from 'iterare'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class HelpReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'help', + signature: '() => void', + description: 'Display all available REPL native functions.', + }; + + static buildHelpMessage = ({ name, description }: ReplFnDefinition) => + clc.cyanBright(name) + + (description ? ` ${clc.bold('-')} ${description}` : ''); + + action(): void { + const sortedNativeFunctions = iterate(this.ctx.nativeFunctions) + .map(([, nativeFunction]) => nativeFunction.fnDefinition) + .toArray() + .sort((a, b) => (a.name < b.name ? -1 : 1)); + + this.ctx.writeToStdout( + `You can call ${clc.bold( + '.help', + )} on any function listed below (e.g.: ${clc.bold('help.help')}):\n\n` + + sortedNativeFunctions.map(HelpReplFn.buildHelpMessage).join('\n') + + // Without the following LF the last item won't be displayed + '\n', + ); + } +} diff --git a/packages/core/repl/native-functions/index.ts b/packages/core/repl/native-functions/index.ts new file mode 100644 index 00000000000..8799e685d0c --- /dev/null +++ b/packages/core/repl/native-functions/index.ts @@ -0,0 +1,6 @@ +export * from './help-repl-fn'; +export * from './get-relp-fn'; +export * from './resolve-repl-fn'; +export * from './select-relp-fn'; +export * from './debug-repl-fn'; +export * from './methods-repl-fn'; diff --git a/packages/core/repl/native-functions/methods-repl-fn.ts b/packages/core/repl/native-functions/methods-repl-fn.ts new file mode 100644 index 00000000000..b8deb83fc5a --- /dev/null +++ b/packages/core/repl/native-functions/methods-repl-fn.ts @@ -0,0 +1,33 @@ +import type { Type } from '@nestjs/common'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { MetadataScanner } from '../../metadata-scanner'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class MethodsReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'methods', + description: 'Display all public methods available on a given provider or controller.', + signature: '(token: ClassRef | string) => void', + }; + + private readonly metadataScanner = new MetadataScanner(); + + action(token: Type | string): void { + const proto = + typeof token !== 'function' + ? Object.getPrototypeOf(this.ctx.app.get(token)) + : token?.prototype; + + const methods = new Set( + this.metadataScanner.getAllFilteredMethodNames(proto), + ); + + this.ctx.writeToStdout('\n'); + this.ctx.writeToStdout(`${clc.green('Methods')}:\n`); + methods.forEach(methodName => + this.ctx.writeToStdout(` ${clc.yellow('◻')} ${methodName}\n`), + ); + this.ctx.writeToStdout('\n'); + } +} diff --git a/packages/core/repl/native-functions/resolve-repl-fn.ts b/packages/core/repl/native-functions/resolve-repl-fn.ts new file mode 100644 index 00000000000..fe587682d1d --- /dev/null +++ b/packages/core/repl/native-functions/resolve-repl-fn.ts @@ -0,0 +1,19 @@ +import type { Type } from '@nestjs/common'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class ResolveReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'resolve', + description: + 'Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception.', + signature: '(token: InjectionToken, contextId: any) => Promise', + }; + + action( + token: string | symbol | Function | Type, + contextId: any, + ): Promise { + return this.ctx.app.resolve(token, contextId); + } +} diff --git a/packages/core/repl/native-functions/select-relp-fn.ts b/packages/core/repl/native-functions/select-relp-fn.ts new file mode 100644 index 00000000000..f35346cd100 --- /dev/null +++ b/packages/core/repl/native-functions/select-relp-fn.ts @@ -0,0 +1,20 @@ +import type { + DynamicModule, + INestApplicationContext, + Type, +} from '@nestjs/common'; +import { ReplFunction } from '../repl-function'; +import type { ReplFnDefinition } from '../repl.interfaces'; + +export class SelectReplFn extends ReplFunction { + public fnDefinition: ReplFnDefinition = { + name: 'select', + description: + 'Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module.', + signature: '(token: DynamicModule | ClassRef) => INestApplicationContext', + }; + + action(token: DynamicModule | Type): INestApplicationContext { + return this.ctx.app.select(token); + } +} diff --git a/packages/core/repl/repl-context.ts b/packages/core/repl/repl-context.ts new file mode 100644 index 00000000000..f8eef91a873 --- /dev/null +++ b/packages/core/repl/repl-context.ts @@ -0,0 +1,179 @@ +import { INestApplication, InjectionToken, Logger } from '@nestjs/common'; +import { ApplicationConfig } from '../application-config'; +import { ModuleRef, NestContainer } from '../injector'; +import { InternalCoreModule } from '../injector/internal-core-module'; +import { Module } from '../injector/module'; +import { + DebugReplFn, + GetReplFn, + HelpReplFn, + MethodsReplFn, + ResolveReplFn, + SelectReplFn, +} from './native-functions'; +import { ReplFunction } from './repl-function'; +import type { ReplFunctionClass } from './repl.interfaces'; + +type ModuleKey = string; +export type ModuleDebugEntry = { + controllers: Record; + providers: Record; +}; + +type ReplScope = Record; + +export class ReplContext { + public readonly logger = new Logger(ReplContext.name); + public debugRegistry: Record = {}; + public readonly globalScope: ReplScope = Object.create(null); + public readonly nativeFunctions = new Map< + string, + InstanceType + >(); + private readonly container: NestContainer; + + constructor( + public readonly app: INestApplication, + nativeFunctionsClassRefs?: ReplFunctionClass[], + ) { + this.container = (app as any).container; // Using `any` because `app.container` is not public. + + this.initializeContext(); + this.initializeNativeFunctions(nativeFunctionsClassRefs || []); + } + + public writeToStdout(text: string) { + process.stdout.write(text); + } + + private initializeContext() { + const modules = this.container.getModules(); + + modules.forEach(moduleRef => { + let moduleName = moduleRef.metatype.name; + if (moduleName === InternalCoreModule.name) { + return; + } + if (this.globalScope[moduleName]) { + moduleName += ` (${moduleRef.token})`; + } + + this.introspectCollection(moduleRef, moduleName, 'providers'); + this.introspectCollection(moduleRef, moduleName, 'controllers'); + + // For in REPL auto-complete functionality + Object.defineProperty(this.globalScope, moduleName, { + value: moduleRef.metatype, + configurable: false, + enumerable: true, + }); + }); + } + + private introspectCollection( + moduleRef: Module, + moduleKey: ModuleKey, + collection: keyof ModuleDebugEntry, + ) { + const moduleDebugEntry = {}; + moduleRef[collection].forEach(({ token }) => { + const stringifiedToken = this.stringifyToken(token); + if ( + stringifiedToken === ApplicationConfig.name || + stringifiedToken === moduleRef.metatype.name || + this.globalScope[stringifiedToken] + ) { + return; + } + // For in REPL auto-complete functionality + Object.defineProperty(this.globalScope, stringifiedToken, { + value: token, + configurable: false, + enumerable: true, + }); + + if (stringifiedToken === ModuleRef.name) { + return; + } + moduleDebugEntry[stringifiedToken] = token; + }); + + this.debugRegistry[moduleKey] = { + ...this.debugRegistry?.[moduleKey], + [collection]: moduleDebugEntry, + }; + } + + private stringifyToken(token: unknown): string { + return typeof token !== 'string' + ? typeof token === 'function' + ? token.name + : token?.toString() + : token; + } + + private addNativeFunction( + NativeFunctionRef: ReplFunctionClass, + ): InstanceType[] { + const nativeFunction = new NativeFunctionRef(this); + const nativeFunctions = [nativeFunction]; + + this.nativeFunctions.set(nativeFunction.fnDefinition.name, nativeFunction); + + nativeFunction.fnDefinition.aliases?.forEach(aliaseName => { + const aliasNativeFunction: InstanceType = + Object.create(nativeFunction); + aliasNativeFunction.fnDefinition = { + name: aliaseName, + description: aliasNativeFunction.fnDefinition.description, + signature: aliasNativeFunction.fnDefinition.signature, + }; + this.nativeFunctions.set(aliaseName, aliasNativeFunction); + nativeFunctions.push(aliasNativeFunction); + }); + + return nativeFunctions; + } + + private registerFunctionIntoGlobalScope( + nativeFunction: InstanceType, + ) { + // Bind the method to REPL's context: + this.globalScope[nativeFunction.fnDefinition.name] = + nativeFunction.action.bind(nativeFunction); + + // Load the help trigger as a `help` getter on each native function: + const functionBoundRef: ReplFunction['action'] = + this.globalScope[nativeFunction.fnDefinition.name]; + Object.defineProperty(functionBoundRef, 'help', { + enumerable: false, + configurable: false, + get: () => + // Dynamically builds the help message as will unlikely to be called + // several times. + this.writeToStdout(nativeFunction.makeHelpMessage()), + }); + } + + private initializeNativeFunctions( + nativeFunctionsClassRefs: ReplFunctionClass[], + ): void { + const builtInFunctionsClassRefs: ReplFunctionClass[] = [ + HelpReplFn, + GetReplFn, + ResolveReplFn, + SelectReplFn, + DebugReplFn, + MethodsReplFn, + ]; + + builtInFunctionsClassRefs + .concat(nativeFunctionsClassRefs) + .forEach(NativeFunction => { + const nativeFunctions = this.addNativeFunction(NativeFunction); + nativeFunctions.forEach(nativeFunction => { + this.registerFunctionIntoGlobalScope(nativeFunction); + }); + }); + } +} diff --git a/packages/core/repl/repl-function.ts b/packages/core/repl/repl-function.ts new file mode 100644 index 00000000000..aa90972c853 --- /dev/null +++ b/packages/core/repl/repl-function.ts @@ -0,0 +1,36 @@ +import { Logger } from '@nestjs/common'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { ReplContext } from './repl-context'; +import type { ReplFnDefinition } from './repl.interfaces'; + +export abstract class ReplFunction< + ActionParams extends Array = Array, + ActionReturn = any, +> { + /** Metadata that describes the built-in function itself. */ + public abstract fnDefinition: ReplFnDefinition; + + protected readonly logger: Logger; + + constructor(protected readonly ctx: ReplContext) { + this.logger = ctx.logger; + } + + /** + * Method called when the function is invoked from the REPL by the user. + */ + abstract action(...args: ActionParams): ActionReturn; + + /** + * @returns A message displayed by calling `.help` + */ + public makeHelpMessage(): string { + const { description, name, signature } = this.fnDefinition; + + const fnSignatureWithName = `${name}${signature}`; + + return `${clc.yellow(description)}\n${clc.magentaBright( + 'Interface:', + )} ${clc.bold(fnSignatureWithName)}\n`; + } +} diff --git a/packages/core/repl/repl-logger.ts b/packages/core/repl/repl-logger.ts new file mode 100644 index 00000000000..e6195269ffe --- /dev/null +++ b/packages/core/repl/repl-logger.ts @@ -0,0 +1,19 @@ +import { ConsoleLogger } from '@nestjs/common'; +import { NestApplication } from '../nest-application'; +import { RouterExplorer } from '../router/router-explorer'; +import { RoutesResolver } from '../router/routes-resolver'; + +export class ReplLogger extends ConsoleLogger { + private static readonly ignoredContexts = [ + RoutesResolver.name, + RouterExplorer.name, + NestApplication.name, + ]; + log(_message: any, context?: string) { + if (ReplLogger.ignoredContexts.includes(context)) { + return; + } + // eslint-disable-next-line + return super.log.apply(this, Array.from(arguments) as [any, string?]); + } +} diff --git a/packages/core/repl/repl.interfaces.ts b/packages/core/repl/repl.interfaces.ts new file mode 100644 index 00000000000..ad4b8fd646c --- /dev/null +++ b/packages/core/repl/repl.interfaces.ts @@ -0,0 +1,21 @@ +import type { ReplContext } from './repl-context'; +import type { ReplFunction } from './repl-function'; + +export type ReplFnDefinition = { + /** Function's name. Note that this should be a valid JavaScript function name. */ + name: string; + + /** Alternative names to the function. */ + aliases?: ReplFnDefinition['name'][]; + + /** Function's description to display when `.help` is entered. */ + description: string; + + /** + * Function's signature following TypeScript _function type expression_ syntax. + * @example '(token: InjectionToken) => any' + */ + signature: string; +}; + +export type ReplFunctionClass = new (replContext: ReplContext) => ReplFunction; diff --git a/packages/core/repl/repl.ts b/packages/core/repl/repl.ts new file mode 100644 index 00000000000..079f16f1fe2 --- /dev/null +++ b/packages/core/repl/repl.ts @@ -0,0 +1,27 @@ +import { Logger, Type } from '@nestjs/common'; +import * as _repl from 'repl'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { NestFactory } from '../nest-factory'; +import { REPL_INITIALIZED_MESSAGE } from './constants'; +import { ReplContext } from './repl-context'; +import { ReplLogger } from './repl-logger'; +import { assignToObject } from './assign-to-object.util'; + +export async function repl(module: Type) { + const app = await NestFactory.create(module, { + abortOnError: false, + logger: new ReplLogger(), + }); + await app.init(); + + const replContext = new ReplContext(app); + Logger.log(REPL_INITIALIZED_MESSAGE); + + const replServer = _repl.start({ + prompt: clc.green('> '), + ignoreUndefined: true, + }); + assignToObject(replServer.context, replContext.globalScope); + + return replServer; +} diff --git a/packages/core/router/router-explorer.ts b/packages/core/router/router-explorer.ts index ab0517dc50b..6dbbad0ae4e 100644 --- a/packages/core/router/router-explorer.ts +++ b/packages/core/router/router-explorer.ts @@ -332,134 +332,12 @@ export class RouterExplorer { routePathMetadata: RoutePathMetadata, handler: Function, ) { - const { versioningOptions } = routePathMetadata; const version = this.routePathFactory.getVersion(routePathMetadata); - if (router?.applyVersionFilter) { - return router.applyVersionFilter(handler, version, versioningOptions); - } - /** - * TODO(v9): This was left for backward-compatibility and can be removed. - */ - return = any, TResponse = any>( - req: TRequest, - res: TResponse, - next: () => void, - ) => { - if (version === VERSION_NEUTRAL) { - return handler(req, res, next); - } - // URL Versioning is done via the path, so the filter continues forward - if (versioningOptions.type === VersioningType.URI) { - return handler(req, res, next); - } - - // Custom Extractor Versioning Handler - if (versioningOptions.type === VersioningType.CUSTOM) { - const extractedVersion = versioningOptions.extractor(req) as - | string - | string[] - | Array; - - if (Array.isArray(version)) { - if ( - Array.isArray(extractedVersion) && - version.filter( - extractedVersion.includes as ( - value: string | symbol, - index: number, - array: Array, - ) => boolean, - ).length - ) { - return handler(req, res, next); - } else if ( - isString(extractedVersion) && - version.includes(extractedVersion) - ) { - return handler(req, res, next); - } - } else { - if ( - Array.isArray(extractedVersion) && - extractedVersion.includes(version) - ) { - return handler(req, res, next); - } else if ( - isString(extractedVersion) && - version === extractedVersion - ) { - return handler(req, res, next); - } - } - } - - // Media Type (Accept Header) Versioning Handler - if (versioningOptions.type === VersioningType.MEDIA_TYPE) { - const MEDIA_TYPE_HEADER = 'Accept'; - const acceptHeaderValue: string | undefined = - req.headers?.[MEDIA_TYPE_HEADER] || - req.headers?.[MEDIA_TYPE_HEADER.toLowerCase()]; - - const acceptHeaderVersionParameter = acceptHeaderValue - ? acceptHeaderValue.split(';')[1] - : undefined; - - // No version was supplied - if (isUndefined(acceptHeaderVersionParameter)) { - if (Array.isArray(version)) { - if (version.includes(VERSION_NEUTRAL)) { - return handler(req, res, next); - } - } - } else { - const headerVersion = acceptHeaderVersionParameter.split( - versioningOptions.key, - )[1]; - - if (Array.isArray(version)) { - if (version.includes(headerVersion)) { - return handler(req, res, next); - } - } else if (isString(version)) { - if (version === headerVersion) { - return handler(req, res, next); - } - } - } - } - // Header Versioning Handler - else if (versioningOptions.type === VersioningType.HEADER) { - const customHeaderVersionParameter: string | undefined = - req.headers?.[versioningOptions.header] || - req.headers?.[versioningOptions.header.toLowerCase()]; - - // No version was supplied - if (isUndefined(customHeaderVersionParameter)) { - if (Array.isArray(version)) { - if (version.includes(VERSION_NEUTRAL)) { - return handler(req, res, next); - } - } - } else { - if (Array.isArray(version)) { - if (version.includes(customHeaderVersionParameter)) { - return handler(req, res, next); - } - } else if (isString(version)) { - if (version === customHeaderVersionParameter) { - return handler(req, res, next); - } - } - } - } - - if (!next) { - throw new InternalServerErrorException( - 'HTTP adapter does not support filtering on version', - ); - } - return next(); - }; + return router.applyVersionFilter( + handler, + version, + routePathMetadata.versioningOptions, + ); } private createCallbackProxy( diff --git a/packages/core/router/router-response-controller.ts b/packages/core/router/router-response-controller.ts index 008eea5a85d..4128e7fcab5 100644 --- a/packages/core/router/router-response-controller.ts +++ b/packages/core/router/router-response-controller.ts @@ -5,9 +5,9 @@ import { RequestMethod, MessageEvent, } from '@nestjs/common'; -import { isFunction, isObject } from '@nestjs/common/utils/shared.utils'; +import { isObject } from '@nestjs/common/utils/shared.utils'; import { IncomingMessage } from 'http'; -import { EMPTY, lastValueFrom, Observable } from 'rxjs'; +import { EMPTY, lastValueFrom, Observable, isObservable } from 'rxjs'; import { catchError, debounce, map } from 'rxjs/operators'; import { AdditionalHeaders, @@ -64,7 +64,7 @@ export class RouterResponseController { } public async transformToResult(resultOrDeferred: any) { - if (resultOrDeferred && isFunction(resultOrDeferred.subscribe)) { + if (isObservable(resultOrDeferred)) { return lastValueFrom(resultOrDeferred); } return resultOrDeferred; @@ -152,8 +152,8 @@ export class RouterResponseController { }); } - private assertObservable(result: any) { - if (!isFunction(result.subscribe)) { + private assertObservable(value: any) { + if (!isObservable(value)) { throw new ReferenceError( 'You must return an Observable stream to use Server-Sent Events (SSE).', ); diff --git a/packages/core/scanner.ts b/packages/core/scanner.ts index 63d02a6c25a..1d9301860dc 100644 --- a/packages/core/scanner.ts +++ b/packages/core/scanner.ts @@ -2,7 +2,6 @@ import { DynamicModule, flatten, ForwardReference, - Logger, Provider, } from '@nestjs/common'; import { @@ -59,7 +58,6 @@ interface ApplicationProviderWrapper { } export class DependenciesScanner { - private readonly logger = new Logger(DependenciesScanner.name); private readonly applicationProvidersApplyMap: ApplicationProviderWrapper[] = []; @@ -102,13 +100,13 @@ export class DependenciesScanner { moduleDefinition as Type | DynamicModule, ) ? this.reflectMetadata( - moduleDefinition as Type, MODULE_METADATA.IMPORTS, + moduleDefinition as Type, ) : [ ...this.reflectMetadata( - (moduleDefinition as DynamicModule).module, MODULE_METADATA.IMPORTS, + (moduleDefinition as DynamicModule).module, ), ...((moduleDefinition as DynamicModule).imports || []), ]; @@ -151,10 +149,7 @@ export class DependenciesScanner { this.isController(moduleToAdd) || this.isExceptionFilter(moduleToAdd) ) { - // TODO(v9): Throw the exception instead of printing a warning - this.logger.warn( - new InvalidClassModuleException(moduleDefinition, scope).message, - ); + throw new InvalidClassModuleException(moduleDefinition, scope); } return this.container.addModule(moduleToAdd, scope); @@ -177,7 +172,7 @@ export class DependenciesScanner { context: string, ) { const modules = [ - ...this.reflectMetadata(module, MODULE_METADATA.IMPORTS), + ...this.reflectMetadata(MODULE_METADATA.IMPORTS, module), ...this.container.getDynamicMetadataByToken( token, MODULE_METADATA.IMPORTS as 'imports', @@ -190,7 +185,7 @@ export class DependenciesScanner { public reflectProviders(module: Type, token: string) { const providers = [ - ...this.reflectMetadata(module, MODULE_METADATA.PROVIDERS), + ...this.reflectMetadata(MODULE_METADATA.PROVIDERS, module), ...this.container.getDynamicMetadataByToken( token, MODULE_METADATA.PROVIDERS as 'providers', @@ -204,7 +199,7 @@ export class DependenciesScanner { public reflectControllers(module: Type, token: string) { const controllers = [ - ...this.reflectMetadata(module, MODULE_METADATA.CONTROLLERS), + ...this.reflectMetadata(MODULE_METADATA.CONTROLLERS, module), ...this.container.getDynamicMetadataByToken( token, MODULE_METADATA.CONTROLLERS as 'controllers', @@ -229,7 +224,7 @@ export class DependenciesScanner { public reflectExports(module: Type, token: string) { const exports = [ - ...this.reflectMetadata(module, MODULE_METADATA.EXPORTS), + ...this.reflectMetadata(MODULE_METADATA.EXPORTS, module), ...this.container.getDynamicMetadataByToken( token, MODULE_METADATA.EXPORTS as 'exports', @@ -245,7 +240,7 @@ export class DependenciesScanner { token: string, metadataKey: string, ) { - const controllerInjectables = this.reflectMetadata(component, metadataKey); + const controllerInjectables = this.reflectMetadata(metadataKey, component); const methodsInjectables = this.metadataScanner.scanFromPrototype( null, component.prototype, @@ -416,7 +411,7 @@ export class DependenciesScanner { this.container.addController(controller, token); } - public reflectMetadata(metatype: Type, metadataKey: string) { + public reflectMetadata(metadataKey: string, metatype: Type) { return Reflect.getMetadata(metadataKey, metatype) || []; } diff --git a/packages/core/test/injector/injector.spec.ts b/packages/core/test/injector/injector.spec.ts index 69361354826..9b6c8492ce8 100644 --- a/packages/core/test/injector/injector.spec.ts +++ b/packages/core/test/injector/injector.spec.ts @@ -93,8 +93,7 @@ describe('Injector', () => { ).to.eventually.be.rejected; }); - it('should await done$ when "isPending"', async () => { - const value = 'test'; + it('should await done$ when "isPending"', () => { const wrapper = new InstanceWrapper({ name: 'MainTest', metatype: MainTest, @@ -102,15 +101,29 @@ describe('Injector', () => { isResolved: false, }); const host = wrapper.getInstanceByContextId(STATIC_CONTEXT); - host.donePromise = Promise.resolve(value) as any; + host.donePromise = Promise.resolve(); host.isPending = true; - const result = await injector.loadInstance( - wrapper, - moduleDeps.providers, - moduleDeps, - ); - expect(result).to.be.eql(value); + expect( + injector.loadInstance(wrapper, moduleDeps.providers, moduleDeps), + ).to.eventually.not.throw(); + }); + + it('should await done$ when "isPending" and rethrow an exception (if thrown)', () => { + const error = new Error('Test error'); + const wrapper = new InstanceWrapper({ + name: 'MainTest', + metatype: MainTest, + instance: Object.create(MainTest.prototype), + isResolved: false, + }); + const host = wrapper.getInstanceByContextId(STATIC_CONTEXT); + host.donePromise = Promise.resolve(error); + host.isPending = true; + + expect( + injector.loadInstance(wrapper, moduleDeps.providers, moduleDeps), + ).to.eventually.throw(error); }); it('should return undefined when metatype is resolved', async () => { diff --git a/packages/core/test/injector/instance-wrapper.spec.ts b/packages/core/test/injector/instance-wrapper.spec.ts index b94cf4e1dad..6b0e1734cdf 100644 --- a/packages/core/test/injector/instance-wrapper.spec.ts +++ b/packages/core/test/injector/instance-wrapper.spec.ts @@ -106,6 +106,118 @@ describe('InstanceWrapper', () => { }); }); + describe('isDependencyTreeDurable', () => { + describe('when request scoped and durable', () => { + it('should return true', () => { + const wrapper = new InstanceWrapper({ + scope: Scope.REQUEST, + durable: true, + }); + expect(wrapper.isDependencyTreeDurable()).to.be.true; + }); + }); + describe('when statically scoped', () => { + describe('dependencies', () => { + describe('when each is static', () => { + it('should return false', () => { + const wrapper = new InstanceWrapper(); + wrapper.addCtorMetadata(0, new InstanceWrapper()); + expect(wrapper.isDependencyTreeDurable()).to.be.false; + }); + }); + describe('when one is not static and non-durable', () => { + it('should return false', () => { + const wrapper = new InstanceWrapper(); + wrapper.addCtorMetadata(0, new InstanceWrapper()); + wrapper.addCtorMetadata( + 1, + new InstanceWrapper({ + scope: Scope.REQUEST, + }), + ); + expect(wrapper.isDependencyTreeDurable()).to.be.false; + }); + }); + describe('when one is not static and durable', () => { + it('should return true', () => { + const wrapper = new InstanceWrapper(); + wrapper.addCtorMetadata(0, new InstanceWrapper()); + wrapper.addCtorMetadata( + 1, + new InstanceWrapper({ + scope: Scope.REQUEST, + durable: true, + }), + ); + expect(wrapper.isDependencyTreeDurable()).to.be.true; + }); + }); + }); + describe('properties', () => { + describe('when each is static', () => { + it('should return false', () => { + const wrapper = new InstanceWrapper(); + wrapper.addPropertiesMetadata('key1', new InstanceWrapper()); + wrapper.addPropertiesMetadata('key2', new InstanceWrapper()); + expect(wrapper.isDependencyTreeDurable()).to.be.false; + }); + }); + describe('when one is not static and non-durable', () => { + it('should return false', () => { + const wrapper = new InstanceWrapper(); + wrapper.addPropertiesMetadata( + 'key1', + new InstanceWrapper({ scope: Scope.REQUEST }), + ); + wrapper.addPropertiesMetadata('key2', new InstanceWrapper()); + expect(wrapper.isDependencyTreeDurable()).to.be.false; + }); + }); + describe('when one is not static and durable', () => { + it('should return true', () => { + const wrapper = new InstanceWrapper(); + wrapper.addPropertiesMetadata( + 'key1', + new InstanceWrapper({ scope: Scope.REQUEST, durable: true }), + ); + wrapper.addPropertiesMetadata('key2', new InstanceWrapper()); + expect(wrapper.isDependencyTreeDurable()).to.be.true; + }); + }); + }); + describe('enhancers', () => { + describe('when each is static', () => { + it('should return false', () => { + const wrapper = new InstanceWrapper(); + wrapper.addEnhancerMetadata(new InstanceWrapper()); + wrapper.addEnhancerMetadata(new InstanceWrapper()); + expect(wrapper.isDependencyTreeDurable()).to.be.false; + }); + }); + describe('when one is not static and non-durable', () => { + it('should return false', () => { + const wrapper = new InstanceWrapper(); + wrapper.addEnhancerMetadata( + new InstanceWrapper({ scope: Scope.REQUEST }), + ); + wrapper.addEnhancerMetadata(new InstanceWrapper()); + expect(wrapper.isDependencyTreeDurable()).to.be.false; + }); + }); + describe('when one is not static and durable', () => { + it('should return true', () => { + const wrapper = new InstanceWrapper(); + wrapper.addEnhancerMetadata( + new InstanceWrapper({ scope: Scope.REQUEST, durable: true }), + ); + wrapper.addEnhancerMetadata(new InstanceWrapper()); + expect(wrapper.isDependencyTreeDurable()).to.be.true; + }); + }); + }); + }); + }); + describe('isNotMetatype', () => { describe('when metatype is nil', () => { it('should return true', () => { diff --git a/packages/core/test/injector/module.spec.ts b/packages/core/test/injector/module.spec.ts index 04581b726cd..9bddc5aa161 100644 --- a/packages/core/test/injector/module.spec.ts +++ b/packages/core/test/injector/module.spec.ts @@ -30,7 +30,7 @@ describe('Module', () => { const setSpy = sinon.spy(collection, 'set'); (module as any)._controllers = collection; - @Controller({ scope: Scope.REQUEST }) + @Controller({ scope: Scope.REQUEST, durable: true }) class Test {} module.addController(Test); @@ -43,6 +43,7 @@ describe('Module', () => { name: 'Test', scope: Scope.REQUEST, metatype: Test, + durable: true, instance: null, isResolved: false, }), @@ -66,6 +67,7 @@ describe('Module', () => { scope: undefined, metatype: TestProvider, instance: null, + durable: undefined, isResolved: false, }), ), @@ -96,6 +98,7 @@ describe('Module', () => { token: TestProvider, scope: undefined, metatype: TestProvider, + durable: undefined, instance: null, isResolved: false, }), @@ -155,7 +158,7 @@ describe('Module', () => { describe('addCustomClass', () => { const type = { name: 'TypeTest' }; - const provider = { provide: type, useClass: type }; + const provider = { provide: type, useClass: type, durable: true }; let setSpy; beforeEach(() => { @@ -174,6 +177,7 @@ describe('Module', () => { name: provider.provide.name, scope: undefined, metatype: type as any, + durable: true, instance: null, isResolved: false, }), @@ -216,7 +220,7 @@ describe('Module', () => { describe('addCustomFactory', () => { const type = { name: 'TypeTest' }; const inject = [1, 2, 3]; - const provider = { provide: type, useFactory: type, inject }; + const provider = { provide: type, useFactory: type, inject, durable: true }; let setSpy; beforeEach(() => { @@ -236,6 +240,7 @@ describe('Module', () => { name: provider.provide.name, scope: undefined, metatype: type as any, + durable: true, instance: null, isResolved: false, inject: inject as any, diff --git a/packages/core/test/repl/assign-to-object.util.spec.ts b/packages/core/test/repl/assign-to-object.util.spec.ts new file mode 100644 index 00000000000..812343aef90 --- /dev/null +++ b/packages/core/test/repl/assign-to-object.util.spec.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai'; +import { assignToObject } from '@nestjs/core/repl/assign-to-object.util'; + +describe('assignToObject', () => { + it('should copy all enumerable properties and their descriptors', () => { + const sourceObj = {}; + Object.defineProperty(sourceObj, 'foo', { + value: 123, + configurable: true, + enumerable: true, + writable: true, + }); + Object.defineProperty(sourceObj, 'bar', { + value: 456, + configurable: true, + enumerable: true, + writable: false, + }); + const targetObj = {}; + + assignToObject(targetObj, sourceObj); + + expect(Object.getOwnPropertyDescriptor(targetObj, 'foo')).to.be.eql({ + value: 123, + configurable: true, + enumerable: true, + writable: true, + }); + expect(Object.getOwnPropertyDescriptor(targetObj, 'bar')).to.be.eql({ + value: 456, + configurable: true, + enumerable: true, + writable: false, + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/debug-repl-fn.spec.ts b/packages/core/test/repl/native-functions/debug-repl-fn.spec.ts new file mode 100644 index 00000000000..e7ddc4a04bc --- /dev/null +++ b/packages/core/test/repl/native-functions/debug-repl-fn.spec.ts @@ -0,0 +1,122 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { DebugReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('DebugReplFn', () => { + let debugReplFn: DebugReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + const aModuleRef = await container.addModule(class ModuleA {}, []); + const bModuleRef = await container.addModule(class ModuleB {}, []); + + container.addController(class ControllerA {}, aModuleRef.token); + container.addProvider(class ProviderA1 {}, aModuleRef.token); + container.addProvider(class ProviderA2 {}, aModuleRef.token); + + container.addProvider(class ProviderB1 {}, bModuleRef.token); + container.addProvider(class ProviderB2 {}, bModuleRef.token); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + debugReplFn = replContext.nativeFunctions.get('debug') as DebugReplFn; + + // To avoid coloring the output: + sinon.stub(clc, 'yellow').callsFake(text => text); + sinon.stub(clc, 'green').callsFake(text => text); + }); + afterEach(() => sinon.restore()); + + it('the function name should be "debug"', () => { + expect(debugReplFn).to.not.be.undefined; + expect(debugReplFn.fnDefinition.name).to.eql('debug'); + }); + + describe('action', () => { + it('should print all modules along with their controllers and providers', () => { + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + debugReplFn.action(); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 +ModuleB: + - providers: + ◻ ProviderB1 + ◻ ProviderB2 + +`); + }); + + describe('when module passed as a class reference', () => { + it("should print a specified module's controllers and providers", () => { + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + debugReplFn.action(class ModuleA {}); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 + +`); + }); + }); + describe("when module passed as a string (module's key)", () => { + it("should print a specified module's controllers and providers", () => { + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + debugReplFn.action('ModuleA'); + + expect(outputText).to.equal(` +ModuleA: + - controllers: + ◻ ControllerA + - providers: + ◻ ProviderA1 + ◻ ProviderA2 + +`); + }); + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/get-repl-fn.spec.ts b/packages/core/test/repl/native-functions/get-repl-fn.spec.ts new file mode 100644 index 00000000000..aec8f335fac --- /dev/null +++ b/packages/core/test/repl/native-functions/get-repl-fn.spec.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { GetReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('GetReplFn', () => { + let getReplFn: GetReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + getReplFn = replContext.nativeFunctions.get('get') as GetReplFn; + }); + afterEach(() => sinon.restore()); + + it('the function name should be "get"', () => { + expect(getReplFn).to.not.be.undefined; + expect(getReplFn.fnDefinition.name).to.eql('get'); + }); + + describe('action', () => { + it('should pass arguments down to the application context', () => { + const token = 'test'; + getReplFn.action(token); + expect(mockApp.get.calledWith(token)).to.be.true; + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/help-repl-fn.spec.ts b/packages/core/test/repl/native-functions/help-repl-fn.spec.ts new file mode 100644 index 00000000000..441d14c43c9 --- /dev/null +++ b/packages/core/test/repl/native-functions/help-repl-fn.spec.ts @@ -0,0 +1,67 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { HelpReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('HelpReplFn', () => { + let helpReplFn: HelpReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + helpReplFn = replContext.nativeFunctions.get('help') as HelpReplFn; + + // To avoid coloring the output: + sinon.stub(clc, 'bold').callsFake(text => text); + sinon.stub(clc, 'cyanBright').callsFake(text => text); + }); + afterEach(() => sinon.restore()); + + it('the function name should be "help"', () => { + expect(helpReplFn).to.not.be.undefined; + expect(helpReplFn.fnDefinition.name).to.eql('help'); + }); + + describe('action', () => { + it('should print all available native functions and their description', () => { + let outputText = ''; + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + helpReplFn.action(); + + expect(outputText).to + .equal(`You can call .help on any function listed below (e.g.: help.help): + +$ - Retrieves an instance of either injectable or controller, otherwise, throws exception. +debug - Print all registered modules as a list together with their controllers and providers. If the argument is passed in, for example, "debug(MyModule)" then it will only print components of this specific module. +get - Retrieves an instance of either injectable or controller, otherwise, throws exception. +help - Display all available REPL native functions. +methods - Display all public methods available on a given provider or controller. +resolve - Resolves transient or request-scoped instance of either injectable or controller, otherwise, throws exception. +select - Allows navigating through the modules tree, for example, to pull out a specific instance from the selected module. +`); + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/methods-repl-fn.spec.ts b/packages/core/test/repl/native-functions/methods-repl-fn.spec.ts new file mode 100644 index 00000000000..a45f5c2551f --- /dev/null +++ b/packages/core/test/repl/native-functions/methods-repl-fn.spec.ts @@ -0,0 +1,108 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { clc } from '@nestjs/common/utils/cli-colors.util'; +import { MethodsReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('MethodsReplFn', () => { + let methodsReplFn: MethodsReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + const aModuleRef = await container.addModule(class ModuleA {}, []); + const bModuleRef = await container.addModule(class ModuleB {}, []); + + container.addController(class ControllerA {}, aModuleRef.token); + container.addProvider(class ProviderA1 {}, aModuleRef.token); + container.addProvider(class ProviderA2 {}, aModuleRef.token); + + container.addProvider(class ProviderB1 {}, bModuleRef.token); + container.addProvider(class ProviderB2 {}, bModuleRef.token); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + methodsReplFn = replContext.nativeFunctions.get('methods') as MethodsReplFn; + + // To avoid coloring the output: + sinon.stub(clc, 'yellow').callsFake(text => text); + sinon.stub(clc, 'green').callsFake(text => text); + }); + afterEach(() => sinon.restore()); + + it('the function name should be "methods"', () => { + expect(methodsReplFn).to.not.be.undefined; + expect(methodsReplFn.fnDefinition.name).to.eql('methods'); + }); + + describe('action', () => { + describe('when token is a class reference', () => { + it('should print all class methods', () => { + class BaseService { + create() {} + } + class TestService extends BaseService { + findAll() {} + findOne() {} + } + + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + methodsReplFn.action(TestService); + + expect(outputText).to.equal(` +Methods: + ◻ findAll + ◻ findOne + ◻ create + +`); + }); + }); + + describe('when token is a string', () => { + it('should grab provider from the container and print its all methods', () => { + class ProviderA1 { + findAll() {} + findOne() {} + } + let outputText = ''; + + sinon + .stub(replContext, 'writeToStdout') + .callsFake(text => (outputText += text)); + + mockApp.get.callsFake(() => new ProviderA1()); + + methodsReplFn.action('ProviderA1'); + + expect(outputText).to.equal(` +Methods: + ◻ findAll + ◻ findOne + +`); + }); + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/resolve-repl-fn.spec.ts b/packages/core/test/repl/native-functions/resolve-repl-fn.spec.ts new file mode 100644 index 00000000000..cf78bbd40e3 --- /dev/null +++ b/packages/core/test/repl/native-functions/resolve-repl-fn.spec.ts @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { ResolveReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('ResolveReplFn', () => { + let resolveReplFn: ResolveReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + resolveReplFn = replContext.nativeFunctions.get('resolve') as ResolveReplFn; + }); + afterEach(() => sinon.restore()); + + it('the function name should be "resolve"', () => { + expect(resolveReplFn).to.not.be.undefined; + expect(resolveReplFn.fnDefinition.name).to.eql('resolve'); + }); + + describe('action', () => { + it('should pass arguments down to the application context', async () => { + const token = 'test'; + const contextId = {}; + + await resolveReplFn.action(token, contextId); + expect(mockApp.resolve.calledWith(token, contextId)).to.be.true; + }); + }); +}); diff --git a/packages/core/test/repl/native-functions/select-repl-fn.spec.ts b/packages/core/test/repl/native-functions/select-repl-fn.spec.ts new file mode 100644 index 00000000000..22a033e542c --- /dev/null +++ b/packages/core/test/repl/native-functions/select-repl-fn.spec.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { SelectReplFn } from '../../../repl/native-functions'; +import { ReplContext } from '../../../repl/repl-context'; +import { NestContainer } from '../../../injector/container'; + +describe('SelectReplFn', () => { + let selectReplFn: SelectReplFn; + + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + beforeEach(() => { + selectReplFn = replContext.nativeFunctions.get('select') as SelectReplFn; + }); + afterEach(() => sinon.restore()); + + it('the function name should be "select"', () => { + expect(selectReplFn).to.not.be.undefined; + expect(selectReplFn.fnDefinition.name).to.eql('select'); + }); + + describe('action', () => { + it('should pass arguments down to the application context', () => { + const moduleCls = class TestModule {}; + selectReplFn.action(moduleCls); + expect(mockApp.select.calledWith(moduleCls)).to.be.true; + }); + }); +}); diff --git a/packages/core/test/repl/repl-context.spec.ts b/packages/core/test/repl/repl-context.spec.ts new file mode 100644 index 00000000000..9654dfce5e8 --- /dev/null +++ b/packages/core/test/repl/repl-context.spec.ts @@ -0,0 +1,38 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { NestContainer } from '../../injector/container'; +import { ReplContext } from '../../repl/repl-context'; + +describe('ReplContext', () => { + let replContext: ReplContext; + let mockApp: { + container: NestContainer; + get: sinon.SinonStub; + resolve: sinon.SinonSpy; + select: sinon.SinonSpy; + }; + + before(async () => { + const container = new NestContainer(); + + mockApp = { + container, + get: sinon.stub(), + resolve: sinon.spy(), + select: sinon.spy(), + }; + replContext = new ReplContext(mockApp as any); + }); + + afterEach(() => sinon.restore()); + + it('writeToStdout', () => { + const stdOutWrite = sinon.stub(process.stdout, 'write'); + const text = sinon.stub() as unknown as string; + + replContext.writeToStdout(text); + + expect(stdOutWrite.calledOnce).to.be.true; + expect(stdOutWrite.calledWith(text)).to.be.true; + }); +}); diff --git a/packages/core/test/router/router-explorer.spec.ts b/packages/core/test/router/router-explorer.spec.ts index 5f534321007..daeff71f93b 100644 --- a/packages/core/test/router/router-explorer.spec.ts +++ b/packages/core/test/router/router-explorer.spec.ts @@ -1,5 +1,3 @@ -import { VERSION_NEUTRAL } from '@nestjs/common'; -import { VersionValue } from '@nestjs/common/interfaces'; import { expect } from 'chai'; import * as sinon from 'sinon'; import { Controller } from '../../../common/decorators/core/controller.decorator'; @@ -10,7 +8,6 @@ import { } from '../../../common/decorators/http/request-mapping.decorator'; import { RequestMethod } from '../../../common/enums/request-method.enum'; import { VersioningType } from '../../../common/enums/version-type.enum'; -import { VersioningOptions } from '../../../common/interfaces/version-options.interface'; import { Injector } from '../../../core/injector/injector'; import { ApplicationConfig } from '../../application-config'; import { ExecutionContextHost } from '../../helpers/execution-context-host'; @@ -21,6 +18,7 @@ import { RoutePathMetadata } from '../../router/interfaces/route-path-metadata.i import { RoutePathFactory } from '../../router/route-path-factory'; import { RouterExceptionFilters } from '../../router/router-exception-filters'; import { RouterExplorer } from '../../router/router-explorer'; +import { NoopHttpAdapter } from '../utils/noop-adapter.spec'; describe('RouterExplorer', () => { @Controller('global') @@ -365,735 +363,34 @@ describe('RouterExplorer', () => { }); describe('applyVersionFilter', () => { - describe('when the versioning type is URI', () => { - describe('and the version is VERSION_NEUTRAL', () => { - it('should return the handler', () => { - const version: VersionValue = VERSION_NEUTRAL; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.URI, - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = {}; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - }); - - it('should return the handler', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.URI, - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = {}; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - expect(handler.calledWith(req, res, next)).to.be.true; - }); - }); - - describe('when the versioning type is MEDIA_TYPE', () => { - it('should return next if there is no Media Type header', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.MEDIA_TYPE, - key: 'v=', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: {} }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return next if there is no version in the Media Type header', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.MEDIA_TYPE, - key: 'v=', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/json;' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - describe('when the handler version is an array', () => { - describe('and the version has VERSION_NEUTRAL', () => { - it('should return the handler if there is no version in the Media Type header', () => { - const version: VersionValue = [VERSION_NEUTRAL]; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.MEDIA_TYPE, - key: 'v=', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = {}; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - it('should return next if the version in the Media Type header does not match the handler version', () => { - const version: VersionValue = ['1', '2', VERSION_NEUTRAL]; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.MEDIA_TYPE, - key: 'v=', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/json;v=3' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - }); - - it('should return next if the version in the Media Type header does not match the handler version', () => { - const version = ['1', '2']; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.MEDIA_TYPE, - key: 'v=', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/json;v=3' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return the handler if the version in the Media Type header matches the handler version', () => { - const version = ['1', '2']; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.MEDIA_TYPE, - key: 'v=', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/json;v=1' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - }); - - describe('when the handler version is a string', () => { - it('should return next if the version in the Media Type header does not match the handler version', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.MEDIA_TYPE, - key: 'v=', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/json;v=3' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return the handler if the version in the Media Type header matches the handler version', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.MEDIA_TYPE, - key: 'v=', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/json;v=1' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - }); - }); - - describe('when the versioning type is CUSTOM', () => { - const extractor = (request: { headers: { accept?: string } }) => { - const match = request.headers.accept?.match(/v(\d+\.?\d*)\+json$/); - if (match) { - return match[1]; - } - return null; + it('should call and return the `applyVersionFilter` from the underlying http server', () => { + const router = sinon.spy(new NoopHttpAdapter({})); + const routePathMetadata: RoutePathMetadata = { + methodVersion: + sinon.fake() as unknown as RoutePathMetadata['methodVersion'], + versioningOptions: + sinon.fake() as unknown as RoutePathMetadata['versioningOptions'], }; + const handler = sinon.stub(); - it('should return next if there is no pertinent request object', () => { - const version = '1'; - const versioningOptions: VersioningOptions = { - type: VersioningType.CUSTOM, - extractor, - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: {} }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return next if there is no version in the request object value', () => { - const version = '1'; - const versioningOptions: VersioningOptions = { - type: VersioningType.CUSTOM, - extractor, - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/json;' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - describe('when the handler version is an array', () => { - it('should return next if the version in the request object value does not match the handler version', () => { - const version = ['1', '2']; - const versioningOptions: VersioningOptions = { - type: VersioningType.CUSTOM, - extractor, - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/foo.v3+json' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return the handler if the version in the request object value matches the handler version', () => { - const version = ['1', '2']; - const versioningOptions: VersioningOptions = { - type: VersioningType.CUSTOM, - extractor, - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/foo.v2+json' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - }); - - describe('when the handler version is a string', () => { - it('should return next if the version in the request object value does not match the handler version', () => { - const version = '1'; - const versioningOptions: VersioningOptions = { - type: VersioningType.CUSTOM, - extractor, - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/foo.v2+json' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return the handler if the version in the request object value matches the handler version', () => { - const version = '1'; - const versioningOptions: VersioningOptions = { - type: VersioningType.CUSTOM, - extractor, - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { accept: 'application/foo.v1+json' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - }); - }); - - describe('when the versioning type is HEADER', () => { - it('should return next if there is no Custom Header', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.HEADER, - header: 'X-API-Version', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: {} }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return next if there is no version in the Custom Header', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.HEADER, - header: 'X-API-Version', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { 'X-API-Version': '' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - describe('when the handler version is an array', () => { - describe('and the version has VERSION_NEUTRAL', () => { - it('should return the handler if there is no version in the Custom Header', () => { - const version: VersionValue = [VERSION_NEUTRAL]; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.HEADER, - header: 'X-API-Version', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = {}; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - it('should return next if the version in the Custom Header does not match the handler version', () => { - const version: VersionValue = ['1', '2', VERSION_NEUTRAL]; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.HEADER, - header: 'X-API-Version', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { 'X-API-Version': '3' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - }); - - it('should return next if the version in the Custom Header does not match the handler version', () => { - const version = ['1', '2']; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.HEADER, - header: 'X-API-Version', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { 'X-API-Version': '3' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return the handler if the version in the Custom Header matches the handler version', () => { - const version = ['1', '2']; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.HEADER, - header: 'X-API-Version', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { 'X-API-Version': '1' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - }); - - describe('when the handler version is a string', () => { - it('should return next if the version in the Custom Header does not match the handler version', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.HEADER, - header: 'X-API-Version', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { 'X-API-Version': '3' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(next.called).to.be.true; - }); - - it('should return the handler if the version in the Custom Header matches the handler version', () => { - const version = '1'; - const versioningOptions: RoutePathMetadata['versioningOptions'] = { - type: VersioningType.HEADER, - header: 'X-API-Version', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = { headers: { 'X-API-Version': '1' } }; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); - - expect(handler.calledWith(req, res, next)).to.be.true; - }); - }); - }); - - describe('when versioning type is unrecognized', () => { - it('should throw an error if there is no next function', () => { - const version = '1'; - const versioningOptions: any = { - type: 'UNKNOWN', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, - handler, - ); - - const req = {}; - const res = {}; - const next = null; - - expect(() => versionFilter(req, res, next)).to.throw( - 'HTTP adapter does not support filtering on version', - ); - }); + // We're using type assertion here because `applyVersionFilter` is private + const versionFilter = (routerBuilder as any).applyVersionFilter( + router, + routePathMetadata, + handler, + ); - it('should return next', () => { - const version = '1'; - const versioningOptions: any = { - type: 'UNKNOWN', - }; - const handler = sinon.stub(); - - const routePathMetadata: RoutePathMetadata = { - methodVersion: version, - versioningOptions, - }; - const versionFilter = (routerBuilder as any).applyVersionFilter( - null, - routePathMetadata, + expect( + router.applyVersionFilter.calledOnceWithExactly( handler, - ); - - const req = {}; - const res = {}; - const next = sinon.stub(); - - versionFilter(req, res, next); + routePathMetadata.methodVersion, + routePathMetadata.versioningOptions, + ), + ).to.be.true; - expect(next.called).to.be.true; - }); + expect(router.applyVersionFilter.returnValues[0]).to.be.equal( + versionFilter, + ); }); }); }); diff --git a/packages/core/test/router/router-response-controller.spec.ts b/packages/core/test/router/router-response-controller.spec.ts index 92a3061353d..aac3212628e 100644 --- a/packages/core/test/router/router-response-controller.spec.ts +++ b/packages/core/test/router/router-response-controller.spec.ts @@ -71,13 +71,13 @@ describe('RouterResponseController', () => { describe('transformToResult', () => { describe('when resultOrDeferred', () => { describe('is Promise', () => { - it('should return Promise', async () => { + it('should return Promise that resolves to the value resolved by the input Promise', async () => { const value = 100; expect( await routerResponseController.transformToResult( Promise.resolve(value), ), - ).to.be.eq(100); + ).to.be.eq(value); }); }); @@ -88,16 +88,25 @@ describe('RouterResponseController', () => { await routerResponseController.transformToResult( of(1, 2, 3, lastValue), ), - ).to.be.eq(100); + ).to.be.eq(lastValue); }); }); - describe('is value', () => { - it('should return Promise', async () => { + describe('is an object that has the method `subscribe`', () => { + it('should return a Promise that resolves to the input value', async () => { + const value = { subscribe() {} }; + expect( + await routerResponseController.transformToResult(value), + ).to.equal(value); + }); + }); + + describe('is an ordinary value', () => { + it('should return a Promise that resolves to the input value', async () => { const value = 100; expect( await routerResponseController.transformToResult(value), - ).to.be.eq(100); + ).to.be.eq(value); }); }); }); diff --git a/packages/core/test/scanner.spec.ts b/packages/core/test/scanner.spec.ts index 2fdad2cc297..f2ef6b2acc0 100644 --- a/packages/core/test/scanner.spec.ts +++ b/packages/core/test/scanner.spec.ts @@ -9,6 +9,7 @@ import { Scope } from '../../common/interfaces'; import { ApplicationConfig } from '../application-config'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '../constants'; import { InvalidModuleException } from '../errors/exceptions/invalid-module.exception'; +import { InvalidClassModuleException } from '../errors/exceptions/invalid-class-module.exception'; import { UndefinedModuleException } from '../errors/exceptions/undefined-module.exception'; import { NestContainer } from '../injector/container'; import { InstanceWrapper } from '../injector/instance-wrapper'; @@ -165,16 +166,6 @@ describe('DependenciesScanner', () => { }); describe('insertModule', () => { - let LoggerWarnSpy: sinon.SinonSpy; - - beforeEach(() => { - LoggerWarnSpy = sinon.stub(Logger.prototype, 'warn'); - }); - - afterEach(() => { - LoggerWarnSpy.restore(); - }); - it('should call forwardRef() when forwardRef property exists', () => { sinon.stub(container, 'addModule').returns({} as any); @@ -183,26 +174,26 @@ describe('DependenciesScanner', () => { expect(module.forwardRef.called).to.be.true; }); - it('should logs an warning when passing a class annotated with `@Injectable()` decorator', () => { + it('should throw "InvalidClassModuleException" exception when suppling a class annotated with `@Injectable()` decorator', () => { sinon.stub(container, 'addModule').returns({} as any); - scanner.insertModule(TestComponent, []); - - expect(LoggerWarnSpy.calledOnce).to.be.true; + expect(scanner.insertModule(TestComponent, [])).to.be.rejectedWith( + InvalidClassModuleException, + ); }); - it('should logs an warning when passing a class annotated with `@Controller()` decorator', () => { + it('should throw "InvalidClassModuleException" exception when suppling a class annotated with `@Controller()` decorator', () => { sinon.stub(container, 'addModule').returns({} as any); - scanner.insertModule(TestController, []); - - expect(LoggerWarnSpy.calledOnce).to.be.true; + expect(scanner.insertModule(TestController, [])).to.be.rejectedWith( + InvalidClassModuleException, + ); }); - it('should logs an warning when passing a class annotated with `@Catch()` (only) decorator', () => { + it('should throw "InvalidClassModuleException" exception when suppling a class annotated with (only) `@Catch()` decorator', () => { sinon.stub(container, 'addModule').returns({} as any); - scanner.insertModule(TestExceptionFilterWithoutInjectable, []); - - expect(LoggerWarnSpy.calledOnce).to.be.true; + expect( + scanner.insertModule(TestExceptionFilterWithoutInjectable, []), + ).to.be.rejectedWith(InvalidClassModuleException); }); }); diff --git a/packages/core/test/utils/noop-adapter.spec.ts b/packages/core/test/utils/noop-adapter.spec.ts index c2e73d09fd6..dc7daecb2c1 100644 --- a/packages/core/test/utils/noop-adapter.spec.ts +++ b/packages/core/test/utils/noop-adapter.spec.ts @@ -1,4 +1,5 @@ -import { RequestMethod } from '@nestjs/common'; +import { RequestMethod, VersioningOptions } from '@nestjs/common'; +import { VersionValue } from '@nestjs/common/interfaces'; import { AbstractHttpAdapter } from '../../adapters'; export class NoopHttpAdapter extends AbstractHttpAdapter { @@ -13,11 +14,13 @@ export class NoopHttpAdapter extends AbstractHttpAdapter { getRequestMethod(request: any): any {} getRequestUrl(request: any): any {} reply(response: any, body: any): any {} + end(response: any, message?: any): any {} status(response: any, statusCode: number): any {} render(response: any, view: string, options: any): any {} redirect(response: any, statusCode: number, url: string) {} setErrorHandler(handler: Function, prefix = '/'): any {} setNotFoundHandler(handler: Function, prefix = '/'): any {} + isHeadersSent(response: any): any {} setHeader(response: any, name: string, value: string): any {} registerParserMiddleware(): any {} enableCors(options: any): any {} @@ -25,4 +28,13 @@ export class NoopHttpAdapter extends AbstractHttpAdapter { getType() { return ''; } + applyVersionFilter( + handler: Function, + version: VersionValue, + versioningOptions: VersioningOptions, + ) { + return (req, res, next) => { + return () => {}; + }; + } } diff --git a/packages/microservices/client/client-kafka.ts b/packages/microservices/client/client-kafka.ts index 7aac2451f5f..ba9ad099a33 100644 --- a/packages/microservices/client/client-kafka.ts +++ b/packages/microservices/client/client-kafka.ts @@ -62,7 +62,7 @@ export class ClientKafka extends ClientProxy { const consumerOptions = this.getOptionsProp(this.options, 'consumer') || ({} as ConsumerConfig); const postfixId = - this.getOptionsProp(this.options, 'postfixId') || '-client'; + this.getOptionsProp(this.options, 'postfixId') ?? '-client'; this.producerOnlyMode = this.getOptionsProp(this.options, 'producerOnlyMode') || false; diff --git a/packages/microservices/client/client-redis.ts b/packages/microservices/client/client-redis.ts index 8b42ce6a0ec..517ea271cee 100644 --- a/packages/microservices/client/client-redis.ts +++ b/packages/microservices/client/client-redis.ts @@ -1,48 +1,31 @@ import { Logger } from '@nestjs/common/services/logger.service'; import { loadPackage } from '@nestjs/common/utils/load-package.util'; import { - EmptyError, - fromEvent, - lastValueFrom, - merge, - Subject, - zip, -} from 'rxjs'; -import { share, take, tap } from 'rxjs/operators'; -import { - CONNECT_EVENT, - ECONNREFUSED, ERROR_EVENT, MESSAGE_EVENT, - REDIS_DEFAULT_URL, + REDIS_DEFAULT_HOST, + REDIS_DEFAULT_PORT, } from '../constants'; -import { - ClientOpts, - RedisClient, - RetryStrategyOptions, -} from '../external/redis.interface'; import { ReadPacket, RedisOptions, WritePacket } from '../interfaces'; import { ClientProxy } from './client-proxy'; -let redisPackage: any = {}; +type Redis = any; + +let redisPackage = {} as any; export class ClientRedis extends ClientProxy { protected readonly logger = new Logger(ClientProxy.name); protected readonly subscriptionsCount = new Map(); - protected readonly url: string; - protected pubClient: RedisClient; - protected subClient: RedisClient; + protected pubClient: Redis; + protected subClient: Redis; protected connection: Promise; protected isExplicitlyTerminated = false; constructor(protected readonly options: RedisOptions['options']) { super(); - this.url = - this.getOptionsProp(options, 'url') || - (!this.getOptionsProp(options, 'host') && REDIS_DEFAULT_URL); - redisPackage = loadPackage('redis', ClientRedis.name, () => - require('redis'), + redisPackage = loadPackage('ioredis', ClientRedis.name, () => + require('ioredis'), ); this.initializeSerializer(options); @@ -64,73 +47,57 @@ export class ClientRedis extends ClientProxy { this.isExplicitlyTerminated = true; } - public connect(): Promise { + public async connect(): Promise { if (this.pubClient && this.subClient) { return this.connection; } - const error$ = new Subject(); - - this.pubClient = this.createClient(error$); - this.subClient = this.createClient(error$); + this.pubClient = this.createClient(); + this.subClient = this.createClient(); this.handleError(this.pubClient); this.handleError(this.subClient); - const pubConnect$ = fromEvent(this.pubClient, CONNECT_EVENT); - const subClient$ = fromEvent(this.subClient, CONNECT_EVENT); + this.connection = Promise.all([ + this.subClient.connect(), + this.pubClient.connect(), + ]); + await this.connection; - this.connection = lastValueFrom( - merge(error$, zip(pubConnect$, subClient$)).pipe( - take(1), - tap(() => - this.subClient.on(MESSAGE_EVENT, this.createResponseCallback()), - ), - share(), - ), - ).catch(err => { - if (err instanceof EmptyError) { - return; - } - throw err; - }); + this.subClient.on(MESSAGE_EVENT, this.createResponseCallback()); return this.connection; } - public createClient(error$: Subject): RedisClient { - return redisPackage.createClient({ - ...this.getClientOptions(error$), - url: this.url, + public createClient(): Redis { + return new redisPackage({ + host: REDIS_DEFAULT_HOST, + port: REDIS_DEFAULT_PORT, + ...this.getClientOptions(), + lazyConnect: true, }); } - public handleError(client: RedisClient) { + public handleError(client: Redis) { client.addListener(ERROR_EVENT, (err: any) => this.logger.error(err)); } - public getClientOptions(error$: Subject): Partial { - const retry_strategy = (options: RetryStrategyOptions) => - this.createRetryStrategy(options, error$); + public getClientOptions(): Partial { + const retryStrategy = (times: number) => this.createRetryStrategy(times); return { ...(this.options || {}), - retry_strategy, + retryStrategy, }; } - public createRetryStrategy( - options: RetryStrategyOptions, - error$: Subject, - ): undefined | number | Error { - if (options.error && (options.error as any).code === ECONNREFUSED) { - error$.error(options.error); - } + public createRetryStrategy(times: number): undefined | number { if (this.isExplicitlyTerminated) { return undefined; } if ( !this.getOptionsProp(this.options, 'retryAttempts') || - options.attempt > this.getOptionsProp(this.options, 'retryAttempts') + times > this.getOptionsProp(this.options, 'retryAttempts') ) { - return new Error('Retry time exhausted'); + this.logger.error('Retry time exhausted'); + return; } return this.getOptionsProp(this.options, 'retryDelay') || 0; } diff --git a/packages/microservices/constants.ts b/packages/microservices/constants.ts index 96f8b2fa331..2ae25680580 100644 --- a/packages/microservices/constants.ts +++ b/packages/microservices/constants.ts @@ -2,7 +2,10 @@ import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; export const TCP_DEFAULT_PORT = 3000; export const TCP_DEFAULT_HOST = 'localhost'; -export const REDIS_DEFAULT_URL = 'redis://localhost:6379'; + +export const REDIS_DEFAULT_PORT = 6379; +export const REDIS_DEFAULT_HOST = 'localhost'; + export const NATS_DEFAULT_URL = 'nats://localhost:4222'; export const MQTT_DEFAULT_URL = 'mqtt://localhost:1883'; export const GRPC_DEFAULT_URL = 'localhost:5000'; @@ -38,8 +41,10 @@ export const RQM_DEFAULT_NOACK = true; export const RQM_DEFAULT_PERSISTENT = false; export const GRPC_DEFAULT_PROTO_LOADER = '@grpc/proto-loader'; +export const NO_EVENT_HANDLER = (text: TemplateStringsArray, pattern: string) => + `There is no matching event handler defined in the remote service. Event pattern: ${pattern}`; export const NO_MESSAGE_HANDLER = `There is no matching message handler defined in the remote service.`; -export const NO_EVENT_HANDLER = `There is no matching event handler defined in the remote service.`; + export const DISCONNECTED_RMQ_MESSAGE = `Disconnected from RMQ. Trying to reconnect.`; export const KAFKA_DEFAULT_CLIENT = 'nestjs-consumer'; diff --git a/packages/microservices/context/rpc-proxy.ts b/packages/microservices/context/rpc-proxy.ts index 081167f62d5..521d5b7cd42 100644 --- a/packages/microservices/context/rpc-proxy.ts +++ b/packages/microservices/context/rpc-proxy.ts @@ -1,5 +1,5 @@ import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host'; -import { Observable, isObservable } from 'rxjs'; +import { isObservable, Observable } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { RpcExceptionsHandler } from '../exceptions/rpc-exceptions-handler'; diff --git a/packages/microservices/ctx-host/kafka.context.ts b/packages/microservices/ctx-host/kafka.context.ts index 922ccb9c5cd..54c80475a38 100644 --- a/packages/microservices/ctx-host/kafka.context.ts +++ b/packages/microservices/ctx-host/kafka.context.ts @@ -1,7 +1,12 @@ -import { KafkaMessage } from '../external/kafka.interface'; +import { Consumer, KafkaMessage } from '../external/kafka.interface'; import { BaseRpcContext } from './base-rpc.context'; -type KafkaContextArgs = [KafkaMessage, number, string]; +type KafkaContextArgs = [ + message: KafkaMessage, + partition: number, + topic: string, + consumer: Consumer, +]; export class KafkaContext extends BaseRpcContext { constructor(args: KafkaContextArgs) { @@ -28,4 +33,11 @@ export class KafkaContext extends BaseRpcContext { getTopic() { return this.args[2]; } + + /** + * Returns the Kafka consumer reference. + */ + getConsumer() { + return this.args[3]; + } } diff --git a/packages/microservices/decorators/event-pattern.decorator.ts b/packages/microservices/decorators/event-pattern.decorator.ts index 34708b007a0..b44825d638f 100644 --- a/packages/microservices/decorators/event-pattern.decorator.ts +++ b/packages/microservices/decorators/event-pattern.decorator.ts @@ -48,7 +48,11 @@ export const EventPattern: { key: string | symbol, descriptor: PropertyDescriptor, ) => { - Reflect.defineMetadata(PATTERN_METADATA, metadata, descriptor.value); + Reflect.defineMetadata( + PATTERN_METADATA, + [].concat(metadata), + descriptor.value, + ); Reflect.defineMetadata( PATTERN_HANDLER_METADATA, PatternHandler.EVENT, diff --git a/packages/microservices/decorators/message-pattern.decorator.ts b/packages/microservices/decorators/message-pattern.decorator.ts index 3e17d8569d9..6cb4ed1d830 100644 --- a/packages/microservices/decorators/message-pattern.decorator.ts +++ b/packages/microservices/decorators/message-pattern.decorator.ts @@ -62,7 +62,11 @@ export const MessagePattern: { key: string | symbol, descriptor: PropertyDescriptor, ) => { - Reflect.defineMetadata(PATTERN_METADATA, metadata, descriptor.value); + Reflect.defineMetadata( + PATTERN_METADATA, + [].concat(metadata), + descriptor.value, + ); Reflect.defineMetadata( PATTERN_HANDLER_METADATA, PatternHandler.MESSAGE, diff --git a/packages/microservices/deserializers/kafka-request.deserializer.ts b/packages/microservices/deserializers/kafka-request.deserializer.ts new file mode 100644 index 00000000000..99dae311976 --- /dev/null +++ b/packages/microservices/deserializers/kafka-request.deserializer.ts @@ -0,0 +1,21 @@ +import { IncomingEvent, IncomingRequest } from '../interfaces'; +import { KafkaRequest } from '../serializers/kafka-request.serializer'; +import { IncomingRequestDeserializer } from './incoming-request.deserializer'; + +export class KafkaRequestDeserializer extends IncomingRequestDeserializer { + mapToSchema( + data: KafkaRequest, + options?: Record, + ): IncomingRequest | IncomingEvent { + if (!options) { + return { + pattern: undefined, + data: undefined, + }; + } + return { + pattern: options.channel, + data: data?.value ?? data, + }; + } +} diff --git a/packages/microservices/exceptions/index.ts b/packages/microservices/exceptions/index.ts index dd6e4d29892..73e4c3176b3 100644 --- a/packages/microservices/exceptions/index.ts +++ b/packages/microservices/exceptions/index.ts @@ -1,2 +1,3 @@ export * from './base-rpc-exception-filter'; +export * from './kafka-retriable-exception'; export * from './rpc-exception'; diff --git a/packages/microservices/exceptions/kafka-retriable-exception.ts b/packages/microservices/exceptions/kafka-retriable-exception.ts new file mode 100644 index 00000000000..c62194a2e30 --- /dev/null +++ b/packages/microservices/exceptions/kafka-retriable-exception.ts @@ -0,0 +1,17 @@ +import { RpcException } from './rpc-exception'; + +/** + * Exception that instructs Kafka driver to instead of introspecting + * error processing flow and sending serialized error message to the consumer, + * force bubble it up to the "eachMessage" callback of the underlying "kafkajs" package + * (even if interceptors are applied, or an observable stream is returned from the message handler). + * + * A transient exception that if retried may succeed. + * + * @publicApi + */ +export class KafkaRetriableException extends RpcException { + public getError(): string | object { + return this; + } +} diff --git a/packages/microservices/external/grpc-options.interface.ts b/packages/microservices/external/grpc-options.interface.ts index f00fb4cb799..3c5bd22c27e 100644 --- a/packages/microservices/external/grpc-options.interface.ts +++ b/packages/microservices/external/grpc-options.interface.ts @@ -16,5 +16,5 @@ export interface ChannelOptions { 'grpc.max_reconnect_backoff_ms'?: number; 'grpc.use_local_subchannel_pool'?: number; 'grpc-node.max_session_memory'?: number; - [key: string]: string | number | undefined; + [key: string]: any; } diff --git a/packages/microservices/external/redis.interface.ts b/packages/microservices/external/redis.interface.ts index 00ae1bba7b3..6de36c43919 100644 --- a/packages/microservices/external/redis.interface.ts +++ b/packages/microservices/external/redis.interface.ts @@ -1,383 +1,213 @@ -/* eslint-disable @typescript-eslint/adjacent-overload-signatures */ +import { ConnectionOptions } from 'tls'; -export interface RetryStrategyOptions { - error: Error; - total_retry_time: number; - times_connected: number; - attempt: number; -} +/** + * @see https://github.dev/luin/ioredis/blob/df04dd8d87a44d3b64b385c86581915248554508/lib/redis/RedisOptions.ts#L184 + */ +export interface IORedisOptions { + Connector?: any; + retryStrategy?: (times: number) => number | void | null; + + /** + * If a command does not return a reply within a set number of milliseconds, + * a "Command timed out" error will be thrown. + */ + commandTimeout?: number; + /** + * Enable/disable keep-alive functionality. + * @link https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay + * @default 0 + */ + keepAlive?: number; + + /** + * Enable/disable the use of Nagle's algorithm. + * @link https://nodejs.org/api/net.html#socketsetnodelaynodelay + * @default true + */ + noDelay?: boolean; + + /** + * Set the name of the connection to make it easier to identity the connection + * in client list. + * @link https://redis.io/commands/client-setname + */ + connectionName?: string; + + /** + * If set, client will send AUTH command with the value of this option as the first argument when connected. + * This is supported since Redis 6. + */ + username?: string; -export interface ClientOpts { - auth_pass?: string; - command_queue_high_water?: number; - command_queue_low_water?: number; - connect_timeout?: number; - db?: string; - detect_buffers?: boolean; - disable_resubscribing?: boolean; - enable_offline_queue?: boolean; - family?: string; - host?: string; - max_attempts?: number; - no_ready_check?: boolean; - parser?: string; + /** + * If set, client will send AUTH command with the value of this option when connected. + */ password?: string; - path?: string; - port?: number; - prefix?: string; - rename_commands?: any; - retry_max_delay?: number; - retry_strategy?: any; - retry_unfulfilled_commands?: boolean; - return_buffers?: boolean; - socket_keepalive?: boolean; - socket_nodelay?: boolean; - string_numbers?: boolean; - tls?: any; - url?: string; -} -export interface RedisClient { - // event: connect - // event: error - // event: message - // event: pmessage - // event: subscribe - // event: psubscribe - // event: unsubscribe - // event: punsubscribe + /** + * Database index to use. + * + * @default 0 + */ + db?: number; - connected: boolean; - retry_delay: number; - retry_backoff: number; - command_queue: any[]; - offline_queue: any[]; - server_info: any; + /** + * When the client reconnects, channels subscribed in the previous connection will be + * resubscribed automatically if `autoResubscribe` is `true`. + * @default true + */ + autoResubscribe?: boolean; /** - * Forcibly close the connection to the Redis server. Note that this does not wait until all replies have been parsed. If you want to exit cleanly, call client.quit() + * Whether or not to resend unfulfilled commands on reconnect. + * Unfulfilled commands are most likely to be blocking commands such as `brpop` or `blpop`. + * @default true + */ + autoResendUnfulfilledCommands?: boolean; + /** + * Whether or not to reconnect on certain Redis errors. + * This options by default is `null`, which means it should never reconnect on Redis errors. + * You can pass a function that accepts an Redis error, and returns: + * - `true` or `1` to trigger a reconnection. + * - `false` or `0` to not reconnect. + * - `2` to reconnect and resend the failed command (who triggered the error) after reconnection. + * @example + * ```js + * const redis = new Redis({ + * reconnectOnError(err) { + * const targetError = "READONLY"; + * if (err.message.includes(targetError)) { + * // Only reconnect when the error contains "READONLY" + * return true; // or `return 1;` + * } + * }, + * }); + * ``` + * @default null + */ + reconnectOnError?: ((err: Error) => boolean | 1 | 2) | null; + + /** + * @default false + */ + readOnly?: boolean; + /** + * When enabled, numbers returned by Redis will be converted to JavaScript strings instead of numbers. + * This is necessary if you want to handle big numbers (above `Number.MAX_SAFE_INTEGER` === 2^53). + * @default false + */ + stringNumbers?: boolean; + + /** + * How long the client will wait before killing a socket due to inactivity during initial connection. + * @default 10000 + */ + connectTimeout?: number; + + /** + * This option is used internally when you call `redis.monitor()` to tell Redis + * to enter the monitor mode when the connection is established. * - * @param {boolean} flush You should set flush to true, if you are not absolutely sure you do not care about any other commands. If you set flush to false all still running commands will silently fail. + * @default false */ - end(flush: boolean): void; - unref(): void; + monitor?: boolean; /** - * Stop sending commands and queue the commands. + * The commands that don't get a reply due to the connection to the server is lost are + * put into a queue and will be resent on reconnect (if allowed by the `retryStrategy` option). + * This option is used to configure how many reconnection attempts should be allowed before + * the queue is flushed with a `MaxRetriesPerRequestError` error. + * Set this options to `null` instead of a number to let commands wait forever + * until the connection is alive again. + * + * @default 20 */ - cork(): void; + maxRetriesPerRequest?: number | null; /** - * Resume and send the queued commands at once. + * @default 10000 */ - uncork(): void; + maxLoadingRetryTime?: number; + /** + * @default false + */ + enableAutoPipelining?: boolean; + /** + * @default [] + */ + autoPipeliningIgnoredCommands?: string[]; + offlineQueue?: boolean; + commandQueue?: boolean; - // Low level command execution - send_command(command: string, ...args: any[]): boolean; + /** + * + * By default, if the connection to Redis server has not been established, commands are added to a queue + * and are executed once the connection is "ready" (when `enableReadyCheck` is true, "ready" means + * the Redis server has loaded the database from disk, otherwise means the connection to the Redis + * server has been established). If this option is false, when execute the command when the connection + * isn't ready, an error will be returned. + * + * @default true + */ + enableOfflineQueue?: boolean; - // Connection (http://redis.io/commands#connection) - auth(password: string, callback?: any): boolean; - ping(callback?: any): boolean; + /** + * The client will sent an INFO command to check whether the server is still loading data from the disk ( + * which happens when the server is just launched) when the connection is established, and only wait until + * the loading process is finished before emitting the `ready` event. + * + * @default true + */ + enableReadyCheck?: boolean; - // Strings (http://redis.io/commands#strings) - append(key: string, value: string, callback?: any): boolean; - bitcount(key: string, callback?: any): boolean; - bitcount(key: string, start: number, end: number, callback?: any): boolean; - set(key: string, value: string, callback?: any): boolean; - get(key: string, callback?: any): boolean; - exists(key: string, value: string, callback?: any): boolean; + /** + * When a Redis instance is initialized, a connection to the server is immediately established. Set this to + * true will delay the connection to the server until the first command is sent or `redis.connect()` is called + * explicitly. + * + * @default false + */ - publish(channel: string, value: any): boolean; - subscribe(channel: string): boolean; - on(event: string, callback: Function): any; - off(event: string, callback: Function): any; - addListener(event: string, callback: Function): any; + lazyConnect?: boolean; - /* - commands = set_union([ - "get", "set", "setnx", "setex", "append", "strlen", "del", "exists", "setbit", "getbit", "setrange", "getrange", "substr", - "incr", "decr", "mget", "rpush", "lpush", "rpushx", "lpushx", "linsert", "rpop", "lpop", "brpop", "brpoplpush", "blpop", "llen", "lindex", - "lset", "lrange", "ltrim", "lrem", "rpoplpush", "sadd", "srem", "smove", "sismember", "scard", "spop", "srandmember", "sinter", "sinterstore", - "sunion", "sunionstore", "sdiff", "sdiffstore", "smembers", "zadd", "zincrby", "zrem", "zremrangebyscore", "zremrangebyrank", "zunionstore", - "zinterstore", "zrange", "zrangebyscore", "zrevrangebyscore", "zcount", "zrevrange", "zcard", "zscore", "zrank", "zrevrank", "hset", "hsetnx", - "hget", "hmset", "hmget", "hincrby", "hincrbyfloat", "hdel", "hlen", "hkeys", "hvals", "hgetall", "hexists", "incrby", "decrby", "getset", "mset", "msetnx", - "randomkey", "select", "move", "rename", "renamenx", "expire", "expireat", "keys", "dbsize", "auth", "ping", "echo", "save", "bgsave", - "bgrewriteaof", "shutdown", "lastsave", "type", "any", "exec", "discard", "sync", "flushdb", "flushall", "sort", "info", "monitor", "ttl", - "persist", "slaveof", "debug", "config", "subscribe", "unsubscribe", "psubscribe", "punsubscribe", "publish", "watch", "unwatch", "cluster", - "restore", "migrate", "dump", "object", "client", "eval", "evalsha"], require("./lib/commands")); + /** + * @default undefined */ + scripts?: Record< + string, + { lua: string; numberOfKeys?: number; readOnly?: boolean } + >; - get(args: any[], callback?: any): boolean; - get(...args: any[]): boolean; - set(args: any[], callback?: any): boolean; - set(...args: any[]): boolean; - setnx(args: any[], callback?: any): boolean; - setnx(...args: any[]): boolean; - setex(args: any[], callback?: any): boolean; - setex(...args: any[]): boolean; - append(args: any[], callback?: any): boolean; - append(...args: any[]): boolean; - strlen(args: any[], callback?: any): boolean; - strlen(...args: any[]): boolean; - del(args: any[], callback?: any): boolean; - del(...args: any[]): boolean; - exists(args: any[], callback?: any): boolean; - exists(...args: any[]): boolean; - setbit(args: any[], callback?: any): boolean; - setbit(...args: any[]): boolean; - getbit(args: any[], callback?: any): boolean; - getbit(...args: any[]): boolean; - setrange(args: any[], callback?: any): boolean; - setrange(...args: any[]): boolean; - getrange(args: any[], callback?: any): boolean; - getrange(...args: any[]): boolean; - substr(args: any[], callback?: any): boolean; - substr(...args: any[]): boolean; - incr(args: any[], callback?: any): boolean; - incr(...args: any[]): boolean; - decr(args: any[], callback?: any): boolean; - decr(...args: any[]): boolean; - mget(args: any[], callback?: any): boolean; - mget(...args: any[]): boolean; - rpush(...args: any[]): boolean; - lpush(args: any[], callback?: any): boolean; - lpush(...args: any[]): boolean; - rpushx(args: any[], callback?: any): boolean; - rpushx(...args: any[]): boolean; - lpushx(args: any[], callback?: any): boolean; - lpushx(...args: any[]): boolean; - linsert(args: any[], callback?: any): boolean; - linsert(...args: any[]): boolean; - rpop(args: any[], callback?: any): boolean; - rpop(...args: any[]): boolean; - lpop(args: any[], callback?: any): boolean; - lpop(...args: any[]): boolean; - brpop(args: any[], callback?: any): boolean; - brpop(...args: any[]): boolean; - brpoplpush(args: any[], callback?: any): boolean; - brpoplpush(...args: any[]): boolean; - blpop(args: any[], callback?: any): boolean; - blpop(...args: any[]): boolean; - llen(args: any[], callback?: any): boolean; - llen(...args: any[]): boolean; - lindex(args: any[], callback?: any): boolean; - lindex(...args: any[]): boolean; - lset(args: any[], callback?: any): boolean; - lset(...args: any[]): boolean; - lrange(args: any[], callback?: any): boolean; - lrange(...args: any[]): boolean; - ltrim(args: any[], callback?: any): boolean; - ltrim(...args: any[]): boolean; - lrem(args: any[], callback?: any): boolean; - lrem(...args: any[]): boolean; - rpoplpush(args: any[], callback?: any): boolean; - rpoplpush(...args: any[]): boolean; - sadd(args: any[], callback?: any): boolean; - sadd(...args: any[]): boolean; - srem(args: any[], callback?: any): boolean; - srem(...args: any[]): boolean; - smove(args: any[], callback?: any): boolean; - smove(...args: any[]): boolean; - sismember(args: any[], callback?: any): boolean; - sismember(...args: any[]): boolean; - scard(args: any[], callback?: any): boolean; - scard(...args: any[]): boolean; - spop(args: any[], callback?: any): boolean; - spop(...args: any[]): boolean; - srandmember(args: any[], callback?: any): boolean; - srandmember(...args: any[]): boolean; - sinter(args: any[], callback?: any): boolean; - sinter(...args: any[]): boolean; - sinterstore(args: any[], callback?: any): boolean; - sinterstore(...args: any[]): boolean; - sunion(args: any[], callback?: any): boolean; - sunion(...args: any[]): boolean; - sunionstore(args: any[], callback?: any): boolean; - sunionstore(...args: any[]): boolean; - sdiff(args: any[], callback?: any): boolean; - sdiff(...args: any[]): boolean; - sdiffstore(args: any[], callback?: any): boolean; - sdiffstore(...args: any[]): boolean; - smembers(args: any[], callback?: any): boolean; - smembers(...args: any[]): boolean; - zadd(args: any[], callback?: any): boolean; - zadd(...args: any[]): boolean; - zincrby(args: any[], callback?: any): boolean; - zincrby(...args: any[]): boolean; - zrem(args: any[], callback?: any): boolean; - zrem(...args: any[]): boolean; - zremrangebyscore(args: any[], callback?: any): boolean; - zremrangebyscore(...args: any[]): boolean; - zremrangebyrank(args: any[], callback?: any): boolean; - zremrangebyrank(...args: any[]): boolean; - zunionstore(args: any[], callback?: any): boolean; - zunionstore(...args: any[]): boolean; - zinterstore(args: any[], callback?: any): boolean; - zinterstore(...args: any[]): boolean; - zrange(args: any[], callback?: any): boolean; - zrange(...args: any[]): boolean; - zrangebyscore(args: any[], callback?: any): boolean; - zrangebyscore(...args: any[]): boolean; - zrevrangebyscore(args: any[], callback?: any): boolean; - zrevrangebyscore(...args: any[]): boolean; - zcount(args: any[], callback?: any): boolean; - zcount(...args: any[]): boolean; - zrevrange(args: any[], callback?: any): boolean; - zrevrange(...args: any[]): boolean; - zcard(args: any[], callback?: any): boolean; - zcard(...args: any[]): boolean; - zscore(args: any[], callback?: any): boolean; - zscore(...args: any[]): boolean; - zrank(args: any[], callback?: any): boolean; - zrank(...args: any[]): boolean; - zrevrank(args: any[], callback?: any): boolean; - zrevrank(...args: any[]): boolean; - hset(args: any[], callback?: any): boolean; - hset(...args: any[]): boolean; - hsetnx(args: any[], callback?: any): boolean; - hsetnx(...args: any[]): boolean; - hget(args: any[], callback?: any): boolean; - hget(...args: any[]): boolean; - hmset(args: any[], callback?: any): boolean; - hmset(key: string, hash: any, callback?: any): boolean; - hmset(...args: any[]): boolean; - hmget(args: any[], callback?: any): boolean; - hmget(...args: any[]): boolean; - hincrby(args: any[], callback?: any): boolean; - hincrby(...args: any[]): boolean; - hincrbyfloat(args: any[], callback?: any): boolean; - hincrbyfloat(...args: any[]): boolean; - hdel(args: any[], callback?: any): boolean; - hdel(...args: any[]): boolean; - hlen(args: any[], callback?: any): boolean; - hlen(...args: any[]): boolean; - hkeys(args: any[], callback?: any): boolean; - hkeys(...args: any[]): boolean; - hvals(args: any[], callback?: any): boolean; - hvals(...args: any[]): boolean; - hgetall(args: any[], callback?: any): boolean; - hgetall(...args: any[]): boolean; - hgetall(key: string, callback?: any): boolean; - hexists(args: any[], callback?: any): boolean; - hexists(...args: any[]): boolean; - incrby(args: any[], callback?: any): boolean; - incrby(...args: any[]): boolean; - decrby(args: any[], callback?: any): boolean; - decrby(...args: any[]): boolean; - getset(args: any[], callback?: any): boolean; - getset(...args: any[]): boolean; - mset(args: any[], callback?: any): boolean; - mset(...args: any[]): boolean; - msetnx(args: any[], callback?: any): boolean; - msetnx(...args: any[]): boolean; - randomkey(args: any[], callback?: any): boolean; - randomkey(...args: any[]): boolean; - select(args: any[], callback?: any): void; - select(...args: any[]): void; - move(args: any[], callback?: any): boolean; - move(...args: any[]): boolean; - rename(args: any[], callback?: any): boolean; - rename(...args: any[]): boolean; - renamenx(args: any[], callback?: any): boolean; - renamenx(...args: any[]): boolean; - expire(args: any[], callback?: any): boolean; - expire(...args: any[]): boolean; - expireat(args: any[], callback?: any): boolean; - expireat(...args: any[]): boolean; - keys(args: any[], callback?: any): boolean; - keys(...args: any[]): boolean; - dbsize(args: any[], callback?: any): boolean; - dbsize(...args: any[]): boolean; - auth(args: any[], callback?: any): void; - auth(...args: any[]): void; - ping(args: any[], callback?: any): boolean; - ping(...args: any[]): boolean; - echo(args: any[], callback?: any): boolean; - echo(...args: any[]): boolean; - save(args: any[], callback?: any): boolean; - save(...args: any[]): boolean; - bgsave(args: any[], callback?: any): boolean; - bgsave(...args: any[]): boolean; - bgrewriteaof(args: any[], callback?: any): boolean; - bgrewriteaof(...args: any[]): boolean; - shutdown(args: any[], callback?: any): boolean; - shutdown(...args: any[]): boolean; - lastsave(args: any[], callback?: any): boolean; - lastsave(...args: any[]): boolean; - type(args: any[], callback?: any): boolean; - type(...args: any[]): boolean; - any(args: any[], callback?: any): any; - any(...args: any[]): any; - exec(args: any[], callback?: any): boolean; - exec(...args: any[]): boolean; - discard(args: any[], callback?: any): boolean; - discard(...args: any[]): boolean; - sync(args: any[], callback?: any): boolean; - sync(...args: any[]): boolean; - flushdb(args: any[], callback?: any): boolean; - flushdb(...args: any[]): boolean; - flushall(args: any[], callback?: any): boolean; - flushall(...args: any[]): boolean; - sort(args: any[], callback?: any): boolean; - sort(...args: any[]): boolean; - info(args: any[], callback?: any): boolean; - info(...args: any[]): boolean; - monitor(args: any[], callback?: any): boolean; - monitor(...args: any[]): boolean; - ttl(args: any[], callback?: any): boolean; - ttl(...args: any[]): boolean; - persist(args: any[], callback?: any): boolean; - persist(...args: any[]): boolean; - slaveof(args: any[], callback?: any): boolean; - slaveof(...args: any[]): boolean; - debug(args: any[], callback?: any): boolean; - debug(...args: any[]): boolean; - config(args: any[], callback?: any): boolean; - config(...args: any[]): boolean; - subscribe(args: any[], callback?: any): boolean; - subscribe(...args: any[]): boolean; - unsubscribe(args: any[], callback?: any): boolean; - unsubscribe(...args: any[]): boolean; - psubscribe(args: any[], callback?: any): boolean; - psubscribe(...args: any[]): boolean; - punsubscribe(args: any[], callback?: any): boolean; - punsubscribe(...args: any[]): boolean; - publish(args: any[], callback?: any): boolean; - publish(...args: any[]): boolean; - watch(args: any[], callback?: any): boolean; - watch(...args: any[]): boolean; - unwatch(args: any[], callback?: any): boolean; - unwatch(...args: any[]): boolean; - cluster(args: any[], callback?: any): boolean; - cluster(...args: any[]): boolean; - restore(args: any[], callback?: any): boolean; - restore(...args: any[]): boolean; - migrate(args: any[], callback?: any): boolean; - migrate(...args: any[]): boolean; - dump(args: any[], callback?: any): boolean; - dump(...args: any[]): boolean; - object(args: any[], callback?: any): boolean; - object(...args: any[]): boolean; - client(args: any[], callback?: any): boolean; - client(...args: any[]): boolean; - eval(args: any[], callback?: any): boolean; - eval(...args: any[]): boolean; - evalsha(args: any[], callback?: any): boolean; - evalsha(...args: any[]): boolean; - script(args: any[], callback?: any): boolean; - script(...args: any[]): boolean; - script(key: string, callback?: any): boolean; - quit(args: any[], callback?: any): boolean; - quit(...args: any[]): boolean; - sscan(...args: any[]): boolean; - sscan(args: any[], callback?: any): boolean; - scan(...args: any[]): boolean; - scan(args: any[], callback?: any): boolean; - hscan(...args: any[]): boolean; - hscan(args: any[], callback?: any): boolean; - zscan(...args: any[]): boolean; - zscan(args: any[], callback?: any): boolean; + keyPrefix?: string; + showFriendlyErrorStack?: boolean; - // Extras - duplicate(options?: any[], callback?: any): RedisClient; + // StandaloneConnectionOptions + disconnectTimeout?: number; + tls?: ConnectionOptions; + + // SentinelConnectionOptions + /** + * Master group name of the Sentinel + */ + name?: string; + /** + * @default "master" + */ + role?: 'master' | 'slave'; + sentinelUsername?: string; + sentinelPassword?: string; + sentinels?: Array>; + sentinelRetryStrategy?: (retryAttempts: number) => number | void | null; + sentinelReconnectStrategy?: (retryAttempts: number) => number | void | null; + preferredSlaves?: any; + sentinelCommandTimeout?: number; + enableTLSForSentinelMode?: boolean; + sentinelTLS?: ConnectionOptions; + natMap?: any; + updateSentinels?: boolean; + /** + * @default 10 + */ + sentinelMaxConnections?: number; + failoverDetector?: boolean; } diff --git a/packages/microservices/interfaces/message-handler.interface.ts b/packages/microservices/interfaces/message-handler.interface.ts index d8152e2f8fd..00aecaa6e2c 100644 --- a/packages/microservices/interfaces/message-handler.interface.ts +++ b/packages/microservices/interfaces/message-handler.interface.ts @@ -1,8 +1,13 @@ import { Observable } from 'rxjs'; export interface MessageHandler { - (data: TInput, ctx?: TContext): Promise>; - next?: (data: TInput, ctx?: TContext) => Promise>; + (data: TInput, ctx?: TContext): + | Promise> + | Promise; + next?: ( + data: TInput, + ctx?: TContext, + ) => Promise> | Promise; isEventHandler?: boolean; extras?: Record; } diff --git a/packages/microservices/interfaces/microservice-configuration.interface.ts b/packages/microservices/interfaces/microservice-configuration.interface.ts index d5f33963262..f167a414ce7 100644 --- a/packages/microservices/interfaces/microservice-configuration.interface.ts +++ b/packages/microservices/interfaces/microservice-configuration.interface.ts @@ -10,7 +10,7 @@ import { ProducerRecord, } from '../external/kafka.interface'; import { MqttClientOptions, QoS } from '../external/mqtt-options.interface'; -import { ClientOpts } from '../external/redis.interface'; +import { IORedisOptions } from '../external/redis.interface'; import { RmqUrl } from '../external/rmq-url.interface'; import { TcpSocket } from '../helpers'; import { CustomTransportStrategy } from './custom-transport-strategy.interface'; @@ -35,9 +35,6 @@ export interface CustomStrategy { export interface GrpcOptions { transport?: Transport.GRPC; options: { - interceptors?: Array< - (options: any, nextCall: (options: any) => any) => any - >; url?: string; maxSendMessageLength?: number; maxReceiveMessageLength?: number; @@ -94,7 +91,7 @@ export interface RedisOptions { retryDelay?: number; serializer?: Serializer; deserializer?: Deserializer; - } & ClientOpts; + } & IORedisOptions; } export interface MqttOptions { @@ -194,6 +191,9 @@ export interface KafkaParserConfig { export interface KafkaOptions { transport?: Transport.KAFKA; options?: { + /** + * Defaults to `"-server"` on server side and `"-client"` on client side. + */ postfixId?: string; client?: KafkaConfig; consumer?: ConsumerConfig; diff --git a/packages/microservices/listener-metadata-explorer.ts b/packages/microservices/listener-metadata-explorer.ts index 83e8f04c259..66ddffaf01c 100644 --- a/packages/microservices/listener-metadata-explorer.ts +++ b/packages/microservices/listener-metadata-explorer.ts @@ -20,7 +20,7 @@ export interface ClientProperties { } export interface EventOrMessageListenerDefinition { - pattern: PatternMetadata; + patterns: PatternMetadata[]; methodKey: string; isEventHandler: boolean; targetCallback: (...args: any[]) => any; @@ -58,13 +58,13 @@ export class ListenerMetadataExplorer { if (isUndefined(handlerType)) { return; } - const pattern = Reflect.getMetadata(PATTERN_METADATA, targetCallback); + const patterns = Reflect.getMetadata(PATTERN_METADATA, targetCallback); const transport = Reflect.getMetadata(TRANSPORT_METADATA, targetCallback); const extras = Reflect.getMetadata(PATTERN_EXTRAS_METADATA, targetCallback); return { methodKey, targetCallback, - pattern, + patterns, transport, extras, isEventHandler: handlerType === PatternHandler.EVENT, diff --git a/packages/microservices/listeners-controller.ts b/packages/microservices/listeners-controller.ts index 7687ccf460a..df275cb112c 100644 --- a/packages/microservices/listeners-controller.ts +++ b/packages/microservices/listeners-controller.ts @@ -71,8 +71,21 @@ export class ListenersController { isUndefined(server.transportId) || transport === server.transportId, ) + .reduce((acc, handler) => { + // Optional chaining for backward-compatibility + handler.patterns?.forEach(pattern => + acc.push({ ...handler, patterns: [pattern] }), + ); + return acc; + }, []) .forEach( - ({ pattern, targetCallback, methodKey, extras, isEventHandler }) => { + ({ + patterns: [pattern], + targetCallback, + methodKey, + extras, + isEventHandler, + }) => { if (isStatic) { const proxy = this.contextCreator.create( instance as object, diff --git a/packages/microservices/microservices-module.ts b/packages/microservices/microservices-module.ts index 735fda9fe53..54632e4122f 100644 --- a/packages/microservices/microservices-module.ts +++ b/packages/microservices/microservices-module.ts @@ -24,13 +24,12 @@ export class MicroservicesModule { private listenersController: ListenersController; public register(container: NestContainer, config: ApplicationConfig) { - const rpcProxy = new RpcProxy(); const exceptionFiltersContext = new ExceptionFiltersContext( container, config, ); const contextCreator = new RpcContextCreator( - rpcProxy, + new RpcProxy(), exceptionFiltersContext, new PipesContextCreator(container, config), new PipesConsumer(), diff --git a/packages/microservices/nest-microservice.ts b/packages/microservices/nest-microservice.ts index be5a79c5f89..dbaf198005a 100644 --- a/packages/microservices/nest-microservice.ts +++ b/packages/microservices/nest-microservice.ts @@ -136,13 +136,6 @@ export class NestMicroservice }); } - public async listenAsync(): Promise { - this.logger.warn( - 'DEPRECATED! "listenAsync" method is deprecated and will be removed in the next major release. Please, use "listen" instead.', - ); - return this.listen(); - } - public async close(): Promise { await this.server.close(); if (this.isTerminated) { diff --git a/packages/microservices/package-lock.json b/packages/microservices/package-lock.json index 4b922833ade..f69497b577a 100644 --- a/packages/microservices/package-lock.json +++ b/packages/microservices/package-lock.json @@ -1,39 +1,39 @@ { - "name": "microservices", - "version": "8.4.5", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "iterare": "1.2.1", - "tslib": "2.4.0" - } - }, - "node_modules/iterare": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", - "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", - "engines": { - "node": ">=6" - } - }, - "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - }, - "dependencies": { - "iterare": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", - "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" - }, - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } + "name": "microservices", + "version": "8.4.5", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.4.0" + } + }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + }, + "dependencies": { + "iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } } diff --git a/packages/microservices/server/server-kafka.ts b/packages/microservices/server/server-kafka.ts index 829d30d91bc..2a426eaf673 100644 --- a/packages/microservices/server/server-kafka.ts +++ b/packages/microservices/server/server-kafka.ts @@ -1,14 +1,17 @@ import { Logger } from '@nestjs/common/services/logger.service'; import { isNil } from '@nestjs/common/utils/shared.utils'; -import { Observable } from 'rxjs'; +import { isObservable, lastValueFrom, Observable, ReplaySubject } from 'rxjs'; import { KAFKA_DEFAULT_BROKER, KAFKA_DEFAULT_CLIENT, KAFKA_DEFAULT_GROUP, + NO_EVENT_HANDLER, NO_MESSAGE_HANDLER, } from '../constants'; import { KafkaContext } from '../ctx-host'; +import { KafkaRequestDeserializer } from '../deserializers/kafka-request.deserializer'; import { KafkaHeaders, Transport } from '../enums'; +import { KafkaRetriableException } from '../exceptions'; import { BrokersFunction, Consumer, @@ -26,6 +29,7 @@ import { CustomTransportStrategy, KafkaOptions, OutgoingResponse, + ReadPacket, } from '../interfaces'; import { KafkaRequestSerializer } from '../serializers/kafka-request.serializer'; import { Server } from './server'; @@ -53,7 +57,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy { const consumerOptions = this.getOptionsProp(this.options, 'consumer') || ({} as ConsumerConfig); const postfixId = - this.getOptionsProp(this.options, 'postfixId') || '-server'; + this.getOptionsProp(this.options, 'postfixId') ?? '-server'; this.brokers = clientOptions.brokers || [KAFKA_DEFAULT_BROKER]; @@ -162,6 +166,7 @@ export class ServerKafka extends Server implements CustomTransportStrategy { rawMessage, payload.partition, payload.topic, + this.consumer, ]); const handler = this.getHandlerByPattern(packet.pattern); // if the correlation id or reply topic is not set @@ -186,7 +191,37 @@ export class ServerKafka extends Server implements CustomTransportStrategy { const response$ = this.transformToObservable( await handler(packet.data, kafkaContext), ); - response$ && this.send(response$, publish); + + const replayStream$ = new ReplaySubject(); + await this.combineStreamsAndThrowIfRetriable(response$, replayStream$); + + this.send(replayStream$, publish); + } + + private combineStreamsAndThrowIfRetriable( + response$: Observable, + replayStream$: ReplaySubject, + ) { + return new Promise((resolve, reject) => { + let isPromiseResolved = false; + response$.subscribe({ + next: val => { + replayStream$.next(val); + if (!isPromiseResolved) { + isPromiseResolved = true; + resolve(); + } + }, + error: err => { + if (err instanceof KafkaRetriableException && !isPromiseResolved) { + isPromiseResolved = true; + reject(err); + } + replayStream$.error(err); + }, + complete: () => replayStream$.complete(), + }); + }); } public async sendMessage( @@ -228,9 +263,12 @@ export class ServerKafka extends Server implements CustomTransportStrategy { if (!outgoingResponse.err) { return; } - outgoingMessage.headers[KafkaHeaders.NEST_ERR] = Buffer.from( - outgoingResponse.err, - ); + const stringifiedError = + typeof outgoingResponse.err === 'object' + ? JSON.stringify(outgoingResponse.err) + : outgoingResponse.err; + outgoingMessage.headers[KafkaHeaders.NEST_ERR] = + Buffer.from(stringifiedError); } public assignCorrelationIdHeader( @@ -251,8 +289,27 @@ export class ServerKafka extends Server implements CustomTransportStrategy { outgoingMessage.partition = parseFloat(replyPartition); } + public async handleEvent( + pattern: string, + packet: ReadPacket, + context: KafkaContext, + ): Promise { + const handler = this.getHandlerByPattern(pattern); + if (!handler) { + return this.logger.error(NO_EVENT_HANDLER`${pattern}`); + } + const resultOrStream = await handler(packet.data, context); + if (isObservable(resultOrStream)) { + await lastValueFrom(resultOrStream); + } + } + protected initializeSerializer(options: KafkaOptions['options']) { this.serializer = (options && options.serializer) || new KafkaRequestSerializer(); } + + protected initializeDeserializer(options: KafkaOptions['options']) { + this.deserializer = options?.deserializer ?? new KafkaRequestDeserializer(); + } } diff --git a/packages/microservices/server/server-redis.ts b/packages/microservices/server/server-redis.ts index beaf02f8536..6e5530165aa 100644 --- a/packages/microservices/server/server-redis.ts +++ b/packages/microservices/server/server-redis.ts @@ -1,41 +1,37 @@ import { isUndefined } from '@nestjs/common/utils/shared.utils'; import { Observable } from 'rxjs'; import { - CONNECT_EVENT, ERROR_EVENT, MESSAGE_EVENT, NO_MESSAGE_HANDLER, - REDIS_DEFAULT_URL, + REDIS_DEFAULT_HOST, + REDIS_DEFAULT_PORT, } from '../constants'; import { RedisContext } from '../ctx-host'; import { Transport } from '../enums'; import { - ClientOpts, - RedisClient, - RetryStrategyOptions, -} from '../external/redis.interface'; -import { CustomTransportStrategy, IncomingRequest } from '../interfaces'; -import { RedisOptions } from '../interfaces/microservice-configuration.interface'; + CustomTransportStrategy, + IncomingRequest, + RedisOptions, +} from '../interfaces'; import { Server } from './server'; -let redisPackage: any = {}; +type Redis = any; + +let redisPackage = {} as any; export class ServerRedis extends Server implements CustomTransportStrategy { public readonly transportId = Transport.REDIS; - private readonly url: string; - private subClient: RedisClient; - private pubClient: RedisClient; + private subClient: Redis; + private pubClient: Redis; private isExplicitlyTerminated = false; constructor(private readonly options: RedisOptions['options']) { super(); - this.url = - this.getOptionsProp(options, 'url') || - (!this.getOptionsProp(options, 'host') && REDIS_DEFAULT_URL); - redisPackage = this.loadPackage('redis', ServerRedis.name, () => - require('redis'), + redisPackage = this.loadPackage('ioredis', ServerRedis.name, () => + require('ioredis'), ); this.initializeSerializer(options); @@ -51,6 +47,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy { this.handleError(this.pubClient); this.handleError(this.subClient); + this.start(callback); } catch (err) { callback(err); @@ -58,11 +55,15 @@ export class ServerRedis extends Server implements CustomTransportStrategy { } public start(callback?: () => void) { - this.bindEvents(this.subClient, this.pubClient); - this.subClient.on(CONNECT_EVENT, callback); + Promise.all([this.subClient.connect(), this.pubClient.connect()]) + .then(() => { + this.bindEvents(this.subClient, this.pubClient); + callback(); + }) + .catch(callback); } - public bindEvents(subClient: RedisClient, pubClient: RedisClient) { + public bindEvents(subClient: Redis, pubClient: Redis) { subClient.on(MESSAGE_EVENT, this.getMessageHandler(pubClient).bind(this)); const subscribePatterns = [...this.messageHandlers.keys()]; subscribePatterns.forEach(pattern => { @@ -79,14 +80,16 @@ export class ServerRedis extends Server implements CustomTransportStrategy { this.subClient && this.subClient.quit(); } - public createRedisClient(): RedisClient { - return redisPackage.createClient({ + public createRedisClient(): Redis { + return new redisPackage({ + port: REDIS_DEFAULT_PORT, + host: REDIS_DEFAULT_HOST, ...this.getClientOptions(), - url: this.url, + lazyConnect: true, }); } - public getMessageHandler(pub: RedisClient) { + public getMessageHandler(pub: Redis) { return async (channel: string, buffer: string | any) => this.handleMessage(channel, buffer, pub); } @@ -94,7 +97,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy { public async handleMessage( channel: string, buffer: string | any, - pub: RedisClient, + pub: Redis, ) { const rawMessage = this.parseMessage(buffer); const packet = await this.deserializer.deserialize(rawMessage, { channel }); @@ -125,7 +128,7 @@ export class ServerRedis extends Server implements CustomTransportStrategy { response$ && this.send(response$, publish); } - public getPublisher(pub: RedisClient, pattern: any, id: string) { + public getPublisher(pub: Redis, pattern: any, id: string) { return (response: any) => { Object.assign(response, { id }); const outgoingResponse = this.serializer.serialize(response); @@ -157,31 +160,25 @@ export class ServerRedis extends Server implements CustomTransportStrategy { stream.on(ERROR_EVENT, (err: any) => this.logger.error(err)); } - public getClientOptions(): Partial { - const retry_strategy = (options: RetryStrategyOptions) => - this.createRetryStrategy(options); + public getClientOptions(): Partial { + const retryStrategy = (times: number) => this.createRetryStrategy(times); return { ...(this.options || {}), - retry_strategy, + retryStrategy, }; } - public createRetryStrategy( - options: RetryStrategyOptions, - ): undefined | number | void { - if (options.error && (options.error as any).code === 'ECONNREFUSED') { - this.logger.error(`Error ECONNREFUSED: ${this.url}`); - } + public createRetryStrategy(times: number): undefined | number | void { if (this.isExplicitlyTerminated) { return undefined; } if ( !this.getOptionsProp(this.options, 'retryAttempts') || - options.attempt > this.getOptionsProp(this.options, 'retryAttempts') + times > this.getOptionsProp(this.options, 'retryAttempts') ) { - this.logger.error(`Retry time exhausted: ${this.url}`); - throw new Error('Retry time exhausted'); + this.logger.error(`Retry time exhausted`); + return; } return this.getOptionsProp(this.options, 'retryDelay') || 0; } diff --git a/packages/microservices/server/server.ts b/packages/microservices/server/server.ts index b36bcfa3a73..4e8ee9bf660 100644 --- a/packages/microservices/server/server.ts +++ b/packages/microservices/server/server.ts @@ -111,9 +111,7 @@ export abstract class Server { ): Promise { const handler = this.getHandlerByPattern(pattern); if (!handler) { - return this.logger.error( - `${NO_EVENT_HANDLER} Event pattern: ${JSON.stringify(pattern)}.`, - ); + return this.logger.error(NO_EVENT_HANDLER`${pattern}`); } const resultOrStream = await handler(packet.data, context); if (isObservable(resultOrStream)) { diff --git a/packages/microservices/test/client/client-redis.spec.ts b/packages/microservices/test/client/client-redis.spec.ts index 8045f90a509..b7fb3ec5e1a 100644 --- a/packages/microservices/test/client/client-redis.spec.ts +++ b/packages/microservices/test/client/client-redis.spec.ts @@ -1,9 +1,7 @@ import { expect } from 'chai'; -import { Subject } from 'rxjs'; import * as sinon from 'sinon'; import { ClientRedis } from '../../client/client-redis'; import { ERROR_EVENT } from '../../constants'; -import { Client } from '../../external/nats-client.interface'; describe('ClientRedis', () => { const test = 'test'; @@ -252,53 +250,39 @@ describe('ClientRedis', () => { }); }); describe('getClientOptions', () => { - it('should return options object with "retry_strategy" and call "createRetryStrategy"', () => { + it('should return options object with "retryStrategy" and call "createRetryStrategy"', () => { const createSpy = sinon.spy(client, 'createRetryStrategy'); - const { retry_strategy } = client.getClientOptions(new Subject()); + const { retryStrategy } = client.getClientOptions(); try { - retry_strategy({} as any); + retryStrategy({} as any); } catch {} expect(createSpy.called).to.be.true; }); }); describe('createRetryStrategy', () => { - const subject = new Subject(); describe('when is terminated', () => { it('should return undefined', () => { (client as any).isExplicitlyTerminated = true; - const result = client.createRetryStrategy({} as any, subject); + const result = client.createRetryStrategy(0); expect(result).to.be.undefined; }); }); describe('when "retryAttempts" does not exist', () => { - it('should return an error', () => { + it('should return undefined', () => { (client as any).isExplicitlyTerminated = false; (client as any).options.options = {}; (client as any).options.options.retryAttempts = undefined; - const result = client.createRetryStrategy({} as any, subject); - expect(result).to.be.instanceOf(Error); + const result = client.createRetryStrategy(1); + expect(result).to.be.undefined; }); }); describe('when "attempts" count is max', () => { - it('should return an error', () => { + it('should return undefined', () => { (client as any).isExplicitlyTerminated = false; (client as any).options.options = {}; (client as any).options.options.retryAttempts = 3; - const result = client.createRetryStrategy( - { attempt: 4 } as any, - subject, - ); - expect(result).to.be.instanceOf(Error); - }); - }); - describe('when ECONNREFUSED', () => { - it('should return error', () => { - (client as any).options.options = {}; - (client as any).options.options.retryAttempts = 10; - - const error = { code: 'ECONNREFUSED' }; - const result = client.createRetryStrategy({ error } as any, subject); - expect(result).to.be.instanceOf(Error); + const result = client.createRetryStrategy(4); + expect(result).to.be.undefined; }); }); describe('otherwise', () => { @@ -307,10 +291,7 @@ describe('ClientRedis', () => { (client as any).isExplicitlyTerminated = false; (client as any).options.retryAttempts = 3; (client as any).options.retryDelay = 3; - const result = client.createRetryStrategy( - { attempt: 2 } as any, - subject, - ); + const result = client.createRetryStrategy(2); expect(result).to.be.eql((client as any).options.retryDelay); }); }); diff --git a/packages/microservices/test/ctx-host/kafka.context.spec.ts b/packages/microservices/test/ctx-host/kafka.context.spec.ts index 9c0c1b3126c..5f350a63585 100644 --- a/packages/microservices/test/ctx-host/kafka.context.spec.ts +++ b/packages/microservices/test/ctx-host/kafka.context.spec.ts @@ -1,13 +1,15 @@ import { expect } from 'chai'; import { KafkaContext } from '../../ctx-host'; -import { KafkaMessage } from '../../external/kafka.interface'; +import { Consumer, KafkaMessage } from '../../external/kafka.interface'; describe('KafkaContext', () => { - const args = ['test', { test: true }]; + const args = ['test', { test: true }, undefined, { test: 'consumer' }]; let context: KafkaContext; beforeEach(() => { - context = new KafkaContext(args as [KafkaMessage, number, string]); + context = new KafkaContext( + args as [KafkaMessage, number, string, Consumer], + ); }); describe('getTopic', () => { it('should return topic', () => { @@ -24,4 +26,9 @@ describe('KafkaContext', () => { expect(context.getMessage()).to.be.eql(args[0]); }); }); + describe('getConsumer', () => { + it('should return consumer instance', () => { + expect(context.getConsumer()).to.deep.eq({ test: 'consumer' }); + }); + }); }); diff --git a/packages/microservices/test/decorators/event-pattern.decorator.spec.ts b/packages/microservices/test/decorators/event-pattern.decorator.spec.ts index edd238d0ada..dbfb076969f 100644 --- a/packages/microservices/test/decorators/event-pattern.decorator.spec.ts +++ b/packages/microservices/test/decorators/event-pattern.decorator.spec.ts @@ -9,14 +9,24 @@ import { EventPattern } from '../../decorators/event-pattern.decorator'; describe('@EventPattern', () => { const pattern = { role: 'test' }; + const patternSecond = { role: 'test2' }; + const patternThird = { role: 'test3' }; const extras = { param: 'value' }; class TestComponent { @EventPattern(pattern, undefined, extras) public static test() {} + + @EventPattern(patternSecond, undefined, extras) + @EventPattern(patternThird, undefined, extras) + public static testOnlyThird() {} + + @EventPattern([patternSecond, patternThird], undefined, extras) + public static testBoth() {} } it(`should enhance method with ${PATTERN_METADATA} metadata`, () => { const metadata = Reflect.getMetadata(PATTERN_METADATA, TestComponent.test); - expect(metadata).to.be.eql(pattern); + expect(metadata.length).to.equal(1); + expect(metadata[0]).to.be.eql(pattern); }); it(`should enhance method with ${PATTERN_EXTRAS_METADATA} metadata`, () => { const metadata = Reflect.getMetadata( @@ -25,6 +35,23 @@ describe('@EventPattern', () => { ); expect(metadata).to.be.deep.equal(extras); }); + it(`should enhance method with last ${PATTERN_METADATA} metadata`, () => { + const metadata = Reflect.getMetadata( + PATTERN_METADATA, + TestComponent.testOnlyThird, + ); + expect(metadata.length).to.equal(1); + expect(metadata[0]).to.be.eql(patternSecond); + }); + it(`should enhance method with both ${PATTERN_METADATA} metadata`, () => { + const metadata = Reflect.getMetadata( + PATTERN_METADATA, + TestComponent.testBoth, + ); + expect(metadata.length).to.equal(2); + expect(metadata[0]).to.be.eql(patternSecond); + expect(metadata[1]).to.be.eql(patternThird); + }); describe('decorator overloads', () => { class TestComponent1 { @@ -45,7 +72,7 @@ describe('@EventPattern', () => { } it(`should enhance method with ${PATTERN_METADATA} metadata`, () => { - const metadataArg = Reflect.getMetadata( + const [metadataArg] = Reflect.getMetadata( PATTERN_METADATA, TestComponent1.test, ); @@ -63,7 +90,7 @@ describe('@EventPattern', () => { }); it(`should enhance method with ${PATTERN_METADATA}, ${TRANSPORT_METADATA} metadata`, () => { - const metadataArg = Reflect.getMetadata( + const [metadataArg] = Reflect.getMetadata( PATTERN_METADATA, TestComponent2.test, ); @@ -81,7 +108,7 @@ describe('@EventPattern', () => { }); it(`should enhance method with ${PATTERN_METADATA}, ${PATTERN_EXTRAS_METADATA} metadata`, () => { - const metadataArg = Reflect.getMetadata( + const [metadataArg] = Reflect.getMetadata( PATTERN_METADATA, TestComponent3.test, ); @@ -100,7 +127,7 @@ describe('@EventPattern', () => { it(`should enhance method with ${PATTERN_METADATA}, ${TRANSPORT_METADATA} and \ ${PATTERN_EXTRAS_METADATA} metadata`, () => { - const metadataArg = Reflect.getMetadata( + const [metadataArg] = Reflect.getMetadata( PATTERN_METADATA, TestComponent4.test, ); diff --git a/packages/microservices/test/decorators/message-pattern.decorator.spec.ts b/packages/microservices/test/decorators/message-pattern.decorator.spec.ts index 5edabc21165..d8b62d2f55e 100644 --- a/packages/microservices/test/decorators/message-pattern.decorator.spec.ts +++ b/packages/microservices/test/decorators/message-pattern.decorator.spec.ts @@ -21,7 +21,10 @@ describe('@MessagePattern', () => { public static test() {} } it(`should enhance method with ${PATTERN_METADATA} metadata`, () => { - const metadata = Reflect.getMetadata(PATTERN_METADATA, TestComponent.test); + const [metadata] = Reflect.getMetadata( + PATTERN_METADATA, + TestComponent.test, + ); expect(metadata).to.be.eql(pattern); }); it(`should enhance method with ${PATTERN_EXTRAS_METADATA} metadata`, () => { @@ -51,7 +54,7 @@ describe('@MessagePattern', () => { } it(`should enhance method with ${PATTERN_METADATA} metadata`, () => { - const metadataArg = Reflect.getMetadata( + const [metadataArg] = Reflect.getMetadata( PATTERN_METADATA, TestComponent1.test, ); @@ -69,7 +72,7 @@ describe('@MessagePattern', () => { }); it(`should enhance method with ${PATTERN_METADATA}, ${TRANSPORT_METADATA} metadata`, () => { - const metadataArg = Reflect.getMetadata( + const [metadataArg] = Reflect.getMetadata( PATTERN_METADATA, TestComponent2.test, ); @@ -87,7 +90,7 @@ describe('@MessagePattern', () => { }); it(`should enhance method with ${PATTERN_METADATA}, ${PATTERN_EXTRAS_METADATA} metadata`, () => { - const metadataArg = Reflect.getMetadata( + const [metadataArg] = Reflect.getMetadata( PATTERN_METADATA, TestComponent3.test, ); @@ -106,7 +109,7 @@ describe('@MessagePattern', () => { it(`should enhance method with ${PATTERN_METADATA}, ${TRANSPORT_METADATA} and \ ${PATTERN_EXTRAS_METADATA} metadata`, () => { - const metadataArg = Reflect.getMetadata( + const [metadataArg] = Reflect.getMetadata( PATTERN_METADATA, TestComponent4.test, ); @@ -139,7 +142,7 @@ describe('@GrpcMethod', () => { it('should derive method and service name', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test); expect(metadata).to.be.eql({ service: TestService.name, rpc: 'Test', @@ -149,7 +152,7 @@ describe('@GrpcMethod', () => { it('should derive method', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test2); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test2); expect(metadata).to.be.eql({ service: 'TestService2', rpc: 'Test2', @@ -159,7 +162,7 @@ describe('@GrpcMethod', () => { it('should override both method and service', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test3); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test3); expect(metadata).to.be.eql({ service: 'TestService2', rpc: 'Test2', @@ -182,7 +185,7 @@ describe('@GrpcStreamMethod', () => { it('should derive method and service name', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test); expect(metadata).to.be.eql({ service: TestService.name, rpc: 'Test', @@ -192,7 +195,7 @@ describe('@GrpcStreamMethod', () => { it('should derive method', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test2); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test2); expect(metadata).to.be.eql({ service: 'TestService2', rpc: 'Test2', @@ -202,7 +205,7 @@ describe('@GrpcStreamMethod', () => { it('should override both method and service', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test3); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test3); expect(metadata).to.be.eql({ service: 'TestService2', rpc: 'Test2', @@ -225,7 +228,7 @@ describe('@GrpcStreamCall', () => { it('should derive method and service name', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test); expect(metadata).to.be.eql({ service: TestService.name, rpc: 'Test', @@ -235,7 +238,7 @@ describe('@GrpcStreamCall', () => { it('should derive method', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test2); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test2); expect(metadata).to.be.eql({ service: 'TestService2', rpc: 'Test2', @@ -245,7 +248,7 @@ describe('@GrpcStreamCall', () => { it('should override both method and service', () => { const svc = new TestService(); - const metadata = Reflect.getMetadata(PATTERN_METADATA, svc.test3); + const [metadata] = Reflect.getMetadata(PATTERN_METADATA, svc.test3); expect(metadata).to.be.eql({ service: 'TestService2', rpc: 'Test2', diff --git a/packages/microservices/test/listeners-controller.spec.ts b/packages/microservices/test/listeners-controller.spec.ts index f372050f018..83d5f43a7cc 100644 --- a/packages/microservices/test/listeners-controller.spec.ts +++ b/packages/microservices/test/listeners-controller.spec.ts @@ -74,8 +74,8 @@ describe('ListenersController', () => { describe('registerPatternHandlers', () => { const handlers = [ - { pattern: 'test', targetCallback: 'tt' }, - { pattern: 'test2', targetCallback: '2', isEventHandler: true }, + { patterns: ['test'], targetCallback: 'tt' }, + { patterns: ['test2'], targetCallback: '2', isEventHandler: true }, ]; beforeEach(() => { @@ -89,7 +89,7 @@ describe('ListenersController', () => { it(`should call "addHandler" method of server for each pattern handler with same transport`, () => { const serverHandlers = [ { - pattern: { cmd: 'test' }, + patterns: [{ cmd: 'test' }], targetCallback: 'tt', transport: Transport.TCP, }, @@ -101,8 +101,12 @@ describe('ListenersController', () => { }); it(`should call "addHandler" method of server without transportID for each pattern handler with any transport value`, () => { const serverHandlers = [ - { pattern: { cmd: 'test' }, targetCallback: 'tt' }, - { pattern: 'test2', targetCallback: '2', transport: Transport.KAFKA }, + { patterns: [{ cmd: 'test' }], targetCallback: 'tt' }, + { + patterns: ['test2'], + targetCallback: '2', + transport: Transport.KAFKA, + }, ]; explorer.expects('explore').returns(serverHandlers); instance.registerPatternHandlers(new InstanceWrapper(), server, ''); @@ -110,10 +114,14 @@ describe('ListenersController', () => { }); it(`should call "addHandler" method of server with transportID for each pattern handler with self transport and without transport`, () => { const serverHandlers = [ - { pattern: 'test', targetCallback: 'tt' }, - { pattern: 'test2', targetCallback: '2', transport: Transport.KAFKA }, + { patterns: ['test'], targetCallback: 'tt' }, + { + patterns: ['test2'], + targetCallback: '2', + transport: Transport.KAFKA, + }, { - pattern: { cmd: 'test3' }, + patterns: [{ cmd: 'test3' }], targetCallback: '3', transport: Transport.TCP, }, @@ -130,11 +138,15 @@ describe('ListenersController', () => { it(`should call "addHandler" method of server with custom transportID for pattern handler with the same custom token`, () => { const serverHandlers = [ { - pattern: { cmd: 'test' }, + patterns: [{ cmd: 'test' }], targetCallback: 'tt', transport: customTransport, }, - { pattern: 'test2', targetCallback: '2', transport: Transport.KAFKA }, + { + patterns: ['test2'], + targetCallback: '2', + transport: Transport.KAFKA, + }, ]; explorer.expects('explore').returns(serverHandlers); @@ -143,7 +155,11 @@ describe('ListenersController', () => { }); it(`should call "addHandler" method of server with extras data`, () => { const serverHandlers = [ - { pattern: 'test', targetCallback: 'tt', extras: { param: 'value' } }, + { + patterns: ['test'], + targetCallback: 'tt', + extras: { param: 'value' }, + }, ]; explorer.expects('explore').returns(serverHandlers); instance.registerPatternHandlers(new InstanceWrapper(), serverTCP, ''); @@ -193,7 +209,7 @@ describe('ListenersController', () => { const module = { controllers: new Map(), } as any; - const pattern = {}; + const patterns = [{}]; const wrapper = new InstanceWrapper({ instance: { [methodKey]: {} } }); it('should pass all arguments to the proxy chain', async () => { @@ -202,7 +218,7 @@ describe('ListenersController', () => { .callsFake(() => Promise.resolve({})); const handler = instance.createRequestScopedHandler( wrapper, - pattern, + patterns, module, moduleKey, methodKey, @@ -221,7 +237,7 @@ describe('ListenersController', () => { const module = { controllers: new Map(), } as any; - const pattern = {}; + const patterns = [{}]; const wrapper = new InstanceWrapper({ instance: { [methodKey]: {} } }); it('should delegete error to exception filters', async () => { @@ -230,7 +246,7 @@ describe('ListenersController', () => { }); const handler = instance.createRequestScopedHandler( wrapper, - pattern, + patterns, module, moduleKey, methodKey, diff --git a/packages/microservices/test/listeners-metadata-explorer.spec.ts b/packages/microservices/test/listeners-metadata-explorer.spec.ts index 985365f9a24..2bb04a763e8 100644 --- a/packages/microservices/test/listeners-metadata-explorer.spec.ts +++ b/packages/microservices/test/listeners-metadata-explorer.spec.ts @@ -2,15 +2,20 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { MetadataScanner } from '../../core/metadata-scanner'; import { Client } from '../decorators/client.decorator'; +import { EventPattern } from '../decorators/event-pattern.decorator'; import { MessagePattern } from '../decorators/message-pattern.decorator'; import { Transport } from '../enums/transport.enum'; import { ListenerMetadataExplorer } from '../listener-metadata-explorer'; describe('ListenerMetadataExplorer', () => { - const pattern = { pattern: 'test' }; - const secPattern = { role: '2', cmd: 'm' }; + const msgPattern = { pattern: 'testMsg' }; + const firstMultipleMsgPattern = { pattern: 'testMultipleMsg1' }; + const secondMultipleMsgPattern = { pattern: 'testMultipleMsg2' }; const clientMetadata = {}; const clientSecMetadata = { transport: Transport.REDIS }; + const evtPattern = { role: 'testEvt' }; + const firstMultipleEvtPattern = { role: 'testMultipleEvt1' }; + const secondMultipleEvtPattern = { role: 'testMultipleEvt2' }; class Test { @Client(clientMetadata as any) @@ -25,11 +30,17 @@ describe('ListenerMetadataExplorer', () => { constructor() {} - @MessagePattern(pattern) - public test() {} + @MessagePattern(msgPattern) + public testMessage() {} - @MessagePattern(secPattern) - public testSec() {} + @MessagePattern([firstMultipleMsgPattern, secondMultipleMsgPattern]) + public testMultipleMessage() {} + + @EventPattern(evtPattern) + public testEvent() {} + + @EventPattern([firstMultipleEvtPattern, secondMultipleEvtPattern]) + public testMultipleEvent() {} public noPattern() {} } @@ -66,20 +77,77 @@ describe('ListenerMetadataExplorer', () => { ); expect(metadata).to.eq(undefined); }); - it(`should return pattern properties when "handlerType" metadata is not undefined`, () => { - const metadata = instance.exploreMethodMetadata( - Object.getPrototypeOf(test), - 'test', - ); - expect(metadata).to.have.keys([ - 'isEventHandler', - 'methodKey', - 'targetCallback', - 'pattern', - 'transport', - 'extras', - ]); - expect(metadata.pattern).to.eql(pattern); + + describe('@MessagePattern', () => { + it(`should return pattern properties when "handlerType" metadata is not undefined`, () => { + const metadata = instance.exploreMethodMetadata( + Object.getPrototypeOf(test), + 'testMessage', + ); + expect(metadata).to.have.keys([ + 'isEventHandler', + 'methodKey', + 'targetCallback', + 'patterns', + 'transport', + 'extras', + ]); + expect(metadata.patterns.length).to.eql(1); + expect(metadata.patterns[0]).to.eql(msgPattern); + }); + it(`should return multiple patterns when more than one is declared`, () => { + const metadata = instance.exploreMethodMetadata( + Object.getPrototypeOf(test), + 'testMultipleMessage', + ); + expect(metadata).to.have.keys([ + 'isEventHandler', + 'methodKey', + 'targetCallback', + 'patterns', + 'transport', + 'extras', + ]); + expect(metadata.patterns.length).to.eql(2); + expect(metadata.patterns[0]).to.eql(firstMultipleMsgPattern); + expect(metadata.patterns[1]).to.eql(secondMultipleMsgPattern); + }); + }); + + describe('@EventPattern', () => { + it(`should return pattern properties when "handlerType" metadata is not undefined`, () => { + const metadata = instance.exploreMethodMetadata( + Object.getPrototypeOf(test), + 'testEvent', + ); + expect(metadata).to.have.keys([ + 'isEventHandler', + 'methodKey', + 'targetCallback', + 'patterns', + 'transport', + 'extras', + ]); + expect(metadata.patterns.length).to.eql(1); + expect(metadata.patterns[0]).to.eql(evtPattern); + }); + it(`should return multiple patterns when more than one is declared`, () => { + const metadata = instance.exploreMethodMetadata( + Object.getPrototypeOf(test), + 'testMultipleEvent', + ); + expect(metadata).to.have.keys([ + 'isEventHandler', + 'methodKey', + 'targetCallback', + 'patterns', + 'transport', + 'extras', + ]); + expect(metadata.patterns.length).to.eql(2); + expect(metadata.patterns[0]).to.eql(firstMultipleEvtPattern); + expect(metadata.patterns[1]).to.eql(secondMultipleEvtPattern); + }); }); }); describe('scanForClientHooks', () => { diff --git a/packages/microservices/test/server/server-kafka.spec.ts b/packages/microservices/test/server/server-kafka.spec.ts index f4ab1eab0e1..da129d56d00 100644 --- a/packages/microservices/test/server/server-kafka.spec.ts +++ b/packages/microservices/test/server/server-kafka.spec.ts @@ -1,5 +1,5 @@ import { Logger } from '@nestjs/common'; -import { expect } from 'chai'; +import { AssertionError, expect } from 'chai'; import * as sinon from 'sinon'; import { NO_MESSAGE_HANDLER } from '../../constants'; import { KafkaHeaders } from '../../enums'; @@ -277,6 +277,7 @@ describe('ServerKafka', () => { sinon.stub(server, 'getPublisher').callsFake(() => getPublisherSpy); }); + it('should call "handleEvent" if correlation identifier is not present', async () => { const handleEventSpy = sinon.spy(server, 'handleEvent'); await server.handleMessage(eventPayload); @@ -289,6 +290,42 @@ describe('ServerKafka', () => { expect(handleEventSpy.called).to.be.true; }); + it('should call event handler when "handleEvent" is called', async () => { + const messageHandler = sinon.mock(); + const context = { test: true } as any; + const messageData = 'some data'; + sinon.stub(server, 'getHandlerByPattern').callsFake(() => messageHandler); + + await server.handleEvent( + topic, + { data: messageData, pattern: topic }, + context, + ); + expect(messageHandler.calledWith(messageData, context)).to.be.true; + }); + + it('should not catch error thrown by event handler as part of "handleEvent"', async () => { + const error = new Error('handler error'); + const messageHandler = sinon.mock().throwsException(error); + sinon.stub(server, 'getHandlerByPattern').callsFake(() => messageHandler); + + try { + await server.handleEvent( + topic, + { data: 'some data', pattern: topic }, + {} as any, + ); + + // code should not be executed + expect(true).to.be.false; + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + expect(e).to.be.eq(error); + } + }); + it('should call "handleEvent" if correlation identifier and reply topic are present but the handler is of type eventHandler', async () => { const handler = sinon.spy(); (handler as any).isEventHandler = true; @@ -320,6 +357,7 @@ describe('ServerKafka', () => { }), ).to.be.true; }); + it(`should call handler with expected arguments`, async () => { const handler = sinon.spy(); (server as any).messageHandlers = objectToMap({ diff --git a/packages/microservices/test/server/server-redis.spec.ts b/packages/microservices/test/server/server-redis.spec.ts index fc9650c8764..2f0602f909a 100644 --- a/packages/microservices/test/server/server-redis.spec.ts +++ b/packages/microservices/test/server/server-redis.spec.ts @@ -15,13 +15,17 @@ describe('ServerRedis', () => { }); describe('listen', () => { let onSpy: sinon.SinonSpy; + let connectSpy: sinon.SinonSpy; let client: any; let callbackSpy: sinon.SinonSpy; beforeEach(() => { onSpy = sinon.spy(); + connectSpy = sinon.spy(); + client = { on: onSpy, + connect: connectSpy, }; sinon.stub(server, 'createRedisClient').callsFake(() => client); @@ -31,13 +35,9 @@ describe('ServerRedis', () => { server.listen(callbackSpy); expect(onSpy.getCall(0).args[0]).to.be.equal('error'); }); - it('should bind "connect" event to handler', () => { - server.listen(callbackSpy); - expect(onSpy.getCall(3).args[0]).to.be.equal('connect'); - }); - it('should bind "message" event to handler', () => { + it('should call "RedisClient#connect()"', () => { server.listen(callbackSpy); - expect(onSpy.getCall(2).args[0]).to.be.equal('message'); + expect(connectSpy.called).to.be.true; }); describe('when "start" throws an exception', () => { it('should call callback with a thrown error as an argument', () => { @@ -191,11 +191,11 @@ describe('ServerRedis', () => { }); }); describe('getClientOptions', () => { - it('should return options object with "retry_strategy" and call "createRetryStrategy"', () => { + it('should return options object with "retryStrategy" and call "createRetryStrategy"', () => { const createSpy = sinon.spy(server, 'createRetryStrategy'); - const { retry_strategy } = server.getClientOptions(); + const { retryStrategy } = server.getClientOptions(); try { - retry_strategy({} as any); + retryStrategy(0); } catch {} expect(createSpy.called).to.be.true; }); @@ -204,37 +204,24 @@ describe('ServerRedis', () => { describe('when is terminated', () => { it('should return undefined', () => { (server as any).isExplicitlyTerminated = true; - const result = server.createRetryStrategy({} as any); + const result = server.createRetryStrategy(0); expect(result).to.be.undefined; }); }); describe('when "retryAttempts" does not exist', () => { - it('should throw an exception', () => { + it('should return undefined', () => { (server as any).options.options = {}; (server as any).options.options.retryAttempts = undefined; - expect(() => server.createRetryStrategy({} as any)).to.throw(Error); + expect(server.createRetryStrategy(4)).to.be.undefined; }); }); describe('when "attempts" count is max', () => { - it('should throw an exception', () => { + it('should return undefined', () => { (server as any).options.options = {}; (server as any).options.options.retryAttempts = 3; - expect(() => - server.createRetryStrategy({ attempt: 4 } as any), - ).to.throw(Error); - }); - }); - describe('when ECONNREFUSED', () => { - it('should call logger', () => { - const loggerErrorSpy = sinon.spy((server as any).logger, 'error'); - try { - server.createRetryStrategy({ - error: { code: 'ECONNREFUSED' }, - } as any); - } catch {} - expect(loggerErrorSpy.called).to.be.true; + expect(server.createRetryStrategy(4)).to.be.undefined; }); }); describe('otherwise', () => { @@ -243,7 +230,7 @@ describe('ServerRedis', () => { (server as any).isExplicitlyTerminated = false; (server as any).options.retryAttempts = 3; (server as any).options.retryDelay = 3; - const result = server.createRetryStrategy({ attempt: 2 } as any); + const result = server.createRetryStrategy(2); expect(result).to.be.eql((server as any).options.retryDelay); }); }); diff --git a/packages/platform-express/adapters/express-adapter.ts b/packages/platform-express/adapters/express-adapter.ts index 609e51459ba..9bb80c5b1ca 100644 --- a/packages/platform-express/adapters/express-adapter.ts +++ b/packages/platform-express/adapters/express-adapter.ts @@ -4,12 +4,10 @@ import { RequestMethod, StreamableFile, VersioningType, -} from '@nestjs/common'; -import { VersioningOptions, - VersionValue, VERSION_NEUTRAL, -} from '@nestjs/common/interfaces'; +} from '@nestjs/common'; +import { VersionValue } from '@nestjs/common/interfaces'; import { CorsOptions, CorsOptionsDelegate, @@ -87,6 +85,10 @@ export class ExpressAdapter extends AbstractHttpAdapter { return response.status(statusCode); } + public end(response: any, message?: string) { + return response.end(message); + } + public render(response: any, view: string, options: any) { return response.render(view, options); } @@ -103,6 +105,10 @@ export class ExpressAdapter extends AbstractHttpAdapter { return this.use(handler); } + public isHeadersSent(response: any): boolean { + return response.headersSent; + } + public setHeader(response: any, name: string, value: string) { return response.set(name, value); } diff --git a/packages/platform-fastify/adapters/fastify-adapter.ts b/packages/platform-fastify/adapters/fastify-adapter.ts index 3113a4a29a0..9ef3e2d5580 100644 --- a/packages/platform-fastify/adapters/fastify-adapter.ts +++ b/packages/platform-fastify/adapters/fastify-adapter.ts @@ -6,8 +6,9 @@ import { StreamableFile, VersioningOptions, VersioningType, + VERSION_NEUTRAL, } from '@nestjs/common'; -import { VersionValue, VERSION_NEUTRAL } from '@nestjs/common/interfaces'; +import { VersionValue } from '@nestjs/common/interfaces'; import { CorsOptions, CorsOptionsDelegate, @@ -68,7 +69,11 @@ type FastifyHttpsOptions< https: https.ServerOptions; }; -type VersionedRoute = Function & { +type VersionedRoute = (( + req: TRequest, + res: TResponse, + next: Function, +) => Function) & { version: VersionValue; versioningOptions: VersioningOptions; }; @@ -218,8 +223,14 @@ export class FastifyAdapter< hostname: string, callback?: () => void, ): void; - public listen(port: string | number, ...args: any[]): Promise { - return this.instance.listen(port, ...args); + public listen(port: string | number, ...args: any[]): void { + const isFirstArgTypeofFunction = typeof args[0] === 'function'; + const callback = isFirstArgTypeofFunction ? args[0] : args[1]; + const options = { + port: +port, + host: isFirstArgTypeofFunction ? undefined : args[0], + }; + return this.instance.listen(options, callback); } public get(...args: any[]) { @@ -254,11 +265,11 @@ export class FastifyAdapter< handler: Function, version: VersionValue, versioningOptions: VersioningOptions, - ) { + ): VersionedRoute { if (!this.versioningOptions) { this.versioningOptions = versioningOptions; } - const versionedRoute = handler as VersionedRoute; + const versionedRoute = handler as VersionedRoute; versionedRoute.version = version; return versionedRoute; } @@ -320,6 +331,10 @@ export class FastifyAdapter< return (response as TReply).code(statusCode); } + public end(response: TReply, message?: string) { + response.raw.end(message); + } + public render( response: TReply & { view: Function }, view: string, @@ -382,8 +397,8 @@ export class FastifyAdapter< public useStaticAssets(options: FastifyStaticOptions) { return this.register( - loadPackage('fastify-static', 'FastifyAdapter.useStaticAssets()', () => - require('fastify-static'), + loadPackage('@fastify/static', 'FastifyAdapter.useStaticAssets()', () => + require('@fastify/static'), ), options, ); @@ -404,6 +419,10 @@ export class FastifyAdapter< ); } + public isHeadersSent(response: TReply): boolean { + return response.sent; + } + public setHeader(response: TReply, name: string, value: string) { return response.header(name, value); } @@ -423,14 +442,14 @@ export class FastifyAdapter< } public enableCors(options: CorsOptions | CorsOptionsDelegate) { - this.register(import('fastify-cors'), options); + this.register(import('@fastify/cors'), options); } public registerParserMiddleware(prefix?: string, rawBody?: boolean) { if (this._isParserRegistered) { return; } - this.register(import('fastify-formbody')); + this.register(import('@fastify/formbody')); if (rawBody) { this.registerContentParserWithRawBody(); @@ -453,7 +472,7 @@ export class FastifyAdapter< // Fallback to "(.*)" to support plugins like GraphQL normalizedPath = normalizedPath === '/(.*)' ? '(.*)' : normalizedPath; - // The following type assertion is valid as we use import('middie') rather than require('middie') + // The following type assertion is valid as we use import('@fastify/middie') rather than require('@fastify/middie') // ref https://github.com/fastify/middie/pull/55 this.instance.use( normalizedPath, @@ -509,7 +528,7 @@ export class FastifyAdapter< private async registerMiddie() { this.isMiddieRegistered = true; - await this.register(import('middie')); + await this.register(import('@fastify/middie')); } private getRequestOriginalUrl(rawRequest: TRawRequest) { diff --git a/packages/platform-fastify/interfaces/external/fastify-static-options.interface.ts b/packages/platform-fastify/interfaces/external/fastify-static-options.interface.ts index 9f591764a29..a0ccb1f22ea 100644 --- a/packages/platform-fastify/interfaces/external/fastify-static-options.interface.ts +++ b/packages/platform-fastify/interfaces/external/fastify-static-options.interface.ts @@ -2,15 +2,28 @@ * "fastify-static" interfaces * @see https://github.com/fastify/fastify-static/blob/master/index.d.ts */ +import { Stats } from 'fs'; + +interface ExtendedInformation { + fileCount: number; + totalFileCount: number; + folderCount: number; + totalFolderCount: number; + totalSize: number; + lastModified: number; +} interface ListDir { href: string; name: string; + stats: Stats; + extendedInfo?: ExtendedInformation; } interface ListFile { href: string; name: string; + stats: Stats; } interface ListRender { @@ -21,6 +34,8 @@ interface ListOptions { format: 'json' | 'html'; names: string[]; render: ListRender; + extendedFolderInfo?: boolean; + jsonFormat?: 'names' | 'extended'; } // Passed on to `send` @@ -48,6 +63,11 @@ export interface FastifyStaticOptions extends SendOptions { wildcard?: boolean; list?: boolean | ListOptions; allowedPath?: (pathName: string, root?: string) => boolean; + /** + * @description + * Opt-in to looking for pre-compressed files + */ + preCompressed?: boolean; // Passed on to `send` acceptRanges?: boolean; diff --git a/packages/platform-fastify/interfaces/external/point-of-view-options.interface.ts b/packages/platform-fastify/interfaces/external/point-of-view-options.interface.ts index 4d5f918c6c7..f7ce1adebd9 100644 --- a/packages/platform-fastify/interfaces/external/point-of-view-options.interface.ts +++ b/packages/platform-fastify/interfaces/external/point-of-view-options.interface.ts @@ -9,7 +9,6 @@ export interface PointOfViewOptions { nunjucks?: any; pug?: any; handlebars?: any; - marko?: any; mustache?: any; 'art-template'?: any; twig?: any; diff --git a/packages/platform-fastify/interfaces/nest-fastify-application.interface.ts b/packages/platform-fastify/interfaces/nest-fastify-application.interface.ts index f275385ba56..71908f43a6f 100644 --- a/packages/platform-fastify/interfaces/nest-fastify-application.interface.ts +++ b/packages/platform-fastify/interfaces/nest-fastify-application.interface.ts @@ -16,7 +16,7 @@ import { FastifyStaticOptions, PointOfViewOptions } from './external'; export interface NestFastifyApplication extends INestApplication { /** * A wrapper function around native `fastify.register()` method. - * Example `app.register(require('fastify-formbody')) + * Example `app.register(require('@fastify/formbody')) * @returns {Promise} */ register( diff --git a/packages/platform-fastify/package.json b/packages/platform-fastify/package.json index fd4c7ce9fe6..28432ef6149 100644 --- a/packages/platform-fastify/package.json +++ b/packages/platform-fastify/package.json @@ -17,16 +17,26 @@ "access": "public" }, "dependencies": { - "fastify": "3.29.0", - "fastify-cors": "6.1.0", - "fastify-formbody": "5.3.0", + "@fastify/cors": "8.0.0", + "@fastify/formbody": "7.0.1", + "@fastify/middie": "8.0.0", + "fastify": "4.0.2", "light-my-request": "5.0.0", - "middie": "6.1.0", "path-to-regexp": "3.2.0", "tslib": "2.4.0" }, "peerDependencies": { "@nestjs/common": "^8.0.0", - "@nestjs/core": "^8.0.0" + "@nestjs/core": "^8.0.0", + "@fastify/static": "^5.0.0", + "point-of-view": "^5.0.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "point-of-view": { + "optional": true + } } } diff --git a/packages/testing/package-lock.json b/packages/testing/package-lock.json index c1a1e3215ab..b26532f4c65 100644 --- a/packages/testing/package-lock.json +++ b/packages/testing/package-lock.json @@ -1,25 +1,25 @@ { - "name": "testing", - "version": "8.4.5", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "dependencies": { - "tslib": "2.4.0" - } - }, - "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - }, - "dependencies": { - "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" - } - } + "name": "testing", + "version": "8.4.5", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "tslib": "2.4.0" + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + }, + "dependencies": { + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + } + } } diff --git a/packages/testing/testing-module.ts b/packages/testing/testing-module.ts index e107acc52bc..ec632c4535c 100644 --- a/packages/testing/testing-module.ts +++ b/packages/testing/testing-module.ts @@ -29,20 +29,43 @@ export class TestingModule extends NestApplicationContext { super(container, scope, contextModule); } + private isHttpServer( + serverOrOptions: + | HttpServer + | AbstractHttpAdapter + | NestApplicationOptions + | undefined, + ): serverOrOptions is HttpServer | AbstractHttpAdapter { + return !!(serverOrOptions && (serverOrOptions as HttpServer).patch); + } + + public createNestApplication( + httpAdapter: HttpServer | AbstractHttpAdapter, + options?: NestApplicationOptions, + ): T; public createNestApplication( - httpAdapter?: HttpServer | AbstractHttpAdapter, + options?: NestApplicationOptions, + ): T; + public createNestApplication( + serverOrOptions: + | HttpServer + | AbstractHttpAdapter + | NestApplicationOptions + | undefined, options?: NestApplicationOptions, ): T { - httpAdapter = httpAdapter || this.createHttpAdapter(); + const [httpAdapter, appOptions] = this.isHttpServer(serverOrOptions) + ? [serverOrOptions, options] + : [this.createHttpAdapter(), serverOrOptions]; - this.applyLogger(options); + this.applyLogger(appOptions); this.container.setHttpAdapter(httpAdapter); const instance = new NestApplication( this.container, httpAdapter, this.applicationConfig, - options, + appOptions, ); return this.createAdapterProxy(instance, httpAdapter); } diff --git a/packages/websockets/test/web-sockets-controller.spec.ts b/packages/websockets/test/web-sockets-controller.spec.ts index 68503abb0e5..659afff09fe 100644 --- a/packages/websockets/test/web-sockets-controller.spec.ts +++ b/packages/websockets/test/web-sockets-controller.spec.ts @@ -345,7 +345,7 @@ describe('WebSocketsController', () => { Promise.resolve(Promise.resolve(value)), ), ), - ).to.be.eq(100); + ).to.be.eq(value); }); }); @@ -356,18 +356,29 @@ describe('WebSocketsController', () => { await lastValueFrom( await instance.pickResult(Promise.resolve(of(value))), ), - ).to.be.eq(100); + ).to.be.eq(value); }); }); - describe('is a value', () => { + describe('is an object that has the method `subscribe`', () => { + it('should return Promise', async () => { + const value = { subscribe() {} }; + expect( + await lastValueFrom( + await instance.pickResult(Promise.resolve(value)), + ), + ).to.equal(value); + }); + }); + + describe('is an ordinary value', () => { it('should return Promise', async () => { const value = 100; expect( await lastValueFrom( await instance.pickResult(Promise.resolve(value)), ), - ).to.be.eq(100); + ).to.be.eq(value); }); }); }); diff --git a/packages/websockets/web-sockets-controller.ts b/packages/websockets/web-sockets-controller.ts index 75e790c8277..d01811e179b 100644 --- a/packages/websockets/web-sockets-controller.ts +++ b/packages/websockets/web-sockets-controller.ts @@ -1,9 +1,14 @@ import { Type } from '@nestjs/common/interfaces/type.interface'; import { Logger } from '@nestjs/common/services/logger.service'; -import { isFunction } from '@nestjs/common/utils/shared.utils'; import { ApplicationConfig } from '@nestjs/core/application-config'; import { MetadataScanner } from '@nestjs/core/metadata-scanner'; -import { from as fromPromise, Observable, of, Subject } from 'rxjs'; +import { + from as fromPromise, + Observable, + isObservable, + of, + Subject, +} from 'rxjs'; import { distinctUntilChanged, mergeAll } from 'rxjs/operators'; import { GATEWAY_OPTIONS, PORT_METADATA } from './constants'; import { WsContextCreator } from './context/ws-context-creator'; @@ -158,7 +163,7 @@ export class WebSocketsController { deferredResult: Promise, ): Promise> { const result = await deferredResult; - if (result && isFunction(result.subscribe)) { + if (isObservable(result)) { return result; } if (result instanceof Promise) { diff --git a/sample/17-mvc-fastify/package-lock.json b/sample/17-mvc-fastify/package-lock.json index 7193960fe19..158c7b4364f 100644 --- a/sample/17-mvc-fastify/package-lock.json +++ b/sample/17-mvc-fastify/package-lock.json @@ -845,6 +845,30 @@ "ajv": "^6.12.6" } }, + "@fastify/static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-5.0.0.tgz", + "integrity": "sha512-GGltJkO0idXa7yCZ0PfdTZ6qokWDX/vigCvmRpjOU2A3jc93c9p+oHDvHmwHK60hwWoBEGqHjGofVyn3H1CjZg==", + "requires": { + "content-disposition": "^0.5.3", + "encoding-negotiator": "^2.0.1", + "fastify-plugin": "^3.0.0", + "glob": "^7.1.4", + "p-limit": "^3.1.0", + "readable-stream": "^3.4.0", + "send": "^0.17.1" + }, + "dependencies": { + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "requires": { + "yocto-queue": "^0.1.0" + } + } + } + }, "@humanwhocodes/config-array": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", @@ -4255,30 +4279,6 @@ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==" }, - "fastify-static": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/fastify-static/-/fastify-static-4.5.0.tgz", - "integrity": "sha512-Q7Tgl55AjsmBwiO4hKYib2BUCt+XTWLJ6Xp8YPPHU3EsrKNpevJ4cz8pjf1Ey1QhHw9O8Y2FDKdu+IC74oHvqw==", - "requires": { - "content-disposition": "^0.5.3", - "encoding-negotiator": "^2.0.1", - "fastify-plugin": "^3.0.0", - "glob": "^7.1.4", - "p-limit": "^3.1.0", - "readable-stream": "^3.4.0", - "send": "^0.17.1" - }, - "dependencies": { - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "requires": { - "yocto-queue": "^0.1.0" - } - } - } - }, "fastify-warning": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", diff --git a/sample/17-mvc-fastify/package.json b/sample/17-mvc-fastify/package.json index e2c4eb63f30..03551e68baf 100644 --- a/sample/17-mvc-fastify/package.json +++ b/sample/17-mvc-fastify/package.json @@ -22,7 +22,7 @@ "@nestjs/common": "8.2.3", "@nestjs/core": "8.2.3", "@nestjs/platform-fastify": "8.2.3", - "fastify-static": "4.5.0", + "@fastify/static": "5.0.0", "handlebars": "4.7.7", "point-of-view": "4.15.3", "reflect-metadata": "0.1.13", diff --git a/sample/29-file-upload/e2e/app/app.e2e-spec.ts b/sample/29-file-upload/e2e/app/app.e2e-spec.ts index 826b5ba8d5f..4107251ff62 100644 --- a/sample/29-file-upload/e2e/app/app.e2e-spec.ts +++ b/sample/29-file-upload/e2e/app/app.e2e-spec.ts @@ -1,7 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { readFileSync } from 'fs'; -import { join } from 'path'; import * as request from 'supertest'; import { AppModule } from '../../src/app.module'; @@ -30,6 +29,28 @@ describe('E2E FileTest', () => { }); }); + it('should allow for file uploads that pass validation', async () => { + return request(app.getHttpServer()) + .post('/file/pass-validation') + .attach('file', './package.json') + .field('name', 'test') + .expect(201) + .expect({ + body: { + name: 'test', + }, + file: readFileSync('./package.json').toString(), + }); + }); + + it('should throw for file uploads that do not pass validation', async () => { + return request(app.getHttpServer()) + .post('/file/fail-validation') + .attach('file', './package.json') + .field('name', 'test') + .expect(400); + }); + afterAll(async () => { await app.close(); }); diff --git a/sample/29-file-upload/src/app.controller.ts b/sample/29-file-upload/src/app.controller.ts index dc59cd260e9..39d3af1fb75 100644 --- a/sample/29-file-upload/src/app.controller.ts +++ b/sample/29-file-upload/src/app.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, + ParseFilePipeBuilder, Post, UploadedFile, UseInterceptors, @@ -31,4 +32,42 @@ export class AppController { file: file.buffer.toString(), }; } + + @UseInterceptors(FileInterceptor('file')) + @Post('file/pass-validation') + uploadFileAndPassValidation( + @Body() body: SampleDto, + @UploadedFile( + new ParseFilePipeBuilder() + .addFileTypeValidator({ + fileType: 'json', + }) + .build(), + ) + file: Express.Multer.File, + ) { + return { + body, + file: file.buffer.toString(), + }; + } + + @UseInterceptors(FileInterceptor('file')) + @Post('file/fail-validation') + uploadFileAndFailValidation( + @Body() body: SampleDto, + @UploadedFile( + new ParseFilePipeBuilder() + .addFileTypeValidator({ + fileType: 'jpg', + }) + .build(), + ) + file: Express.Multer.File, + ) { + return { + body, + file: file.buffer.toString(), + }; + } }