Overview
Let's dig a little deeper into our connector calls, how to optimize them, and how to deal with a common error.
In this lesson, we will:
- Learn about entities and how to define them in the schema
- Learn about the satisfiability error
More listing fields
We've got more to add to listings: some important attributes to showcase our destination.
In our schema, we'll add four new fields to our
Listing
type: description, photo, and coordinates for latitude and longitude.listings.graphql"The listing's description"description: String"The thumbnail image for the listing"photoThumbnail: String"Latitude coordinates for the destination"latitude: Float"Longitude coordinates for the destination"longitude: FloatThese should already exist in our REST API. First, let's take a look at the
GET /listings/:id
endpoint: https://airlock-listings.demo-api.apollo.dev/listings/listing-1. We've got all four properties available in the response!{"costPerNight": 120,"title": "Cave campsite in snowy MoundiiX","locationType": "CAMPSITE","description": "Enjoy this amazing cave campsite in snow MoundiiX, where you'll be one with the nature and wildlife in this wintery planet. All space survival amenities are available. We have complementary dehydrated wine upon your arrival. Check in between 34:00 and 72:00. The nearest village is 3AU away, so please plan accordingly. Recommended for extreme outdoor adventurers.","id": "listing-1","numOfBeds": 2,"closedForBookings": false,"photoThumbnail": "https://res.cloudinary.com/apollographql/image/upload/v1644350721/odyssey/federation-course2/illustrations/listings-01.png","hostId": "user-1","isFeatured": true,"latitude": 1023.4,"longitude": -203.4,"amenities": [{"id": "am-2","category": "Accommodation Details","name": "Towel"},{"id": "am-10","category": "Space Survival","name": "Oxygen"}// more amenities]}So we know that we can add these to our selection for the
Query.listing
connector, in a one-to-one mapping.listings.graphqllisting(id: ID!): Listing@connect(source: "listings"http: { GET: "/listings/{$args.id}" }selection: """idtitlenumOfBedscostPerNightclosedForBooking: closedForBookingsdescriptionphotoThumbnaillatitudelongitude""")But what about the other path that returns a Listing type:
Query.featuredListings
? Jumping over to the endpoint for that connector: https://airlock-listings.demo-api.apollo.dev/featured-listingsGET /featured-listings[{"id": "listing-1","title": "Cave campsite in snowy MoundiiX","numOfBeds": 2,"costPerNight": 120,"closedForBookings": false,"amenities": [{"id": "am-2"},{"id": "am-10"}// more amenities]}// more listing objects]It doesn't seem to have any of those fields.
This is a common pattern in REST APIs, where endpoints might not return the same properties. So how can we account for this discrepancy? We'll turn to the power of entities.
About entities
An entity is an object that can be fetched using a unique identifier. You can think of it like a row in a database, where we can retrieve a user by ID, or a product by its UPC.
In a GraphQL API, we typically have multiple, separate data sources that populate different fields of an entity.
For example, listing information like title and description might come from one REST API, but host and guest information for that same listing might come from another. Or, as we saw earlier, one endpoint might only return a subset of listing information, compared to another, from that same API, that returns the full listing object.
Entity schema syntax
To define an entity type in our schema, we use the @key
directive, followed by the fields of its unique identifier.
type Product @key(fields: "upc") {upc: ID!}
An entity type also needs to provide instructions for the router on how to retrieve the data for the entity's fields. These instructions will be in the form of a connector. Specifically, one that is:
- applied to a
Query
field - uses the entity's
key
field as an argument, and - returns the entity type.
In the schema, we can mark that connector with entity: true
.
The Listing
entity
Let's see this in action with the Listing
type.
First, we'll define it as an entity by applying
@key
and choosing theid
field as its unique identifier.schema.graphqltype Listing @key(fields: "id") {id: ID!# ... other Listing fields}Next up, those instructions for fetching data. We have just the connector for that!
Query.listing
takes in a listingid
(which is the entity's@key
field) and returns aListing
type. So we'll mark this connector withentity: true
.schema.graphqllisting(id: ID!): Listing@connect(source: "listings"http: { GET: "/listings/{$args.id}" }selection: """idtitlenumOfBedscostPerNightclosedForBooking: closedForBookingsdescriptionphotoThumbnaillatitudelongitude"""entity: true)
Checking our work
Let's save our changes before switching over to the client side. Back in Sandbox, we'll start with the operation for a single listing and add those new fields to our query.
query GetListingWithAmenities($listingId: ID!) {listing(id: $listingId) {idtitlenumOfBedscostPerNightclosedForBookingdescriptionlatitudelongitudephotoThumbnailamenities {idcategoryname}}}
And the response is looking good!
Let's check out what's happening behind the scenes in the Connectors Debugger.
We've got two calls: one to our GET /listing/:id
endpoint, and another for GET /listing/:id/amenities
.
By the way, you can avoid that extra amenities
call. Check out the section below for more details.
Next, let's look at the Query.featuredListings
path. We can switch over to our first operation, GetFeaturedListings
. We'll add those four new fields, and check out the response.
query FeaturedListings {featuredListings {idtitlenumOfBedscostPerNightcloseddescriptionlatitudelongitudephotoThumbnail}}
Looks like things are working!
Let's see what's happening under the hood.
We can see our original call to the GET /featured-listings
endpoint, then an additional call for each specific listing.
This is our entity in action, with the router and connectors handling the API orchestration for us. The first connector mapping did not include those new fields. So in order to satisfy the rest of the client's query, the router knows it can reach out to the Query.listing
connector, following the instructions marked by entity: true
.
Without entities
One last thing. Let's take a peek into an alternate universe: what would have happened if we didn't create the Listing
as an entity?
Let's remove entity: true
and the @key
fields.
type Query {# ... featuredListings"A specific listing"listing(id: ID!): Listing@connect(source: "listings"http: { GET: "/listings/{$args.id}" }selection: """# ..."""- entity: true)}"A particular intergalactic location available for booking"- type Listing @key(fields: "id") {+ type Listing {}
Save our changes, and... we have SATISFIABILITY
errors.
error[E029]: Encountered 4 build errors while trying to build a supergraph.Caused by:SATISFIABILITY_ERROR: The following supergraph API query:{featuredListings {description}}cannot be satisfied by the subgraphs because:- from subgraph "listings":- cannot find field "Listing.description".- cannot move to subgraph "listings", which has field "Listing.description", because type "Listing" has no @key defined in subgraph "listings".- from subgraph "listings": cannot find field "Listing.description".SATISFIABILITY_ERROR: The following supergraph API query:{featuredListings {latitude}}# ... same errors for longitude and photoThumbnail
Satisfiability errors
Before Rover can mark a schema as valid, it checks if all the paths to a specific field can be satisfied. This ensures that the router can take on any operation the client sends its way.
In our case, the error is pointing to the featuredListings
path in particular, stating that the router won't be able to satisfy client requests that ask for description, photo, and coordinates that come through the featuredListings
entry point. And we already know that particular connector can't retrieve the data for those fields.
But we don't need to worry: the Listing
entity, along with the connector that provides the instructions, can step in to handle the rest of those fields for us.
Let's make sure we undo our changes, and bring that entity back!
type Query {# ... featuredListings"A specific listing"listing(id: ID!): Listing@connect(source: "listings"http: { GET: "/listings/{$args.id}" }selection: """# ..."""+ entity: true)}"A particular intergalactic location available for booking"+ type Listing @key(fields: "id") {}
We've only just scratched the surface of how entities help enable API orchestration. As we grow and add more domains, we'll explore more of what entities have to offer.
Practice
Key takeaways
- An entity is an object that can be fetched using a unique identifier. To define an entity type in the schema, we use the
@key
directive along with the field(s) acting as its unique identifier. - An entity type also needs to provide instructions for the router on how to retrieve the data for the entity's fields. To define the connector with this role, we use
entity: true
. - Satisfiability errors occur when a field in an operation can't be reached through the schema's connectors.
Up next
One last lesson to go. We've tackled queries to retrieve data—now it's time to switch our attention over to manipulating data!
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.