Format request before send by ClientProxy - microservices

I use microservices and RabbitMQ as a transporter. My microservices communicate with each other, so one of them can send message to another one. I use the following way to send messages:
await this.client.send(SOME_COMMAND, obj).toPromise();
Now I need to format objects I send in all requests to any microservice in one place. For example add reqId, or serialize Map. Is it possible?

1. Per controller solution. here for simplicity I removed the handler part:
#Controller()
export class AppController {
constructor(#Inject('MATH_SERVICE') private client: ClientProxy) {
let send = client.send.bind(client);
client.send = function (pattern, payload) {
return send(pattern, { payload, systemWideProp: ""})
}
}
sum() {
this.client.send(COMMAND, obj)
}
}
2. This could be as a provider for injecting it on each controller you want using your rabbitmq client service:
custom-client-proxy.ts
import { Inject, Injectable } from '#nestjs/common';
import { ClientProxy } from '#nestjs/microservices';
#Injectable()
export class CustomClientProxy {
constructor(#Inject('MATH_SERVICE') private client: ClientProxy) { }
send(pattern, payload) {
// payload and pattern manipulations such as:
// payload.say = "Hi";
// const scopePattern = { cmd: `${pattern.cmd}_dev` };
return this.client.send(pattern, payload)
}
}
app.module.ts
import { Module } from '#nestjs/common';
import { ClientsModule, Transport } from '#nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CustomClientProxy } from './custom-client-proxy';
#Module({
imports: [ClientsModule.register([{
name: 'MATH_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'math_queue'
},
}])],
controllers: [AppController],
providers: [AppService, CustomClientProxy],
exports: [CustomClientProxy] // export here for other modules of your app
})
export class AppModule { }
app.controller.ts
also any controller you have imported under app's modules
#Controller()
export class AppController {
constructor(private client: CustomClientProxy) {}
sum() {
this.client.send(COMMAND, obj)
}
}
3. When you need to use this functionality for more than one queue:
// custom-client-proxy.ts
#Injectable()
export class CustomClientProxy {
constructor() { }
send(client: ClientProxy, pattern, payload) {
// payload and pattern manipulations such as:
// payload.say = "Hi";
// const scopePattern = { cmd: `${pattern.cmd}_dev` };
return client.send(pattern, payload)
}
}
// app.module.ts
#Module({
imports: [ClientsModule.register([{
name: 'MATH_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'math_queue'
},
},
{
name: 'MESSAGE_SERVICE',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
queue: 'message_queue',
},
}])],
controllers: [AppController],
providers: [AppService, CustomClientProxy],
exports: [CustomClientProxy]
})
export class AppModule { }
// app.controller.ts
#Controller()
export class AppController {
constructor(
#Inject('MATH_SERVICE') private mathClient: ClientProxy,
#Inject('MESSAGE_SERVICE') private messageClient: ClientProxy,
private client: CustomClientProxy) {}
sum(a) {
return this.client.sendTo(this.mathClient, pattern, payload);
}
}

The answer is pretty simple: you need to pass custom Serializer to the ClientProviderOptions and you can implement everything there

Related

How to pass a dynamic port to the Websockets-gateway in NestJS?

