How to use socket.io with graphql subscription? - socket.io

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.

Related

Format request before send by ClientProxy

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

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.

Problem with e2e testing with NestJS TestingModule, GraphQL code first and TypeOrm

I'm in struggle since few days with e2e testing my NestJS application using GraphQL code first approach and TypeOrm.
I'm trying to create a TestingModule by injecting nestjs GraphQLModule with autoSchemaFile and I'm always getting the error "Schema must contain uniquely named types but contains multiple types named ...".
Here a reproduction of my bug with minimal code:
character.entity.ts:
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { ObjectType, Field, ID } from 'type-graphql';
#Entity()
#ObjectType()
export class Character {
#PrimaryGeneratedColumn()
#Field(() => ID)
id: string;
#Column({ unique: true })
#Field()
name: string;
}
character.resolver.ts:
import { Query, Resolver } from '#nestjs/graphql';
import { Character } from './models/character.entity';
import { CharacterService } from './character.service';
#Resolver(() => Character)
export class CharacterResolver {
constructor(private readonly characterService: CharacterService) {}
#Query(() => [Character], { name: 'characters' })
async getCharacters(): Promise<Character[]> {
return this.characterService.findAll();
}
}
character.module.ts:
import { Module } from '#nestjs/common';
import { CharacterResolver } from './character.resolver';
import { CharacterService } from './character.service';
import { TypeOrmModule } from '#nestjs/typeorm';
import { Character } from './models/character.entity';
#Module({
imports: [TypeOrmModule.forFeature([Character])],
providers: [CharacterResolver, CharacterService],
})
export class CharacterModule {}
app.module.ts:
import { Module } from '#nestjs/common';
import { CharacterModule } from './character/character.module';
import { TypeOrmModule } from '#nestjs/typeorm';
import { Connection } from 'typeorm';
import { GraphQLModule } from '#nestjs/graphql';
#Module({
imports: [TypeOrmModule.forRoot(), GraphQLModule.forRoot({ autoSchemaFile: 'schema.gql' }), CharacterModule],
controllers: [],
providers: [],
})
export class AppModule {
constructor(private readonly connection: Connection) {}
}
and finally: character.e2e-spec.ts:
import { INestApplication } from '#nestjs/common';
import { Test, TestingModule } from '#nestjs/testing';
import { TypeOrmModule } from '#nestjs/typeorm';
import { CharacterModule } from '../src/character/character.module';
import { GraphQLModule } from '#nestjs/graphql';
describe('CharacterResolver (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot(),
GraphQLModule.forRoot({ playground: false, autoSchemaFile: 'schema.gql' }),
CharacterModule,
],
}).compile();
app = module.createNestApplication();
await app.init();
});
it('should create testing module', () => {
expect(1).toBe(1);
});
afterAll(async () => {
await app.close();
});
});
And after running npm run test:e2e:
Schema must contain uniquely named types but contains multiple types named "Character".
at typeMapReducer (../node_modules/graphql/type/schema.js:262:13)
at Array.reduce (<anonymous>)
at new GraphQLSchema (../node_modules/graphql/type/schema.js:145:28)
at Function.generateFromMetadataSync (../node_modules/type-graphql/dist/schema/schema-generator.js:31:24)
at Function.<anonymous> (../node_modules/type-graphql/dist/schema/schema-generator.js:16:33)
at ../node_modules/tslib/tslib.js:110:75
at Object.__awaiter (../node_modules/tslib/tslib.js:106:16)
at Function.generateFromMetadata (../node_modules/type-graphql/dist/schema/schema-generator.js:15:24)
I don't find any other way to create a testing module with graphql code first approach on official doc or while googling... Am I missing something ?
Your ormconfig.json need to look like this:
"entities": [
"src/**/*.entity.js"
],
"migrations": [
"src/migration/*.js"
],
"cli": {
"migrationsDir": "src/migration"
}
I.e you need to specify the entities being in the src, not dist folder. If not TypeGraphQL will generate the schema for each resolver twice. To get the generate and run migration commands to work you would have to setup a different ormconfig.json for your development environment.

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);
}

