13. Resolver chains
5m

Overview

We can for a listing's amenities, but only through the listing(id: ID) root , not through featuredListings. What's going on?

In this lesson, we will:

  • Learn about chains
  • Learn about the parent of a

Examining the data source response

Let's examine the response from our GET /featured-listings endpoint again. Open a new browser tab, and paste in the URL below.

The GET /featured-listings endpoint
https://rt-airlock-services-listing.herokuapp.com/featured-listings

The array that we get back contains the listings objects that we expect; but we'll notice that one property in each listing object is different! From the /featured-listings endpoint, a listing's list of "amenities" is an array that contains just the identifier for each amenity. No additional data!

This is a common pattern in REST APIs. Imagine if the amenities list for each listing included the full data for each amenity. That would make for a very large response, to have a list of listings and a list of full amenities for each listing! The problem would compound if the endpoint decided to return even more featured listings than the three we already have.

To make sure that we can return featured listings along with their amenities, we'll need to make one additional call per listing to the REST API. In this case, to a new endpoint: GET /listings/{listing_id}/amenities.

The next question becomes: where in our code will we make that call?

Accounting for missing amenities

To account for our missing amenities, we could update our featuredListings function. We might give it some extra logic to reach out to GET /listings/{listing_id}/amenities to fetch this extra data.

async getFeaturedListings(): Promise<Listing[]> {
const listings = await this.get<Listing[]>("featured-listings");
// for each listing ID, request this.get<Amenity[]>(`listings/{id}/amenities`)
// Then map each set of amenities back to its listing
return listings;
}

Well, this would work; but it would mean that every time we for featuredListings, we would always make an additional network call to the REST API, whether the asked for a listing's amenities or not.

So instead, we're going to make use of the chain.

Following the resolver chain

A resolver chain is the order in which functions are called when resolving a particular . It can contain a sequential path as well as parallel branches.

Let's take an example from our project. This GetListing retrieves the title of a listing.

query GetListing($listingId: ID!) {
listing(id: $listingId) {
title
}
}

When resolving this , the will first call the Query.listing() function, which returns a Listing type, then the Listing.title() which returns a String type and ends the chain.

Resolver chain in a diagram

Note: We didn't need to define a separate for Listing.title because the title property can be found directly on the object returned by Query.listing.

Each passes the value it returns to the next function down, using the resolver's parent . Hence: the chain!

Remember, a has access to a number of parameters. So far, we've used contextValue (to access our ListingAPI ) and args (to get the id for a listing). parent is another such parameter!

In the example above, Query.listing() returns a Listing object, which the Listing.title() would receive as its parent .

Let's look at another .

query GetListingAmenities($listingId: ID!) {
listing(id: $listingId) {
title
amenities {
name
}
}
}

This time, we've added more and asked for each listing's list of amenities, specifically their name values.

Our chain grows, adding a parallel branch.

Resolver chain in a diagram

Because Listing.amenities returns a list of potentially multiple amenities, this might run more than once to retrieve each amenity's name.

Following the trail of the , Listing.amenities() would have access to Listing as the parent, just as Amenity.name() would have access to the Amenity object as the parent.

If our didn't include the amenities (like the first example we showed), then the Listing.amenities() would never be called!

Implementing the Listing.amenities resolver

So far, we've defined functions exclusively for that exist on our Query type. But we can actually define a function for any in our schema.

Let's create a function whose sole responsibility is to return amenities data for a given Listing object.

Jump into resolvers.ts. Here, we'll add a new entry, just below the Query object, called Listing.

resolvers.ts
export const resolvers: Resolvers = {
Query: {
// query resolvers, featuredListings and listing
},
Listing: {
// TODO
},
};

Inside of the Listing object, we'll define a new function called amenities. Right away we'll return null so that TypeScript continues to compile as we explore our function's parameters.

resolvers.ts
Listing: {
amenities: (parent, args, contextValue, info) => {
return null;
}
},

We know the value of the parent —by following the chain, we know that this resolver will receive the Listing object that it's attempting to return amenities for. (We won't need the args or info parameters here, so we'll replace args with _ and remove info from the function signature.)

Let's log out the value of parent.

resolvers.ts
Listing: {
amenities: (parent, _, contextValue) => {
console.log(parent);
return null;
}
},

Then we'll return to Sandbox to run a that will call this function.

query GetFeaturedListings {
featuredListings {
id
title
description
amenities {
id
name
category
}
}
}

When we run this , the Response panel will show "Cannot return null for non-nullable field Listing.amenities.", but that's ok, we're more interested in investigating parent right now.

We can see from the value we logged out in the terminal that parent is the Listing object that we queried for—along with all of its properties. Now within the Listing.amenities , let's clean up our log and return statements. Then we'll destructure parent for its id and amenities properties, and contextValue for its dataSources.

