Overview
We recommend keeping resolvers focused on retrieving only the data it's responsible for. We want to reduce duplicate code and keep network calls to a minimum, making requests only when necessary.
In this lesson, we will:
- Learn about resolver chains
- Learn about the
parent
argument of a resolver
Resolver chains
A resolver chain is the order in which resolver functions are called when resolving a particular GraphQL operation. It can contain a sequential path as well as parallel branches.
Let's take an example from our project. This GetPlaylist
operation retrieves the name of a playlist.
query GetPlaylist($playlistId: ID!) {playlist(id: $playlistId) {name}}
When resolving this operation, the GraphQL server will first call the Query.playlist()
resolver function, then the Playlist.name()
function, which returns a string
type and ends the chain.
Each resolver passes the value it returns to the next function down, using the resolver's parent
argument. Hence: the resolver chain!
Remember, a resolver has access to a number of parameters. So far, we've used contextValue
(to access our SpotifyAPI
data source) and arg
(to get the id
for a playlist). parent
is another such parameter!
In the example above, Query.playlist()
returns a Playlist
object, which the Playlist.name()
resolver function would receive as its parent
argument.
Let's look at another GraphQL operation.
query GetPlaylistTracks($playlistId: ID!) {playlist(id: $playlistId) {nametracks {uri}}}
This time, we've added more fields and asked for each playlist's list of tracks, specifically their uri
values.
Our resolver chain grows, adding a parallel branch.
Note that since Playlist.tracks
returns a list of potentially multiple tracks, this resolver might run more than once to retrieve each track's URI.
Following the trail of the resolver, Playlist.tracks()
would have access to Playlist
as the parent
, Track.uri()
would have access to the Track
object as the parent
.
If our operation didn't include the tracks
field (like the first example we showed), then the Playlist.tracks()
function would never be called!
Implementing the Playlist.tracks
resolver
So far, we've defined resolver functions exclusively for fields that exist on our Query
type. But we can actually define a resolver function for any field in our schema.
Let's create a resolver function whose sole responsibility is to return tracks
data for a given Playlist
object.
Jump into resolvers.ts
. Here, we'll add a new entry, just below the Query
object, called Playlist
.
export const resolvers: Resolvers = {Query: {// query resolvers, featuredPlaylists and playlist},Playlist: {// TODO},};
Inside of the Playlist
object, we'll define a new resolver function called tracks
. Right away we'll return null
so that TypeScript continues to compile as we explore our function's parameters.
Playlist: {tracks: (parent, args, contextValue, info) => {return null;}},
We know the value of the parent
argument—by following the resolver chain, we know that this resolver will receive the Playlist
object that it's attempting to return tracks
for. (We won't need the args
, contextValue
, or info
parameters here, so we'll remove them from the function signature.)
Let's log out the value of parent
.
Playlist: {tracks: (parent) => {console.log(parent);return null;}},
Then we'll return to Sandbox to run a query that will call this resolver function.
query Playlist($playlistId: ID!) {playlist(id: $playlistId) {namedescriptiontracks {name}}}
And in the Variables panel:
{ "playlistId": "6LB6g7S5nc1uVVfj00Kh6Z" }
When we run this operation, the Response panel will show "Cannot return null for non-nullable field Playlist.tracks."
, 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 Playlist
object that we queried for—along with all of its properties. Now within the Playlist.tracks
resolver, let's clean up our log and return statements, and destructure parent
for its tracks
property.
Playlist: {tracks: ({tracks}) => {// TODO}},
Another path
There's another situation we need to consider here: we can query just a singular playlist with the Query.playlist
field, or we can access a list of featured playlists through Query.featuredPlaylists
.
However, if we review the result from querying the GET /browse/featured-playlists
endpoint, we'll see that it does not actually contain full-fledged track objects. Instead, each playlist contains a tracks
key with just two properties: href
and total
.
"tracks": {"href": "string","total": 5}
Instead of a list of track objects (similar to the list that the earlier GET /playlists/{playlist_id}
endpoint returns), we get a single object with two properties: total
, the total number of tracks available, and href
, a URL for the endpoint where we can retrieve the full list of track objects.
This is a common pattern in REST APIs. Imagine if the response did include the full list of track objects. That would make for a very large response, to have a list of playlists and a list of tracks for each playlist.
Instead, if the playlist we're resolving tracks
for only contains these two properties, we'll need to make one more additional call to the REST API. In this case, to the GET /playlists/{playlist_id}/tracks
endpoint. This means that we'll need to access the playlist's id
.
Let's update our Playlist.tracks
resolver to destructure the parent
argument for a playlist's id
as well.
tracks: ({ id, tracks }) => {// TODO};
We need our resolver function to handle these two situations.
- If the playlist contains
tracks.items
, map throughitems
and return each object'strack
property. - If the playlist does not contain
tracks.items
, make a follow-up request using the playlist'sid
to get the list of tracks.
We can handle that first scenario right away. We'll use a ternary to first check if items
exists, and if so, we'll map through the tracks and return them.
return tracks.items? tracks.items.map(({track}) => track): // make a call to the REST API for tracks
To make a follow-up call to our REST API, we'll first need to give our SpotifyAPI
class a new method. We'll call this method getTracks
, and give it a playlistId
argument.
We expect this method to return a Promise
that resolves to an array of Track
types. (Be sure to import the Track
type from ./types
at the top!)
getTracks(playlistId: string): Promise<Track[]> {// TODO}
Let's build our call to the GET /playlists/{playlist_id}/tracks
endpoint. We'll pass our this.get
call a type variable which represents the shape that our response will initially take; from the REST API we know that we'll get back an object with an items
property, which contains a list of Track
types. Because we'll await the results of this call, let's make our function async
.
async getTracks(playlistId: string): Promise<Track[]> {const response = await this.get<{ items: { track: Track }[] }>(`playlists/${playlistId}/tracks`)}
Now we can dig into the response
object for its items
and return them.
async getTracks(playlistId: string): Promise<Track[]> {const response = await this.get<{ items: { track: Track }[] }>(`playlists/${playlistId}/tracks`)return response?.items?.map(({track}) => track) ?? [];}
Great - now let's return to resolvers.ts
. We'll update our function to destructure its third positional argument, contextValue
, for the dataSources
property. Then, we'll complete our ternary in case track.items
does not exist: and we'll make a call to the getTracks
method, passing in our playlist's id
.
tracks: ({ tracks, id }, _, { dataSources }) => {return tracks.items? tracks.items.map(({ track }) => track): dataSources.spotifyAPI.getTracks(id);};
Unfortunately, there are some errors appearing everywhere we try to access items
.
Property 'items' does not exist on type 'Track[]'.
TypeScript is mad about something we've done here. Let's take a closer look in the next lesson.
Practice
Use the following schema and GraphQL query to answer the multiple choice question.
type Query {featuredArtists: [Artist!]!}type Artist {name: String!album: Album!}type Album {name: String!totalTracks: Int!releaseDate: String}
query GetFeaturedArtistsAlbumNames {featuredArtists {album {name}}}
GetFeaturedArtistsAlbumNames
query above?Key takeaways
- A resolver chain is the order in which resolver functions are called when resolving a particular GraphQL operation.
Up next
Our Track
type exists in our schema, but differences between our GraphQL types and our backend data objects are causing TypeScript to throw some errors. Let's see how we can get around this, and inform TypeScript about what our backend data looks like, in the next lesson.
Share your questions and comments about this lesson
This course is currently in
You'll need a GitHub account to post below. Don't have one? Post in our Odyssey forum instead.