Skip to content

Instantly share code, notes, and snippets.

@adrianhall
Created April 13, 2018 16:01
Show Gist options
  • Save adrianhall/50e9fdf08e7a7e52d3ab0f01467b72f7 to your computer and use it in GitHub Desktop.
Save adrianhall/50e9fdf08e7a7e52d3ab0f01467b72f7 to your computer and use it in GitHub Desktop.
An example CloudFormation template for AWS AppSync
---
Description: AWSAppSync DynamoDB Example
Resources:
GraphQLApi:
Type: "AWS::AppSync::GraphQLApi"
Properties:
Name: AWSAppSync DynamoDB Example
AuthenticationType: AWS_IAM
PostDynamoDBTableDataSource:
Type: "AWS::AppSync::DataSource"
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
Name: PostDynamoDBTable
Description: The Post DynamoDB table in us-west-2
Type: AMAZON_DYNAMODB
ServiceRoleArn: "arn:aws:iam::YOUR_ACCOUNT_ID:role/AppSyncTutorialAmazonDynamoDBRole"
DynamoDBConfig:
AwsRegion: "us-west-2"
TableName: "AppSyncTutorial-Post"
QueryGetPostResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Query
FieldName: getPost
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "GetItem",
"key" : {
"id" : $util.dynamodb.toDynamoDBJson($ctx.args.id)
}
}
ResponseMappingTemplate: "$utils.toJson($ctx.result)"
QueryAllPostResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Query
FieldName: allPost
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "Scan",
#if( $ctx.args.count )
"limit": $ctx.args.count,
#end
#if( ${ctx.args.nextToken} )
"nextToken": "${ctx.args.nextToken}"
#end
}
ResponseMappingTemplate: |
{
"posts": $utils.toJson($ctx.result.items),
#if( ${ctx.result.nextToken} )
"nextToken": "${ctx.result.nextToken}",
#end
}
QueryAllPostsByAuthorResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Query
FieldName: allPostsByAuthor
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "Query",
"index" : "author-index",
"query" : {
"expression": "author = :author",
"expressionValues" : {
":author" : $util.dynamodb.toDynamoDBJson($ctx.args.author)
}
},
#if( $ctx.args.count )
"limit": $ctx.args.count,
#end
#if( ${ctx.args.nextToken} )
"nextToken": "${ctx.args.nextToken}",
#end
}
ResponseMappingTemplate: |
{
"posts": $utils.toJson($ctx.result.items),
#if( ${ctx.result.nextToken} )
"nextToken": "${ctx.result.nextToken}",
#end
}
QueryAllPostsByTagResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Query
FieldName: allPostsByTag
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "Scan",
"filter": {
"expression": "contains (tags, :tag)",
"expressionValues": {
":tag": $util.dynamodb.toStringJson($ctx.args.tag)
}
},
#if( $ctx.args.count )
"limit": $ctx.args.count,
#end
#if( ${ctx.args.nextToken} )
"nextToken": "${ctx.args.nextToken}"
#end
}
ResponseMappingTemplate: |
{
"posts": $utils.toJson($ctx.result.items),
#if( ${ctx.result.nextToken} )
"nextToken": "${ctx.result.nextToken}",
#end
}
MutationaddPostResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: addPost
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
#set( $d = $util.dynamodb )
#set( $values = $d.toMapValues($ctx.args))
$!{values.put("ups", $d.toNumber(1))}
$!{values.put("downs", $d.toNumber(0))}
$!{values.put("version", $d.toNumber(1))}
$!{values.put("created", $d.toDynamoDB($util.time.nowISO8601()))}
$!{values.put("lastUpdated", $values.get("created"))}
{
"version" : "2017-02-28",
"operation" : "PutItem",
"key" : {
"id" : $d.toStringJson($utils.autoId())
},
"attributeValues" : $util.toJson($values),
}
ResponseMappingTemplate: "$util.toJson($ctx.result)"
MutationAddCommentResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: addComment
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : $util.dynamodb.toStringJson($ctx.args.id)
},
"update" : {
"expression" : "SET comments = list_append(if_not_exists(comments, :emptyList), :newComment), lastUpdated = :lastUpdated ADD version :plusOne",
"expressionValues" : {
":emptyList": $util.dynamodb.toListJson([]),
":newComment" : $util.dynamodb.toListJson([$util.map.copyAndRetainAllKeys($ctx.args, ["author","comment"])]),
":plusOne" : $util.dynamodb.toNumberJson(1),
":lastUpdated" : $util.dynamodb.toDynamoDBJson($util.time.nowISO8601())
}
},
"condition" : {
"expression" : "attribute_exists(id)"
},
}
ResponseMappingTemplate: "$util.toJson($ctx.result)"
MutationAddTagResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: addTag
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : $util.dynamodb.toStringJson($ctx.args.id)
},
"update" : {
"expression" : "ADD tags :tags, version :plusOne SET lastUpdated = :lastUpdated",
"expressionValues" : {
":tags" : $util.dynamodb.toStringSetJson([$ctx.args.tag]),
":plusOne" : $util.dynamodb.toNumberJson(1),
":lastUpdated" : $util.dynamodb.toDynamoDBJson($util.time.nowISO8601()),
}
},
"condition" : {
"expression" : "attribute_exists(id)"
},
}
ResponseMappingTemplate: "$util.toJson($ctx.result)"
MutationRemoveTagResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: removeTag
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : $util.dynamodb.toStringJson($ctx.args.id)
},
"update" : {
"expression" : "DELETE tags :tags ADD version :plusOne SET lastUpdated = :lastUpdated",
"expressionValues" : {
":tags" : $util.dynamodb.toStringSetJson([$ctx.args.tag]),
":plusOne" : $util.dynamodb.toNumberJson(1),
":lastUpdated" : $util.dynamodb.toDynamoDBJson($util.time.nowISO8601()),
}
},
"condition" : {
"expression" : "attribute_exists(id)"
},
}
ResponseMappingTemplate: "$util.toJson($ctx.result)"
MutationDeletePostResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: deletePost
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "DeleteItem",
"key": {
"id": $util.dynamodb.toStringJson($ctx.args.id)
},
#if( $ctx.args.containsKey("expectedVersion") )
"condition" : {
"expression" : "attribute_not_exists(id) OR version = :expectedVersion",
"expressionValues" : {
":expectedVersion" : $util.dynamodb.toNumberJson($ctx.args.expectedVersion)
}
},
#end
}
ResponseMappingTemplate: "$util.toJson($ctx.result)"
MutationUpvotePostResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: upvotePost
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : $util.dynamodb.toStringJson($ctx.args.id)
},
"update" : {
"expression" : "ADD ups :plusOne, version :plusOne SET lastUpdated = :lastUpdated",
"expressionValues" : {
":plusOne" : $util.dynamodb.toNumberJson(1),
":lastUpdated" : $util.dynamodb.toDynamoDBJson($util.time.nowISO8601())
}
},
"condition" : {
"expression" : "attribute_exists(id)"
},
}
ResponseMappingTemplate: "$util.toJson($ctx.result)"
MutationDownvotePostResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: downvotePost
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : $util.dynamodb.toStringJson($ctx.args.id)
},
"update" : {
"expression" : "ADD downs :plusOne, version :plusOne SET lastUpdated = :lastUpdated",
"expressionValues" : {
":plusOne" : $util.dynamodb.toNumberJson(1),
":lastUpdated" : $util.dynamodb.toDynamoDBJson($util.time.nowISO8601()),
}
},
"condition" : {
"expression" : "attribute_exists(id)"
},
}
ResponseMappingTemplate: "$util.toJson($ctx.result)"
MutationUpdatePostResolver:
Type: "AWS::AppSync::Resolver"
DependsOn: Schema
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
TypeName: Mutation
FieldName: updatePost
DataSourceName: !GetAtt PostDynamoDBTableDataSource.Name
RequestMappingTemplate: |
#set( $ddb = $util.dynamodb )
{
"version" : "2017-02-28",
"operation" : "UpdateItem",
"key" : {
"id" : $ddb.toDynamoDBJson($ctx.args.id)
},
## Set up some space to keep track of things we're updating **
#set( $expNames = {} )
#set( $expValues = {} )
#set( $expSet = {} )
#set( $expAdd = {} )
#set( $expRemove = [] )
## Increment "version" by 1 **
$!{expAdd.put("version", ":one")}
$!{expValues.put(":one", $ddb.toDynamoDB(1))}
## Set the "lastUpdated" timestamp **
$!{expSet.put("lastUpdated", ":lastUpdated")}
$!{expValues.put(":lastUpdated", $ddb.toDynamoDB($util.time.nowISO8601()))}
## Iterate through each argument, skipping "id" and "expectedVersion" **
#foreach( $entry in $util.map.copyAndRemoveAllKeys($ctx.args, ["id","expectedVersion"]).entrySet() )
#if( $util.isNull($entry.value) )
## If the argument is set to "null", then remove that attribute from the item in DynamoDB **
#set( $discard = ${expRemove.add("#${entry.key}")} )
$!{expNames.put("#${entry.key}", "${entry.key}")}
#else
## Otherwise set (or update) the attribute on the item in DynamoDB **
$!{expSet.put("#${entry.key}", ":${entry.key}")}
$!{expNames.put("#${entry.key}", "${entry.key}")}
$!{expValues.put(":${entry.key}", $ddb.toDynamoDB($entry.value))}
#end
#end
## Start building the update expression, starting with attributes we're going to SET **
#set( $expression = "" )
#if( !${expSet.isEmpty()} )
#set( $expression = "SET" )
foreach( $entry in $expSet.entrySet() )
set( $expression = "${expression} ${entry.key} = ${entry.value}" )
if ( $foreach.hasNext )
set( $expression = "${expression}," )
end
end
end
## Continue building the update expression, adding attributes we're going to ADD **
#if( !${expAdd.isEmpty()} )
#set( $expression = "${expression} ADD" )
#foreach( $entry in $expAdd.entrySet() )
#set( $expression = "${expression} ${entry.key} ${entry.value}" )
#if ( $foreach.hasNext )
#set( $expression = "${expression}," )
#end
#end
#end
## Continue building the update expression, adding attributes we're going to REMOVE **
#if( !${expRemove.isEmpty()} )
#set( $expression = "${expression} REMOVE" )
#foreach( $entry in $expRemove )
#set( $expression = "${expression} ${entry}" )
#if ( $foreach.hasNext )
#set( $expression = "${expression}," )
#end
#end
#end
## Finally, write the update expression into the document, along with any expressionNames and expressionValues **
"update" : {
"expression" : "${expression}",
#if( !${expNames.isEmpty()} )
"expressionNames" : $utils.toJson($expNames),
#end
#if( !${expValues.isEmpty()} )
"expressionValues" : $utils.toJson($expValues),
#end
},
"condition" : {
"expression" : "attribute_exists(id) and version = :expectedVersion",
"expressionValues" : {
":expectedVersion" : $ddb.toDynamoDBJson($context.arguments.expectedVersion)
}
}
}
ResponseMappingTemplate: "$util.toJson($ctx.result)"
Schema:
Type: "AWS::AppSync::GraphQLSchema"
Properties:
ApiId: !GetAtt GraphQLApi.ApiId
Definition: |
type Comment {
author: String!
comment: String!
}
type Mutation {
addComment(id: ID!, author: String!, comment: String!): Post
addTag(id: ID!, tag: String!): Post
removeTag(id: ID!, tag: String!): Post
deletePost(id: ID!, expectedVersion: Int): Post
upvotePost(id: ID!): Post
downvotePost(id: ID!): Post
updatePost(
id: ID!,
author: String,
title: String,
content: String,
url: String,
expectedVersion: Int!
): Post
addPost(
author: String!,
title: String!,
content: String!,
url: String!
): Post!
}
type PaginatedPosts {
posts: [Post!]!
nextToken: String
}
type Post {
id: ID!
author: String
title: String
content: String
url: String
ups: Int!
downs: Int!
version: Int!
tags: [String!]
comments: [Comment!]
created: String
lastUpdated: String
}
type Query {
allPostsByTag(tag: String!, count: Int, nextToken: String): PaginatedPosts!
allPostsByAuthor(author: String!, count: Int, nextToken: String): PaginatedPosts!
allPost(count: Int, nextToken: String): PaginatedPosts!
getPost(id: ID!): Post
}
schema {
query: Query
mutation: Mutation
}
Outputs:
GraphQLApiARN:
Description: The App ID of the GraphQL endpoint.
Value: !Ref GraphQLApi
GraphQLApiId:
Description: The App ID of the GraphQL endpoint.
Value: !GetAtt GraphQLApi.ApiId
GraphQLApiEndpoint:
Description: The URL for the GraphQL endpoint.
Value: !GetAtt GraphQLApi.GraphQLUrl
PostDynamoDBTableDataSourceARN:
Description: The ARN for the Post DynamoDB table DataSource.
Value: !Ref PostDynamoDBTableDataSource
PostDynamoDBTableDataSourceName:
Description: The ARN for the Post DynamoDB table DataSource.
Value: !GetAtt PostDynamoDBTableDataSource.Name
@simon998yang
Copy link

