Build a GraphQL Service the easy way with AWS Amplify Model Transforms
Creating a functional GraphQL API is hard. You have to create a GraphQL schema, decide on authentication and database structures, implement the schema in a GraphQL service, wire up the authentication, hook up the database sources, ensure the whole thing is scalable, worry about logging and monitoring, and then write your app.
AWS AppSync helps you with everything except the GraphQL schema and the app. Now, AWS Amplify is helping you with the GraphQL schema by introducing model transforms. This is an open-source library that transforms an annotated GraphQL schema into a real GraphQL schema that you can deploy (technically, the schema is embedded in an Amazon CloudFormation template that can be deployed on AWS). If you are using the AWS Amplify CLI (and you should — it makes the process much easier), it will also deploy all the resources necessary to implement the GraphQL schema on AWS AppSync and wire them up with appropriate VTL mapping templates to properly do the operations.
Let’s take a little example. I use my notes app as my go-to teaching tool. It uses one table — a list of notes. I can represent this in standard GraphQL schema language like this:
Of course, this isn’t “best practices”. It’s probably very similar to the first GraphQL schema just about everyone who is learning the schema language writes. We do basic CRUD operations and don’t worry about searching, sorting, pagination, input types, or subscriptions. Those things can be added on later.
With an annotated schema, we can write the following:
Yep — that’s it. Just five lines. What happens when you run it through the model transforms library (and deploy it via the AWS Amplify CLI) is that this is expanded. You get a CRUD API with paging and filtering built-in; input types are created for each of the mutations and subscriptions are set up on each mutation; the backing store in a DynamoDB database. Five lines turns into over 100 lines of schema that will take care of most of the requirements of your typical app.
@model is an annotation that tells the model transform library to do something. There are a big list of them, but basically, they fall into three camps:
- Backing or data store directives
- Authorization directives
- Capabilities directives
Backing Data Store
I’ve already introduced you to one data store directive in my example above. The
@model directive stores the data in a table dedicated to the type within Amazon DynamoDB. This is good for basic searches and filtering, but you may want better searchability of your data. For example, you might want to do geo-lookups, or faceted search. For this sort of functionality, it would be a good idea to stream the data from DynamoDB to an ElasticSearch Service. To do that, add the
@searchable directive to the type:
Underneath the covers, AWS Amplify deploys a DynamoDB table and an ElasticSearch Service instance with a Lambda function that streams data from DynamoDB to ElasticSearch. All the queries will hit the ElasticSearch Service and all the mutations will be sent to DynamoDB.
Before model transforms, you needed to adjust the VTL-based mapping templates to properly implement authorization within the API. The model transforms library makes authorization a breeze. Let’s say I wanted to change my note into a multi-tenant service. Here is how I would do it:
@auth directive provides rules to say who can access the specific type operations. These can get quite complex. Note that the rules is a JSON array of individual rules. Each rule can have several components, the only one of which is required is the
Let’s take a slightly more complex version of this. Let’s say you wanted to allow the owner to create, update, and delete their own notes, but you wanted the owner and a group of people called “managers” to read the notes:
If any auth rule matches, then the operation is allowed. Here is a list of all the things you can currently set:
allowfield is required and can be “owner” or “groups”
ownerFieldis the field where the owner of the record is stored. This defaults to the field name
identityFieldis the field within the
$context.identityobject within the VTL that you store in the
ownerFieldto recognize the owner. It defaults to
groupsFieldis the field within record that provides the group information that is compared.
groupsfield is the list of groups to allow when the
allowfield is set to
groups. This field can be a string or an array of strings if you want to represent multiple groups.
mutationsfield is the list of mutations to allow. This field is always an array and can consist of zero or more of “create”, “update” or “delete”.
queriesfield is the list of queries to allow. This field is (as with the mutations), an array and consists of zero or more of “get” and “list”.
If you don’t specify any queries or mutations, then it assumes all of the available operations are handled by the rule. If even one query or mutation is listed, then it assumes the others are not allowed by the rule.
I fully expect this area to get much richer as time goes by to support more and more authorization scenarios.
To show off capabilities, let’s take a different model. Let’s say I have a blog application where multiple people can have a blog. Each blog can have a number of posts, and each post can have a number of comments. This may seem a little contrived, but relationships between types is common. Most applications will implement this with a SQL database and represent the types as individual tables that can be joined.
With GraphQL, you get cascading resolvers instead, so you don’t need to program in complex SQL queries to manage this for you. However, you need to model the data. For this, you can use the
@connection directive. Let’s take a look at the blog example:
@connection supports both one-to-one and one-to-many relationships. There is a little bit more to do if you want the bi-directional connections to provide reverse linkage. For example, if you wanted to say “give me all the posts for a particular blog”, you may want the bi-directional linkage. You can do this by naming the connection:
Use the same name in both sides of the connection (one in the
Blog type and one in the
Another good capability to add is versioning. This is especially useful when you are using the interface for an fully offline scenario when you need object versioning and conflict detection. In this scenario, when you send an update, you must also send a version field. This says “I’m updating record with id X and version Y”. If the current version doesn’t match Y, then a conflict response is produced. You can use this on the client to produce the correct dialog. Here is my original Notes type with versioning:
The current version is stored in a field called
version. When you send an update or delete, send an
expectedVersion field as well to handle conflict resolution.
Note that most of the directives (
@searchable) currently rely on DynamoDB as the backing store, so they require the
@model directive to be added to the type.
You can use the GraphQL transforms library independently of the AWS Amplify CLI as well. It produces a CloudFormation template that has the schema, resolver mapping templates, and resource descriptions that are needed to implement the transformed GraphQL API in AWS AppSync. Check out the documentation if you want to try that out.