diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index 7349a744d..beb4d002c 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -38,6 +38,12 @@ class DynamoType(object): def __repr__(self): return "DynamoType: {0}".format(self.to_json()) + def add(self, dyn_type): + if self.type == "SS": + self.value.append(dyn_type.value) + if self.type == "N": + self.value = str(int(self.value) + int(dyn_type.value)) + def to_json(self): return {self.type: self.value} @@ -281,6 +287,20 @@ class Table(CloudFormationModel): except KeyError: return None + def update_item(self, hash_key, range_key, attr_updates): + item = self.get_item(hash_key, range_key) + if not item: + return None + + for attr, update in attr_updates.items(): + if update["Action"] == "PUT": + item.attrs[attr] = DynamoType(update["Value"]) + if update["Action"] == "DELETE": + item.attrs.pop(attr) + if update["Action"] == "ADD": + item.attrs[attr].add(DynamoType(update["Value"])) + return item + def get_cfn_attribute(self, attribute_name): from moto.cloudformation.exceptions import UnformattedGetAttTemplateException @@ -360,5 +380,15 @@ class DynamoDBBackend(BaseBackend): return table.delete_item(hash_key, range_key) + def update_item(self, table_name, hash_key_dict, range_key_dict, attr_updates): + table = self.tables.get(table_name) + if not table: + return None + + hash_key = DynamoType(hash_key_dict) + range_key = DynamoType(range_key_dict) if range_key_dict else None + + return table.update_item(hash_key, range_key, attr_updates) + dynamodb_backend = DynamoDBBackend() diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py index 34ab8f187..f9c38408b 100644 --- a/moto/dynamodb/responses.py +++ b/moto/dynamodb/responses.py @@ -275,3 +275,22 @@ class DynamoHandler(BaseResponse): else: er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" return self.error(er) + + def update_item(self): + name = self.body["TableName"] + key = self.body["Key"] + hash_key = key["HashKeyElement"] + range_key = key.get("RangeKeyElement") + updates = self.body["AttributeUpdates"] + return_values = self.body.get("ReturnValues", "") # noqa + + item = dynamodb_backend.update_item(name, hash_key, range_key, updates) + + if item: + item_dict = item.to_json() + item_dict["ConsumedCapacityUnits"] = 0.5 + + return dynamo_json_dump(item_dict) + else: + er = "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException" + return self.error(er) diff --git a/tests/test_dynamodb/test_server.py b/tests/test_dynamodb/test_server.py index 310643628..aa70373d8 100644 --- a/tests/test_dynamodb/test_server.py +++ b/tests/test_dynamodb/test_server.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +import json import sure # noqa import moto.server as server @@ -18,3 +19,169 @@ def test_table_list(): headers = {"X-Amz-Target": "TestTable.ListTables"} res = test_client.get("/", headers=headers) res.data.should.contain(b"TableNames") + + +def test_update_item(): + backend = server.create_backend_app("dynamodb") + test_client = backend.test_client() + + create_table(test_client) + + headers, res = put_item(test_client) + + # UpdateItem + headers["X-Amz-Target"] = "DynamoDB_20111205.UpdateItem" + request_body = { + "TableName": "Table1", + "Key": { + "HashKeyElement": {"S": "customer"}, + "RangeKeyElement": {"N": "12341234"}, + }, + "AttributeUpdates": {"new_att": {"Value": {"SS": ["val"]}, "Action": "PUT"}}, + } + res = test_client.post("/", headers=headers, json=request_body) + + # UpdateItem + headers["X-Amz-Target"] = "DynamoDB_20111205.UpdateItem" + request_body = { + "TableName": "Table1", + "Key": { + "HashKeyElement": {"S": "customer"}, + "RangeKeyElement": {"N": "12341234"}, + }, + "AttributeUpdates": {"new_n": {"Value": {"N": "42"}, "Action": "PUT"}}, + } + res = test_client.post("/", headers=headers, json=request_body) + res = json.loads(res.data) + + res["ConsumedCapacityUnits"].should.equal(0.5) + res["Attributes"].should.equal( + { + "hkey": "customer", + "name": "myname", + "rkey": "12341234", + "new_att": ["val"], + "new_n": "42", + } + ) + + # UpdateItem - multiples + headers["X-Amz-Target"] = "DynamoDB_20111205.UpdateItem" + request_body = { + "TableName": "Table1", + "Key": { + "HashKeyElement": {"S": "customer"}, + "RangeKeyElement": {"N": "12341234"}, + }, + "AttributeUpdates": { + "new_n": {"Value": {"N": 7}, "Action": "ADD"}, + "new_att": {"Value": {"S": "val2"}, "Action": "ADD"}, + "name": {"Action": "DELETE"}, + }, + } + res = test_client.post("/", headers=headers, json=request_body) + res = json.loads(res.data) + + res["ConsumedCapacityUnits"].should.equal(0.5) + res["Attributes"].should.equal( + { + "hkey": "customer", + "rkey": "12341234", + "new_att": ["val", "val2"], + "new_n": "49", + } + ) + + # GetItem + headers["X-Amz-Target"] = "DynamoDB_20111205.GetItem" + request_body = { + "TableName": "Table1", + "Key": { + "HashKeyElement": {"S": "customer"}, + "RangeKeyElement": {"N": "12341234"}, + }, + } + res = test_client.post("/", headers=headers, json=request_body) + res = json.loads(res.data) + res["Item"].should.have.key("new_att").equal({"SS": ["val", "val2"]}) + res["Item"].should.have.key("new_n").equal({"N": "49"}) + res["Item"].shouldnt.have.key("name") + + +def test_update_item_that_doesnt_exist(): + backend = server.create_backend_app("dynamodb") + test_client = backend.test_client() + + create_table(test_client) + + # UpdateItem + headers = {"X-Amz-Target": "DynamoDB_20111205.UpdateItem"} + request_body = { + "TableName": "Table1", + "Key": { + "HashKeyElement": {"S": "customer"}, + "RangeKeyElement": {"N": "12341234"}, + }, + "AttributeUpdates": {"new_att": {"Value": {"SS": ["val"]}, "Action": "PUT"}}, + } + res = test_client.post("/", headers=headers, json=request_body) + res.status_code.should.equal(400) + json.loads(res.data).should.equal( + {"__type": "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"} + ) + + +def test_update_item_in_nonexisting_table(): + backend = server.create_backend_app("dynamodb") + test_client = backend.test_client() + + # UpdateItem + headers = {"X-Amz-Target": "DynamoDB_20111205.UpdateItem"} + request_body = { + "TableName": "nonexistent", + "Key": { + "HashKeyElement": {"S": "customer"}, + "RangeKeyElement": {"N": "12341234"}, + }, + "AttributeUpdates": {"new_att": {"Value": {"SS": ["val"]}, "Action": "PUT"}}, + } + res = test_client.post("/", headers=headers, json=request_body) + res.status_code.should.equal(400) + json.loads(res.data).should.equal( + {"__type": "com.amazonaws.dynamodb.v20111205#ResourceNotFoundException"} + ) + + +def put_item(test_client, rkey="12341234"): + headers = { + "X-Amz-Target": "DynamoDB_20111205.PutItem", + "Content-Type": "application/x-amz-json-1.0", + } + request_body = { + "TableName": "Table1", + "Item": { + "hkey": {"S": "customer"}, + "rkey": {"N": rkey}, + "name": {"S": "myname"}, + }, + "ReturnValues": "ALL_OLD", + } + res = test_client.post("/", headers=headers, json=request_body) + res = json.loads(res.data) + return headers, res + + +def create_table(test_client): + headers = { + "X-Amz-Target": "DynamoDB_20111205.CreateTable", + "Content-Type": "application/x-amz-json-1.0", + } + request_body = { + "TableName": "Table1", + "KeySchema": { + "HashKeyElement": {"AttributeName": "hkey", "AttributeType": "S"}, + "RangeKeyElement": {"AttributeName": "rkey", "AttributeType": "N"}, + }, + "ProvisionedThroughput": {"ReadCapacityUnits": 5, "WriteCapacityUnits": 10}, + } + return test_client.post("/", headers=headers, json=request_body)