Skip to main content

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 you Speaker.talks in a context-dependent way.