It's Luis' blog

A Simple TypeScript Class to query information from DynamoDB

May 07, 2022

I wrote a wrapper for DocClient which I think some people might find useful. The main purpose is to simplify retreiving and inserting data, especially for Lambda functions that call DynamoDB. You can see the code here.

It requires the aws-sdk, lodash and uuid, which you can download with:

npm install aws-sdk lodash uuid

It also assumes that you use id as your main key, which is most likely the case.

Let’s see how it works, maybe you find it interesting!

Constructor

You need to start a new instance of the class with the name of the table:

const posts = new DynamoTable('posts', 'us-east-1'); // the region is optional

One of the advantages of storing the name in the class is that you can use the same variable for both production and development.

const posts = new DynamoTable(isProd ? 'posts' : 'dev_posts', 'us-east-1'); 

Methods

This instance now contains the following methods:

  • addItem(item)
  • updateItem(item)
  • deleteItem(id)
  • getItem(id)
  • batchGetItem(ids)
  • batchWriteItem(ids)
  • scan({ nextToken, limit, filter })
  • simpleScan(filter)
  • scanAll(filter)
  • query(index, queryExpression, nextToken, limit, filter)
  • simpleQuery(index, queryExpression, filter)
  • queryAll(index, queryExpression, filter)

Let’s see how to use them!

Inserting

You can add single items with addItem. It will automatically generate an ID if you pass one.

import DynamoTable from './DynamoTable';

const posts = new DynamoTable('posts', 'us-east-1'); // the region is optional

async function main() {
  const post = {
    title: 'New post',
    content: 'I am the body!'
  };

  const newPost = await posts.addItem(post);
  console.log(newPost);
  /*
    {
      id: '7da9576c-a97c-47fc-a884-fbc7fda3ab3a',
      title: 'New post',
      content: 'I am the body!'
    }
  */
}

main();

You can insert multiple items using batchWriteItem:

import DynamoTable from './DynamoTable';

const posts = new DynamoTable('posts', 'us-east-1'); // the region is optional

async function main() {
  const post1 = {
    title: 'New post 1',
    content: 'I am the body of post 1!'
  };
  const post2 = {
    title: 'New post 2',
    content: 'I am the body of post 2!'
  };

  await posts.batchWriteItem([post1, post2]);
}

main();

Updating

You can update a post using the updateItem, which allows you to specify the fields you want to update only. It also returns the full item so you can pass it as a response to your API.

import DynamoTable from './DynamoTable';

const posts = new DynamoTable('posts', 'us-east-1'); // the region is optional

async function main() {
  const postUpdated = {
    id: '7da9576c-a97c-47fc-a884-fbc7fda3ab3a',
    title: 'New post updated',
  }

  const newPost = await posts.updateItem(postUpdated);
  console.log(newPost);
  /*
    {
      content: 'I am the body!',
      id: '7da9576c-a97c-47fc-a884-fbc7fda3ab3a',
      title: 'New post updated'
    }
  */
}

main();

Retrieving

The class supports 4 ways of retreiving data: A single item, multiple items, and listing by scanning or querying.

The simplest one is getting a simple item using its ID:

import DynamoTable from './DynamoTable';

const posts = new DynamoTable('posts', 'us-east-1'); // the region is optional

async function main() {
  const post = await posts.getItem('7da9576c-a97c-47fc-a884-fbc7fda3ab3a');
  console.log(post);
  /*
    {
      content: 'I am the body!',
      id: '7da9576c-a97c-47fc-a884-fbc7fda3ab3a',
      title: 'New post updated'
    }
  */
}

main();

But you can also get a bunch of items using their IDs:

const items = await posts.batchGetItem([
  '767311af-b122-420d-9b7f-a5692dbfbd45',
  'd7fce7ab-252f-4b66-a1f8-fc940db14f5c',
]);
console.log(items);
/*
[
  {
    authorId: '1',
    content: 'Title 1',
    id: '767311af-b122-420d-9b7f-a5692dbfbd45',
    title: 'Post 1'
  },
  {
    authorId: '2',
    content: 'Title 3',
    id: 'd7fce7ab-252f-4b66-a1f8-fc940db14f5c',
    title: 'Post 3'
  }
]
*/

There are three methods to scan a table. A base scan method, which is friendly to the way you probably use scan. A simpleScan method which ignores pagination, and a scanAll method which will continue to retrieve data until there’s nothing more.

The scan method accepts one parameter with 3 fields: nextToken, limit and filter.

  • nextToken tells DynamoDB to retrieve items after this key.
  • limit determines the maximum amount of items to retrieve.
  • filter can either be an object like { key: value } (for key = value) or expression and values (for something like attribute_not_exists(:example))

The method returns items (an array) and nextToken (a string or null).

You can retrieve all items from a table like this:

const postsScan = await posts.scan();
console.log(postsScan);
/*
  {
    items: [
      {
        content: 'I am the body!',
        id: '7da9576c-a97c-47fc-a884-fbc7fda3ab3a',
        title: 'New post updated'
      },
      {
        content: 'I am the body of post 1!',
        id: '7796b42d-4e20-4cc1-ab85-ca3240da5991',
        title: 'New post 1'
      },
      {
        content: 'I am the body of post 2!',
        id: 'fb4d00ab-ffd8-473d-8e5f-bb506506ab30',
        title: 'New post 2'
      }
    ],
    nextToken: null
  }
*/