How Do I use Angular 4 Dialog Directives?

I am developing an Angular web application using:
Angular 4.1.2
Angular Material 2.0.0-beta.11
I am trying to create a simple modal dialog and on reading the Angular guide documents (Dialog | Angular Material) I see that there are several directives available to make it easier to structure the dialog content.
I cannot work out how to implement md-dialog-title, <md-dialog-content>, <md-dialog-actions> or md-dialog-close. The attributes, when applied to an element, appear to make no difference at all and the <md-dialog-content> and <md-dialog-actions> create errors like this:
Unhandled Promise rejection: Template parse errors: 'md-dialog-content' is not a known element:
Any guidance would be very welcome please. Here are some further details of my project:
For my initial development I have created an Angular module, named AngularMaterialModule to manage my Angular Materials. Here is a summary of it:
import { NgModule } from '#angular/core';
import {
MdAutocompleteModule,
MdButtonModule,
....
MdStepperModule,
StyleModule,
} from '#angular/material';
import { CdkTableModule } from '#angular/cdk/table';
import { A11yModule } from '#angular/cdk/a11y';
import { BidiModule } from '#angular/cdk/bidi';
import { OverlayModule } from '#angular/cdk/overlay';
import { PlatformModule } from '#angular/cdk/platform';
import { ObserversModule } from '#angular/cdk/observers';
import { PortalModule } from '#angular/cdk/portal';
#NgModule({
exports: [
// Material Modules
MdAutocompleteModule,
MdButtonModule,
....
PlatformModule,
PortalModule
],
declarations: []
})
export class AngularMaterialModule { }
My Visual Studio Solution was created using dotnet new angular taking advantage of the Microsoft ASP.Net SPA Templates.
In my app.module.client.ts file I import the AngularMaterialModule, described above, like this:
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { BrowserAnimationsModule } from '#angular/platform-browser/animations';
import { FormsModule } from '#angular/forms';
import { HttpModule } from '#angular/http';
import { AngularMaterialModule } from './core/angular-material.module';
import { sharedConfig } from './app.module.shared';
#NgModule({
bootstrap: sharedConfig.bootstrap,
declarations: sharedConfig.declarations,
imports: [
BrowserModule,
BrowserAnimationsModule,
AngularMaterialModule,
FormsModule,
HttpModule,
...sharedConfig.imports
],
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin }
]
})
export class AppModule {
}
You need to import the actual dialog module into the module you want to use it in.
import { MdDialogModule } from '#angular/material';
#NgModule({
imports: [
MdDialogModule
],
})
After that, it's a straight forward and follow the example in their docs
import {Component, Inject} from '#angular/core';
import {MdDialog, MdDialogRef, MD_DIALOG_DATA} from '#angular/material';
/**
* #title Dialog Overview
*/
#Component({
selector: 'dialog-overview-example',
templateUrl: 'dialog-overview-example.html'
})
export class DialogOverviewExample {
animal: string;
name: string;
constructor(public dialog: MdDialog) {}
openDialog(): void {
let dialogRef = this.dialog.open(DialogOverviewExampleDialog, {
width: '250px',
data: { name: this.name, animal: this.animal }
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
this.animal = result;
});
}
}
#Component({
selector: 'dialog-overview-example-dialog',
templateUrl: 'dialog-overview-example-dialog.html',
})
export class DialogOverviewExampleDialog {
constructor(
public dialogRef: MdDialogRef<DialogOverviewExampleDialog>,
#Inject(MD_DIALOG_DATA) public data: any) { }
onNoClick(): void {
this.dialogRef.close();
}
}
If you ever want to create a custom dialog component, you will have to add it into your entryComponents in the module.
import { MdDialogModule } from '#angular/material';
#NgModule({
entryComponents: [
AddressDialogComponent,
],
imports: [
MdDialogModule,
],
exports: [
AddressDialogComponent,
],
})

Resources