7. Contributing to an entity
15m

Overview

It's time to witness the true strength of federation; we're about to join the pieces and make our queries more powerful and dynamic than ever before.

In this lesson, we will:

  • Learn what an type is and where to find entities in our using Studio
  • Contribute to an existing defined in another

Recipes and soundtracks

Remember our dream ?

query GetRecipeAndRecommendedSoundtracks {
randomRecipe {
id
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
tracks {
id
name
explicit
durationMs
}
}
}
}

We envision generating a random recipe and immediately getting recommendations for the perfect soundtrack to pair with it. We'll call this : recommendedPlaylists.

The recommendedPlaylists needs to live in the soundtracks , because it's related to music data, but it belongs to the Recipe type.

The big problem with that? The soundtracks has no idea what the Recipe type is! Let's fix that—with entities.

What's an entity?

An entity is an with split between multiple . When working with an , each subgraph can do one or both of the following:

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

Contributing vs. referencing

To differentiate between that contribute to an , and those that reference an , think of it this way: a that contributes is actually adding new data capabilities from its own domain to the type.

This is in contrast to a that merely references the ; it's essentially just "mentioning" the existence of the entity, and providing it as the return type for another field.

In federation, we use entities to create cohesive types that aren't confined to just one or another; instead, they can span our entire API!

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

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

studio.apollographql.com

The Schema page, listing Object types with the Recipe entity highlighted

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

To create an , a needs to provide two things: a primary key and a reference resolver.

Defining a primary key

An 's primary key is the (or fields) that can uniquely identify an instance of that within a . The uses primary keys to collect data from across multiple subgraphs and associate it with a single entity instance. It's how we know that each subgraph is talking about—and providing data for—the same object!

For example, a recipe 's primary key is its id . This means that the can use a particular recipe's id to gather its data from multiple .

Illustration showing three entities with unique ids

We use the @key , along with a property called fields to set the we want to use as the 's primary key.

Entity syntax
type EntityType @key(fields: "id")

Reference resolvers and entity representations

Because an 's can be divided across , each subgraph that provides needs a way to identify the instance the is gathering data for. When it can identify this instance, the can provide additional data. Identifying a particular instance of an takes the form of a method called a reference resolver.

This method receives a basic object called an representation. An entity representation is an object that the uses to identify a particular instance. It always includes the typename for the type, along with the @key and its corresponding value.

Note: The __typename exists on all types automatically. It always returns the name of the type, as a string. For example, Recipe.__typename returns "Recipe".

An representation for a recipe might look like this:

Example recipe entity representation
{
"__typename": "Recipe",
"id": "rec3j49yFpY2uRNM1"
}

You can think of the representation as the minimum basic information the needs to associate data from multiple , and ensure that each subgraph is talking about the same object.

As we'll see shortly, the representation for a particular recipe will give our soundtracks all the information it needs (nothing more, nothing less!) to contribute some recommended playlists.

Contributing fields to an entity

The Recipe type is an ; this means that even though it's originally defined in the recipes , we can use it—and build onto it—in our soundtracks .

To add to the Recipe , we need it to exist in our soundtracks .

Open up your code editor, and navigate to the src/schema.graphql file. We'll start by adding a basic stub of the Recipe type: this consists of the type name, the @key , and the assigned as its primary key, id.

schema.graphql
type Recipe @key(fields: "id") {
id: ID!
}

The @key is federation-specific, so let's also make sure to import it in our federation definition at the top of the file.

extend schema @link(url: "https://specs.apollo.dev/federation/v2.5", import: ["@key"])

Notice that we don't need to include any other from the Recipe as defined in the recipes . We can keep our soundtracks clean and concerned only with the data it requires to do its job.

Next, let's add that recommendedPlaylists to the Recipe type. We'll give it a return type of [Playlist!]!.

schema.graphql
type Recipe @key(fields: "id") {
id: ID!
"A list of recommended playlists for this particular recipe. Returns 1 to 3 playlists."
recommendedPlaylists: [Playlist!]!
}

Adding a reference resolver

Let's open our resolvers.ts file. We need to provide our soundtracks with a reference resolver: this is the function that will receive an representation from the that represents a Recipe object.

We'll start by adding a new entry to our resolvers object, called Recipe.

resolvers.ts
Recipe: {
// TODO
}

We define each 's reference right alongside all the resolvers for that type, so this is right where it belongs. Every reference resolver has the name: __resolveReference.

resolvers.ts
Recipe: {
__resolveReference: () => {};
}

The __resolveReference function has a slightly different signature from other functions. Instead of the usual four , __resolveReference only takes three:

  • reference: The representation object that's passed in by the . This tells the which instance of an entity is being requested; it will include the instance's __typename (Recipe) along with its primary key, id.
  • context: The object shared across all . (This is the same as in normal resolvers, but note that by convention, we refer to this __resolveReference as context, rather than contextValue as in other !)
  • info: Contains information about the 's execution state, just like in a normal . We won't use this here.

Let's update our __resolveReference function to log out, and then return, its first parameter, reference.

resolvers.ts
Recipe: {
__resolveReference: (reference) => {
console.log(reference);
return reference;
},
}

To resolve the TypeScript error that has just emerged from adding this function, we can modify our codegen.ts file slightly. We'll pass in a new federation property to our config property, and set it to true. This automatically enables some federation-specific settings, such as using __resolveReference functions.