when I try, addPost just works. but updatePost throws exception , not sure what's wrong.

{
"data": {
"updatePost": null
},
"errors": [
{
"path": [
"updatePost"
],
"data": null,
"errorType": "MappingTemplate",
"errorInfo": null,
"locations": [
{
"line": 50,
"column": 3,
"sourceName": null
}
],
"message": "Encountered "" at velocity[line 80, column 2]\nWas expecting one of:\n "(" ...\n ...\n <ESCAPE_DIRECTIVE> ...\n <SET_DIRECTIVE> ...\n "##" ...\n "\\\\" ...\n "\\" ...\n ...\n "#" ...\n "#" ...\n "]]#" ...\n <STRING_LITERAL> ...\n ...\n <IF_DIRECTIVE> ...\n <ELSEIF_DIRECTIVE> ...\n <ELSE_DIRECTIVE> ...\n <INTEGER_LITERAL> ...\n <FLOATING_POINT_LITERAL> ...\n ...\n <BRACKETED_WORD> ...\n ...\n ...\n "{" ...\n "}" ...\n <EMPTY_INDEX> ...\n "
}
]
}

@Shehu-Yakubu
Copy link

I used this type of update resolver as well in my application but didn't work too.

@carlosrojaso
Copy link

Support DeltaSync?

@adrianhall
Copy link
Author

I suspect the template has changed. I no longer work on AppSync, so I cannot assist further. I suggest reaching out to @dabit3 if you need to discuss updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment