Is it possible to atomically add several items to DynamoDB array with checking their occurance in it (to avoid duplication)? - amazon-web-services

I have email black list stored as one item in DynamoDB
// item example
{
id: "blackList" // PrimaryKey of item
list: [ "email_1#example.com", "email_2#example.com" ]
}
It is possible to add new email to the list and the same time check if it's not already presented in the list (to avoid duplication) by atomic update:
const email = "email_new#example.com";
const params = {
TableName: "myTable",
Key: {
id: "blackList"
},
AttributeUpdates: {
list: {
Action: "ADD",
Value: [email] // several emails can also be added with incorrect Expected check
},
},
Expected: {
list: {
ComparisonOperator: "NOT_CONTAINS",
Value: email
},
}
};
await docClient.update(params).promise();
The question is whether it's possible to perform the same atomic operation for several emails at once?

Use a string set if you want there to be no duplicates. If you want to see if they existed in the set before you added them, return the old item.
const email = "email_new#example.com";
const params = {
TableName: "myTable",
Key: {
id: "blackList"
},
UpdateExpression: "ADD #email :email",
ExpressionAttributeNames: {
"#email": "email"
},
ExpressionAttributeValues: {
":email": docClient.createSet(email)
}
};
await docClient.update(params).promise();
This will add the email_new#example.com to the email attribute as a string set. If the email attribute doesn't exist on the object it will be created.
See https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.ADD for the documentation.

Related

DynamoDB return the modified document (Old or new) when using TransactWrtieItems

Is there a way of making transactWriteItem return the document it updated?
const transactionParams = {
ReturnConsumedCapacity: "INDEXES",
TransactItems: [
{
Delete: {
TableName: reactionTableName,
Key: {
"SOME_PK_",
"SOME_SK_",
},
ReturnValues: 'ALL_OLD',
},
},
{
Update: {
TableName: reviewTableName,
Key: { PK: "SOME_PK", SK: "SOME_SK" },
ReturnValues: 'ALL_OLD',
},
},
],
};
try {
const result = await docClient.transactWrite(transactionParams).promise();
} catch (error) {
context.done(error, null);
}
For example in the above code get the documents that were touched (before or after update)?
No, TransactWriteItems API does not provide the ability to return values of a modified item, however, you could obtain those values using DynamoDB Streams, otherwise you would need to default to the singleton APIs UpdateItem/DeleteItem which are not ACID compliant together.

How can I update a nested field in DynamoDB?

