Dustin Coates in Alexa

DynamoDB Attributes Helper: A continued in-depth analysis of the Alexa Skills Kit SDK for Node.js

In this post, we’re going to look at the DyamodDB attributes helper. As a reminder, this is the Dig Deep series, where we look line-by-line at the tools and libraries we use to build voice-first experiences. This is not the place to go for tutorials, but if you want to learn interesting little nuggets about what you use every day, off we go…

Something interesting to note right out of the gate is that these helpers aren’t exported beyond the internal needs to build the SDK. Which means that if you have your own DynamoDB needs, you’ll have to write your own code to handle it. It seems like a bit of a waste, but we’ll see soon that the use case of this is rather specific so it wouldn’t be able to be generalized anyway.

The wild thing that we’ll see soon is that you don’t even need to create a table ahead of time!

The Returned Value

The exports from this file is simply an object with get and set. We already saw get in ValidateRequest:

attributesHelper.get(this.dynamoDBTableName, userId, (err, data) => {
  // Truncated
});

While set is used only in the response handlers, which we haven’t looked at yet.

get

This takes three arguments: table, userId, and callback, which in turn takes arguments for an error string and an attributes object..

if(!table) {
  callback('DynamoDB Table name is not set.', null);
}

if(!doc) {
  doc = new aws.DynamoDB.DocumentClient({apiVersion: '2012-08-10'});
}

Here we see that we exit early and do nothing if no table name is set, which makes sense.

And if no doc is set (which is global to the file), then we’re creating a new DocumentClient. Note that it uses the current version of the low-level API, rather than the previous 2011-12-05, which again makes sense.

var params = {
  Key: {
    userId: userId
  },
  TableName: table,
  ConsistentRead: true
};

The most interesting thing here is that we are using strongly consistent reads. DynamoDB has two types of consistency (i.e. ensuring that data is synced across all nodes/machines/regions): eventually consistent and strongly consistent. Eventually consistent allows you to retrieve a value from a DynamoDB node before it’s been updated across all nodes. This means that there might be nodes that disagree with each other and outdated data on some nodes. Strongly consistent implies the opposite, which is that new data is not available on any nodes until it is available on all nodes. This has the upshot that latency or network issues might delay syncing and being able to retrieve new data.

doc.get

doc.get(params, function(err, data){
  if(err) {
    console.log('get error: ' + JSON.stringify(err, null, 4));

    if(err.code === 'ResourceNotFoundException') {
      var dynamoClient = new aws.DynamoDB();
      newTableParams['TableName'] = table;
      dynamoClient.createTable(newTableParams, function (err, data) {
        if(err) {
          console.log('Error creating table: ' + JSON.stringify(err, null, 4));
        }
        console.log('Creating table ' + table + ':\n' + JSON.stringify(data));
        callback(err, {});
      });
    } else {
      callback(err, null);
    }
  } else {
    if(isEmptyObject(data)) {
      callback(null, {});
    } else {
      callback(null, data.Item['mapAttr']);
    }
  }
});

There’s only one thing that’s out of the ordinary here. See it? If we don’t have a table already created, this creates one on our behalf! (Insert applause gif of choice here.) Of course, if you want this to happen, your Lambda function has to have table creation permissions. If you don’t want to grant those, create a table ahead of time with a primary key of userId. (Recall that the table name was passed in from our configurations.)

The newTableParams on which we set our TableName is an object that largely just sets our primary key as userId and our provisioned throughput, which is a reflection on our capacity to do reads and writes over a given second. Throughput uses “units” which looks at the consistency of the table and how large the record is. For reads, a record of 4KB takes one unit. For writes, a record of 1KB takes one.

Reproduced in full:

var newTableParams = {
  AttributeDefinitions: [
    {
      AttributeName: 'userId',
      AttributeType: 'S'
    }
  ],
  KeySchema: [
    {
      AttributeName: 'userId',
      KeyType: 'HASH'
    }
  ],
  ProvisionedThroughput: {
    ReadCapacityUnits: 5,
    WriteCapacityUnits: 5
  }
};

set

I won’t reproduce set here, since it’s pretty much just get with a put insead of a get. The only thing to note is that it won’t create a table for us. This is of course because get should always be called before set anyway, i.e. at the beginning of a new session.

As you can see, the DynamoDB attributes helper is incredibly simple. It has one fairly interesting piece where it creates a database for us, but is otherwise a simple get and put. I’ll leave you with one thing to note: you can’t set your own DynamoDB client. Why would you want to? For example, to use DynamoDB local or a mock for testing purposes. There’s a PR to solve this but no movement toward merging it into the SDK.