Skip to content

Commit

Permalink
feat!: add async module configuration
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `QueueModule.forRoot()` connection argument
interface was restructured and `AMQPService.getConnectionOptions()`
was renamed to `AMQPService.getModuleOptions()`.
  • Loading branch information
tahubu committed Apr 13, 2021
1 parent c602846 commit de4a3df
Show file tree
Hide file tree
Showing 21 changed files with 490 additions and 122 deletions.
106 changes: 94 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,115 @@ $ npm install --save class-transformer class-validator
In the subsections you can see how to send and receive messages, how to handle message transfer and how to use DTO classes to message
payload transformation and validation.

### Create connection
### Module import and connection options

To create a connection, you have to set the connection details. The library provides an easy way to set the connection details via a string
connection URI. The library will parse this connection URI and set the appropriate connection options. Besides, you can add your custom
connection options or other library settings with the module options.

#### Create connection

To create a connection, you have to import the `QueueModule.forRoot()` module into your application's root module. The `forRoot()`
static method has 2 parameters: the first and required is the connection URI string, and the second is the *optional* connection
options object. The connection options object extends the [Rhea's connection options](https://www.npmjs.com/package/rhea#connectoptions)
and accepts these new properties:
* **throwExceptionOnConnectionError**?: A boolean value. If it's `true` then AMQPModule will throw forward the exception which occurs
during the connection creation. Default value is `false`.
* **acceptValidationNullObjectException**?: A boolean value. If it's `true` then AMQPModule will accept the message when a
`ValidationNullObjectException` error was thrown. (ValidationNullObjectException will be thrown when message body is null). Otherwise the
message will be rejected on `ValidationNullObjectException` error. Default value is `false`.
static method has 2 parameters: the first and required is the connection URI string, and the second is the *optional* module
options object. To see the available module options, scroll down. Here is the example:

> Note: the AMQPModule package does not support multiple connection!
> Note: the @team-supercharge/nest-amqp package does not support multiple connection!
```typescript
import { Module } from '@nestjs/common';
import { QueueModule } from '@team-supercharge/nest-amqp';

@Module({
imports: [
QueueModule.forRoot('amqp://user:password@localhost:5672'),
// ...
],
})
export class AppModule {}
```

#### Create connection with asynchronous module configuration

Generally we are using environment variables to configure our application. Nest provides the
[ConfigService](https://docs.nestjs.com/techniques/configuration) to use these env variables nicely. If you would like to configure the
AMQP module with env variables, or you are using another asynchronous way to get the configuration, then you have to use the
`forRootAsync()` static method instead of `forRoot()` on the `QueueModule` class. To see the available module options, scroll down.
Here is an example:

> Note: the @team-supercharge/nest-amqp package does not support multiple connection!
```typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { QueueModule, QueueModuleOptions } from '@team-supercharge/nest-amqp';

@Module({
imports: [
QueueModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService): QueueModuleOptions => ({
connectionUri: configService.get<string>('AMQP_CONNECTION_URI'),
connectionOptions: {
transport: 'tcp'
}
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
```

Instead of `useFactory` you can use `useClass` or `useExisting` to set module options. You can see the examples in the
[test file](https://github.com/team-supercharge/nest-amqp/tree/master/src/queue.module.spec.ts).

#### Module options

The module options object needs to be added to the `forRoot()` or `forRootAsync()` static method. The possible options can be these:
* **throwExceptionOnConnectionError**?: A boolean value. If it's `true` then AMQPModule will throw forward the exception which occurs
during the connection creation. Default value is `false`.
* **acceptValidationNullObjectException**?: A boolean value. If it's `true` then AMQPModule will accept the message when a
`ValidationNullObjectException` error was thrown. (ValidationNullObjectException will be thrown when message body is null). Otherwise the
message will be rejected on `ValidationNullObjectException` error. Default value is `false`.
* **connectionUri**?: It is an optional string property. This is required only when you are use the `forRootAsync()` method or the
`forRoot()` method with only an object argument.
* **connectionOptions**?: It is an optional object. With this you can set
[Rhea's connection options](https://www.npmjs.com/package/rhea#connectoptions) options which will be passed to the `Connection` object
during connection creation. The default value is `{}`.

First basic example:
```typescript
@Module({
imports: [
QueueModule.forRoot(
'amqp://user:password@localhost:5672',
{
throwExceptionOnConnectionError: true
throwExceptionOnConnectionError: true,
connectionOptions: {
transport: 'tcp'
}
}
),
// ...
],
})
export class AppModule {}
```

Second example with asynchronous configuration:
```typescript
@Module({
imports: [
QueueModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService): QueueModuleOptions => ({
connectionUri: configService.get<string>('AMQP_CONNECTION_URI'),
throwExceptionOnConnectionError: true,
connectionOptions: {
transport: 'tcp'
}
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:clear": "jest --clearCache true --clearMocks true && jest"
},
"dependencies": {
"debug": "^4.3.1",
Expand Down
1 change: 1 addition & 0 deletions src/constant/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './queue.const';
3 changes: 3 additions & 0 deletions src/constant/queue.const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const QUEUE_MODULE_OPTIONS = 'QueueModuleOptions';
export const AMQP_CLIENT_TOKEN = 'AMQP_CLIENT';
export const AMQP_CONNECTION_RECONNECT = 'amqp_connection_reconnect';
4 changes: 3 additions & 1 deletion src/interface/amqp-connection-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { ConnectionOptions } from 'rhea-promise';
*
* @publicApi
*/
export type AMQPConnectionOptions = ConnectionOptions & {
export type QueueModuleOptions = {
throwExceptionOnConnectionError?: boolean;
acceptValidationNullObjectException?: boolean;
connectionUri?: string;
connectionOptions?: ConnectionOptions;
};
1 change: 1 addition & 0 deletions src/interface/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './amqp-connection-options.interface';
export * from './queue.interface';
export * from './queue-options.interface';
14 changes: 14 additions & 0 deletions src/interface/queue-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ModuleMetadata, Type } from '@nestjs/common';

import { QueueModuleOptions } from './index';

export interface QueueModuleOptionsFactory {
createQueueModuleOptions(): Promise<QueueModuleOptions> | QueueModuleOptions;
}

export interface QueueModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
useExisting?: Type<QueueModuleOptionsFactory>;
useClass?: Type<QueueModuleOptionsFactory>;
useFactory?: (...args: any[]) => Promise<QueueModuleOptions> | QueueModuleOptions;
inject?: any[];
}
2 changes: 1 addition & 1 deletion src/interface/queue.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Message } from 'rhea-promise';

import { ObjectValidationOptions } from '../util/object-validator';
import { ObjectValidationOptions } from '../service';

/**
* Interface defining options that can be passed to `@Listen()` decorator
Expand Down
143 changes: 143 additions & 0 deletions src/queue.module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Injectable, Module } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';

jest.mock('rhea-promise');

import { QueueModuleOptions, QueueModuleOptionsFactory } from './interface';
import { QueueModule } from './queue.module';
import { AMQPService, QueueService } from './service';

describe('QueueModule', () => {
const connectionUri = 'amqp://localhost:5672';
const moduleOptions: QueueModuleOptions = {
connectionUri,
};
const originalModuleProviders = (QueueModule as any).moduleDefinition.providers;
let module: TestingModule;

@Injectable()
class TestForFeatureService {
constructor(public readonly queueService: QueueService) {}
}

@Module({
imports: [QueueModule.forFeature()],
providers: [TestForFeatureService],
exports: [TestForFeatureService],
})
class TestForFeatureModule {}

@Injectable()
class TestConfigService {
public getAmqpUrl(): string {
return connectionUri;
}
}

@Module({
providers: [TestConfigService],
exports: [TestConfigService],
})
class TestConfigModule {}

@Injectable()
class TestQueueConfigService implements QueueModuleOptionsFactory {
public async createQueueModuleOptions(): Promise<QueueModuleOptions> {
return { connectionUri };
}
}

@Module({
providers: [TestQueueConfigService],
exports: [TestQueueConfigService],
})
class TestQueueConfigModule {}

afterEach(async () => {
await module.close();
(QueueModule as any).moduleDefinition.imports = [];
(QueueModule as any).moduleDefinition.providers = originalModuleProviders;
});

describe('forRoot()', () => {
it('should import as sync root module', async () => {
module = await Test.createTestingModule({
imports: [QueueModule.forRoot(connectionUri)],
}).compile();
const amqpService = module.get<AMQPService>(AMQPService);

expect(amqpService.getModuleOptions()).toEqual(moduleOptions);
});
});

describe('forFeature()', () => {
it('should import as feature module', async () => {
module = await Test.createTestingModule({
imports: [QueueModule.forRoot(connectionUri), TestForFeatureModule],
}).compile();
const forFeatureTestService = module.get<TestForFeatureService>(TestForFeatureService);

expect((forFeatureTestService.queueService as any).amqpService.getModuleOptions()).toEqual(moduleOptions);
});
});

describe('forRootAsync()', () => {
it(`should import as sync module with 'useFactory'`, async () => {
module = await Test.createTestingModule({
imports: [
QueueModule.forRootAsync({
useFactory: () => ({ connectionUri }),
}),
],
}).compile();
const amqpService = module.get<AMQPService>(AMQPService);

expect(amqpService.getModuleOptions()).toEqual({ connectionUri });
});

it(`should import as async module with 'useFactory'`, async () => {
module = await Test.createTestingModule({
imports: [
QueueModule.forRootAsync({
imports: [TestConfigModule],
inject: [TestConfigService],
useFactory: (testConfigService: TestConfigService) => ({
connectionUri: testConfigService.getAmqpUrl(),
}),
}),
],
}).compile();
const amqpService = module.get<AMQPService>(AMQPService);

expect(amqpService.getModuleOptions()).toEqual({ connectionUri });
});

it(`should import as async module with 'useClass'`, async () => {
module = await Test.createTestingModule({
imports: [
QueueModule.forRootAsync({
imports: [TestQueueConfigModule],
useClass: TestQueueConfigService,
}),
],
}).compile();
const amqpService = module.get<AMQPService>(AMQPService);

expect(amqpService.getModuleOptions()).toEqual({ connectionUri });
});

it(`should import as async module with 'useExisting'`, async () => {
module = await Test.createTestingModule({
imports: [
QueueModule.forRootAsync({
imports: [TestQueueConfigModule],
useExisting: TestQueueConfigService,
}),
],
}).compile();
const amqpService = module.get<AMQPService>(AMQPService);

expect(amqpService.getModuleOptions()).toEqual({ connectionUri });
});
});
});
Loading

0 comments on commit de4a3df

Please sign in to comment.