5. Writing mutation resolvers
10m

How a GraphQL mutation modifies data

We've written all of the we need for schema that apply to . Now, let's write resolvers for our schema's mutations. This process is nearly identical.

login

First, let's write a for Mutation.login, which enables a user to log in to our application. Add the following to your map below the Query :

server/src/resolvers.js
// Query: {
// ...
// },
Mutation: {
login: async (_, { email }, { dataSources }) => {
const user = await dataSources.userAPI.findOrCreateUser({ email });
if (user) {
user.token = Buffer.from(email).toString('base64');
return user;
}
},
},

This takes an email address and returns corresponding user data from our userAPI. We add a token to the object to represent the user's active session. In a later chapter, we'll learn how to persist this returned user data in our application client.

Authenticating logged-in users

The authentication method used in our example application is not at all secure and should not be used by production applications. However, you can apply the principles demonstrated below to a token-based authentication method that is secure.

The User object returned by our Mutation.login includes a token that clients can use to authenticate themselves to our server. Now, we need to add logic to our server to actually perform the authentication.

In src/index.js, import the isEmail function and pass a context function to the constructor of ApolloServer that matches the following:

server/src/index.js
const isEmail = require("isemail");
const server = new ApolloServer({
context: async ({ req }) => {
// simple auth check on every request
const auth = (req.headers && req.headers.authorization) || "";
const email = Buffer.from(auth, "base64").toString("ascii");
if (!isEmail.validate(email)) return { user: null };
// find a user by their email
const users = await store.users.findOrCreate({ where: { email } });
const user = (users && users[0]) || null;
return { user: { ...user.dataValues } };
},
// Additional constructor options
});

The context function defined above is called once for every GraphQL operation that clients send to our server. The return value of this function becomes the context argument that's passed to every that runs as part of that .

You might have noticed that dataSources is nowhere to be found, even though we expect it to be part of the context in our . This is because although we defined dataSources outside of context, it is automatically included for each .

Here's what our context function does:

  1. Obtain the value of the Authorization header (if any) included in the incoming request.
  2. Decode the value of the Authorization header.
  3. If the decoded value resembles an email address, obtain user details for that email address from the database and return an object that includes those details in the user .

By creating this context object at the beginning of each 's execution, all of our can access the details for the logged-in user and perform actions specifically for that user.

bookTrips and cancelTrip

Now back in resolvers.js, let's add for bookTrips and cancelTrip to the Mutation object.

server/src/resolvers.js
//Mutation: {
// login: ...
bookTrips: async (_, { launchIds }, { dataSources }) => {
const results = await dataSources.userAPI.bookTrips({ launchIds });
const launches = await dataSources.launchAPI.getLaunchesByIds({
launchIds,
});
return {
success: results && results.length === launchIds.length,
message:
results.length === launchIds.length
? 'trips booked successfully'
: `the following launches couldn't be booked: ${launchIds.filter(
id => !results.includes(id),
)}`,
launches,
};
},
cancelTrip: async (_, { launchId }, { dataSources }) => {
const result = await dataSources.userAPI.cancelTrip({ launchId });
if (!result)
return {
success: false,
message: 'failed to cancel trip',
};
const launch = await dataSources.launchAPI.getLaunchById({ launchId });
return {
success: true,
message: 'trip cancelled',
launches: [launch],
};
},

To match our schema, these two both return an object that conforms to the structure of the TripUpdateResponse type. This type's include a success indicator, a status message, and an array of launches that the either booked or canceled.

The bookTrips needs to account for the possibility of a partial success, where some are booked successfully and others fail. The code above indicates a partial success in the message .

Run test mutations

We're ready to test out our ! Return to Apollo Sandbox.

Obtain a login token

are structured exactly like queries, except they use the mutation keyword. Paste the below and run it:

mutation LoginUser {
login(email: "daisy@apollographql.com") {
token
}
}

The server will respond like this:

"data": {
"login": {
"token": "ZGFpc3lAYXBvbGxvZ3JhcGhxbC5jb20="
}
}

The value of the token is our login token (which is just the Base64 encoding of the email address we provided). We'll use this value in the next .

Book trips

Let's try booking some trips. Only authenticated users are allowed to book trips, so we'll include our login token in our request.

First, paste the below into your tool's editor:

mutation BookTrips {
bookTrips(launchIds: [67, 68, 69]) {
success
message
launches {
id
}
}
}

Next, use the following values to set the Headers in the Headers tab (a separate text area in the panel below the editor). We've provided both the header key and value to copy in:

Header Key
Authorization
Header Value
'ZGFpc3lAYXBvbGxvZ3JhcGhxbC5jb20='

Run the . You should see a success message, along with the ids of the trips we just booked.

Task!

Running manually like this is a helpful way to test out our API, but a real-world needs additional tooling to make sure it grows and changes safely. In the next section, we'll connect our server to Apollo Studio to activate that tooling.

Previous