You are viewing the preview version of this book
Click here for the full version.

Resolvers

Resolvers are the glue code between the GraphQL world and the configured data source. For every resolver, two pieces of code are run: one before the data source is called and it gets the parameters for the query, and the other after the data source returns with its result. The former is called the request mapping template, and the latter is the response mapping template.

As we've seen in the Data sources chapter, there are several data sources that AppSync supports, each with its parameters and return types. For example, the DynamoDB data source to retrieve an item needs a JSON input with a version, operation, and a key:

{
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": {
    "id": {"S": "group1"}
  }
}

The request mapping template can use a field argument to construct the above structure:

{
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": {
    "id": {"S": $util.toJson($ctx.args.id)}
  }
}

In VTL, references start with $, so $util.toJson calls the toJson function of util, and $ctx.args.id returns the args.id field of $ctx. In this code, the group ID comes from the GraphQL query for this field. This allows powerful control over how the field gets its value. In practice, most of the time goes into writing resolvers when working with an AppSync backend.

Resolving fields

A resolver is attached to a field and this concept comes from GraphQL specification and as such it does not depend on AppSync. We've covered this in the Resolvers chapter.

After the data source returns a value, the response mapping template runs and it gets the result or the error. This allows error handling and processing of the data returned. The result of the resolver must be a value that is suitable for the GraphQL response. This means either a scalar, such as a number if the schema defines an Int or a Float, a text if the type is String or ID, a special-formatted text for AWSDateTime, AWSDate, AWSJSON, and similar types. And when the field's type is an object then it can be anything, but usually a JSON object. In the last case, the next inner resolver will run and returns the expected scalar type as we've discussed in the Nested fields chapter.

Field resolving in GraphQL

On the Management Console, resolvers are under the Schema entry:

Resolvers are configured under the Schema menu

Here, you can attach, remove, and modify resolvers for each field. When a field has no resolver attached, then a trivial resolver is added by default. This brings down the number of resolvers you need to write when the database matches the GraphQL schema.

Then for each resolver, you can define the data source and the mapping templates:

Resolver page

VTL

While almost everything is in JSON, resolvers work on plain-text. Currently, VTL is the only supported language you can use and that requires some getting used to.

VTL, short for Velocity Template Language, is a Java-based open-source solution for templating. Templating engines get various inputs and a template file and produce textual output. AppSync then parses that to JSON and calls the data source, or returns as the value of the field.

VTL uses a template and input parameters to produce a textual output

This process of generating JSON using a text-based templating language sounds counter-intuitive and error-prone, and it definitely requires care, especially with escaping. It is very easy to end up with invalid JSON by inserting control characters (such as " and }). Also, it mixes Java into the mix, which is again an unfamiliar territory for most backend developers. The choice of using VTL is often seen as the biggest barrier to entry for AppSync.

For example, consider this resolver:

{
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": {
    "id": {"S": "$ctx.args.id"}
  }
}

When a query specifies the id it inserts it into the key:

{
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": {
    "id": {"S": "group1"}
  }
}

But what happens if the id contains a control character? For example, a client sends this query:

query MyQuery {
  groupById(id: "group1\"") {
    id
    name
  }
}

The result is no longer a valid JSON:

