Skip to main content

Usage

DynamoDB-Toolbox mainly exposes three classes:

  • 🏗️ Tables that describe the configuration of your DynamoDB Tables
  • 🐶 Entities that categorize the items contained in your Tables
  • 📐 Schemas that list the attributes of your entities

Instantiation

import { Table } from 'dynamodb-toolbox/table'
import { Entity } from 'dynamodb-toolbox/entity'
import { schema } from 'dynamodb-toolbox/schema'

// Define a Table
const PokeTable = new Table(...)

// Define an entity
const PokemonEntity = new Entity({
// Assign it to a table
table: PokeTable,
// Specify its schema
schema: schema(...)
...
})

An entity must belong to a Table, but the same Table can contain items from several entities. DynamoDB-Toolbox is designed with Single Tables in mind, but works just as well with multiple tables, it'll still make your life much easier (batch gets and writes support multiple tables, so we've got you covered).

Once you have defined your Tables and Entities. You can start using them in combination with Actions.

Methods vs Actions

Queries, updates, transactions, batch operations... DynamoDB has a wide range of features. Exposing all of them as distinct methods would bloat the Entity and Table classes. Class methods are not tree-shakable, and why bother bundling the code needed for a feature (which can be quite large) if you don't need it?

Instead, Tables, Entities and Schemas have a single .build method which is exactly 1-line long 🤯 and acts as a gateway to perform Actions:

import { GetItemCommand } from 'dynamodb-toolbox/entity/actions/get'

const { Item } = await PokemonEntity.build(GetItemCommand)
.key(key)
.send()
note

Notice how the action is imported through a deep import, thanks to the exports field of the package.json.

Although all classes and actions are exposed in the main entry path, we recommend using subpaths, and that's what we'll do in the rest of the documentation.

DynamoDB operations like the GetItemCommand are instances of actions, but DynamoDB-Toolbox also exposes utility actions, e.g. for parsing, formatting or spying.

The syntax is a bit more verbose than a simple PokemonEntity.get(key), but it allows for extensibility, better code-splitting and lighter bundles while keeping an intuitive entity-oriented and type-inheriting syntax.

info

Note that if you don't mind large bundle sizes, you can still use the TableRepository and EntityRepository actions that expose all the others as methods.

Aborting an Action

All the actions that use the DocumentClient (like the GetItemCommand) expose an asynchronous .send() method to perform the underlying operation.

Any option provided to this method is passed to the DocumentClient. This includes the abortSignal option mentioned in the AWS SDK documentation:

const abortController = new AbortController()
const abortSignal = abortController.signal

const { Item } = await PokemonEntity.build(GetItemCommand)
.key(key)
.send({ abortSignal })

// 👇 Aborts the command
abortController.abort()

How do Actions work?

There are three types of actions: Table Actions, Entity Actions and Schema Actions.

Each type of action is essentially a class that respectively accepts a Table, Entity or a Schema as the first parameter of its constructor, with all other parameters being optional.

For instance, here's the definition of a simple NameGetter action that... well, gets the name of an Entity:

import {
Entity,
EntityAction
} from 'dynamodb-toolbox/entity'

export class NameGetter<
ENTITY extends Entity = Entity
> extends EntityAction<ENTITY> {
constructor(entity: ENTITY) {
super(entity)
}

get(): ENTITY['name'] {
return this.entity.name
}
}

const pokeNameGetter = PokemonEntity.build(NameGetter)
// => NameGetter<typeof PokemonEntity>
const pokemonEntityName = pokeNameGetter.get()
// => "POKEMON"

PokemonEntity.build simply instanciates a new action with PokemonEntity as the constructor first parameter. Another way to do it would be:

const pokeNameGetter = new NameGetter(PokemonEntity)

Although, we find, this action-oriented syntax is less readable than the entity-oriented one, it leads to exactly the same result, so feel free to use it if you prefer!

Here's a comparison of both syntaxes on the GetItemCommand action:

// 👇 Entity-oriented
const { Item } = await PokemonEntity.build(GetItemCommand)
.key({ pokemonId: 'pikachu1' })
.options({ consistent: true })
.send()

// 👇 Action-oriented
const { Item } = await new GetItemCommand(
PokemonEntity,
{ pokemonId: 'pikachu1' },
{ consistent: true }
).send()