I defined a DynamoDB table and it has a nested field site. I'd like to update the field site.enable in below code. But when I run the update command I got this error:
ValidationException: The document path provided in the update expression is invalid for update`
What should I do in order to fix the issue?
{
TableName: 'MyTable',
Key: {
id: '4b7020d2-2d19-4aeb-7f27e49d5bec',
type: '80422149-c97d-4a1a-7bf20ef57056',
},
UpdateExpression: 'set #site.#siteenable= :siteenable',
ExpressionAttributeValues: {
':siteenable': true,
},
ExpressionAttributeNames: {
'#siteenable': 'enable',
'#site': 'site',
}
}
You don't mention a programming language, so I'm going to assume what I'm used to: Python.
In Python there are two ways you can do this:
The lower level client API, which requires you to format the data the way DynamoDB would
def enable_site_with_client():
ddb = boto3.client("dynamodb")
ddb.update_item(
TableName=TABLE_NAME,
Key={
"PK": {"S": "SITE_ENTRY"}
},
UpdateExpression="SET #site.#enabled = :update_value",
ExpressionAttributeNames={
"#site": "site",
"#enabled": "enabled"
},
ExpressionAttributeValues={
":update_value": {"BOOL": True}
}
)
The higher level resource API, which allows you to use the language native data structures
def enable_site_with_resource():
ddb = boto3.resource("dynamodb")
ddb.Table(TABLE_NAME).update_item(
Key={
"PK": "SITE_ENTRY"
},
UpdateExpression="SET #site.#enabled = :update_value",
ExpressionAttributeNames={
"#site": "site",
"#enabled": "enabled"
},
ExpressionAttributeValues={
":update_value": True
}
)
I have tested both of these and they work.
Given code works fine if the map site exists already, seeing the error message, it looks like the path site doesn't exist.
We could create empty map during the create of the document, then update it easily OR
We could create the map during the update "set #site = :siteValue"
Here is slightly modified query which creates the map.
const dynamodb = new AWS.DynamoDB();
let docClient = new AWS.DynamoDB.DocumentClient();
docClient.update(
{
TableName: "MyTable",
Key: {
id: "4b7020d2-2d19-4aeb-7f27e49d5bec",
type: "80422149-c97d-4a1a-7bf20ef57056",
},
UpdateExpression: "set #site = :siteValue",
ExpressionAttributeValues: {
":siteValue": { enable: true },
},
ExpressionAttributeNames: {
"#site": "site",
},
},
function (error, result) {
console.log("error", error, "result", result);
}
);
Here is an example for Java SDK V2 that will update root->nested->someValue
Map<String, AttributeValue> attributeValues = new HashMap<>();
attributeValues.put(":myValue", AttributeValue.builder().s(jsonString).build());
UpdateItemRequest updateRequest = UpdateItemRequest.builder()
.tableName("my_table_name")
.key(keyToUpdate)
.updateExpression("SET nested.someValue= :myValue")
.expressionAttributeValues(attributeValues)
.build();
client.updateItem(updateRequest);

AWS DynamoDB | Check if List of Maps contains a specific value

Im storing user data in AWS DynamoDB.
One of the attributes is a List of Maps:
skills: [
{
name: 'foo'
},
{
name: 'bar'
}
]
How can I write a Scan that checks if skills have a map with name = foo?
I'm using DocumentClient
I have tried using contains but can't get it to work with Maps nested in a List:
let params: {
TableName: 'tablename',
FilterExpression: 'contains(skills, :val)',
ExpressionAttributeValues: {
':val': 'foo'
}
}

Append to or create StringSet if it doesn't exist

So this should be simple...
I want to append a string to a StringSet in a DynamoDB if it exists, or create the StringSet property if it doesn't and set the value. If we could initialize the StringSet on creation with an empty array, it would be fine, but alas we can not.
Here's what I have so far:
const companiesTable = 'companies';
dynamodb.updateItem({
TableName: companiesTable,
Key: {
id: {
S: company.id
}
},
UpdateExpression: 'ADD socialAccounts = list_append(socialAccount, :socialAccountId)',
ExpressionAttributeValues: {
':socialAccountId': {
'S': [socialAccountId]
}
},
ReturnValues: "ALL_NEW"
}, function(err, companyData) {
if (err) return cb({ error: true, message: err });
const response = {
error: false,
message: 'Social account created',
socialAccountData
};
cb(response);
});
I've also tried...
UpdateExpression: 'SET socialAccounts = list_append(socialAccounts, :socialAccountId)',
ExpressionAttributeValues: {
':socialAccountId': {
S: socialAccountId
}
},
and...
UpdateExpression: 'ADD socialAccounts = :socialAccountId',
ExpressionAttributeValues: {
':socialAccountId': {
S: socialAccountId
}
},
and...
UpdateExpression: 'SET socialAccounts = [:socialAccountId]',
ExpressionAttributeValues: {
':socialAccountId': socialAccountId
},
and...
UpdateExpression: 'ADD socialAccounts = :socialAccountId',
ExpressionAttributeValues: {
':socialAccountId': socialAccountId
},
Among about every other variation of the above. Am I dumb? Is DynamoDB not capable of simple write/updates to an array type field? Do I REALLY have to lookup the item first to see if it has that field before I try to either add or set that field, because I can't instantiate that field with an empty array?
The ADD action handles the create/update logic, but only supports numbers and sets. You are trying to add a string type 'S'. You need to wrap this string in an array and pass it as a string set 'SS'. You also don't need the equals sign.
Your UpdateExpression and ExpressionAttributeValues should look like this:
UpdateExpression: 'ADD socialAccounts :socialAccountId',
ExpressionAttributeValues: {
':socialAccountId': {
'SS': [socialAccountId]
}
},
More information about updating items can be found here

AWS DynamoDB Attempting to ADD to a Set - Incorrect Operand

I am creating an API using Nodejs and DynamoDB as a back end. I am attempting to update an item to add to a set of "friends". When I update the user, I get the error, "Invalid UpdateExpression: Incorrect operand type for operator or function; operator: ADD, operand type: MAP". My understanding is that when adding to a set that does not exist, the set will be created. If it already exists, the new value should be added to the set. I do not understand why the set I attempt to ADD is being read as a map.
How users are created:
var params = {
TableName: "users",
Item:{
"id": Number(id),
"name": name,
"password": password
}
};
documentClient.put(params, function(err, data) {
if(err)
res.json(500, err);
else
res.json(200, data);
});
How friends are added:
var params = {
TableName: "users",
Key: {
"id": id
},
UpdateExpression: "ADD friends :friendId",
ExpressionAttributeValues: {
":friendId": { "NS": [friendId] }
},
ReturnValues: "UPDATED_NEW"
};
documentClient.update(params, function(err, data) {
if(err)
res.json(500, err);
else
res.json(200, data);
});
This question has an answer here
https://stackoverflow.com/a/38960676/4975772
Here's the relevant code formatted to fit your question
let AWS = require('aws-sdk');
let docClient = new AWS.DynamoDB.DocumentClient();
...
var params = {
TableName : 'users',
Key: {'id': id},
UpdateExpression : 'ADD #friends :friendId',
ExpressionAttributeNames : {
'#friends' : 'friends'
},
ExpressionAttributeValues : {
':friendId' : docClient.createSet([friendId])
},
ReturnValues: 'UPDATED_NEW'
};
docClient.update(params, callback);
If the set doesn't exist, then that code will create it for you. You can also run that code with a different set to update the set's elements. Super convenient.
Here is the working code. You don't need ADD here. Just use "set friends = :friendId" as friends attribute is not already present in the table (i.e. before the update you have only id, name and password in the table). The friend attribute is being added newly as part of the update.
var docClient = new AWS.DynamoDB.DocumentClient();
var table = "users";
var userid = 1;
var friendId = [123];
var params = {
TableName : table,
Key: {
"id" : userid
},
"UpdateExpression": "set friends = :friendId",
"ExpressionAttributeValues": {
":friendId": {"NS": friendId}
},
"ReturnValues" : "UPDATED_NEW"
};