resolvers.ts
Listing: {
amenities: ({ id, amenities }, _, { dataSources }) => {
// TODO
}
},

There are two scenarios we need our Listing.amenities to handle.

  1. If we've queried for a single listing, we'll already have full amenity data available on the parent . In this case, we can return the amenities directly.
  2. If our 's parent comes from Query.featuredListings, however, we'll have only an array of amenity IDs. In this case, we'll need to make a follow-up request to the REST API!
resolvers.ts
Listing: {
amenities: ({ id, amenities }, _, { dataSources }) => {
// If `amenities` contains full-fledged Amenity objects, return them
// Otherwise make a follow-up request to /listings/{listing_id}/amenities
}
},

Let's build out our new ListingAPI method for amenity data, and then we'll return to handle these two paths.

The getAmenities method

In listing-api.ts, let's bring in our Amenity type from types.ts.

listing-api.ts
import { Listing, Amenity } from "../types";

We'll give our class a new method: getAmenities. This method accepts a single , a listingId, which is a string, and returns a Promise that returns a list of Amenity types.

listing-api.ts
getAmenities(listingId: string): Promise<Amenity[]> {
// TODO
}

Inside the function, we'll call this.get, which returns a list of Amenity types, and pass in the endpoint we need.

listing-api.ts
getAmenities(listingId: string): Promise<Amenity[]> {
return this.get<Amenity[]>(`listings/${listingId}/amenities`);
}

Finishing the resolver

Then, back in our Listing.amenities , we'll make a call to the listingAPI.getAmenities method, passing in our Listing's id.

resolvers.ts
amenities: ({ id, amenities }, _, { dataSources }) => {
return dataSources.listingAPI.getAmenities(id);
};

We've taken care of the scenario when we need to request follow-up amenity data. Now, let's update our to first check whether we already have full amenity data available on the parent .

We've provided a utility that helps us do just that. Jump into src/helpers.ts and uncomment the code there. We'll use the validateFullAmenities function exported here: it takes an amenities parameter (a Amenity[] type), and checks to see whether at least some of the objects in the array provided contain a name property.

src/helpers.ts
export const validateFullAmenities = (amenityList: Amenity[]) =>
amenityList.some(hasOwnPropertyName);
Task!

Note: We made the arbitrary choice to check for the presence of a name property (to determine whether we're dealing with complete amenity data), but we could have just as well used the category property instead to perform the check.

Back in resolvers.ts, let's import the validateFullAmenities function.

resolvers.ts
import { validateFullAmenities } from "./helpers";

Down in our Listing.amenities , we'll first check whether our amenities contain all their properties. If so, we'll return them directly. Otherwise, we can make our follow-up request for additional amenity data.

amenities: ({ id, amenities }, _, { dataSources }) => {
return validateFullAmenities(amenities)
? amenities
: dataSources.listingAPI.getAmenities(id);
},

Trying out our queries

To see when our ListingAPI's getAmenities method gets called for follow-up amenity data, let's add a console log inside the method in listing-api.ts.

listing-api.ts
getAmenities(listingId: string): Promise<Amenity[]> {
console.log("Making a follow-up call for amenities with ", listingId);
return this.get<Amenity[]>(`listings/${listingId}/amenities`)
}

Now we can return to Explorer at http://localhost:4000 and try out a few queries.

First, a for an individual listing.

query GetListing($listingId: ID!) {
listing(id: $listingId) {
amenities {
id
name
}
}
}

And in the Variables panel:

{
"listingId": "listing-1"
}

When we run this , we should see that the response for a single listing and its amenities hasn't changed. Our simply returns amenities from the parent .

Now let's try the same thing for our featured listings . Open up a new tab in the Explorer, and paste in the following query.

query GetFeaturedListings {
featuredListings {
id
title
description
amenities {
id
name
category
}
}
}

Now when we run this , we'll still see amenity data—but our terminal shows that three additional requests have been made to populate the remaining properties for each listing's set of amenities!

Making a follow-up call for amenities with listing-1
Making a follow-up call for amenities with listing-2
Making a follow-up call for amenities with listing-3

Practice

Use the following schema and to answer the multiple choice question.

An example schema
type Query {
featuredPlanets: [Planet!]!
}
type Planet {
name: String!
galaxy: Galaxy!
}
type Galaxy {
name: String!
totalPlanets: Int!
dateDiscovered: String
}
An example query operation
query GetFeaturedPlanetsGalaxies {
featuredPlanets {
galaxy {
name
}
}
}
Which of the following accurately describes the resolver chain for the GetFeaturedPlanetsGalaxies query above?

Key takeaways

  • A chain is the order in which resolver functions are called when resolving a particular .

Up next

Feeling confident with queries? It's time to explore the other side of : .

Previous

Share your questions and comments about this lesson

This course is currently in

beta
. 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.