GithubHelp home page GithubHelp logo

alaricode / nestjs-rmq Goto Github PK

View Code? Open in Web Editor NEW
288.0 7.0 39.0 1.36 MB

A custom library for NestJS microservice. It allows you to use RabbitMQ or AMQP.

Home Page: https://purpleschool.ru

License: MIT License

TypeScript 98.29% JavaScript 1.71%
nestjs rabbitmq amqp microservices nodejs typescript javascript

nestjs-rmq's Introduction

NestJS - RabbitMQ custom strategy

alt cover

More NestJS libs on purpleschool.ru

npm version npm version npm version npm version

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.

Updated for NestJS 9!

Why use this over RabbitMQ transport in NestJS docs?

  • Support for RMQ queue patterns with * and #.
  • Using exchanges with topic bindings rather the direct queue sending.
  • Additional forTest() method for emulating messages in unit or e2e tests without needing of RabbitMQ instance.
  • Additional decorators for getting info out of messages.
  • Support for class-validator decorators.
  • Real production usage with more than 100 microservices.

Start

First, install the package:

npm i nestjs-rmq

Setup your connection in root module:

import { RMQModule } from 'nestjs-rmq';

@Module({
	imports: [
		RMQModule.forRoot({
			exchangeName: configService.get('AMQP_EXCHANGE'),
			connections: [
				{
					login: configService.get('AMQP_LOGIN'),
					password: configService.get('AMQP_PASSWORD'),
					host: configService.get('AMQP_HOST'),
				},
			],
		}),
	],
})
export class AppModule {}

In forRoot() you pass connection options:

  • exchangeName (string) - Exchange that will be used to send messages to.
  • connections (Object[]) - Array of connection parameters. You can use RMQ cluster by using multiple connections.

Additionally, you can use optional parameters:

  • 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. Example:
{
	exchangeName: 'my_exchange',
	connections: [
		{
			login: 'admin',
			password: 'admin',
			host: 'localhost',
		},
	],
	queueName: 'my-service-queue',
}
  • connectionOptions (object) - Additional connection options. You can read more here.
  • prefetchCount (boolean) - You can read more here.
  • isGlobalPrefetchCount (boolean) - You can read more here.
  • queueOptions (object) - options for created queue.
  • reconnectTimeInSeconds (number) - Time in seconds before reconnection retry. Default is 5 seconds.
  • heartbeatIntervalInSeconds (number) - Interval to send heartbeats to broker. Defaults to 5 seconds.
  • queueArguments (!!! deprecated. Use queueOptions instead) - You can read more about queue parameters here.
  • messagesTimeout (number) - Number of milliseconds 'post' method will wait for the response before a timeout error. Default is 30 000.
  • isQueueDurable (!!! deprecated. Use queueOptions instead) - Makes created queue durable. Default is true.
  • isExchangeDurable (!!! deprecated. Use exchangeOptions instead) - Makes created exchange durable. Default is true.
  • exchangeOptions (Options.AssertExchange) - You can read more about exchange options here.
  • logMessages (boolean) - Enable printing all sent and recieved messages in console with its route and content. Default is false.
  • logger (LoggerService) - Your custom logger service that implements LoggerService interface. Compatible with Winston and other loggers.
  • 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.
  • errorHandler (class) - custom error handler for dealing with errors from replies, use errorHandler in module options and pass class that extends RMQErrorHandler.
  • serviceName (string) - service name for debugging.
  • autoBindingRoutes (boolean) - set false you want to manage route binding manualy. Default to true.
class LogMiddleware extends RMQPipeClass {
	async transfrom(msg: Message): Promise<Message> {
		console.log(msg);
		return msg;
	}
}
  • 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.
export class MyIntercepter extends RMQIntercepterClass {
	async intercept(res: any, msg: Message, error: Error): Promise<any> {
		// res - response body
		// msg - initial message we are replying to
		// error - error if exists or null
		return res;
	}
}

Config example with middleware and intercepters:

import { RMQModule } from 'nestjs-rmq';

@Module({
	imports: [
		RMQModule.forRoot({
			exchangeName: configService.get('AMQP_EXCHANGE'),
			connections: [
				{
					login: configService.get('AMQP_LOGIN'),
					password: configService.get('AMQP_PASSWORD'),
					host: configService.get('AMQP_HOST'),
				},
			],
			middleware: [LogMiddleware],
			intercepters: [MyIntercepter],
		}),
	],
})
export class AppModule {}

Async initialization

If you want to inject dependency into RMQ initialization like Configuration service, use forRootAsync:

import { RMQModule } from 'nestjs-rmq';
import { ConfigModule } from './config/config.module';
import { ConfigService } from './config/config.service';

@Module({
	imports: [
		RMQModule.forRootAsync({
			imports: [ConfigModule],
			inject: [ConfigService],
			useFactory: (configService: ConfigService) => {
				return {
					exchangeName: 'test',
					connections: [
						{
							login: 'guest',
							password: 'guest',
							host: configService.getHost(),
						},
					],
					queueName: 'test',
				};
			},
		}),
	],
})
export class AppModule {}
  • useFactory - returns IRMQServiceOptions.
  • imports - additional modules for configuration.
  • inject - additional services for usage inside useFactory.

Sending messages

To send message with RPC topic use send() method in your controller or service:

@Injectable()
export class ProxyUpdaterService {
	constructor(private readonly rmqService: RMQService) {}

	myMethod() {
		this.rmqService.send<number[], number>('sum.rpc', [1, 2, 3]);
	}
}

