30
votes

Is there is way to achieve the following few points with updateItem: 1. Add attributes if the attributes not exist in DynamoDB 2. Update attributes if the attributes exist in DynamoDB 3. Leave those attributes as what they are if the attributes are not contained in the params.

Here is an example: This is the object in DynamoDB:

{
    id: "1234",
    variable1: "hello",
    variable2: "world"
}

Here is the input that I wish to update:

{
    id: "1234",
    variable1: "hello2",
    variable23: "dog"  // the variable name "variable23" could be anything
}

Here is the updated item in the DynamoDB that I want to achieve:

{
    id: "1234",
    variable1: "hello2",
    variable2: "world",
    variable23: "dog"
}

The "variable23" could be any variable name as input.

Please help! I use node.js, I really appreciate if anyone can show me some code how to achieve this.

thanks!

9

9 Answers

60
votes

This is exactly what AWS.DynamoDB.DocumentClient's update method does.

There is already a sample code on how to use the update method here for AWS SDK for JavaScript in Node.js.

For example:

'use strict';

const aws = require('aws-sdk');

// It is recommended that we instantiate AWS clients outside the scope of the handler 
// to take advantage of connection re-use.
const docClient = new aws.DynamoDB.DocumentClient();

exports.handler = (event, context, callback) => {
    const params = {
        TableName: "MYTABLE",
        Key: {
            "id": "1"
        },
        UpdateExpression: "set variable1 = :x, #MyVariable = :y",
        ExpressionAttributeNames: {
            "#MyVariable": "variable23"
        },
        ExpressionAttributeValues: {
            ":x": "hello2",
            ":y": "dog"
        }
    };

    docClient.update(params, function(err, data) {
        if (err) console.log(err);
        else console.log(data);
    });
};
19
votes

You can update attributes dynamically. see below code.

export const update = (item) => {
  console.log(item)
  const Item = {
    note: "dynamic",
    totalChild: "totalChild",
    totalGuests: "totalGuests"
  };
  let updateExpression='set';
  let ExpressionAttributeNames={};
  let ExpressionAttributeValues = {};
  for (const property in Item) {
    updateExpression += ` #${property} = :${property} ,`;
    ExpressionAttributeNames['#'+property] = property ;
    ExpressionAttributeValues[':'+property]=Item[property];
  }

  
  console.log(ExpressionAttributeNames);


  updateExpression= updateExpression.slice(0, -1);
  
  
   const params = {
     TableName: TABLE_NAME,
     Key: {
      booking_attempt_id: item.booking_attempt_id,
     },
     UpdateExpression: updateExpression,
     ExpressionAttributeNames: ExpressionAttributeNames,
     ExpressionAttributeValues: ExpressionAttributeValues
   };

   return dynamo.update(params).promise().then(result => {
       return result;
   })
   
}
6
votes

Here is a utility method to do that:

update: async (tableName, item, idAttributeName) => {

    var params = {
        TableName: tableName,
        Key: {},
        ExpressionAttributeValues: {},
        ExpressionAttributeNames: {},
        UpdateExpression: "",
        ReturnValues: "UPDATED_NEW"
    };

    params["Key"][idAttributeName] = item[idAttributeName];

    let prefix = "set ";
    let attributes = Object.keys(item);
    for (let i=0; i<attributes.length; i++) {
        let attribute = attributes[i];
        if (attribute != idAttributeName) {
            params["UpdateExpression"] += prefix + "#" + attribute + " = :" + attribute;
            params["ExpressionAttributeValues"][":" + attribute] = item[attribute];
            params["ExpressionAttributeNames"]["#" + attribute] = attribute;
            prefix = ", ";
        }
    }

    return await documentClient.update(params).promise();
}
4
votes

Here is a more secure and up-to-date function to achieve this:

const {
  DynamoDBClient, UpdateItemCommand,
} = require('@aws-sdk/client-dynamodb');
const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb');

const client = new DynamoDBClient({});

/**
 * Update item in DynamoDB table
 * @param {string} tableName // Name of the target table
 * @param {object} key // Object containing target item key(s)
 * @param {object} item // Object containing updates for target item
 */
const update = async (tableName, key, item) => {
  const itemKeys = Object.keys(item);

  // When we do updates we need to tell DynamoDB what fields we want updated.
  // If that's not annoying enough, we also need to be careful as some field names
  // are reserved - so DynamoDB won't like them in the UpdateExpressions list.
  // To avoid passing reserved words we prefix each field with "#field" and provide the correct
  // field mapping in ExpressionAttributeNames. The same has to be done with the actual
  // value as well. They are prefixed with ":value" and mapped in ExpressionAttributeValues
  // along witht heir actual value
  const { Attributes } = await client.send(new UpdateItemCommand({
    TableName: tableName,
    Key: marshall(key),
    ReturnValues: 'ALL_NEW',
    UpdateExpression: `SET ${itemKeys.map((k, index) => `#field${index} = :value${index}`).join(', ')}`,
    ExpressionAttributeNames: itemKeys.reduce((accumulator, k, index) => ({ ...accumulator, [`#field${index}`]: k }), {}),
    ExpressionAttributeValues: marshall(itemKeys.reduce((accumulator, k, index) => ({ ...accumulator, [`:value${index}`]: item[k] }), {})),
  }));

  return unmarshall(Attributes);
};
3
votes

I think some of the examples are a bit confusing. If I have the following table columns

ID  | Name | Age

And I want to update the Name attribute and leave the Age attribute unchanged.