codegen.ts
config: {
contextType: "./context#DataSourceContext",
federation: true,
mappers: {
// mappers
}
}

Adding the RecipeModel type

There's one last little issue with our Recipe.__resolveReference (you might be seeing a red squiggly error yourself!) TypeScript expects the object that arrives (reference) to match the shape that we gave the Recipe type in our schema: this means it should have an id and a recommendedPlaylists .

schema.graphql
type Recipe @key(fields: "id") {
id: ID!
"A list of recommended playlists for this particular recipe. Returns 1 to 3 playlists."
recommendedPlaylists: [Playlist!]!
}

Of course, that's just what we intend our forthcoming Recipe.recommendedPlaylists to do—but this doesn't resolve the TypeScript error. Instead, we need to define a new model to represent the shape of the incoming reference object more accurately, and tell TypeScript to use it when referring to a Recipe object.

Back in models.ts, we'll add a new entry. We'll call it RecipeModel, and give it just one : id. (We don't need to explicitly pass it a __typename , as this is added automatically to all types.)

models.ts
export type RecipeModel = {
id: string;
};

Last step: back in codegen.ts, we'll add our model to our mappers.

codegen.ts
mappers: {
Playlist: "./models#PlaylistModel",
Track: "./models#TrackModel",
AddItemsToPlaylistPayload: "./models#AddItemsToPlaylistPayloadModel",
Recipe: "./models#Recipe"
},

Great! That's our type errors taken care of. Let's restart the server to start fresh.

npm run dev

Reviewing the entity representation

Returning back to our rover dev process on http://localhost:4001, let's execute our dream and see the representation that gets logged out in our terminal.

query GetRecipeAndRecommendedSoundtracks {
randomRecipe {
id
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
tracks {
id
name
explicit
durationMs
}
}
}
}

We'll see that the representation comes through exactly as we expected—with both the __typename and the id ! (Note that because it's a random recipe we're getting back, your value for id might be different!)

{ __typename: 'Recipe', id: 'rec3j49yFpY2uRNM1' }

So what is this __referenceResolver actually meant to do for us in the soundtracks ?

We can think of it as the first stop for any representation sent by the . When our soundtracks has been asked to provide a for one of these instances (such as recommendedPlaylists), it first receives the representation and decides what to do next.

In most cases, when a receives an representation, it will use the primary key—in this case, id—to correlate it with data it has access to.

In this case, our __resolveReference doesn't need to match data from the recipes with data from soundtracks: we can receive the representation and return it directly. Whatever the __resolveReference function returns is passed through the chain to the next resolver; so, what we return here will arrive in recommendedPlaylists, as its parent .

Let's update our __resolveReference function so that it simply receives the representation and returns it.

resolvers.ts
Recipe: {
__resolveReference: (reference) => reference;
}

Now we can move onto the recommendedPlaylists . We'll add a new entry for it, just below __resolveReference.

Recipe: {
__resolveReference: (reference) => reference,
recommendedPlaylists: (parent, args, contextValue, info) => {
// TODO
}
}

For now, let's just make sure that our method is working by creating a couple of mock playlists.

recommendedPlaylists: (parent, args, contextValue, info) => {
const playlists = [
{
id: "1",
name: "Rock n' Roll",
description: "A rock n' roll playlist",
tracks: {
items: [
{
track: {
id: "6",
name: "Rockin' out",
duration_ms: 13434,
explicit: false,
uri: 'uri-string'
}
}
]
},
},
{
id: "2",
name: "Pop",
description: "A pop playlist",
tracks: {
items: [
{
track: {
id: "7",
name: "Pop it up",
duration_ms: 13433,
explicit: false,
uri: 'uri-string'
}
}
]
},
},
];
return playlists;
},

The query plan

Our running rover dev process will pick up the changes that we've made, recompose our , and restart the local . So let's try out our at http://localhost:4001.

query GetRecipeAndRecommendedSoundtracks {
randomRecipe {
id
name
description
ingredients {
text
}
instructions
recommendedPlaylists {
id
name
description
tracks {
id
name
explicit
durationMs
}
}
}
}

Let's check out the , making sure to view it as text.

We'll see that first the fetches data from recipes, and then uses that data to build out its request to soundtracks. The last step is flattening the response from both into a single instance of a Recipe type for the randomRecipe we've queried!

http://localhost:4001

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

Let's try it out, and... we've got data!

Practice

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

Key takeaways

  • An is a type that can resolve its across multiple .
  • To create an , we can use the @key to specify which (s) can uniquely identify an object of that type.
  • We can use entities in two ways:
    • As a return type for a (referencing an ).
    • Defining for an from multiple (contributing to an entity).
  • Any that contributes to an needs to define a reference function for that entity. This function 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).

Up next

Awesome! Our recipes and soundtracks services are now collaborating on the same Recipe . Each contributes its own , and the packages up the response for us.

However, our musical recommendations are still pretty weak. In fact, they're not much like recommendations at all—they're hardcoded playlist objects! We haven't actually used any recipe-specific data to determine the playlists we recommend to our potential chef users. In the next lesson, we'll dive deeper into federation-specific and fortify our soundtrack suggestions.

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.