{
  "version": "2018-05-29",
  "operation": "GetItem",
  "key": {
    "id": {"S": "group1""}
  }
}

This is one example of how improper escaping can cause server-side errors.

Skipping VTL

By using Direct Lambda resolvers you can skip VTL altogether. This seems like a good approach to save the time and complexity of using a new language but then you miss all the built-in managed features of AppSync.

There is a push from the community to add a Javascript-based resolver architecture that provides a more familiar (and hopefully, safer) environment to develop resolvers. See this ticket for the current status.

Escaping

Because VTL generates text that is then parsed as JSON, escaping becomes an important topic. It is very easy to write resolvers that work for the "happy cases" but then break when a special character is present or something is missing.

For example, this resolver looks like working:

{
  "version": "2018-05-29",
  "payload": "$ctx.args.input"
}

And indeed, when called with a "normal" input, it returns it as expected:

query MyQuery {
  unescaped(input: "test")
}

The result:

{
  "data": {
    "unescaped": "test",
  }
}

But what happens when the input is missing? In VTL, when a value does not exist its name is inserted into the output:

query MyQuery {
  unescaped
}

The result is surprising:

{
  "data": {
    "unescaped": "$ctx.args.input"
  }
}

Even worse, if the input contains special characters, most notable a " then it can break out of the JSON property:

query MyQuery {
  unescaped(input: "\"")
}

In this case, it simply returns an error:

{
  "data": {
    "unescaped": null
  },
  "errors": [
    {
      "path": [
        "unescaped"
      ],
      "data": null,
      "errorType": "MappingTemplate",
      "errorInfo": null,
      "locations": [
        {
          "line": 6,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Unexpected character ('\"' (code 34)):
was expecting comma to separate Object entries\n at
[Source: (String)\"{\n\t\"version\": \"2018-05-29\",\n\t\
"payload\": \"\"\"\n}\n\"; line: 3, column: 16]"
    }
  ]
}

In the above case, the only downside is an error, but it can happen that an attacker could insert malicious parameters to a database query or a function call. We covered a vulnerability with the RDS data source where this is a huge problem in the Input sanitization chapter. But since VTL is text-based in various amounts it affects all data sources.

To handle escaping with a JSON output, use the $util.toJson function when inserting values potentially containing arbitrary values:

{
  "version": "2018-05-29",
  "payload": $util.toJson($ctx.args.input)
}

With this, all the above cases return the expected values:

query MyQuery {  
  basic: escaped(input: "test")
  missing: escaped
  quote: escaped(input: "\"")
}

The results:

{
  "data": {
    "basic": "test",
    "missing": null,
    "quote": "\""
  }
}

Utils

See the full list here.

AppSync provides a lot of utility function that you can use in the resolvers. The list is quite long, but as usual, a fraction of these functions pop up over and over. This is a list of these most useful ones.

$util.error throws an error where you can define various parts of the information included.

Some examples:

# throws an error with the given message
$util.error("Error from request template")

# throws an error with a message and a custom type
$util.error($ctx.result.body, "Request failed")

The $util.toJson converts some value to a JSON stringified representation. This is probably the most used function of all the available ones due to VTL is producing text that is then parsed back to JSON. This means the $util.toJson escapes values properly and thus makes them safe to insert in the response.

Some examples:

# returns a value from a data source
$util.toJson($ctx.result)

# inserts a value to a dynamodb request
"key" : {
  "id" : {"S": $util.toJson($ctx.args.id)}
}

The $util.autoId returns a random UUID string. This is useful for using as database identifiers. For example, this code inserts an item to a DynamoDB table with a random ID:

{
  "version" : "2018-05-29",
  "operation" : "PutItem",
  "key" : {
    "id" : {"S": "$util.autoId()"}
  },
  "attributeValues": {
    "name": {"S": $util.toJson($ctx.args.name)}
  }
}

The $util.unauthorized throws an access denied error and it immediately terminates the resolver. Useful when implementing access control in the resolvers. For example, this code allows retrieving a user when it matches the current user's name:

#if ($ctx.identity.username != $ctx.args.username)
  $util.unauthorized()
#else
  ...
#end

The $util.isNull and the $util.isNullOrBlank checks if a value is not supplied or is an empty text (in the latter case). Most useful in #if conditionals, this code fills the owner attribute if it was defined in the GraphQL query:


{
  "version" : "2018-05-29",
  "operation" : "PutItem",
  "key" : {
    "id": {"S": $util.toJson($util.autoId())}
  },
  "attributeValues": {
    #if(!$util.isNull($ctx.arguments.owner))
      "owner": {"S": $util.toJson($ctx.arguments.owner)},
    #end
    "title":
      {"S": $util.toJson($ctx.arguments.details.title)},
    "description":
      {"S": $util.toJson($ctx.arguments.details.description)},
    "severity":
      {"S": $util.toJson($ctx.arguments.details.severity)},
  }
}

The #return function provides a way to terminate the processing with a value. This is useful in several cases, for example, when a field can be null but can also be an object. In that case, if the field is null, getting the value from the database is unnecessary:

#if($util.isNull($ctx.source.owner))
  #return
#end
... fetch from database

And this code returns the prev.result if it is not null, useful in pipelines:

#if(!$util.isNull($ctx.prev.result))
  #return($ctx.prev.result)
#end

The $util.time.nowEpochMilliSeconds returns the current time. Useful when you need to store when a change happened. For example, this adds a created_at field to the new item:

{
  "version" : "2018-05-29",
  "operation" : "PutItem",
  "key" : {
    "id": {"S": $util.toJson($util.autoId())}
  },
  "attributeValues": {
    "created_at":
      {"N": $util.time.nowEpochMilliSeconds()},
    "title":
      {"S": $util.toJson($ctx.arguments.details.title)},
    "description":
      {"S": $util.toJson($ctx.arguments.details.description)}
  }
}

The $util.time.parseISO8601ToEpochMilliSeconds and the $util.time.epochMilliSecondsToISO8601 functions convert time to different formats. They come useful as the AWSDateTime requires the format defined in ISO8601 while in databases it's common to store timestamps. For example, this uses the None data source to convert a value stored in the database as a timestamp to an ISO8601 so that it can be returned to GraphQL:

There is more, but you've reached the end of this preview
Read this and all other chapters in full and get lifetime access to:
  • all future updates
  • full web-based access
  • PDF and Epub versions