I wanted to dynamically set the Websockets-gateway port from config in NestJS. Below is my websockets-gateway code.
import { WebSocketGateway } from '#nestjs/websockets';
const WS_PORT = parseInt(process.env.WS_PORT);
#WebSocketGateway(WS_PORT)
export class WsGateway {
constructor() {
console.log(WS_PORT);
}
}
But the WS_PORT is always NaN.
This is my bootstrap function insdie main.ts :
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: false });
const configService = app.get(ConfigService);
initAdapters(app);
await app.listen(configService.get(HTTP_PORT), () => {
console.log('Listening on port ' + configService.get(HTTP_PORT));
});
}
Below is my app.module.ts :
#Module({
imports: [
ConfigModule.forRoot({
envFilePath: './src/config/dev.env',
isGlobal: true,
}),
RedisModule,
SocketStateModule,
RedisPropagatorModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>(JWT_SECRET_KEY),
}),
inject: [ConfigService],
}),
],
controllers: [AppController],
providers: [WsGateway, AppService],
})
export class AppModule {}
I put a console log in the Gateway constructor to print the value of 'WS_PORT' but it's always NaN.
[Nest] 13252 - 10/04/2021, 5:05:34 PM LOG [NestFactory] Starting Nest application...
NaN
Thanks in advance.
I could not find a way to add dynamic data to the decorator. So to be able to dynamically choose the port and other configurations I had to:
Create an adapter for socket-io:
Tell NestJs to use the new adapter
SocketIoAdapter.ts
import { INestApplicationContext } from '#nestjs/common';
import { IoAdapter } from '#nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { ConfigService } from '#nestjs/config';
export class SocketIoAdapter extends IoAdapter {
constructor(
private app: INestApplicationContext,
private configService: ConfigService,
) {
super(app);
}
createIOServer(port: number, options?: ServerOptions) {
port = this.configService.get<number>('SOCKETIO.SERVER.PORT');
const path = this.configService.get<string>('SOCKETIO.SERVER.PATH');
const origins = this.configService.get<string>(
'SOCKETIO.SERVER.CORS.ORIGIN',
);
const origin = origins.split(',');
options.path = path;
options.cors = { origin };
const server = super.createIOServer(port, options);
return server;
}
}
Now, you need to edit the main.ts to use the adapter
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '#nestjs/config';
import { SocketIoAdapter } from './socket-io/socket-io.adapter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
const hosts = configService.get<string>('CORS.HOST');
const hostsArray = hosts.split(',');
app.enableCors({
origin: hostsArray,
credentials: true,
});
//Here you use the adapter and sent the config service
app.useWebSocketAdapter(new SocketIoAdapter(app, configService));
await app.listen(4300);
}
bootstrap();
In this case I set the port and the cors origin, here an example of the conf file (using .env)
env.local
SOCKETIO.SERVER.PORT=4101
SOCKETIO.SERVER.PATH=
SOCKETIO.SERVER.CORS.ORIGIN=http://localhost:4200,http://localhost.com:8080
Here a link to config service Config Service NestJs
You can do it relatively straightforward if you decorate the Gateway before app.init is called:
Import the class in main.ts
Get an instance of your ConfigurationService
Manually call the decorator on the class with the config data
function decorateGateway(class_, config) {
// Just calling the decorator as a function with the class
// as argument does the same as `#WebSocketGateway`
WebSocketGateway({
cors: {
origin: config.get("websocket.cors.origin"),
}
})(class_)
}
async function bootstrap() {
const app = await NestFactory.create(AppModule, {});
const config = app.get(ConfigService);
decorateGateway(ChatGateway, config);
...
app.init();
}
The tricky part with a Gateway is that it starts up together with the server, and the decorator metadata needs to be applied to the class earlier than for other components. You can do this in main.ts before app.init.
port = this.configService.get<number>('SOCKETIO.SERVER.PORT');
In my case I found its return string(from .env), port gets 'string' but not 'number',
but if put parseInt(this.configService.get<number>('SOCKETIO.SERVER.PORT'), 10);
then it is ok
Mind that socket-io ports must be the same on server & client side

Angular, error 500 after sending the request in the header

