How to execute multiple operations while updating an item in Amazon DynamoDB? - node.js

I'm trying to update an item in Amazon DynamoDB. I want to execute an operation that consists of one sum and one subtraction, but I'm not able to accomplish my goal.
Here is the function, ddb is an instance of the class AWS.DynamoDB.DocumentClient:
function updateLecture(event){
const params = {
TableName: 'lecture',
Key: {
'lecture_id': Number.parseInt(event.lecture_id)
},
UpdateExpression: 'SET #free = :total - (#total + #free)',
ExpressionAttributeValues: {
':total': event.total
},
ExpressionAttributeNames: {
"#total" : "total",
"#free" : "free"
},
ReturnValues : 'UPDATED_NEW'
}
return ddb.update(params).promise();
}
When I try to run the function, I get the following error:
"errorType": "ValidationException",
"errorMessage": "Invalid UpdateExpression: Syntax error; token: \"+\", near: \"#total + #free\""
I've tried to work with parenthesis, but I always receive the same error.
I'm afraid I can't use more than one operation, but I couldn't find any trace of that in the docs. Anyway, is there a way to use multiple operations?

Just tried the following PartiQL statement in the DynamoDB console, and it worked fine:
// Works just fine
UPDATE "ddb-playground"
SET myNumA = 10 - (myNumB + myNumA)
WHERE PK = 'foo' AND SK = 'bar'
It runs just fine, so at least there's no fundamental limitation here.
However, just like you, I'm not able to get the JS SDK to do this with an UpdateExpression, I get syntax error no matter what I try. So it seems like the JS SDK is limited to a single arithmetic expression on numeric values.
// Fails with:
//
// ValidationException: Invalid UpdateExpression: Syntax error; token: "+", near: "myNumB + myNumA"
import AWS from "aws-sdk"
const ddb = new AWS.DynamoDB()
await ddb.putItem({
TableName: "ddb-playground",
Item: {
PK: {S: "foo"},
SK: {S: "bar"},
myNumA: {N: "10"},
myNumB: {N: "20"}
}
}).promise()
await ddb.updateItem({
TableName: "ddb-playground",
Key: {
PK: {S: "foo"},
SK: {S: "bar"}
},
UpdateExpression: "SET myNumA = :newVal - (myNumB + myNumA)",
ExpressionAttributeValues: {
":newVal": {N: "30"}
}
}).promise()
So perhaps your only option here is to actually use PariQL for this one?
await ddb.executeStatement({
Statement: 'UPDATE "ddb-playground" SET myNumA = ? - (myNumB + myNumA) WHERE PK = ? AND SK = ?',
Parameters: [{N: "30"}, {S: "foo"}, {S: "bar"}]
}).promise()

Related

cant update using updateExpression on Dynamodb using nodejs

