Getting gRPC Metadata in NestJS from Client - nestjs

I would like to be able to read gRPC metadata when making a unary call with the gRPC client.
Currently this is already possible with StreamCalls' which is also properly documented, but I can't figure it out with Unary Call's
Consider the following code:
Simplified Provider:
{
provide: 'SERVICE_GRPC',
useFactory: () => {
return ClientProxyFactory.create({
transport: Transport.GRPC,
options: {
package: 'myPackage',
protoPath: 'my.proto
url: '127.0.0.1',
},
});
}
},
Simplified Class
interface SomeService {
someGrpcMethod: (argument: Request) => Observable<Response>;
}
class SomeClass {
constructor(#Inject('SERVICE_GRPC') private readonly _client: ClientGrpc) {}
onModuleInit(): void {
this._someService = this._client.getService<SomeService>('SomeService');
}
someMethod(): void {
const observer = this._someService.someGrpcMethod({param1:true})
// Need the metadata send by server here
observer.subscribe(...);
}
}
Does anyone know how to do this?

Related

NestJS v9: implement durable providers

[SOLVED] I'm pretty new to NestJS and trying to get my head around durable providers but i can't get them to work.
My scenario is that i have a service with some logic and two providers that implement the same interface to get some data. Depending on a custom header value i want to use Provider1 or Provider2 and the service itself does not have to know about the existing provider implementations.
Since i'm in a request scoped scenario but i know there are only 2 possible dependency-subtrees i want to use durable providers that the dependencies are not newly initialised for each request but reused instead.
I set up the ContextIdStrategy as described in the official docs and it is executed on each request but i miss the part how to connect my provider implementations with the ContextSubtreeIds created in the ContextIdStrategy.
Interface:
export abstract class ITest {
abstract getData(): string;
}
Implementations:
export class Test1Provider implements ITest {
getData() {
return "TEST1";
}
}
export class Test2Provider implements ITest {
getData() {
return "TEST2";
}
}
Service:
#Injectable()
export class AppService {
constructor(private readonly testProvider: ITest) {}
getHello(): string {
return this.testProvider.getData();
}
}
Controller:
#Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
#Get()
getData(): string {
return this.appService.getData();
}
}
ContextIdStrategy:
const providers = new Map<string, ContextId>([
["provider1", ContextIdFactory.create()],
["provider2", ContextIdFactory.create()],
]);
export class AggregateByProviderContextIdStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: Request) {
const providerId = request.headers["x-provider-id"] as string;
let providerSubTreeId: ContextId;
if (providerId == "provider1") {
providerSubTreeId = providers["provider1"];
} else if (providerId == "provider2") {
providerSubTreeId = providers["provider2"];
} else {
throw Error(`x-provider-id ${providerId} not supported`);
}
// If tree is not durable, return the original "contextId" object
return (info: HostComponentInfo) =>
info.isTreeDurable ? providerSubTreeId : contextId;
}
}
Main:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
ContextIdFactory.apply(new AggregateByProviderContextIdStrategy());
await app.listen(3000);
}
bootstrap();
Module:
#Module({
imports: [],
controllers: [AppController],
providers: [
{
provide: ITest,
useFactory: () => {
// THIS IS THE MISSING PIECE.
// Return either Test1Provider or Test2Provider based on the ContextSubtreeId
// which is created by the ContextIdStrategy
return new Test1Provider();
},
},
AppService,
],
})
export class AppModule {}
The missing part was a modification of the ContextIdStrategy return statement:
return {
resolve: (info: HostComponentInfo) => {
const context = info.isTreeDurable ? providerSubTreeId : contextId;
return context;
},
payload: { providerId },
}
after that change, the request object can be injected in the module and where it will only contain the providerId property and based on that, the useFactory statement can return different implementations

NestJs, RabbitMq, CQRS & BFF: Listening for event inside the bff

