Front-End Web & Mobile

Introducing new AWS AppSync module and functions for DynamoDB JavaScript resolvers

This article is written by Kim Wendt, Sr. Solutions Architect

AWS AppSync is a service that allows you to build, manage, and host GraphQL APIs in the cloud. With AppSync, you simply write your GraphQL schema and connect to data sources using resolvers. Resolvers are how AppSync translates GraphQL requests to retrieve information from the different data sources. In November 2022, AppSync introduced JavaScript resolvers to make it easier for developers to write their AppSync business logic. The launch of JavaScript resolvers included NPM libraries to simplify development: @aws-appsync/utils to provide type validation and autocompletion in code editors and @aws-appsync/eslint-plugin to catch and fix problems quickly during development.

Today, we’re making it even easier to write resolvers for Amazon DynamoDB with the launch of a new module and functions to interact with DynamoDB data sources. The new module simplify the code required to create DynamoDB requests for common operations like put, get, delete, update, scan, sync, and query. In addition, the utility provides operation helpers to help you make granular changes to item attributes during updates. The module is available in the public @aws-appsync/utils package along with type-definitions to allow developers to write type-safe code locally, when using TypeScript.

Overview

The new JavaScript module for DynamoDB data sources makes is easy to express DynamoDB requests with a few lines of code. For example, let’s say we want to add a new item to our DynamoDB table with a primaryKey of id and a set of key/value pairs for the attributes. The code below uses the ddb.put utility which takes as input the key and item to construct a DynamoDB PutRequest.

import * as ddb from '@aws-appsync/utils/dynamodb';

export function request(ctx) {
    const item = { id: util.autoId(), ...ctx.args };
    return ddb.put({ key: { id: item.id }, item });
}

export const response = (ctx) => ctx.result;

We can also retrieve information from our DyanamoDB data source using a scan operation. For example, let’s say we’ve added a few more items to our table and now we want to retrieve a list of those items. The code below uses the ddb.scan utility which takes as input limit and nextToken values to paginate the results returned from DynamoDB.

import * as ddb from '@aws-appsync/utils/dynamodb';

export function request(ctx) {
    const { limit = 10, nextToken } = ctx.args;
    return ddb.scan({ limit, nextToken });
}

export const response = (ctx) => ctx.result.items;

Now it’s your turn to use these functions with an AppSync API with additional resolver logic.

Getting started

You can get started with JavaScript module for DynamoDB in the AppSync console.

  1. In the AppSync console, choose Create API.
  2. For the API Type, leave all defaults selected then choose Next.
  3. On the Specify API details page, name your API ToDo-API then choose Next.
  4. On the Specify GraphQL resources page, choose Create type backed by a DynamoDB table now.

The AppSync console can assist you in creating a new GraphQL type, the operations associated with the type, and a DynamoDB table to serve as the data source. To illustrate the new JavaScript module functions, we will create a Todo type with the following fields: idownernameseverity, and dueOn.

  1. Fill in the Model information with the values below. To add a field, choose Add new field. For Model name, enter ToDo. In the Additional settings section, leave the Resolver runtime selected as AppSync JavaScript.
  1. preview of the create API wizardIn the Configure model table section, fill in the table name, primary key, and sort key with the values below, then choose Next.

Create DynamoDB table

  1. Confirm your API details and then choose Create API.

The ToDo-API, DynamoDB data source, and JavaScript resolvers are automatically created for you. From the Schema page, you can review and modify the GraphQL schema, edit resolvers, or attach new resolvers to the fields in your schema. Let’s update our resolver logic to use the new JavaScript module for DynamoDB.

  1. Navigate to the Schema page. In the Resolvers section, enter Mutation in the Filter types... search bar.
  2. Choose the ToDoTable resolver for the createToDo(...): ToDo field.

The createToDo resolver contains a helper function to construct a DynamoDB PutItem request including a conditionkey, and attributeValues. These values are all converted to a map object which is how DynamoDB expects the PutItem request.

