diff --git a/examples/kitchen-sink/src/rabbit-example/rabbit-example.module.ts b/examples/kitchen-sink/src/rabbit-example/rabbit-example.module.ts index 9d0c239be..107c23931 100644 --- a/examples/kitchen-sink/src/rabbit-example/rabbit-example.module.ts +++ b/examples/kitchen-sink/src/rabbit-example/rabbit-example.module.ts @@ -5,18 +5,20 @@ import { MessagingService } from './messaging/messaging.service'; @Module({ imports: [ - RabbitMQModule.build({ - exchanges: [ - { - name: 'exchange1', - type: 'topic', - }, - { - name: 'exchange2', - type: 'fanout', - }, - ], - uri: 'amqp://rabbitmq:rabbitmq@localhost:5672', + RabbitMQModule.forRootAsync({ + useFactory: () => ({ + exchanges: [ + { + name: 'exchange1', + type: 'topic', + }, + { + name: 'exchange2', + type: 'fanout', + }, + ], + uri: 'amqp://rabbitmq:rabbitmq@localhost:5672', + }), }), RabbitExampleModule, ], diff --git a/integration/rabbitmq/e2e/configuration.e2e-spec.ts b/integration/rabbitmq/e2e/configuration.e2e-spec.ts new file mode 100644 index 000000000..3569354b1 --- /dev/null +++ b/integration/rabbitmq/e2e/configuration.e2e-spec.ts @@ -0,0 +1,112 @@ +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RabbitMQModule, RabbitMQConfig } from '@nestjs-plus/rabbitmq'; +import * as amqplib from 'amqplib'; + +const uri = 'amqp://rabbitmq:rabbitmq@localhost:5672'; + +class RabbitConfig { + createOptions(): RabbitMQConfig { + return { + uri, + }; + } +} + +describe('Module Configuration', () => { + let app: TestingModule; + + afterEach(() => jest.clearAllMocks()); + + describe('forRoot', () => { + it('should configure RabbitMQ', async () => { + const spy = jest.spyOn(amqplib, 'connect'); + + app = await Test.createTestingModule({ + imports: [ + RabbitMQModule.forRoot({ + uri, + }), + ], + }).compile(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(uri); + }); + }); + + describe('forRootAsync', () => { + it('should configure RabbitMQ with useFactory', async () => { + const spy = jest.spyOn(amqplib, 'connect'); + + app = await Test.createTestingModule({ + imports: [ + RabbitMQModule.forRootAsync({ + useFactory: async () => { + return { + uri, + }; + }, + }), + ], + }).compile(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(uri); + }); + + it('should configure RabbitMQ with useClass', async () => { + const spy = jest.spyOn(amqplib, 'connect'); + + app = await Test.createTestingModule({ + imports: [ + RabbitMQModule.forRootAsync({ + useClass: RabbitConfig, + }), + ], + }).compile(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(uri); + }); + + it('should configure RabbitMQ with useExisting explicit provide', async () => { + const spy = jest.spyOn(amqplib, 'connect'); + + const instance = new RabbitConfig(); + + app = await Test.createTestingModule({ + imports: [ + RabbitMQModule.forRootAsync({ + useExisting: { + provide: RabbitConfig, + value: instance, + }, + }), + ], + }).compile(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(uri); + }); + + it('should configure RabbitMQ with useExisting implicit provide', async () => { + const spy = jest.spyOn(amqplib, 'connect'); + + const instance = new RabbitConfig(); + + app = await Test.createTestingModule({ + imports: [ + RabbitMQModule.forRootAsync({ + useExisting: { + value: instance, + }, + }), + ], + }).compile(); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(uri); + }); + }); +}); diff --git a/integration/rabbitmq/e2e/nack-and-requeue.e2e-spec.ts b/integration/rabbitmq/e2e/nack-and-requeue.e2e-spec.ts index ee98a8629..29c1a49f1 100644 --- a/integration/rabbitmq/e2e/nack-and-requeue.e2e-spec.ts +++ b/integration/rabbitmq/e2e/nack-and-requeue.e2e-spec.ts @@ -62,14 +62,16 @@ describe('Nack and Requeue', () => { const moduleFixture = await Test.createTestingModule({ providers: [SubscribeService], imports: [ - RabbitMQModule.build({ - exchanges: [ - { - name: exchange, - type: 'topic', - }, - ], - uri: 'amqp://rabbitmq:rabbitmq@localhost:5672', + RabbitMQModule.forRootAsync({ + useFactory: () => ({ + exchanges: [ + { + name: exchange, + type: 'topic', + }, + ], + uri: 'amqp://rabbitmq:rabbitmq@localhost:5672', + }), }), ], }).compile(); diff --git a/integration/rabbitmq/e2e/subscribe.e2e-spec.ts b/integration/rabbitmq/e2e/subscribe.e2e-spec.ts index e3c79dba1..e909a6457 100644 --- a/integration/rabbitmq/e2e/subscribe.e2e-spec.ts +++ b/integration/rabbitmq/e2e/subscribe.e2e-spec.ts @@ -35,7 +35,7 @@ describe('Rabbit Subscribe', () => { const moduleFixture = await Test.createTestingModule({ providers: [SubscribeService], imports: [ - RabbitMQModule.build({ + RabbitMQModule.forRoot({ exchanges: [ { name: exchange, diff --git a/integration/rabbitmq/src/app.module.ts b/integration/rabbitmq/src/app.module.ts index 8e534b2ac..1c6980d1b 100644 --- a/integration/rabbitmq/src/app.module.ts +++ b/integration/rabbitmq/src/app.module.ts @@ -5,14 +5,16 @@ import { RpcService } from './rpc/rpc.service'; @Module({ imports: [ - RabbitMQModule.build({ - exchanges: [ - { - name: 'exchange1', - type: 'topic', - }, - ], - uri: 'amqp://rabbitmq:rabbitmq@localhost:5672', + RabbitMQModule.forRootAsync({ + useFactory: () => ({ + exchanges: [ + { + name: 'exchange1', + type: 'topic', + }, + ], + uri: 'amqp://rabbitmq:rabbitmq@localhost:5672', + }), }), ], controllers: [AppController], diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 2b3d8cffd..026d66a56 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1 +1,2 @@ export * from './mixins'; +export * from './options'; diff --git a/packages/common/src/options.ts b/packages/common/src/options.ts new file mode 100644 index 000000000..69f281fac --- /dev/null +++ b/packages/common/src/options.ts @@ -0,0 +1,49 @@ +import { Type } from '@nestjs/common'; +import { ModuleMetadata, Provider } from '@nestjs/common/interfaces'; +import { get } from 'lodash'; + +export interface OptionsFactory { + createOptions(): Promise | T; +} + +// type OptionsFactoryImpl> = T; + +export interface AsyncOptionsFactoryProvider + extends Pick { + useExisting?: { + value: OptionsFactory; + provide?: string | symbol | Type; + }; + useClass?: Type>; + useFactory?: (...args: any[]) => Promise | T; + inject?: any[]; +} + +export function createAsyncOptionsProvider( + provide: string | symbol | Type, + options: AsyncOptionsFactoryProvider +): Provider { + if (options.useFactory) { + return { + provide, + useFactory: options.useFactory, + inject: options.inject || [] + }; + } + + return { + provide, + useFactory: async (optionsFactory: OptionsFactory) => { + const options = await optionsFactory.createOptions(); + return options; + }, + inject: [ + options.useClass || + get( + options, + 'useExisting.provide', + (options.useExisting as any).value.constructor.name + ) + ] + }; +} diff --git a/packages/rabbitmq/README.md b/packages/rabbitmq/README.md index 3690e2d3e..da141d794 100644 --- a/packages/rabbitmq/README.md +++ b/packages/rabbitmq/README.md @@ -42,7 +42,7 @@ import { MessagingService } from './messaging/messaging.service'; @Module({ imports: [ - RabbitMQModule.build({ + RabbitMQModule.forRoot({ exchanges: [ { name: 'exchange1', diff --git a/packages/rabbitmq/src/rabbitmq.constants.ts b/packages/rabbitmq/src/rabbitmq.constants.ts index d4ece9d6c..e193a5aba 100644 --- a/packages/rabbitmq/src/rabbitmq.constants.ts +++ b/packages/rabbitmq/src/rabbitmq.constants.ts @@ -1 +1,2 @@ export const RABBIT_HANDLER = Symbol('RABBIT_HANDLER'); +export const RABBIT_CONFIG = Symbol('RABBIT_CONFIG'); diff --git a/packages/rabbitmq/src/rabbitmq.module.ts b/packages/rabbitmq/src/rabbitmq.module.ts index 73fb7eb41..1ae809edf 100644 --- a/packages/rabbitmq/src/rabbitmq.module.ts +++ b/packages/rabbitmq/src/rabbitmq.module.ts @@ -1,9 +1,19 @@ import { DiscoveryModule, DiscoveryService } from '@nestjs-plus/discovery'; -import { DynamicModule, Logger, Module, OnModuleInit } from '@nestjs/common'; +import { + DynamicModule, + Logger, + Module, + OnModuleInit, + Provider +} from '@nestjs/common'; +import { + AsyncOptionsFactoryProvider, + createAsyncOptionsProvider +} from '@nestjs-plus/common'; import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator'; import { groupBy } from 'lodash'; import { AmqpConnection } from './amqp/connection'; -import { RABBIT_HANDLER } from './rabbitmq.constants'; +import { RABBIT_HANDLER, RABBIT_CONFIG } from './rabbitmq.constants'; import { RabbitHandlerConfig, RabbitMQConfig } from './rabbitmq.interfaces'; @Module({ @@ -18,7 +28,54 @@ export class RabbitMQModule implements OnModuleInit { private readonly externalContextCreator: ExternalContextCreator ) {} + public static forRootAsync( + asyncOptionsFactoryProvider: AsyncOptionsFactoryProvider + ): DynamicModule { + return { + module: RabbitMQModule, + exports: [AmqpConnection], + imports: asyncOptionsFactoryProvider.imports, + providers: [ + ...this.createAsyncProviders(asyncOptionsFactoryProvider), + { + provide: AmqpConnection, + useFactory: async (config): Promise => { + const connection = new AmqpConnection(config); + await connection.init(); + const logger = new Logger(RabbitMQModule.name); + logger.log('Successfully connected to RabbitMQ'); + return connection; + }, + inject: [RABBIT_CONFIG] + } + ] + }; + } + + public static forRoot(config: RabbitMQConfig): DynamicModule { + return { + module: RabbitMQModule, + providers: [ + { + provide: AmqpConnection, + useFactory: async (): Promise => { + const connection = new AmqpConnection(config); + await connection.init(); + const logger = new Logger(RabbitMQModule.name); + logger.log('Successfully connected to RabbitMQ'); + return connection; + } + } + ], + exports: [AmqpConnection] + }; + } + public static build(config: RabbitMQConfig): DynamicModule { + const logger = new Logger(RabbitMQModule.name); + logger.warn( + 'build() is deprecated. use forRoot() or forRootAsync() to configure RabbitMQ' + ); return { module: RabbitMQModule, providers: [ @@ -89,4 +146,41 @@ export class RabbitMQModule implements OnModuleInit { ); } } + + private static createAsyncProviders( + asyncOptionsFactoryProvider: AsyncOptionsFactoryProvider + ): Provider[] { + const optionsProvider = createAsyncOptionsProvider( + RABBIT_CONFIG, + asyncOptionsFactoryProvider + ); + + if (asyncOptionsFactoryProvider.useFactory) { + return [optionsProvider]; + } + + if (asyncOptionsFactoryProvider.useClass) { + return [ + optionsProvider, + { + provide: asyncOptionsFactoryProvider.useClass, + useClass: asyncOptionsFactoryProvider.useClass + } + ]; + } + + if (asyncOptionsFactoryProvider.useExisting) { + return [ + optionsProvider, + { + provide: + asyncOptionsFactoryProvider.useExisting.provide || + asyncOptionsFactoryProvider.useExisting.value.constructor.name, + useValue: asyncOptionsFactoryProvider.useExisting.value + } + ]; + } + + return []; + } } diff --git a/packages/rabbitmq/src/rabbitmq.spec.ts b/packages/rabbitmq/src/rabbitmq.spec.ts index c157d8e75..942c5ee3a 100644 --- a/packages/rabbitmq/src/rabbitmq.spec.ts +++ b/packages/rabbitmq/src/rabbitmq.spec.ts @@ -30,40 +30,44 @@ describe('RabbitMQ', () => { let app: TestingModule; let amqpMock: AmqpConnection; - beforeEach(async () => { - amqpMock = new AmqpConnection({ - uri: '', - exchanges: [] - }); + describe('Module configuration', () => {}); - app = await Test.createTestingModule({ - imports: [ExampleModule, RabbitMQModule.attach(amqpMock)] - }).compile(); + describe('Attaching Handlers', () => { + beforeEach(async () => { + amqpMock = new AmqpConnection({ + uri: '', + exchanges: [] + }); - await app.init(); - }); + app = await Test.createTestingModule({ + imports: [ExampleModule, RabbitMQModule.attach(amqpMock)] + }).compile(); - it('should register rabbit rpc handlers', async () => { - expect(amqpMock.createRpc).toBeCalledTimes(1); + await app.init(); + }); - expect(amqpMock.createRpc).toBeCalledWith( - expect.any(Function), - expect.objectContaining({ - exchange: 'exchange1', - routingKey: 'rpc' - }) - ); - }); + it('should register rabbit rpc handlers', async () => { + expect(amqpMock.createRpc).toBeCalledTimes(1); - it('should register rabbit subscribe handlers', async () => { - expect(amqpMock.createSubscriber).toBeCalledTimes(1); + expect(amqpMock.createRpc).toBeCalledWith( + expect.any(Function), + expect.objectContaining({ + exchange: 'exchange1', + routingKey: 'rpc' + }) + ); + }); + + it('should register rabbit subscribe handlers', async () => { + expect(amqpMock.createSubscriber).toBeCalledTimes(1); - expect(amqpMock.createSubscriber).toBeCalledWith( - expect.any(Function), - expect.objectContaining({ - exchange: 'exchange2', - routingKey: 'subscribe' - }) - ); + expect(amqpMock.createSubscriber).toBeCalledWith( + expect.any(Function), + expect.objectContaining({ + exchange: 'exchange2', + routingKey: 'subscribe' + }) + ); + }); }); }); diff --git a/packages/rabbitmq/tsconfig.json b/packages/rabbitmq/tsconfig.json index aab4f4ce2..b7058197e 100644 --- a/packages/rabbitmq/tsconfig.json +++ b/packages/rabbitmq/tsconfig.json @@ -5,5 +5,5 @@ "rootDir": "./src" }, "include": ["./src"], - "references": [{ "path": "../discovery" }] + "references": [{ "path": "../discovery" }, { "path": "../common" }] }