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