function dynamodbPutRequest(params) {
    const { key, values, condition: inCondObj } = params;
    
    let condition;
    if (inCondObj) {
        condition = JSON.parse(util.transform.toDynamoDBConditionExpression(inCondObj));
        if (condition && condition.expressionValues && !Object.keys(condition.expressionValues).length) {
            delete condition.expressionValues;
        }
    }
    return {
        operation: 'PutItem',
        key: util.dynamodb.toMapValues(key),
        attributeValues: util.dynamodb.toMapValues(values),
        condition,
    }
}

Let’s simplify this logic using the JavaScript module for DynamoDB.

  1. Add the following import statement to the resolver code.
import { put } from '@aws-appsync/utils/dynamodb';
  1. Replace the request handler with the code below (and optionally delete the dynamodbPutRequest function), then choose Save.
export function request(ctx) {
    const { id, owner, ...item } = ctx.args.input;
    const key = { id, owner };
    
    const condition = { };
    Object.keys(key).forEach(k => condition[k] = { attributeExists: false });
    
    return put({key, item, condition});
}

Using the new module functions, we’re able to simplify the logic for constructing the condition operation, key, and attribute values. Now you no longer need to use the toMapValues or toDynamoDBConditionExpression utilities; this functionality is abstracted in the put utility method.

Let’s take a look at a more complicated resolver for the updateToDo operation.

  1. Navigate to the Schema page. In the Resolvers section, enter Mutation in the Filter types... search bar.
  2. Choose the ToDoTable resolver for the updateToDo(...): ToDo field.

Similar to the createToDo resolver, the updateToDo resolver contains a helper function to construct a DynamoDB UpdateItem request. Let’s simplify this logic using the new module.

  1. Add the following import statement to the resolver code.
import { update, operations as ops } from '@aws-appsync/utils/dynamodb';
  1. Replace the request handler with the code below, then choose Save.
export function request(ctx) {
    const { id, owner, ...values } = ctx.args.input;
    const key = { id, owner };
    
    const condition = {};
    Object.keys(key).forEach(k => condition[k] = { attributeExists: true });
    
    const updateObj = {};
    Object.entries(values).forEach(([k,v]) => updateObj[k] = v ?? ops.remove());
    
    return update({ key, condition, update: updateObj });
}

Using the JavaScript module for DynamoDB, we’re able to simplify the code used to create the update expression by leveraging the operations and update functions. Here, to delete an attribute during the update, we check if a specified attribute is null and use operations.remove() to mark it as being deleted.

The operations helpers provide functions that you can use to take different actions on your item attributes during an update. The available helpers are:

  • add(): adds a new attribute, including complex structures with nested attributes
  • remove(): removes an attribute from the item
  • replace(): replaces an existing attribute (or nested attribute) during an update
  • increment(): increments an attribute by a given number
  • decrement(): decrements an attribute by a given number
  • append(): Add items to the end of an attribute list
  • prepend(): Add items to the start of an attribute list
  • updateListItem(): Updates an item at specific index in the list

Using operations helpers, you can write complex update operations in a couple of lines in your JavaScript resolvers. For example:

import { update, operations as ops } from '@aws-appsync/utils/dynamodb';
export function request(ctx) {
    const updateObj = {
        count: ops.increment(1),
        friends: ops.append(['John']),
        address: ops.add({ street1: '123 Main St', street2: 'Unit A', city: 'New York', zip: '10001' }),
        pets: [ops.updateListItem('rex', 2)],
    };
    const condition = { friends: { size: { lt: 5 } }, id: { attributeExists: true } };
    return update({ key: { id: 1 }, update: updateObj, condition });
}

The following update request will:

  • increment the count attribute
  • add “John” as a friend to the friends list
  • add an address to the item
  • and change the value of the 3rd list entry of the pet attribute.

The update is only be allowed if an item with the provided key (id) exists and if the size of the friends list is less than 5.

You can find out more about the module functions in the AppSync documentation. For examples on how to use these new functions, please refer to the AWS AppSync examples repository.

Conclusion

In this post, we reviewed the new JavaScript module for DynamoDB to simplify resolver logic in AppSync resolvers. Additionally, we covered how to easily get started using AWS AppSync and how to write resolvers to use the new module. To learn more about JavaScript resolvers and the new functions for DynamoDB, see the documentation and the tutorials. You can also find easy-to-use samples and guides in the samples repository.