Working: Apollo Client/Server Setup, with Context, for Both Queries and Subscriptions

It seems like I finally got Apollo setup working, with subscriptions, and context for both queries and subscriptions. That was so much work. People will be so much better off when they can just drop in an updated version of swydo.

Thanks to @storyteller and @minhna for your help on this.

Here’s my current setup.

If you see stuff that can be improved, please let me know. I’m sure there are things that can be improved.

E.g. objects from the Sequelize ORM have their data in a dataValues field of an object. That works fine for queries and mutations, but currently if I want them to arrive on the client in a subscription, I have to do something like myObject = myObject.dataValues. Then the data makes it to the client in data from a useSubscription call.

ApolloClient Setup

import {ApolloClient, split, HttpLink, InMemoryCache, ApolloLink, ApolloProvider, createHttpLink} from "@apollo/client";
import {GraphQLWsLink} from "@apollo/client/link/subscriptions";
import {setContext} from "@apollo/client/link/context";
import {getMainDefinition} from "@apollo/client/utilities";
import {createClient} from "graphql-ws";

const theWsLinkUrl = 'ws://localhost:4000/graphql'
const appApiUrl = 'http://localhost:4000/graphql'



const wsLink = theWsLinkUrl
    ? new GraphQLWsLink(
        createClient({
            url: theWsLinkUrl,
            connectionParams: () => {
                return {
                    authorization: localStorage.getItem("Meteor.loginToken") || "",
                };
            },
            on: {
                error: props => {
                    console.log("wsLink error", props);
                },
            },
        }),
    )
    : null;

const httpLink = createHttpLink({
    uri: appApiUrl,
    fetchOptions: {
        rejectUnauthorized: false,
    },
});


// Middleware to add custom headers
const customHeadersMiddleware = new ApolloLink((operation, forward) => {
    // Define your custom headers
    const customHeaders = {
        "token": localStorage.getItem("Meteor.loginToken")
    };

    // Use operation.setContext to add the custom headers to the request
    operation.setContext(({ headers }) => ({
        headers: {
            ...headers,
            ...customHeaders,
        },
    }));

    return forward(operation);
});

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

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


export {apolloClient};

ApolloServer

import {ApolloServer} from "@apollo/server";
import {expressMiddleware} from "@apollo/server/express4";
import {ApolloServerPluginDrainHttpServer} from "@apollo/server/plugin/drainHttpServer";
import express from "express";
import http from "http";
import cors from "cors";
import {PubSub, withFilter} from "graphql-subscriptions-continued";
import {WebSocketServer} from "ws";
import {useServer} from "graphql-ws/lib/use/ws";
import {makeExecutableSchema} from "@graphql-tools/schema";
import {MongodbPubSub} from 'graphql-mongodb-subscriptions';
import {getUserIdByLoginToken} from "./utils_server/meteor-apollo-utils";
import typeDefs from "./api/schema";
import {resolvers} from "./api/resolvers";
import mongoose from "mongoose";
import bodyParser from "body-parser";
import {getMainDefinition} from "@apollo/client/utilities";

const MONGODB_URI = `mongodb://localhost:3001/meteor`;

const connectToDb = async () => {
    await mongoose.connect(MONGODB_URI);
};

await connectToDb();
console.log('🎉 Connected to database successfully');

const mongodbpubsub = new MongodbPubSub({
    connectionDb: mongoose.connections[0].db
});
// Create the schema, which will be used separately by ApolloServer and
// the WebSocket server.
const schema = makeExecutableSchema({typeDefs, resolvers});

// https://www.youtube.com/watch?v=AcZ5dcYMwA4
const app = express();
const httpServer = http.createServer(app);
const wsServer = new WebSocketServer({
// port: 4000,
    path: "/graphql",
    server: httpServer,
});

async function getDynamicContext(ctx, msg, args, isSubscription){
    let token = null;
    if (isSubscription){
        token = ctx.connectionParams.authorization
    }
    else{
        token = ctx.req.headers['token']
    }

    let user = null;
    let userId = null;
    try {
        if ((!!token) && (token !== "null")) {
            [user, userId] = await getUserIdByLoginToken(token);
        }
    } catch (error) {
        console.log('context: ', error)
    }

    let clientIp = '';
    try {
        clientIp = ctx.req.headers['x-forwarded-for'] || ctx.req.connection.remoteAddress;
    } catch {
        console.log("Couldn't get clientIp in ctx.req")
    }

    return {
        user: user,
        userId: userId,
        clientIp: clientIp,
        pubsub: mongodbpubsub
    };
}

const serverCleanUp =useServer({
    schema,
    context: async (ctx, msg, args) => {
        let context = await getDynamicContext(ctx, msg, args, true)
        return context;
    },
}, wsServer);

const apolloServer = new ApolloServer({
    schema,
    plugins: [
        ApolloServerPluginDrainHttpServer({httpServer}),
        {
            async serverWillStart() {
                return {
                    async drainServer() {
                        await serverCleanUp.dispose();
                    }
                }
            }
        }
    ],
});

await apolloServer.start();

app.use(
    "/graphql",
    cors({
        origin: "http://localhost:3000",
        credentials: true,
    }),
    express.json(),
    expressMiddleware(apolloServer, {
        context: async (ctx, msg, args) => {
            const context = await getDynamicContext(ctx, msg, args, false);
            return context;
        }
    }),
);

await new Promise((resolve) => httpServer.listen({port: 4000}, resolve));

const PORT = 4000;
console.log(`Server is now running on http://localhost:${PORT}/graphql`);

// https://github.com/mjwheatley/graphql-mongodb-subscriptions/issues/43
// https://github.com/mjwheatley/apollo-graphql-mongodb/blob/main/src/index.ts
// https://www.apollographql.com/docs/apollo-server/migration/#migrate-from-apollo-server-express
// …in the section following the text:
//     The context function's syntax is similar for the expressMiddleware function:
// https://www.apollographql.com/docs/apollo-server/data/subscriptions
// https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries
3 Likes

There is a lot of stuff going on. Probably would be a good idea to go through it to make sure that everything is needed. Especially related to GraphQL subscriptions.
Little nitpick so far with express. I would import that from WebApp:

const express = WebAppInternals.NpmModules.express.module

so that there isn’t an issue with version mismatch in the future.

1 Like

I’ve updated the Express initializer per your advice. Thanks!

Probably would be a good idea to go through it to make sure that everything is needed. Especially related to GraphQL subscriptions.

Agreed. For me, the hardest part to get working, was subscriptions with context.