emeraldwalk

AWS AppSync - Schema with 1 to N Types

I recently decided to build a progressive web app (PWA) and was interested in supporting offline capabilities. I've also wanted to get some more experience using AWS, so I started researching my options. After a little googling, I decided to start building my app using AWS AppSync.

The App

The application I'm building is a journaling app for tracking various projects and technologies I work on. I've had to update my resume a couple of times in the last couple of years and realized I don't always remember the things I have worked on. Seemed like a good opportunity to build something.

GraphQL Schema

For anyone unfamiliar with AppSync, it allows you to build a GraphQL api on top of various AWS services. You have the option of defining a GraphQL schema and then generating the appropriate GraphQL resolvers and DynamoDB tables.

For my journal app, I started with the following schema.

# Inline types
enum SpanType {
  image,
  text
}

# Inline things (images & text for now)
type Span {
  type: SpanType!
  content: String!
}

# Container for multiple spans
type Block {
  spans: [Span!]!
}

# Journal entry
type Entry {
  id: ID!
  blocks: [Block!]!
  date: AWSDate!
  title: String!
}

# Fetch a single journal entry
type Query {
  fetchEntry(id: ID!): Entry
}

Let's break this down a bit.

  • Block - represents a content container (think html div or p)
  • Span - represents an inline thing contained within a Block
  • SpanType - different span types
  • Entry - a journal entry

There's some additional fields that will eventually need to be added such as creation date, updated date, author, etc., but this is a good start.

Generating the GraphQL Resolvers and DynamoDB

Once we have a basic schema, we still need some additional things to have a working GraphQL api. AppSync has a lovely feature that can auto-generate much of this for us.

AppSync Create Resources

Clicking on "Create Resources" opens up a wizard that allows you to pick a type from your schema, create a new DynamoDB table, and generate some additional things that will be merged into your schema. I decided to select my Entry type hoping that it would create everything I needed.

Using the "Create Resources" feature added some additional type and input declarations and added some query and mutation methods. Here's the input types that were created:

# Input type for creating entries
input CreateEntryInput {
  date: AWSDate
  title: String!
}

# Input type for updating entries
input UpdateEntryInput {
  id: ID!
  date: AWSDate
  title: String
}

# Input type for deleting entries
input DeleteEntryInput {
  id: ID!
}

# Mutation uses the input types to make changes to entries
type Mutation {
  createEntry(input: CreateEntryInput!): Entry
  updateEntry(input: UpdateEntryInput!): Entry
  deleteEntry(input: DeleteEntryInput!): Entry
}

Input types define data that is passed to mutation methods, and the Mutation type defines the mutation methods. The part that surprised me was that none of the input types included my blocks list. This is where my challenges started.

Including my Nested Types

I spent a few days googling how to deal with 1 to n relationships in AppSync, GraphQL, AWS, etc. I found some examples showing how to create an additional DynamoDB table using a sort index, but nothing I tried was quite working. I also didn't really want to have to update my Block instances independent of my Entry instances. I eventually decided to just save my blocks as a JSON string.

type Entry {
  id: ID!
  blocks: String!
  date: AWSDate!
  title: String!
}

I then stumbled across a post on how to store multiple GraphQL types in the same DynamoDB table which got me rethinking my approach. It turned out that the solution was simpler than I thought.

For my particular use case, my sub types (Block and Span) are always associated with a single Entry instance. I never need to query them apart from their parent entry, and modifying them will always be by replacing the entire blocks list for a particular entry. This means I only needed to include the data as part of saving and loading an entry.

In order to include my data, I created some additional input types:

# Mirrors Span type
input SpanInput {
  type: SpanType!
  content: String!
}

# Mirrors Block type
input BlockInput {
  spans: [SpanInput!]!
}

and then updated my CreateEntryInput and UpdateEntryInput types:

input CreateEntryInput {
  blocks: [BlockInput!]! # added block inputs
  date: AWSDate
  title: String!
}

input UpdateEntryInput {
  id: ID!
  blocks: [BlockInput!]! # added block inputs
  date: AWSDate
  title: String
}

Once I had updated my schema, it was time to test some queries.

GraphQL Queries

The AppSync console has a Queries section that allows you to run queries and mutations against your GraphQL instance.

Mutation

To test creating a journal entry, I ran the following mutation:

mutation createEntry {
  createEntry(input: {
    blocks: [
      {
        spans: [
          {
            type: text
            content: "Here is some test text."
          },
          {
            type: image
            content: "/path/img.png"
          }
        ]
      }
    ]
    tags: ["some tag"]
    title: "Testing"
  }) {
    # Return data
    id
    tags
    blocks {
      spans {
        type
        content
      }
    }
  }
}

Here I'm passing an instance of my CreateEntryInput input type into my createEntry mutation method and defining the shape of data I will return. The result looks something like this:

{
  "data": {
    "createEntry": {
      "id": "1bae0e2e-4750-4a85-9d04-82047b685986",
      "tags": [
        "some tag"
      ],
      "blocks": [
        {
          "spans": [
            {
              "type": "text",
              "content": "Here is some test text."
            },
            {
              "type": "image",
              "content": "/path/img.png"
            }
          ]
        }
      ]
    }
  }
}

Query

query listEntries {
  listEntries{
    items {
      id
      title
      blocks {
        spans {
          type
          content
        }
      }
    }
  }
}

Here's the result after creating 2 entries:

{
  "data": {
    "listEntries": {
      "items": [
        {
          "id": "1bae0e2e-4750-4a85-9d04-82047b685986",
          "title": "Testing",
          "blocks": [
            {
              "spans": [
                {
                  "type": "text",
                  "content": "Here is some test text."
                },
                {
                  "type": "image",
                  "content": "/path/img.png"
                }
              ]
            }
          ]
        },
        {
          "id": "a605dbb2-934b-4208-9976-0da0e2969333",
          "title": "Another Test",
          "blocks": [
            {
              "spans": [
                {
                  "type": "text",
                  "content": "This is another test."
                }
              ]
            }
          ]
        }
      ]
    }
  }
}

I was pleasantly surprised to see that this just worked.

Summary

AWS AppSync is looking promising as a good option for the GraphQL api for my new PWA. I still need to add some things to the schema such as timestamps, and I still need to write the client side app, but this is a good start. I hope to blog about the experience as I have more to share. Stay tuned.