const updateName = async () => {
  const aws = require('aws-sdk');
  const docClient = new aws.DynamoDB.DocumentClient();

  const newName = 'Bob';

  const params = {
    TableName: 'myTable',
    Key: {
      ID: 'myId',
    },
    UpdateExpression: 'set Name = :r',
    ExpressionAttributeValues: {
      ':r': newName,
    },
  };

  await docClient.update(params).promise();
}

updateName();

This seemed a bit more simple.

1
votes

For anyone here for the velocity template version of the solution, they have something documented in their docs, which it took me a while to find, so here's a link if it helps anyone else

https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-dynamodb.html#id4 under 'option 2'

0
votes

Sorry for being late to the party, but this was a top [non-aws-documentation] result on google and didn't answer my use case - using DocumentClient without [un]marshalling AND having a dynamic item. So I want to drop my 2 cents here and try to be helpful by merging the approved answer from @khalid-t and one from @Arno.

'use strict';
const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient();
const updateItem = async (pk, item) => await docClient.update({
    TableName,
    Key: {pk},
    UpdateExpression: 'set ' + Object.keys(item).map(k => `#${k} = :${k}`).join(', '),
    ExpressionAttributeNames: Object.entries(item).reduce((acc, cur) => ({...acc, [`#${cur[0]}`]: cur[0]}), {}),
    ExpressionAttributeValues: Object.entries(item).reduce((acc, cur) => ({...acc, [`:${cur[0]}`]: cur[1]}), {}),
}).promise();
0
votes

I made this using dynamo DB client:

updateItem(item: { [key: string]: any }) {
  const marshaledItem = marshall(item, { removeUndefinedValues: true, });
  const marshaledItemKeys = Object.entries(marshaledItem);

  const params: UpdateItemInput = {
    TableName: this.tableName,
    UpdateExpression: 'set',
    ExpressionAttributeNames: {},
    ExpressionAttributeValues: {},
    Key: marshall({ pk: item.pk, sk: item.sk })
  };

  marshaledItemKeys.forEach(([key, value] ) => {
    if (key === 'sk' || key === 'pk') return;
    params.UpdateExpression += ` #${key} = :${key},`;
    params.ExpressionAttributeNames[`#${key}`] = key;
    params.ExpressionAttributeValues[`:${key}`] = value;
  })

  params.UpdateExpression = params.UpdateExpression.slice(0, -1);
  console.log('REVEAL YOURSELF, YOU MIGHTY BUG: ', params);

  return this.dynamoDbClient.send(new UpdateItemCommand(params));
}

This worked really well for me. Marshall and unmarshall are part of:

import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';

If I pass values that are undefined it will remove those values from the query. If I keep them null it will overwrite them with null

Here is an example how I use it:

async updatePatient(data: PutPatientData): Promise<DBPatient> {
    const {
      pk,
      sk,
      databaseId,
      betterPatientId,
      clinicientPatientId,
      latestClinicientCaseId,
      firstName,
      lastName,
      email,
      birthday,
      gender,
      phone,
    } = data;

    if (!pk && !databaseId) throw Error('Please provide PK or databaseId');
    if (!sk && !betterPatientId) throw Error('Please provide SK or betterPatientId');

    const patientRequestData = {
      pk: pk || `DATABASE#${databaseId}`,
      sk: sk || `PATIENT#${betterPatientId}`,
      itemType: 'Patient',
      lastUpdatedAt: DateTime.now().toString(),
      latestClinicientCaseId: latestClinicientCaseId || undefined,
      clinicientPatientId: clinicientPatientId || undefined,
      firstName: firstName || undefined,
      lastName: lastName || undefined,
      email: email || undefined,
      birthday: birthday || undefined,
      gender: gender || undefined,
      phone: phone || undefined,
      betterPatientId: betterPatientId || undefined,
    } as DBPatient;
    // Composite key
    if (email && birthday) patientRequestData['itemId'] = `PATIENT#${email}#${birthday}`;
        console.log('PATIENT UPDATE', patientRequestData)
    return this.updateItem(patientRequestData).then(() => patientRequestData);
}
0
votes

Here's the batch update function I use, with an emphasis on readability.

const documentClient = new AWS.DynamoDB.DocumentClient(options);

const update = async ({  tableName,  primaryKeyName,  primaryKeyValue,  updates }) => {
    const keys = Object.keys(updates)
    const keyNameExpressions = keys.map(name => `#${name}`)
    const keyValueExpressions = keys.map(value => `:${value}`)
    const UpdateExpression = "set " + keyNameExpressions
        .map((nameExpr, idx) => `${nameExpr} = ${keyValueExpressions[idx]}`)
        .join(", "),
    const ExpressionAttributeNames = keyNameExpressions
        .reduce((exprs, nameExpr, idx) => ({ ...exprs, [nameExpr]: keys[idx] }), {})
    const ExpressionAttributeValues = keyValueExpressions
        .reduce((exprs, valueExpr, idx) => ({ ...exprs, [valueExpr]: updates[keys[idx]] }), {})

    const params = {
        TableName: tableName,
        Key: { [primaryKeyName]: primaryKeyValue },
        UpdateExpression,
        ExpressionAttributeNames,
        ExpressionAttributeValues
    };
    return documentClient.update(params).promise();
}

// USAGE
let { ID, ...fields} = {
    ID: "1234",
    field1: "hello",
    field2: "world"
}

update('tableName', 'ID', ID, fields)