I'm about to implement a Microservice Architecture with CQRS Design Pattern. The Microservices are communicating with RMQ.
Additionally, I'm adding a BFF for my Application UI.
In this scenario, the BFF needs to listen to certain domain events.
For instance: After the user sends an request to the BFF, the BFF calls a method of a Microservice which invokes an asynchronous event.
The events result will go back to the BFF and then to the user.
I'm thinking of different ways I might be able to implement this and I came up with this concept:
// BFF Application: sign-up.controller.ts
import { Controller, Post, Body } from '#nestjs/common';
import { Client, ClientProxy, Transport } from '#nestjs/microservices';
#Controller('signup')
export class SignupController {
#Client({ transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'signup_request' } })
client: ClientProxy;
#Post()
async signup(#Body() body: any) {
// Generate a unique identifier for the request
const requestId = uuid();
// Send the request with the unique identifier
const response = await this.client.send<any>({ cmd: 'signup', requestId }, body).toPromise();
// Wait for the SignUpEvent to be emitted before sending the response
return new Promise((resolve, reject) => {
this.client.subscribe<any>('signup_response', (response: any) => {
// Check the unique identifier to ensure the event corresponds to the original request
if (response.requestId === requestId) {
resolve(response);
}
});
});
}
}
After sending the request with the unique identifier, the microservice will execute a sign up command:
// Microservice Application: sign-up.handler.ts
import { CommandHandler, ICommandHandler } from '#nestjs/cqrs';
import { SignUpCommand } from './commands/sign-up.command';
import { SignUpEvent } from './events/sign-up.event';
import { EventBus } from '#nestjs/cqrs';
#CommandHandler(SignUpCommand)
export class SignUpCommandHandler implements ICommandHandler<SignUpCommand> {
constructor(private readonly eventBus: EventBus) {}
async execute(command: SignUpCommand) {
// Validating the user/account aggregate
// ...
// Emit the SignUpEvent with the unique identifier
this.eventBus.publish(new SignUpEvent(user, command.requestId));
}
}
Now the Event Handler gets called:
// Microservice Application: signed-up.handler.ts
import { EventsHandler, IEventHandler } from '#nestjs/cqrs';
import { SignUpEvent } from './events/sign-up.event';
import { ClientProxy, Transport } from '#nestjs/microservices';
#EventsHandler(SignUpEvent)
export class SignUpEventHandler implements IEventHandler<SignUpEvent> {
#Client({ transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'signup_response' } })
client: ClientProxy;
async handle(event: SignUpEvent) {
// Persist the user
const user = await this.persistUser(event.user);
// Generate access and refresh tokens
const tokens = this.generateTokens(user);
// Emit the SignUpResponse event with a unique identifier
await this.client.emit('signup_response', { user, tokens, requestId: event.requestId });
}
}
Is this, a valid way to implement this type of behaviour?
Thank you in advance.

How to send raw data as payload with MQTT on Nestjs

I want to send just raw data to the broker but the string is sent as string with quotations.
i.e.
the message I want to sent is: ti=0F:0000000000&id=E8EB1BE99345
but what is sent is: "ti=0F:0000000000&id=E8EB1BE99345"
How can I achieve that?
I have declared serializer in the mqtt client like below,
#Module({
imports: [
ClientsModule.register([
{
name: 'MQTT_CLIENT',
transport: Transport.MQTT,
options: {
url: 'mqtt://XX.XXX.XXX.XXX:1883',
clientId: 'my-client-id',
serializer: {
serialize: (value: any) => value.data,
},
},
},
]),
ConfigModule.forRoot(),
],
controllers: [AppController],
})
export class AppModule {}
You can see here that the ACKs are sent with the quotations, and they should be sent like above, without them:
Apparently this behaviour is something standard in Nestjs, the publishing response will be returned as the result object of applying JSON.stringify().
There's no way of configure it anyhow.
The only option for doing that is to create a custom ClientProxy or even easier extend from ClientMqtt and then override the publish method, so all the logic related with serialization is ignored, and just publish the raw data of the packet.
Custom class with the modified publish method:
import { ClientMqtt, ReadPacket, WritePacket } from '#nestjs/microservices';
export class RawPayloadProxy extends ClientMqtt {
protected publish(
partialPacket: ReadPacket,
callback: (packet: WritePacket) => any,
): () => void {
try {
const pattern = this.normalizePattern(partialPacket.pattern);
const responseChannel = this.getResponsePattern(pattern);
let subscriptionsCount =
this.subscriptionsCount.get(responseChannel) || 0;
const publishPacket = () => {
subscriptionsCount = this.subscriptionsCount.get(responseChannel) || 0;
this.subscriptionsCount.set(responseChannel, subscriptionsCount + 1);
this.mqttClient.publish(
this.getRequestPattern(pattern),
partialPacket.data,
);
};
if (subscriptionsCount <= 0) {
this.mqttClient.subscribe(
responseChannel,
(err: any) => !err && publishPacket(),
);
} else {
publishPacket();
}
return () => {
this.unsubscribeFromChannel(responseChannel);
};
} catch (err) {
callback({ err });
}
}
}
Definition of the custom client class on AppModule:
#Module({
imports: [
ClientsModule.register([
{
name: 'MQTT_CLIENT',
customClass: RawPayloadProxy,
options: {
url: 'mqtt://XX.XXX.XXX.XXX:1883',
clientId: 'client-xxx',
},
},
]),
],
controllers: [AppController],
})
export class AppModule {}