This method returns a Promise. First type - is a type you send, and the second - you recive.

  • 'sum.rpc' - name of subscription topic that you are sending to.
  • [1, 2, 3] - data payload. To get a reply:
this.rmqService.send<number[], number>('sum.rpc', [1, 2, 3])
    .then(reply => {
        //...
    })
    .catch(error: RMQError => {
        //...
    });

Also you can use send options:

this.rmqService.send<number[], number>('sum.rpc', [1, 2, 3], {
	expiration: 1000,
	priority: 1,
	persistent: true,
	timeout: 30000,
});
  • expiration - if supplied, the message will be discarded from a queue once it’s been there longer than the given number of milliseconds.
  • priority - a priority for the message.
  • persistent - if truthy, the message will survive broker restarts provided it’s in a queue that also survives restarts.
  • timeout - if supplied, the message will have its own timeout.

If you want to just notify services:

const a = this.rmqService.notify<string>('info.none', 'My data');

This method returns a Promise.

  • 'info.none' - name of subscription topic that you are notifying.
  • 'My data' - data payload.

Recieving messages

To listen for messages bind your controller or service methods to subscription topics with RMQRoute() decorator:

export class AppController {
	//...

	@RMQRoute('sum.rpc')
	sum(numbers: number[]): number {
		return numbers.reduce((a, b) => a + b, 0);
	}

	@RMQRoute('info.none')
	info(data: string) {
		console.log(data);
	}
}

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:

this.rmqService.send('sum.rpc', [1, 2, 3]).then((reply) => {
	// reply: 6
});

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.

@RMQRoute('my.rpc')
myMethod(numbers: number[]): number {
	//...
    throw new RMQError('Error message', 2);
	throw new Error('Error message');
	//...
}

Message patterns

With exchange type topic you can use message patterns to subscribe to messages that corresponds to that pattern. You can use special symbols:

  • * - (star) can substitute for exactly one word.
  • #- (hash) can substitute for zero or more words.

For example:

  • Pattern *.*.rpc will match my.own.rpc or any.other.rpc and will not match this.is.cool.rpc or my.rpc.
  • Pattern compute.# will match compute.this.equation.rpc and will not do.compute.anything.

To subscribe to pattern, use it as route:

import { RMQRoute } from 'nestjs-rmq';

@RMQRoute('*.*.rpc')
myMethod(): number {
	// ...
}

Note: If two routes patterns matches message topic, only the first will be used.

Getting message metadata

To get more information from message (not just content) you can use @RMQMessage parameter decorator:

import { RMQRoute, Validate, RMQMessage, ExtendedMessage } from 'nestjs-rmq';

@RMQRoute('my.rpc')
myMethod(data: myClass, @RMQMessage msg: ExtendedMessage): number {
	// ...
}

You can get all message properties that RMQ gets. Example:

{
	"fields": {
		"consumerTag": "amq.ctag-1CtiEOM8ioNFv-bzbOIrGg",
		"deliveryTag": 2,
		"redelivered": false,
		"exchange": "test",
		"routingKey": "appid.rpc"
	},
	"properties": {
		"contentType": "undefined",
		"contentEncoding": "undefined",
		"headers": {},
		"deliveryMode": "undefined",
		"priority": "undefined",
		"correlationId": "ce7df8c5-913c-2808-c6c2-e57cfaba0296",
		"replyTo": "amq.rabbitmq.reply-to.g2dkABNyYWJiaXRAOTE4N2MzYWMyM2M0AAAenQAAAAAD.bDT8S9ZIl5o3TGjByqeh5g==",
		"expiration": "undefined",
		"messageId": "undefined",
		"timestamp": "undefined",
		"type": "undefined",
		"userId": "undefined",
		"appId": "test-service",
		"clusterId": "undefined"
	},
	"content": "<Buffer 6e 75 6c 6c>"
}

TSL/SSL support

To configure certificates and learn why do you need it, read here.

To use amqps connection:

RMQModule.forRoot({
	exchangeName: 'test',
	connections: [
		{
			protocol: RMQ_PROTOCOL.AMQPS, // new
			login: 'admin',
			password: 'admin',
			host: 'localhost',
		},
	],
	connectionOptions: {
		cert: fs.readFileSync('clientcert.pem'),
		key: fs.readFileSync('clientkey.pem'),
		passphrase: 'MySecretPassword',
		ca: [fs.readFileSync('cacert.pem')]
	} // new
}),

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.

To use it with pkcs12 files:

connectionOptions: {
	pfx: fs.readFileSync('clientcertkey.p12'),
	passphrase: 'MySecretPassword',
	ca: [fs.readFileSync('cacert.pem')]
},

Manual message Ack/Nack

If you want to use your own ack/nack logic, you can set manual acknowledgement to @RMQRoute. Than in any place you have to manually ack/nack message that you get with @RMQMessage.

import { RMQRoute, Validate, RMQMessage, ExtendedMessage, RMQService } from 'nestjs-rmq';

@Controller()
export class MyController {
	constructor(private readonly rmqService: RMQService) {}

	@RMQRoute('my.rpc', { manualAck: true })
	myMethod(data: myClass, @RMQMessage msg: ExtendedMessage): number {
		// Any logic goes here
		this.rmqService.ack(msg);
		// Any logic goes here
	}

	@RMQRoute('my.other-rpc', { manualAck: true })
	myOtherMethod(data: myClass, @RMQMessage msg: ExtendedMessage): number {
		// Any logic goes here
		this.rmqService.nack(msg);
		// Any logic goes here
	}
}

