--- 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