Front-End Web & Mobile

Amplify Framework adds support for AWS Lambda functions and Amazon DynamoDB custom indexes in GraphQL schemas

Written by Kurt Kemple, Sr. Developer Advocate at AWS, Nikhil Dabhade, Sr. Product Manager at AWS, & Me!

The Amplify Framework is an open source project for building cloud-enabled mobile and web applications. Today, we’re happy to announce new features for the Function and API categories in the Amplify CLI.

It’s now possible to add an AWS Lambda function as a data source for your AWS AppSync API using the GraphQL transformer that’s included in the Amplify CLI. You can also grant permissions for interacting with AWS resources from the Lambda function. This updates the associated IAM execution role policies without needing you to perform manual IAM policy updates.

The GraphQL transformer also includes a new @key directive that simplifies the syntax for creating custom indexes and performing advanced query operations with Amazon DynamoDB. This streamlines the process of configuring complex key structures to fit various access patterns when you’re using DynamoDB as a data source.

Adding a Lambda function as a data source for your AWS AppSync API

The new @function directive in the GraphQL transform library provides an easy mechanism to call a Lambda function from a field in your AWS AppSync API.  To connect a Lambda data source, add the @function directive to a field in your annotated GraphQL schema that’s managed by the Amplify CLI. You can also create and deploy the Lambda functions by using the Amplify CLI.

Let’s look at how you can use this feature.

What are we building?

In this blog post, we’re creating a React JavaScript application that uses a Lambda function as a data source for your GraphQL API. The Lambda function writes to storage which in this case is DynamoDB. In addition, we will illustrate how you can easily grant create/read/update/delete permissions for interacting with AWS resources (such as DynamoDB) from a Lambda function.

Setting up the project

Pre-requisites

Download, install and configure the Amplify CLI.

$ npm install -g @aws-amplify/cli 
$ amplify configure

Next, create your project if you don’t already have one. We’re creating a React application here, but you can choose to create a project with any other Amplify-supported framework, such as Angular, Vue or Ionic.

$ npx create-react-app my-project

Download, install and configure the Amplify CLI.

$ cd my-project
$ amplify init
$ npm i aws-amplify
$ cd my-project<br />$ amplify init<br />$ npm i aws-amplify

The ‘amplify init’ command initializes the project, sets up deployment resources in the cloud, and makes your project ready for Amplify.

Adding storage to your project

Next, we will setup the backend to add storage using Amazon DynamoDB for your React JavaScript application.

$ amplify add storage
? Please select from one of the below mentioned services NoSQL Database
Welcome to the NoSQL DynamoDB database wizard
This wizard asks you a series of questions to help determine how to set up your NoSQL database table.

? Please provide a friendly name for your resource that will be used to label this category in the project: teststorage
? Please provide table name: teststorage
You can now add columns to the table.
? What would you like to name this column: id
? Please choose the data type: number
? Would you like to add another column? Yes
? What would you like to name this column: email
? Please choose the data type: string
? Would you like to add another column? Yes
? What would you like to name this column: createdAt
? Please choose the data type: string
? Would you like to add another column? No
Before you create the database, you must specify how items in your table are uniquely organized. You do this by specifying a primary key. The primary key uniquely identifies each item in the table so that no two items can have the same key.
This can be an individual column, or a combination that includes a primary key and a sort key.
To learn more about primary keys, see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.PrimaryKey
? Please choose partition key for the table: id
? Do you want to add a sort key to your table? No
You can optionally add global secondary indexes for this table. These are useful when you run queries defined in a different column than the primary key.
To learn more about indexes, see: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.SecondaryIndexes
? Do you want to add global secondary indexes to your table? No
Successfully added resource teststorage locally

Adding a function to your project

Next, we’re adding a Lambda function by using the Amplify CLI. We will also grant permissions for the Lambda function to be able to interact with the DynamoDB table that we created in the previous step.

$ amplify add function
Using service: Lambda, provided by: awscloudformation
? Provide a friendly name for your resource to be used as a label for this category in the project: addEntry
? Provide the AWS Lambda function name: addEntry
? Choose the function template that you want to use: Hello world function
? Do you want to access other resources created in this project from your Lambda function? Yes
? Select the category storage
? Select the resources for storage category teststorage
? Select the operations you want to permit for teststorage create, read, update, delete

You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION
var storageTeststorageName = process.env.STORAGE_TESTSTORAGE_NAME
var storageTeststorageArn = process.env.STORAGE_TESTSTORAGE_ARN

