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

UPDATED CODE

I had ApolloGraphQL working on localdev using port 4000 for subscriptions, but on Galaxy I only have access to the Meteor port, i.e. usually port 3000. So I had to figure out how to get a websocket listener on port 3000. That took some research (described in this thread). Here is the updated code.

SERVER SETUP:

import {ApolloServer} from "@apollo/server";
import {expressMiddleware} from "@apollo/server/express4";
import {ApolloServerPluginDrainHttpServer} from "@apollo/server/plugin/drainHttpServer";
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 {array_of_cors_origins} from "../both";

const schema = makeExecutableSchema({typeDefs, resolvers});

// https://forums.meteor.com/t/working-apollo-client-server-setup-with-context-for-both-queries-and-subscriptions/61770/2?u=vikr00001
const meteorInternalVersionOfExpress = WebAppInternals.NpmModules.express.module;
const app = meteorInternalVersionOfExpress()
const httpServer = http.createServer(app);
const wsServer = new WebSocketServer({
    path: "/graphql",
    noServer: true,
});

//https://forums.meteor.com/t/apollo-server-express-server-setup-for-subscriptions/57871/2?u=vikr00001
WebApp.httpServer.on("upgrade", function upgrade(request, socket, head) {
    if (!request.url) {
        return;
    }
    switch (request.url) {
        case "/graphql":
            return wsServer.handleUpgrade(request, socket, head, function done(ws) {
                wsServer.emit("connection", ws, request);
            });
        default:
            break;
    }
});


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

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

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

await apolloServer.start();

WebApp.handlers.use(
    "/graphql",
    cors({
        origin: function (origin, callback) {
            const allowedOrigins = array_of_cors_origins;
            if (!origin || allowedOrigins.indexOf(origin) !== -1) {
                callback(null, true);
            } else {

                console.log('origin: ', origin)
                console.log('array_of_cors_origins: ', array_of_cors_origins)
                callback(new Error('Not allowed by CORS'));
            }
        },
        credentials: true,
    }),
    meteorInternalVersionOfExpress.json(),
    expressMiddleware(apolloServer, {
        context: async (ctx, msg, args) => {
            const context = await getDynamicContext(ctx, msg, args, false);
            return context;
        }
    }),
);

CLIENT 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";
import {graphQLQueryEndpoint, graphQLSubscriptionEndpoint} from "../both";

\\examples:
\\
\\graphQLQueryEndpoint == 'http://localhost:3000/graphql'
\\graphQLSubscriptionEndpoint == 'ws://localhost:3000/graphql'
\\
\\graphQLQueryEndpoint == 'https://myDomainHostedOnGalaxy.com/graphql'
\\graphQLSubscriptionEndpoint == 'wss://myDomainHostedOnGalaxy.com/graphql'

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

const httpLink = createHttpLink({
    uri: graphQLQueryEndpoint,
    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);
        // console.log('definition.kind: ', definition.kind)
        // console.log('definition.operation: ', definition?.operation)
        return (
            definition.kind === 'OperationDefinition' &&
            definition.operation === 'subscription'
        );
    },
    customHeadersMiddleware.concat(wsLink),
    customHeadersMiddleware.concat(httpLink)
)

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


export {apolloClient};