Graphql: Subscriptions client examples?

Created on 14 Jan 2018  路  17Comments  路  Source: nestjs/graphql

Very excited about NestJS. Thinking of becoming a sponsor if it proves out for my new project.

I need to get GraphQL subscriptions working. For starters, I've implemented the example from docs, and now I'm trying to connect GraphiQL with something like this:

  consumer
    .apply(graphiqlExpress({
      endpointURL: "/graphql", 
      subscriptionsEndpoint: `ws://localhost:${process.env.PORT || 3000}/subscriptions`
    }))
    .forRoutes({path: "/graphiql", method: RequestMethod.GET})
    .apply(graphqlExpress(req => ({schema, rootValue: req})))
    .forRoutes({path: "/graphql", method: RequestMethod.ALL});

I'm getting ERR_CONNECTION_REFUSED in browser console. I feel like I'm missing the connection between GraphQL Subscriptions and WebSockets, but I can't seem to piece it together from the docs.

Are there any working e2e examples out there?

question

Most helpful comment

I was having the same issue with [Object Object] as @joefru but I was able to resolve it by playing around with the package versions in the dependency versions. Below are the versions i ended up with to have it working. Might help you out @joefru.

"dependencies": {
    "@nestjs/common": "^4.5.9",
    "@nestjs/core": "^4.5.10",
    "@nestjs/graphql": "^2.0.0",
    "apollo-server-express": "^1.3.2",
    "graphql": "0.11.7",
    "graphql-subscriptions": "^0.5.6",
    "graphql-tools": "^2.19.0",
    "reflect-metadata": "^0.1.12",
    "rxjs": "^5.5.6",
    "subscriptions-transport-ws": "^0.9.5",
    "typescript": "^2.6.2",
    "ws": "^4.0.0"
  },
  "devDependencies": {
    "@types/node": "^9.4.0",
    "ts-node": "^4.1.0"
  }

All 17 comments

Hi @joefru,
I'm glad that you enjoy the project & and happy about the sponsor idea 馃檪
Have you created the subscriptions endpoint in the same way as explained here? https://dev-blog.apollodata.com/tutorial-graphql-subscriptions-server-side-e51c32dc2951 (WebSocket Transport for Subscriptions)

I did happen to read that article. I'm wondering how to include the following code in 'the nest way'...

// Wrap the Express server
const ws = createServer(server);
ws.listen(PORT, () => {
  console.log(`GraphQL Server is now running on http://localhost:${PORT}`);
  // Set up the WebSocket for handling GraphQL subscriptions
  new SubscriptionServer({
    execute,
    subscribe,
    schema
  }, {
    server: ws,
    path: '/subscriptions',
  });
});

I tried something like this:

@WebSocketGateway({ port: process.env.PORT || 3000, namespace: "subscriptions" })
export class SubscriptionGateway implements OnGatewayInit {

    constructor(private graphQLFactory: GraphQLFactory) {}

    afterInit(server: any) {
        const schema = this.createSchema();

        new SubscriptionServer({
            execute,
            subscribe,
            schema
        }, {
            server: server,
            path: "/subscriptions",
        });
    }

    createSchema() {
        const typeDefs = this.graphQLFactory.mergeTypesByPaths("./**/*.graphql");

        return this.graphQLFactory.createSchema({ typeDefs });
    }
}

But that yields the following build-time error: Error: listen EADDRINUSE :::3000. When I switch to port 3001, it compiles but then I get the following error in browser console:

WebSocket connection to 'ws://localhost:3001/subscriptions' failed: Connection closed before receiving a handshake response

Hi @joefru,
I think you should avoid using @Gateway() here, let's make use of an async component instead. Register a SUBSCRIPTION_SERVER somewhere, for example inside the AppModule:

components: [
    {
      provide: SUBSCRIPTION_SERVER,
      useFactory: async () => {
        const server = createServer();
        return await new Promise((resolve) => server.listen(PORT, resolve));
      },
    }
  ]

