Building a Multi-user GraphQL table with AWS AppSync

With AWS AppSync, creating a cloud GraphQL service is easy – just import your schema, create resources, and test away. If you don’t have any authorization or request/response modification needs, this gets you started within a couple of minutes. But what if you do have some authorization needs. I’m going to be exploring various use cases over the next few posts and see how flexible the AWS AppSync service can really be.

AWS AppSync translates a GraphQL query or mutation from the submitted query to the query that the data source needs through the use of the Velocity Template Language and a resolver. In short:

The GraphQL piece translates what the remote device sends into an appropriate form, the velocity template converts that into something that the resolver can understand. the resolver then executes the query or mutation on your backend data store. The only pieces you need to change are the GraphQL schema and the velocity template.

Let’s say you want to create a mobile app that implements a cloud-connected notes list. You need to ensure that the authenticated user can access their own notes (and no-one else’s) and that when a note is stored, it is tagged with the current user ID. The user should be unaware that this is happening. In other words, they do not see their user ID within the response, nor can they submit a user ID as a security condition. Let’s see how we can set this up.

Set up Amazon DynamoDB and Amazon Cognito

The first step is to create the resources we are going to use. In this case, I’m going to use AWS Mobile. You can use the awsmobile CLI or you can use the AWS Mobile Hub console.

Using AWS Mobile Hub console to provision backend resources (Cognito & DynamoDB):

  • Log on to the AWS Mobile Hub console.
  • Create a new project, name it, and select an app platform (optional).
  • Add User Sign-in to the project. Select and configure Email and Password.
  • Add NoSQL Database to the project.
  • Create a Notes table under Example > Notes database schema. Leave permissions as Private.

We want to test this without writing code. For this, we need a web client ID and a user. Both of these can be done via the Amazon Cognito console. Click on Resources (in the top-right corner), then choose the Amazon Cognito User Pools link. To create a user, click Users & Groups, then Create User and fill in the form. Remember your username and temporary password, and ensure the user is validated prior to creation.

To get the client ID, click App Clients in the left-hand menu. Find the App client id and make a note of it as we’ll use it later in Testing your API:

AWS Mobile Hub will take care of linking Amazon Cognito to Amazon DynamoDB for you so you don’t have to worry about the permissions there.

Create the AWS AppSync Schema

Now that we have the resources created for the data sources, we can turn our attention to the API layer. Open the AWS AppSync console and click Create API. Give your API an awesome name, select Custom Schema, then click Create.

The first stop is the Data Sources tab. Click New, then enter an awesome name for your data source. It will be referenced and shown only within the AWS AppSync console so you can be a little creative. Then select Amazon DynamoDB table as the data source type. This will light up a new field (Region), followed by another field (existing table) once you fill in the region. Finally, it will give you a choice of “New Role” or “Existing Role”. It’s asking for an IAM role to allow AWS AppSync to access to your table. Create a new role for this experiment. Once the form is complete, click Create.

The next stop is the Settings tab. The default authorization scheme is an API key. We want to change this to an Amazon Cognito User Pools authorization scheme. Once you select that option, you will get another form necessary to link your Amazon Cognito User Pool to the AWS AppSync API. The only weird question here is “Default Action”. The options are ALLOW or DENY. Within the GraphQL schema, you can specify authorization rules, linking certain operations to (for example) user pool groups. The question is “what is the default action if the schema operation does not have an authorization rule attached to it”. I’m going to cover authorization rules in a different blog post. However, I want all authenticated users to be able to use this API, so the appropriate option is ALLOW.

Leave the Appid Client Regex field blank and click Save when complete.

The next stop is the Schema tab. The Schema will depend on your requirements. Here is my test schema:

type Note {
	noteId: ID!
	content: String!
	title: String!
}

type PaginatedNotes {
	notes: [Note]
	nextToken: String
}

type Query {
	getNote(noteId: ID!): Note
	allNotes(count: Int, nextToken: String): PaginatedNotes
}

type Mutation {
	saveNote(noteId: ID!, title: String!, content: String!): Note
}

schema {
	query: Query
	mutation: Mutation
}

I’ve got two queries – one of which returns a single note and the other that returns all the notes. I’ve also got a single mutation for saving a note. Once you have entered the schema, click Save. This will update the right-hand panel (the Resolver list) to include the Attach buttons:

In the simple case, we would click Create Resources here. Not so, this time. We need to link each query and mutation separately. Let’s start with the Mutuation>saveNote operation. Click the Attach button next to it. Select your data source from the drop-down. This will make the request mapping template and response mapping template appear:

