Search code examples
node.jstypescriptamazon-web-servicesamazon-dynamodb

AWS Dynamodb document client UpdateCommand


The UpdateCommand in the AWS DynamoDB documentation is not very well documented and hard to use.

I wanted to use the update command to just take a Javascript object and insert all of the keys from that onto the object matching the primary key.

Looking at the InsertCommand I thought it would be simple like this:

async updateItem(tableName: string, primaryKeyName: string, id: string, item: { [key: string]: any }) {
    const input: UpdateCommandInput = {
      Key: {
        [primaryKeyName]: id,
      },
      AttributeUpdates: item,
      TableName: tableName,
    };

    const command = new UpdateCommand(input);

    return await this.client.send(command);
  }

But this seems to be failing.


Solution

  • Now when I was looking around the documentation was hard to come by but I managed to piece it together over several different sources.

    I have created a function that will take an object and then just overwrite with all those properties on the object referenced with primary key. I hope this can be of help to others having a hard time figuring out how UpdateCommand works.

    import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
    import {
      UpdateCommandInput,
      UpdateCommand,
      DynamoDBDocumentClient,
    } from '@aws-sdk/lib-dynamodb';
    
    export class DynamoDBService {
      private client: DynamoDBDocumentClient;
      constructor() {
        const client = new DynamoDBClient({
          region: config.get(<<your_region>>),
        });
        const marshallOptions = {
          // Whether to automatically convert empty strings, blobs, and sets to `null`.
          convertEmptyValues: false, // false, by default.
          // Whether to remove undefined values while marshalling.
          removeUndefinedValues: false, // false, by default.
          // Whether to convert typeof object to map attribute.
          convertClassInstanceToMap: true, // false, by default.
        };
        this.client = DynamoDBDocumentClient.from(client, { marshallOptions });
      }
    
      /**
       * Takes a javascript object and transforms it into update expressions on the dynamodb object.
       *
       * It translates all of the actions to SET which will overwrite any attribute that is already there.
       * It works best with simple types but can also serialise arrays and objects.
       *
       * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-dynamodb-utilities.html
       *
       * @param tableName Name of the table to update on
       * @param primaryKeyName The primary key name to update on
       * @param id The primary key value
       * @param item The item to update
       */
      async upsertProperties(tableName: string, primaryKeyName: string, id: string, item: { [key: string]: any }) {
        const updatedItem = this.removePrimaryKey(primaryKeyName, item);
        const { updateExpression, expressionAttribute, expressionAttributeNames } =
          this.createUpdateExpressions(updatedItem);
        const input: UpdateCommandInput = {
          Key: {
            [primaryKeyName]: id,
          },
          UpdateExpression: `SET ${updateExpression.join(', ')}`,
          ExpressionAttributeValues: expressionAttribute,
          ExpressionAttributeNames: expressionAttributeNames,
          TableName: tableName,
        };
    
        const command = new UpdateCommand(input);
        return await this.client.send(command);
      }
    
      /**
       * We alias properties to be sure we can insert reserved names (status fx).
       *
       * It is a bit complicated:
       * updateExpression: The actual update state, example: SET #alias = :placeholder
       * expressionAttribute: The value to insert in placeholder example: :placeholder = value
       * expressionAttributeNames: Are the aliases properties to avoid clashe: #alias = key
       *
       * So in the end it ties together and both the inserted value and alias are fixed.
       */
      private createUpdateExpressions(item: { [key: string]: any }) {
        const updateExpression: string[] = [];
        const expressionAttribute: { [key: string]: any } = {};
        const expressionAttributeNames: { [key: string]: any } = {};
        Object.keys(item).map((key) => {
          const placeholder = `:p${key}`;
          const alias = `#a${key}`;
          updateExpression.push(`${alias} = ${placeholder}`);
          expressionAttribute[placeholder] = item[key];
          expressionAttributeNames[alias] = key;
        });
        return { updateExpression, expressionAttribute, expressionAttributeNames };
      }
    
      /**
       * Remove primary key from the object or else we cannot make an
       * update statement because the primary key would be overwritten
       * which violates the insert.
       *
       * Copy to new object to ensure we don't manipulate the reference.
       */
      private removePrimaryKey(primaryKeyName: string, item: { [key: string]: any }) {
        const itemWithoutPrimaryKey = { ...item };
        delete itemWithoutPrimaryKey[primaryKeyName];
        return itemWithoutPrimaryKey;
      }
    }