Search code examples
node.jstypescriptaws-lambdaamazon-dynamodbamazon-dynamodb-streams

How to map a DynamoDB Streams object to a Javascript object?


I have a Lambda trigger hooked up to DynamoDB Streams. The relevant DynamoDB document is passed to the Lambda object as an AttributeValue.

My Lambda function looks like this:

import { Handler, Context, Callback } from 'aws-lambda'
import { config, DynamoDB } from 'aws-sdk'

const handler: Handler = async (event: any, context: Context, callback: Callback) => {
  if (!event) {
    return callback(null, `Failed processing records. event is null.`);
  }
  if (!event.Records) {
    return callback(null, `Failed processing records. No records present.`);
  }
  event.Records.forEach((record) => {
    console.log(record.eventID);
    console.log(record.eventName);
    console.log('DynamoDB Record: %j', record.dynamodb);
    //An example of record.dynamodb is shown below.
  });
  return callback(null, `Successfully processed ${event.Records.length} records.`);
}

export { handler }

An example of the AttributeValue looks like this:

{
  "ApproximateCreationDateTime": 1554660820,
  "Keys": {
    "id": {
      "S": "ab66eb3e-045e-41d6-9633-75597cd47234"
    },
    "date_time": {
      "S": "2019-04-07T18:13:40.084Z"
    }
  },
  "NewImage": {
    "some_property": {
      "S": "some data"
    },
    "another_property": {
      "S": "more data"
    },
    "id": {
      "S": "ab66eb3e-045e-41d6-9633-75597cd47234"
    },
    "date_time": {
      "S": "2019-04-07T18:13:40.084Z"
    }
  },
  "SequenceNumber": "60215400000000011976585954",
  "SizeBytes": 1693,
  "StreamViewType": "NEW_IMAGE"
}

How do I map the AttributeValue to a javascript object, preferably using a Typescript definition? I.e. if I define a Typescript class like this:

class DynamoDBDocument {
    id: string;
    date_time: Date;
    some_property: string
    another_property: string
}

it would result in an object like this:

{
    "id": "ab66eb3e-045e-41d6-9633-75597cd47234",
    "date_time": "S": "2019-04-07T18:13:40.084Z",
    "some_property": "some data",
    "another_property": "more data"
}

DynamoDB Data Mapper looks like a promising solution, but I can't figure out how to use it with the AttributeValue.


Solution

  • Here is a solution using @shiftcoders/dynamo-easy:

    First define your model (make sure to define the correct date mapper if anything other than ISO string is required)

    import { DateProperty, Model, PartitionKeyUUID, SortKey } from '@shiftcoders/dynamo-easy'
    
    @Model({ tableName: 'dynamo-table-name' })
    class CustomModel {
      // hash key with auto generated uuid
      @PartitionKeyUUID()
      id: string
    
      // range key
      @SortKey()
      @DateProperty()
      date_time: Date
    
      some_property: string
      another_property: string
    }
    

    implement the handler function

    import { DateProperty, fromDb, Model, PartitionKeyUUID, SortKey } from '@shiftcoders/dynamo-easy'
    import { Callback, Context } from 'aws-lambda'
    import * as DynamoDB from 'aws-sdk/clients/dynamodb'
    import { Handler } from 'aws-sdk/clients/lambda'
    
    const handler: Handler = async (event: any, context: Context, callback: Callback) => {
      event.Records.forEach((record) => {
        const newImage: DynamoDB.AttributeMap = record.dynamodb.NewImage
        if (newImage) {
          // map the dynamoDB attributes to a JS object using the CustomModel
          const newObject = fromDb(newImage, CustomModel)
          console.log(`item with id ${newObject.id} / ${newObject.date_time.toDateString()} changed to %j`, newObject)
    
          // start with the business logic which requires the newObject
        }
    
      })
    
      // no need to use callback when using async handler
      return `Successfully processed ${event.Records.length} records.`
    }
    

    Checkout the docs for more information on how to execute requests if eventually required by your business logic.