Skip to content

Commit

Permalink
feat(rabbitmq): adds consistent rabbitmq config
Browse files Browse the repository at this point in the history
use community convention of forRoot and forRootAsync to bootstrap module

fix #34
  • Loading branch information
WonderPanda committed Apr 7, 2019
1 parent 9bba7cb commit 8d6de1d
Show file tree
Hide file tree
Showing 12 changed files with 329 additions and 62 deletions.
26 changes: 14 additions & 12 deletions examples/kitchen-sink/src/rabbit-example/rabbit-example.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:https://rabbitmq:rabbitmq@localhost:5672',
RabbitMQModule.forRootAsync({
useFactory: () => ({
exchanges: [
{
name: 'exchange1',
type: 'topic',
},
{
name: 'exchange2',
type: 'fanout',
},
],
uri: 'amqp:https://rabbitmq:rabbitmq@localhost:5672',
}),
}),
RabbitExampleModule,
],
Expand Down
112 changes: 112 additions & 0 deletions integration/rabbitmq/e2e/configuration.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -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:https://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);
});
});
});
18 changes: 10 additions & 8 deletions integration/rabbitmq/e2e/nack-and-requeue.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:https://rabbitmq:rabbitmq@localhost:5672',
RabbitMQModule.forRootAsync({
useFactory: () => ({
exchanges: [
{
name: exchange,
type: 'topic',
},
],
uri: 'amqp:https://rabbitmq:rabbitmq@localhost:5672',
}),
}),
],
}).compile();
Expand Down
2 changes: 1 addition & 1 deletion integration/rabbitmq/e2e/subscribe.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('Rabbit Subscribe', () => {
const moduleFixture = await Test.createTestingModule({
providers: [SubscribeService],
imports: [
RabbitMQModule.build({
RabbitMQModule.forRoot({
exchanges: [
{
name: exchange,
Expand Down
18 changes: 10 additions & 8 deletions integration/rabbitmq/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { RpcService } from './rpc/rpc.service';

@Module({
imports: [
RabbitMQModule.build({
exchanges: [
{
name: 'exchange1',
type: 'topic',
},
],
uri: 'amqp:https://rabbitmq:rabbitmq@localhost:5672',
RabbitMQModule.forRootAsync({
useFactory: () => ({
exchanges: [
{
name: 'exchange1',
type: 'topic',
},
],
uri: 'amqp:https://rabbitmq:rabbitmq@localhost:5672',
}),
}),
],
controllers: [AppController],
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './mixins';
export * from './options';
49 changes: 49 additions & 0 deletions packages/common/src/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Type } from '@nestjs/common';
import { ModuleMetadata, Provider } from '@nestjs/common/interfaces';
import { get } from 'lodash';

export interface OptionsFactory<T> {
createOptions(): Promise<T> | T;
}

// type OptionsFactoryImpl<T extends OptionsFactory<T>> = T;

export interface AsyncOptionsFactoryProvider<T>
extends Pick<ModuleMetadata, 'imports'> {
useExisting?: {
value: OptionsFactory<T>;
provide?: string | symbol | Type<any>;
};
useClass?: Type<OptionsFactory<T>>;
useFactory?: (...args: any[]) => Promise<T> | T;
inject?: any[];
}

export function createAsyncOptionsProvider<T>(
provide: string | symbol | Type<any>,
options: AsyncOptionsFactoryProvider<T>
): Provider {
if (options.useFactory) {
return {
provide,
useFactory: options.useFactory,
inject: options.inject || []
};
}

return {
provide,
useFactory: async (optionsFactory: OptionsFactory<T>) => {
const options = await optionsFactory.createOptions();
return options;
},
inject: [
options.useClass ||
get(
options,
'useExisting.provide',
(options.useExisting as any).value.constructor.name
)
]
};
}
2 changes: 1 addition & 1 deletion packages/rabbitmq/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { MessagingService } from './messaging/messaging.service';

@Module({
imports: [
RabbitMQModule.build({
RabbitMQModule.forRoot({
exchanges: [
{
name: 'exchange1',
Expand Down
1 change: 1 addition & 0 deletions packages/rabbitmq/src/rabbitmq.constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const RABBIT_HANDLER = Symbol('RABBIT_HANDLER');
export const RABBIT_CONFIG = Symbol('RABBIT_CONFIG');
98 changes: 96 additions & 2 deletions packages/rabbitmq/src/rabbitmq.module.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -18,7 +28,54 @@ export class RabbitMQModule implements OnModuleInit {
private readonly externalContextCreator: ExternalContextCreator
) {}

public static forRootAsync(
asyncOptionsFactoryProvider: AsyncOptionsFactoryProvider<RabbitMQConfig>
): DynamicModule {
return {
module: RabbitMQModule,
exports: [AmqpConnection],
imports: asyncOptionsFactoryProvider.imports,
providers: [
...this.createAsyncProviders(asyncOptionsFactoryProvider),
{
provide: AmqpConnection,
useFactory: async (config): Promise<AmqpConnection> => {
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<AmqpConnection> => {
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: [
Expand Down Expand Up @@ -89,4 +146,41 @@ export class RabbitMQModule implements OnModuleInit {
);
}
}

private static createAsyncProviders(
asyncOptionsFactoryProvider: AsyncOptionsFactoryProvider<RabbitMQConfig>
): 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 [];
}
}
Loading

0 comments on commit 8d6de1d

Please sign in to comment.