Send debug information to error or log

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.

import { RMQRoute, Validate, RMQMessage, ExtendedMessage, RMQService } from 'nestjs-rmq';

@Controller()
export class MyController {
	constructor(private readonly rmqService: RMQService) {}

	@RMQRoute('my.rpc')
	myMethod(data: myClass, @RMQMessage msg: ExtendedMessage): number {
		// ...
		console.log(msg.getDebugString());
		// ...
	}
}

You will get info about message, field and properties:

{
	"fields": {
		"consumerTag": "amq.ctag-Q-l8A4Oh76cUkIKbHWNZzA",
		"deliveryTag": 4,
		"redelivered": false,
		"exchange": "test",
		"routingKey": "debug.rpc"
	},
	"properties": {
		"headers": {},
		"correlationId": "388236ad-6f01-3de5-975d-f9665b73de33",
		"replyTo": "amq.rabbitmq.reply-to.g1hkABNyYWJiaXRANzQwNDVlYWQ5ZTgwAAAG2AAAAABfmnkW.9X12ySrcM6BOXpGXKkR+Yg==",
		"timestamp": 1603959908996,
		"appId": "test-service"
	},
	"message": {
		"prop1": [1],
		"prop2": "Buffer - length 11"
	}
}

Customizing massage with msgFactory

@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.

To do that, you may pass a param to the RMQRoute a custom message factory msgFactory?: (msg: Message) => any;.

The default msgFactory:

@RMQRoute('topic', {
	msgFactory: (msg: Message) => JSON.parse(msg.content.toString())
})

Custom msgFactory that returns additional argument (sender appId) and change request:

@RMQRoute(CustomMessageFactoryContracts.topic, {
	msgFactory: (msg: Message) => {
		const content: CustomMessageFactoryContracts.Request = JSON.parse(msg.content.toString());
		content.num = content.num * 2;
		return [content, msg.properties.appId];
	}
})
customMessageFactory({ num }: CustomMessageFactoryContracts.Request, appId: string): CustomMessageFactoryContracts.Response {
	return { num, appId };
}

Validating data

NestJS-rmq uses class-validator to validate incoming data. To use it, decorate your route method with RMQValidate:

import { RMQRoute, RMQValidate } from 'nestjs-rmq';

@RMQValidate()
@RMQRoute('my.rpc')
myMethod(data: myClass): number {
	// ...
}

Where myClass is data class with validation decorators:

import { IsString, MinLength, IsNumber } from 'class-validator';

export class myClass {
	@MinLength(2)
	@IsString()
	name: string;

	@IsNumber()
	age: string;
}

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.

Transforming data

NestJS-rmq uses class-transformer to transform incoming data. To use it, decorate your route method with RMQTransform:

import { RMQRoute, RMQTransform } from 'nestjs-rmq';

@RMQTransform()
@RMQValidate()
@RMQRoute('my.rpc')
myMethod(data: myClass): number {
	// ...
}

Where myClass is data class with transformation decorators:

import { Type } from 'class-transformer';
import { IsDate } from 'class-validator';

export class myClass {
	@IsDate()
	@Type(() => Date)
	date: Date;
}

After this you can use data.date in your controller as Date object and not a string. You can check class-validator docs here. You can use transformation and validation at the same time - first transformation will be applied and then validation.

Using pipes

To intercept any message to any route, you can use @RMQPipe decorator:

import { RMQRoute, RMQPipe } from 'nestjs-rmq';

@RMQPipe(MyPipeClass)
@RMQRoute('my.rpc')
myMethod(numbers: number[]): number {
	//...
}

where MyPipeClass extends RMQPipeClass with one method transform:

class MyPipeClass extends RMQPipeClass {
	async transfrom(msg: Message): Promise<Message> {
		// do something
		return msg;
	}
}

Using RMQErrorHandler

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:

class MyErrorHandler extends RMQErrorHandler {
	public static handle(headers: IRmqErrorHeaders): Error | RMQError {
		// do something
		return new RMQError(
			headers['-x-error'],
			headers['-x-type'],
			headers['-x-status-code'],
			headers['-x-data'],
			headers['-x-service'],
			headers['-x-host']
		);
	}
}

HealthCheck

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:

const isConnected = this.rmqService.healthCheck();

If isConnected equals true, you are successfully connected.

Disconnecting

If you want to close connection, for example, if you are using RMQ in testing tools, use disconnect() method;

Unit and E2e tests

Using in tests

RMQ library supports using RMQ module in your test suites without needing RabbitMQ instance. To use library in tests, use forTest method in module.

import { RMQTestService } from 'nestjs-rmq';

let rmqService: RMQTestService;

beforeAll(async () => {
	const apiModule = await Test.createTestingModule({
		imports: [RMQModule.forTest({})],
		controllers: [MicroserviceController],
	}).compile();
	api = apiModule.createNestApplication();
	await api.init();

	rmqService = apiModule.get(RMQService);
});

You can pass any options you pass in normal forRoot (except errorHandler).

From module, you will get rmqService which is similar to normal service, with two additional methods:

  • triggerRoute - trigger your RMQRoute, simulating incoming message.
  • mockReply - mock reply if you are using send method.
  • mockError - mock error if you are using send method.

triggerRoute

Emulates message received buy your RMQRoute.

const { result } = await rmqService.triggerRoute<Request, Response>(topic, data);
  • topic - topic, that you want to trigger (pattern supported).
  • data - data to send in your method.

