Joey Yi Zhao
Joey Yi Zhao

Reputation: 42418

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
};

Upvotes: 1

Views: 2401

Answers (3)

Kushal
Kushal

Reputation: 67

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

Upvotes: 1

craig
craig

Reputation: 161

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.

Upvotes: 2

E.J. Brennan
E.J. Brennan

Reputation: 46841

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/

Upvotes: 0

Related Questions