├── .env.dist ├── .github └── dependabot.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── docker-compose.yml ├── package.json ├── src ├── __tests__ │ ├── __stubs__ │ │ └── config │ │ │ └── amqp.ts │ ├── amqp.inject.decorator.spec.ts │ ├── amqp.module.spec.ts │ ├── amqp.retry.spec.ts │ └── setup.js ├── amqp.constants.ts ├── amqp.module.ts ├── decorators │ └── index.ts ├── index.ts ├── interfaces │ ├── amqp.async.options.ts │ ├── amqp.options.object.interface.ts │ ├── amqp.options.ts │ └── index.ts └── utils │ ├── create.tokens.ts │ ├── index.ts │ └── retry.ts ├── tsconfig.json └── yarn.lock /.env.dist: -------------------------------------------------------------------------------- 1 | HOST=localhost -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: "@types/node" 10 | versions: 11 | - 14.14.22 12 | - 14.14.24 13 | - 14.14.25 14 | - 14.14.26 15 | - 14.14.28 16 | - 14.14.32 17 | - 14.14.33 18 | - 14.14.34 19 | - 14.14.35 20 | - 14.14.36 21 | - 14.14.37 22 | - 14.14.39 23 | - 14.14.41 24 | - 15.0.0 25 | - dependency-name: y18n 26 | versions: 27 | - 4.0.1 28 | - 4.0.2 29 | - dependency-name: "@types/jest" 30 | versions: 31 | - 26.0.20 32 | - 26.0.21 33 | - 26.0.22 34 | - dependency-name: typescript 35 | versions: 36 | - 4.1.5 37 | - 4.2.2 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | /dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src 2 | tsconfig.json 3 | docker-compose.yml 4 | .travis.yml 5 | .env.dist 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80, 4 | "proseWrap": "always", 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "semi": true 11 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: node_js 4 | node_js: 5 | - "8" 6 | - "10" 7 | - "node" 8 | services: 9 | - docker 10 | before_install: 11 | - docker-compose up -d --build --force-recreate 12 | - npm i -g npm@latest 13 | - npm i -g yarn 14 | - cp .env.dist .env 15 | install: 16 | - yarn 17 | script: 18 | - yarn run test 19 | after_success: yarn run coveralls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ashleigh Simonelli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

3 | 4 | 5 | 6 | 7 | 8 | 9 | Awesome Nest 10 | Nest Powered 11 |

12 |

Nestjs Amqp

13 | 14 |

An AMQP connection service for NestJS.

15 | 16 |

Using the AMQPlib for node package.