? Do you want to edit the local lambda function now? Yes 

This opens the Hello world function template file ‘index.js’ in the editor that you selected during the ‘amplify init’ step.

Auto populating environment variables for your Lambda function

The Amplify CLI adds the environment variables as comments to your index.js files at the top for ease of reference. The environment variables represent the AWS resources that the Lambda function interacts with. In this case, the AWS resource is DynamoDB. We want to have the Lambda function add an entry to the DynamoDB table with the parameters that we pass to the GraphQL API. Add the following code to the Lambda function that uses the environment variables that represent the DynamoDB table and AWS Region:

/* Amplify Params - DO NOT EDIT
You can access the following resource attributes as environment variables from your Lambda function
var environment = process.env.ENV
var region = process.env.REGION
var storageTeststorageName = process.env.STORAGE_TESTSTORAGE_NAME
var storageTeststorageArn = process.env.STORAGE_TESTSTORAGE_ARN

Amplify Params - DO NOT EDIT */

var AWS = require('aws-sdk');
var region = process.env.REGION
var storageTeststorageName = process.env.STORAGE_TESTSTORAGE_NAME
AWS.config.update({region: region});
var ddb = new AWS.DynamoDB({apiVersion: '2012-08-10'});
var ddb_table_name = storageTeststorageName
var ddb_primary_key = 'id';

function write(params, context){
    ddb.putItem(params, function(err, data) {
    if (err) {
      console.log("Error", err);
    } else {
      console.log("Success", data);
    }
  });
}
 

exports.handler = function (event, context) { //eslint-disable-line
  
  var params = {
    TableName: ddb_table_name,
    Item: AWS.DynamoDB.Converter.input(event.arguments)
  };
  
  console.log('len: ' + Object.keys(event).length)
  if (Object.keys(event).length > 0) {
    write(params, context);
  } 
}; 

After you replace the function, jump back to the command line and press Enter to continue.

Next, run the ‘amplify push’ command to deploy your changes to the AWS Cloud.

$ amplify push

Adding and updating the Lambda execution IAM role for Amplify managed resources

When you run the ‘amplify push’ command, the IAM execution role policies that are associated with the permissions you granted earlier are updated automatically to allow the Lambda function to interact with DynamoDB.

Setting up the API

After completing the function setup, the next step is to add a GraphQL API to your project:

$ amplify add api
? Please select from one of the below mentioned services GraphQL
? Provide API name: myproject
? Choose an authorization type for the API: API key
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? Yes

This will open the schema.graphql file in the editor you selected during the ‘amplify init’ step.

Replace the annotated schema template located in your <project-root>/amplify/backend/api/<api-name>/schema.graphql file with the following code:

type Customer @model {
  id: ID!
  name: String!
  createdAt: String
}

type Mutation {
  addEntry(id: Int, email: String, createdAt: String): String @function(name: "addEntry-${env}")
}

Check to see if the updates to your schema are compiled successfully by running the following command:

$ amplify api gql-compile

Now that your API is configured, run the amplify push command to deploy your changes to create the corresponding AWS backend resources.

When you’re prompted about code generation for your API, choose Yes. You can accept all default options. This generates queries, mutations, subscriptions, and boilerplate code for the Amplify libraries to consume. For more information, see Codegen in the Amplify CLI documentation.

Accessing the function from your project

Now that your function and API are configured, you can access them through the API class, which is part of the Amplify JavaScript Library.

Open App.js and add the following imports and call to the Amplify API as shown below:


import React from 'react';
import logo from './logo.svg';
import './App.css';

import { addEntry }  from './graphql/mutations';
import awsconfig from './aws-exports';
import { API, graphqlOperation } from "aws-amplify";
API.configure(awsconfig);

class App extends React.Component {

  constructor() {
    super();
    this.state = {};
  }

  componentDidMount() {
    this.addData();  
  }

  async addData() {
    const entry = {id:“1”, email:“email@example.com”, createdAt:“2019-5-29”}
    const data = await API.graphql(graphqlOperation(addEntry, entry))
    console.log(data)
    this.setState({data});
  }
  
}
export default App;

Running the app

Now that you have your application code complete, run the application and verify that the API call outputs “Success”.

Setting Amazon DynamoDB custom indexes in your GraphQL schemas

