Motivation
Support union
types in GraphQL as specified in the GraphQL spec.
User Impact
Users will now be able to able to use unions with our GraphQL. It will enhance the user experience.
Implementation
Introduction
As per GraphQL spec:
GraphQL Unions represent an object that could be one of a list of GraphQL Object types, but provides for no guaranteed fields between those types. So no fields may be queried on this type without the use of type refining fragments or inline fragments.
Unions are never valid inputs
Union types have the potential to be invalid if incorrectly defined.
- A Union type must include one or more unique member types.
- The member types of a Union type must all be Object base types; Scalar, Interface and Union types must not be member types of a Union. Similarly, wrapping types must not be member types of a Union.
So, we will allow defining a schema like following with union:
enum Category {
Fish
Amphibian
Reptile
Bird
Mammal
InVertebrate
}
interface Animal {
id: ID!
category: Category @search
}
type Dog implements Animal {
breed: String @search
}
type Parrot implements Animal {
repeatsWords: [String]
}
type Cheetah implements Animal {
speed: Float
}
type Human {
name: String!
pets: [Animal!]!
}
union HomeMember = Dog | Parrot | Human
type Zoo {
id: ID!
animals: [Animal]
city: String
}
type Home {
id: ID!
address: String
members: [HomeMember]
}
Example
So, when you want to query members in a home, you will be able to do a GraphQL query like this:
query {
queryHome {
address
members {
... on Animal {
category
}
... on Dog {
breed
}
... on Parrot {
repeatsWords
}
... on Human {
name
}
}
}
}
the above GraphQL query will be rewritten to DQL in following way:
query {
queryHome(func: type(Home)) {
address: Home.address
members: Home.members {
category: Animal.category
breed: Dog.breed
repeatsWords: Parrot.repeatsWords
name: Human.name
}
}
}
And the results of the GraphQL query will look like the following:
{
"data": {
"queryHome": {
"address": "Earth",
"members": [
{
"category": "Mammal",
"breed": "German Shepherd"
}, {
"category": "Bird",
"repeatsWords": ["Good Morning!", "I am a GraphQL parrot"]
}, {
"name": "Alice"
}
]
}
}
}
Querying
Unions can be queried only as a field of some type.
The Home
type in the given schema will be modified so that the member
field will have arguments for filtering and pagination:
type Home {
id: ID!
address: String
# **ORDERING**
# Note that there is no `order` argument. Union queries can't be ordered, but you can filter and paginate them.
# I feel, Ordering on union based on a field doesn't make sense because a union doesn't have fields of its own.
# Also, it is not possible at present because in DQL combining ordered results isn't supported.
# The results will be ordered in the increasing order of uid of each node.
# **Filtering**
# Not specifying any filter at all or specifying any of the null values for this filter would mean query all members.
# See comments on the Filter definition for a list of what is considered as a null value.
members(filter: HomeMemberFilter, first: Int, offset: Int): [HomeMember]
}
# Null values for this filter are:
# 1. null
# 2. {}
# 3. Any other combination of its fields where all of its fields have null values.
# Not specifying a field in the filter input will be considered as a null value for that field.
# This principle results in {} as null value for this filter in point 2.
input HomeMemberFilter {
# `homeMemberTypes` is used to specify which types to report back.
# It is necessary because there can be scenarios like someone wants to query all parrots and humans but have a filter on dogs,
# and the other case where someone doesn't want to query parrots and humans at all but still have a filter on dogs.
# It helps in distinguishing those cases.
# `homeMemberTypes` doesn't have any null value other than it not being specified.
# Note that specifying [] as the value would mean query nothing.
# This behaviour is similar to specifying { id: [] } in other filter types.
# Also note that the semantics of not specifying this field at all
# (i.e. you don't want to set a filter based on the concrete type)
# is similar to specifying [dog, parrot, human] as the value.
# Although, the underlying DQL query will be different for both the cases.
homeMemberTypes: [HomeMemberType]
# **Null values for dogFilter**
# 1. null
# 2. {}
# specifying a null value for this field means query all dogs
dogFilter: DogFilter
# **Null values for parrotFilter**
# 1. null
# 2. {}
# specifying a null value for this field means query all parrots
parrotFilter: ParrotFilter
# note that there is no HumanFilter because the Human type wasn't filterable
}
enum HomeMemberType {
dog
parrot
human
}
input DogFilter {
id: [ID!]
category: Category_hash
breed: StringTermFilter
and: DogFilter
or: DogFilter
not: DogFilter
}
input ParrotFilter {
id: [ID!]
category: Category_hash
and: ParrotFilter
or: ParrotFilter
not: ParrotFilter
}
It will enable one to query a union field as shown in the example section with filters and pagination.
Let’s repeat the same example, but this time with filter and pagination arguments and see how it will be rewritten to DQL:
query {
queryHome {
address
members (
filter: {
homeMemberTypes: [dog, parrot] # means we don't want to query humans
dogFilter: {
# means in Dogs, we only want to query "German Shepherd" breed
breed: { allofterms: "German Shepherd"}
}
# not specifying any filter for parrots means we want to query all parrots
}
first: 5
offset: 10
) {
... on Animal {
category
}
... on Dog {
breed
}
... on Parrot {
repeatsWords
}
... on HomeMember {
name
}
}
}
}
It would be re-written to DQL as:
query {
queryHome(func: type(Home)) {
address: Home.address
members: Home.members @filter( (type(Dog) AND allofterms(Dog.breed, "German Shepherd")) OR (type(Parrot)) ) {
category: Animal.category
breed: Dog.breed
repeatsWords: Parrot.repeatsWords
name: HomeMember.name
}
}
}
Mutations
A node can be added to a union field in some type.
For the example schema, members can be added to Home
. We will add HomeMemberRef
to the schema, and modify it like so:
# Note that only one of dogRef, parrotRef, and humanRef should be given at a time.
# Giving more than one of them together will raise an error.
input HomeMemberRef {
dogRef: DogRef
parrotRef: ParrotRef
humanRef: HumanRef
}
input AddHomeInput {
address: String
members: [HomeMemberRef]
}
type Mutation {
addHome(input: [AddHomeInput!]!): Home
}
Interaction of existing directives with union
@hasInverse
: Not applicable, as unions don’t have fields of their own.@search
: Not applicable.@dgraph
: Not applicable. Unions aren’t stored in Dgraph as such, but concrete types are. So, doesn’t make sense on Union.@id
: Not applicable.@withSubscription
: Should work as expected.@secret
: Not applicable, as unions don’t have fields of their own@auth
:@auth
can’t be applied directly on the union type because there won’t be any queries/mutations generated for a union type, but it is possible to use union fields inside auth queries using the filter on the union field. Also, one may want to put a filter on a field inside the selection set of a union field in auth queries. Both of these scenarios are presented below:
For the first version in the master branch,# it would return homes only if they have a dog member with its breed as the $BREED in auth JWT # AND have a human member who has a parrot as a pet. type Home @auth( query: { and: [ { rule: """ query ($BREED: String!) { queryHome { members(filter: { homeMemberTypes: [dog], dogFilter: { breed: { allofterms: $BREED } } } ) { __typename } } }""" } { rule: """ query { queryHome { members(filter: { homeMemberTypes: [human] } ) { ... on Human { pets(filter: { category: { eq: PARROT } }) { __typename } } } } }""" }] }) { id: ID! address: String members: [HomeMember] }
@auth
may not be supported with unions, as it would require fragments to work inside@auth
queries. This may be worked on later but should make its way to the final v20.11 release.@custom
: Custom queries and mutations returning union will work as expected. Custom field returning union and a union having a custom field in one of its types should also work as expected.@remote
: Will work as expected.@cascade
: Won’t be supported on the union field. But, will work on fields inside the union field’s selection set.
References
- GraphQL Intro: https://graphql.org/learn/schema/#union-types
- GraphQL Spec on Unions: GraphQL
- Several Discuss posts:
- GitHub’s GraphQL Schema: graphql-schema/schema.graphql at main · octokit/graphql-schema · GitHub