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
orp
) - 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.
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.