Front-End Web & Mobile

AWS AppSync pipeline resolvers and functions now support additional array methods and arrow functions

AWS AppSync is a managed service that makes it easy to build scalable APIs that connect applications to data. Developers use AppSync every day to build GraphQL APIs that interact with data sources like Amazon DynamoDB, AWS Lambda, and HTTP APIs. With AppSync, developers can write their resolvers using JavaScript, and run their code on AppSync’s APPSYNC_JS runtime.

Today, we are adding functionality to the APPSYNC_JS runtime with support for the following higher-order functions on arrays:

  • Array.prototype.forEach
  • Array.prototype.map
  • Array.prototype.flatMap
  • Array.prototype.filter
  • Array.prototype.reduce
  • Array.prototype.reduceRight
  • Array.prototype.find
  • Array.prototype.some
  • Array.prototype.every
  • Array.prototype.findIndex
  • Array.prototype.findLast
  • Array.prototype.findLastIndex

With this update, APPSYNC_JS now also supports arrow functions, which can be defined at any level within our resolver or function code. This allows to write code like this:

// filter the array to return odd numbers only
export function response() {
  return [1,2,3,4,5].filter(x => x % 2)
}

or

// return the array sum
export function response() {
  return [1,2,3,4,5].reduce((sum,x) => sum + x)
}

and

export function response() {
  // define a test function
  const test = (x) => x > 42
  // return the first match (or null)
  return [12, 34, 56, 9, 75].find(test)
}

With arrow functions and the new Array function support, we can now compactly write our business logic. This makes it easier to write code to solve common problems with arrays and objects, and we can also use them to write more complex functionality and utilities. We give a couple of examples below.

Writing a library to update an item in a DynamoDB table

Updating an item is a common task when working with data in Amazon DynamoDB tables. We can use the newly introduced Array functions to write a helper that creates an UpdateItem request. Given the schema

 

type User {
  id: ID!
  email: String!
  name: String
  status: String
}

input UpdateUserInput {
    id: ID!
    email: String
    name: String
}

type Mutation {
  updateUser(input: UpdateUserInput!): User
}

We can define the following APPSYNC_JS function to attach to our resolver. We define an update function that uses Array.reduce to iterate over the list of values and create an UpdateItem request.

import { util } from '@aws-appsync/utils'

export function request(ctx) {
  const { input: { id, ...values } } = ctx.args
  return update({ id }, values)
}

export function response(ctx) {
  return ctx.result
}

// build an UpdateItem request to SET or DELETE attributes
// of an item identified by `key`.
function update(key, values) {
  const exp = Object.entries(values).reduce(
    (prev, [key, value]) => {
      // expression attribute name is a placeholder that you use in an
      // Amazon DynamoDB expression as an alternative to an actual attribute name.
      prev.names[`#${key}`] = key
      if (value) {
        // if a value exist:
        // Use the SET action in an update expression to add one or
        // more attributes to an item. 
        prev.sets.push(`#${key} = :${key}`)
        // Expression attribute values in Amazon DynamoDB are substitutes
        // for the actual values that you want to compare
        prev.values[`:${key}`] = value
      } else {
        // if the value is null, add it to a list and:
        // Use the REMOVE action in an update expression to remove
        // one or more attributes from an item in Amazon DynamoDB
        prev.removes.push(`#${key}`)
      }
      return prev
    },
    { sets: [], removes: [], names: {}, values: {} }
  )

  // create the update expression
  let expression = exp.sets.length ? `SET ${exp.sets.join(', ')}` : ''
  expression += exp.removes.length ? ` REMOVE ${exp.removes.join(', ')}` : ''

  return {
    operation: 'UpdateItem',
    key: util.dynamodb.toMapValues(key),
    update: {
      expression,
      expressionNames: exp.names,
      expressionValues: util.dynamodb.toMapValues(exp.values),
    },
  }
}

After attaching the function to our pipeline resolver for the updatedUser mutation, we can update the user’s name and delete their status with this operation:

mutation UPDATE {
  updateUser(input: {
    id: "123-32-xyz", email: "hsperry@fake.net", name: "Helen S. Perry", status: null
  }) {
    id
    email
    name
    status
  }
}

Handling single table design queries

AppSync allows to define DynamoDB datasources that use a single-table design or a multi-table approach. In a single-table design, different data types are stored in one DynamoDB table. With single-table design, we can retrieve all of our items in a single query from a single table. For example, we can update our schema to retrieve a user and their orders in a single request. First, we update our schema as show below. A user has orders, and each order is made up of zero or more items.

type User {
    id: ID!
    email: String!
    name: String
    status: String
    orders: [Order]
}

type Order {
    id: ID
    items: [Item]
    createdOn: AWSDateTime!
    updatedOn: AWSDateTime!
}

type Item {
    id: ID!
    orderId: ID!
    description: String!
    shipped: Boolean!
    shippedOn: AWSDateTime!
}

type Query {
    getUserAndOrderDetails(id: ID!): User
}

The data is stored in a DynamoDB table using a composite key:

  • the partition key id
  • a sort key called SK

To fetch data about a user and their orders and items, we query the table for any items where the partition key is equal to the user id. Each item in the table has a __typename attributes that specifies the item type (e.g.: User, Order, or Item).

We attach the following APPSYNC_JS function to the getUserAndOrderDetails resolver:

import { util } from "@aws-appsync/utils";

export function request(ctx) {
  const { id } = ctx.args;
  let query = { id: { eq: id } };
  query = JSON.parse(util.transform.toDynamoDBConditionExpression(query));
  return { operation: "Query", query };
}

export function response(ctx) {
  let { items } = ctx.result;
  // find the user
  const user = items.find((item) => item.__typename === "User");
  
  // if no user is found, return null
  if (!user) {
    console.log("could not find user in reponse items");
    return null;
  }
  
  const orders = {};
  const orderItems = [];
  
  // identify each `Order` and `OrderItem` in the returned items
  items.forEach((item) => {
    switch (item.__typename) {
      case "Order":
        orders[item.SK] = { ...item, id: item.SK, items: [] };
        break;
      case "Item":
        orderItems.push({ ...item, id: item.SK });
        break;
      default:
        break;
    }
  });
  
  // associated each order item with an order
  orderItems.forEach((item) => orders[item.orderId].items.push(item));
  
  // finally, assign the orders to the user
  user.orders = Object.values(orders);
  return user;
}

In the response, we find the User item using the Array.find method. We then iterate through the items using Array.forEach to find each order and order item. We then associate each item with its order. We can now use a query to get our data in the appropriate shape for clients:

query MyQuery {
  getUserAndOrderDetails(id: "<user-id>") {
    id
    name
    email
    status
    orders {
      id
      items {
        id
      }
    }
  }
}

Conclusion

In this post, we went over the new Array functions and arrow function notation introduced in APPSYNC_JS. We saw how these updated features allows to easily work with arrays and objects. We also saw how you can use them to solve common complex problems when working with data in resolvers. You can get started today in any region when AppSync is available, by reviewing APPSYNC_JS’s supported built-in objects and functions, and by reading the DynamoDB JavaScript resolvers tutorial for AppSync.