The AWS AppSync documentation has some good information on these templates and you can select a starter template from the drop-down in each case. However, it took me a fair amount of trial and error to get things right. In this case, you can start with the PutItem template. My request template looks like this:

{
    "version" : "2017-02-28",
    "operation" : "PutItem",
    "key" : {
        "noteId" : { "S" : "${context.arguments.noteId}" },
        "userId" : { "S" : "${context.identity.sub}" }
    },
    "attributeValues" : {
        "title" : { "S" : "${context.arguments.title}" },
        "content": { "S" : "${context.arguments.content}" }
    }
}

Writing these templates requires knowledge of the DynamoDB operations and the context layout for AWS AppSync. Basically, I am creating a PutItem operation for DynamoDB. The key field is made up of the noteId, which I pull from the passed in argument, and the userId, which I pull from the identity of the user. The title and content are pulled from the information that the user passes to me.

For the response template, use the default “Return Single Item” from the drop-down. It looks like this:

$util.toJson($context.result)

Remember to click Save after you are done. Then return to the Schema tab and click the Attach button next to the Query > getNote operation. Again, select the data source from the drop-down. This time, choose “GetItem by Id” from the template drop-down for the request, and “Return Single Item” from the template drop-down for the response. Edit the request template as follows:

{
    "version" : "2017-02-28",
    "operation" : "GetItem",
    "key" : {
        "noteId" : { "S" : "${context.arguments.noteId}" },
        "userId": { "S" : "${context.identity.sub}" }
    }
}

As with the saveNote operation, we are adding the userId to the request based on the passed in identity. Within DynamoDB, the noteId+userId must be unique, so this will only return objects that the user owns. Our final operation is the allNotes request. Return to the Schema tab and click the Attach button next to the Query > allNotes operation.  Select your data source. For the request, select the “Paginated query” template and modify the request template as shown:

{
    "version" : "2017-02-28",
    "operation" : "Query",
    "query" : {
        ## Provide a query expression. **
        "expression": "userId = :userId",
        "expressionValues" : {
            ":userId" : {
                "S" : "${context.identity.sub}"
            }
        }
    },
    ## Add 'limit' and 'nextToken' arguments to this field in your schema to implement pagination. **
    "limit": #if(${context.arguments.limit}) ${context.arguments.limit} #else 20 #end,
    "nextToken": #if(${context.arguments.nextToken}) "${context.arguments.nextToken}" #else null #end
}

This is based on the Paginated Query template. I’ve added the userId match as an expression. Again, if you are familiar with DynamoDB queries, this should be familiar territory. The response template “Return paginated results” looks like this:

{
    "notes": $util.toJson($context.result.items),
    "nextToken": #if(${context.result.nextToken}) "${context.result.nextToken}" #else null #end
}

Note that the user will not ever see their userId in the results. That’s because the GraphQL schema does not allow you to request the userId field within the response.

Testing your API

I don’t want to write client just to test my API. Fortunately, AWS AppSync provides a capable query service as well. Click on the Queries tab. Copy the following in the Queries field:

query ListAllNotes {
  allNotes {
    notes {
      noteId, title, content
    },
    nextToken
  }
}

query GetNote($noteId:ID!) {
  getNote(noteId:$noteId){
    noteId, title, content
  }
}

mutation SaveNote($noteId:ID!, $title:String!, $content:String!) {
  saveNote(noteId: $noteId, title:$title, content: $content) {
    noteId, title, content
  }
}

Also, copy the following into the Query Variables section below the queries window:

{
  "noteId": "4c34d384-b715-4258-9825-1d34e8e6003b",
  "title": "Console Test",
  "content": "A test from the console"
}

The actual values are not important really. They just need to be filled in. If you press the Run button at this time, you will see messages in the results indicating you are not authenticated. Now, click Login with User Pools. You will get a pop up for signing in:

The clientId is your Cognito User Pool App client id (web) you copied earlier from the Amazon Cognito console. The username and password are from the user you created at the same time. The first time through this process, you will be prompted to change your password. Once this is done, you are returned to the Queries tab. You can now run the queries again and see the results.

The proof that it is working is in the DynamoDB console, though. Go back to your AWS Mobile Hub console, click on Resources and select your DynamoDB table. In the right-hand section, click the Items tab, then click Start search. You will see any entries you have created:

Note the userId field. This has been automatically filled in for you. You can log out of the User Pool, then create a new user (go back to the Cognito console) and log back in with the new user. Again, run the queries, but note that you can’t see the records for the original user.

Wrap Up

This is just the first several scenarios that I want to discuss and only half of this scenario. In the next post, I’ll work on updating my Notes app to link Cognito authentication to the AWS AppSync client. Right now, sign up for the AWS AppSync preview and start playing with GraphQL today.