mockReply

If your service needs to send data to other microservice, you can emulate its reply with:

rmqService.mockReply(topic, res);
  • topic - all messages sent to this topic will be mocked.
  • res - mocked response data.

After this, all rmqService.send(topic, { ... }) calls will return res data.

mockError

If your service needs to send data to other microservice, you can emulate its error with:

rmqService.mockError(topic, error);
  • topic - all messages sent to this topic will be mocked.
  • error - error that send method will throw.

After this, all rmqService.send(topic, { ... }) calls will throw error.

Contributing

For e2e tests you need to install Docker in your machine and start RabbitMQ docker image with docker-compose.yml in e2e folder:

docker-compose up -d

Then change IP in tests to localhost and run tests with:

npm run test:e2e

alt cover

For unit tests just run:

npm run test

Migrating from version 1

New version of nestjs-rmq contains minor breaking changes, and is simple to migrate to.

  • @RMQController decorator is deprecated. You will get warning if you continue to use it, and it will be deleted in future versions. 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
  • msgFactory changed its interface from
msgFactory?: (msg: Message, topic: IRouteMeta) => any[];

to

msgFactory?: (msg: Message) => any[];

because all IRouteMeta already contained in Message.

  • msgFactory can be passed to @RMQRoute instead of @RMQController

nestjs-rmq's People

Contributors

alaricode avatar dependabot[bot] avatar djflyte avatar falahati avatar gustavoms avatar kevalin avatar mikelavigne avatar milovidov983 avatar minenkom avatar mjarmoc avatar ponomarevkonst avatar rollingneko avatar soft-atom avatar timursevimli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

nestjs-rmq's Issues

Update packages please

npm audit report

jsonwebtoken <=8.5.1
Severity: high
jsonwebtoken has insecure input validation in jwt.verify function - GHSA-27h2-hvpr-p74q
No fix available
node_modules/jsonwebtoken
@nestjs/jwt *
Depends on vulnerable versions of jsonwebtoken
node_modules/@nestjs/jwt
passport-jwt <=4.0.0
Depends on vulnerable versions of jsonwebtoken
node_modules/passport-jwt

3 high severity vulnerabilities

To address issues that do not require attention, run:
npm audit fix

Some issues need review, and may require choosing
a different dependency.

jsonwebtoken <=8.5.1
Severity: high
jsonwebtoken has insecure input validation in jwt.verify function - GHSA-27h2-hvpr-p74q
No fix available
node_modules/jsonwebtoken
@nestjs/jwt *
Depends on vulnerable versions of jsonwebtoken
node_modules/@nestjs/jwt
passport-jwt <=4.0.0
Depends on vulnerable versions of jsonwebtoken
node_modules/passport-jwt

3 high severity vulnerabilities

library goes bootloop when its connected

Im connecting library as shown in docs, but whenever I try to use it, it goes bootloop (server is not starting fully and not responding to requests), but no errors are shown in bash

result with nest-rmq imported:
image

result without inport:
image

Requested service doesn't have RMQRoute with this path

Hello @AlariCode,
I'm trying to connect nest.js with python, and return this error
RMQError: Requested service doesn't have RMQRoute with this path

// server.ts
  async generate(id: number): Promise<any> {
    try {
      return await this.rmqService.send('generate', id, { timeout: 30000 });
    } catch (error) {
      // ...
    }
  }
// main.py
#!/usr/bin/env python3
import os
import pika as broker
import json
from dotenv import load_dotenv, find_dotenv

load_dotenv(find_dotenv())

class Application:
    def __init__(self):
        self.connection = broker.BlockingConnection(
            broker.ConnectionParameters(host=os.getenv("RABBITMQ_HOST")))
        self.channel = self.connection.channel()

        self.channel.queue_declare(queue=os.getenv("RABBITMQ_QUEUE"), exclusive=False)
        self.channel.basic_consume(queue=os.getenv("RABBITMQ_QUEUE"), on_message_callback=self.callback, auto_ack=False)

        print("[DEBUG] Waiting for messages. To exit press CTRL+C")
        self.channel.start_consuming()

    def callback(self, ch, method, props, body):
        try:
            print("[DEBUG] Received")
            self.channel.basic_ack(delivery_tag = method.delivery_tag)
            self.channel.basic_publish(exchange='pdfs', routing_key=props.reply_to, body=json.dumps({'pattern': 'generate' }))
        except Exception as e:
            print(e)


if __name__ == "__main__":
    Application()
  }

A full example on how to set this up on both ends (Gateway + Microservice)

Hi, Would you kindly provide a small example on how to set up a microservice and a gateway (consumer) that receives API requests?

Do I have to configure my microservice and start it, and then how to configure the consumer app? Do I need to start these normally, and install this package and will get everything working straight up? I mean, I don't even need to install @nestjs/microservices packages?

Thank you

Feature Request: Exclusive Queue

This is a feature request for allowing users to create a nameless exclusive queue.

Defining a queue without a name automatically generates a random name for the queue but this library regards an empty queue name as the absence of a value and therefore such functionality provided by the underlying library is not available to the users of this library.

Also, an isQueueExlusive config can be added to allow users to specifically set the exclusivity of the defined queue with the default value of true for when the queue name is empty and false when there is a specific queue name provided.

This is a simple change and I can probably offer to create a small PR if you are willing to accept it.

Dead code - isTopicExists

