6. A look forward
15m

The future of FlyBy

Let's look back at our to-do list of problems the FlyBy team was facing:

  1. We cleaned up some extra syntax that Federation 2 no longer requires.
  2. We eliminated the maintenance burden of two different terrain enums, and captured all possible values for location and activity terrains in one type.
  3. We then included this field in the shared interface Attraction that both of our Location and Activity types implement.

Combined, these small changes have had a big impact on how our team can continue work on different subgraphs while still satisfying larger business needs.

A new feature

Finally, we can look to the future: a new feature! The team wants to introduce Stats as a part of the UI. This new type will provide statistics about the climate and atmosphere for locations and activities.

An activity shown in the UI with its populated stats bar showing: terrain, temperature, gravity and other data.

There will be some shared fields between the two subgraphs (averageTemperature, gravity, and lengthOfDay), as well as some new fields specific to activities (exosuitRequired and groupSize).

Diagram comparing the planned fields for a Stats type

Updating our schema

To introduce this new feature, we have a few changes we'll need to make to our schema.

  1. First, we'll define a Stats type in both of our subgraphs. We've identified the three fields that are applicable to both subgraphs (averageTemperature, gravity, and lengthOfDay), so we'll define Stats as an interface that contains this shared logic.
  2. We know there are a few fields that are applicable only to the activities data, so we'll define an implementing type for this interface in each subgraph. This will allow us to handle these activity-specific fields in our activities subgraph implementing type.
  3. With a new Stats type, we'll revisit whether there are any other fields in our schema that make sense to migrate into this definition.
  4. Finally, we'll define a stats field on our Location and Activity types to make use of these new types!

Let's put together everything we learned about Apollo Federation 2 to get this feature up and running.

The Stats interface

We already know that our Stats type will have some fields in common between the two subgraphs of locations and activities. Let's start by defining this type in both subgraphs as an interface that includes just those shared fields.

interface Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float!
"The length of day, in minutes"
lengthOfDay: Int!
}

Defining the interface

The implementing types

We also learned that an activity will have two additional fields - groupSize and exosuitRequired. We've identified the boundary around the fields that are shared between activities and locations, so let's first create a new type in each subgraph that implements this interface: ActivityStats and LocationStats, respectively.

(The locations implementation won't change, but this will allow us to add the additional fields that the activities stats will be concerned with.)

Defining the implementing types

subgraph-activities/activities.graphql
interface Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float!
"The length of day, in minutes"
lengthOfDay: Int!
}
type ActivityStats implements Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float!
"The length of day, in minutes"
lengthOfDay: Int!
}
subgraph-locations/locations.graphql
interface Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float!
"The length of day, in minutes"
lengthOfDay: Int!
}
type LocationStats implements Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float!
"The length of day, in minutes"
lengthOfDay: Int!
}

New activity stats

Next, let's take care of the two fields that we want to include in the ActivityStats type. These won't apply to locations data at all, so that's why we'll keep them out of the Stats interface our type implements.

Let's add exosuitRequired as a non-nullable Boolean, and groupSize as a non-nullable Int.

Adding new activities stats

subgraph-activities/activities.graphql
type ActivityStats implements Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float!
"The length of day, in minutes"
lengthOfDay: Int!
"Whether or not an exosuit is required for this activity"
exosuitRequired: Boolean!
"How many people this activity can accommodate at once"
groupSize: Int!
}

If we take a look at some of our activities data in activities_data.json, we'll see that some of them do not include a value for gravity. Let's keep our schema as accurate as possible and modify the gravity field on both types in this subgraph (Stats and ActivityStats) to reflect its nullability.

Task!

subgraph-activities/activities.graphql
interface Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float
"The length of day, in minutes"
lengthOfDay: Int!
}
type ActivityStats implements Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float
"The length of day, in minutes"
lengthOfDay: Int!
"Whether or not an exosuit is required for this activity"
exosuitRequired: Boolean!
"How many people this activity can accommodate at once"
groupSize: Int!
}

We'll note that our data file does not include a field for exosuitRequired in our activity objects. This is a value that we'll calculate in a resolver function based on other factors involved in an activity.

Let's add a new entry for ActivityStats to our resolvers map in subgraph-activities/resolvers.js, and define a function called exosuitRequired. This function will take into consideration an activity's averageTemperature and gravity and assess whether an exosuit will be required for participation.

subgraph-activities/resolvers.js
{
// ...
ActivityStats: {
exosuitRequired: ({averageTemperature, gravity}) => {
const hasExtremeTemp = averageTemperature < 0 || averageTemperature > 50;
const hasExtremeGravity = gravity < 4 || gravity > 9;
// an exosuit is only required if the activity has extreme temperatures or extreme gravity
return hasExtremeTemp || hasExtremeGravity;
},
}
// ...
}

Task!

Great! Both types have now been updated to return information about the climate, temperature and other requirements.

Migrating the minimumAge field

We have one more piece of shared data that can be relocated to our Stats type. In an earlier lesson, we defined the minimumAge field as part of the Attraction interface. This field feels more related to our other activity and location characteristics, so let's include it in the Stats interface instead.

Migrating the minimumAge field: activities

