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
__resolveType
resolver - Implement the
__resolveReference
resolver functions for the entities - Return a representation of the entity from the
monolith
subgraph
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.js
file in themonolith
directory. Copy the file and paste it into thesubgraph-accounts/datasources
directory. This lets us access the data source in theaccounts
subgraph.You can also delete the placeholder
datasources.js
file in thesubgraph-accounts/datasources
directory while you're here.Open up
subgraph-accounts/index.js
and add the import at the top:subgraph-accounts/index.jsconst AccountsAPI = require("./datasources/accounts");Then, find the line where we return the
contextValue
object for resolvers to use. We'll set up theAccountsAPI
inside thedataSources
object.subgraph-accounts/index.jsreturn {...userInfo,dataSources: {accountsAPI: new AccountsAPI(),},};Let's not forget to include the
cache
when we initialize theAccountsAPI
, enabling theRESTDataSource
cache.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.js
file in theaccounts
subgraph, and delete the default query that came with the template, along with its_todo
resolver function.subgraph-accounts/resolvers.jsconst resolvers = {- Query: {- _todo: () => "TODO",- },};Find the resolvers for
Query.user
,Query.me
andMutation.updateProfile
in themonolith/resolvers.js
file (or you can copy them below). We'll paste these into theresolvers
map in theaccounts
subgraph.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
resolvers
object, add a reference resolver for theHost
type:subgraph-accounts/resolvers.jsHost: {__resolveReference: (user, { dataSources }) => {return dataSources.accountsAPI.getUser(user.id);},},We're using the
accountsAPI
data source'sgetUser
method and passing it theid
of the user to retrieve that user's information.Let's add a similar reference resolver for the
Guest
type.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 aHost
typeBooking.guest
, which returns aGuest
typeReview.author
, which returns aUser
type
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
__resolveReference
resolver function for that entity. - An entity representation is an object that includes the entity's
__typename
and@key
fields. - An interface needs a
__resolveType
resolver.
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.