Then inject this component into module class:

export class ApplicationModule implements NestModule {
  constructor(
    private readonly graphQLFactory: GraphQLFactory,
    @Inject(SUBSCRIPTION_SERVER) private readonly ws,
  ) {}

  /// rest of code
}

Afterwards, create instance of SubscriptionServer:

  configure(consumer: MiddlewaresConsumer) {
    const schema = this.createSchema();
    this.initSubscriptionServer(schema);

    consumer
      .apply(graphiqlExpress({
        endpointURL: "/graphql", 
        subscriptionsEndpoint: `ws://localhost:${process.env.PORT || 3000}/subscriptions`
      }))
      .forRoutes({path: "/graphiql", method: RequestMethod.GET})
      .apply(graphqlExpress(req => ({schema, rootValue: req})))
      .forRoutes({path: "/graphql", method: RequestMethod.ALL});
  }

  initSubscriptionServer(schema) {
    new SubscriptionServer({ execute, subscribe, schema}, {
    server: this.ws,
    path: "/subscriptions",
    });
  }

That's all. Remember to kill server (ws instance) using OnModuleDestroy hook.

First of all, thanks for the help and quick responses. Unfortunately, I still think something is missing. I followed your directions using the nest graphql-apollo example for simplicity's sake, and I get these warnings:

