3

I've just finished up a React/Node.js/Apollo GraphQL/Postgres personal project. I've had minimal issues getting the app working on Heroku, and I'm able to make GraphQL queries and mutations without issue. However, when it comes to GraphQL subscriptions, the client hasn't been able to connect to the web socket server, so subscriptions keep failing.

I've created a simple example demonstrating the problem, which I've hosted here on Heroku and whose code I've posted below. The simple example app allows you to add users to a list and view added users. GraphQL queries and mutations both work(you just have to refresh the page to see changes since subscriptions aren't working). However, if you examine the developer console, you'll see an error being printed indicating that the browser can't establish a connection to the provided websocket address-thus preventing GrpahQL subscriptions from working.

I've already tried:

  1. Different ports including 80, 443, and 3000
  2. using ws and wss

(Github repo).

src/index.js

const dotenv = require("dotenv")
const express = require("express")
const cors = require("cors")
const {typeDefs, resolvers} = require("./schema")
const { ApolloServer } = require("apollo-server-express")
const {createServer} = require('http')
const {execute, subscribe} = require('graphql')
const {SubscriptionServer} = require('subscriptions-transport-ws')
const {makeExecutableSchema} = require('@graphql-tools/schema')
const path = require("path")

dotenv.config()
async function startApolloServer(){
    const app = express()
    app.use(express.json());

    app.use(express.static(__dirname + "/../build"))
    app.get("*", (req, res) => {
        res.sendFile(path.resolve(__dirname, '..' ,'build' , 'index.html'))
    })
    
    app.use(cors());

    dotenv.config();

    const httpServer = createServer(app)

    const schema = makeExecutableSchema({typeDefs, resolvers})

    const subscriptionServer = SubscriptionServer.create(
        {schema,execute,subscribe}, 
        {server: httpServer, path: "/subscriptions"})

    const server = new ApolloServer({
        schema,
        plugins: [{
            async serverWillStart(){
                return {
                    async drainServer() {
                        subscriptionServer.close()
                    }
                }
            }
        }
        ],
        context: ({req}) => {
            const user = req.user || null;
            return { user }
        }
    })

    await server.start()
    server.applyMiddleware({app})

    await new Promise(resolve => httpServer.listen({port: process.env.PORT}, resolve))

    return {server, app}
}

startApolloServer()

src/models/index.js

const {Sequelize, DataTypes} = require("sequelize");

const dotenv = require("dotenv")
dotenv.config()

const sequelize = new Sequelize(process.env.DATABASE_URL ,{
    dialect: "postgres",
    operatorAliases: false,
    logging: false,
    dialectOptions: {
        ssl: {
            require: true,
            rejectUnauthorized: false
        }
    }
})

let auser = sequelize.define("auser", {
        name:{
            type: DataTypes.TEXT,
            allowNull: false
        },
        email: {
            type: DataTypes.TEXT,
            allowNull: false
        }
    },
    {
        freezeTableName: true
    }
)

const db = {}

db.sequelize = sequelize
db.Sequelize = Sequelize
db.auser = auser

db.sequelize.sync()

module.exports = db

src/schema/index.js

const {buildSchema} = require('graphql')
const db = require("../models")
const db_auser = db.auser
const { PubSub } = require("graphql-subscriptions")
const pubsub = new PubSub()
const schema = buildSchema(`
    type User{
        name: String
        email: String
    }
    type Query{
        users: [User]
    }
    type Mutation{
        new_user(name: String, email: String): User
    }
    type Subscription{
        new_user: User
    }

`)

let resolvers = {
    Query: {
        users: async(prev, args, context) => {
            const users_query = await db_auser.findAll()
            const users = users_query.map((user) => {
                return {name: user.dataValues.name, email: user.dataValues.email}
            })
            return users
        }
    },
    Mutation: {
        new_user: async(prev, {name, email}, context) => {
            let new_user = {
                name: name,
                email: email
            }
            await db_auser.create(new_user)
            pubsub.publish('NEW_USER', {new_user: new_user})
            return new_user
        }
    },
    Subscription: {
        new_user: {
            subscribe: () => pubsub.asyncIterator(['NEW_USER'])
        }
    }

}

module.exports.typeDefs = schema
module.exports.resolvers = resolvers

client/src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache, split } from '@apollo/client';
import { WebSocketLink} from "@apollo/client/link/ws"
import { getMainDefinition } from '@apollo/client/utilities';

const httpLink = createHttpLink({
  uri: '/graphql', 
});


const wsLink = new WebSocketLink({
  uri:  "wss://" + window.location.hostname + ":80/subscriptions",
  options: {
    reconnect: true
  }
})

const splitLink = split(
    ({query}) => {
      const definition = getMainDefinition(query)
      return (
        definition.kind === 'OperationDefinition' &&
        definition.operation === 'subscription'
      )
    },
    wsLink,
    httpLink
)

const apolloClient = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache()
})

ReactDOM.render(
  <React.StrictMode>
    <ApolloProvider client={apolloClient}>
      <App />
    </ApolloProvider>
  </React.StrictMode>,
  document.getElementById('root')
);

client/src/App.js

import React, { useState } from 'react';
import {
  useQuery,
  useMutation,
  gql
} from "@apollo/client"

const USERS = gql`
  query{
    users{
      name
      email
    }
  }
`

const NEW_USER = gql`
  mutation New_user($name: String, $email: String){
    new_user(name: $name, email: $email){
      name
      email
    }
  }
  
`

const NEW_USER_SUBSCRIPTION = gql`
  subscription{
    new_user{
      name
      email
    }
  }
`

function App() {
  const [user, set_user] = useState({name: "", email: ""})
  const {subscribeToMore, loading, data} = useQuery(USERS, { fetchPolicy: "cache-and-network"})
  const [new_user_mutation] = useMutation(NEW_USER)
  subscribeToMore({
    document: NEW_USER_SUBSCRIPTION,
    updateQuery: (prev, {subscriptionData}) => {
      if(!subscriptionData.data) return prev
      const new_user = subscriptionData.data.new_user
      return {users: [ ...prev.users,  new_user]}
    }
  })
  async function handleSubmit(event){
    event.preventDefault()
    await new_user_mutation({variables: {name: user.name, email: user.email}})
  }

  return (
    <div className="App">
      <h1>Users</h1>
      <div>
        {!loading && data && data.users.map((user)=> {
          return <div>Name: {user.name} Email: {user.email}</div>
        })}
      </div>
      <h2>Add a user</h2>
      <form onSubmit={handleSubmit}>
        <input type="text" placeholder="Name" value={user.name} onChange={(event) => {set_user({...user, name: event.target.value})}}/>
        <input type="text" placeholder="Email" value={user.email} onChange={(event) => {set_user({...user, email: event.target.value})}}/>
        <input type="submit" value="Submit"/>
      </form>
    </div>
  );
}

export default App;
Chris
  • 121
  • 6

0 Answers0