Front-End Web & Mobile

AWS AppSync and the GraphQL Info Object

This article was written by Brice Pellé, Principal Specialist Solutions Architect, AWS

 

AWS AppSync is a fully managed service that allows to deploy Serverless GraphQL backends in the AWS cloud. GraphQL is a data language for your API that makes it easy and straight forward to interact with multiple data sources. One of the advantages of GraphQL is that it can retrieve data from multiple sources at once, with a single request. Additionally GraphQL queries provide a selection set that specifies the shape of the expected results, ensuring that only the wanted fields and nested fields are returned.

Today we are pleased to announce support for the GraphQL Info object in AWS AppSync. When making queries or mutations, resolvers now have access to an info object that provides information about the GraphQL request being made. In addition to providing the field name, parent name and variables of the query, the info object also exposes details about the selection set. This makes it possible to make calls to your data sources that are fine-tuned based on your queries expected data shape.

For example, the info object comes in handy when a type is backed by a primary data source, and is also composed of fields whose types are backed by other data sources. Using a single resolver, the info object’s selection set information can be used to identify which fields have been requested and which data sources should be queried. It removes the need to attach additional resolvers to nested fields and optimizes the GraphQL query performance by reducing the number of resolver invocations.

The info object is part of the $context variable in the AppSync resolver and has the following definition:

{
    "fieldName": "string",
    "parentTypeName": "string",
    "variables": { ... },
    "selectionSetList": ["string"],
    "selectionSetGraphQL": "string"
}

The field selectionSetList contains the list of the fields in the selection set. The field names include the full path to the field. (i.e.: field/childField). Note that alias fields will only be referenced by their alias name. The field selectionSetGraphQL is a string representation of the selection set formatted as GraphQL schema definition language (SDL).

To give you an idea of how the info object works and how to use it with your AppSync resolvers, let’s take a look at this simple schema that consists of posts and comments, where a post can have multiple comments associated with it:

type Post {
  id: ID!
  content: String!
  comments: [Comment]!
}

type Comment {
  id: ID!
  content: String!
  postID: ID!
}

type Query {
  getPost(id: String!): Post
}

The application data is stored in a SQL database hosted with the Amazon Relational Database Service (RDS). The Post and Comment entries are respectively stored in the tables Posts and Comments. The getPost query uses a Lambda resolver that invokes an AWS Lambda function to connect and make calls to the backend database.

As a side note, using Lambda to connect to RDS instances can be made even better with the newly released RDS Proxy which optimizes database connections by managing connection pooling, as well as Lambda support for provisioned concurrency to address any concerns related to cold starts. GraphQL clients can now leverage these new capabilities when accessing SQL data backed by AppSync APIs and Lambda functions, however this is outside of the scope of this article.

Clients interacting with the AppSync API defined by the schema above can get a post with its list of comments using the following GraphQL query:

query GetPost($id: ID!)  {
   getPost(id: $id) {
    id
    content
    comments {
      id
      content
    }
  }
}

and related variables:

{ "id": "<id>" }

Previously, without the info object, there was no way for the getPost resolver to know if fetching comments was necessary. The resolver had to always fetch comments (even if not requested) or another resolver had to be attached to the comments field of the Post type. Now when a request to the query getPost is made, the AppSync resolver is called and the following info object is available.

{
   "fieldName": "getPost",
   "parentTypeName": "Query",
   "variables": { "id": "<id>" },
   "selectionSetList": [
     "id",
     "content",
     "comments",
     "comments/id",
     "comments/content"
   ],
   "selectionSetGraphQL": "{\n  id\n  content\n  comments {\n    id\n    content\n  }\n}"
 }

You can pass selectionSetList and selectionSetGraphQL to your Lambda function in your resolver by adding them to the payload:

{
  "version" : "2017-02-28",
  "operation": "Invoke",
  "payload": {
    "arguments": $utils.toJson($ctx.args),
    "selectionSetList": $utils.toJson($ctx.info.selectionSetList),
    "selectionSetGraphQL": $utils.toJson($ctx.info.selectionSetGraphQL)
  }
}

Note that you must specifically reference the selection set items if you want them to be available in the payload. They will not be available if you only pass the info object directly with $utils.toJson($ctx.info).

Now your Lambda function can use specific business logic to make an optimized call to your backend. For example, Lambda can check for the existence of the comments field. If present, the Lambda function can then query the database for the post with a matching ID and for all the comments with a postId equal to the post’s ID. The code below shows how selectionSetList can be used in a Lambda function:

const mysql = require('mysql')
const connection = mysql.createConnection({...}) // connect to the database

function executeSQL(sql, fields) {
  console.log('Executing SQL:', sql, fields)
  return new Promise((resolve, reject) => {
    connection.query(sql, fields, (err, results) => {
      if (err) {
        return reject(err)
      }
      return resolve(results)
    })
  })
}

exports.handler = async event => {
  const id = event.arguments.id
  const fields = event.selectionSetList.filter(f => !f.startsWith('comments'))
  const postQuery = `SELECT ?? FROM ?? where id = ?`
  const results = await executeSQL(postQuery, [fields, 'Posts', id])

  if (results.length === 0) {
    return null
  }

  const post = results[0]
  var comments = []
  const found = event.selectionSetList.find(f => f === 'comments')
  if (found) {
    const fields = event.selectionSetList
      .filter(f => f.startsWith('comments/'))
      .map(f => f.split('/')[1])
    const commentQuery = `SELECT ?? FROM ?? where postId = ?`
    comments = await executeSQL(commentQuery, [fields, 'Comments', id])
  }
  return { ...post, comments }
}

Next steps

The introduction of the info object opens up the door to new improvements and further optimization of your AppSync resolvers business logic. We’re looking forward to seeing how AppSync users can leverage it.

If you have any questions or want to submit feedback about this new feature, make sure to visit the AWS AppSync Forum. You can find more information about the info object by visiting the AppSync Resolver Context reference in the documentation.