Referencing Ancestors
One of the common requests that I see in GraphQL is users wanting a way to query an ancestor object from a descendant object. Throughout this article, I'm going to use an example based on the one given in GraphQL spec issue #144; here's the schema under discussion:
type Query {
events: [Event]
}
type Event {
id: ID!
name: String
speakers: [Speaker]
talks: [Talk]
}
type Speaker {
id: ID!
name: String
events: [Event]
talks: [Talk]
}
type Talk {
id: ID!
name: String
speaker: Speaker
event: Event
}
Consider this query:
query {
events {
id
name
speakers {
id
name
talks {
id
name
}
}
}
}
The issue author would like to make it such that >events>speakers>talks
(using
operation expressions
syntax) returns the list of talks that the speaker has in the ancestor event.
At first glance, this might seem like a reasonable proposition... However, when
considering graph traversal it becomes apparent why this
would be problematic — the value of a Speaker
's talks
field should be
independent of the path through which it was accessed. In particular,
normalized stores rely on the graph traversal execution
behaviour outlined in the GraphQL specification; to change this fundamental
behaviour could cause applications using normalized stores to produce subtle
issues resulting in a lot of confusion and frustration.
Breaks normalized stores​
Let's see why normalized stores would be broken by this. If you implement the described change, you might get a result from the request above such as:
{
"events": [
{
"id": "EGQLC",
"name": "GraphQLConf",
"speakers": [
{
"id": "UB3NJ",
"name": "Benjie",
"talks": [
{
"id": "TGFST",
"name": "The Future of Efficiency Is Here: Schema Planning"
}
]
}
]
},
{
"id": "EVRGU",
"name": "VR Gamers",
"speakers": [
{
"id": "UB3NJ",
"name": "Benjie",
"talks": [
{
"id": "TPOPC",
"name": "Mastering Climbing in Population:One"
}
]
}
]
}
]
}
(I've omitted all other speakers and all other events for brevity.)
When you turn this into a normalized store, both events
reference the speaker UB3NJ
, who has a field talks
that accepts no
arguments; so when you merge these, the latter will overwrite the former, and
the resulting store will end up being something like:
{
"Query": {
"events": [ { "$ref": "EGQLC" }, { "$ref": "EVRGU" } ]
}
"EGQLC": {
"id": "EGQLC",
"name": "GraphQLConf",
"speakers": [ { "$ref": "UB3NJ" } ]
},
"EVRGU": {
"id": "EVRGU",
"name": "VR Gamers",
"speakers": [ { "$ref": "UB3NJ" } ]
},
"UB3NJ": {
"id": "UB3NJ",
"name": "Benjie",
/* This gets overwritten:
"talks": [
{
"id": "TGFST",
"name": "The Future of Efficiency Is Here: Schema Planning"
}
] */
"talks": [
{
"id": "TPOPC",
"name": "Mastering Climbing in Population:One"
}
]
}
}
When you reconstitute this back into data to render you'll end up with Benjie's talk for the "VR Gamers" event being used for GraphQLConf... A subtle issue when testing the software (very hard to notice) but super obvious to the attendees of GraphQLConf who wonder how relevant "Mastering Climbing in Population:One" is to GraphQL!
Solution: rewrite the query​
One solution to this problem is to write your query in such a way that each field can execute in a context-free way and still give you the data you need:
query {
events {
id
name
talks {
id
name
speakers {
id
name
}
}
}
}
Solution: add another field​
Another solution is to perform the grouping on the server side and expose this via the schema:
extend type Event {
speakersAndTalks: [SpeakerAndTalks]
}
type SpeakerAndTalks {
speaker: Speaker
talks: [Talk]
}
which you could query like this:
query {
events {
id
name
speakersAndTalks {
speaker {
id
name
}
talks {
id
name
}
}
}
}
Thanks​
Thank you to fellow TSC member Matt Mahoney for proof-reading this article. Matt comments that this can also occur in:
REST-based resolvers, where the
events.speaker
field is backed by a REST endpoint, which always gives youSpeaker.talks
in a context-dependent way.