Overview
We added the User interface as well as the Host and Guest entities to the accounts subgraph schema, but they need accompanying resolvers! In this lesson, we will:
- Copy over the relevant data sources
- Implement resolvers for root fields
- Implement the interface's
__resolveTyperesolver - Implement the
__resolveReferenceresolver functions for the entities - Return a representation of the entity from the
monolithsubgraph
Creating data sources
Currently, our accounts subgraph doesn't have any data sources connected yet. However, we know that it needs the accountsAPI data source to get access to user and account information.
Let's look at the
datasources/accounts.jsfile in themonolithdirectory. Copy the file and paste it into thesubgraph-accounts/datasourcesdirectory. This lets us access the data source in theaccountssubgraph.You can also delete the placeholder
datasources.jsfile in thesubgraph-accounts/datasourcesdirectory while you're here.Open up
subgraph-accounts/index.jsand add the import at the top:subgraph-accounts/index.jsconst AccountsAPI = require("./datasources/accounts");Then, find the line where we return the
contextValueobject for resolvers to use. We'll set up theAccountsAPIinside thedataSourcesobject.subgraph-accounts/index.jsreturn {...userInfo,dataSources: {accountsAPI: new AccountsAPI(),},};Let's not forget to include the
cachewhen we initialize theAccountsAPI, enabling theRESTDataSourcecache.subgraph-accounts/index.jsreturn {...userInfo,dataSources: {accountsAPI: new AccountsAPI({ cache }),},};
Great, we can now use this data source in our resolvers!
Resolving the Query and Mutation fields
Let's first tackle the resolvers for our root fields.
Open up the
resolvers.jsfile in theaccountssubgraph, and delete the default query that came with the template, along with its_todoresolver function.subgraph-accounts/resolvers.jsconst resolvers = {- Query: {- _todo: () => "TODO",- },};Find the resolvers for
Query.user,Query.meandMutation.updateProfilein themonolith/resolvers.jsfile (or you can copy them below). We'll paste these into theresolversmap in theaccountssubgraph.subgraph-accounts/resolvers.jsQuery: {user: async (_, { id }, { dataSources }) => {const user = await dataSources.accountsAPI.getUser(id);if (!user) {throw new Error("No user found for this Id");}return user;},me: async (_, __, { dataSources, userId }) => {if (!userId) throw new AuthenticationError(authErrMessage);const user = await dataSources.accountsAPI.getUser(userId);return user;},},Mutation: {updateProfile: async (_,{ updateProfileInput },{ dataSources, userId }) => {if (!userId) throw new AuthenticationError(authErrMessage);try {const updatedUser = await dataSources.accountsAPI.updateUser({userId,userInfo: updateProfileInput,});return {code: 200,success: true,message: "Profile successfully updated!",user: updatedUser,};} catch (err) {return {code: 400,success: false,message: err.message,};}},},
Resolving the interface
Next let's tackle the User interface, which needs a resolveType. We actually already have one defined in the monolith subgraph, so all we need to do is copy that function over to the accounts subgraph.
User: {__resolveType(user) {return user.role;}},
Resolving entities
As we covered in Voyage I, subgraphs that contribute fields to an entity use a special resolver called a reference resolver to resolve entities. This function enables the router to directly access entity fields that each subgraph contributes.
We'll implement reference resolvers for the Host and Guest entities in the accounts subgraph.
Inside the
resolversobject, add a reference resolver for theHosttype:subgraph-accounts/resolvers.jsHost: {__resolveReference: (user, { dataSources }) => {return dataSources.accountsAPI.getUser(user.id);},},We're using the
accountsAPIdata source'sgetUsermethod and passing it theidof the user to retrieve that user's information.Let's add a similar reference resolver for the
Guesttype.subgraph-accounts/resolvers.jsGuest: {__resolveReference: (user, { dataSources }) => {return dataSources.accountsAPI.getUser(user.id);},},
Perfect, the entities have their reference resolvers!
We've covered all of our accounts schema fields with resolvers. One last thing before we test out our changes...
The monolith subgraph
In a previous lesson, we mentioned that we could include other fields in the accounts subgraph, specifically: Listing.host, Booking.guest and Review.author. We're going to keep these fields in the monolith schema for now (eventually they'll migrate their way over to listings, bookings and reviews subgraphs respectively), but we can update the resolvers.
Currently, the resolver for each of these fields uses the Accounts API. For example:
Listing: {host: ({ hostId }, _, { dataSources }) => {return dataSources.accountsAPI.getUser(hostId);},// other resolvers}
We want to start removing this dependency on the Accounts service (after all, that's what the accounts subgraph is for!). So we'll remove these calls to the accountsAPI and return entity representations instead.
Returning entity representations
If you recall from Voyage I, an entity representation is an object that the router uses to represent a specific entity from another subgraph. It contains a __typename property and values for the entity's primary key fields. In our case, the primary key field for both Host and Guest is id.
We'll return an entity representation for each the following resolvers in the monolith subgraph:
Listing.host, which returns aHosttypeBooking.guest, which returns aGuesttypeReview.author, which returns aUsertype
To get started, open up the resolvers.js file in the monolith directory.
Listing.host
Let's find the Listing.host resolver. We'll remove the body of the function, where it currently calls the accountsAPI data source. We now want this resolver to return a representation of the Host entity.
Listing: {host: ({ hostId }) => {return { id: hostId };},}
Here we've destructured the parent argument (which is a Listing object) to retrieve the hostId property. This hostId is used as the value for the primary key field id.
Note: We don't need to include the __typename field in this entity representation. The router automatically knows how to get the __typename field (because __typename is a meta-field that's included on all objects, per the GraphQL specification).
Booking.guest
Let's do the same for the Booking.guest resolver. Find the resolver in the resolvers.js file and replace the body of the function so that it returns an object as the entity representation.
Booking: {// other Booking resolversguest: ({ guestId }) => {return { id: guestId };},}
Similar to the Listing.host resolver, here we've destructured the parent argument (a Booking object) to retrieve the guestId property. This guestId is used as the value for the primary key field id.
Review.author
Finally, we have the Review.author resolver, which we'll handle slightly differently.
In the schema, the Review.author field returns a User interface, which is an abstract type. So the Review.author resolver will need to specify whether the returned type will be a Host or a Guest.
To determine the type, we'll need to look at business-domain-specific logic. We know that only guests can write reviews for listings and hosts. Similarly, only hosts can write reviews for guests. Knowing that, we can investigate the shape of a Review object and determine what property might be able to help us with that logic. Here's what a review looks like:
{"id": "review-1","authorId": "user-2","targetId": "listing-1","bookingId": "booking-1","targetType": "LISTING","text": "Wow, what an experience! I've never stayed in a cave before, so I was a little unprepared. Luckily, this listing had all the amenities I needed to feel safe and prepared for anything.","rating": 4}
We can use the review's targetType property (which will either be LISTING, GUEST or HOST) to determine what the review author's type is. If a user is writing a review about a Listing or a Host, we can safely assume that user is a Guest!
Find the Review.author resolver in the resolvers.js file and replace it with the following:
Review: {author: (review) => {let role = '';if (review.targetType === 'LISTING' || review.targetType === 'HOST') {role = 'Guest';} else {role = 'Host';}return { id: review.authorId, role };},},
Note: We've renamed the first argument of the resolver (parent) to review to clarify the argument's type.
And that's it for all the resolvers that return representations of entities!
Practice
Use this schema for the following code challenge:
interface Book {isbn: ID!title: String!genre: String!}type PictureBook implements Book @key(fields: "isbn") {isbn: ID!title: String!genre: String!numberOfPictures: IntisInColor: Boolean}type YoungAdultNovel implements Book {isbn: ID!title: String!genre: String!wordCount: IntnumberOfChapters: Int}type LibraryMember @key(fields: "id") {id: ID! @externalfaveBook: Book!}
Write a resolver for LibraryMember.faveBook to return a representation of the entity. The underlying object for each book contains a hasPictures property, which you can use to determine if it's a PictureBook or a YoungAdultNovel.
Key takeaways
- Any subgraph that contributes fields to an entity needs to define a
__resolveReferenceresolver function for that entity. - An entity representation is an object that includes the entity's
__typenameand@keyfields. - An interface needs a
__resolveTyperesolver.
Up next
Whew, that's it for resolvers! In the next lesson, we'll test that our changes are working locally before publishing the new accounts subgraph in GraphOS!
Share your questions and comments about this lesson
Your feedback helps us improve! If you're stuck or confused, let us know and we'll help you out. All comments are public and must follow the Apollo Code of Conduct. Note that comments that have been resolved or addressed may be removed.
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.