├── .DS_Store ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── e2e ├── contracts │ └── mock.contracts.ts ├── docker-compose.yml ├── jest-e2e.json ├── mocks │ ├── api.controller.ts │ ├── config.module.ts │ ├── config.service.ts │ ├── double.pipe.ts │ ├── error-host.handler.ts │ ├── microservice.controller.ts │ └── zero.intercepter.ts └── tests │ ├── rmq.e2e-spec.ts │ ├── rmqAsync.e2e-spec.ts │ └── rmqTest.e2e-spec.ts ├── img ├── new-logo.jpg └── tests.png ├── jest.json ├── lib ├── classes │ ├── rmq-error-handler.class.ts │ ├── rmq-error.class.ts │ ├── rmq-extended-message.class.ts │ ├── rmq-intercepter.class.ts │ └── rmq-pipe.class.ts ├── constants.ts ├── decorators │ ├── rmq-message.decorator.ts │ ├── rmq-pipe.decorator.ts │ ├── rmq-route.decorator.ts │ ├── transform.decorator.ts │ └── validate.decorator.ts ├── emmiters │ └── router.emmiter.ts ├── helpers │ └── logger.ts ├── index.ts ├── interfaces │ ├── queue-meta.interface.ts │ ├── rmq-controller-options.interface.ts │ ├── rmq-error-headers.interface.ts │ ├── rmq-options.interface.ts │ ├── rmq-publish-options.interface.ts │ └── rmq-service.interface.ts ├── option.validator.ts ├── rmq-error.service.ts ├── rmq-metadata.accessor.ts ├── rmq-test.service.ts ├── rmq.explorer.ts ├── rmq.module.ts ├── rmq.service.spec.ts ├── rmq.service.ts └── utils │ └── get-uniq-id.ts ├── package-lock.json ├── package.json └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/nestjs-rmq/3b173a95a053f18c6190a10b04ae9550399563d1/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | "no-empty-function": "off", 20 | "prettier/prettier": ['error', { 21 | 'singleQuote': true, 22 | 'useTabs': true, 23 | 'semi': true, 24 | 'trailingComma': 'all', 25 | 'bracketSpacing': true, 26 | 'printWidth': 100, 27 | 'endOfLine': 'auto' 28 | }], 29 | "@typescript-eslint/no-empty-function": ["off"], 30 | '@typescript-eslint/interface-name-prefix': 'off', 31 | '@typescript-eslint/no-unused-vars': 'off', 32 | '@typescript-eslint/ban-types': 'off', 33 | '@typescript-eslint/explicit-function-return-type': 'off', 34 | '@typescript-eslint/explicit-module-boundary-types': 'off', 35 | '@typescript-eslint/no-explicit-any': 'off', 36 | '@typescript-eslint/no-namespace': 'off', 37 | "prettier/prettier": [ 38 | "error", 39 | { 40 | "endOfLine": "auto" 41 | }, 42 | ], 43 | }, 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: 20 16 | registry-url: https://registry.npmjs.org/ 17 | - run: npm install 18 | - run: npm publish 19 | env: 20 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /.idea 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /tsconfig.json 2 | /node_modules 3 | /lib 4 | /img -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": true, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "trailingComma": "es5", 8 | "tabWidth": 4 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[typescript]": { 3 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 4 | }, 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | } 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | ## 2.14.0 4 | 5 | - change access modifier of is RMQError (thx Jorbanog) 6 | 7 | ## 2.13.0 8 | 9 | - Fix error when null response was ignored 10 | 11 | ## 2.12.0 12 | 13 | - Use native UUID 14 | - Update dependencies 15 | 16 | ## 2.11.0 17 | 18 | - Update class-validator 19 | 20 | ## 2.10.0 21 | 22 | - Overriding intercepters 23 | 24 | ## 2.9.0 25 | 26 | - Updated to Nest 10 27 | 28 | ## 2.8.1 29 | 30 | - Throw error on wrong credentials (thx @ponomarevkonst) 31 | 32 | ## 2.8.0 33 | 34 | - Updated to nestjs 9 35 | 36 | ## 2.7.2 37 | 38 | - Nack message if no ROUTE 39 | 40 | ## 2.7.1 41 | 42 | - Double ACK fix, when a provider is exported multiple times (thx @falahati) 43 | 44 | ## 2.7.0 45 | 46 | - Added autoBindingRoutes option 47 | 48 | ## 2.6.2 49 | 50 | - Update dependencies 51 | 52 | ## 2.6.1 53 | 54 | - Added nack for ERROR_NO_ROUTE 55 | 56 | ## 2.6.0 57 | 58 | - Updated dependencies 59 | - Migrated tslint to eslint 60 | - Added @RMQTransform() decorator to transform incomming message 61 | - Changed @Validate() decorator to @RMQValidate(). @Validate() will be depricated. 62 | 63 | ## 2.5.1 64 | 65 | - Updated RQMColorLogger performance and changed debug message content. 66 | 67 | ## 2.5.0 68 | 69 | - Added support for random queue name and additional queueOprions. 70 | - Added deprecation warning. 71 | - Removed deprecated @RMQController 72 | 73 | ## 2.4.0 74 | 75 | - Added TLS/SSL support 76 | 77 | ## 2.3.1 78 | 79 | - Fixed import 80 | 81 | ## 2.3.0 82 | 83 | - Added RMQModule.forTest() to test your routes in unit or e2e test 84 | - New RMQTestService lets you mock RMQ replies and errors 85 | 86 | ## 2.2.0 87 | 88 | - Added pattern mathing for routes with `*` and `#` 89 | - Added unit tests 90 | - Updated e2e test 91 | 92 | ## 2.1.1 93 | 94 | - Compatible with other amqp libraries (thx @Kevalin) 95 | 96 | ## 2.1.0 97 | 98 | - Migration to NestJS 8 99 | - Added port and vhost options (thx @Kevalin) 100 | - Update dependencies 101 | 102 | ## 2.0.6 103 | 104 | - Update dependencies 105 | 106 | ## 2.0.5 107 | 108 | - Fixed ack on validation error 109 | - Added warning message on RMQRoute without queue 110 | 111 | ## 2.0.4 112 | 113 | - Fixed validation stom request 114 | 115 | ## 2.0.3 116 | 117 | - Added RMQRoute mapping log on start 118 | - Added topic name in timeout error 119 | 120 | ## 2.0.2 121 | 122 | - Fix race condition on send() after start 123 | 124 | ## 2.0.1 125 | 126 | - Fix validate decoration order 127 | 128 | ## 2.0.0 129 | 130 | - Moved to NestJS DI system 131 | - Removed @RMQController (deprecation warning) 132 | - Initialization refactor. 133 | - Added msgFactory to @RMQRoute 134 | - Changed msgFactory interface. 135 | - MsgFactory e2e test 136 | 137 | ## 1.16.0 138 | 139 | - Added warning message if service name is not specified (thx @milovidov983) 140 | - Client and subscription channels splitted for performance (thx @milovidov983) 141 | 142 | ## 1.15.0 143 | 144 | - Added nack method (thx @mikelavigne) 145 | - Added more publish options (thx @mikelavigne) 146 | - Added exchange options (thx @mikelavigne) 147 | 148 | ## 1.14.0 149 | 150 | - Added Extended message and debug method 151 | - Added types to package 152 | 153 | ## 1.13.2 154 | 155 | - Added timestamp to message 156 | - Added appId and timestamp to notify 157 | 158 | ## 1.13.0 159 | 160 | - Added manual message acknowledgement 161 | - Added `@RMQMessage` decorator to get message metadata 162 | - Updated dependencies 163 | 164 | ## 1.12.0 165 | 166 | - Added healthCheck method 167 | 168 | ## 1.11.0 169 | 170 | - Added forRootAsync method 171 | 172 | ## 1.10.1 173 | 174 | - Added heartbeat option. 175 | 176 | ## 1.9.0 177 | 178 | - Custom message factory inside controller decorator (thx to mjarmoc) 179 | 180 | ## 1.8.0 181 | 182 | - Messages publishing options exposed (thx to mjarmoc) 183 | 184 | ## 1.7.1 185 | 186 | - Fixed event emmitor leak (thx to mjarmoc) 187 | 188 | ## 1.7.0 189 | 190 | - Fixed reconnection bug 191 | - Async init all modules loaded (thx to mjarmoc) 192 | 193 | ## 1.6.0 194 | 195 | - Custom logger injection (thx to @minenkom) 196 | 197 | ## 1.5.2 198 | 199 | - Fixed double logging 200 | 201 | ## 1.5.1 202 | 203 | - Fixed ack race condition 204 | - Added tests 205 | 206 | ## 1.5.0 207 | 208 | - Added error handler (thx to @mjarmoc) 209 | - Added more debug info to error message (thx to @mjarmoc) 210 | - Refactoring 211 | - Fixed error message ack with notify command 212 | 213 | ## 1.4.6 214 | 215 | - Fixed ack none RPC messages 216 | - Fixed logs 217 | - Fixed connection with messages already in queue 218 | - Added error if RPC method returns undefined 219 | 220 | ## 1.4.4 221 | 222 | - Fix await consuming replyQueue 223 | 224 | ## 1.4.3 225 | 226 | - Added message ack 227 | 228 | ## 1.4.0 229 | 230 | - Added -x-status-code and RMQError class 231 | 232 | ## 1.3.3 233 | 234 | - Fixed no RMQRoute issue, added error message 235 | 236 | ## 1.3.0 237 | 238 | - Added validation decorator. 239 | 240 | ## 1.2.0 241 | 242 | - Added global intercepters to deal with responses and errors 243 | 244 | ## 1.1.0 245 | 246 | - Added @RMQPipe to transform messages 247 | - Added global middleware option 248 | 249 | ## 1.0.0 250 | 251 | - Changed for new pattern 252 | 253 | ## 0.1.2 254 | 255 | - Added additional check for callback() function 256 | - Moved events to constants 257 | 258 | ## 0.1.1 259 | 260 | - Added reconnection in client and server, if your RabbitMQ instanse is down. 261 | - Support for multiple urls for cluster usage. 262 | 263 | ## 0.1.0 264 | 265 | - First stable version of the package 266 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Anton Larichev 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 | # NestJS - RabbitMQ custom strategy 2 | 3 | ![alt cover](https://github.com/AlariCode/nestjs-rmq/raw/master/img/new-logo.jpg) 4 | 5 | **More NestJS libs on [purpleschool.ru](https://purpleschool.ru)** 6 | 7 | [![npm version](https://badgen.net/npm/v/nestjs-rmq)](https://www.npmjs.com/package/nestjs-rmq) 8 | [![npm version](https://badgen.net/npm/license/nestjs-rmq)](https://www.npmjs.com/package/nestjs-rmq) 9 | [![npm version](https://badgen.net/github/open-issues/AlariCode/nestjs-rmq)](https://github.com/AlariCode/nestjs-rmq/issues) 10 | [![npm version](https://badgen.net/github/prs/AlariCode/nestjs-rmq)](https://github.com/AlariCode/nestjs-rmq/pulls) 11 | 12 | This library will take care of RPC requests and messaging between microservices. It is easy to bind to our existing controllers to RMQ routes. This version is only for NestJS. 13 | 14 | **Updated for NestJS 9!** 15 | 16 | ## Why use this over RabbitMQ transport in NestJS docs? 17 | 18 | - Support for RMQ queue patterns with \* and #. 19 | - Using exchanges with topic bindings rather the direct queue sending. 20 | - Additional `forTest()` method for emulating messages in unit or e2e tests without needing of RabbitMQ instance. 21 | - Additional decorators for getting info out of messages. 22 | - Support for class-validator decorators. 23 | - Real production usage with more than 100 microservices. 24 | 25 | ## Start 26 | 27 | First, install the package: 28 | 29 | ```bash 30 | npm i nestjs-rmq 31 | ``` 32 | 33 | Setup your connection in root module: 34 | 35 | ```typescript 36 | import { RMQModule } from 'nestjs-rmq'; 37 | 38 | @Module({ 39 | imports: [ 40 | RMQModule.forRoot({ 41 | exchangeName: configService.get('AMQP_EXCHANGE'), 42 | connections: [ 43 | { 44 | login: configService.get('AMQP_LOGIN'), 45 | password: configService.get('AMQP_PASSWORD'), 46 | host: configService.get('AMQP_HOST'), 47 | }, 48 | ], 49 | }), 50 | ], 51 | }) 52 | export class AppModule {} 53 | ``` 54 | 55 | In forRoot() you pass connection options: 56 | 57 | - **exchangeName** (string) - Exchange that will be used to send messages to. 58 | - **connections** (Object[]) - Array of connection parameters. You can use RMQ cluster by using multiple connections. 59 | 60 | Additionally, you can use optional parameters: 61 | 62 | - **queueName** (string) - Queue name which your microservice would listen and bind topics specified in '@RMQRoute' decorator to this queue. If this parameter is not specified, your microservice could send messages and listen to reply or send notifications, but it couldn't get messages or notifications from other services. If you use empty string, RabbitMQ will generate name for you. 63 | Example: 64 | 65 | ```typescript 66 | { 67 | exchangeName: 'my_exchange', 68 | connections: [ 69 | { 70 | login: 'admin', 71 | password: 'admin', 72 | host: 'localhost', 73 | }, 74 | ], 75 | queueName: 'my-service-queue', 76 | } 77 | ``` 78 | 79 | - **connectionOptions** (object) - Additional connection options. You can read more [here](http://www.squaremobius.net/amqp.node/). 80 | - **prefetchCount** (boolean) - You can read more [here](http://www.squaremobius.net/amqp.node/). 81 | - **isGlobalPrefetchCount** (boolean) - You can read more [here](http://www.squaremobius.net/amqp.node/). 82 | - **queueOptions** (object) - options for created queue. 83 | - **reconnectTimeInSeconds** (number) - Time in seconds before reconnection retry. Default is 5 seconds. 84 | - **heartbeatIntervalInSeconds** (number) - Interval to send heartbeats to broker. Defaults to 5 seconds. 85 | - **queueArguments** (!!! deprecated. Use queueOptions instead) - You can read more about queue parameters [here](https://www.rabbitmq.com/parameters.html). 86 | - **messagesTimeout** (number) - Number of milliseconds 'post' method will wait for the response before a timeout error. Default is 30 000. 87 | - **isQueueDurable** (!!! deprecated. Use queueOptions instead) - Makes created queue durable. Default is true. 88 | - **isExchangeDurable** (!!! deprecated. Use exchangeOptions instead) - Makes created exchange durable. Default is true. 89 | - **exchangeOptions** (Options.AssertExchange) - You can read more about exchange options [here](squaremobius.net/amqp.node/channel_api.html#channel_assertExchange). 90 | - **logMessages** (boolean) - Enable printing all sent and recieved messages in console with its route and content. Default is false. 91 | - **logger** (LoggerService) - Your custom logger service that implements `LoggerService` interface. Compatible with Winston and other loggers. 92 | - **middleware** (array) - Array of middleware functions that extends `RMQPipeClass` with one method `transform`. They will be triggered right after recieving message, before pipes and controller method. Trigger order is equal to array order. 93 | - **errorHandler** (class) - custom error handler for dealing with errors from replies, use `errorHandler` in module options and pass class that extends `RMQErrorHandler`. 94 | - **serviceName** (string) - service name for debugging. 95 | - **autoBindingRoutes** (boolean) - set false you want to manage route binding manualy. Default to `true`. 96 | 97 | ```typescript 98 | class LogMiddleware extends RMQPipeClass { 99 | async transfrom(msg: Message): Promise { 100 | console.log(msg); 101 | return msg; 102 | } 103 | } 104 | ``` 105 | 106 | - **intercepters** (array) - Array of intercepter functions that extends `RMQIntercepterClass` with one method `intercept`. They will be triggered before replying on any message. Trigger order is equal to array order. 107 | 108 | ```typescript 109 | export class MyIntercepter extends RMQIntercepterClass { 110 | async intercept(res: any, msg: Message, error: Error): Promise { 111 | // res - response body 112 | // msg - initial message we are replying to 113 | // error - error if exists or null 114 | return res; 115 | } 116 | } 117 | ``` 118 | 119 | Config example with middleware and intercepters: 120 | 121 | ```typescript 122 | import { RMQModule } from 'nestjs-rmq'; 123 | 124 | @Module({ 125 | imports: [ 126 | RMQModule.forRoot({ 127 | exchangeName: configService.get('AMQP_EXCHANGE'), 128 | connections: [ 129 | { 130 | login: configService.get('AMQP_LOGIN'), 131 | password: configService.get('AMQP_PASSWORD'), 132 | host: configService.get('AMQP_HOST'), 133 | }, 134 | ], 135 | middleware: [LogMiddleware], 136 | intercepters: [MyIntercepter], 137 | }), 138 | ], 139 | }) 140 | export class AppModule {} 141 | ``` 142 | 143 | ## Async initialization 144 | 145 | If you want to inject dependency into RMQ initialization like Configuration service, use `forRootAsync`: 146 | 147 | ```typescript 148 | import { RMQModule } from 'nestjs-rmq'; 149 | import { ConfigModule } from './config/config.module'; 150 | import { ConfigService } from './config/config.service'; 151 | 152 | @Module({ 153 | imports: [ 154 | RMQModule.forRootAsync({ 155 | imports: [ConfigModule], 156 | inject: [ConfigService], 157 | useFactory: (configService: ConfigService) => { 158 | return { 159 | exchangeName: 'test', 160 | connections: [ 161 | { 162 | login: 'guest', 163 | password: 'guest', 164 | host: configService.getHost(), 165 | }, 166 | ], 167 | queueName: 'test', 168 | }; 169 | }, 170 | }), 171 | ], 172 | }) 173 | export class AppModule {} 174 | ``` 175 | 176 | - **useFactory** - returns `IRMQServiceOptions`. 177 | - **imports** - additional modules for configuration. 178 | - **inject** - additional services for usage inside useFactory. 179 | 180 | ## Sending messages 181 | 182 | To send message with RPC topic use send() method in your controller or service: 183 | 184 | ```typescript 185 | @Injectable() 186 | export class ProxyUpdaterService { 187 | constructor(private readonly rmqService: RMQService) {} 188 | 189 | myMethod() { 190 | this.rmqService.send('sum.rpc', [1, 2, 3]); 191 | } 192 | } 193 | ``` 194 | 195 | This method returns a Promise. First type - is a type you send, and the second - you recive. 196 | 197 | - 'sum.rpc' - name of subscription topic that you are sending to. 198 | - [1, 2, 3] - data payload. 199 | To get a reply: 200 | 201 | ```typescript 202 | this.rmqService.send('sum.rpc', [1, 2, 3]) 203 | .then(reply => { 204 | //... 205 | }) 206 | .catch(error: RMQError => { 207 | //... 208 | }); 209 | ``` 210 | 211 | Also you can use send options: 212 | 213 | ```typescript 214 | this.rmqService.send('sum.rpc', [1, 2, 3], { 215 | expiration: 1000, 216 | priority: 1, 217 | persistent: true, 218 | timeout: 30000, 219 | }); 220 | ``` 221 | 222 | - **expiration** - if supplied, the message will be discarded from a queue once it’s been there longer than the given number of milliseconds. 223 | - **priority** - a priority for the message. 224 | - **persistent** - if truthy, the message will survive broker restarts provided it’s in a queue that also survives restarts. 225 | - **timeout** - if supplied, the message will have its own timeout. 226 | 227 | If you want to just notify services: 228 | 229 | ```typescript 230 | const a = this.rmqService.notify('info.none', 'My data'); 231 | ``` 232 | 233 | This method returns a Promise. 234 | 235 | - 'info.none' - name of subscription topic that you are notifying. 236 | - 'My data' - data payload. 237 | 238 | ## Recieving messages 239 | 240 | To listen for messages bind your controller or service methods to subscription topics with **RMQRoute()** decorator: 241 | 242 | ```typescript 243 | export class AppController { 244 | //... 245 | 246 | @RMQRoute('sum.rpc') 247 | sum(numbers: number[]): number { 248 | return numbers.reduce((a, b) => a + b, 0); 249 | } 250 | 251 | @RMQRoute('info.none') 252 | info(data: string) { 253 | console.log(data); 254 | } 255 | } 256 | ``` 257 | 258 | Return value will be send back as a reply in RPC topic. In 'sum.rpc' example it will send sum of array values. And sender will get `6`: 259 | 260 | ```typescript 261 | this.rmqService.send('sum.rpc', [1, 2, 3]).then((reply) => { 262 | // reply: 6 263 | }); 264 | ``` 265 | 266 | Each '@RMQRoute' topic will be automatically bound to queue specified in 'queueName' option. If you want to return an Error just throw it in your method. To set '-x-status-code' use custom RMQError class. 267 | 268 | ```typescript 269 | @RMQRoute('my.rpc') 270 | myMethod(numbers: number[]): number { 271 | //... 272 | throw new RMQError('Error message', 2); 273 | throw new Error('Error message'); 274 | //... 275 | } 276 | ``` 277 | 278 | ## Message patterns 279 | 280 | With exchange type `topic` you can use message patterns to subscribe to messages that corresponds to that pattern. You can use special symbols: 281 | 282 | - `*` - (star) can substitute for exactly one word. 283 | - `#`- (hash) can substitute for zero or more words. 284 | 285 | For example: 286 | 287 | - Pattern `*.*.rpc` will match `my.own.rpc` or `any.other.rpc` and will not match `this.is.cool.rpc` or `my.rpc`. 288 | - Pattern `compute.#` will match `compute.this.equation.rpc` and will not `do.compute.anything`. 289 | 290 | To subscribe to pattern, use it as route: 291 | 292 | ```typescript 293 | import { RMQRoute } from 'nestjs-rmq'; 294 | 295 | @RMQRoute('*.*.rpc') 296 | myMethod(): number { 297 | // ... 298 | } 299 | ``` 300 | 301 | > Note: If two routes patterns matches message topic, only the first will be used. 302 | 303 | ## Getting message metadata 304 | 305 | To get more information from message (not just content) you can use `@RMQMessage` parameter decorator: 306 | 307 | ```typescript 308 | import { RMQRoute, Validate, RMQMessage, ExtendedMessage } from 'nestjs-rmq'; 309 | 310 | @RMQRoute('my.rpc') 311 | myMethod(data: myClass, @RMQMessage msg: ExtendedMessage): number { 312 | // ... 313 | } 314 | ``` 315 | 316 | You can get all message properties that RMQ gets. Example: 317 | 318 | ```json 319 | { 320 | "fields": { 321 | "consumerTag": "amq.ctag-1CtiEOM8ioNFv-bzbOIrGg", 322 | "deliveryTag": 2, 323 | "redelivered": false, 324 | "exchange": "test", 325 | "routingKey": "appid.rpc" 326 | }, 327 | "properties": { 328 | "contentType": "undefined", 329 | "contentEncoding": "undefined", 330 | "headers": {}, 331 | "deliveryMode": "undefined", 332 | "priority": "undefined", 333 | "correlationId": "ce7df8c5-913c-2808-c6c2-e57cfaba0296", 334 | "replyTo": "amq.rabbitmq.reply-to.g2dkABNyYWJiaXRAOTE4N2MzYWMyM2M0AAAenQAAAAAD.bDT8S9ZIl5o3TGjByqeh5g==", 335 | "expiration": "undefined", 336 | "messageId": "undefined", 337 | "timestamp": "undefined", 338 | "type": "undefined", 339 | "userId": "undefined", 340 | "appId": "test-service", 341 | "clusterId": "undefined" 342 | }, 343 | "content": "" 344 | } 345 | ``` 346 | 347 | ## TSL/SSL support 348 | 349 | To configure certificates and learn why do you need it, [read here](https://www.rabbitmq.com/ssl.html). 350 | 351 | To use `amqps` connection: 352 | 353 | ```typescript 354 | RMQModule.forRoot({ 355 | exchangeName: 'test', 356 | connections: [ 357 | { 358 | protocol: RMQ_PROTOCOL.AMQPS, // new 359 | login: 'admin', 360 | password: 'admin', 361 | host: 'localhost', 362 | }, 363 | ], 364 | connectionOptions: { 365 | cert: fs.readFileSync('clientcert.pem'), 366 | key: fs.readFileSync('clientkey.pem'), 367 | passphrase: 'MySecretPassword', 368 | ca: [fs.readFileSync('cacert.pem')] 369 | } // new 370 | }), 371 | ``` 372 | 373 | This is the basic example with reading files, but you can do however you want. `cert`, `key` and `ca` must be Buffers. Notice: `ca` is array. If you don't need keys, just use `RMQ_PROTOCOL.AMQPS` protocol. 374 | 375 | To use it with `pkcs12` files: 376 | 377 | ```typescript 378 | connectionOptions: { 379 | pfx: fs.readFileSync('clientcertkey.p12'), 380 | passphrase: 'MySecretPassword', 381 | ca: [fs.readFileSync('cacert.pem')] 382 | }, 383 | ``` 384 | 385 | ## Manual message Ack/Nack 386 | 387 | If you want to use your own [ack](https://www.squaremobius.net/amqp.node/channel_api.html#channel_nack)/[nack](https://www.squaremobius.net/amqp.node/channel_api.html#channel_ack) logic, you can set manual acknowledgement to `@RMQRoute`. Than in any place you have to manually ack/nack message that you get with `@RMQMessage`. 388 | 389 | ```typescript 390 | import { RMQRoute, Validate, RMQMessage, ExtendedMessage, RMQService } from 'nestjs-rmq'; 391 | 392 | @Controller() 393 | export class MyController { 394 | constructor(private readonly rmqService: RMQService) {} 395 | 396 | @RMQRoute('my.rpc', { manualAck: true }) 397 | myMethod(data: myClass, @RMQMessage msg: ExtendedMessage): number { 398 | // Any logic goes here 399 | this.rmqService.ack(msg); 400 | // Any logic goes here 401 | } 402 | 403 | @RMQRoute('my.other-rpc', { manualAck: true }) 404 | myOtherMethod(data: myClass, @RMQMessage msg: ExtendedMessage): number { 405 | // Any logic goes here 406 | this.rmqService.nack(msg); 407 | // Any logic goes here 408 | } 409 | } 410 | ``` 411 | 412 | ## Send debug information to error or log 413 | 414 | `ExtendedMessage` has additional method to get all data from message to debug it. Also it serializes content and hides Buffers, because they can be massive. Then you can put all your debug info into Error or log it. 415 | 416 | ```typescript 417 | import { RMQRoute, Validate, RMQMessage, ExtendedMessage, RMQService } from 'nestjs-rmq'; 418 | 419 | @Controller() 420 | export class MyController { 421 | constructor(private readonly rmqService: RMQService) {} 422 | 423 | @RMQRoute('my.rpc') 424 | myMethod(data: myClass, @RMQMessage msg: ExtendedMessage): number { 425 | // ... 426 | console.log(msg.getDebugString()); 427 | // ... 428 | } 429 | } 430 | ``` 431 | 432 | You will get info about message, field and properties: 433 | 434 | ```json 435 | { 436 | "fields": { 437 | "consumerTag": "amq.ctag-Q-l8A4Oh76cUkIKbHWNZzA", 438 | "deliveryTag": 4, 439 | "redelivered": false, 440 | "exchange": "test", 441 | "routingKey": "debug.rpc" 442 | }, 443 | "properties": { 444 | "headers": {}, 445 | "correlationId": "388236ad-6f01-3de5-975d-f9665b73de33", 446 | "replyTo": "amq.rabbitmq.reply-to.g1hkABNyYWJiaXRANzQwNDVlYWQ5ZTgwAAAG2AAAAABfmnkW.9X12ySrcM6BOXpGXKkR+Yg==", 447 | "timestamp": 1603959908996, 448 | "appId": "test-service" 449 | }, 450 | "message": { 451 | "prop1": [1], 452 | "prop2": "Buffer - length 11" 453 | } 454 | } 455 | ``` 456 | 457 | ## Customizing massage with msgFactory 458 | 459 | `@RMQRoute` handlers accepts a single parameter `msg` which is a ampq `message.content` parsed as a JSON. You may want to add additional custom layer to that message and change the way handler is called. For example, you may want to structure your message with two different parts: payload (containing actual data) and appId (containing request applicationId) and process them explicitly in your handler. 460 | 461 | To do that, you may pass a param to the `RMQRoute` a custom message factory `msgFactory?: (msg: Message) => any;`. 462 | 463 | The default msgFactory: 464 | 465 | ```typescript 466 | @RMQRoute('topic', { 467 | msgFactory: (msg: Message) => JSON.parse(msg.content.toString()) 468 | }) 469 | ``` 470 | 471 | Custom msgFactory that returns additional argument (sender appId) and change request: 472 | 473 | ```typescript 474 | @RMQRoute(CustomMessageFactoryContracts.topic, { 475 | msgFactory: (msg: Message) => { 476 | const content: CustomMessageFactoryContracts.Request = JSON.parse(msg.content.toString()); 477 | content.num = content.num * 2; 478 | return [content, msg.properties.appId]; 479 | } 480 | }) 481 | customMessageFactory({ num }: CustomMessageFactoryContracts.Request, appId: string): CustomMessageFactoryContracts.Response { 482 | return { num, appId }; 483 | } 484 | ``` 485 | 486 | ## Validating data 487 | 488 | NestJS-rmq uses [class-validator](https://github.com/typestack/class-validator) to validate incoming data. To use it, decorate your route method with `RMQValidate`: 489 | 490 | ```typescript 491 | import { RMQRoute, RMQValidate } from 'nestjs-rmq'; 492 | 493 | @RMQValidate() 494 | @RMQRoute('my.rpc') 495 | myMethod(data: myClass): number { 496 | // ... 497 | } 498 | ``` 499 | 500 | Where `myClass` is data class with validation decorators: 501 | 502 | ```typescript 503 | import { IsString, MinLength, IsNumber } from 'class-validator'; 504 | 505 | export class myClass { 506 | @MinLength(2) 507 | @IsString() 508 | name: string; 509 | 510 | @IsNumber() 511 | age: string; 512 | } 513 | ``` 514 | 515 | If your input data will be invalid, the library will send back an error without even entering your method. This will prevent you from manually validating your data inside route. You can check all available validators [here](https://github.com/typestack/class-validator). 516 | 517 | ## Transforming data 518 | 519 | NestJS-rmq uses [class-transformer](https://github.com/typestack/class-transformer) to transform incoming data. To use it, decorate your route method with `RMQTransform`: 520 | 521 | ```typescript 522 | import { RMQRoute, RMQTransform } from 'nestjs-rmq'; 523 | 524 | @RMQTransform() 525 | @RMQValidate() 526 | @RMQRoute('my.rpc') 527 | myMethod(data: myClass): number { 528 | // ... 529 | } 530 | ``` 531 | 532 | Where `myClass` is data class with transformation decorators: 533 | 534 | ```typescript 535 | import { Type } from 'class-transformer'; 536 | import { IsDate } from 'class-validator'; 537 | 538 | export class myClass { 539 | @IsDate() 540 | @Type(() => Date) 541 | date: Date; 542 | } 543 | ``` 544 | 545 | After this you can use `data.date` in your controller as Date object and not a string. You can check class-validator docs [here](https://github.com/typestack/class-transformer). You can use transformation and validation at the same time - first transformation will be applied and then validation. 546 | 547 | ## Using pipes 548 | 549 | To intercept any message to any route, you can use `@RMQPipe` decorator: 550 | 551 | ```typescript 552 | import { RMQRoute, RMQPipe } from 'nestjs-rmq'; 553 | 554 | @RMQPipe(MyPipeClass) 555 | @RMQRoute('my.rpc') 556 | myMethod(numbers: number[]): number { 557 | //... 558 | } 559 | ``` 560 | 561 | where `MyPipeClass` extends `RMQPipeClass` with one method `transform`: 562 | 563 | ```typescript 564 | class MyPipeClass extends RMQPipeClass { 565 | async transfrom(msg: Message): Promise { 566 | // do something 567 | return msg; 568 | } 569 | } 570 | ``` 571 | 572 | ## Using RMQErrorHandler 573 | 574 | If you want to use custom error handler for dealing with errors from replies, use `errorHandler` in module options and pass class that extends `RMQErrorHandler`: 575 | 576 | ```typescript 577 | class MyErrorHandler extends RMQErrorHandler { 578 | public static handle(headers: IRmqErrorHeaders): Error | RMQError { 579 | // do something 580 | return new RMQError( 581 | headers['-x-error'], 582 | headers['-x-type'], 583 | headers['-x-status-code'], 584 | headers['-x-data'], 585 | headers['-x-service'], 586 | headers['-x-host'] 587 | ); 588 | } 589 | } 590 | ``` 591 | 592 | ## HealthCheck 593 | 594 | RQMService provides additional method to check if you are still connected to RMQ. Although reconnection is automatic, you can provide wrong credentials and reconnection will not help. So to check connection for Docker healthCheck use: 595 | 596 | ```typescript 597 | const isConnected = this.rmqService.healthCheck(); 598 | ``` 599 | 600 | If `isConnected` equals `true`, you are successfully connected. 601 | 602 | ## Disconnecting 603 | 604 | If you want to close connection, for example, if you are using RMQ in testing tools, use `disconnect()` method; 605 | 606 | ## Unit and E2e tests 607 | 608 | ### Using in tests 609 | 610 | RMQ library supports using RMQ module in your test suites without needing RabbitMQ instance. To use library in tests, use `forTest` method in module. 611 | 612 | ```typescript 613 | import { RMQTestService } from 'nestjs-rmq'; 614 | 615 | let rmqService: RMQTestService; 616 | 617 | beforeAll(async () => { 618 | const apiModule = await Test.createTestingModule({ 619 | imports: [RMQModule.forTest({})], 620 | controllers: [MicroserviceController], 621 | }).compile(); 622 | api = apiModule.createNestApplication(); 623 | await api.init(); 624 | 625 | rmqService = apiModule.get(RMQService); 626 | }); 627 | ``` 628 | 629 | You can pass any options you pass in normal `forRoot` (except `errorHandler`). 630 | 631 | From module, you will get `rmqService` which is similar to normal service, with two additional methods: 632 | 633 | - `triggerRoute` - trigger your RMQRoute, simulating incoming message. 634 | - `mockReply` - mock reply if you are using `send` method. 635 | - `mockError` - mock error if you are using `send` method. 636 | 637 | ### triggerRoute 638 | 639 | Emulates message received buy your RMQRoute. 640 | 641 | ```typescript 642 | const { result } = await rmqService.triggerRoute(topic, data); 643 | ``` 644 | 645 | - `topic` - topic, that you want to trigger (pattern supported). 646 | - `data` - data to send in your method. 647 | 648 | ### mockReply 649 | 650 | If your service needs to send data to other microservice, you can emulate its reply with: 651 | 652 | ```typescript 653 | rmqService.mockReply(topic, res); 654 | ``` 655 | 656 | - `topic` - all messages sent to this topic will be mocked. 657 | - `res` - mocked response data. 658 | 659 | After this, all `rmqService.send(topic, { ... })` calls will return `res` data. 660 | 661 | ### mockError 662 | 663 | If your service needs to send data to other microservice, you can emulate its error with: 664 | 665 | ```typescript 666 | rmqService.mockError(topic, error); 667 | ``` 668 | 669 | - `topic` - all messages sent to this topic will be mocked. 670 | - `error` - error that `send` method will throw. 671 | 672 | After this, all `rmqService.send(topic, { ... })` calls will throw `error`. 673 | 674 | ## Contributing 675 | 676 | For e2e tests you need to install Docker in your machine and start RabbitMQ docker image with `docker-compose.yml` in `e2e` folder: 677 | 678 | ``` 679 | docker-compose up -d 680 | ``` 681 | 682 | Then change IP in tests to `localhost` and run tests with: 683 | 684 | ``` 685 | npm run test:e2e 686 | ``` 687 | 688 | ![alt cover](https://github.com/AlariCode/nestjs-rmq/raw/master/img/tests.png) 689 | 690 | For unit tests just run: 691 | 692 | ``` 693 | npm run test 694 | ``` 695 | 696 | ### Migrating from version 1 697 | 698 | New version of nestjs-rmq contains minor breaking changes, and is simple to migrate to. 699 | 700 | - `@RMQController` decorator is deprecated. 701 | You will get warning if you continue to use it, and it will be deleted in future versions. 702 | You can safely remove it from a controller or service. `msgFactory` inside options will not be functional anymore. You have to move it to `@RMQRoute` 703 | - `msgFactory` changed its interface from 704 | 705 | ```typescript 706 | msgFactory?: (msg: Message, topic: IRouteMeta) => any[]; 707 | ``` 708 | 709 | to 710 | 711 | ```typescript 712 | msgFactory?: (msg: Message) => any[]; 713 | ``` 714 | 715 | because all `IRouteMeta` already contained in `Message`. 716 | 717 | - `msgFactory` can be passed to `@RMQRoute` instead of `@RMQController` 718 | -------------------------------------------------------------------------------- /e2e/contracts/mock.contracts.ts: -------------------------------------------------------------------------------- 1 | import { Type } from 'class-transformer'; 2 | import { IsNumber, IsString } from 'class-validator'; 3 | 4 | export namespace SumContracts { 5 | export const topic: string = 'sum.rpc'; 6 | export class Request { 7 | @IsNumber( 8 | {}, 9 | { 10 | each: true, 11 | } 12 | ) 13 | arrayToSum: number[]; 14 | } 15 | export class Response { 16 | result: number; 17 | } 18 | } 19 | 20 | export namespace MultiplyContracts { 21 | export const topic: string = 'multiply.rpc'; 22 | export class Request { 23 | @IsNumber( 24 | {}, 25 | { 26 | each: true, 27 | } 28 | ) 29 | arrayToMultiply: number[]; 30 | } 31 | export class Response { 32 | result: number; 33 | } 34 | } 35 | 36 | export namespace DivideContracts { 37 | export const topic: string = 'divide.rpc'; 38 | export class Request { 39 | @IsNumber() 40 | first: number; 41 | 42 | @IsNumber() 43 | second: number; 44 | } 45 | export class Response { 46 | result: number; 47 | } 48 | } 49 | 50 | export namespace NotificationContracts { 51 | export const topic: string = 'notification.none'; 52 | export class Request { 53 | @IsString() 54 | message: string; 55 | } 56 | } 57 | 58 | export namespace TimeOutContracts { 59 | export const topic: string = 'timeout.rpc'; 60 | } 61 | 62 | export namespace AppIdContracts { 63 | export const topic: string = 'appid.rpc'; 64 | export class Response { 65 | appId: string; 66 | } 67 | } 68 | 69 | export namespace ManualAckContracts { 70 | export const topic: string = 'manualAck.rpc'; 71 | export class Response { 72 | appId: string; 73 | } 74 | } 75 | 76 | export namespace DebugContracts { 77 | export const topic: string = 'debug.rpc'; 78 | export class Request { 79 | prop1: number[]; 80 | prop2: Buffer; 81 | } 82 | export class Response { 83 | debugString: string; 84 | } 85 | } 86 | 87 | export namespace CustomMessageFactoryContracts { 88 | export const topic: string = 'custom-message-factory.rpc'; 89 | export class Request { 90 | num: number; 91 | } 92 | export class Response { 93 | num: number; 94 | appId: string; 95 | } 96 | } 97 | 98 | export namespace PatternStarContracts { 99 | export const topic: string = '*.*.star'; 100 | export class Request { 101 | num: number; 102 | } 103 | export class Response { 104 | num: number; 105 | } 106 | } 107 | 108 | export namespace PatternHashContracts { 109 | export const topic: string = '#.hash'; 110 | export class Request { 111 | num: number; 112 | } 113 | export class Response { 114 | num: number; 115 | } 116 | } 117 | 118 | export namespace TransformContracts { 119 | export const topic: string = 'transform.rpc'; 120 | export class Request { 121 | @Type(() => Date) 122 | date: Date; 123 | } 124 | export class Response { 125 | res: number; 126 | type: string; 127 | } 128 | } -------------------------------------------------------------------------------- /e2e/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | rmq: 4 | image: rabbitmq:3-management 5 | restart: always 6 | ports: 7 | - "15672:15672" 8 | - "5672:5672" 9 | -------------------------------------------------------------------------------- /e2e/jest-e2e.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "moduleFileExtensions": [ 4 | "ts", 5 | "tsx", 6 | "js", 7 | "json" 8 | ], 9 | "transform": { 10 | "^.+\\.tsx?$": "ts-jest" 11 | }, 12 | "testRegex": "/tests/.*\\.(e2e-test|e2e-spec).(ts|tsx|js)$", 13 | "collectCoverageFrom" : ["src/**/*.{js,jsx,tsx,ts}", "!**/node_modules/**", "!**/vendor/**"], 14 | "coverageReporters": ["json", "lcov"] 15 | } 16 | -------------------------------------------------------------------------------- /e2e/mocks/api.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { RMQService } from '../../lib'; 3 | import { 4 | AppIdContracts, CustomMessageFactoryContracts, 5 | DebugContracts, 6 | DivideContracts, 7 | ManualAckContracts, 8 | MultiplyContracts, 9 | NotificationContracts, 10 | PatternHashContracts, 11 | PatternStarContracts, 12 | SumContracts, 13 | TimeOutContracts, 14 | TransformContracts, 15 | } from '../contracts/mock.contracts'; 16 | 17 | @Controller() 18 | export class ApiController { 19 | constructor(private readonly rmq: RMQService) { } 20 | 21 | async sumSuccess(arrayToSum: number[]): Promise { 22 | return this.rmq.send(SumContracts.topic, { arrayToSum }); 23 | } 24 | 25 | async sumFailed(arrayToSum: string[]): Promise { 26 | return this.rmq.send(SumContracts.topic, { arrayToSum }); 27 | } 28 | 29 | async notificationSuccess(message: string): Promise { 30 | return this.rmq.notify(NotificationContracts.topic, { message }); 31 | } 32 | 33 | async notificationFailed(message: number): Promise { 34 | return this.rmq.notify(NotificationContracts.topic, { message }); 35 | } 36 | 37 | async multiply(arrayToMultiply: number[]): Promise { 38 | return this.rmq.send(MultiplyContracts.topic, { 39 | arrayToMultiply, 40 | }); 41 | } 42 | 43 | async timeOutMessage(num: number): Promise { 44 | return this.rmq.send(TimeOutContracts.topic, num, { timeout: 4000 }); 45 | } 46 | 47 | async divide(first: number, second: number): Promise { 48 | return this.rmq.send(DivideContracts.topic, { 49 | first, 50 | second, 51 | }); 52 | } 53 | 54 | async appId(): Promise { 55 | return this.rmq.send(AppIdContracts.topic, null); 56 | } 57 | 58 | async manualAck(): Promise { 59 | return this.rmq.send(ManualAckContracts.topic, null); 60 | } 61 | 62 | async debug(): Promise { 63 | return this.rmq.send( 64 | DebugContracts.topic, { prop1: [1], prop2: Buffer.from('test buffer') } 65 | ); 66 | } 67 | 68 | async customMessageFactory(num: number): Promise { 69 | return this.rmq.send( 70 | CustomMessageFactoryContracts.topic, { num } 71 | ); 72 | } 73 | 74 | async star(num: number): Promise { 75 | return this.rmq.send('this.is.star', { num }); 76 | } 77 | 78 | async hash(num: number): Promise { 79 | return this.rmq.send('this.is.hash', { num }); 80 | } 81 | 82 | async tarnsform(date: Date): Promise { 83 | return this.rmq.send(TransformContracts.topic, { date }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /e2e/mocks/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigService } from './config.service'; 3 | 4 | @Module({ 5 | providers: [ConfigService], 6 | exports: [ConfigService] 7 | }) 8 | export class ConfigModule {} 9 | -------------------------------------------------------------------------------- /e2e/mocks/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class ConfigService { 5 | getHost() { 6 | return 'localhost'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /e2e/mocks/double.pipe.ts: -------------------------------------------------------------------------------- 1 | import { RMQPipeClass } from '../../lib'; 2 | import { Message } from 'amqplib'; 3 | import { MultiplyContracts } from '../contracts/mock.contracts'; 4 | 5 | export class DoublePipe extends RMQPipeClass { 6 | async transform(msg: Message): Promise { 7 | if (msg.fields.routingKey === MultiplyContracts.topic) { 8 | let { arrayToMultiply }: MultiplyContracts.Request = JSON.parse(msg.content.toString()); 9 | arrayToMultiply = arrayToMultiply.map(x => x*2); 10 | msg.content = Buffer.from(JSON.stringify({ arrayToMultiply })); 11 | } 12 | return msg; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /e2e/mocks/error-host.handler.ts: -------------------------------------------------------------------------------- 1 | import { IRmqErrorHeaders, RMQError, RMQErrorHandler } from '../../lib'; 2 | 3 | export class ErrorHostHandler implements RMQErrorHandler { 4 | public static handle(headers: IRmqErrorHeaders): Error | RMQError { 5 | headers['-x-host'] = 'handler'; 6 | return new RMQError( 7 | headers['-x-error'], 8 | headers['-x-type'], 9 | headers['-x-status-code'], 10 | headers['-x-data'], 11 | headers['-x-service'], 12 | headers['-x-host'], 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /e2e/mocks/microservice.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller } from '@nestjs/common'; 2 | import { RMQMessage, RMQError, RMQRoute, RMQValidate, ExtendedMessage, RMQService, RMQTransform } from '../../lib'; 3 | import { 4 | DivideContracts, 5 | MultiplyContracts, 6 | NotificationContracts, 7 | SumContracts, 8 | TimeOutContracts, 9 | AppIdContracts, 10 | ManualAckContracts, 11 | DebugContracts, CustomMessageFactoryContracts, PatternStarContracts, PatternHashContracts, TransformContracts, 12 | } from '../contracts/mock.contracts'; 13 | import { ERROR_TYPE } from '../../lib/constants'; 14 | import { Message } from 'amqplib'; 15 | 16 | @Controller() 17 | export class MicroserviceController { 18 | constructor(private readonly rmqService: RMQService) { } 19 | 20 | @RMQRoute(SumContracts.topic) 21 | @RMQValidate() 22 | sumRpc({ arrayToSum }: SumContracts.Request): SumContracts.Response { 23 | const result = arrayToSum.reduce((prev, cur) => prev + cur); 24 | if (result === 0) { 25 | throw new Error('My error from method'); 26 | } 27 | if (result < 0 && result >= -10) { 28 | throw new RMQError('My RMQError from method', ERROR_TYPE.RMQ, 0, 'data'); 29 | } 30 | if (result < -10) { 31 | return; 32 | } 33 | return { result: arrayToSum.reduce((prev, cur) => prev + cur) }; 34 | } 35 | 36 | @RMQRoute(NotificationContracts.topic) 37 | @RMQValidate() 38 | notificationNone({ message }: NotificationContracts.Request): void { 39 | console.log(message); 40 | return; 41 | } 42 | 43 | @RMQRoute(MultiplyContracts.topic) 44 | @RMQValidate() 45 | multiplyRpc({ arrayToMultiply }: MultiplyContracts.Request): MultiplyContracts.Response { 46 | return { result: arrayToMultiply.reduce((prev, cur) => prev * cur) }; 47 | } 48 | 49 | @RMQRoute(DivideContracts.topic) 50 | @RMQValidate() 51 | divide({ first, second }: DivideContracts.Request): DivideContracts.Response { 52 | return { result: first / second }; 53 | } 54 | 55 | @RMQRoute(TimeOutContracts.topic) 56 | timeOut(num: number): Promise { 57 | return new Promise((resolve, reject) => { 58 | setTimeout(function () { 59 | resolve(num); 60 | }, 3000); 61 | }); 62 | } 63 | 64 | @RMQRoute(AppIdContracts.topic) 65 | appId(@RMQMessage msg: ExtendedMessage): AppIdContracts.Response { 66 | return { appId: msg.properties.appId }; 67 | } 68 | 69 | @RMQRoute(ManualAckContracts.topic, { manualAck: true }) 70 | manualAck(@RMQMessage msg: ExtendedMessage): ManualAckContracts.Response { 71 | this.rmqService.ack(msg); 72 | return { appId: msg.properties.appId }; 73 | } 74 | 75 | @RMQRoute(DebugContracts.topic) 76 | debugMessage(@RMQMessage msg: ExtendedMessage): DebugContracts.Response { 77 | return { debugString: msg.getDebugString() }; 78 | } 79 | 80 | @RMQRoute(CustomMessageFactoryContracts.topic, { 81 | msgFactory: (msg: Message) => { 82 | const content: CustomMessageFactoryContracts.Request = JSON.parse(msg.content.toString()); 83 | content.num = content.num * 2; 84 | return [content, msg.properties.appId]; 85 | } 86 | }) 87 | customMessageFactory( 88 | { num }: CustomMessageFactoryContracts.Request, appId: string 89 | ): CustomMessageFactoryContracts.Response { 90 | return { num, appId }; 91 | } 92 | 93 | @RMQRoute(PatternStarContracts.topic) 94 | starPattern({ num }: PatternStarContracts.Request): PatternStarContracts.Response { 95 | return { num }; 96 | } 97 | 98 | @RMQRoute(PatternHashContracts.topic) 99 | hashPattern({ num }: PatternHashContracts.Request): PatternHashContracts.Response { 100 | return { num }; 101 | } 102 | 103 | @RMQTransform() 104 | @RMQRoute(TransformContracts.topic) 105 | transform({ date }: TransformContracts.Request): TransformContracts.Response { 106 | return { res: date.getFullYear() + 1, type: typeof date }; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /e2e/mocks/zero.intercepter.ts: -------------------------------------------------------------------------------- 1 | import { RMQIntercepterClass } from '../../lib'; 2 | import { Message } from 'amqplib'; 3 | import { DivideContracts } from '../contracts/mock.contracts'; 4 | 5 | export class ZeroIntercepter extends RMQIntercepterClass { 6 | async intercept(res: any, msg: Message, error: Error): Promise { 7 | if (msg.fields.routingKey === DivideContracts.topic) { 8 | res.result = 0; 9 | } 10 | return res; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /e2e/tests/rmq.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { RMQModule, RMQService } from '../../lib'; 3 | import { INestApplication } from '@nestjs/common'; 4 | import { ApiController } from '../mocks/api.controller'; 5 | import { MicroserviceController } from '../mocks/microservice.controller'; 6 | import { ERROR_UNDEFINED_FROM_RPC } from '../../lib/constants'; 7 | import { DoublePipe } from '../mocks/double.pipe'; 8 | import { ZeroIntercepter } from '../mocks/zero.intercepter'; 9 | import { ErrorHostHandler } from '../mocks/error-host.handler'; 10 | 11 | describe('RMQe2e', () => { 12 | let api: INestApplication; 13 | let apiController: ApiController; 14 | let microserviceController: MicroserviceController; 15 | let rmqService: RMQService; 16 | 17 | beforeAll(async () => { 18 | const apiModule = await Test.createTestingModule({ 19 | imports: [ 20 | RMQModule.forRoot({ 21 | exchangeName: 'test', 22 | connections: [ 23 | { 24 | login: 'guest', 25 | password: 'guest', 26 | host: 'localhost', 27 | }, 28 | ], 29 | queueName: 'test', 30 | heartbeatIntervalInSeconds: 10, 31 | prefetchCount: 10, 32 | middleware: [DoublePipe], 33 | intercepters: [ZeroIntercepter], 34 | errorHandler: ErrorHostHandler, 35 | serviceName: 'test-service', 36 | messagesTimeout: 2000, 37 | }), 38 | ], 39 | controllers: [ApiController, MicroserviceController], 40 | }).compile(); 41 | api = apiModule.createNestApplication(); 42 | await api.init(); 43 | 44 | apiController = apiModule.get(ApiController); 45 | microserviceController = apiModule.get(MicroserviceController); 46 | rmqService = apiModule.get(RMQService); 47 | console.warn = jest.fn(); 48 | console.log = jest.fn(); 49 | }); 50 | 51 | describe('rpc', () => { 52 | it('check connection', async () => { 53 | const isConnected = rmqService.healthCheck(); 54 | expect(isConnected).toBe(true); 55 | }); 56 | it('successful send()', async () => { 57 | const { result } = await apiController.sumSuccess([1, 2, 3]); 58 | expect(result).toBe(6); 59 | }); 60 | it('successful appId from message', async () => { 61 | const { appId } = await apiController.appId(); 62 | expect(appId).toBe('test-service'); 63 | }); 64 | it('manualAck', async () => { 65 | const { appId } = await apiController.manualAck(); 66 | expect(appId).toBe('test-service'); 67 | }); 68 | it('debug message', async () => { 69 | const { debugString } = await apiController.debug(); 70 | expect(debugString).toContain('"message":{"prop1":[1],"prop2":"Buffer - length 11"}'); 71 | }); 72 | it('request transform', async () => { 73 | const { res, type } = await apiController.tarnsform(new Date('01-01-2021')); 74 | expect(res).toBe(2022); 75 | expect(type).toBe('object'); 76 | }); 77 | it('request validation failed', async () => { 78 | try { 79 | await apiController.sumFailed(['a', 'b', 'c']); 80 | expect(true).toBe(false); 81 | } catch (error) { 82 | expect(error.message).toBe( 83 | 'each value in arrayToSum must be a number conforming to the specified constraints', 84 | ); 85 | expect(error.type).toBeUndefined(); 86 | expect(error.code).toBeUndefined(); 87 | expect(error.data).toBeUndefined(); 88 | expect(error.service).toBe('test-service'); 89 | expect(error.host).not.toBeNull(); 90 | } 91 | }); 92 | it('get common Error from method', async () => { 93 | try { 94 | const { result } = await apiController.sumSuccess([0, 0, 0]); 95 | expect(result).not.toBe(0); 96 | } catch (error) { 97 | expect(error.message).toBe('My error from method'); 98 | expect(error.type).toBeUndefined(); 99 | expect(error.code).toBeUndefined(); 100 | expect(error.data).toBeUndefined(); 101 | expect(error.service).toBe('test-service'); 102 | expect(error.host).not.toBeNull(); 103 | } 104 | }); 105 | it('get RMQError from method', async () => { 106 | try { 107 | const { result } = await apiController.sumSuccess([-1, 0, 0]); 108 | expect(result).not.toBe(-1); 109 | } catch (error) { 110 | expect(error.message).toBe('My RMQError from method'); 111 | expect(error.type).toBe('RMQ'); 112 | expect(error.code).toBe(0); 113 | expect(error.data).toBe('data'); 114 | expect(error.service).toBe('test-service'); 115 | expect(error.host).not.toBeNull(); 116 | } 117 | }); 118 | it('get undefined return Error', async () => { 119 | try { 120 | const { result } = await apiController.sumSuccess([-11, 0, 0]); 121 | expect(result).not.toBe(-11); 122 | } catch (error) { 123 | expect(error.message).toBe(ERROR_UNDEFINED_FROM_RPC); 124 | expect(error.type).toBeUndefined(); 125 | expect(error.code).toBeUndefined(); 126 | expect(error.data).toBeUndefined(); 127 | expect(error.service).toBe('test-service'); 128 | expect(error.host).not.toBeNull(); 129 | } 130 | }); 131 | it('long message timeout', async () => { 132 | try { 133 | const num = await apiController.timeOutMessage(10); 134 | expect(num).toBe(10); 135 | } catch (e) { 136 | expect(e.message).toBeNull(); 137 | } 138 | }); 139 | }); 140 | 141 | describe('none', () => { 142 | it('successful notify()', async () => { 143 | const res = await apiController.notificationSuccess('test'); 144 | await delay(1000); 145 | expect(console.log).toBeCalledTimes(1); 146 | expect(console.log).toHaveBeenCalledWith('test'); 147 | expect(res).toBeUndefined(); 148 | }); 149 | it('notify validation failed', async () => { 150 | const res = await apiController.notificationFailed(0); 151 | expect(console.log).toBeCalledTimes(1); 152 | expect(res).toBeUndefined(); 153 | }); 154 | }); 155 | 156 | describe('middleware', () => { 157 | it('doublePipe', async () => { 158 | const { result } = await apiController.multiply([1, 2]); 159 | expect(result).toBe(8); 160 | }); 161 | }); 162 | 163 | describe('interceptor', () => { 164 | it('zeroInterceptor', async () => { 165 | const { result } = await apiController.divide(10, 5); 166 | expect(result).toBe(0); 167 | }); 168 | }); 169 | 170 | describe('errorHandler', () => { 171 | it('error host change', async () => { 172 | try { 173 | const { result } = await apiController.sumSuccess([0, 0, 0]); 174 | expect(result).not.toBe(0); 175 | } catch (error) { 176 | expect(error.host).toBe('handler'); 177 | } 178 | }); 179 | }); 180 | 181 | describe('msgFactory', () => { 182 | it('customMessageFactory', async () => { 183 | const { num, appId } = await apiController.customMessageFactory(1); 184 | expect(num).toBe(2); 185 | expect(appId).toBe('test-service'); 186 | }); 187 | }); 188 | 189 | describe('msgPattent', () => { 190 | it('* pattern', async () => { 191 | const { num } = await apiController.star(1); 192 | expect(num).toBe(1); 193 | }); 194 | 195 | it('# pattern', async () => { 196 | const { num } = await apiController.hash(1); 197 | expect(num).toBe(1); 198 | }); 199 | }); 200 | 201 | afterAll(async () => { 202 | await delay(500); 203 | await rmqService.disconnect(); 204 | await api.close(); 205 | }); 206 | }); 207 | 208 | async function delay(time: number): Promise { 209 | return new Promise((resolve) => { 210 | setTimeout(() => { 211 | resolve(); 212 | }, time); 213 | }); 214 | } 215 | -------------------------------------------------------------------------------- /e2e/tests/rmqAsync.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { RMQModule, RMQService } from '../../lib'; 3 | import { INestApplication } from '@nestjs/common'; 4 | import { ApiController } from '../mocks/api.controller'; 5 | import { MicroserviceController } from '../mocks/microservice.controller'; 6 | import { ConfigModule } from '../mocks/config.module'; 7 | import { ConfigService } from '../mocks/config.service'; 8 | 9 | describe('RMQe2e', () => { 10 | let api: INestApplication; 11 | let apiController: ApiController; 12 | let rmqService: RMQService; 13 | 14 | beforeAll(async () => { 15 | const apiModule = await Test.createTestingModule({ 16 | imports: [ 17 | ConfigModule, 18 | RMQModule.forRootAsync({ 19 | imports: [ConfigModule], 20 | inject: [ConfigService], 21 | useFactory: (configService: ConfigService) => { 22 | return { 23 | exchangeName: 'test', 24 | connections: [ 25 | { 26 | login: 'guest', 27 | password: 'guest', 28 | host: configService.getHost(), 29 | }, 30 | ], 31 | serviceName: 'test-service', 32 | queueName: 'test' 33 | }; 34 | }, 35 | }), 36 | ], 37 | controllers: [ApiController, MicroserviceController], 38 | }).compile(); 39 | api = apiModule.createNestApplication(); 40 | await api.init(); 41 | 42 | apiController = apiModule.get(ApiController); 43 | rmqService = apiModule.get(RMQService); 44 | }); 45 | 46 | describe('rpc', () => { 47 | it('successful send()', async () => { 48 | const { result } = await apiController.sumSuccess([1, 2, 3]); 49 | expect(result).toBe(6); 50 | }); 51 | }); 52 | 53 | afterAll(async () => { 54 | await delay(500); 55 | await rmqService.disconnect(); 56 | await api.close(); 57 | }); 58 | }); 59 | 60 | async function delay(time: number): Promise { 61 | return new Promise((resolve) => { 62 | setTimeout(() => { 63 | resolve(); 64 | }, time); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /e2e/tests/rmqTest.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import { RMQModule, RMQService } from '../../lib'; 3 | import { INestApplication } from '@nestjs/common'; 4 | import { MicroserviceController } from '../mocks/microservice.controller'; 5 | import { AppIdContracts, CustomMessageFactoryContracts, DivideContracts, MultiplyContracts, PatternHashContracts, PatternStarContracts, SumContracts } from '../contracts/mock.contracts'; 6 | import { ERROR_UNDEFINED_FROM_RPC } from '../../lib/constants'; 7 | import { DoublePipe } from '../mocks/double.pipe'; 8 | import { ZeroIntercepter } from '../mocks/zero.intercepter'; 9 | import { RMQTestService } from '../../lib/rmq-test.service'; 10 | 11 | describe('RMQe2e forTest()', () => { 12 | let api: INestApplication; 13 | let rmqService: RMQTestService; 14 | 15 | beforeAll(async () => { 16 | const apiModule = await Test.createTestingModule({ 17 | imports: [ 18 | RMQModule.forTest({ 19 | serviceName: 'test-service', 20 | middleware: [DoublePipe], 21 | intercepters: [ZeroIntercepter], 22 | }) 23 | ], 24 | controllers: [MicroserviceController], 25 | }).compile(); 26 | api = apiModule.createNestApplication(); 27 | await api.init(); 28 | 29 | rmqService = apiModule.get(RMQService); 30 | }); 31 | 32 | describe('Running methods', () => { 33 | it('successful send()', async () => { 34 | const { result } = await rmqService.triggerRoute(SumContracts.topic, { 35 | arrayToSum: [1, 2, 3] 36 | }); 37 | expect(result).toEqual(6); 38 | }); 39 | it('successful appId from message', async () => { 40 | const { appId } = await rmqService.triggerRoute(AppIdContracts.topic, null); 41 | expect(appId).toBe('test-service'); 42 | }); 43 | it('request validation failed', async () => { 44 | try { 45 | await rmqService.triggerRoute(SumContracts.topic, { 46 | arrayToSum: ['a', 'b', 'c'] 47 | }); 48 | expect(true).toBe(false); 49 | } catch (error) { 50 | expect(error.message).toBe( 51 | 'each value in arrayToSum must be a number conforming to the specified constraints', 52 | ); 53 | } 54 | }); 55 | it('get common Error from method', async () => { 56 | try { 57 | const { result } = await rmqService.triggerRoute(SumContracts.topic, { 58 | arrayToSum: [0, 0, 0] 59 | }); 60 | expect(result).not.toBe(0); 61 | } catch (error) { 62 | expect(error.message).toBe('My error from method'); 63 | } 64 | }); 65 | it('get RMQError from method', async () => { 66 | try { 67 | const { result } = await rmqService.triggerRoute(SumContracts.topic, { 68 | arrayToSum: [-1, 0, 0] 69 | }); 70 | expect(result).not.toBe(-1); 71 | } catch (error) { 72 | expect(error.message).toBe('My RMQError from method'); 73 | expect(error.type).toBe('RMQ'); 74 | expect(error.code).toBe(0); 75 | expect(error.data).toBe('data'); 76 | } 77 | }); 78 | it('get undefined return Error', async () => { 79 | try { 80 | const { result } = await rmqService.triggerRoute(SumContracts.topic, { 81 | arrayToSum: [-11, 0, 0] 82 | }); 83 | expect(result).not.toBe(-11); 84 | } catch (error) { 85 | expect(error.message).toBe(ERROR_UNDEFINED_FROM_RPC); 86 | expect(error.code).toBeUndefined(); 87 | expect(error.data).toBeUndefined(); 88 | } 89 | }); 90 | }); 91 | 92 | describe('Mock results', () => { 93 | it('Mock reply', async () => { 94 | const res = { a: 1 }; 95 | const topic = 'a'; 96 | rmqService.mockReply(topic, res); 97 | const data = await rmqService.send(topic, ''); 98 | expect(data).toEqual(res); 99 | }); 100 | 101 | it('Mock error', async () => { 102 | const error = new Error('error'); 103 | const topic = 'a'; 104 | rmqService.mockError(topic, error); 105 | try { 106 | const data = await rmqService.send(topic, ''); 107 | expect(true).toBeFalsy(); 108 | } catch (e) { 109 | expect(e.message).toEqual('error'); 110 | } 111 | }); 112 | }); 113 | 114 | describe('middleware', () => { 115 | it('doublePipe', async () => { 116 | const { result } = await rmqService.triggerRoute(MultiplyContracts.topic, { 117 | arrayToMultiply: [1, 2] 118 | }); 119 | expect(result).toBe(8); 120 | }); 121 | }); 122 | 123 | describe('interceptor', () => { 124 | it('zeroInterceptor', async () => { 125 | const { result } = await rmqService.triggerRoute(DivideContracts.topic, { 126 | first: 10, 127 | second: 5 128 | }); 129 | expect(result).toBe(0); 130 | }); 131 | }); 132 | 133 | describe('msgFactory', () => { 134 | it('customMessageFactory', async () => { 135 | const { num, appId } = await rmqService.triggerRoute(CustomMessageFactoryContracts.topic, { 136 | num: 1 137 | }); 138 | expect(num).toBe(2); 139 | expect(appId).toBe('test-service'); 140 | }); 141 | }); 142 | 143 | describe('msgPattent', () => { 144 | it('* pattern', async () => { 145 | const { num } = await rmqService.triggerRoute(PatternStarContracts.topic, { 146 | num: 1 147 | }); 148 | expect(num).toBe(1); 149 | }); 150 | 151 | it('# pattern', async () => { 152 | const { num } = await rmqService.triggerRoute(PatternHashContracts.topic, { 153 | num: 1 154 | }); 155 | expect(num).toBe(1); 156 | }); 157 | }); 158 | 159 | afterAll(async () => { 160 | await api.close(); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /img/new-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/nestjs-rmq/3b173a95a053f18c6190a10b04ae9550399563d1/img/new-logo.jpg -------------------------------------------------------------------------------- /img/tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlariCode/nestjs-rmq/3b173a95a053f18c6190a10b04ae9550399563d1/img/tests.png -------------------------------------------------------------------------------- /jest.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "ts", 4 | "tsx", 5 | "js", 6 | "json" 7 | ], 8 | "transform": { 9 | "^.+\\.tsx?$": "ts-jest" 10 | }, 11 | "testRegex": "/lib/.*\\.(test|spec).(ts|tsx|js)$", 12 | "collectCoverageFrom": [ 13 | "src/**/*.{js,jsx,tsx,ts}", 14 | "!**/node_modules/**", 15 | "!**/vendor/**" 16 | ], 17 | "coverageReporters": [ 18 | "json", 19 | "lcov" 20 | ] 21 | } -------------------------------------------------------------------------------- /lib/classes/rmq-error-handler.class.ts: -------------------------------------------------------------------------------- 1 | import { RMQError } from './rmq-error.class'; 2 | import { IRmqErrorHeaders } from '../interfaces/rmq-error-headers.interface'; 3 | import { MessagePropertyHeaders } from 'amqplib'; 4 | 5 | export class RMQErrorHandler { 6 | public static handle(headers: IRmqErrorHeaders | MessagePropertyHeaders): Error | RMQError { 7 | return new RMQError( 8 | headers['-x-error'], 9 | headers['-x-type'], 10 | headers['-x-status-code'], 11 | headers['-x-data'], 12 | headers['-x-service'], 13 | headers['-x-host'] 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/classes/rmq-error.class.ts: -------------------------------------------------------------------------------- 1 | import { ERROR_TYPE } from '../constants'; 2 | 3 | export class RMQError extends Error { 4 | /** 5 | * @summary Error message 6 | */ 7 | message: string; 8 | 9 | /** 10 | * @summary Error code 11 | */ 12 | code?: number | string; 13 | 14 | /** 15 | * @summary Error custom data 16 | */ 17 | data?: any; 18 | 19 | /** 20 | * @summary Service name 21 | */ 22 | service?: string; 23 | 24 | /** 25 | * @summary Host name 26 | */ 27 | host?: string; 28 | 29 | /** 30 | * @summary Host name 31 | */ 32 | type?: ERROR_TYPE; 33 | 34 | constructor( 35 | message: string, 36 | type: ERROR_TYPE, 37 | code?: number | string, 38 | data?: any, 39 | service?: string, 40 | host?: string 41 | ) { 42 | super(); 43 | Object.setPrototypeOf(this, new.target.prototype); 44 | this.message = message; 45 | this.type = type; 46 | this.code = code; 47 | this.data = data; 48 | this.service = service; 49 | this.host = host; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/classes/rmq-extended-message.class.ts: -------------------------------------------------------------------------------- 1 | import { MessageFields, MessageProperties, Message } from 'amqplib'; 2 | 3 | export class ExtendedMessage implements Message { 4 | content: Buffer; 5 | fields: MessageFields; 6 | properties: MessageProperties; 7 | 8 | constructor(msg: Message) { 9 | this.content = msg.content; 10 | this.fields = msg.fields; 11 | this.properties = msg.properties; 12 | } 13 | 14 | public getDebugString(): string { 15 | try { 16 | const content = JSON.parse(this.content.toString()); 17 | const debugMsg = { 18 | fields: this.fields, 19 | properties: this.properties, 20 | message: this.maskBuffers(content), 21 | }; 22 | return JSON.stringify(debugMsg); 23 | } catch (e) { 24 | return e.message; 25 | } 26 | } 27 | 28 | private maskBuffers(obj: any) { 29 | const result: any = {}; 30 | for (const prop in obj) { 31 | if (obj[prop].type === 'Buffer') { 32 | result[prop] = 'Buffer - length ' + (obj[prop].data as Buffer).length; 33 | } else { 34 | result[prop] = obj[prop]; 35 | } 36 | } 37 | return result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/classes/rmq-intercepter.class.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'amqplib'; 2 | import { LoggerService } from '@nestjs/common'; 3 | 4 | export class RMQIntercepterClass { 5 | protected logger: LoggerService; 6 | 7 | constructor(logger: LoggerService = console) { 8 | this.logger = logger; 9 | } 10 | 11 | async intercept(res: any, msg: Message, error?: Error): Promise { 12 | return res; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/classes/rmq-pipe.class.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'amqplib'; 2 | import { LoggerService } from '@nestjs/common'; 3 | 4 | // tslint:disable-next-line: interface-name 5 | export class RMQPipeClass { 6 | protected logger: LoggerService; 7 | 8 | constructor(logger: LoggerService = console) { 9 | this.logger = logger; 10 | } 11 | 12 | async transform(msg: Message): Promise { 13 | return msg; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const RMQ_ROUTES_META = 'RMQ_ROUTES_META'; 2 | export const RMQ_MESSAGE_META = 'RMQ_MESSAGE_META'; 3 | export const RMQ_ROUTES_OPTIONS = 'RMQ_ROUTES_OPTIONS'; 4 | export const RMQ_ROUTES_PATH = 'RMQ_ROUTES_PATH'; 5 | export const RMQ_ROUTES_VALIDATE = 'RMQ_ROUTES_VALIDATE'; 6 | export const RMQ_ROUTES_TRANSFORM = 'RMQ_ROUTES_TRANSFORM'; 7 | export const RMQ_MODULE_OPTIONS = 'RMQ_MODULE_OPTIONS'; 8 | 9 | export const DISCONNECT_EVENT = 'disconnect'; 10 | export const CONNECT_EVENT = 'connect'; 11 | export const CONNECT_FAILED = 'connectFailed'; 12 | export const DISCONNECT_MESSAGE = 'Disconnected from RMQ. Trying to reconnect'; 13 | export const CONNECTED_MESSAGE = 'Successfully connected to RMQ'; 14 | export const CONNECT_FAILED_MESSAGE = 'Failed to connect to RMQ'; 15 | export const WRONG_CREDENTIALS_MESSAGE = 'Wrong credentials for RMQ'; 16 | export const REPLY_QUEUE = 'amq.rabbitmq.reply-to'; 17 | export const ERROR_NONE_RPC = 'This is none RPC queue. Use notify() method instead'; 18 | export const ERROR_NO_ROUTE = "Requested service doesn't have RMQRoute with this path"; 19 | export const ERROR_NO_QUEUE = 'No queueName specified! You will not recieve messages in RMQRoute'; 20 | export const ERROR_UNDEFINED_FROM_RPC = 'RPC method returned undefined'; 21 | export const ERROR_TIMEOUT = 'Response timeout error'; 22 | 23 | export const DEFAULT_RECONNECT_TIME = 5; 24 | export const DEFAULT_HEARTBEAT_TIME = 5; 25 | export const DEFAULT_TIMEOUT = 30000; 26 | export const DEFAULT_PREFETCH_COUNT = 0; 27 | export const INITIALIZATION_STEP_DELAY = 300; 28 | 29 | export enum ERROR_TYPE { 30 | TRANSPORT = 'TRANSPORT', 31 | RMQ = 'RMQ', 32 | } 33 | 34 | export enum RMQ_PROTOCOL { 35 | AMQP = 'amqp', 36 | AMQPS = 'amqps', 37 | } 38 | -------------------------------------------------------------------------------- /lib/decorators/rmq-message.decorator.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { RMQ_MESSAGE_META } from '../constants'; 3 | 4 | export function RMQMessage(target: any, propertyKey: string | symbol, parameterIndex: number) { 5 | const messageParams: number[] = Reflect.getOwnMetadata(RMQ_MESSAGE_META, target, propertyKey) || []; 6 | messageParams.push(parameterIndex); 7 | Reflect.defineMetadata(RMQ_MESSAGE_META, messageParams, target, propertyKey); 8 | } 9 | -------------------------------------------------------------------------------- /lib/decorators/rmq-pipe.decorator.ts: -------------------------------------------------------------------------------- 1 | import { RMQPipeClass } from '../classes/rmq-pipe.class'; 2 | 3 | export const RMQPipe = (pipe: typeof RMQPipeClass) => { 4 | return (target: any, methodName: string, descriptor: PropertyDescriptor) => { 5 | const method = descriptor.value; 6 | descriptor.value = async function (...args) { 7 | args[0] = await new pipe().transform(args[0]); 8 | return method.apply(this, args); 9 | }; 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /lib/decorators/rmq-route.decorator.ts: -------------------------------------------------------------------------------- 1 | import { RMQ_ROUTES_OPTIONS, RMQ_ROUTES_PATH } from '../constants'; 2 | import { IRouteOptions } from '../interfaces/queue-meta.interface'; 3 | import { applyDecorators, SetMetadata } from '@nestjs/common'; 4 | 5 | export const RMQRoute = (topic: string, options?: IRouteOptions): MethodDecorator => { 6 | return applyDecorators( 7 | SetMetadata(RMQ_ROUTES_OPTIONS, { 8 | ...options, 9 | }), 10 | SetMetadata(RMQ_ROUTES_PATH, topic) 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/decorators/transform.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, SetMetadata } from '@nestjs/common'; 2 | import { RMQ_ROUTES_TRANSFORM } from '../constants'; 3 | 4 | export const RMQTransform = (): MethodDecorator => { 5 | return applyDecorators(SetMetadata(RMQ_ROUTES_TRANSFORM, true)); 6 | }; 7 | -------------------------------------------------------------------------------- /lib/decorators/validate.decorator.ts: -------------------------------------------------------------------------------- 1 | import { applyDecorators, SetMetadata } from '@nestjs/common'; 2 | import { RMQ_ROUTES_VALIDATE } from '../constants'; 3 | 4 | export const RMQValidate = (): MethodDecorator => { 5 | return applyDecorators(SetMetadata(RMQ_ROUTES_VALIDATE, true)); 6 | }; 7 | 8 | /** @depreceated */ 9 | export const Validate = RMQValidate; 10 | -------------------------------------------------------------------------------- /lib/emmiters/router.emmiter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | 3 | export const requestEmitter = new EventEmitter(); 4 | export const responseEmitter = new EventEmitter(); 5 | 6 | requestEmitter.setMaxListeners(0); 7 | responseEmitter.setMaxListeners(0); 8 | 9 | export enum ResponseEmitterResult { 10 | success = 'success', 11 | error = 'error', 12 | ack = 'ack', 13 | } 14 | -------------------------------------------------------------------------------- /lib/helpers/logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LoggerService } from '@nestjs/common'; 2 | import { blueBright, white, yellow } from 'chalk'; 3 | 4 | export class RQMColorLogger implements LoggerService { 5 | logMessages: boolean; 6 | 7 | constructor(logMessages: boolean) { 8 | this.logMessages = logMessages ?? false; 9 | } 10 | log(message: any, context?: string): any { 11 | Logger.log(message, context); 12 | } 13 | error(message: any, trace?: string, context?: string): any { 14 | Logger.error(message, trace, context); 15 | } 16 | debug(message: any, context?: string): any { 17 | if (!this.logMessages) { 18 | return; 19 | } 20 | const msg = JSON.stringify(message); 21 | const action = context.split(',')[0]; 22 | const topic = context.split(',')[1]; 23 | Logger.log(`${blueBright(action)} [${yellow(topic)}] ${white(msg)}`); 24 | console.warn(`${blueBright(action)} [${yellow(topic)}] ${white(msg)}`); 25 | } 26 | warn(message: any, context?: string): any { 27 | Logger.warn(message, context); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './rmq.module'; 2 | export * from './rmq.service'; 3 | export * from './rmq-test.service'; 4 | export * from './interfaces/rmq-error-headers.interface'; 5 | export * from './interfaces/rmq-options.interface'; 6 | export * from './interfaces/rmq-publish-options.interface'; 7 | export * from './interfaces/rmq-service.interface'; 8 | export * from './decorators/rmq-route.decorator'; 9 | export * from './decorators/rmq-pipe.decorator'; 10 | export * from './decorators/validate.decorator'; 11 | export * from './decorators/transform.decorator'; 12 | export * from './decorators/rmq-message.decorator'; 13 | export * from './classes/rmq-pipe.class'; 14 | export * from './classes/rmq-intercepter.class'; 15 | export * from './classes/rmq-error.class'; 16 | export * from './classes/rmq-error-handler.class'; 17 | export * from './classes/rmq-extended-message.class'; 18 | 19 | export { Message } from 'amqplib'; 20 | -------------------------------------------------------------------------------- /lib/interfaces/queue-meta.interface.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'amqplib'; 2 | 3 | export interface IRouteMeta { 4 | topic: string; 5 | methodName: string; 6 | target: any; 7 | options?: IRouteOptions; 8 | } 9 | 10 | export interface IRouteOptions { 11 | manualAck?: boolean; 12 | msgFactory?: (msg: Message) => any[]; 13 | } 14 | -------------------------------------------------------------------------------- /lib/interfaces/rmq-controller-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { Message } from 'amqplib'; 2 | import { IRouteMeta } from './queue-meta.interface'; 3 | 4 | export interface IRMQControllerOptions { 5 | msgFactory?: (msg: Message, topic: IRouteMeta) => any[]; 6 | } 7 | -------------------------------------------------------------------------------- /lib/interfaces/rmq-error-headers.interface.ts: -------------------------------------------------------------------------------- 1 | import { ERROR_TYPE } from '../constants'; 2 | 3 | export interface IRmqErrorHeaders { 4 | '-x-error': string; 5 | '-x-type': ERROR_TYPE; 6 | '-x-status-code': number; 7 | '-x-data': string; 8 | '-x-service': string; 9 | '-x-host': string; 10 | } 11 | -------------------------------------------------------------------------------- /lib/interfaces/rmq-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { RMQPipeClass } from '../classes/rmq-pipe.class'; 2 | import { RMQIntercepterClass } from '../classes/rmq-intercepter.class'; 3 | import { RMQErrorHandler } from '../classes/rmq-error-handler.class'; 4 | import { LoggerService } from '@nestjs/common'; 5 | import { ModuleMetadata } from '@nestjs/common/interfaces'; 6 | import { Channel, Options } from 'amqplib'; 7 | import { RMQ_PROTOCOL } from '../constants'; 8 | import { ConnectionOptions } from 'tls'; 9 | 10 | export interface IRMQServiceOptions { 11 | exchangeName: string; 12 | connections: IRMQConnection[]; 13 | queueName?: string; 14 | queueArguments?: { 15 | [key: string]: string; 16 | }; 17 | connectionOptions?: ConnectionOptions & { 18 | noDelay?: boolean; 19 | timeout?: number; 20 | keepAlive?: boolean; 21 | keepAliveDelay?: number; 22 | clientProperties?: any; 23 | credentials?: { 24 | mechanism: string; 25 | username: string; 26 | password: string; 27 | response: () => Buffer; 28 | }; 29 | }; 30 | prefetchCount?: number; 31 | isGlobalPrefetchCount?: boolean; 32 | queueOptions?: Options.AssertQueue; 33 | isQueueDurable?: boolean; 34 | isExchangeDurable?: boolean; 35 | assertExchangeType?: Parameters[1]; 36 | exchangeOptions?: Options.AssertExchange; 37 | reconnectTimeInSeconds?: number; 38 | heartbeatIntervalInSeconds?: number; 39 | messagesTimeout?: number; 40 | logMessages?: boolean; 41 | logger?: LoggerService; 42 | middleware?: (typeof RMQPipeClass)[]; 43 | intercepters?: (typeof RMQIntercepterClass)[]; 44 | errorHandler?: typeof RMQErrorHandler; 45 | serviceName?: string; 46 | autoBindingRoutes?: boolean; 47 | } 48 | 49 | export interface IRMQConnection { 50 | login: string; 51 | password: string; 52 | host: string; 53 | protocol?: RMQ_PROTOCOL; 54 | port?: number; 55 | vhost?: string; 56 | } 57 | 58 | export interface IRMQServiceAsyncOptions extends Pick { 59 | useFactory?: (...args: any[]) => Promise | IRMQServiceOptions; 60 | inject?: any[]; 61 | } 62 | -------------------------------------------------------------------------------- /lib/interfaces/rmq-publish-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { Options } from 'amqplib'; 2 | 3 | export interface IPublishOptions extends Options.Publish { 4 | timeout?: number; 5 | } 6 | -------------------------------------------------------------------------------- /lib/interfaces/rmq-service.interface.ts: -------------------------------------------------------------------------------- 1 | import { Channel } from 'amqplib'; 2 | import { IPublishOptions } from '..'; 3 | 4 | export interface IRMQService { 5 | init: () => Promise; 6 | ack: (...params: Parameters) => ReturnType; 7 | nack: (...params: Parameters) => ReturnType; 8 | send: (topic: string, message: IMessage, options?: IPublishOptions) => Promise; 9 | notify: (topic: string, message: IMessage, options?: IPublishOptions) => Promise; 10 | healthCheck: () => boolean; 11 | disconnect: () => Promise; 12 | mockReply?: (topic: string, reply: T) => void; 13 | mockError?: (topic: string, error: T) => void; 14 | triggerRoute?: (path: string, data: T) => Promise; 15 | } 16 | -------------------------------------------------------------------------------- /lib/option.validator.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@nestjs/common'; 2 | import { IRMQServiceOptions } from './interfaces/rmq-options.interface'; 3 | 4 | export function validateOptions(options: IRMQServiceOptions, logger: LoggerService) { 5 | if (options.serviceName === undefined) { 6 | logger.warn('Check your configuration, RabbitMQ service name not specified! serviceName is undefined.'); 7 | } 8 | if (options.isQueueDurable) { 9 | logger.warn('isQueueDurable is deprecated and will be removed in future versions. Use queueOptions instead.'); 10 | } 11 | if (options.queueArguments) { 12 | logger.warn('queueArguments is deprecated and will be removed in future versions. Use queueOptions instead.'); 13 | } 14 | if (options.isExchangeDurable) { 15 | logger.warn( 16 | 'isExchangeDurable is deprecated and will be removed in future versions. Use exchangeOptions instead.' 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/rmq-error.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { RMQError } from './classes/rmq-error.class'; 3 | import { hostname } from 'os'; 4 | import { Message } from 'amqplib'; 5 | import { RMQErrorHandler } from './classes/rmq-error-handler.class'; 6 | import { IRMQServiceOptions } from './interfaces/rmq-options.interface'; 7 | import { RMQ_MODULE_OPTIONS } from './constants'; 8 | 9 | @Injectable() 10 | export class RmqErrorService { 11 | private options: IRMQServiceOptions; 12 | 13 | constructor(@Inject(RMQ_MODULE_OPTIONS) options: IRMQServiceOptions) { 14 | this.options = options; 15 | } 16 | 17 | public buildError(error: Error | RMQError) { 18 | if (!error) { 19 | return null; 20 | } 21 | let errorHeaders = {}; 22 | errorHeaders['-x-error'] = error.message; 23 | errorHeaders['-x-host'] = hostname(); 24 | errorHeaders['-x-service'] = this.options.serviceName; 25 | if (this.isRMQError(error)) { 26 | errorHeaders = { 27 | ...errorHeaders, 28 | '-x-status-code': (error as RMQError).code, 29 | '-x-data': (error as RMQError).data, 30 | '-x-type': (error as RMQError).type, 31 | }; 32 | } 33 | return errorHeaders; 34 | } 35 | 36 | public errorHandler(msg: Message): any { 37 | const { headers } = msg.properties; 38 | if (this.options.errorHandler) { 39 | return this.options.errorHandler.handle(headers); 40 | } 41 | return RMQErrorHandler.handle(headers); 42 | } 43 | 44 | protected isRMQError(error: Error | RMQError): error is RMQError { 45 | return (error as RMQError).code !== undefined; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/rmq-metadata.accessor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { 4 | RMQ_ROUTES_PATH, 5 | RMQ_ROUTES_OPTIONS, 6 | RMQ_ROUTES_META, 7 | RMQ_MESSAGE_META, 8 | RMQ_ROUTES_VALIDATE, 9 | RMQ_ROUTES_TRANSFORM, 10 | } from './constants'; 11 | import { IRouteOptions } from './interfaces/queue-meta.interface'; 12 | import { RMQService } from './rmq.service'; 13 | 14 | @Injectable() 15 | export class RMQMetadataAccessor { 16 | constructor(private readonly reflector: Reflector) {} 17 | 18 | getRMQPath(target: Function): string | undefined { 19 | return this.reflector.get(RMQ_ROUTES_PATH, target); 20 | } 21 | 22 | getAllRMQPaths(): string[] { 23 | return Reflect.getMetadata(RMQ_ROUTES_META, RMQService) ?? []; 24 | } 25 | 26 | addRMQPath(path: string): void { 27 | const paths: string[] = this.getAllRMQPaths(); 28 | paths.push(path); 29 | Reflect.defineMetadata(RMQ_ROUTES_META, paths, RMQService); 30 | } 31 | 32 | getRMQOptions(target: Function): IRouteOptions | undefined { 33 | return this.reflector.get(RMQ_ROUTES_OPTIONS, target); 34 | } 35 | 36 | getRMQValidation(target: Function): boolean | undefined { 37 | return this.reflector.get(RMQ_ROUTES_VALIDATE, target); 38 | } 39 | 40 | getRMQTransformation(target: Function): boolean | undefined { 41 | return this.reflector.get(RMQ_ROUTES_TRANSFORM, target); 42 | } 43 | 44 | getRMQMessageIndexes(target: any, method: string): number[] { 45 | return Reflect.getOwnMetadata(RMQ_MESSAGE_META, target, method) ?? []; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/rmq-test.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, LoggerService, OnModuleInit } from '@nestjs/common'; 2 | import { Channel, Message } from 'amqplib'; 3 | import { IPublishOptions, IRMQServiceOptions, RMQError } from '.'; 4 | import { CONNECTED_MESSAGE, ERROR_NO_ROUTE, ERROR_TYPE, RMQ_MODULE_OPTIONS } from './constants'; 5 | import { RQMColorLogger } from './helpers/logger'; 6 | import { IRMQService } from './interfaces/rmq-service.interface'; 7 | import { RMQMetadataAccessor } from './rmq-metadata.accessor'; 8 | import { requestEmitter, responseEmitter, ResponseEmitterResult } from './emmiters/router.emmiter'; 9 | import { validateOptions } from './option.validator'; 10 | import { getUniqId } from './utils/get-uniq-id'; 11 | 12 | @Injectable() 13 | export class RMQTestService implements OnModuleInit, IRMQService { 14 | private options: IRMQServiceOptions; 15 | private routes: string[]; 16 | private logger: LoggerService; 17 | private isInitialized = false; 18 | private replyStack = new Map(); 19 | private mockStack = new Map(); 20 | private mockErrorStack = new Map(); 21 | 22 | constructor( 23 | @Inject(RMQ_MODULE_OPTIONS) options: IRMQServiceOptions, 24 | private readonly metadataAccessor: RMQMetadataAccessor 25 | ) { 26 | this.options = options; 27 | this.logger = options.logger ? options.logger : new RQMColorLogger(this.options.logMessages); 28 | validateOptions(this.options, this.logger); 29 | } 30 | 31 | async onModuleInit() { 32 | await this.init(); 33 | this.isInitialized = true; 34 | } 35 | 36 | public mockReply(topic: string, reply: T) { 37 | this.mockStack.set(topic, reply); 38 | } 39 | 40 | public mockError(topic: string, error: T) { 41 | this.mockErrorStack.set(topic, error); 42 | } 43 | 44 | public async triggerRoute(path: string, data: T): Promise { 45 | return new Promise(async (resolve, reject) => { 46 | const correlationId = getUniqId(); 47 | let msg: Message = { 48 | content: Buffer.from(JSON.stringify(data)), 49 | fields: { 50 | deliveryTag: 1, 51 | redelivered: false, 52 | exchange: 'mock', 53 | routingKey: path, 54 | }, 55 | properties: { 56 | messageId: 1, 57 | timestamp: new Date(), 58 | appId: this.options.serviceName, 59 | clusterId: 1, 60 | userId: 1, 61 | type: '', 62 | contentType: JSON, 63 | contentEncoding: undefined, 64 | headers: [], 65 | deliveryMode: '', 66 | priority: 0, 67 | correlationId, 68 | expiration: 0, 69 | replyTo: 'mock', 70 | }, 71 | }; 72 | const route = this.getRouteByTopic(path); 73 | if (route) { 74 | msg = await this.useMiddleware(msg); 75 | this.replyStack.set(correlationId, { resolve, reject }); 76 | requestEmitter.emit(route, msg); 77 | } else { 78 | throw new RMQError(ERROR_NO_ROUTE, ERROR_TYPE.TRANSPORT); 79 | } 80 | }); 81 | } 82 | 83 | public async init(): Promise { 84 | this.bindRMQRoutes(); 85 | this.logConnected(); 86 | this.attachEmitters(); 87 | } 88 | 89 | public ack(...params: Parameters): ReturnType {} 90 | 91 | public nack(...params: Parameters): ReturnType {} 92 | 93 | public async send(topic: string, message: IMessage, options?: IPublishOptions): Promise { 94 | const error = this.mockErrorStack.get(topic); 95 | if (error) { 96 | throw error; 97 | } 98 | return this.mockStack.get(topic) as IReply; 99 | } 100 | 101 | public async notify(topic: string, message: IMessage, options?: IPublishOptions): Promise {} 102 | 103 | public healthCheck() { 104 | return true; 105 | } 106 | 107 | public async disconnect() { 108 | responseEmitter.removeAllListeners(); 109 | } 110 | 111 | private attachEmitters(): void { 112 | responseEmitter.on(ResponseEmitterResult.success, async (msg: Message, result) => { 113 | const { resolve } = this.replyStack.get(msg.properties.correlationId); 114 | result = await this.intercept(result, msg); 115 | resolve(result); 116 | }); 117 | responseEmitter.on(ResponseEmitterResult.error, async (msg: Message, err) => { 118 | const { reject } = this.replyStack.get(msg.properties.correlationId); 119 | await this.intercept('', msg, err); 120 | reject(err); 121 | }); 122 | responseEmitter.on(ResponseEmitterResult.ack, async (msg: Message) => { 123 | this.ack(msg); 124 | }); 125 | } 126 | 127 | private async intercept(res: any, msg: Message, error?: Error) { 128 | if (!this.options.intercepters || this.options.intercepters.length === 0) { 129 | return res; 130 | } 131 | for (const intercepter of this.options.intercepters) { 132 | res = await new intercepter(this.logger).intercept(res, msg, error); 133 | } 134 | return res; 135 | } 136 | 137 | private async bindRMQRoutes(): Promise { 138 | this.routes = this.metadataAccessor.getAllRMQPaths(); 139 | if (this.routes.length > 0) { 140 | this.routes.map(async (r) => { 141 | this.logger.log(`Mapped ${r}`, 'RMQRoute'); 142 | }); 143 | } 144 | } 145 | 146 | private async useMiddleware(msg: Message) { 147 | if (!this.options.middleware || this.options.middleware.length === 0) { 148 | return msg; 149 | } 150 | for (const middleware of this.options.middleware) { 151 | msg = await new middleware(this.logger).transform(msg); 152 | } 153 | return msg; 154 | } 155 | 156 | private getRouteByTopic(topic: string): string { 157 | return this.routes.find((route) => { 158 | if (route === topic) { 159 | return true; 160 | } 161 | const regexString = '^' + route.replace(/\*/g, '([^.]+)').replace(/#/g, '([^.]+.?)+') + '$'; 162 | return topic.search(regexString) !== -1; 163 | }); 164 | } 165 | 166 | private logConnected() { 167 | this.logger.log(CONNECTED_MESSAGE, 'RMQModule'); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /lib/rmq.explorer.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { DiscoveryService } from '@nestjs/core'; 3 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; 4 | import { MetadataScanner } from '@nestjs/core/metadata-scanner'; 5 | import { RMQMetadataAccessor } from './rmq-metadata.accessor'; 6 | import { Message } from 'amqplib'; 7 | import { requestEmitter, responseEmitter, ResponseEmitterResult } from './emmiters/router.emmiter'; 8 | import { ERROR_TYPE, ERROR_UNDEFINED_FROM_RPC } from './constants'; 9 | import { ExtendedMessage } from './classes/rmq-extended-message.class'; 10 | import { RMQError } from './classes/rmq-error.class'; 11 | import { IRouteOptions } from './interfaces/queue-meta.interface'; 12 | import { validate } from 'class-validator'; 13 | import { plainToClass } from 'class-transformer'; 14 | 15 | @Injectable() 16 | export class RMQExplorer implements OnModuleInit { 17 | constructor( 18 | private readonly discoveryService: DiscoveryService, 19 | private readonly metadataAccessor: RMQMetadataAccessor, 20 | private readonly metadataScanner: MetadataScanner 21 | ) {} 22 | 23 | async onModuleInit() { 24 | this.explore(); 25 | } 26 | 27 | explore() { 28 | const instanceWrappers: InstanceWrapper[] = [ 29 | ...this.discoveryService.getControllers(), 30 | ...this.discoveryService.getProviders(), 31 | ]; 32 | 33 | instanceWrappers.forEach((wrapper: InstanceWrapper, i: number) => { 34 | const { instance } = wrapper; 35 | if (!instance || !Object.getPrototypeOf(instance)) { 36 | return; 37 | } 38 | 39 | if (instanceWrappers.findIndex((w) => w.instance === instance) !== i) { 40 | return; 41 | } 42 | 43 | this.metadataScanner 44 | .getAllMethodNames(Object.getPrototypeOf(instance)) 45 | .map((key) => this.lookupRMQRoute(instance, key)); 46 | }); 47 | } 48 | 49 | lookupRMQRoute(instance: Record, key: string) { 50 | const methodRef = instance[key]; 51 | const options = this.metadataAccessor.getRMQOptions(methodRef); 52 | const path = this.metadataAccessor.getRMQPath(methodRef); 53 | if (!path || !options) { 54 | return; 55 | } 56 | this.metadataAccessor.addRMQPath(path); 57 | this.attachEmitter(path, options, instance, methodRef); 58 | } 59 | 60 | private attachEmitter( 61 | path: string, 62 | options: IRouteOptions, 63 | instance: Record, 64 | methodRef: Function 65 | ) { 66 | requestEmitter.on(path, async (msg: Message) => { 67 | const messageParams: number[] = this.metadataAccessor.getRMQMessageIndexes( 68 | Object.getPrototypeOf(instance), 69 | methodRef.name 70 | ); 71 | try { 72 | let funcArgs = options?.msgFactory ? options.msgFactory(msg) : RMQMessageFactory(msg); 73 | if (messageParams.length > 0) { 74 | for (const param of messageParams) { 75 | funcArgs[param] = new ExtendedMessage(msg); 76 | } 77 | } 78 | funcArgs = this.transformRequest(instance, methodRef, funcArgs); 79 | const error = await this.validateRequest(instance, methodRef, funcArgs); 80 | if (error) { 81 | responseEmitter.emit(ResponseEmitterResult.error, msg, new RMQError(error, ERROR_TYPE.RMQ)); 82 | responseEmitter.emit(ResponseEmitterResult.ack, msg); 83 | return; 84 | } 85 | const result = await methodRef.apply(instance, funcArgs); 86 | if (msg.properties.replyTo && result !== undefined) { 87 | responseEmitter.emit(ResponseEmitterResult.success, msg, result); 88 | } else if (msg.properties.replyTo && result === undefined) { 89 | responseEmitter.emit( 90 | ResponseEmitterResult.error, 91 | msg, 92 | new RMQError(ERROR_UNDEFINED_FROM_RPC, ERROR_TYPE.RMQ) 93 | ); 94 | } 95 | } catch (err) { 96 | if (msg.properties.replyTo) { 97 | responseEmitter.emit(ResponseEmitterResult.error, msg, err); 98 | } 99 | } 100 | if (!options?.manualAck) { 101 | responseEmitter.emit(ResponseEmitterResult.ack, msg); 102 | } 103 | }); 104 | } 105 | 106 | private transformRequest(instance: Record, methodRef: Function, funcArgs: any[]): any[] { 107 | const transformMsg = this.metadataAccessor.getRMQTransformation(methodRef); 108 | if (!transformMsg) { 109 | return funcArgs; 110 | } 111 | const types = Reflect.getMetadata('design:paramtypes', Object.getPrototypeOf(instance), methodRef.name); 112 | funcArgs[0] = plainToClass(types[0], funcArgs[0]); 113 | return funcArgs; 114 | } 115 | 116 | private async validateRequest( 117 | instance: Record, 118 | methodRef: Function, 119 | funcArgs: any[] 120 | ): Promise { 121 | const validateMsg = this.metadataAccessor.getRMQValidation(methodRef); 122 | if (!validateMsg) { 123 | return; 124 | } 125 | const types = Reflect.getMetadata('design:paramtypes', Object.getPrototypeOf(instance), methodRef.name); 126 | const classData = funcArgs[0]; 127 | const test = Object.assign(new types[0](), classData); 128 | const errors = await validate(test); 129 | if (errors.length) { 130 | const message = errors 131 | .map((m) => { 132 | return Object.values(m.constraints).join('; '); 133 | }) 134 | .join('; '); 135 | return message; 136 | } 137 | } 138 | } 139 | 140 | export const RMQMessageFactory = (msg: Message) => { 141 | return [JSON.parse(msg.content.toString())]; 142 | }; 143 | -------------------------------------------------------------------------------- /lib/rmq.module.ts: -------------------------------------------------------------------------------- 1 | import { RMQService } from './rmq.service'; 2 | import { DynamicModule, Global, Module, Provider } from '@nestjs/common'; 3 | import { IRMQServiceAsyncOptions, IRMQServiceOptions } from './interfaces/rmq-options.interface'; 4 | import { RMQMetadataAccessor } from './rmq-metadata.accessor'; 5 | import { RMQExplorer } from './rmq.explorer'; 6 | import { DiscoveryModule } from '@nestjs/core'; 7 | import { RMQ_MODULE_OPTIONS } from './constants'; 8 | import { RmqErrorService } from './rmq-error.service'; 9 | import { RMQTestService } from './rmq-test.service'; 10 | 11 | @Global() 12 | @Module({ 13 | imports: [DiscoveryModule], 14 | providers: [RMQMetadataAccessor, RMQExplorer, RmqErrorService], 15 | }) 16 | export class RMQModule { 17 | static forRoot(options: IRMQServiceOptions): DynamicModule { 18 | return { 19 | module: RMQModule, 20 | providers: [RMQService, { provide: RMQ_MODULE_OPTIONS, useValue: options }], 21 | exports: [RMQService], 22 | }; 23 | } 24 | 25 | static forRootAsync(options: IRMQServiceAsyncOptions): DynamicModule { 26 | const asyncOptions = this.createAsyncOptionsProvider(options); 27 | return { 28 | module: RMQModule, 29 | imports: options.imports, 30 | providers: [RMQService, RMQMetadataAccessor, RMQExplorer, asyncOptions], 31 | exports: [RMQService], 32 | }; 33 | } 34 | 35 | static forTest(options: Partial) { 36 | return { 37 | module: RMQModule, 38 | providers: [ 39 | { 40 | provide: RMQService, 41 | useClass: RMQTestService, 42 | }, 43 | { provide: RMQ_MODULE_OPTIONS, useValue: options }, 44 | ], 45 | exports: [RMQService], 46 | }; 47 | } 48 | 49 | private static createAsyncOptionsProvider(options: IRMQServiceAsyncOptions): Provider { 50 | return { 51 | provide: RMQ_MODULE_OPTIONS, 52 | useFactory: async (...args: any[]) => { 53 | const config = await options.useFactory(...args); 54 | return config; 55 | }, 56 | inject: options.inject || [], 57 | }; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/rmq.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { RMQService } from './rmq.service'; 2 | import { RMQMetadataAccessor } from './rmq-metadata.accessor'; 3 | import { Reflector } from '@nestjs/core'; 4 | import { RmqErrorService } from './rmq-error.service'; 5 | 6 | describe('RMQService', () => { 7 | let rmqService: RMQService; 8 | 9 | beforeEach(async () => { 10 | const accessor = new RMQMetadataAccessor(new Reflector()); 11 | const errorService = new RmqErrorService({ 12 | exchangeName: 'test', 13 | connections: [], 14 | }); 15 | rmqService = new RMQService( 16 | { 17 | exchangeName: 'test', 18 | serviceName: '', 19 | connections: [], 20 | }, 21 | accessor, 22 | errorService 23 | ); 24 | rmqService['routes'] = ['exect.match.rpc', '*.*.star', '#.hash', 'pattent.#']; 25 | }); 26 | 27 | describe('Test regex', () => { 28 | it('Matching', async () => { 29 | const res = rmqService['getRouteByTopic']('exect.match.rpc'); 30 | expect(res).toBe(rmqService['routes'][0]); 31 | }); 32 | 33 | it('Pattern * - success', async () => { 34 | const res = rmqService['getRouteByTopic']('oh.thisis.star'); 35 | expect(res).toBe(rmqService['routes'][1]); 36 | }); 37 | 38 | it('Pattern * - fail', async () => { 39 | const res = rmqService['getRouteByTopic']('oh.this.is.star'); 40 | expect(res).toBe(undefined); 41 | }); 42 | 43 | it('Pattern # - success start', async () => { 44 | const res = rmqService['getRouteByTopic']('this.is.real.hash'); 45 | expect(res).toBe(rmqService['routes'][2]); 46 | }); 47 | 48 | it('Pattern # - success end', async () => { 49 | const res = rmqService['getRouteByTopic']('pattent.topic'); 50 | expect(res).toBe(rmqService['routes'][3]); 51 | }); 52 | 53 | it('Pattern # - fail', async () => { 54 | const res = rmqService['getRouteByTopic']('this.pattent.topic'); 55 | expect(res).toBe(undefined); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /lib/rmq.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, LoggerService, OnModuleInit } from '@nestjs/common'; 2 | import { 3 | CONNECT_EVENT, 4 | CONNECTED_MESSAGE, 5 | DEFAULT_PREFETCH_COUNT, 6 | DEFAULT_RECONNECT_TIME, 7 | DEFAULT_TIMEOUT, 8 | DISCONNECT_EVENT, 9 | DISCONNECT_MESSAGE, 10 | ERROR_NO_ROUTE, 11 | ERROR_NONE_RPC, 12 | ERROR_TIMEOUT, 13 | ERROR_TYPE, 14 | REPLY_QUEUE, 15 | DEFAULT_HEARTBEAT_TIME, 16 | RMQ_MODULE_OPTIONS, 17 | INITIALIZATION_STEP_DELAY, 18 | ERROR_NO_QUEUE, 19 | RMQ_PROTOCOL, 20 | CONNECT_FAILED_MESSAGE, 21 | WRONG_CREDENTIALS_MESSAGE, 22 | CONNECT_FAILED, 23 | } from './constants'; 24 | import { EventEmitter } from 'events'; 25 | import { Channel, Message } from 'amqplib'; 26 | import * as amqp from 'amqp-connection-manager'; 27 | // tslint:disable-next-line:no-duplicate-imports 28 | import { AmqpConnectionManager, ChannelWrapper } from 'amqp-connection-manager'; 29 | import { IRMQConnection, IRMQServiceOptions } from './interfaces/rmq-options.interface'; 30 | import { requestEmitter, responseEmitter, ResponseEmitterResult } from './emmiters/router.emmiter'; 31 | import { IPublishOptions } from './interfaces/rmq-publish-options.interface'; 32 | import { RMQError } from './classes/rmq-error.class'; 33 | import { RQMColorLogger } from './helpers/logger'; 34 | import { validateOptions } from './option.validator'; 35 | import { RMQMetadataAccessor } from './rmq-metadata.accessor'; 36 | import { RmqErrorService } from './rmq-error.service'; 37 | import { getUniqId } from './utils/get-uniq-id'; 38 | import { IRMQService } from './interfaces/rmq-service.interface'; 39 | 40 | @Injectable() 41 | export class RMQService implements OnModuleInit, IRMQService { 42 | private server: AmqpConnectionManager = null; 43 | private clientChannel: ChannelWrapper = null; 44 | private subscriptionChannel: ChannelWrapper = null; 45 | private options: IRMQServiceOptions; 46 | private sendResponseEmitter: EventEmitter = new EventEmitter(); 47 | private replyQueue: string = REPLY_QUEUE; 48 | private routes: string[]; 49 | private logger: LoggerService; 50 | 51 | private isConnected = false; 52 | private isInitialized = false; 53 | 54 | constructor( 55 | @Inject(RMQ_MODULE_OPTIONS) options: IRMQServiceOptions, 56 | private readonly metadataAccessor: RMQMetadataAccessor, 57 | private readonly errorService: RmqErrorService 58 | ) { 59 | this.options = options; 60 | this.logger = options.logger ? options.logger : new RQMColorLogger(this.options.logMessages); 61 | validateOptions(this.options, this.logger); 62 | } 63 | 64 | async onModuleInit() { 65 | await this.init(); 66 | this.isInitialized = true; 67 | } 68 | 69 | public async init(): Promise { 70 | return new Promise(async (resolve, reject) => { 71 | const connectionURLs: string[] = this.options.connections.map((connection: IRMQConnection) => { 72 | return this.createConnectionUri(connection); 73 | }); 74 | const AMQPConnectionOptions: amqp.AmqpConnectionManagerOptions = { 75 | reconnectTimeInSeconds: this.options.reconnectTimeInSeconds ?? DEFAULT_RECONNECT_TIME, 76 | heartbeatIntervalInSeconds: this.options.heartbeatIntervalInSeconds ?? DEFAULT_HEARTBEAT_TIME, 77 | connectionOptions: this.options.connectionOptions ?? {}, 78 | }; 79 | this.server = amqp.connect(connectionURLs, AMQPConnectionOptions); 80 | 81 | this.server.on(CONNECT_EVENT, (connection) => { 82 | this.isConnected = true; 83 | this.attachEmitters(); 84 | }); 85 | this.server.on(DISCONNECT_EVENT, (err) => { 86 | this.isConnected = false; 87 | this.detachEmitters(); 88 | this.logger.error(DISCONNECT_MESSAGE); 89 | this.logger.error(err.err); 90 | }); 91 | this.server.on(CONNECT_FAILED, (err) => { 92 | this.logger.error(CONNECT_FAILED_MESSAGE); 93 | this.logger.error(err.err); 94 | if (err.err.message.includes('ACCESS-REFUSED') || err.err.message.includes('403')) { 95 | this.logger.error(WRONG_CREDENTIALS_MESSAGE); 96 | reject(err); 97 | } 98 | }); 99 | 100 | await Promise.all([this.createClientChannel(), this.createSubscriptionChannel()]); 101 | resolve(); 102 | }); 103 | } 104 | 105 | public ack(...params: Parameters): ReturnType { 106 | return this.subscriptionChannel.ack(...params); 107 | } 108 | 109 | public nack(...params: Parameters): ReturnType { 110 | return this.subscriptionChannel.nack(...params); 111 | } 112 | 113 | public async send(topic: string, message: IMessage, options?: IPublishOptions): Promise { 114 | return new Promise(async (resolve, reject) => { 115 | await this.initializationCheck(); 116 | const correlationId = getUniqId(); 117 | const timeout = options?.timeout ?? this.options.messagesTimeout ?? DEFAULT_TIMEOUT; 118 | const timerId = setTimeout(() => { 119 | reject(new RMQError(`${ERROR_TIMEOUT}: ${timeout} while sending to ${topic}`, ERROR_TYPE.TRANSPORT)); 120 | }, timeout); 121 | this.sendResponseEmitter.once(correlationId, (msg: Message) => { 122 | clearTimeout(timerId); 123 | if (msg.properties?.headers?.['-x-error']) { 124 | reject(this.errorService.errorHandler(msg)); 125 | } 126 | const { content } = msg; 127 | if (content.toString()) { 128 | this.logger.debug(content, `Received ▼,${topic}`); 129 | resolve(JSON.parse(content.toString())); 130 | } else { 131 | reject(new RMQError(ERROR_NONE_RPC, ERROR_TYPE.TRANSPORT)); 132 | } 133 | }); 134 | await this.clientChannel.publish(this.options.exchangeName, topic, Buffer.from(JSON.stringify(message)), { 135 | replyTo: this.replyQueue, 136 | appId: this.options.serviceName, 137 | timestamp: new Date().getTime(), 138 | correlationId, 139 | ...options, 140 | }); 141 | this.logger.debug(message, `Sent ▲,${topic}`); 142 | }); 143 | } 144 | 145 | public async notify(topic: string, message: IMessage, options?: IPublishOptions): Promise { 146 | await this.initializationCheck(); 147 | await this.clientChannel.publish(this.options.exchangeName, topic, Buffer.from(JSON.stringify(message)), { 148 | appId: this.options.serviceName, 149 | timestamp: new Date().getTime(), 150 | ...options, 151 | }); 152 | this.logger.debug(message, `Notify ▲,${topic}`); 153 | } 154 | 155 | public healthCheck() { 156 | return this.isConnected; 157 | } 158 | 159 | public async disconnect() { 160 | this.detachEmitters(); 161 | this.sendResponseEmitter.removeAllListeners(); 162 | await this.clientChannel.close(); 163 | await this.subscriptionChannel.close(); 164 | await this.server.close(); 165 | } 166 | 167 | private createConnectionUri(connection: IRMQConnection): string { 168 | let uri = `${connection.protocol ?? RMQ_PROTOCOL.AMQP}://${connection.login}:${connection.password}@${ 169 | connection.host 170 | }`; 171 | if (connection.port) { 172 | uri += `:${connection.port}`; 173 | } 174 | if (connection.vhost) { 175 | uri += `/${connection.vhost}`; 176 | } 177 | return uri; 178 | } 179 | 180 | private async createSubscriptionChannel() { 181 | return new Promise((resolve) => { 182 | this.subscriptionChannel = this.server.createChannel({ 183 | json: false, 184 | setup: async (channel: Channel) => { 185 | await channel.assertExchange( 186 | this.options.exchangeName, 187 | this.options.assertExchangeType ? this.options.assertExchangeType : 'topic', 188 | { 189 | durable: this.options.isExchangeDurable ?? true, 190 | ...this.options.exchangeOptions, 191 | } 192 | ); 193 | await channel.prefetch( 194 | this.options.prefetchCount ?? DEFAULT_PREFETCH_COUNT, 195 | this.options.isGlobalPrefetchCount ?? false 196 | ); 197 | if (typeof this.options.queueName === 'string') { 198 | this.listen(channel); 199 | } 200 | this.logConnected(); 201 | resolve(); 202 | }, 203 | }); 204 | }); 205 | } 206 | 207 | private async createClientChannel() { 208 | return new Promise((resolve) => { 209 | this.clientChannel = this.server.createChannel({ 210 | json: false, 211 | setup: async (channel: Channel) => { 212 | await channel.consume( 213 | this.replyQueue, 214 | (msg: Message) => { 215 | this.sendResponseEmitter.emit(msg.properties.correlationId, msg); 216 | }, 217 | { 218 | noAck: true, 219 | } 220 | ); 221 | resolve(); 222 | }, 223 | }); 224 | }); 225 | } 226 | 227 | private async listen(channel: Channel) { 228 | const queue = await channel.assertQueue(this.options.queueName, { 229 | durable: this.options.isQueueDurable ?? true, 230 | arguments: this.options.queueArguments ?? {}, 231 | ...this.options.queueOptions, 232 | }); 233 | this.options.queueName = queue.queue; 234 | this.routes = this.metadataAccessor.getAllRMQPaths(); 235 | 236 | if (this.options.autoBindingRoutes ?? true) { 237 | await this.bindRMQRoutes(channel); 238 | } 239 | 240 | await channel.consume( 241 | this.options.queueName, 242 | async (msg: Message) => { 243 | this.logger.debug(msg.content, `Received ▼,${msg.fields.routingKey}`); 244 | const route = this.getRouteByTopic(msg.fields.routingKey); 245 | if (route) { 246 | msg = await this.useMiddleware(msg); 247 | requestEmitter.emit(route, msg); 248 | } else { 249 | this.reply('', msg, new RMQError(ERROR_NO_ROUTE, ERROR_TYPE.TRANSPORT)); 250 | this.ack(msg); 251 | } 252 | }, 253 | { noAck: false } 254 | ); 255 | } 256 | 257 | private async bindRMQRoutes(channel: Channel): Promise { 258 | if (this.routes.length > 0) { 259 | this.routes.map(async (r) => { 260 | this.logger.log(`Mapped ${r}`, 'RMQRoute'); 261 | await channel.bindQueue(this.options.queueName, this.options.exchangeName, r); 262 | }); 263 | } 264 | } 265 | 266 | private detachEmitters(): void { 267 | responseEmitter.removeAllListeners(); 268 | } 269 | 270 | private attachEmitters(): void { 271 | responseEmitter.on(ResponseEmitterResult.success, async (msg, result) => { 272 | this.reply(result, msg); 273 | }); 274 | responseEmitter.on(ResponseEmitterResult.error, async (msg, err) => { 275 | this.reply('', msg, err); 276 | }); 277 | responseEmitter.on(ResponseEmitterResult.ack, async (msg) => { 278 | this.ack(msg); 279 | }); 280 | } 281 | 282 | private async reply(res: any, msg: Message, error: Error | RMQError = null) { 283 | try { 284 | res = await this.intercept(res, msg, error); 285 | } catch (e) { 286 | error = e; 287 | } 288 | await this.subscriptionChannel.sendToQueue(msg.properties.replyTo, Buffer.from(JSON.stringify(res)), { 289 | correlationId: msg.properties.correlationId, 290 | headers: { 291 | ...this.errorService.buildError(error), 292 | }, 293 | }); 294 | this.logger.debug(res, `Sent ▲,${msg.fields.routingKey}`); 295 | } 296 | 297 | private getRouteByTopic(topic: string): string { 298 | return this.routes.find((route) => { 299 | if (route === topic) { 300 | return true; 301 | } 302 | const regexString = '^' + route.replace(/\*/g, '([^.]+)').replace(/#/g, '([^.]+.?)+') + '$'; 303 | return topic.search(regexString) !== -1; 304 | }); 305 | } 306 | 307 | private async useMiddleware(msg: Message) { 308 | if (!this.options.middleware || this.options.middleware.length === 0) { 309 | return msg; 310 | } 311 | for (const middleware of this.options.middleware) { 312 | msg = await new middleware(this.logger).transform(msg); 313 | } 314 | return msg; 315 | } 316 | 317 | private async intercept(res: any, msg: Message, error?: Error) { 318 | if (!this.options.intercepters || this.options.intercepters.length === 0) { 319 | return res; 320 | } 321 | for (const intercepter of this.options.intercepters) { 322 | res = await new intercepter(this.logger).intercept(res, msg, error); 323 | } 324 | return res; 325 | } 326 | 327 | private async initializationCheck() { 328 | if (this.isInitialized) { 329 | return; 330 | } 331 | await new Promise((resolve) => { 332 | setTimeout(() => { 333 | resolve(); 334 | }, INITIALIZATION_STEP_DELAY); 335 | }); 336 | await this.initializationCheck(); 337 | } 338 | 339 | private logConnected() { 340 | this.logger.log(CONNECTED_MESSAGE, 'RMQModule'); 341 | if (!this.options.queueName && this.metadataAccessor.getAllRMQPaths().length > 0) { 342 | this.logger.warn(ERROR_NO_QUEUE, 'RMQModule'); 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /lib/utils/get-uniq-id.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto'; 2 | 3 | export const getUniqId = (): string => randomUUID(); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-rmq", 3 | "version": "2.14.0", 4 | "description": "NestJS RabbitMQ Module", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "prepare": "tsc -p tsconfig.json", 9 | "test:e2e": "jest --config e2e/jest-e2e.json --runInBand", 10 | "test": "jest --config jest.json", 11 | "lint": "eslint ./lib/** --fix" 12 | }, 13 | "author": "Anton Larichev ", 14 | "keywords": [ 15 | "amqp", 16 | "rabbitmq", 17 | "typescript", 18 | "nodejs", 19 | "nestjs" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/AlariCode/nestjs-rmq" 24 | }, 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/AlariCode/nestjs-rmq/issues" 28 | }, 29 | "homepage": "https://github.com/AlariCode/nestjs-rmq", 30 | "dependencies": { 31 | "@types/amqplib": "^0.10.3", 32 | "amqp-connection-manager": "^4.1.14", 33 | "amqplib": "^0.10.3", 34 | "chalk": "^4.1.2", 35 | "class-transformer": "^0.5.1", 36 | "class-validator": "^0.14.0", 37 | "reflect-metadata": "^0.1.13" 38 | }, 39 | "devDependencies": { 40 | "@nestjs/common": "^10.2.7", 41 | "@nestjs/core": "^10.2.7", 42 | "@nestjs/platform-express": "^10.2.7", 43 | "@nestjs/testing": "^10.2.7", 44 | "@types/amqp-connection-manager": "^3.4.1", 45 | "@types/chalk": "^2.2.0", 46 | "@types/jest": "^29.5.6", 47 | "@types/node": "^20.8.7", 48 | "@typescript-eslint/eslint-plugin": "^6.8.0", 49 | "@typescript-eslint/parser": "^6.8.0", 50 | "eslint": "^8.51.0", 51 | "eslint-config-prettier": "^9.0.0", 52 | "eslint-plugin-prettier": "^5.0.1", 53 | "jest": "^29.7.0", 54 | "prettier": "^3.0.3", 55 | "rxjs": "^7.8.1", 56 | "ts-jest": "^29.1.1", 57 | "ts-node": "^10.9.1", 58 | "typescript": "^5.2.2" 59 | }, 60 | "peerDependencies": { 61 | "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es6", 11 | "sourceMap": false, 12 | "outDir": "./dist", 13 | "rootDir": "./lib", 14 | "skipLibCheck": true 15 | }, 16 | "include": [ 17 | "lib/**/*" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } --------------------------------------------------------------------------------