9. Reference resolvers10m

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:

  • Implement the __resolveReference resolver functions for the entities
  • Migrate over the interface's __resolveType resolver
  • Copy over the relevant data sources
  • Return a representation of the entity from the monolith subgraph

✏️ 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.

  1. Open up the resolvers.js file in the accounts subgraph, and delete the default query that came with the template, along with its example resolver function.

    const resolvers = {
    - Query: {
    - example: () => 'Hello World!',
    - },
    };
  2. Inside the resolvers object, add a reference resolver for the Host type:

    subgraph-accounts/resolvers.js
    Host: {
    __resolveReference: (user, { dataSources }) => {
    return dataSources.accountsAPI.getUser(user.id);
    },
    },

    We're using the accountsAPI data source's getUser method and passing it the id of the user to retrieve that user's information.

    We haven't connected the accountsAPI yet, but we will later on in the lesson!

  3. Let's add a similar reference resolver for the Guest type.

    subgraph-accounts/resolvers.js
    Guest: {
    __resolveReference: (user, { dataSources }) => {
    return dataSources.accountsAPI.getUser(user.id);
    },
    },
  4. The full resolvers map should now look like this:

    subgraph-accounts/resolvers.js
    const resolvers = {
    Host: {
    __resolveReference: (user, { dataSources }) => {
    return dataSources.accountsAPI.getUser(user.id);
    },
    },
    Guest: {
    __resolveReference: (user, { dataSources }) => {
    return dataSources.accountsAPI.getUser(user.id);
    },
    },
    };

Perfect, the entities have their reference resolvers! These resolvers are used when the router sends this subgraph an entity representation. Now, let's jump over to the monolith subgraph and check out where we can return representations of these entities.

Returning entity representations

There are a few fields in the monolith subgraph that will need to reference the Host and Guest entities. For these fields, we're going to jump to their corresponding resolvers and have them return an entity representation.

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 a Host type
  • Booking.guest, which returns a Guest type
  • Review.author, which returns a User type

To get started, open 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.

monolith/resolvers.js
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.

monolith/resolvers.js
Booking: {
// other Booking resolvers
guest: ({ 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, and include it as the __typename in its entity representation.

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:

monolith/resolvers.js
Review: {
author: (review) => {
let role = '';
if (review.targetType === 'LISTING' || review.targetType === 'HOST') {
role = 'Guest';
} else {
role = 'Host';
}
return { __typename: role, id: review.authorId };
},
},

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! There's just one more resolver we need: __resolveType.

Resolving the interface

We need a resolveType resolver for the User interface! We actually already have one defined in the monolith subgraph, so all we need to do is move that function over to the accounts subgraph. This resolver should only belong in the accounts resolvers, so delete it from the monolith resolvers.

subgraph-accounts/resolvers.js
User: {
__resolveType(user) {
return user.role;
}
},

Note: Make sure you delete the __resolveType function from the monolith resolvers.

monolith/resolvers.js
- User: {
- __resolveType(user) {
- return user.role;
- }
- },

That's all the resolvers we need for our subgraphs to handle our schema changes!

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, because we've been using it in our resolvers. Let's tackle that next.

  1. Let's look at the datasources/accounts.js file in the monolith directory. This is the data source we want to use in the accounts subgraph!

  2. Copy the datasources/accounts.js file and paste it into the subgraph-accounts/datasources directory. This lets us access the data source in the accounts subgraph!

    You can also delete the placeholder datasources.js file in the subgraph-accounts/datasources directory while you're here.

  3. Open up subgraph-accounts/index.js and add the import at the top:

    subgraph-accounts/index.js
    const AccountsAPI = require("./datasources/accounts");
  4. Then, find the line where we return the context object for resolvers to use. We'll set up the AccountsAPI inside the dataSources object.

    subgraph-accounts/index.js
    return {
    ...userInfo,
    dataSources: {
    accountsAPI: new AccountsAPI(),
    },
    };

Testing our changes

Let's test whether our reference resolvers our working! Try out this query in the Explorer:

query GetListing {
listing(id: "listing-1") {
host {
name
profilePicture
profileDescription
}
reviews {
rating
author {
id
name
}
}
}
}

This query retrieves a listing's host information (testing out our Listing.host resolver) and a listing's reviews. For each review, it asks for the review's rating, author's id and name (testing out our Review.author resolver).

When you run the query, you should get back real data! Awesome! 🎉

One more thing, let's try out the query from last lesson:

query GetMyProfile {
me {
id
name
profilePicture
}
}

Make sure your Headers are set to:

Authorization: Bearer user-1

When you run the query... uh-oh! We still get an error, but it's a new one: Abstract type "User" must resolve to an Object type at runtime for field "Query.me".

Hmm, you might be thinking: "Didn't we implement a __resolveType resolver to take care of this?"

Well, we did, but that resolver belonged to the accounts subgraph. And right now, the me field in our GetMyProfile query above still belongs to the monolith subgraph! The router is asking the monolith subgraph to resolve the field, when really it should be asking the accounts subgraph.

We'll fix this in the next lesson.

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: Int
isInColor: Boolean
}
type YoungAdultNovel implements Book {
isbn: ID!
title: String!
genre: String!
wordCount: Int
numberOfChapters: Int
}
type LibraryMember @key(fields: "id") {
id: ID! @external
faveBook: Book!
}
Code Challenge!

Write a resolver for LibraryMember.faveBook to return a representation of the entity. You can use the book's hasPictures property 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

Let's finish up the accounts subgraph by moving over the remaining Query and Mutation fields it's responsible for.

Previous
Next