Model Unions as sparse fields in GraphQL

Problem

The GraphQL API does not support Union types, yet you need query results that are comparable to using Unions.

This recipe is one solution to the problem outlined in Composable Types without Unions.

Solution

One method to model a union is to provide all fields for all entities on the same type. This is especially useful when the various entities share many properties. When creating or updating an entity of a given type, only the fields related to that type are provided, while the rest remain null. You can provide a String or Enum "type" or "tag" field that identifies the entity as a certain type of item.

Setup

  1. Update your GraphQL schema to include:

    type User {
      name: String!
      favorites: [MediaItem]! @relation
      # ...
    }
    
    enum MediaType {
      MOVIE
      SHOW
      BOOK
    }
    
    type MediaItem {
      type: MediaType!
    
      title: String!
      newRelease: Boolean
      # ...movie fields
      # ...show fields
      # ...book fields
    
      favoritedBy: [User]! @relation
    }
    
    type Query {
      itemsByType(type: MediaType!): [MediaItem]!
      itemsByNewRelease(newRelease: Boolean!): [MediaItem]!
    }

    This involves editing the GraphQL schema definition, wherever you have stored it, and then uploading the new schema in the Fauna Dashboard.

  2. Run GraphQL queries to create a User and some MediaItems:

    mutation {
      createUser(
        data: {
          name: "Alice"
          favorites: {
            create: [
              { title: "Black Widow", type:MOVIE, newRelease: true }
              { title: "Psycho", type:MOVIE }
              { title: "The Expanse", type:SHOW, newRelease: true }
              { title: "Fellowship of the Ring", type:BOOK }
            ]
          }
        }
      ) {
        name
        _id
        favorites {
          data { title, type, newRelease, _id }
        }
      }
    }

    The result should be similar to:

    {
      "data": {
        "createUser": {
          "name": "Alice",
          "_id": "332200338156159522",
          "favorites": {
            "data": [
              {
                "title": "Black Widow",
                "type": "MOVIE",
                "newRelease": true,
                "_id": "332200338158256674"
              },
              {
                "title": "Psycho",
                "type": "MOVIE",
                "newRelease": null,
                "_id": "332200338163499554"
              },
              {
                "title": "The Expanse",
                "type": "SHOW",
                "newRelease": true,
                "_id": "332200338166645282"
              },
              {
                "title": "Fellowship of the Ring",
                "type": "BOOK",
                "newRelease": null,
                "_id": "332200338170839586"
              }
            ]
          }
        }
      }
    }
    The document IDs presented in the _id fields are different for every new document created in Fauna. The Objective 3 query requires two of the document IDs present in this result.

Objective 1: Query for all entities of a given type

You can get all items of a type by indexing on a "type" or "tag" field. Indexes are automatically added by the GraphQL API using the fields specified on the Query type to support search by fields, such as the type field:

{
  allMovies: itemsByType(type: MOVIE) {
    data { title, newRelease }
  }

  allShows: itemsByType(type: SHOW) {
    data { title, newRelease }
  }

  allBooks: itemsByType(type: BOOK) {
    data { title, newRelease }
  }
}

You should see the result:

{
  "data": {
    "allMovies": {
      "data": [
        {
          "title": "Black Widow",
          "newRelease": true
        },
        {
          "title": "Psycho",
          "newRelease": null
        }
      ]
    },
    "allShows": {
      "data": [
        {
          "title": "The Expanse",
          "newRelease": true
        }
      ]
    },
    "allBooks": {
      "data": [
        {
          "title": "Fellowship of the Ring",
          "newRelease": null
        }
      ]
    }
  }
}

Objective 2: Query for all entities with certain properties

Every entity is searchable by every property. Indexes are automatically added by the GraphQL API using the fields specified on the Query type to support search-by-field, such as the newRelease field:

{
  itemsByNewRelease(newRelease: true) {
    data { title, type, newRelease }
  }
}

The result should be:

{
  "data": {
    "itemsByNewRelease": {
      "data": [
        {
          "title": "Black Widow",
          "type": "MOVIE",
          "newRelease": true
        },
        {
          "title": "The Expanse",
          "type": "SHOW",
          "newRelease": true
        }
      ]
    }
  }
}

Objective 3: Query for all relationships of an entity, regardless of which type it points to

  1. You can search for all user favorites by selecting the single favorites field.

  2. You can search for all users that have marked the media item as a favorite through the favoritedBy field.

To make the following query work:

  • replace <USERID> with the document ID for the user created in the setup’s step 2 (just below the name field in the result).

  • replace <MEDIAITEMID> with the document ID for the "Black Widow" movie created in the setup’s step 2.

{
  findUserByID(id: "<USERID>") {
    name
    favorites {
      data { title, type, newRelease }
    }
  }

  findMediaItemByID(id: "<MEDIAITEMID>") {
    title
    type
    newRelease
    favoritedBy {
      data { name }
    }
  }
}

The result should be similar to:

  "data": {
    "findUserByID": {
      "name": "Alice",
      "favorites": {
        "data": [
          {
            "title": "Black Widow",
            "type": "MOVIE",
            "newRelease": true
          },
          {
            "title": "Psycho",
            "type": "MOVIE",
            "newRelease": null
          },
          {
            "title": "The Expanse",
            "type": "SHOW",
            "newRelease": true
          },
          {
            "title": "Fellowship of the Ring",
            "type": "BOOK",
            "newRelease": null
          }
        ]
      }
    },
    "findMediaItemByID": {
      "title": "Black Widow",
      "type": "MOVIE",
      "newRelease": true,
      "favoritedBy": {
        "data": [
          {
            "name": "Alice"
          }
        ]
      }
    }
  }
}

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!