9. Using entities with connectors
5m

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 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 to showcase our destination.

Get listing details mockup

  1. In our schema, we'll add four new 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: Float
  2. These 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
    ]
    }
  3. So we know that we can add these to our selection for the Query.listing connector, in a one-to-one mapping.

    listings.graphql
    listing(id: ID!): Listing
    @connect(
    source: "listings"
    http: { GET: "/listings/{$args.id}" }
    selection: """
    id
    title
    numOfBeds
    costPerNight
    closedForBooking: closedForBookings
    description
    photoThumbnail
    latitude
    longitude
    """
    )
  4. 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-listings

    GET /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 .

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 .

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 API, we typically have multiple, separate data sources that populate different of an .

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 in our schema, we use the @key , followed by the of its unique identifier.

Entity syntax
type Product @key(fields: "upc") {
upc: ID!
}

An also needs to provide instructions for the on how to retrieve the data for the entity's . These instructions will be in the form of a connector. Specifically, one that is:

  • applied to a Query
  • uses the 's key as an , and
  • returns the .

In the schema, we can mark that connector with entity: true.

The Listing entity

Let's see this in action with the Listing type.

  1. First, we'll define it as an by applying @key and choosing the id as its unique identifier.

    schema.graphql
    type Listing @key(fields: "id") {
    id: ID!
    # ... other Listing fields
    }
  2. Next up, those instructions for fetching data. We have just the connector for that! Query.listing takes in a listing id (which is the 's @key ) and returns a Listing type. So we'll mark this connector with entity: true.

    schema.graphql
    listing(id: ID!): Listing
    @connect(
    source: "listings"
    http: { GET: "/listings/{$args.id}" }
    selection: """
    id
    title
    numOfBeds
    costPerNight
    closedForBooking: closedForBookings
    description
    photoThumbnail
    latitude
    longitude
    """
    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 for a single listing and add those new to our .

GetListingWithAmenities operation
query GetListingWithAmenities($listingId: ID!) {
listing(id: $listingId) {
id
title
numOfBeds
costPerNight
closedForBooking
description
latitude
longitude
photoThumbnail
amenities {
id
category
name
}
}
}

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.

http://localhost:4000

Connectors Debugger calls for GetListingWithAmenities

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 , GetFeaturedListings. We'll add those four new , and check out the response.

GetFeaturedListings operation
query FeaturedListings {
featuredListings {
id
title
numOfBeds
costPerNight
closed
description
latitude
longitude
photoThumbnail
}
}

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.

http://localhost:4000

Connectors Debugger calls for GetFeaturedListings

This is our in action, with the and connectors handling the for us. The first connector mapping did not include those new . So in order to satisfy the rest of the client's , 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 ?

Let's remove entity: true and the @key .

listings.graphql
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.

Rover errors (truncated)
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 can mark a schema as valid, it checks if all the paths to a specific can be satisfied. This ensures that the can take on any the client sends its way.

Rover checking each selection against the Listing fields

In our case, the error is pointing to the featuredListings path in particular, stating that the 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 .

Rover checks resulting in satisfiability error

But we don't need to worry: the Listing , along with the connector that provides the instructions, can step in to handle the rest of those for us.

Let's make sure we undo our changes, and bring that back!

listings.graphql
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 help enable . As we grow and add more domains, we'll explore more of what entities have to offer.

Practice

What directive is used to define an entity type in the GraphQL schema?
Which of the following scenarios can cause a satisfiability error?

Key takeaways

  • An is an object that can be fetched using a unique identifier. To define an in the schema, we use the @key along with the (s) acting as its unique identifier.
  • An also needs to provide instructions for the on how to retrieve the data for the entity's . To define the connector with this role, we use entity: true.
  • Satisfiability errors occur when a in an 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!

Previous

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.