(node:71978) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 3): TypeError: Cannot convert undefined or null to object
(node:71978) [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.

When trying to access GraphiQL on localhost, the browser churns for several minutes, and eventually I get ERR_CONNECTION_REFUSED. Any thoughts?

Will try to reproduce this locally

Could you provide a little bit more code? What actually have you changed? Did you add a snippet that I shared with your or sth more?

I cloned the graphql-apollo example and contained all changes within app.module.ts:

import {
  Inject,
  Module,
  MiddlewaresConsumer,
  NestModule,
  RequestMethod,
} from '@nestjs/common';
import { createServer } from 'http';
import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
import { GraphQLModule, GraphQLFactory } from '@nestjs/graphql';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import { execute, subscribe } from 'graphql';
import { CatsModule } from './cats/cats.module';

@Module({
  components: [
    {
      provide: "SUBSCRIPTION_SERVER",
      useFactory: async () => {
        const server = createServer();

        return new Promise((resolve) => server.listen(process.env.PORT || 3000, resolve));
      },
    }
  ],
  imports: [CatsModule, GraphQLModule],
})
export class ApplicationModule implements NestModule {
  constructor(private readonly graphQLFactory: GraphQLFactory, @Inject("SUBSCRIPTION_SERVER") private readonly ws) {}

  configure(consumer: MiddlewaresConsumer) {
    const schema = this.createSchema();

    this.initSubscriptionServer(schema);

    consumer
      .apply(graphiqlExpress({
        endpointURL: '/graphql', 
        subscriptionsEndpoint: `ws://localhost:${process.env.PORT || 3000}/subs`
      }))
      .forRoutes({ path: '/graphiql', method: RequestMethod.GET })
      .apply(graphqlExpress(req => ({ schema, rootValue: req })))
      .forRoutes({ path: '/graphql', method: RequestMethod.ALL });
  }

  createSchema() {
    const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql');
    const schema = this.graphQLFactory.createSchema({ typeDefs });
    return this.graphQLFactory.createSchema({ typeDefs });
  }

  initSubscriptionServer(schema) {
    const ss = new SubscriptionServer({execute, subscribe, schema}, {
      server: this.ws,
      path: "/subs",
    });
  }

  onModuleDestroy() {
    this.ws.close();
  }
}

@joefru I'm sorry.. It was my mistake 馃檨 Let's use code below instead:

{
      provide: 'SUBSCRIPTION_SERVER',
      useFactory: () => {
        const server = createServer();
        return new Promise(resolve => server.listen(process.env.PORT || 3000, () => resolve(server)));
      },
},

Making progress... That change let's me successfully create the subscription server. I still can't get a subscription to listen. I have this:

cats.resolvers.ts

import { Component, UseGuards } from '@nestjs/common';
import { Query, Mutation, Resolver, Subscription } from '@nestjs/graphql';

import { Cat } from './interfaces/cat.interface';
import { CatsService } from './cats.service';
import { CatsGuard } from './cats.guard';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

@Resolver('Cat')
export class CatsResolvers {
  constructor(private readonly catsService: CatsService) {}

  @Query()
  @UseGuards(CatsGuard)
  async getCats() {
    return await this.catsService.findAll();
  }

  @Query('cat')
  async findOneById(obj, args, context, info): Promise<Cat> {
    const { id } = args;
    return await this.catsService.findOneById(+id);
  }

  @Mutation('createCat')
  async create(obj, args, context, info): Promise<Cat> {
    const cat = await this.catsService.create(args.name);

    pubsub.publish('catCreated', {catCreated: cat});

    return cat;
  }

  @Subscription('catCreated')
  catCreated() {
    return {
      subscribe: () => pubsub.asyncIterator('catCreated'),
    };
  }
}

cats.types.graphql

type Query {
  getCats: [Cat]
  cat(id: ID!): Cat
  catByHumanId(id: ID!): Cat
}

type Mutation {
  createCat(name: String): Cat
}

type Subscription {
  catCreated: Cat
}

type Cat {
  id: Int
  name: String
  age: Int
  humanId: Int
}

When I run the following subscription in GraphiQL:

subscription catname {
  catCreated {
    name
  }
}

It displays [object Object] in the results window. It looks like it quickly flashes Your subscription data will appear here after server publication! and then immediately changes to [object Object]. Similar behavior was described in this thread. Unfortunately, the suggestions in that discussion have not led me to a solution.

Ultimately, I'd love to see the graphql-apollo example be extended to include a simple working subscription like catCreated that I can run from GraphiQL.

I updated example with the subscription server:
https://github.com/nestjs/nest/tree/master/examples/12-graphql-apollo

Subscribe in one window:

subscription {
  catCreated {
    name
  }
}

Mutate in the different one:

mutation {
  createCat(name: "Nest") {
    name
  }
}

Everythin works fine 馃檪

Downloaded the latest example but I still get [object Object] in the results window when I run the subscription query.

Also, what I'd really like to achieve is to have the subscription server listen on the same port as the express http server. That way, I can deploy a single application to Heroku, which only allows a single port per deployment.

I have tested this example and it works fine for me, there's no [object Object], that's weird. What kind of mutation are you executing from the graphiql? It has nothing to do with @nestjs/graphql module, but rather with this issue that you mentioned about earlier (https://github.com/apollographql/subscriptions-transport-ws/issues/236#issuecomment-326869192)

I was having the same issue with [Object Object] as @joefru but I was able to resolve it by playing around with the package versions in the dependency versions. Below are the versions i ended up with to have it working. Might help you out @joefru.

"dependencies": {
    "@nestjs/common": "^4.5.9",
    "@nestjs/core": "^4.5.10",
    "@nestjs/graphql": "^2.0.0",
    "apollo-server-express": "^1.3.2",
    "graphql": "0.11.7",
    "graphql-subscriptions": "^0.5.6",
    "graphql-tools": "^2.19.0",
    "reflect-metadata": "^0.1.12",
    "rxjs": "^5.5.6",
    "subscriptions-transport-ws": "^0.9.5",
    "typescript": "^2.6.2",
    "ws": "^4.0.0"
  },
  "devDependencies": {
    "@types/node": "^9.4.0",
    "ts-node": "^4.1.0"
  }

@kamilmysliwiec i really don't understand why you delete the comment...

Its a Community right ? and you can share your Subscription Methods to Others ? What is your point ? and problem ?

I am out will ban nest community for sure

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings