5. Referencing an entity
5m

Overview

Remember our dream query?

query GetRecipeAndCookwareInformation {
recipe(id: "rec3j49yFpY2uRNM1") {
name
description
ingredients {
text
}
instructions
cookware {
name
description
cleaningInstructions
}
}
}

This query requires a change in the recipes subgraph : adding a cookware to the Recipe type, that returns a list of Cookware types. The recipes subgraph has information available about the names of cookware a recipes uses, but the doesn't allow us to access it.

The recipes subgraph also has no idea what the Cookware type is! Let's fix that.

In this lesson, we will:

  • Learn what an entity type is and where to find entities in our supergraph using Studio
  • Reference an existing entity defined in another subgraph

What's an entity?

An entity is an with s split between multiple subgraphs.

We already have an entity in our Poetic Plates . Let's head over to Studio to our page and navigate to the Schema page.

Select Objects from the left-hand list. See that green E label beside the Cookware type? E stands for entity!

https://studio.apollographql.com/

Schema reference page for Objects in Studio

Note: We saw that same label in , in the Documentation panel, beside any that returned the Cookware type.

Because it was defined in the kitchenware subgraph, which we didn't clone or run locally, we didn't really see what was happening behind the scenes in the file. But that's okay, we actually didn't need to: Studio helped us identify what entity types were available to us!

You can still find the schema file here, if you're interested.

With entities, each subgraph can do one or both of the following:

  • Contribute different fields to the entity
  • Reference an entity, which means using it as a return type for another field defined in the subgraph

In this course, we'll focus only on learning how to reference an entity. If you're curious to dive deeper into entities and more federation-specific concepts, you can check out the Voyage series.

Referencing an entity

The Cookware type is an entity; we know that we can reference an entity, which means using it as a return type for another . That's exactly what we want to do with the Recipe.cookware ! Let's try it out.

First, make sure both rover dev processes are still running, as well as the recipes subgraph.

Next, let's open up the schema.graphql file in our recipes subgraph.

Scroll down to find the Recipe type and add the new at the end.

schema.graphql
type Recipe {
# ... other Recipe fields
"List of cookware used in the recipe"
cookware: [Cookware]
}

Save your changes! The first rover dev process is listening for changes to that schema.graphql file; when it detects changes, it will re-compose the local . Let's check out the main rover dev process... oh no! An error!

โœจ change detected in ./schema.graphql...
๐Ÿ”ƒ updating the schema for the 'recipes' subgraph in the session
๐ŸŽถ composing supergraph with Federation v2.3.1
๐Ÿ’€ composition failed, killing the router
error[E029]: Encountered 1 build error while trying to build a supergraph.
Caused by:
UNKNOWN: [recipes] Unknown type Cookware
The subgraph schemas you provided are incompatible with each other. See https://www.apollographql.com/docs/federation/errors/ for more information on resolving build errors.

We've got a build error "Unknown type Cookware".

That makes sense! We haven't given the recipes subgraph any definition for Cookware, so it doesn't understand what we're referring to yet. Let's go ahead and fix that.

In the same file, just below the Recipe type, add the Cookware type. Specifically, we're going to add a stub of the Cookware entity, which contains the minimum s the recipes subgraph needs to reference it.

To define the stub, we'll add the @key after the type definition. This is used to define the entity's primary key, which uniquely identifies an instance of an entity. (Giving us a way to tell an object representing a โ€œcast iron skilletโ€ apart from one representing a โ€œwokโ€, for example!)

schema.graphql
type Cookware @key {
}

We can find the Cookware type's primary key using Studio. Back in the Schema page, with the Objects list selected, click the Cookware type to see its details. We'll see the name listed under the Keys section.

Let's jump back to the schema.graphql file. The @key needs a fields property, which is set to its primary key (or keys!). In this case, we'll set the fields property to name. Additionally, we'll add the name inside the type definition, which returns a non-nullable String.

schema.graphql
type Cookware @key(fields: "name") {
name: String!
}

Lastly, since the subgraph doesn't contribute any additional s to the entity (we're only referencing the entity using its name , not contributing s to it!), we need to add one more thing. Inside the @key , add another for resolvable and set it to false.

schema.graphql
type Cookware @key(fields: "name", resolvable: false) {
name: String!
}

