8. Contributing fields to an entity
15m

Overview

Even though we've defined the Listing in our reviews and contributed to it, we haven't told it how to resolve these fields.

In this lesson, we will:

  • Create a reference
  • Populate the reviews and overallRating with data

The reference resolver

When we for a particular listing and its review data, we need a way to tell the reviews which listing we're talking about. Then the reviews will understand what data to provide!

To do exactly this, the passes along the representation we talked about in the last lesson: the minimum information required for the reviews to put together some idea of which listing it's fetching data for.

Example Listing entity representation
{
"__typename": "Listing",
"id": "listing-3"
}

But where in the reviews does this representation actually go?

This is just the piece we're missing: the reference . The reference resolver is the special method that can receive an representation from the , 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 , we need to tell our server what to do when it receives an representation for a particular listing. We'll do this by defining a special function, called __resolveReference, under a new entry in our map for Listing.

Open up reviews/src/resolvers.ts. Let's add the Listing key and the reference as shown below.

reviews/src/resolvers.ts
// Query, Mutation entries above
Listing: {
__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 : such as resolving representations received from the !

codegen.ts
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 representation in our __resolveReference function as a parameter called representation. Then, we'll log it out, and return it.

reviews/src/resolvers.ts
__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 representation from the ) 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 .

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 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 representation. To do this, we'll use a model.

Introducing models

The defines how 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 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.

A diagram showing data with different shapes entering a resolver, and data that conforms to the GraphqL schema leaving the resolver

To maintain type-safety, we need to clarify how our types of data differ between what resolvers receive (from , or the , as with our representation) and what resolvers return to clients.

In our case, our __resolveReference thinks that it's going to receive an object of data that looks like our Listing 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 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.

models.ts
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 type. This means that when codegen sees us working with a particular GraphQL object in our , it'll use the model that we provide as the reference for which properties the object should have.

codegen.ts
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 again.

query GetListingAndReviews {
listing(id: "listing-1") {
title
description
numOfBeds
amenities {
name
category
}
overallRating
reviews {
id
text
}
}
}

When we run the , we still won't get any review-related data back; but when we check the terminal of our reviews , we'll see that our representation has arrived and is printed out!

{__typename=Listing, id=listing-1}

Our reviews is successfully receiving the listing representation from the ; this means we know which listing to provide data for. Now, we just need to define the for the that the reviews 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 for the Listing.reviews .

reviews/src/resolvers.ts
Listing: {
__resolveReference: (representation) => {
return representation;
},
reviews: () => {}
}

This method should use the id from the representation to find and return all the relevant reviews in the database. Because this resolves a on an entity type, we know that the previous resolver in the chain was the __resolveReference we just defined. This means we can access its return value (the Listing instance) using parent, the first positional each receives.

Let's destructure parent for the id property.

reviews/src/resolvers.ts
reviews: ({ id }) => {
// TODO
},

Our 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 's third positional , contextValue, for the dataSources property.

reviews/src/resolvers.ts
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 .

reviews/src/resolvers.ts
reviews: ({ id }, _, { dataSources }) => {
return dataSources.reviewsDb.getReviewsByListing(id);
},

The overallRating method

Onto the overallRating !

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 function below.

The overallRating resolver
overallRating: ({ id }, _, { dataSources }) => {
return dataSources.reviewsDb.getOverallRatingForListing(id);
},

Running our dream query

With the rover dev process still running, let's try out our at http://localhost:4002.

query GetListingAndReviews {
listing(id: "listing-1") {
title
description
numOfBeds
amenities {
name
category
}
overallRating
reviews {
id
text
}
}
}

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 to see how this data came together. We'll see that first, the 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 into a single instance of a Listing type for the listing we've queried!

http://localhost:4002

The operation in Sandbox, with the Query Plan Preview opened, showing a linear path to retrieve data from both subgraphs

Still seeing reviews: null? Try restarting your reviews server! The rover dev process running our on port 4002 will automatically refresh.

Practice

Where should an entity's reference resolver method be defined?

Key takeaways

  • Any that contributes to an needs to define a reference method for that entity. This method is called whenever the needs to access fields of the entity from within another subgraph.
  • An representation is an object that the uses to represent a specific instance of an entity. It includes the entity's type and its key (s).
  • The __resolveReference function receives an representation from the for a specific type. This representation contains all the data a needs to understand how to populate data for the it contributes.

Up next

Awesome! Our listings and reviews services are now collaborating on the same Listing . Each contributes its own , and the 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 about them!

In the next lesson, we'll take a look at how we can land these changes safely and confidently using and .

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.