You can do a scanAll to keep retrieving items until there are no more:

  const postsScan = await posts.scanAll();
  console.log(postsScan);
  /*
    [
      {
        content: 'I am the body!',
        id: '7da9576c-a97c-47fc-a884-fbc7fda3ab3a',
        title: 'New post updated'
      },
      {
        content: 'I am the body of post 1!',
        id: '7796b42d-4e20-4cc1-ab85-ca3240da5991',
        title: 'New post 1'
      },
      {
        content: 'I am the body of post 2!',
        id: 'fb4d00ab-ffd8-473d-8e5f-bb506506ab30',
        title: 'New post 2'
      }
    ]
  */

A simple simpleScan will return the first batch of scan, without pagination information.

Filtering

Before moving into queries, let’s add an “authorId” key to our posts table so we scan and filter using it.

const postsToInsert = [
  {
    authorId: '1',
    content: 'Title 1',
    title: 'Post 1',
  },
  {
    authorId: '1',
    content: 'Title 2',
    title: 'Post 2',
  },
  {
    authorId: '2',
    content: 'Title 3',
    title: 'Post 3',
  },
  {
    authorId: '4',
    content: 'Title 4',
    title: 'Post 4',
  },
];

await posts.batchWriteItem(postsToInsert);

We can now scan and filter for “authorId”:

const postsByAuthor1 = await posts.scan({ filter: { authorId: '1' } }); // expression would be authorId = 1
console.log(postsByAuthor1);
/*
{
  items: [
    {
      authorId: '1',
      content: 'Title 1',
      id: '767311af-b122-420d-9b7f-a5692dbfbd45',
      title: 'Post 1'
    },
    {
      authorId: '1',
      content: 'Title 2',
      id: 'a46ec412-1e95-4c9c-a24e-1d4d15092d3f',
      title: 'Post 2'
    }
  ],
  nextToken: null
}
*/

For more complex, or even custom filters, you can use an expression and values:

const postsByAuthor1 = await posts.scan({
  filter: {
    expression: 'authorId = :authorId',
    values: {
      authorId: '1'
    }
  }
});
console.log(postsByAuthor1);
/*
{
  items: [
    {
      authorId: '1',
      content: 'Title 1',
      id: '767311af-b122-420d-9b7f-a5692dbfbd45',
      title: 'Post 1'
    },
    {
      authorId: '1',
      content: 'Title 2',
      id: 'a46ec412-1e95-4c9c-a24e-1d4d15092d3f',
      title: 'Post 2'
    }
  ],
  nextToken: null
}
*/

Querying

Now we can create an index for our ‘authorId’ field, called ‘authorId-index’.

const postsByAuthor1 = await posts.query({
  index: 'authorId-index',
  queryExpression: { authorId: '1' }
});
console.log(postsByAuthor1);
/*
{
  items: [
    {
      content: 'Title 1',
      authorId: '1',
      id: '767311af-b122-420d-9b7f-a5692dbfbd45',
      title: 'Post 1'
    },
    {
      content: 'Title 2',
      authorId: '1',
      id: 'a46ec412-1e95-4c9c-a24e-1d4d15092d3f',
      title: 'Post 2'
    }
  ],
  nextToken: null
}
*/

query also accepts a filter, nextToken and limit much like a scan, for the results after the query.

You can also use simpleQuery like simpleScan:

const postsByAuthor1 = await posts.simpleQuery('authorId-index', { authorId: '1' });
console.log(postsByAuthor1);
/*
[
  {
    content: 'Title 1',
    authorId: '1',
    id: '767311af-b122-420d-9b7f-a5692dbfbd45',
    title: 'Post 1'
  },
  {
    content: 'Title 2',
    authorId: '1',
    id: 'a46ec412-1e95-4c9c-a24e-1d4d15092d3f',
    title: 'Post 2'
  }
]
*/

simpleQuery doesn’t deal with pagination (so there may be more items) and it accepts a filter as a third parameter.

You also have a queryAll method which does deal with pagination and keeps querying until all items have been retrieved.

const postsByAuthor1 = await posts.queryAll('authorId-index', { authorId: '1' });
console.log(postsByAuthor1);
  /*
[
  {
    content: 'Title 1',
    authorId: '1',
    id: '767311af-b122-420d-9b7f-a5692dbfbd45',
    title: 'Post 1'
  },
  {
    content: 'Title 2',
    authorId: '1',
    id: 'a46ec412-1e95-4c9c-a24e-1d4d15092d3f',
    title: 'Post 2'
  }
]
*/

Deleting

You can delete an item using the deleteItem method:

const deletedPost = await posts.deleteItem('a46ec412-1e95-4c9c-a24e-1d4d15092d3f');
console.log(deletedPost);
/*
{
  authorId: '1',
  content: 'Title 2',
  id: 'a46ec412-1e95-4c9c-a24e-1d4d15092d3f',
  title: 'Post 2'
}
*/

Hope this is useful!