I'm using AWS AppSync for the first time and trying to understand DynamoDB resolvers. My schema includes three metadata attributes that I'd like to be set automatically: createdAt: AWSTimestamp
, updatedAt: AWSTimestamp
, and version: Integer
. I'm able to set their initial values in the creatItem
resolver, but I'm having trouble understanding how the updateItem
resolver works.
For reference, this is all at a URL similar to this, on AWS: https://console.aws.amazon.com/appsync/home#/[apiId]/v1/schema/Mutation/updateItem/resolver
Here's the code I'm at now, after lots of trial and error:
##Update versioning attributes
#if( !${expNames.isEmpty()} )
$!{expSet.put("#updatedAt", ":updatedAt")}
$!{expNames.put("#updatedAt", "updatedAt")}
$!{expValues.put(":updatedAt", $util.dynamodb.toDynamoDB($util.time.nowEpochMilliSeconds() ))}
$!{expSet.put("#version", ":version")}
$!{expNames.put("#version", "version")}
$!{expValues.put(":version", $util.dynamodb.toNumber(99))}
#end
And here's the current error:
{
"data": {
"updateItem": null
},
"errors": [
{
"path": [
"updateItem"
],
"data": null,
"errorType": "DynamoDB:AmazonDynamoDBException",
"errorInfo": null,
"locations": [
{
"line": 74,
"column": 3,
"sourceName": null
}
],
"message": "Value provided in ExpressionAttributeNames unused in expressions: keys: {#updatedAt, #version} (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException; Request ID: YADDAYADDAYADDA)"
}
]
}
You'll also notice that I'm setting version to a static value of 99 rather than to n+1
. That's the next thing for me to figure out, but if you have any tips, I'd be happy to here them.
Here's the full resolver, including all of AWS' boilderplate:
{
"version": "2017-02-28",
"operation": "UpdateItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id),
},
## Set up some space to keep track of things we're updating **
#set( $expNames = {} )
#set( $expValues = {} )
#set( $expSet = {} )
#set( $expAdd = {} )
#set( $expRemove = [] )
## Iterate through each argument, skipping keys **
#foreach( $entry in $util.map.copyAndRemoveAllKeys($ctx.args.input, ["id"]).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}", $util.dynamodb.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
##Update versioning attributes
#if( !${expNames.isEmpty()} )
$!{expSet.put("#updatedAt", ":updatedAt")}
$!{expNames.put("#updatedAt", "updatedAt")}
$!{expValues.put(":updatedAt", $util.dynamodb.toDynamoDB($util.time.nowEpochMilliSeconds() ))}
$!{expSet.put("#version", ":version")}
$!{expNames.put("#version", "version")}
$!{expValues.put(":version", $util.dynamodb.toNumber(99))}
#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)",
"expressionNames": {
"#id": "id",
},
}
}
Thanks in advance!
Edit: I tried to generalize the table name above, but here's the actual schema, as requested:
input CreateMapInput {
title: String!
imageUrl: AWSURL!
center: AWSJSON
zoom: Float
bearing: Float
imageType: String
sourceSize: AWSJSON
cropSize: AWSJSON
cropAnchor: AWSJSON
corners: AWSJSON
country: String
city: String
published: Boolean
}
input DeleteMapInput {
id: ID!
}
type Map {
id: ID!
title: String!
imageUrl: AWSURL!
center: AWSJSON
zoom: Float
bearing: Float
imageType: String
sourceSize: AWSJSON
cropSize: AWSJSON
cropAnchor: AWSJSON
corners: AWSJSON
country: String
city: String
published: Boolean
createdAt: AWSTimestamp
updatedAt: AWSTimestamp
version: Int
}
type MapConnection {
items: [Map]
nextToken: String
}
type Mutation {
createMap(input: CreateMapInput!): Map
updateMap(input: UpdateMapInput!): Map
deleteMap(input: DeleteMapInput!): Map
}
type Query {
getMap(id: ID!): Map
listMaps(filter: TableMapFilterInput, limit: Int, nextToken: String): MapConnection
}
type Subscription {
onCreateMap(
id: ID,
title: String,
imageUrl: AWSURL,
center: AWSJSON,
zoom: Float
): Map
@aws_subscribe(mutations: ["createMap"])
onUpdateMap(
id: ID,
title: String,
imageUrl: AWSURL,
center: AWSJSON,
zoom: Float
): Map
@aws_subscribe(mutations: ["updateMap"])
onDeleteMap(
id: ID,
title: String,
imageUrl: AWSURL,
center: AWSJSON,
zoom: Float
): Map
@aws_subscribe(mutations: ["deleteMap"])
}
input TableBooleanFilterInput {
ne: Boolean
eq: Boolean
}
input TableFloatFilterInput {
ne: Float
eq: Float
le: Float
lt: Float
ge: Float
gt: Float
contains: Float
notContains: Float
between: [Float]
}
input TableIDFilterInput {
ne: ID
eq: ID
le: ID
lt: ID
ge: ID
gt: ID
contains: ID
notContains: ID
between: [ID]
beginsWith: ID
}
input TableIntFilterInput {
ne: Int
eq: Int
le: Int
lt: Int
ge: Int
gt: Int
contains: Int
notContains: Int
between: [Int]
}
input TableMapFilterInput {
id: TableIDFilterInput
title: TableStringFilterInput
imageUrl: TableStringFilterInput
zoom: TableFloatFilterInput
bearing: TableFloatFilterInput
imageType: TableStringFilterInput
country: TableStringFilterInput
city: TableStringFilterInput
published: TableBooleanFilterInput
createdAt: TableIntFilterInput
updatedAt: TableIntFilterInput
version: TableIntFilterInput
}
input TableStringFilterInput {
ne: String
eq: String
le: String
lt: String
ge: String
gt: String
contains: String
notContains: String
between: [String]
beginsWith: String
}
input UpdateMapInput {
id: ID!
title: String
imageUrl: AWSURL
center: AWSJSON
zoom: Float
bearing: Float
imageType: String
sourceSize: AWSJSON
cropSize: AWSJSON
cropAnchor: AWSJSON
corners: AWSJSON
country: String
city: String
published: Boolean
}
And also, the query & variables:
mutation updateMap($updatemapinput: UpdateMapInput!) {
updateMap(input: $updatemapinput) {
id
title
imageUrl
center
zoom
bearing
imageType
sourceSize
cropSize
cropAnchor
corners
country
city
published
createdAt
updatedAt
version
}
}
{
"updatemapinput": {
"id": "e1be0d61-9e8d-4f85-b0ed-91a23527f3e7",
"zoom": 14
}
}
The problem here is that you are not updating the expression variable with the attributes you are setting.
If you update your code to this, it should work.
##Update versioning attributes
#if( !${expNames.isEmpty()} )
#set( $expression = "${expression}, SET updatedAt = $util.time.nowEpochMilliSeconds()" )
$!{expSet.put("#updatedAt", ":updatedAt")}
$!{expNames.put("#updatedAt", "updatedAt")}
$!{expValues.put(":updatedAt", $util.dynamodb.toDynamoDB($util.time.nowEpochMilliSeconds() ))}
#set( $expression = "${expression}, SET version = $util.dynamodb.toNumber(99)" )
$!{expSet.put("#version", ":version")}
$!{expNames.put("#version", "version")}
$!{expValues.put(":version", $util.dynamodb.toNumber(99))}
#end
Another option I would recommend is to insert the values in the input at the beginning of the template, in that way, you don't have to deal with updating the expression variable. the previous can be achieved in this way.
#set($ctx.args.input['updatedAt'] = $util.time.nowEpochMilliSeconds())
#set($ctx.args.input['version'] = 99)
and you would have to remove the '##Update versioning attributes' code section that you added to the template.