Building AWS AppSync Pipeline Resolvers with AWS CloudFormation
Among the many updates from AWS AppSync over the past few months, the addition of Pipeline Resolvers was particularly exciting. Pipeline Resolvers allow us to build a resolver from a series of reusable functions, each of which can query a separate data source. This approach can be useful in performing an authorization check or resolving a batch request across data sources.
For example, I recently built a Pipeline Resolver composed of two functions:
- Retrieves a list of recent items, each identified by a unique ID, from Amazon ElastiCache via a AWS Lambda data source.
- Retrieves each of the items from an Amazon DynamoDB data source using BatchGetItem. Using a Pipeline Resolver allowed for separation of functionality as well as composition via AppSync.
Instead of reviewing how I built the resolver, in this post, I will share how I *built it using *AWS CloudFormation. If you are interested in an overview of Pipeline Resolvers or an example implementation, see the Resources section below.
Building a Pipeline Resolver with CloudFormation
When building a Pipeline Resolver in CloudFormation, we specify a resolver as well as the functions that compose that resolver. The resolver and its composite functions will contain request and response mapping templates, but only the functions will have a data source. To demonstrate, we will implement theListPosts resolver found in Nader Dabit’s Intro to AWS AppSync Pipeline Functions:
First, we’ll implement the resolver. We’ll assume the AppSync API and other applicable resources have already been created:
ListPostsQueryResolver:
Type: AWS::AppSync::Resolver
DependsOn: MySchema
Properties:
ApiId: !GetAtt MyApi.ApiId
TypeName: Query
FieldName: listPosts
# configure this resolver as a pipeline
Kind: PIPELINE
PipelineConfig:
Functions:
- !GetAtt IsUserCallerFunction.FunctionId
- !GetAtt GetPostsFunction.FunctionId
# see Nader's article for details on these templates
RequestMappingTemplate: |
$util.qr($ctx.stash.put("callerId", $ctx.identity.sub))
$util.qr($ctx.stash.put("userId", $ctx.args.userId))
{}
ResponseMappingTemplate: |
$util.toJson($ctx.result)
Next, we will implement the first of two pipeline functions:
IsUserCallerFunction:
Type: AWS::AppSync::FunctionConfiguration
Properties:
ApiId: !GetAtt MyApi.ApiId
Name: is_user_caller
Description: Checks to see if the user is also the caller
DataSourceName: !GetAtt UsersDynamoDBDataSource.Name
FunctionVersion: "2018-05-29"
RequestMappingTemplate: |
#if($ctx.stash.callerId == $ctx.stash.userId)
#return($ctx.prev.result)
#else
{
"operation": "GetItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.stash.callerId),
}
}
#end
ResponseMappingTemplate: |
#if($ctx.result.level != "admin")
$util.error("User is not authorized to make this query")
#else
$util.toJson($ctx.result)
#end
Finally, we’ll add the second function:
GetPostsFunction:
Type: AWS::AppSync::FunctionConfiguration
Properties:
ApiId: !GetAtt MyApi.ApiId
Name: get_posts
Description: Scan table and retrieve items created by user
DataSourceName: !GetAtt PostsDynamoDBDataSource.Name
FunctionVersion: "2018-05-29"
RequestMappingTemplate: |
{
"operation" : "Scan",
"filter" : {
"expression" : "#userId = :userId",
"expressionNames" : {
"#userId" : "userId"
},
"expressionValues" : {
":userId" : $util.dynamodb.toDynamoDBJson($ctx.stash.userId)
}
},
"nextToken": $util.toJson($util.defaultIfNullOrBlank($ctx.args.nextToken, null))
}
ResponseMappingTemplate: |
#if($ctx.error)
$util.error($ctx.error.message, $ctx.error.type)
#end
{
"items": $util.toJson($ctx.result.items),
"nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))
}
After creating the CloudFormation stack, we can test the resolver via the AppSync Console. As usual, CloudFormation brings the value of reuse, portability, configurability.
Thanks for reading and please feel free to leave a comment with any questions.