if (this.isTopicExists(msg.fields.routingKey)) {
msg = await this.useMiddleware(msg);
requestEmitter.emit(msg.fields.routingKey, msg);
} else {
this.reply('', msg, new Error(ERROR_NO_ROUTE));
}

I believe this one will never get called. If there is no route, then it won't be registered as a routing key in the exchange. If so, then the message will never reach the queue, thus won't reach the rmq-controller.

when using other service in the RMQRoute, it will be undefined

  @RMQRoute('businesses.create')
  async createBusiness(createBusinessDto: CreateBusinessDto) {
    
    const business = Object.assign(new Business(), createBusinessDto);

    const createBusiness = await this.businessesService.createBusiness(
      createBusinessDto.clientKey,
      business,
    );
    return createBusiness;
  }

If I put this.businessesService.createBusiness into this controller I will get this error:

Screen Shot 2021-03-26 at 10 06 29 am

However, if take it out, everything works well.

 @RMQRoute('businesses.create')
  async createBusiness(createBusinessDto: CreateBusinessDto) {
    
    const business = Object.assign(new Business(), createBusinessDto);

    return business;
  }

Any ideas? Thanks

Как публиковать в разные exchange и queue?

Пытался добавить несколько конфигураций и по разному регистрировать несколько раз в модуле, но после того как я пытаюсь получить их то все время возвращается один и тот же инстанс. Создал отдельный модуль и в нем другим конфигом инициализировал и сделал экспорт в основной модуль все равно одно и то же
Подскажите пожалуйста как нужно сделать чтоб пользоваться несколькими exchange

Update package please

Anton, please increase
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0"
}
to last version of nest.

Thanks!

Add queues per function

Add this feature if its possible.
We would like to add queues per function.
Users using rabbitmq only need to know the name of the functions.

@RMQRoute({queue: '/cmd/sum', 'routingKey': '/cmd/sum', ...})
sum(numbers: number[]): number {
	return numbers.reduce((a, b) => a + b, 0);
}

@RMQRoute({queue: '/cmd/info', 'routingKey': '/cmd/info', ...})
info(data: string) {
	console.log(data);
}

