How to get Apollo 2.0 working with GraphQL + subscriptions

I’ve noticed there’s been a lot of confusion lately around Apollo’s recent change to version 2.0. A lot of packages are breaking and people are having trouble navigating it, especially since the docs and most examples online are not yet reflective of the changes.

So, I created a write-up on how to use Apollo 2.0 with a GraphQL server (with subscriptions!), along with an example Meteor app since there are virtually no examples showing how to do it in Meteor.

Here is the write-up and associated example app written with Meteor.

Enjoy!

5 Likes

Packages breaking? Hmm… Does apollo 2.0 still work with meteor-apollo-accounts?

I’m not sure. I haven’t tested it, but if anyone else is willing to try it out (maybe on top of my example app) and let us know, that would be cool. What I referred to as breaking was deprecated functions like SubscriptionManager and addGraphQLSubscriptions.

@michaelcbrook What is a good approach for passing the userid of the logged-in user to a resolver in Apollo 2.0? My Apollo 1.x approach doesn’t seem to be working yet for Apollo 2.0.

There are a couple ways - one where you can pass it over the websocket when using Subscriptions (adding the user to the context), or the other where you pass the user id with each request. I prefer the second method because you don’t need to manually close/re-open socket connections if a user logs out and a different one logs in.

Example using websockets:

Server

const context = {}; //this gets passed to createApolloServer...

new SubscriptionServer({
  schema,
  execute,
  subscribe,
  // on connect subscription lifecycle event
  onConnect: async (connectionParams, webSocket) => {
    
    let subscriptionContext = context;
    
    if (connectionParams.userToken) {
    
      //if user token is given, add user to context
      const lookupUser = function(context, userToken) {
        const hashedToken = Accounts._hashLoginToken(userToken);
    		const user = Meteor.users.findOne({ "services.resume.loginTokens.hashedToken": hashedToken });
    		if (user) {
          context.user = user;
    		}
        return context;
      };
      
      //must use async/await, otherwise database lookups will fail (not sure why?)
      subscriptionContext = await lookupUser(context, connectionParams.userToken);
      
    }
      
    return subscriptionContext;
    
  }
}, {
  server: WebApp.httpServer,
  path: '/subscriptions'
});

Client

const link = ApolloLink.split(
  operation => {
  	const operationAST = getOperationAST(operation.query, operation.operationName);
  	return !!operationAST && operationAST.operation === 'subscription';
  },
  new WebSocketLink({
		uri: wsUri,
		options: {
			reconnect: true, // tells client to reconnect websocket after being disconnected (which will happen after a hot-reload)
            // carry login state from client
            // it is recommended you use the secure version of websockets (wss) when transporting sensitive login information
			connectionParams: {
				userToken: localStorage.getItem("Meteor.loginToken")
			}
		}
	}),
  new HttpLink({ uri: httpUri })
);

Now you should have a user object in your context for each resolver if the user is successfully logged in. The pitfall with this method is if a user is logged in, then logs out and another user logs in during the same session, the old login token from the previous session will persist as long as the websocket connection is open. I had a hard time getting this to work properly.

Example passing user token with each request

The other (in my opinion, easier and cleaner) solution is to pass the user token with each query. This way, it is guaranteed the user must have a valid login token with each request, and it doesn’t live in websocket connections (so this will work with regular requests as well).

Client (using blaze-apollo)

const result = Template.instance().gqlQuery({
  query: GET_DATA,
  variables: {
    userToken: localStorage.getItem("Meteor.loginToken")
  }
}).get();

Server (in your resolvers.js)

const resolvers = {
  
  Query: {
    
    getUser(obj, args, context) {
      
      //check token exists
      if (!args.userToken) {
        throw new Meteor.Error("not-authenticated", "You must be logged in");
      }
      
      //validate token
      const hashedToken = Accounts._hashLoginToken(args.userToken);
      const user = Meteor.users.findOne({ "services.resume.loginTokens.hashedToken": hashedToken });
      if (!user) {
        throw new Meteor.Error("not-authenticated", "You must be logged in");
      }
      
      //user is logged in, do stuff if you want...
      
      return user;
      
    }
    
  }
  
};

You will have to validate the user token with in each resolver, but you could abstract this and you would have to do this anyway if it were typical REST queries. You’ll know for sure, though, the user is valid.

Hope that helps.

2 Likes