I have a hard time passing the right angular request to the header. This is my service:
import { Injectable } from '#angular/core';
import { HttpClient, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpHeaders }
from '#angular/common/http';
import { Utente } from '../model/Utente ';
import { Prodotto } from '../model/Prodotto ';
import { OktaAuthService } from '#okta/okta-angular';
import { Observable, from } from 'rxjs';
import { Carrello } from '../model/Carrello ';
import { userInfo } from 'node:os';
import { getLocaleCurrencyCode } from '#angular/common';
const headers = new HttpHeaders().set('Accept', 'application/json');
#Injectable({
providedIn: 'root'
})
export class HttpClientService {
constructor(
private httpClient:HttpClient, private oktaAuth:OktaAuthService ) {}
getCarr(){
return this.httpClient.get<Carrello[]>('http://localhost:8080/prodotti/utente/vedicarrelloo', {headers} );
}
}
This is my spring method:
#Transactional(readOnly = true)
public List<Carrello> getCarrello(#AuthenticationPrincipal OidcUser utente){
Utente u= utenteRepository.findByEmail(utente.getEmail());
return carrelloRepository.findByUtente(u);
}
In console I get this error (error 500):
https://i.stack.imgur.com/BiONS.png
this error corresponds in my console to "java.lang.NullPointerException: null.
But if I access localhost: 8080, I can see the answer correctly, so I assume there is a problem in passing the request header in angular, can anyone tell me where am I wrong, please? I specify that I get this error only in the methods where the OidcUser is present, the rest works perfectly. Thank you!
You need to send an access token with your request. Like this:
import { Component, OnInit } from '#angular/core';
import { OktaAuthService } from '#okta/okta-angular';
import { HttpClient } from '#angular/common/http';
import sampleConfig from '../app.config';
interface Message {
date: string;
text: string;
}
#Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
failed: Boolean;
messages: Array<Message> [];
constructor(public oktaAuth: OktaAuthService, private http: HttpClient) {
this.messages = [];
}
async ngOnInit() {
const accessToken = await this.oktaAuth.getAccessToken();
this.http.get(sampleConfig.resourceServer.messagesUrl, {
headers: {
Authorization: 'Bearer ' + accessToken,
}
}).subscribe((data: any) => {
let index = 1;
const messages = data.messages.map((message) => {
const date = new Date(message.date);
const day = date.toLocaleDateString();
const time = date.toLocaleTimeString();
return {
date: `${day} ${time}`,
text: message.text,
index: index++
};
});
[].push.apply(this.messages, messages);
}, (err) => {
console.error(err);
this.failed = true;
});
}
}
On the Spring side, if you want it to accept a JWT, you'll need to change to use Jwt instead of OidcUser. Example here.
#GetMapping("/")
public String index(#AuthenticationPrincipal Jwt jwt) {
return String.format("Hello, %s!", jwt.getSubject());
}

How to use socket.io with graphql subscription?

I want to make a real-time chat app by using nestjs and graphql technology. Most of tutorial uses PubSub but I don't know how to send a message to a specific client (?).
I think it is much easy to use socket.io for sending a message to a specific client by using socket id.
this example using PubSub:
Is there a way to send a message to a specific client by using PubSub? By reciving some data about sender like id.
How can I replace PubSub with Socket.io ?I am really comfortable with socket.io
App.module.ts
import { Module, MiddlewareConsumer, RequestMethod, Get } from '#nestjs/common';
import { TypeOrmModule } from '#nestjs/typeorm';
import { typeOrmConfig } from './config/typeorm.config';
import { AuthModule } from './auth/auth.module';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { Connection } from 'typeorm';
import { GraphQLModule } from '#nestjs/graphql';
import { join } from 'path';
import { ChatModule } from './chat/chat.module';
import { ChatService } from './chat/chat.service';
#Module({
imports: [
TypeOrmModule.forRoot(typeOrmConfig), // connect to database
GraphQLModule.forRoot({
debug: true,
playground: true,
typePaths: ['**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
},
}),
AuthModule,
ChatModule,
],
controllers: [],
providers: [
],
})
export class AppModule {
}
Chat.module.ts
import { Module } from '#nestjs/common';
import { ChatService } from './chat.service';
import { ChatResolver } from './chat.resolver';
import { TypeOrmModule } from '#nestjs/typeorm';
import { AuthModule } from '../auth/auth.module';
import { PubSub } from 'graphql-subscriptions';
#Module({
imports: [
// Add all repository here; productRepository, ...
TypeOrmModule.forFeature( [
// repositories ...
]),
AuthModule
],
providers: [
//=> How to replace with socket.io ?
{
provide: 'PUB_SUB',
useValue: new PubSub(),
},
ChatService,
ChatResolver,
]
})
export class ChatModule {}
Chat.resolver.ts
import { Resolver, Query, Mutation, Args, Subscription } from '#nestjs/graphql';
import { PubSubEngine } from 'graphql-subscriptions';
import { Inject } from '#nestjs/common';
const PONG_EVENT_NAME = 'pong';
#Resolver('Chat')
export class ChatResolver {
constructor(
private chatService: ChatService,
#Inject('PUB_SUB')
private pubSub: PubSubEngine,
) {}
// Ping Pong
#Mutation('ping')
async ping() {
const pingId = Date.now();
//=> How to send deta to specific client by using user-id?
this.pubSub.publish(PONG_EVENT_NAME, { ['pong']: { pingId } });
return { id: pingId };
}
#Subscription(PONG_EVENT_NAME)
pong() {
//=> how to get data about sender like id?
return this.pubSub.asyncIterator(PONG_EVENT_NAME);
}
}
Chat.graphql
type Mutation {
ping: Ping
}
type Subscription {
tagCreated: Tag
clientCreated: Client
pong: Pong
}
type Ping {
id: ID
}
type Pong {
pingId: ID
}
How can replace PubSub with Socket.io?
I didn't find any solution or example to get client-socket from graphql subscription.
In most example they use Gateway instead of graphql pubsub. So I used GateWay to implement real-time activities and graphql for other request.

