I've got my new ApolloServer 4 client and server setup. It's working for queries and mutations, but not for subscriptions. Subscriptions aren't connecting at all. It's sending context to queries and mutations, but my context function isn't being triggered at all for subscriptions. I think it may need a split link of some kind but I've been studying this and trying different splitLinks for 3-4 weeks and haven't solved it yet.
What am I missing?
ApolloServer Setup
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";
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,
});
const serverCleanUp = useServer({schema}, 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) => {
// You can define your own function for setting a dynamic context
// or provide a static value
// return getDynamicContext(ctx, msg, args);
let 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
};
}
})
);
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
ApolloClient setup
import { ApolloClient, HttpLink, InMemoryCache, ApolloLink, ApolloProvider } from "@apollo/client";
// Create an HttpLink pointing to your GraphQL endpoint
const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql' });
// 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);
});
// Combine the middleware with the HttpLink
const apolloClient = new ApolloClient({
link: customHeadersMiddleware.concat(httpLink),
cache: new InMemoryCache(),
});
export {apolloClient};
It seems like I finally got Apollo setup working, with subscriptions, and context for both queries and subscriptions. That was so much work.
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