When you’re building an application on top of DynamoDB, it helps to first think about access patterns. The new @key directive, which is a part of the GraphQL transformer in the Amplify CLI, makes it simple to configure complex key structures in DynamoDB that can fit your access patterns.

Let’s say we’re using DynamoDB as a backend for your GraphQL API. The initial GraphQL schemas that we can use to represent @model types Customer and Item are as shown here:

type Customer @model {
  email: String!
  username: String!
}

type Item @model {
    orderId: ID!
    status: Status!
    createdAt: AWSDateTime!
    name: String!
}

enum Status {
    DELIVERED
    IN_TRANSIT
    PENDING
    UNKNOWN
}

Access patterns

For example, let’s say that this application needs to facilitate the following access patterns:

  • Get customers by email – email is the primary key.
  • Get Items by status and by createdAt – orderId is the primary key

Let’s walkthrough how you would accomplish these use cases and call the APIs for these queries in your React JavaScript application.

We’re assuming that you completed the pre-requisites and created your React JavaScript application as shown in the first section.

Creating an API

First, we will create a GraphQL API by using the ‘amplify add api’ command:

$ amplify add api
? Please select from one of the below mentioned services GraphQL
? Provide API name: myproject
? Choose an authorization type for the API: API key
? Do you have an annotated GraphQL schema? No
? Do you want a guided schema creation? Yes
? What best describes your project: Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now? Yes
? Press enter to continue

This will open the schema.graphql file under <myproject>/amplify/backend/api/myproject/schema.graphql

Modifying the schema.graphql file

Let’s dive in to the details with respect to the new @key directive.

Querying by primary key

Add the following Customer @model type to your schema.graphql

type Customer @model @key(fields: ["email"]) {
    email: String!
    username: String
}

For Customer @model type, a @key without a name specifies the key for the DynamoDB table’s primary index. Here the hash key for the table’s primary index is email. You can only provide one @key without a name per @model type.

Querying by composite keys (one or more fields are sort key)

type Item @model
    @key(fields: ["orderId", "status", "createdAt"])
    @key(name: "ByStatusAndCreatedAt", fields: ["status", "createdAt"], queryField: "itemsByStatusAndCreatedAt")
{
    orderId: ID!
    status: Status!
    createdAt: AWSDateTime!
    name: String!
}

enum Status {
    DELIVERED
    IN_TRANSIT
    PENDING
    UNKNOWN
}

Let’s break down the above Item @model type.

DynamoDB lets you query by at most two attributes. We added three fields to our first key directive ‘@key(fields: [“orderId”, “status”, “createdAt”])‘. The first field ‘orderId is the hash key as expected, but the sort key is the new composite key named status#createdAt that is made of the status and createdAt fields. This enables us to run queries using more than two attributes at a time.

Run the ‘amplify push’ command to deploy your changes to the AWS Cloud. Because we have the @key directive, it creates the DynamoDB tables for Customer and Item–with the primary indexes, sort keys and generate resolvers that inject composite key values during queries and mutation.

$ amplify push
Current Environment: dev
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2

The file <myproject>/src/graphlql/queries.js will contain the auto generated queries for our  intended access patterns “Get customers by email” and “Get Items by status and by createdAt”.

Accessing the API from your application

Now that your API is configured, you can access it through the API class, which is part of the Amplify JavaScript Library. We will call the query for “Get Items by status and by createdAt”

Open App.js and add the following imports and call to the Amplify API as shown below:

import React from 'react';
import logo from './logo.svg';
import './App.css';

import { itemsByStatusAndCreatedAt }  from './graphql/queries';
import awsconfig from './aws-exports';
import { API, graphqlOperation } from "aws-amplify";
API.configure(awsconfig);

class App extends React.Component {

  constructor() {
    super();
    this.state = {};
  }

  componentDidMount() {
    this.fetchData();  
  }

  async fetchData() {
    const entry = {status:'PENDING', createdAt: {beginsWith:"2019"}};
    const data = await API.graphql(graphqlOperation(itemsByStatusAndCreatedAt, entry));
    this.setState({data});
  }
  
  render() {
    return (
      <div className="App">
        <p>
          {JSON.stringify(this.state.data, null, 4)}
        </p>
      </div>
    );
  }
}
export default App;

To learn more, see GraphQL Transform.

Feedback

We hope you like these new features! As always, let us know how we’re doing, and submit any requests in the Amplify Framework GitHub Repository. You can read more about AWS Amplify on the AWS Amplify website.