Postman hangs when sending "POST" request to API

So i'm using Postman and i need to send a POST method request to my api but every time i do it hangs even if the handler does nothing except return a simple value. I'm using hapi so the syntax might differ a little bit from the standard express app.
currency.ts
//this is where i define the route methods and paths
import * as Hapi from "hapi";
import IRouteArea from "../server/IRouteArea";
import CurrencyController from "../controller/CurrencyController";
import hapiAuthJwt2 = require("hapi-auth-jwt2");
import { join } from "lodash";
import { RESOLVER } from "awilix";
const CurrencyArea = ({ currencyController }: any): IRouteArea => {
let _controller = currencyController as CurrencyController;
return {
registerRoutes(server: Hapi.Server) {
server.bind(_controller);
server.route({
method: "GET",
path: "/api/currencies",
options: {
auth: {
mode: "try"
},
plugins: { "hapi-auth-cookie": { redirectTo: false } },
handler: _controller.getCurrencies
}
});
server.route({
method: "POST", // this one is causing me problems
path: "/api/currencies/{id}",
options: {
auth: {
mode: "try"
},
plugins: { "hapi-auth-cookie": { redirectTo: false }},
handler: _controller.editCurrency
}
});
}
};
};
export default CurrencyArea;
and even though the actual handler of the request isn't doing anything special...
CurrencyController.ts
//here i define the handlers of requests sent to routes in the previous file
import * as Hapi from "hapi";
import { name } from "mustache";
import GetCurrencyListInteractor from "../../interactor/currencies/GetCurrencyListInteractor";
import Task from "../../runtime/Task";
export default class CurrencyController {
private task: Task;
constructor({ task }: any) {
this.task = task;
}
public async getCurrencies(
request: Hapi.Request,
h: Hapi.ResponseToolkit
): Promise<any> {
try {
return this.task.start<GetCurrencyListInteractor>(
"getCurrencyList",
t => t.execute()
);
} catch (error) {
return error as any;
}
}
public async editCurrency( //it's supposed to just return the parameter sent in the url
request: Hapi.Request,
h: Hapi.ResponseToolkit
): Promise<any> {
try {
return request.params.id;
} catch (error) {
return error as any;
}
}
}
it always hangs and gets stuck on the "sending request" screen when i send the request. One interesting thing is that the exact same route works as intended but only if the method defined in currency.ts is "GET". If i leave everything else the way it is and change the method from "GET" to "POST" or "PUT" or "PATCH" it stops working.
this goes on forever

Nestjs resolve custom startup dependency

I have this factory which resolves redis:
import {RedisClient} from "redis";
export const RedisProvider = {
provide: 'RedisToken',
useFactory: async () => {
return new Promise((resolve, reject) => {
let redisClient = new RedisClient({
host: 'resolver_redis'
});
redisClient.on('ready', function () {
resolve(redisClient);
});
});
}
};
But in addition to that, i wanna check if a specific key exists in redis, and if its not i want to retrive it using microservices, but for that i need to inject the microservices "client", i wanna change it to someting like that:
import {RedisClient} from "redis";
export const RedisProvider = {
provide: 'RedisToken',
useFactory: async () => {
return new Promise((resolve, reject) => {
let redisClient = new RedisClient({
host: 'resolver_redis'
});
redisClient.on('ready', async function () {
let campaigns = await redisClient.get('campaigns');
if ( ! campaigns) {
// Note: "client" is not available in this scope
client.send('get:campaigns').subscribe(async function (campaigns) {
await redisClient.set('campaigns', campaigns);
resolve(redisClient);
});
}
else {
resolve(redisClient);
}
});
});
}
};
the only problem is that i dont have access to the "client", or do i?
it can also be anther provider if it makes more sense, but then i will also need priority for loading the providers, i am doing this because the application requires this data for startup stuff
I don't recommend adding more logic in providers factory, since it could be more controlled in the consumer of this provider ie: the services
in your services code you could do something like this
import { Component, Inject, OnModuleInit } from '#nestjs/common';
...
#Component()
export class MyService implements OnModuleInit {
constructor(#Inject(REDIS_TOKEN) private readonly redis: RedisClient) {}
onModuleInit() {
const campaigns = await this.redis.get('campaigns');
...
}
}
I assume that REDIS_TOKEN is constant saved on constants.ts file and equal to RedisToken.
this will be more controlled.
the code of checking for the campaigns key will be executed once your Module init.
for more information about Module Lifecycle checkout: Modules Lifecycle

Resources