Connect NestJS to a websocket server

How can NestJS be use as a websocket client? I want to connect to a remote websocket server as a client using NestJS, but I didn't find any information about this implementation in the framework.
As Nestjs is simply a framework for Nodejs, so you need to find an NPM package that supports Websocket. For example, I use ws with #types/ws type definition, and create a Websocket client as a Nestjs service class:
// socket-client.ts
import { Injectable } from "#nestjs/common";
import * as WebSocket from "ws";
#Injectable()
export class WSService {
// wss://echo.websocket.org is a test websocket server
private ws = new WebSocket("wss://echo.websocket.org");
constructor() {
this.ws.on("open", () => {
this.ws.send(Math.random())
});
this.ws.on("message", function(message) {
console.log(message);
});
}
send(data: any) {
this.ws.send(data);
}
onMessage(handler: Function) {
// ...
}
// ...
}
// app.module.ts
import { Module } from "#nestjs/common";
import { WSService } from "./socket-client";
#Module({
providers: [WSService]
})
export class AppModule {}
I try it by another way. I write an adapter with socket.io-client. Then use this adapter in boostrap by method useWebSocketAdapter. After that i can write handle websocket event in gateway like the way working with socket server (use decorator #SubscribeMessage)
My Adapter file
import { WebSocketAdapter, INestApplicationContext } from '#nestjs/common';
import { MessageMappingProperties } from '#nestjs/websockets'
import * as SocketIoClient from 'socket.io-client';
import { isFunction, isNil } from '#nestjs/common/utils/shared.utils';
import { fromEvent, Observable } from 'rxjs';
import { filter, first, map, mergeMap, share, takeUntil } from 'rxjs/operators';
export class IoClientAdapter implements WebSocketAdapter {
private io;
constructor(private app: INestApplicationContext) {
}
create(port: number, options?: SocketIOClient.ConnectOpts) {
const client = SocketIoClient("http://localhost:3000" , options || {})
this.io = client;
return client;
}
bindClientConnect(server: SocketIOClient.Socket, callback: Function) {
this.io.on('connect', callback);
}
bindClientDisconnect(client: SocketIOClient.Socket, callback: Function) {
console.log("it disconnect")
//client.on('disconnect', callback);
}
public bindMessageHandlers(
client: any,
handlers: MessageMappingProperties[],
transform: (data: any) => Observable<any>,
) {
const disconnect$ = fromEvent(this.io, 'disconnect').pipe(
share(),
first(),
);
handlers.forEach(({ message, callback }) => {
const source$ = fromEvent(this.io, message).pipe(
mergeMap((payload: any) => {
const { data, ack } = this.mapPayload(payload);
return transform(callback(data, ack)).pipe(
filter((response: any) => !isNil(response)),
map((response: any) => [response, ack]),
);
}),
takeUntil(disconnect$),
);
source$.subscribe(([response, ack]) => {
if (response.event) {
return client.emit(response.event, response.data);
}
isFunction(ack) && ack(response);
});
});
}
public mapPayload(payload: any): { data: any; ack?: Function } {
if (!Array.isArray(payload)) {
return { data: payload };
}
const lastElement = payload[payload.length - 1];
const isAck = isFunction(lastElement);
if (isAck) {
const size = payload.length - 1;
return {
data: size === 1 ? payload[0] : payload.slice(0, size),
ack: lastElement,
};
}
return { data: payload };
}
close(server: SocketIOClient.Socket) {
this.io.close()
}
}
main.js
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import {IoClientAdapter} from './adapters/ioclient.adapter'
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new IoClientAdapter(app))
await app.listen(3006);
console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();
then Gateway
import {
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsResponse,
} from '#nestjs/websockets';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Server } from 'socket.io';
#WebSocketGateway()
export class EventsGateway {
#WebSocketServer()
server: Server;
#SubscribeMessage('hello')
async identity(#MessageBody() data: number): Promise<number> {
console.log(data)
return data;
}
}
It a trick, but look so cool. Message handler can write more like nestjs style.

