In DynamoDB can you UpdateItem ignoring undefined values? - node.js

DynamoDB is used to manage PATCH requests where 1 or more properties may be provided. I'd like those properties to be updated if they exist in the request, otherwise ignored in the update. DocumentClient.update(params) where params is:
TableName: '...',
Key: {...},
UpdateExpression: `set
Cost = :Cost,
Sales = :Sales,
...
ExpressionAttributeValues: {
':Cost': get(requestBody, 'form.cost', undefined),
':Sales': get(requestBody, 'form.sales', undefined),
...
}
Or is this achieving this only possible by manipulating the expression strings?

I feel like I'm using DynamoDB wrong for this multi-field PATCH, especially since the solution is overly complex.
Leaving this here in case anyone else finds it helpful, or better, has a tidier solution:
let fieldsToUpdate = [
// these are just string constants from another file
[dynamoDbFields.cost, apiFields.cost],
[dynamoDbFields.annualSales, apiFields.annualSales],
... ]
// get the new DynamoDB value from the request body (it may not exist)
.map(([dynamoDbField, apiField]) => [dynamoDbField, _.get(requestBody, apiField)])
// filter any keys that are undefined on the request body
.filter(([dynamoDbField, value]) => value !== undefined)
// create a mapping of the field identifier (positional index in this case) to the DynamoDB value, e.g. {':0': '123'}
let expressionAttributeValues = fieldsToUpdate.reduce((acc, [dynamoDbField, value], index) =>
_.assignIn(acc, {[`:${index}`]: value}), {})
// and create the reciprocal mapping of the identifier to the DynamoDB field name, e.g {ID: ':0'}
let updateExpression = fieldsToUpdate.reduce((acc, [dynamoDbField], index) =>
_.assignIn(acc, {[dynamoDbField]: `:${index}`}), {})
const params = {
TableName: TABLE_NAME,
Key: {[dynamoDbFields.id]: _.get(requestBody, apiFields.id)},
UpdateExpression: `set ${Object.entries(updateExpression).map((v) => v.join('=')).join(',')}`,
ExpressionAttributeValues: expressionAttributeValues,
ConditionExpression: `attribute_exists(${dynamoDbFields.id})`,
ReturnValues: 'ALL_NEW'
}

Related

Updating DynamoDB record with DELETE Set on UpdateExpression fails with node.js

I'm trying to perform a update call to a DynamoDB.DocumentClient instance using AWS SDK with the payload on the code snippet below:
const AWS = require('aws-sdk')
const DynamoDB = new AWS.DynamoDB.DocumentClient()
...
const TableName = 'MyTable'
const Key = { PK: 'MyPK', SK: 'MySK' }
const operation = 'DELETE'
const myId = 'abcde'
const currentRecord = await DynamoDB.get({TableName, Key)
DynamoDB.update({
TableName,
Key,
UpdateExpression: `
${operation} myIds :valuesToModify,
version :incrementVersionBy
`,
ConditionExpression: `version = :version`,
ExpressionAttributeValues: {
":version": currentRecord.version,
":incrementVersionBy": 1,
":valuesToModify": DynamoDB.createSet([myId])
}
})...
I get this error as result:
ERROR Invoke Error
{
"errorType":"Error",
"errorMessage":"ValidationException: Invalid UpdateExpression: Incorrect operand type for operator or function;
operator: DELETE, operand type: NUMBER, typeSet: ALLOWED_FOR_DELETE_OPERAND",
"stack":[...]
}
Interestingly, if operation is changed to ADD it works well.
Any clues that could be helpful to understand why ADD works and not DELETE and/or how to fix and/or yet alternative approaches compatible with this update operation are highly appreciated!
The only workaround possible here is not to use a DELETE operation, instead, you gotta query the item, find the index in the array you wish to delete, and remove it a REMOVE operation:
like in this case, arrayField contains an array of Users, and I want to delete by user's phoneNumber.
const dataStore = await dynamodb.get(queryParams).promise();
let i=0; //save the index
for(i = 0; i < dataStore.Item.myTable.length; i++){
if(dataStore.Item.arrayField[i].phone === phoneNumber)
{
break;
}
}
if(i < dataStore.Item.arrayField.length){
const updateStoreParams = {
TableName: tableName,
Key: storeTableKey,
UpdateExpression: `REMOVE arrayField[${i}]`,
}
await dynamodb.update(updateStoreParams).promise().catch((err) => {
console.log(err);
throw err;
});
}
It ended up being a semantic error I didn't pay attention to.
When ${operation} was ADD the version field of the UpdateExpression would work because it is a numeric increment.
When ${operation} was DELETE, the version didn't work because, as the error states it was Incorrect operand type for operator or function as it will only work for Removing Elements From a Set as per the docs.
The error was a bit misleading at first but when I tried to implement with other SDK I ended up with the same error then I tried to focus within the UpdateExpression part and found that I had to refactor to something like this in order to it to work:
// Notice below that I inject ADD if operation is DELETE and a comma otherwise
DynamoDB.update({
TableName,
Key,
UpdateExpression: `
${operation} socketIds :valuesToModify
${operation == 'DELETE' ? 'ADD' : ','} version :incrementVersionBy
`,
ConditionExpression: `version = :version`,
ExpressionAttributeValues: {
':version': channelRecord.version,
':incrementVersionBy': 1,
':valuesToModify': DynamoDB.createSet([socketId])
}
})
Hopefully it will become useful to others in the future!

update attributes in a dynamodb record

I am trying to update my existing record in my dynamodb table.
Like below I have an Item in my table
let params = {
TableName: proces.env.dynamoDbTable,
Item: {
productId: "id",
att1: val1,
att2: val2
}
}
I want to perform an update. I am using the aws dynamodb sdk's update method and passing it params like below
let aws = require('aws-sdk');
let dbb = new aws.DynamoDb.DocumentClient();
let params = {
TableName: process.env.tableName,
Key: {productID}
ExpressionAttributeNames: { "#updatedAt" : "updatedAt" }
ExpressionAttributeValues: {":u":moment().unix(), ":val1" : a, ":val2": b}
UpdateExpression: "SET att1 = :val1, att2: val2, #updatedAt: :u"
}
// a, b are passed as argument to function and are optional
dbb.update(params).promise()
When an argument goes missing the dynamo raises ExpressionAttributeValue missing exception and I know it is straight. Is there a way I can update my Item with the attributes provided
Unfortunately dynamodb does not make this easy. But you can use some fancy js to create the params object dynamically:
// for some object `attrs` we find which keys we need to update
const keys = Object.keys(attrs);
const values = Object.values(attrs);
// get a list of key names and value names to match the dynamodb syntax
const attributeKeyNames = keys.map((k) => '#key_' + k);
const attributeValueNames = keys.map((k) => ':val_' + k);
// create individual expressions for each attribute that needs to be updated
const expressions = attributeValueNames.map((attr, i) => {
return `${attributeKeyNames[i]} = ${attr}`;
});
// add the SET keyword to the beginning of the string
// and join all the expressions with a comma
const UpdateExpression = 'SET ' + expressions.join(', ');
// I use `zipObject()` from lodash https://lodash.com/docs/4.17.15#zipObject
// it makes an object map from two arrays where the first is the keys and
// the second is the values
const ExpressionAttributeValues = _.zipObject(attributeValueNames, values);
const ExpressionAttributeNames = _.zipObject(attributeKeyNames, keys);
// now you have all the params
const params = {
TableName,
Key: {
uuid,
},
UpdateExpression,
ExpressionAttributeValues,
ExpressionAttributeNames,
};
Lucas D's answer works great. A couple of things to keep in mind though:
If you're sending an object with a unique key into your update function, you must remove that key before mapping your expression arrays or it will be included in the query and will get an error:
const newObject = {...originalObject}
delete newObject.unique_key
If you can't or don't want to use Lodash, you can use Object.assign to map your keys/values arrays:
const ExpressionAttributeValues = Object.assign(...attributeValueNames.map((k, i) => ({[k]: values[i]})));
const ExpressionAttributeNames = Object.assign(...attributeKeyNames.map((k, i) => ({[k]: keys[i]})));

DynamoDB update item with conditionExpression instead of key

I try to update an item in dynamodb by adding a condition, without passing the key in the parameters.
And as soon as my condition is true update. Is it possible to do this?
Below an example of an item:
{
"id" : "bcc2f32e-305e-4469-88e2-463724b5c6a9",
"name" : "toto",
"email" : "toto#titi.com"
}
Where email is unique for items.
I tested this code and it works :
const name= "updateName";
const params = {
TableName: MY_TABLE,
Key: {
id
},
UpdateExpression: 'set #name = :name',
ExpressionAttributeNames: { '#name': 'name' },
ExpressionAttributeValues: { ':name': name },
ReturnValues: "ALL_NEW"
}
dynamoDb.update(params, (error, result) => {
if (error) {
res.status(400).json({ error: 'Could not update Item' });
}
res.json(result.Attributes);
})
But i want to do something like this (replace the Key by conditionExpression):
const params = {
TableName: MY_TABLE,
UpdateExpression: 'set #name = :name',
ConditionExpression: '#email = :email',
ExpressionAttributeNames: {
'#name': 'name',
'#email': 'email'
},
ExpressionAttributeValues: {
':name': name,
':email': email
},
ReturnValues: "ALL_NEW"
}
dynamoDb.update(params, (error, result) => {
if (error) {
res.status(400).json({ error: 'Could not update User' });
}
res.json(result.Attributes);
})
But this code doesn't work.
Any ideas?
You cannot update an item in DynamoDB without using the entire primary key (partition key, and sort key if present). This is because you must specify exactly one record for the update. See the documentation here.
If you want to find an item using a field that is not the primary key, then you can search using a scan (potentially slow and expensive) or by using a Global Secondary Index (GSI) on that field. Either of these methods requires that you do a separate request to find the item in question, and then use its primary key to perform the update.
It sounds like you want to do an update that waits for a condition. That's not how DynamoDb works; it cannot wait for anything (except consistency, I suppose, but that's somewhat different). What you can do is make a request with a condition, and if it fails the condition (returning immediately), make the request again later. If you do this you'll need to be careful to backoff appropriately, or you might end up making a lot of requests very quickly.
The key is a required parameter when doing updates; the condition expression can be used in addition to providing the key, but can't be used instead of the key.
Also, I am not sure you fully understand what the conditionExpression is for - its not like the 'where' clause in an SQL update statement (i.e. update mytable set name='test' where email='myemail.com'.
Instead, logically the conditionExpression in an update would be more like:
update mytable set name='test' where key='12345' but only if quantity >0 - for example,
i.e. you are telling dynamodb the exact key of the record you want updated, and once it finds it it uses the condition expression to determine if the update should proceed - i.e. find the record with id=12345, and change the name to 'test', only of the quantity is greater than 0.
It does not use the conditionExpression to find records to update.

DynamoDB Scan with FilterExpression in nodejs

I'm trying to retrieve all items from a DynamoDB table that match a FilterExpression, and although all of the items are scanned and half do match, the expected items aren't returned.
I have the following in an AWS Lambda function running on Node.js 6.10:
var AWS = require("aws-sdk"),
documentClient = new AWS.DynamoDB.DocumentClient();
function fetchQuotes(category) {
let params = {
"TableName": "quotient-quotes",
"FilterExpression": "category = :cat",
"ExpressionAttributeValues": {":cat": {"S": category}}
};
console.log(`params=${JSON.stringify(params)}`);
documentClient.scan(params, function(err, data) {
if (err) {
console.error(JSON.stringify(err));
} else {
console.log(JSON.stringify(data));
}
});
}
There are 10 items in the table, one of which is:
{
"category": "ChuckNorris",
"quote": "Chuck Norris does not sleep. He waits.",
"uuid": "844a0af7-71e9-41b0-9ca7-d090bb71fdb8"
}
When testing with category "ChuckNorris", the log shows:
params={"TableName":"quotient-quotes","FilterExpression":"category = :cat","ExpressionAttributeValues":{":cat":{"S":"ChuckNorris"}}}
{"Items":[],"Count":0,"ScannedCount":10}
The scan call returns all 10 items when I only specify TableName:
params={"TableName":"quotient-quotes"}
{"Items":[<snip>,{"category":"ChuckNorris","uuid":"844a0af7-71e9-41b0-9ca7-d090bb71fdb8","CamelCase":"thevalue","quote":"Chuck Norris does not sleep. He waits."},<snip>],"Count":10,"ScannedCount":10}
You do not need to specify the type ("S") in your ExpressionAttributeValues because you are using the DynamoDB DocumentClient. Per the documentation:
The document client simplifies working with items in Amazon DynamoDB by abstracting away the notion of attribute values. This abstraction annotates native JavaScript types supplied as input parameters, as well as converts annotated response data to native JavaScript types.
It's only when you're using the raw DynamoDB object via new AWS.DynamoDB() that you need to specify the attribute types (i.e., the simple objects keyed on "S", "N", and so on).
With DocumentClient, you should be able to use params like this:
const params = {
TableName: 'quotient-quotes',
FilterExpression: '#cat = :cat',
ExpressionAttributeNames: {
'#cat': 'category',
},
ExpressionAttributeValues: {
':cat': category,
},
};
Note that I also moved the field name into an ExpressionAttributeNames value just for consistency and safety. It's a good practice because certain field names may break your requests if you do not.
I was looking for a solution that combined KeyConditionExpression with FilterExpression and eventually I worked this out.
Where aws is the uuid. Id is an assigned unique number preceded with the text 'form' so I can tell I have form data, optinSite is so I can find enquiries from a particular site. Other data is stored, this is all I need to get the packet.
Maybe this can be of help to you:
let optinSite = 'https://theDomainIWantedTFilterFor.com/';
let aws = 'eu-west-4:EXAMPLE-aaa1-4bd8-9ean-1768882l1f90';
let item = {
TableName: 'Table',
KeyConditionExpression: "aws = :Aw and begins_with(Id, :form)",
FilterExpression: "optinSite = :Os",
ExpressionAttributeValues: {
":Aw" : { S: aws },
":form" : { S: 'form' },
":Os" : { S: optinSite }
}
};

AWS DynamoDB Node.js scan- certain number of results

I try to get first 10 items which satisfy condition from DynamoDB using lambda AWS. I was trying to use Limit parameter but it is (basis on that website)
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#scan-property
"maximum number of items to evaluate (not necessarily the number of matching items)".
How to get 10 first items which satisfy my condition?
var AWS = require('aws-sdk');
var db = new AWS.DynamoDB();
exports.handler = function(event, context) {
var params = {
TableName: "Events", //"StreamsLambdaTable",
ProjectionExpression: "ID, description, endDate, imagePath, locationLat, locationLon, #nm, startDate, #tp, userLimit", //specifies the attributes you want in the scan result.
FilterExpression: "locationLon between :lower_lon and :higher_lon and locationLat between :lower_lat and :higher_lat",
ExpressionAttributeNames: {
"#nm": "name",
"#tp": "type",
},
ExpressionAttributeValues: {
":lower_lon": {"N": event.low_lon},
":higher_lon": {"N": event.high_lon}, //event.high_lon}
":lower_lat": {"N": event.low_lat},
":higher_lat": {"N": event.high_lat}
}
};
db.scan(params, function(err, data) {
if (err) {
console.log(err); // an error occurred
}
else {
data.Items.forEach(function(record) {
console.log(
record.name.S + "");
});
context.succeed(data.Items);
}
});
};
I think you already know the reason behind this: the distinction that DynamoDB makes between ScannedCount and Count. As per this,
ScannedCount — the number of items that were queried or scanned,
before any filter expression was applied to the results.
Count — the
number of items that were returned in the response.
The fix for that is documented right above this:
For either a Query or Scan operation, DynamoDB might return a LastEvaluatedKey value if the operation did not return all matching items in the table. To get the full count of items that match, take the LastEvaluatedKey value from the previous request and use it as the ExclusiveStartKey value in the next request. Repeat this until DynamoDB no longer returns a LastEvaluatedKey value.
So, the answer to your question is: use the LastEvaluatedKey from DynamoDB response and Scan again.

Resources