This indicates that the recipes subgraph doesn't define a reference for the Cookware entity. Again, if you're curious to dive deeper into entities and more federation-specific concepts, you can check out the Voyage series!

Let's save our changes and check out the rover dev output again.

โœจ change detected in ./schema.graphql...
๐Ÿ”ƒ updating the schema for the 'recipes' subgraph in the session
๐ŸŽถ composing supergraph with Federation v2.3.1
โœ… successfully composed after updating the 'recipes' subgraph

All is well! Our changes have successfully been composed with the other subgraph and the is working smoothly.

So... we're all done right? ๐Ÿค” Let's jump over back to the Sandbox tab which is connected to our locally-running router on http://localhost:3000 and try to run the dream query.

query GetRecipeAndCookwareInformation {
recipe(id: "rec3j49yFpY2uRNM1") {
name
description
ingredients {
text
}
instructions
cookware {
name
description
cleaningInstructions
}
}
}

We've got the recipe's details... but scrolling down to the cookware portion of our data... we're seeing null!

Now, it's possible there's just no available information about cast iron skillets. Let's try querying for it directly using the cookware .

query GetCookware {
cookware(name: "cast iron skillet") {
name
description
cleaningInstructions
}
}

Looks like we get data back! So... what's the problem?

Resolving the cookware field

Our query resolves data for a specific recipe, but when it gets to the Recipe type's cookware and tries to resolve it... it has no idea what to do! That's because we haven't written the function for the Recipe.cookware ! Without a function, the recipes subgraph doesn't know what data to return โ€“ or even where to find it โ€“ when we query for a particular recipe's cookware. Let's fix that!

In the src/resolvers/Recipe.js file, add the following function for the cookware .

src/resolvers/Recipe.js
cookware(recipe, _, { dataSources }) {
const cookwareNamesList = dataSources.recipesAPI.getRecipeCookware(recipe.id);
if (!cookwareNamesList) return;
return cookwareNamesList.map((c) => ({
name: c,
}));
},

What's happening here? Let's break it down.

First, the parameters of the . We're using the first parameter parent (renamed as recipe) and the third parameter contextValue (destructured to access dataSources).

Next, inside the body of the function, we're using the dataSource method getRecipeCookware to retrieve a list of the recipe's cookware. This method was already implemented for us.

In the event that no cookware list was available for that recipe (it's possible that it just doesn't use any, or it is missing that data!), we'll return early.

Otherwise, we'll map over the list and for each item, return an object. This object is called an entity representation. It's what the uses to represent a specific instance of an entity. A representation always includes the typename for that entity and the @key for the specific instance.

  • The __typename field: This field exists on all GraphQL types automatically. It always returns the name of its containing type, as a string. For example, Cookware.__typename returns "Cookware".
  • The @key field: defines the entity's primary key, which uniquely identifies an instance of an entity. We know that the Cookware type's primary key is its name field.

So, going back to the last line in the Recipe.cookware :

src/resolvers/Recipe.js
return cookwareNamesList.map((c) => ({
name: c,
}));

In this case, we've omitted the __typename because GraphQL takes care of returning that automatically. The cookwareNamesList only has a list of Strings, so we can't simply return that list, as-is. The map function takes care of transforming each item in the list into the shape the is expecting.

Testing with the local router

Alright, we've got the code changes to back our changes! Let's jump back over to Sandbox to check on our local . We should be getting the full data back for our dream query.

query GetRecipeAndCookwareInformation {
recipe(id: "rec3j49yFpY2uRNM1") {
name
description
ingredients {
text
}
instructions
cookware {
name
description
cleaningInstructions
}
}
}

Fantastic, we've got it!

Practice

Use the incomplete below to answer the next question.

type Fruit {
id: ID!
name: String
soldBy: Vendor
}
??? {
vendorId: ID!
}
Which of the following code snippets should be on line 7 to replace the question marks?

Key takeaways

  • An entity is an object type with fields split between multiple subgraphs. A subgraph can contribute fields to an entity or simply reference it.
  • Referencing an entity means using it as a return type for a field defined in the subgraph. To reference an entity, the subgraph must include a stub of the entity definition containing the entity's @key, which includes the entity's primary field(s) and the resolvable property set to false.
  • Additions to the schema may compose successfully, but don't forget to add all code necessary to resolve the new fields!

Up next

Everything is working great locally! Let's push these changes to and get our production up to date.

Previous
Next

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.