`

Example:
image

image

Feature Request: Multi exchange and multi queue binding

This feature request asks for the possibility of defining more than one exchange or queue and to allow selecting the right queue or exchange using the decorator. This allows for wider advanced usages like for example when a queue is used exclusively by an instance of the service for getting notifications while another is used for load balancing messages or when two exchanges has to be used for a single topic/message.
Implementing this feature also greatly decreases the need for non-global feature specific support for the library.

To achieve this and still be compatible with the older version I suggest adding a new queues and exchanges configurable properties with information about one or more exchanges or queues.

A bindingOption optional argument is then can be added to the decorator allowing it to gets binded to a all or one or more queues or exchanges.

channel.consume obviously has to change to account for this change but it is the only heavy part of this feature.

Are you ok with me creating a PR for this?

P.S. Been using this library in production with a dozen microservices happily for the last year, good job, and thank you.

[ExceptionsHandler] Requested service doesn't have RMQRoute with this path

Hello @AlariCode
I write a rpc-server.ts and a rpc-client.ts that they are running different TCP ports.

// .env
AMQP_EXCHANGE=test.topic
AMQP_LOGIN=dlz
AMQP_PASSWORD=123456
AMQP_HOST=127.0.0.1
AMQP_PORT=5672
// rpc-server.ts
import { Controller } from '@nestjs/common';
import { ExtendedMessage, RMQMessage, RMQRoute, RMQService } from 'nestjs-rmq';

@Controller()
export class AppController {
  constructor(private readonly rmqService: RMQService) {}

  @RMQRoute('sum.rpc', { manualAck: true })
  sum(numbers: number[], @RMQMessage msg: ExtendedMessage) {
    const ret = numbers.reduce((a, b) => a + b);
    this.rmqService.ack(msg);
    return ret;
  }
}
// rpc-client.ts
import { Controller, Get, Query } from '@nestjs/common';
import { RMQService } from 'nestjs-rmq';

@Controller()
export class AppController {
  constructor(private readonly rmq: RMQService) {}

  @Get('sum')
  async httpSum(@Query() query: { nums: string }) {
    const { nums } = query;
    return await this.rmq.send<number[], number>(
      'sum.rpc',
      nums.split(',').map(Number),
    );
  }
}
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

import { RMQModule } from 'nestjs-rmq';
import { ConfigModule, ConfigService } from '@nestjs/config';

import configiration from 'src/config/configuration';

@Module({
  imports: [
    RMQModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        const amqp = configService.get('amqp');
        return {
          exchangeName: amqp.exchange,
          connections: [
            {
              login: amqp.login,
              password: amqp.password,
              host: amqp.host,
              port: amqp.port,
            },
          ],
          queueName: 'test',
          serviceName: 'test',
          logMessages: true
        };
      },
    }),
    ConfigModule.forRoot({ load: [configiration] }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Right now, I request 'http://localhost:3000/sum?nums=1,2,3' by Postman
1, I got 6
2, I got a 500 error, rpc-client.ts throw a Error as same as the issue title
3, I got 6
4, I got a 500 error ... and so on 🤣

What's happend? I don't know.
Next, I used ab test the http url.

ab -n 1000 -c 10 http://localhost:3000/sum?nums=1,2,3

Result: 500 failds, 50% that's very strange.

Now, I am reading your code, I hope get a answer.

Manual processing problem

Hi, Thank you for coding such awesome library.
Is it possible for us to manually process the results?

this.rmqService.notify < string > ('info.none', 'My data!!');

The above code is going to drive the message into unacked, and when my service restarts, it's going to be accepted again. Can we get it off the queue?

Putting errors into a queue

Hello.

Thank you very much for this great module.

There is a scenario where I want to avoid reply-to RPC and want to put errors along with the original data into a specific error queue to be processed later.

How is this possible?

Catch if Rabbit server is down

How can I check if the RabbitMQ server is down?
In the client, I shutdown RabbitMQ local server to check if I can catch the error but all I receive is:

(node:90094) UnhandledPromiseRejectionWarning: Error: connect ECONNREFUSED 127.0.0.1:5672
    at Object._errnoException (util.js:992:11)
    at _exceptionWithHostPort (util.js:1014:20)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1186:14)
(node:90094) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:90094) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
(node:90094) UnhandledPromiseRejectionWarning: Error: connect ECONNREFUSED 127.0.0.1:5672
    at Object._errnoException (util.js:992:11)
    at _exceptionWithHostPort (util.js:1014:20)
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1186:14)
(node:90094) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)

This happens as a response to

    this.client = new ClientRMQ({
      url: `${process.env.CLOUDAMQP_URL}`,
      queue: 'ampanova_mails',
      queueOptions: { durable: false }
    })

Listen a queue messages, and not queue topics. Its Possible?

Hi! Very nice lib maan. Thank's for that!!

So, my question is about listening a queue, and not a topic.

I'm interested in rabbitmq queue messages, topic is only a path key that we use, and need to be mutable. So how can I listen all messages from queue and not a specific topic?

To be more specific. I have an queue binding which have #.topic. And some producers publish a.topic and b.topic messages. How can I listen all Queue messages, and not only specific topic?

I'm trying to use @RMQRoute('#.topic'), but its not working.

I'm referring to this feature https://www.rabbitmq.com/tutorials/tutorial-five-python.html

Thanks for your time.

Looking forward to the answer

Nestjs 9

The new major version of nest has been released.

I got a peer dependency conflict:
image

Are you going to update nestjs-rmq?

How do you ack messages?

Hello, great library, I couldn't find it in the docs, how do you acknowledge messages in handlers with this library?

Post and preprocessors feature

I propose to implement the following post and pre handlers in the library in which the library user would be able to handle calls at his discretion:

public enum MessageProcessResult {
    Ask,
    Reject,
    Nack,
}

/**
 * @summary
 * Raw payload from message broker
 */
interface IDeliveredMessage {}

/**
 * @summary
 * Common post-pre handlers lib configuration
 */
interface IQueueHandlersConfig {
    /**
     * @summary
     * The handler is started last in the chain, and you can change the result of the previous MessageProcessResult in it
     */
    afterExecute: (
        handler: (dm: IDeliveredMessage, result: MessageProcessResult) => Promise<MessageProcessResult>
    ) => void;
    /**
     * @summary
     * The handler is started first in the call chain.
     * Depending on the needs of the library user, the message can either be passed down the chain of handlers
     * `return true`, or the message `return false` can be ignored
     */
    beforeExecute: (handler: (dm: IDeliveredMessage) => Promise<boolean>) => void;
    /**
     * @summary
     * The handler is called in case of an exception in the client code
     *
     */
    onException: (handler: (error: Error, dm: IDeliveredMessage) => Promise<boolean>) => void;
    /**
     * @summary
     * Called if a message has arrived for which a handler is not assigned
     */
    onUnexpectedTopic: (handelr: (dm: IDeliveredMessage) => Promise<MessageProcessResult>) => void;
}

In general, I really need onException.

Let's discuss it, and I could implement this improvement myself, if possible, once we approve the interface and other details.

Example issue with configService?

Hi,
I am new at NestJs.
Is this example correct? I have no Idea how to get the "configService" as described in the example.
I only can find examples with use of "useFactory".

`import { RMQModule } from 'nestjs-rmq';