NestJS websocket Broadcast event to clients

I'm not able to send data from server NestJS to clients via websocket. Nothing is emitted.
My use case:
several clients connected to a server via websocket
client sends a message to the server via websocket
server broadcast the message to all client
My stack:
NestJS server with websocket
Angular client and other (like chrome extension for testing websockets)
My code:
simple-web-socket.gateway.ts:
import { SubscribeMessage, WebSocketGateway, WsResponse, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit } from '#nestjs/websockets';
#WebSocketGateway({ port: 9995, transports: ['websocket'] })
export class SimpleWebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
#WebSocketServer() private server: any;
wsClients=[];
afterInit() {
this.server.emit('testing', { do: 'stuff' });
}
handleConnection(client: any) {
this.wsClients.push(client);
}
handleDisconnect(client) {
for (let i = 0; i < this.wsClients.length; i++) {
if (this.wsClients[i].id === client.id) {
this.wsClients.splice(i, 1);
break;
}
}
this.broadcast('disconnect',{});
}
private broadcast(event, message: any) {
const broadCastMessage = JSON.stringify(message);
for (let c of this.wsClients) {
c.emit(event, broadCastMessage);
}
}
#SubscribeMessage('my-event')
onChgEvent(client: any, payload: any) {
this.broadcast('my-event',payload);
}
}
main.ts:
import { NestFactory } from '#nestjs/core';
import { AppModule } from './app.module';
import { WsAdapter } from '#nestjs/websockets';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter());
await app.listen(3000);
}
bootstrap();
app.module.ts:
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SimpleWebSocketGateway } from 'simple-web-socket/simple-web-socket.gateway';
#Module({
imports: [],
controllers: [AppController],
providers: [AppService, SimpleWebSocketGateway],
})
export class AppModule {}
Additionnal Informations:
Client emiting (with code line c.emit(event, broadCastMessage);) return false.
I suspect an error in the framework as my usage is quite simple. But I want to double-check with the community here if I'm doing something wrong.
As mentionned in the previous comment, c.send() works fine with the following snippet:
import { SubscribeMessage, WebSocketGateway, WsResponse, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit } from '#nestjs/websockets';
#WebSocketGateway({ port: 9995, transports: ['websocket'] })
export class SimpleWebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit {
#WebSocketServer() private server: any;
wsClients=[];
afterInit() {
this.server.emit('testing', { do: 'stuff' });
}
handleConnection(client: any) {
this.wsClients.push(client);
}
handleDisconnect(client) {
for (let i = 0; i < this.wsClients.length; i++) {
if (this.wsClients[i] === client) {
this.wsClients.splice(i, 1);
break;
}
}
this.broadcast('disconnect',{});
}
private broadcast(event, message: any) {
const broadCastMessage = JSON.stringify(message);
for (let c of this.wsClients) {
c.send(event, broadCastMessage);
}
}
#SubscribeMessage('my-event')
onChgEvent(client: any, payload: any) {
this.broadcast('my-event',payload);
}
}
I tried to broadcast a message to all connected clients using client.send():
broadcastMessage(event: string, payload: any) {
for (const client of this.clients) {
client.send(event, payload);
}
}
testBroadcast() {
this.broadcastMessage('msgInfo', { name: 'foo' });
}
the data sent ended up looking like this:
["message", "msgInfo", { "name": "foo" }]
The above did not work for me, so instead I used the client.emit() which works fine:
broadcastMessage(event: string, payload: any) {
for (const client of this.clients) {
client.emit(event, payload);
}
}
now the data looks like this:
["msgInfo", { "name": "foo" }]
With socket.io we could use socket.broadcast.emit()?:
#SubscribeMessage('my-event')
onChgEvent(
#MessageBody() message: any,
#ConnectedSocket() socket: Socket,
): void {
socket.broadcast.emit('my-event', message);
}

Resources