subgraph-activities/activities.graphql
interface Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float
"The length of day, in minutes"
lengthOfDay: Int!
+ "The minimum age in years for participation on this attraction"
+ minimumAge: Int!
}
type ActivityStats implements Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float
"The length of day, in minutes"
lengthOfDay: Int!
+ "The minimum age in years for participation on this attraction"
+ minimumAge: Int!
"Whether or not an exosuit is required for this activity"
exosuitRequired: Boolean!
"How many people this activity can accommodate at once"
groupSize: Int!
}
interface Attraction {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
- "The minimum age in years for participation on this attraction"
- minimumAge: Int!
"The attraction's terrain"
terrain: Terrain
}
type Activity implements Attraction @key(fields: "id") {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
- "The minimum age in years for participation on this attraction"
- minimumAge: Int!
"The attraction's terrain"
terrain: Terrain!
"The activity's location"
location: Location
}

Migrating the minimumAge field: locations

subgraph-locations/locations.graphql
interface Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float!
"The length of day, in minutes"
lengthOfDay: Int!
+ "The minimum age in years for participation on this attraction"
+ minimumAge: Int
}
type LocationStats implements Stats {
"The average temperature, in Fahrenheit"
averageTemperature: Int!
"The strength of the gravity field, in g units"
gravity: Float!
"The length of day, in minutes"
lengthOfDay: Int!
+ "The minimum age in years for participation on this attraction"
+ minimumAge: Int
}
interface Attraction {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
- "The minimum age in years for participation on this attraction"
- minimumAge: Int
"The attraction's terrain"
terrain: Terrain
}
type Location implements Attraction @key(fields: "id") {
id: ID!
"The name of the location"
name: String!
"A short description about the location"
description: String!
"The location's main photo as a URL"
photo: String!
- "The minimum age in years for participation on this attraction"
- minimumAge: Int
"The terrain for an attraction"
terrain: Terrain!
}

Note: We shuffled things about quite a bit! Make sure that you've kept the minimumAge field nullable in locations.graphql and non-nullable in activities.graphql, so that our schema continues to adhere to business rules! Even though we've moved the field, its difference in nullability between subgraphs is still allowed as one of the variations in a shared type.

Adding the stats field

Now, let's make sure both subgraphs are hooked up to use these new types. For both the Activity and Location types, let's define a new stats field to return our implementing types.

Defining a new stats field

subgraph-activities/activities.graphql
type Activity implements Attraction @key(fields: "id") {
id: ID!
"The name of the attraction"
name: String!
"A short description about the attraction"
description: String!
"The attraction's main photo as a URL"
photo: String!
"The attraction's terrain"
terrain: Terrain!
"The activity's location"
location: Location
+ "The activity's stats"
+ stats: ActivityStats!
}
subgraph-locations/locations.graphql
type Location implements Attraction @key(fields: "id") {
id: ID!
"The name of the location"
name: String!
"A short description about the location"
description: String!
"The location's main photo as a URL"
photo: String!
"The terrain for an attraction"
terrain: Terrain!
+ "The location's stats"
+ stats: LocationStats!
}

To make sure our stats data returns for both the Activity and Location types, we'll need to define corresponding resolver functions. This is because if we take a look at either one of our data sources, we'll see that there's no explicit property for stats on our activities or locations data!

// An activity object, without an explicit 'stats' field
{
"id": "act-3",
"name": "Acid Lake Diving",
"description": "There's not much to see on Krypton, but one thing you can do is take a tingly plunge into any one of the acid crater lakes sprinkled about the planet! It'll singe away any boredom you're feeling!",
"locationId": "loc-4",
"photo": "https://res.cloudinary.com/apollographql/image/upload/v1646347751/odyssey/federation-course1/FlyBy%20illustrations/acid-lake-diving_oibyo2.png",
"terrain": "AQUATIC",
"minimumAge": 21,
"gravity": 10.4,
"averageTemperature": 4,
"groupSize": 5,
"lengthOfDay": 3
}

Because we can't simply pluck a stats property from each object, we'll define a stats resolver in each subgraph that can locate the correct Activity or Location object and pull each queried stats field.

Adding the stats resolvers

subgraph-activities/resolvers.js
Activity: {
__resolveReference: ({id}, { dataSources }) => {
return dataSources.activitiesAPI.getActivity(id)
},
location: ({ locationId }) => ({id: locationId}),
stats: ({ id }, _, { dataSources}) => {
return dataSources.activitiesAPI.getActivity(id)
}
},
subgraph-locations/resolvers.js
Location: {
__resolveReference: ({id}, {dataSources}) => {
return dataSources.locationsAPI.getLocation(id);
},
stats: ({ id }, _, { dataSources }) => {
return dataSources.locationsAPI.getLocation(id)
}
},

First, publish the changes that we've made to the activities and locations subgraphs.

Publishing subgraph updates

Let's try out a query that returns all statistics for both activities and locations!

query GetLocationAndActivityStats {
activities {
stats {
averageTemperature
gravity
lengthOfDay
minimumAge
groupSize
exosuitRequired
}
}
locations {
stats {
averageTemperature
gravity
lengthOfDay
minimumAge
}
}
}

With that, we should see statistics for all of our activities and locations returned in Explorer.

https://studio.apollographql.com
The stats query for activities and locations, along with the returned data.

We've almost completed our implementation of the new Stats feature. In the next and final section, we'll incorporate these new fields into our queries from the frontend and render data in the UI.

Previous
Next