Here is my data structure -
{
"pk": "USER-12560000000",
"sk": "USER",
"data": {
"name": "George", //I want to modify the value of this attribute
"updatedAt": Date.now() // I want to add this attribute to the data field
}
}
I have been trying this below updateExpression, but for some reason it doesn't even give a clue on whats wrong in cloudwatch. In the below code I am only trying to update the value of the displayName attribute, not sure on the code on how to add an extra attribute. The cloudwatch logs dont print the error reason as well. I have read the documentation on aws also a few other stackoveflow posts. I haven't been able to make this work.
Main Lambda Handler -
const updateUser = require('./user-queries/updateUser');
exports.handler = async (event) => {
var userId = event.arguments.userId;
var name = event.arguments.name;
var avatarUrl = event.arguments.avatarUrl;
console.log(event.info.fieldName);
switch(event.info.fieldName) {
case "updateUser":
return updateUser(userId, name, avatarUrl);
}
};
const AWS = require("aws-sdk")
AWS.config.update({ region: "ca-central-1" })
const dynamoDB = new AWS.DynamoDB.DocumentClient()
async function updateUser(userId, name, avatarUrl) {
dynamoDB
.update({
TableName: "Bol-Table",
Key: {
pk: "USER-12560000000",
sk: "USER",
},
UpdateExpression: `set data = :data`,
ExpressionAttributeValues: {
":data": {
"name": "Test"
},
},
})
.promise()
.then(data => console.log(data.Attributes))
.catch(console.error)
}
module.exports = updateUser;
This is what gets printed out in cloudwatch along with some billing information
2021-10-09T16:25:15.527Z b1a1e4cb-6001-415c-82a3-cfdc90413e4e INFO USER-12560000000 Sam www.bol.com
Your UpdateExpression & ExpressionAttributeValues are wrong, they should be:
UpdateExpression: `set data.name = :x`
...
ExpressionAttributeValues: {
":x": "Test"
}
The Key object also takes in the keys as a string so your Key object needs to look like:
Key: {
"pk": "USER-12560000000",
"sk": "USER"
}
To update/insert multiple fields, the below will work:
UpdateExpression: `set data.name = :x, data.updatedAt = :y`
...
ExpressionAttributeValues: {
":x": "Test",
":y": Date.now()
}
Changing the dynamodb.update like below made it all work after all.
await dynamoDB.update(params).promise();
Here is the full code for anyone who comes across this issue.
const AWS = require("aws-sdk")
AWS.config.update({ region: "ca-central-1" })
const dynamoDB = new AWS.DynamoDB.DocumentClient()
async function updateUser(userId, name, avatarUrl) {
var params = {
TableName: "Bol-Table",
Key: {
"pk": userId,
"sk": 'USER',
},
UpdateExpression: 'SET details.displayName = :name, details.avatarUrl = :avatarUrl, details.updatedAt = :updateDate',
ExpressionAttributeValues: {
':name': name,
':avatarUrl':avatarUrl,
':updateDate': new Date().toISOString()
},
ReturnValues: 'ALL_NEW'
};
const Item = await dynamoDB.update(params).promise();
console.log(Item)
}
module.exports = updateUser;

Is there an easier way to convert javascript object to dynamodb update expression?