17 | 18 | This package was intented to be used in execution content and provides a basic AMQPlib connection via the providers to allow developers to develop their amqp queue consumers and publishers. For microservice transport; check out the docs for rabbitMQ. 19 | 20 | ## Install 21 | 22 | ```bash 23 | $ yarn add nestjs-amqp 24 | $ yarn add -D @types/amqplib 25 | ``` 26 | 27 | ## Basic usage 28 | 29 | ```ts 30 | import {Module} from '@nestjs/common'; 31 | import {AmqpModule} from 'nestjs-amqp'; 32 | 33 | @Module({ 34 | imports: [AmqpModule.forRoot({ 35 | name: 'rabbitmq', 36 | hostname: 'localhost', 37 | port: 5672, 38 | username: 'test', 39 | password: 'test', 40 | })], 41 | }) 42 | export default class AppModule {} 43 | 44 | ``` 45 | 46 | ## Advanced usage 47 | ### Usage with nestjs-config 48 | 49 | ```ts 50 | import {Module} from '@nestjs/common'; 51 | import {AmqpModule} from 'nestjs-amqp'; 52 | import {ConfigModule, ConfigService} from 'nestjs-config'; 53 | import * as path from 'path'; 54 | 55 | @Module({ 56 | imports: [ 57 | ConfigModule.load(path.resolve(__dirname, 'config', '**/*.ts')), 58 | AmqpModule.forRootAsync({ 59 | useFactory: (config: ConfigService) => config.get('amqp'), 60 | inject: [ConfigService], 61 | }), 62 | ], 63 | }) 64 | export default class AppModule {} 65 | 66 | //src/config/amqp.ts 67 | export default { 68 | name: 'rabbitmq', 69 | hostname: process.env.AMQP_HOST, 70 | port: process.env.AMQP_PORT, 71 | username: process.env.USERNAME, 72 | password: process.env.PASSWORD, 73 | } 74 | ``` 75 | > Unfortunately multiple connections are unavailable when using the `forRootAsync` method. 76 | 77 | ### Usage in custom provider 78 | It is possible to inject the AmqpConnection in a factory of a custom provider if one needs such capability. 79 | 80 | 81 | ```ts 82 | import { Connection as AmqpConnection } from 'amqplib'; 83 | import {ConfigService} from 'nestjs-config'; 84 | import {createConnectionToken} from 'nestjs-amqp/utils'; 85 | 86 | export const queueServiceProvider = { 87 | provider: 'QueueService', 88 | useFactory: (amqp: AmqpConnection, configService: ConfigService) => new QueueService(amqp, config.get('queue')), 89 | inject: [createConnectionToken('default'), ConfigService], 90 | } 91 | ``` 92 | 93 | It's also possible to give your connections names, if you have done so then use the name of your connection instead of ``default``. 94 | ### Connection Decorators 95 | 96 | ```ts 97 | import {Module} from '@nestjs/common'; 98 | import {AmqpModule} from 'nestjs-amqp'; 99 | 100 | @Module({ 101 | imports: [AmqpModule.forRoot([ 102 | { 103 | hostname: 'test:test@localhost', 104 | }, 105 | { 106 | username: 'test', 107 | password: 'test', 108 | hostname: 'localhost', 109 | port: 5672, 110 | protocol: 'amqps', 111 | name: 'test', 112 | } 113 | ])], 114 | }) 115 | export default class ExecutionModule { 116 | } 117 | ``` 118 | 119 | ```ts 120 | import {Injectable} from '@nestjs/common'; 121 | import {InjectAmqpConnection} from 'nestjs-amqp'; 122 | import {Connection} from 'amqplib'; 123 | 124 | @Injectable() 125 | export default class TestService { 126 | constructor( 127 | @InjectAmqpConnection('test') 128 | private readonly connectionTest: Connection, //gets connection with name 'test' defined in module 129 | @InjectAmqpConnection(0) 130 | private readonly connection0: Connection, //gets first defined connection without a name 131 | ) {} 132 | } 133 | ``` 134 | > Use InjectAmqpConnection without a parameter for default connection 135 | 136 | ### Example publish 137 | 138 | ```ts 139 | import {Injectable, Logger} from '@nestjs/common'; 140 | import {InjectAmqpConnection} from 'nestjs-amqp'; 141 | import {Connection} from 'amqplib'; 142 | 143 | @Injectable() 144 | export default class TestProvider { 145 | constructor( 146 | @InjectAmqpConnection() 147 | private readonly amqp: Connection, 148 | ) {} 149 | async publish(message: string) { 150 | await this.amqp.createChannel((err, channel) => { 151 | if (err != null) { 152 | Logger.alert(err, 'TestProvider'); 153 | } 154 | channel.assertQueue('test_queue_name'); 155 | channel.sendToQueue('test_queue_name', message); 156 | }); 157 | } 158 | } 159 | ``` 160 | More information and examples about amqplib can be found [here](https://www.npmjs.com/package/amqplib). 161 | 162 | ## Available Options 163 | 164 | Name | For | Default 165 | --- | --- | --- 166 | hostname | The host url for the connection | `localhost` 167 | port | The port of the amqp host | `5672` 168 | name | The name of the connection | `default` or the array key index `[0]` 169 | retrys | The amount of retry attempts before surrender | 3 170 | retryDelay | The amount of milliseconds to wait before attempting retry | 3000 171 | protocol | The protocol for the connection | `amqp` 172 | username | The username for the connection | 173 | password | The password for the connection | 174 | locale | The desired locale for error messages | `en_US` 175 | frameMax | The size in bytes of the maximum frame allowed over the connection | 0 176 | heartbeat | The period of the connection heartbeat in seconds | 0 177 | vhost | What VHost shall be used | `/` 178 | 179 | ## Testing this package 180 | 181 | In order to test first you need to start the rabbitmq container. We've provided a `docker-compose` file to make this easier. 182 | 183 | ```bash 184 | $ docker-compose up -d 185 | $ yarn test 186 | ``` 187 | > Navigate to localhost:15672 for rabbitmq manager, username and password are both `guest` 188 | 189 | > If you're using docker-machine or a VM then change the env for `HOST` in the `.env` file or create one using the provided `.env.dist` file. 190 | 191 | ## Future implementation 192 | 193 | > WARNING: The below examples have not been implemented 194 | 195 | So far this package manages multiple AMQP connections using the nestjs container and injects them into other providers. 196 | Alternatively I'd like to implement something like this: 197 | 198 | ```ts 199 | import {Injectable} from '@nestjs/common'; 200 | import { 201 | AmqpConnection, 202 | Consume, 203 | Publish, 204 | Message, 205 | } from 'nestjs-amqp'; 206 | 207 | @Injectable() 208 | @AmqpConnection() 209 | export default class MyAmqpService { 210 | 211 | @Consume("queue_name", { 212 | noAck: true, 213 | }) 214 | async listen(@Message message) { 215 | console.log('Message received', message); 216 | 217 | //send a message back 218 | this.publish(); 219 | } 220 | 221 | @Publish("queue_name") 222 | async publish() { 223 | return "Send this to 'queue queue_name'"; 224 | } 225 | } 226 | ``` 227 | 228 | Then using executable context 229 | 230 | ```ts 231 | 232 | import { NestFactory } from '@nestjs/core'; 233 | import QueueModule, { MyAmqpService } from './queue'; 234 | 235 | async function bootstrap() { 236 | const app = await NestFactory.create(QueueModule); 237 | const event = app.get(MyAmqpService); 238 | 239 | await event.listen(); 240 | 241 | } 242 | bootstrap(); 243 | 244 | process.stdin.resume(); 245 | ``` 246 | 247 | Or something similar to the above is what I'd like to implement 248 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | rabbitmq: 4 | image: rabbitmq:3-management 5 | ports: 6 | - 15672:15672 7 | - 5672:5672 8 | secure-rabbitmq: 9 | image: rabbitmq:3-management 10 | ports: 11 | - 15673:15672 12 | - 5673:5672 13 | environment: 14 | RABBITMQ_DEFAULT_PASS: pass 15 | RABBITMQ_DEFAULT_USER: user -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-amqp", 3 | "description": "AMQP connection service for nestjs", 4 | "keywords": [ 5 | "nestjs", 6 | "amqp" 7 | ], 8 | "main": "dist/index.js", 9 | "version": "0.2.2", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@nestjs/common": "^6.4.1", 13 | "@nestjs/core": "^6.4.1", 14 | "@nestjs/platform-express": "^7.0.0", 15 | "@nestjs/testing": "^7.0.0", 16 | "@types/amqplib": "^0.5.13", 17 | "@types/es6-promise": "^3.3.0", 18 | "@types/jest": "^27.0.2", 19 | "@types/node": "^15.12.4", 20 | "coveralls": "^3.0.2", 21 | "dotenv": "^10.0.0", 22 | "jest": "^24.9.0", 23 | "nestjs-config": "^1.2.3", 24 | "prettier": "^2.4.1", 25 | "ts-jest": "^24.0.2", 26 | "typescript": "^4.1.3" 27 | }, 28 | "peerDependencies": { 29 | "@nestjs/common": "^5.1.0 || ^6.0.3", 30 | "@types/amqplib": "^0.5.13" 31 | }, 32 | "dependencies": { 33 | "amqplib": "^0.7.0", 34 | "reflect-metadata": "^0.1.12", 35 | "rxjs": "^6.2.1" 36 | }, 37 | "scripts": { 38 | "build": "rm -rf ./dist && tsc", 39 | "coverage": "jest --coverage", 40 | "coveralls": "yarn run coverage --coverageReporters=text-lcov | coveralls", 41 | "test": "jest", 42 | "format": "prettier **/**/*.ts --ignore-path ./.prettierignore --write && git status", 43 | "prepublish": "npm run format && npm run build" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/nestjs-community/nestjs-amqp.git" 48 | }, 49 | "jest": { 50 | "moduleFileExtensions": [ 51 | "js", 52 | "json", 53 | "ts" 54 | ], 55 | "rootDir": "src", 56 | "testRegex": ".spec.ts$", 57 | "transform": { 58 | "^.+\\.(t|j)s$": "ts-jest" 59 | }, 60 | "coverageDirectory": "./coverage", 61 | "setupFiles": [ 62 | "/__tests__/setup.js" 63 | ] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/__tests__/__stubs__/config/amqp.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | hostname: process.env.HOST, 3 | port: 5672, 4 | }; -------------------------------------------------------------------------------- /src/__tests__/amqp.inject.decorator.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { AmqpModule } from './../index'; 4 | import { InjectAmqpConnection } from '../decorators'; 5 | const ChannelModel = require('amqplib/lib/channel_model').ChannelModel; 6 | 7 | describe('InjectAmqpConnection', () => { 8 | it('Connection should inject', async () => { 9 | @Injectable() 10 | class TestProvider { 11 | constructor(@InjectAmqpConnection() private readonly connection) {} 12 | 13 | getConnection() { 14 | return this.connection; 15 | } 16 | } 17 | 18 | const module: TestingModule = await Test.createTestingModule({ 19 | imports: [ 20 | AmqpModule.forRoot({ 21 | hostname: process.env.HOST, 22 | retrys: 1, 23 | }), 24 | ], 25 | providers: [TestProvider], 26 | }).compile(); 27 | 28 | const app = module.createNestApplication(); 29 | await app.init(); 30 | 31 | const provider = module.get(TestProvider); 32 | 33 | expect(provider.getConnection()).toBeInstanceOf(ChannelModel); 34 | await app.close(); 35 | }); 36 | 37 | it('Connection should inject with name', async () => { 38 | @Injectable() 39 | class TestProvider { 40 | constructor( 41 | @InjectAmqpConnection('1') private readonly connection, 42 | @InjectAmqpConnection('0') private readonly connection0, 43 | ) {} 44 | 45 | getConnection() { 46 | return this.connection; 47 | } 48 | 49 | getConnection0() { 50 | return this.connection0; 51 | } 52 | } 53 | 54 | const module: TestingModule = await Test.createTestingModule({ 55 | imports: [ 56 | AmqpModule.forRoot([ 57 | { 58 | hostname: process.env.HOST, 59 | retrys: 1, 60 | }, 61 | { 62 | hostname: process.env.HOST, 63 | retrys: 1, 64 | }, 65 | ]), 66 | ], 67 | providers: [TestProvider], 68 | }).compile(); 69 | 70 | const app = module.createNestApplication(); 71 | await app.init(); 72 | 73 | const provider = module.get(TestProvider); 74 | 75 | expect(provider.getConnection()).toBeInstanceOf(ChannelModel); 76 | expect(provider.getConnection0()).toBeInstanceOf(ChannelModel); 77 | await app.close(); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/__tests__/amqp.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AmqpModule } from './../index'; 3 | import { createConnectionToken } from '../utils/create.tokens'; 4 | import { Module } from '@nestjs/common'; 5 | const ChannelModel = require('amqplib/lib/channel_model').ChannelModel; 6 | import { ConfigModule, ConfigService } from 'nestjs-config'; 7 | import * as path from 'path'; 8 | import { InjectAmqpConnection } from '../decorators'; 9 | 10 | describe('AmqpModule', () => { 11 | it('Instace Amqp', async () => { 12 | const module: TestingModule = await Test.createTestingModule({ 13 | imports: [ 14 | AmqpModule.forRoot({ 15 | name: 'instance', 16 | hostname: process.env.HOST, 17 | retrys: 1, 18 | }), 19 | ], 20 | }).compile(); 21 | 22 | const app = module.createNestApplication(); 23 | await app.init(); 24 | 25 | const amqpModule = module.get(AmqpModule); 26 | 27 | expect(amqpModule).toBeInstanceOf(AmqpModule); 28 | 29 | await app.close(); 30 | }); 31 | 32 | it('Instace Amqp Connection provider', async () => { 33 | const module: TestingModule = await Test.createTestingModule({ 34 | imports: [ 35 | AmqpModule.forRoot({ 36 | name: 'first', 37 | hostname: process.env.HOST, 38 | retrys: 1, 39 | }), 40 | ], 41 | }).compile(); 42 | 43 | const app = module.createNestApplication(); 44 | await app.init(); 45 | 46 | const amqpConnection = module.get(createConnectionToken('first')); 47 | 48 | expect(amqpConnection).toBeInstanceOf(ChannelModel); 49 | await app.close(); 50 | }); 51 | 52 | it('Multiple connection options', async () => { 53 | const module: TestingModule = await Test.createTestingModule({ 54 | imports: [ 55 | AmqpModule.forRoot([ 56 | { 57 | hostname: process.env.HOST, 58 | name: 'test', 59 | retrys: 1, 60 | }, 61 | { 62 | hostname: process.env.HOST, 63 | retrys: 1, 64 | }, 65 | ]), 66 | ], 67 | }).compile(); 68 | 69 | const app = module.createNestApplication(); 70 | await app.init(); 71 | 72 | const amqpConnectionTest = module.get(createConnectionToken('test')); 73 | const amqpConnection1 = module.get(createConnectionToken('1')); 74 | 75 | expect(amqpConnectionTest).toBeInstanceOf(ChannelModel); 76 | expect(amqpConnection1).toBeInstanceOf(ChannelModel); 77 | await app.close(); 78 | }); 79 | 80 | it('Connection options', async () => { 81 | const module: TestingModule = await Test.createTestingModule({ 82 | imports: [ 83 | AmqpModule.forRoot({ 84 | hostname: process.env.HOST, 85 | name: 'test2', 86 | port: 5673, 87 | username: 'user', 88 | password: 'pass', 89 | retrys: 1, 90 | }), 91 | ], 92 | }).compile(); 93 | 94 | const app = module.createNestApplication(); 95 | await app.init(); 96 | 97 | const amqpConnectionTest = module.get(createConnectionToken('test2')); 98 | 99 | expect(amqpConnectionTest).toBeInstanceOf(ChannelModel); 100 | await app.close(); 101 | }); 102 | 103 | it('Connection available in submodule', async () => { 104 | @Module({ 105 | imports: [AmqpModule.forFeature()], 106 | }) 107 | class SubModule {} 108 | 109 | const module: TestingModule = await Test.createTestingModule({ 110 | imports: [ 111 | AmqpModule.forRoot({ 112 | name: 'subModule', 113 | hostname: process.env.HOST, 114 | retrys: 1, 115 | }), 116 | SubModule, 117 | ], 118 | }).compile(); 119 | 120 | const app = module.createNestApplication(); 121 | await app.init(); 122 | 123 | const provider = module 124 | .select(SubModule) 125 | .get(createConnectionToken('subModule')); 126 | 127 | expect(provider).toBeInstanceOf(ChannelModel); 128 | await app.close(); 129 | }); 130 | 131 | it('Connections should build with AmqpAsyncOptionsInterface', async () => { 132 | class TestProvider { 133 | constructor(@InjectAmqpConnection() private readonly amqp) {} 134 | 135 | getAmqp() { 136 | return this.amqp; 137 | } 138 | } 139 | 140 | const module: TestingModule = await Test.createTestingModule({ 141 | imports: [ 142 | ConfigModule.load( 143 | path.resolve(__dirname, '__stubs__', 'config', '*.ts'), 144 | ), 145 | AmqpModule.forRootAsync({ 146 | useFactory: async config => config.get('amqp'), 147 | inject: [ConfigService], 148 | }), 149 | ], 150 | providers: [TestProvider], 151 | }).compile(); 152 | 153 | const app = module.createNestApplication(); 154 | await app.init(); 155 | 156 | const provider = module.get(TestProvider); 157 | 158 | expect(provider.getAmqp()).toBeInstanceOf(ChannelModel); 159 | await app.close(); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /src/__tests__/amqp.retry.spec.ts: -------------------------------------------------------------------------------- 1 | import retry from '../utils/retry'; 2 | import { from } from 'rxjs'; 3 | import * as amqp from 'amqplib'; 4 | 5 | jest.setTimeout(30000); 6 | 7 | describe('Amqp Retry', () => { 8 | it('retrys are called', async () => { 9 | try { 10 | const result = await from( 11 | amqp.connect({ 12 | hostname: 'notathing', 13 | port: 3444, 14 | }), 15 | ) 16 | .pipe(retry()) 17 | .toPromise(); 18 | expect(false).toBeTruthy(); 19 | } catch (e) { 20 | expect(true).toBeTruthy(); 21 | } 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/__tests__/setup.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv'); 2 | var path = require('path'); 3 | 4 | dotenv.config({ 5 | path: path.resolve(__dirname, '..', '..', '.env'), 6 | }); -------------------------------------------------------------------------------- /src/amqp.constants.ts: -------------------------------------------------------------------------------- 1 | export const AMQP_OPTIONS_PROVIDER = 'AMQP_OPTIONS_PROVIDER'; 2 | export const AMQP_CONNECTION_PROVIDER = 'AMQP_CONNECTION_PROVIDER'; 3 | -------------------------------------------------------------------------------- /src/amqp.module.ts: -------------------------------------------------------------------------------- 1 | import {Module, DynamicModule, Provider, OnModuleDestroy} from '@nestjs/common'; 2 | import { AmqpOptionsInterface, AmqpAsyncOptionsInterface, AmqpOptionsObjectInterface } from './interfaces'; 3 | import { createConnectionToken, createOptionsToken } from './utils/create.tokens'; 4 | import {from} from 'rxjs'; 5 | import * as amqp from 'amqplib'; 6 | import retry from './utils/retry'; 7 | import { AMQP_OPTIONS_PROVIDER, } from './amqp.constants'; 8 | import { ModuleRef } from '@nestjs/core'; 9 | 10 | @Module({}) 11 | export class AmqpModule implements OnModuleDestroy { 12 | private static connectionNames: string[] = []; 13 | 14 | constructor(private readonly moduleRef: ModuleRef) {} 15 | 16 | public static forRoot(options: AmqpOptionsInterface | AmqpOptionsInterface[]): DynamicModule { 17 | 18 | const optionsProviders: Provider[] = []; 19 | const connectionProviders: Provider[] = []; 20 | 21 | options = this.resolveOptions(options); 22 | 23 | optionsProviders.push(this.createOptionsProvider(options)); 24 | 25 | options.forEach(options => { 26 | connectionProviders.push(this.createConnectionProvider(options)); 27 | }); 28 | 29 | return { 30 | module: AmqpModule, 31 | providers: [ 32 | ...optionsProviders, 33 | ...connectionProviders, 34 | ], 35 | exports: connectionProviders, 36 | }; 37 | } 38 | 39 | public static forFeature(): DynamicModule { 40 | return { 41 | module: AmqpModule, 42 | }; 43 | } 44 | 45 | public static forRootAsync(options: AmqpAsyncOptionsInterface): DynamicModule { 46 | 47 | AmqpModule.connectionNames.push(createConnectionToken('default')); 48 | 49 | const connectionProviders = [ 50 | { 51 | provide: createConnectionToken('default'), 52 | useFactory: async (config: AmqpOptionsInterface) => await from(amqp.connect(config)).pipe(retry(options.retrys, options.retryDelay)).toPromise(), 53 | inject: [createOptionsToken('default')], 54 | }, 55 | ]; 56 | 57 | return { 58 | module: AmqpModule, 59 | providers: [ 60 | { 61 | provide: createOptionsToken('default'), 62 | useFactory: options.useFactory, 63 | inject: options.inject || [], 64 | }, 65 | ...connectionProviders, 66 | ], 67 | exports: connectionProviders, 68 | }; 69 | } 70 | 71 | private static createOptionsProvider(options: AmqpOptionsInterface[]): Provider { 72 | const optionsObject: AmqpOptionsObjectInterface = {}; 73 | 74 | if (Array.isArray(options)) { 75 | options.forEach((options) => { 76 | optionsObject[options.name] = options; 77 | }); 78 | } 79 | 80 | return { 81 | provide: AMQP_OPTIONS_PROVIDER, 82 | useValue: optionsObject, 83 | }; 84 | } 85 | 86 | private static createConnectionProvider(options: AmqpOptionsInterface): Provider { 87 | AmqpModule.connectionNames.push(createConnectionToken(options.name)); 88 | return { 89 | provide: createConnectionToken(options.name), 90 | //TODO resolve host url: do I need to? Seems to work aready? Just verify 91 | useFactory: async (config: AmqpOptionsInterface) => await from(amqp.connect(config[options.name ? options.name : 'default'])).pipe(retry(options.retrys, options.retryDelay)).toPromise(), 92 | inject: [AMQP_OPTIONS_PROVIDER], 93 | }; 94 | } 95 | 96 | private static resolveOptions(options: AmqpOptionsInterface|AmqpOptionsInterface[]): AmqpOptionsInterface[] { 97 | if (!Array.isArray(options) && !options.hasOwnProperty('name')) options.name = 'default'; 98 | 99 | if (!Array.isArray(options)) { 100 | options = [options]; 101 | } 102 | 103 | options.forEach((options, key) => { 104 | if (!options.hasOwnProperty('name')) { 105 | options.name = key.toString(); 106 | } 107 | }); 108 | 109 | return options; 110 | } 111 | 112 | async onModuleDestroy(): Promise { 113 | AmqpModule.connectionNames.forEach(async connectionName => { 114 | const connection = this.moduleRef.get(connectionName); 115 | connection !== null && await connection.close(); 116 | }); 117 | 118 | AmqpModule.connectionNames = []; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | import { createConnectionToken } from '../utils/create.tokens'; 2 | import { Inject } from '@nestjs/common'; 3 | 4 | export const InjectAmqpConnection = (name: string = 'default') => { 5 | return Inject(createConnectionToken(name)); 6 | }; 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './amqp.module'; 2 | export * from './interfaces'; 3 | export * from './decorators'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /src/interfaces/amqp.async.options.ts: -------------------------------------------------------------------------------- 1 | import { AmqpOptionsInterface } from './amqp.options'; 2 | 3 | export interface AmqpAsyncOptionsInterface { 4 | inject?: any[]; 5 | useFactory?: ( 6 | ...args: any[] 7 | ) => Promise | AmqpOptionsInterface; 8 | retrys?: number; 9 | retryDelay?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/amqp.options.object.interface.ts: -------------------------------------------------------------------------------- 1 | import { AmqpAsyncOptionsInterface } from './amqp.async.options'; 2 | 3 | export interface AmqpOptionsObjectInterface { 4 | [key: string]: AmqpAsyncOptionsInterface; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/amqp.options.ts: -------------------------------------------------------------------------------- 1 | import { Options } from 'amqplib'; 2 | 3 | export interface AmqpOptionsInterface extends Partial { 4 | name?: string; 5 | retrys?: number; 6 | retryDelay?: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './amqp.options'; 2 | export * from './amqp.async.options'; 3 | export * from './amqp.options.object.interface'; 4 | -------------------------------------------------------------------------------- /src/utils/create.tokens.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AMQP_CONNECTION_PROVIDER, 3 | AMQP_OPTIONS_PROVIDER, 4 | } from '../amqp.constants'; 5 | 6 | export const createOptionsToken = (name: string): string => { 7 | return `${AMQP_OPTIONS_PROVIDER}_${name}`; 8 | }; 9 | 10 | export const createConnectionToken = (name: string): string => { 11 | return `${AMQP_CONNECTION_PROVIDER}_${name}`; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create.tokens'; 2 | -------------------------------------------------------------------------------- /src/utils/retry.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | import { retryWhen, scan, delay } from 'rxjs/operators'; 3 | import { Logger } from '@nestjs/common'; 4 | 5 | export default function retry( 6 | retryAttempts: number = 3, 7 | retryDelay: number = 3000, 8 | ): (source: Observable) => Observable { 9 | return (source: Observable) => 10 | source.pipe( 11 | retryWhen(e => 12 | e.pipe( 13 | scan((acc: number, error: Error) => { 14 | Logger.error( 15 | `Unable to connect to amqp server. Retrying`, 16 | error.stack, 17 | 'AmqpModule', 18 | ); 19 | if (acc + 1 >= retryAttempts) { 20 | throw error; 21 | } 22 | return acc + 1; 23 | }, 0), 24 | delay(retryDelay), 25 | ), 26 | ), 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "noUnusedLocals": false, 7 | "removeComments": false, 8 | "noLib": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": false, 13 | "allowJs": false, 14 | "rootDir": "./src", 15 | "baseUrl": "./", 16 | "outDir": "./dist" 17 | }, 18 | "include": [ 19 | "*.ts", 20 | "**/*.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "./**/*.spec.ts", 25 | "examples" 26 | ] 27 | } --------------------------------------------------------------------------------