Overview
Even though we've defined the Listing
entity in our reviews
subgraph and contributed fields to it, we haven't told it how to resolve these fields.
In this lesson, we will:
- Create a reference resolver
- Populate the
reviews
andoverallRating
fields with data
The reference resolver
When we query for a particular listing and its review data, we need a way to tell the reviews
subgraph which listing we're talking about. Then the reviews
subgraph will understand what data to provide!
To do exactly this, the router passes along the entity representation we talked about in the last lesson: the minimum information required for the reviews
subgraph to put together some idea of which listing it's fetching data for.
{"__typename": "Listing","id": "listing-3"}
But where in the reviews
subgraph does this entity representation actually go?
This is just the piece we're missing: the reference resolver. The reference resolver is the special method that can receive an entity representation from the router, create a new instance of a Listing
from that representation data, and return it.
Let's see what this looks like in code, and walk through the process step-by-step.
Adding __resolveReference
Back in the reviews
subgraph, we need to tell our server what to do when it receives an entity representation for a particular listing. We'll do this by defining a special function, called __resolveReference
, under a new entry in our resolvers map for Listing
.
Open up reviews/src/resolvers.ts
. Let's add the Listing
key and the reference resolver as shown below.
// Query, Mutation entries aboveListing: {__resolveReference: () => {},}
You'll see an error on this new function we've defined. This is because __resolveReference
is not a known property on our Listing
type as we've described it in the reviews
schema.
Object literal may only specify known properties, and '__resolveReference'does not exist in type 'ListingResolvers<any, Listing>'.
We can solve this by adding an additional property to our codegen.ts
. Inside of the config
object, we can add a new key called federation
, and set its value to true
. This lets our codegen process consider some of the federation-specific requirements of our resolvers: such as resolving entity representations received from the router!
config: {contextType: "./context#DataSourceContext",federation: true},
You might not see a change in your resolvers.ts
file; but this time, TypeScript is upset that our function hasn't returned anything yet. Let's take care of that next.
We're going to receive the entity representation in our __resolveReference
function as a parameter called representation
. Then, we'll log it out, and return it.
__resolveReference: (representation) => {console.log(representation)return representation;},
Even with this change, our function has yet another error. Here's what we'll see when we hover over __resolveReference
.
Property 'reviews' is missing in type '{ __typename: "Listing"; }& GraphQLRecursivePick<Listing, { id: true; }>' but required in type 'Listing'.
This long error is telling us one thing loud and clear: the "listing" that the __resolveReference
function receives (an entity representation from the router) is missing some of the essential properties that we said it would have in our schema file. Essentially, our codegen process expects all of the Listing
objects we work with in the resolvers.ts
file to have all of the following fields.
type Listing @key(fields: "id") {id: ID!"The submitted reviews for this listing"reviews: [Review!]!"The overall calculated rating for a listing"overallRating: Float}
The entity representation, however, consists of just two properties: __typename
and id
.
We need to fix the codegen process' idea of what a Listing
looks like when it enters the __resolveReference
function as an entity representation. To do this, we'll use a model.
Introducing models
The GraphQL schema defines how object types relate and how we get from one to the next. By moving from object to object, we can construct detailed queries that fetch everything we need in a single client request.
It's the job of the resolver functions to make this magic possible: they need the freedom to receive data that might not look like the types in our schema, and perform the logic needed to return the types we do expect.
To maintain type-safety, we need to clarify how our types of data differ between what resolvers receive (from data sources, or the router, as with our entity representation) and what resolvers return to clients.
In our case, our __resolveReference
resolver thinks that it's going to receive an object of data that looks like our Listing
GraphQL type; so we need to use a model to redefine its expectation.
Adding a ListingEntityRepresentation
model
We'll create a new model that lets us more accurately describe the shape the listing entity representation takes. Then we'll integrate the model into our codegen process and resolve our type errors.
In the reviews/src
directory, create a new file called models.ts
.
📦 src┣ 📂 datasources┣ 📂 sequelize┣ 📄 context.ts┣ 📄 graphql.d.ts┣ 📄 index.ts┣ 📄 models.ts┣ 📄 resolvers.ts┣ 📄 schema.graphql┗ 📄 types.ts
Inside, we'll define a basic model called ListingEntityRepresentation
. We'll give it the single property we care about: id
.
export type ListingEntityRepresentation = {id: string;};
Next, we'll add this model to our codegen process in codegen.ts
.
Under the config
key, we'll add another property called mappers
. Here, we're able to specify a path to each model we want to be used as a mapper for a particular GraphQL type. This means that when codegen sees us working with a particular GraphQL object in our resolvers, it'll use the model that we provide as the reference for which properties the object should have.
config: {contextType: "./context#DataSourceContext",federation: true,mappers: {Listing: "./models#ListingEntityRepresentation"}},
Let's stop our running reviews
server and restart it to make sure that the new codegen settings are applied. This should take care of our error!
Now let's try it out. Let's return to Sandbox and try out our query again.
query GetListingAndReviews {listing(id: "listing-1") {titledescriptionnumOfBedsamenities {namecategory}overallRatingreviews {idtext}}}
When we run the query, we still won't get any review-related data back; but when we check the terminal of our reviews
subgraph, we'll see that our entity representation has arrived and is printed out!
{__typename=Listing, id=listing-1}
Our reviews
subgraph is successfully receiving the listing representation from the router; this means we know which listing to provide data for. Now, we just need to define the resolvers for the fields that the reviews
subgraph is responsible for: reviews
and overallRating
.
Adding Listing.reviews
Let's tackle the reviews
method first.
Return to reviews/src/resolvers.ts
. Let's clean up the console.log
statement from __resolveReference
. Then, just below __resolveReference
, we'll add a new resolver for the Listing.reviews
field.
Listing: {__resolveReference: (representation) => {return representation;},reviews: () => {}}
This method should use the id
from the entity representation to find and return all the relevant reviews in the database. Because this resolver resolves a field on an entity type, we know that the previous resolver in the chain was the __resolveReference
resolver we just defined. This means we can access its return value (the Listing
instance) using parent
, the first positional argument each resolver receives.
Let's destructure parent
for the id
property.
reviews: ({ id }) => {// TODO},
Our resolvers already have access to a property on the server's dataSources
called reviewsDb
, which is a class that provides a number of methods that operate on the underlying reviews database. Let's destructure our resolver's third positional argument, contextValue
, for the dataSources
property.
reviews: ({ id }, _, { dataSources }) => {// TODO},
With our listing id
in hand, we can look up all of the reviews in our database that are associated with that listing. The reviewsDb
class instance provides a method to look up reviews by a specific listing ID, called getReviewsByListing
. Let's call this method, passing in the id
property we got from our parent
argument.
reviews: ({ id }, _, { dataSources }) => {return dataSources.reviewsDb.getReviewsByListing(id);},
The overallRating
method
Onto the overallRating
resolver!
Try this one out on your own. (Hint: check out the datasources/reviews.ts
file for a helpful method!)
When you're ready, compare your code to our finished resolver function below.
overallRating: ({ id }, _, { dataSources }) => {return dataSources.reviewsDb.getOverallRatingForListing(id);},
Running our dream query
With the rover dev
process still running, let's try out our query at http://localhost:4002
.
query GetListingAndReviews {listing(id: "listing-1") {titledescriptionnumOfBedsamenities {namecategory}overallRatingreviews {idtext}}}
Submit the query, and... we've got data! 🎉 We've associated reviews with a listing and made our dream query come to life!
Reviewing the query plan
Let's check out the query plan to see how this data came together. We'll see that first, the router will fetch data from listings
, and then use that data to build out its request to reviews
. The last step is flattening the response from both subgraphs into a single instance of a Listing
type for the listing
field we've queried!
Still seeing reviews: null
? Try restarting your reviews
server! The rover dev
process running our router on port 4002
will automatically refresh.
Practice
We're working in the missions
subgraph: let's contribute a missions
field to the Planet
entity. First, add the Planet.__resolveReference
function to return the entity representation. Next, add the Planet.missions
resolver. Access id
from the parent object, and pass it as an argument to the missionService.getMissionsForPlanet
method, accessible on the server's context.
Key takeaways
- Any subgraph that contributes fields to an entity needs to define a reference resolver method for that entity. This method is called whenever the router needs to access fields of the entity from within another subgraph.
- An entity representation is an object that the router uses to represent a specific instance of an entity. It includes the entity's type and its key field(s).
- The
__resolveReference
function receives an entity representation from the router for a specific GraphQL type. This representation contains all the data a subgraph needs to understand how to populate data for the fields it contributes.
Up next
Awesome! Our listings
and reviews
services are now collaborating on the same Listing
entity. Each subgraph contributes its own fields, and the router launched by our local rover dev
process packages up the response for us. Emphasis on the word local; to get our changes actually "live" (at least in the tutorial sense of the word), we need to tell GraphOS about them!
In the next lesson, we'll take a look at how we can land these changes safely and confidently using schema checks and launches.
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.