@module({
imports: [
RMQModule.forRoot({
exchangeName: configService.get('AMQP_EXCHANGE'),
connections: [
{
login: configService.get('AMQP_LOGIN'),
password: configService.get('AMQP_PASSWORD'),
host: configService.get('AMQP_HOST'),
},
],
}),
],
})
export class AppModule {}`

Sorry but don't get the code formating in the github editor

Accessing configService in module

Hi.

In your example:

import { RMQModule } from 'nestjs-tests';

@Module({
	imports: [
		RMQModule.forRoot({
			exchangeName: configService.get('AMQP_EXCHANGE'),
			connections: [
				{
					login: configService.get('AMQP_LOGIN'),
					password: configService.get('AMQP_PASSWORD'),
					host: configService.get('AMQP_HOST'),
				},
			],
		}),
	],
})
export class AppModule {}

Can you please explain how can we access the configService in this module? In the example that you provided you have hardcoded the settings.

Thanks.

Error handling broken for 2.8.0

When I use incorrect password or login in the connections array for my RabbitMQ instance, for example:

connections: [{
      login: 'incorrect_login',
      password: 'correct_password',
      host: 'correct_host'
    }],

The service didn't raise error in the console. I saw only logs:

[Nest] 35783 - 11/21/2022, 10:38:00 PM LOG [InstanceLoader] RMQModule dependencies initialized +0ms
[Nest] 35783 - 11/21/2022, 10:38:00 PM LOG [RoutesResolver] MyController {/api}: +2ms

And server didn't show any information just freeze.

This bug reproduced only for 2.8.0 version (tested at nestjs 8.0.0, 9.0.0)

Expected result

If I use 2.7.2 I see:

[Nest] 35783 - 11/21/2022, 10:38:00 PM LOG [InstanceLoader] RMQModule dependencies initialized +0ms
[Nest] 35783 - 11/21/2022, 10:38:00 PM LOG [RoutesResolver] MyController {/api}: +2ms
[Nest] 36063 - 11/21/2022, 10:41:14 PM ERROR Disconnected from RMQ. Trying to reconnect
[Nest] 36063 - 11/21/2022, 10:41:14 PM ERROR undefined
[Nest] 36063 - 11/21/2022, 10:41:14 PM ERROR undefined
[Nest] 36063 - 11/21/2022, 10:41:14 PM ERROR Error: Handshake terminated by server: 403 (ACCESS-REFUSED) with message "ACCESS_REFUSED - Login was refused using authentication mechanism PLAIN. For details see the broker logfile."
[Nest] 36063 - 11/21/2022, 10:41:14 PM ERROR undefined
[Nest] 36063 - 11/21/2022, 10:41:14 PM ERROR undefined

Determine serviceName on @RMQRoute

Hey, Thanks for this library and to solve my previous issue. 🚀

So, my question is:

how can i have multiples RMQModule at the same project?

I have 2 consumers. that listen 2 distinct queues

BAKERY CONSUMER

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RMQModule } from 'nestjs-rmq';
import { BakeryConsumerController } from './bakery-consumer.controller';

@Module({
  imports: [
    RMQModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        return {
          exchangeName: configService.get('RABBITMQ_EXCHANGE'),
          connections: [
            {
              login: configService.get('RABBITMQ_DEFAULT_USER'),
              password: configService.get('RABBITMQ_DEFAULT_PASS'),
              host: configService.get('RABBITMQ_URL'),
            },
          ],
          serviceName: 'Bakery',
          queueName: 'bakery',
        };
      },
    }),
  ],
  controllers: [BakeryConsumerController],
})
export class BakeryConsumerModule {}

PHARMACY CONSUMER

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { RMQModule } from 'nestjs-rmq';
import { PharmacyConsumerController } from './pharmacy-consumer.controller';

@Module({
  imports: [
    RMQModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => {
        return {
          exchangeName: configService.get('RABBITMQ_EXCHANGE'),
          connections: [
            {
              login: configService.get('RABBITMQ_DEFAULT_USER'),
              password: configService.get('RABBITMQ_DEFAULT_PASS'),
              host: configService.get('RABBITMQ_URL'),
            },
          ],
          serviceName: 'Pharmacy',
          queueName: 'pharmacy',
        };
      },
    }),
  ],
  controllers: [PharmacyConsumerController],
})
export class PharmacyConsumerModule {}

And when I declare a consumer inside each service, only the first module config is used. so ONLY the BAKERY queue is being listened.

Here my controllers

import { Controller } from '@nestjs/common';
import { RMQRoute } from 'nestjs-rmq';

@Controller('pharmacy-consumer')
export class PharmacyConsumerController {
  @RMQRoute('#')
  myMethod(message: any): any {
    console.log(message);
    return message;
  }
}
import { Controller } from '@nestjs/common';
import { RMQRoute } from 'nestjs-rmq';

@Controller('bakery-consumer')
export class BakeryConsumerController {
  @RMQRoute('#'})
  myMethod(message: any): any {
    console.log(message);
    return message;
  }
}

My suggestion is to use serviceName inside @RMQRoute like this

@RMQRoute('#', { serviceName: 'Bakery'})

There is another way to make this work on current version?

Cannot read property 'MICROSERVICE_READY' of undefined

Hi,

just tried ur lib (as we discussed over at the NestJS issue tracker). However, when I add nestjs-rmq (latest version from npm 0.1.2).

However when running it as described in the README, I get the following error:

TypeError: Cannot read property 'MICROSERVICE_READY' of undefined
    at NestMicroservice.listen (/Users/jstrumpflohner/sites/r3-gateway/node/proxy-coordinator/node_modules/@nestjs/microservices/nest-microservice.js:79:46)
    at exceptions_zone_1.ExceptionsZone.run (/Users/jstrumpflohner/sites/r3-gateway/node/proxy-coordinator/node_modules/@nestjs/core/nest-factory.js:99:48)
    at Function.run (/Users/jstrumpflohner/sites/r3-gateway/node/proxy-coordinator/node_modules/@nestjs/core/errors/exceptions-zone.js:8:13)
    at Proxy.args (/Users/jstrumpflohner/sites/r3-gateway/node/proxy-coordinator/node_modules/@nestjs/core/nest-factory.js:98:54)
    at /Users/jstrumpflohner/sites/r3-gateway/node/proxy-coordinator/src/main.ts:15:7
    at Generator.next (<anonymous>)
    at fulfilled (/Users/jstrumpflohner/sites/r3-gateway/node/proxy-coordinator/src/main.ts:4:58)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:228:7)
    at Function.Module.runMain (module.js:684:11)
    at Object.<anonymous> (/Users/jstrumpflohner/sites/r3-gateway/node/proxy-coordinator/node_modules/ts-node/src/bin.ts:157:12)
    at Module._compile (module.js:641:30)
    at Object.Module._extensions..js (module.js:652:10)
    at Module.load (module.js:560:32)
    at tryModuleLoad (module.js:503:12)
    at Function.Module._load (module.js:495:3)
(node:4009) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Unhandled Runtime Exception.
(node:4009) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Any idea?

My package.json deps

{
  "dependencies": {
    "@nestjs/common": "^5.1.0",
    "@nestjs/core": "^5.1.0",
    "nestjs-rmq": "^0.1.2",
    "reflect-metadata": "^0.1.12",
    "rxjs": "^6.2.2",
    "typescript": "^3.0.1"
  },
  "devDependencies": {
    "@nestjs/testing": "^5.1.0",
    "@types/express": "^4.16.0",
    "@types/jest": "^23.3.1",
    "@types/node": "^10.7.1",
    "@types/supertest": "^2.0.5",
    "jest": "^23.5.0",
    "nodemon": "^1.18.3",
    "prettier": "^1.14.2",
    "rimraf": "^2.6.2",
    "supertest": "^3.1.0",
    "ts-jest": "^23.1.3",
    "ts-loader": "^4.4.2",
    "ts-node": "^7.0.1",
    "tsconfig-paths": "^3.5.0",
    "tslint": "5.11.0",
    "webpack": "^4.16.5",
    "webpack-cli": "^3.1.0",
    "webpack-node-externals": "^1.7.2"
  }
}

thx

validation + manual ack/nack

If you use both validation via @RMQValidate and manual message ack/nack in the handler, then the handler stops working after the first invalid message, because it cannot send ack/nack message.

RabbitMQ URI

Is it possible to create the connection via the RabbitMQ URI?
i.e. amqp://user:pass@host:10000/vhost

Assert queue programmatically

Hello,
thanks for the nice project.
I'm interested to understand if there is a way to assert queues directly using your library or if you could recommend a solution based on amqp directly.
Thanks and best regards

Enhancement: Disable logging for messages

Logging is great and all but for big messages like file upload, the log is used to notify users of receiving a new message, is not only quite noisy but also performance hungry. Providing a mock logger also doesn't help in this case since the message is already converted to a string and passed through. A flag to enable or disable logs for debugging and to enable or disable the inclusion of the actual message could be used to get around this.

Also, as a suggestion, we might be able to move createClientChannel to the send method so that it is created on the first message that requires a response. I am however not sure how much if any help this is to the performance of the library.

Again, if you think these changes are ok, I can propose a PR.

Non global module support

Hi, we would like the ability to instantiate and inject RMQ on a per module basis, which means we would like the ability to pass in a name | symbol to register the module as.

This would enable us to have different modules use different exchanges, and listen to messages on different queues

Support for amqps protocol

As it stands, it doesn't seem like it's possible to connected with the amqps protocol when using this library. Is this correct and if so, is this a feature this library would be interested in supporting?

How to handle ERROR_NO_ROUTE?

Hi.
Thank you for this great library.

How can i handle ERROR_NO_ROUTE, and confirm the message?

Background

The queue gets a message with 'test'(topic), but it's route has been removed.
The consumer received this message , reply an error , but no ack.
The state of the message will remain unack.

Related Code

const route = this.getRouteByTopic(msg.fields.routingKey);
if (route) {
	msg = await this.useMiddleware(msg);
	requestEmitter.emit(route, msg);
} else {
	this.reply('', msg, new RMQError(ERROR_NO_ROUTE, ERROR_TYPE.TRANSPORT));
}

expectation

  • Able to confirm this exception message(ack, nack or reject).

I know the more correct way is to remove the useless bindings, but it's would be nice that could handle such error in project code.

RPC use case

Hi.

My use case:
I want to use RPC (request-reply).
I have many static queues - A, B, C, D, E, etc.
Job from queue A must be started only after previous job from queue A completed.
Jobs from different queues can be processed in parallel for better performance.
As I understand, I need one Consumer per queue and "prefetch: 1" per Consumer.
Actual code to be called is the same for any job in any queue.

What is the best way to solve it using this library?

I am new to RabbitMQ. Sorry for "StackOverflow" like question here.
Any help is appreciated!

How i supposed to catch errors thrown from handlers?

Interceptor catch errors like "Requested service doesn't have RMQRoute with this path" but not RMQ errors thrown from @RMQRoute

My config

  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    serviceName: configService.get('APP_NAME'),
    exchangeName: configService.get('EXCHANGE'),
    prefetchCount: parseInt(configService.get('RMQ_PREFETCH_COUNT')),
    queueName: configService.get('APP_NAME'),
    logMessages: process.env.NODE_ENV === 'development',
    connections: [
      {
        login: configService.get('RMQ_USER'),
        password: configService.get('RMQ_PASSWORD'),
        host: configService.get('RMQ_HOST'),
        port: parseInt(configService.get('RMQ_PORT')),
      },
    ],
    intercepters: [RMQErrorInterceptor],
  })

Interceptor:

@Injectable()
export class RMQErrorInterceptor extends RMQIntercepterClass {
  protected logger = new LoggerWithSentry('ERROR INTERCEPTOR');

  intercept(res: any, msg: Message, error: Error): Promise<any> {
    if (error) {
      this.logger.error(error);
    }

    return super.intercept(res, msg, error);
  }
}

Sample handler:

@RMQRoute('sample.route')
  async handleSample() {
    console.log('HERE');
    throw new RMQError(
      'userInfo.internal_server_error',
      ERROR_TYPE.RMQ,
      HttpStatus.INTERNAL_SERVER_ERROR,
    );
  }

If i post message on this route, I get 'HERE' in console but error not getting logged

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.