Resolvers are the glue code between the GraphQL world and the configured data source. For every resolver, two pieces of code are run: the request function that runs before the data source is called and it gets the parameters for the query, and the response function after the data source returns with its result.
The language for resolvers is a constrained version of Javascript. It supports only the basics, such as arrays, functions, variables, template literals, String operations. But it is missing a lot of more advanced features: there is no support for try..catch, async functions, regular expressions, to name just a few. Also, the functions must be in a single file, and must return in a few milliseconds. In practice, these functions are only capable of doing basic transformations on the data source request and response.
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
:
export function request(ctx) {
return {
version: "2018-05-29",
operation: "GetItem",
key: {
id: {S: "group1"}
}
};
}
The function can use a field argument to construct the query with a dynamic id:
export function request(ctx) {
return {
version: "2018-05-29",
operation: "GetItem",
key: {
id: {S: ctx.args.id}
}
};
}
The context object contains a lot of useful fields that contain information about the request such as the field, the source, the identity of the caller. We'll take a deeper look into what is available in the Context chapter. Also, AppSync provides built-in utility functions for common tasks, such as generating a UUID, converting between date formats, and a few others. We'll see a few examples in the Utils chapter.
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.
On the Management Console, resolvers are under the Schema entry:
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 resolver code:
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.
To use them, you need to import from the @aws-appsync/utils
package:
import {util} from "@aws-appsync/utils";
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.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:
return {
version: "2018-05-29",
operation: "PutItem",
key: {
id: {S: util.autoId()}
},
attributeValues: {
name: {S: ctx.args.name}
}
}