I am using nodejs aws-sdk/clients/dynamodb library with dynamodb. I need to update an item in the table. Below is the sample code to update an item:
params = {
TableName:table,
Key:{
"year": year,
"title": title
},
UpdateExpression: "set info.rating = info.rating + :val",
ExpressionAttributeValues:{
":val": 1
},
ReturnValues:"UPDATED_NEW"
};
I will have to specify each attribute in info in UpdateExpression. My info object is very big and I am looking for an easier way to do that. Is there a build-in method to support update an object to dynamodb item? something like:
params = {
TableName:table,
Key:{
"year": year,
"title": title
},
Item: info
};
The answer given by E.J. Brennan is great for cases where it's ok to replace the entire item. DocumentClient eases the hassle of dealing with DynamoDB attribute types, but the example given uses the put method. According to the docs put passes through to putItem which
Creates a new item, or replaces an old item with a new item
That means that it's not going to help with partial updates to existing items where you don't already have the full record (and can get away with a full replacement). For partial updates you have to use updateItem, or it's DocumentClient counterpart, update.
The AWS labs has published a utility to help with constructing update expressions to use with updateItem. Since I generally prefer to use DocumentClient, I unmarshall values with the utility function provided by DynamoDB's Converter (yes, I know it's a bit a back and forth, but it makes testing easier).
const AWS = require('aws-sdk');
const db = new AWS.DynamoDB.DocumentClient();
const { UpdateExpression, ExpressionAttributes } = require('#aws/dynamodb-expressions');
const { unmarshall } = AWS.DynamoDB.Converter;
const updateExpressionProps = ({ category, classification }) => {
attributes = new ExpressionAttributes();
expression = new UpdateExpression();
expression.set('category', category);
expression.set('classification', classification);
return {
UpdateExpression: expression.serialize(attributes),
ExpressionAttributeNames: attributes.names,
ExpressionAttributeValues: unmarshall(attributes.values),
};
};
const updateRequest = async ({ id, subject, category, classification }) =>
await db
.update({
TableName: 'table-name',
Key: {
id,
subject,
},
...updateExpressionProps({ category, classification }),
})
.promise();
This bit of code only updates the category and classification attributes on the record identified with id and subject without the hassle of manually building a correct UpdateExpression string. This example could easily be generalized into something reusable throughout your project.
I wrote the following utility method to convert a given object to UpdateExpression (SET only), ExpressionAttributeNames, and ExpressionAttributeValues.
const convertToCompositePathObj = (obj: Record<any, any>) => {
const res: Record<string, string | number | []> = {};
const getPropertyPath = (obj: Record<any, any>, current = "") => {
for (let key in obj) {
const value = obj[key];
const newKey = current ? [current, key].join(".") : key;
if (value && typeof value === "object" && !Array.isArray(value)) {
getPropertyPath(value, newKey);
} else res[newKey] = value;
}
};
getPropertyPath(sampleObject);
return res;
};
const generateDynamoDbUpdateExpression = (obj: Record<any, any>) => {
const compositePathObj = convertToCompositePathObj(sampleObject);
let counter = 0;
let updateExpression = "SET ";
const expressionAttNamesMap: any = {};
const expressionAttValuesMap: any = {};
for (let k in compositePathObj) {
const newUpdateExpression = k
.split(".")
.map((item) => {
const attName = `#${item}`;
if (!expressionAttNamesMap[attName]) {
expressionAttNamesMap[attName] = item;
}
return attName;
})
.join(".")
.concat(`= :${counter} AND `);
expressionAttValuesMap[`:${counter}`] = compositePathObj[k];
counter += 1;
updateExpression += newUpdateExpression;
}
updateExpression = updateExpression.substring(0, updateExpression.length - 5);
return {
UpdateExpression: updateExpression,
ExpressionAttributeNames: expressionAttNamesMap,
ExpressionAttributeValues: expressionAttValuesMap
};
};
// example usage:
const sampleObject = {
name: {
first: "John",
last: "Doe"
},
address: {
line1: "123 test st.",
line2: "Apt 123",
city: "Los Angeless",
state: "CA",
zip: 92763
},
phone: 8675768475
};
console.log(generateDynamoDbUpdateExpression(sampleObject));
/**
*****OUTPUT*****
{
UpdateExpression: "SET #name.#first= :0 AND #name.#last= :1 AND #address.#line1= :2 AND #address.#line2= :3 AND #address.#city= :4 AND #address.#state= :5 AND #address.#zip= :6 AND #phone= :7",
ExpressionAttributeNames:{
#name: "name"
#first: "first"
#last: "last"
#address: "address"
#line1: "line1"
#line2: "line2"
#city: "city"
#state: "state"
#zip: "zip"
#phone: "phone"
},
ExpressionAttributeValues:{
:0: "John"
:1: "Doe"
:2: "123 test st."
:3: "Apt 123"
:4: "Los Angeless"
:5: "CA"
:6: 92763
:7: 8675768475
}
}
**/
PS: I wrote this in a hurry so please excuse the formatting and any types.
https://codesandbox.io/s/aws-dynamic-update-expression-set-33tye7?file=/src/index.ts
You could use the Document Client:
Version 2.2.0 of the AWS SDK for JavaScript introduces support for the
document client abstraction in the AWS.DynamoDB namespace. The
document client abstraction makes it easier to read and write data to
Amazon DynamoDB with the AWS SDK for JavaScript. Now you can use
native JavaScript objects without annotating them as AttributeValue
types.
For example:
var docClient = new AWS.DynamoDB.DocumentClient({region: 'us-west-2'});
var params = {
Item: {
hashkey: 'key',
boolAttr: true,
listAttr: [1, 'baz', true]
mapAttr: {
foo: 'bar'
}
},
TableName: 'table'
};
docClient.put(params, function(err, data){
if (err) console.log(err);
else console.log(data);
});
https://aws.amazon.com/blogs/developer/announcing-the-amazon-dynamodb-document-client-in-the-aws-sdk-for-javascript/

AWS DynamoDB Transact Write using IN operator on Array

I am working on one query where I need to use TrasactWrite of dynamoDB and perform update query on one of my table.
Scenario is I am having array of unique IDs and I need to perform same update operation on each ID in array and change one flag in table without using Loop or map.
But I am having difficulty in finding example or docs related to how do I use IN operator on IDs which are primary keys in a Key parameter of transactWrite.
Here is my sample code:
let IDs = await find('table', { type: 'some_type' }, 'type-index}
let params = {
TransactItems: [{
Update: {
TableName: 'table',
Key: '#_id IN :IDs,
UpdateExpression: 'set #flag = :flag',
ExpressionAttributeNames: {
'#flag': 'flag',
'#_id': '_id'
},
ExpressionAttributeValues: {
':flag': false,
":IDs": IDs
}
}
}]
}
Already tried: Answer Its not same question as mine
I will answer if anyone has any question. Any help will be really helpful. Thank you
You can't use IN operator in this case - Update. With update operator you have to put key value to Update object. A key includes partitionKey and sortKey, I think in your case, you just set partitionKey.
To update more than one item by ID, you can put many Update object to TransactItems.
let IDs = await find('table', { type: 'some_type' }, 'type-index');
let params = {
TransactItems: [], // init empty array
}
IDs.forEach((ID) => {// loop though IDs array
params.TransactItems.push({
Update: {
TableName: 'table',
Key: {
'_id': ID, // ID value
},
UpdateExpression: 'set #flag = :flag',
ExpressionAttributeNames: {
'#flag': 'flag',
// '#_id': '_id' // remove this line
},
ExpressionAttributeValues: {
':flag': false,
// ":IDs": IDs // remove this line
}
}
})
});
// await client.transactWrite(params).promise()

How to query a table by using IN conditions in DynamoDB

Struggling to find an example of how to query a table to return rows with ids from a given list.
The query below throws on the inclusion of IN
var params = {
id: '7deb3df9-552b-47a4-aef3-ad601f141d50'
};
var p = {
TableName: 'players',
KeyConditionExpression: 'id IN (:id)',
ExpressionAttributeValues: buildQuery(params)
};
You can't use "IN" operator with KeyConditionExpression, please see details in this SO question
You may want to use batchGetItem instead of query, which is not so efficient though.
Here is how your params could look like:
var params = {
RequestItems: {
'players': {
Keys: [{
id: "7deb3df9-552b-47a4-aef3-ad601f141d50",
rangeKey: "<range key 1>" // <--- if your table has a range key, you must specify its value here
}, {
id: "<ANOTHER ID 2>",
rangeKey: "<range key 2>"
}, {
id: "<ANOTHER ID 3>",
rangeKey: "<range key 3>"
}]
}
}
};
dynamodbDoc.batchGet(params, function(err, data) {
});

DynamoDB ConditionalExpression not recognizing Attribute Names or Values

I'm using NodeJS's aws-sdk and am trying to do an update such that if the item doesn't exist it will throw an error. I'm using the Expression API instead of the legacy one. Here is my contrived example that isn't working for me.
client.update({
TableName: 'User',
Key: {'_id': '10'},
UpdateExpression: 'SET username = :user, password = :pword',
ConditionalExpression: 'attribute_exists(#idKey) AND #idKey = :idVal',
ExpressionAttributeNames: {
'#idKey': '_id'
},
ExpressionAttributeValues: {
':idVal': '10',
':user': 'user10',
':pword': 'password10'
}}, function(err, data){
if(err) console.log(err);
else console.log(data);
});
ValidationException: Value provided in ExpressionAttributeNames unused in expressions: keys: {#idKey}
I've tried various other ConditionalExpressions both using attribute names and inserting the actual value into the expression. I'm beginning to think this is a bug. Using the legacy Expected->Exists with the legacy AttributeUpdate works but I am unable to demonstrate this feature with Expressions.
You are already narrowing down to the specific item where _id=10 with the Key parameter of your UpdateItemRequest. If an item does not exist, you cannot condition the UpdateItem call on a specific value of a key. Therefore, only the attribute_exists(#idKey) in the ConditionExpression is necessary.
The following code elicits the behavior you desire (I had to change table name to Images and primary key to Id to match the contents of the DynamoDB Local Shell tutorial.
var params = {
TableName: 'Image',
Key: { // The primary key of the item (a map of attribute name to AttributeValue)
'_id': 'dynamodb.png'
},
UpdateExpression: 'SET username = :user, password = :pword',
ConditionExpression: 'attribute_exists(#id)',
ExpressionAttributeValues: {
':user': 'user10',
':pword': 'password10'
},
ExpressionAttributeNames: {
'#id': '_id'
},
ReturnValues: 'ALL_NEW'
};
docClient.update(params, function(err, data) {
if (err) ppJson(err); // an error occurred
else ppJson(data); // successful response
});
As a reminder, please do not post any real password data here :)

Resources