Skip to content

brandonburrus/dynamo-document-builder

Dynamo Document Builder

DynamoDB single table design and data validation made easy using TypeScript and Zod ⚡️

Full documentation can be found at dynamodocumentbuilder.com.

Features

  • Data integraty using Zod for schema validation
  • Follows Single Table Design principles
  • Easy ergonomic API for a better DynamoDB developer experience
  • Built from the group up to be type-safe
  • Completely tree-shakable for minimal bundle sizes
  • Extensively documented with guides, example code, and complete API reference

Installation

npm i dynamo-document-builder zod @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

Getting Started

import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoTable, DynamoEntity, type Entity, Put, Get } from 'dynamo-document-builder';
import { z } from 'zod';

// Define you DynamoDB clients
const dynamoDbClient = new DynamoDBClient();
const docClient = DynamoDBDocumentClient.from(dynamoDbClient);

// Define your table
const myTable = new DynamoTable({
  tableName: 'MyDynamoTable',
  documentClient: docClient,
  keyNames: {
    partitionKey: 'PK',
    sortKey: 'SK',
  },
});

// Define an entity that belongs to the table
const userEntity = new DynamoEntity({
  table: myTable,
  schema: z.object({
    id: z.string(),
    name: z.string(),
    email: z.string().email(),
  }),
  partitionKey: user => `USER#${user.id}`,
  sortKey: user => `EMAIL#${user.email}`,
});

// Infer the TypeScript type of the entity from the schema
type User = Entity<typeof userEntity>;

// Create a new item
const newUser: User = {
  id: '123',
  name: 'John Doe',
  email: 'johndoe@example.com',
};

// Put the item to DynamoDB
await userEntity.send(new Put({
  item: newUser, // Validated against the Zod schema
}))

// Get the item back from DynamoDB
const retrievedUser = await userEntity.send(new Get({
  // PK and SK auto-built from the key functions in the Entity
  key: {
    id: '123',
    email: 'johndoe@example.com',
  },
}));

Examples

Get

Retrieve a single item by primary key:

import { Get } from 'dynamo-document-builder';

// Basic get
const { item } = await userEntity.send(new Get({
  key: {
    id: '123',
    email: 'johndoe@example.com',
  },
}));

// Strongly consistent read
const { item } = await userEntity.send(new Get({
  key: {
    id: '123',
    email: 'johndoe@example.com',
  },
  consistent: true,
}));

Put

Create or replace an item:

import { Put, ConditionalPut } from 'dynamo-document-builder';

// Basic put
await userEntity.send(new Put({
  item: {
    id: '123',
    name: 'John Doe',
    email: 'johndoe@example.com',
  },
}));

// Put with condition (only if item doesn't exist)
await userEntity.send(new ConditionalPut({
  item: {
    id: '456',
    name: 'Jane Smith',
    email: 'janesmith@example.com',
  },
  condition: { id: notExists() },
}));

Update

Modify existing item attributes:

import { Update } from 'dynamo-document-builder';
import { add, append } from 'dynamo-document-builder';

await userEntity.send(new Update({
  key: { id: '123', email: 'johndoe@example.com' },
  updates: {
    name: 'John D. Doe',
    loginCount: add(1),
    tags: append(['premium']),
  },
  returnValues: 'ALL_NEW',
}));

Delete

Remove an item:

import { Delete, ConditionalDelete } from 'dynamo-document-builder';

// Basic delete
await userEntity.send(new Delete({
  key: {
    id: '123',
    email: 'johndoe@example.com',
  },
}));

// Conditional delete
await userEntity.send(new ConditionalDelete({
  key: {
    id: '456',
    email: 'janesmith@example.com',
  },
  condition: {
    status: 'inactive',
  },
}));

Query

Retrieve multiple items by partition key:

import { Query } from 'dynamo-document-builder';
import { beginsWith, greaterThan } from 'dynamo-document-builder';

// Query all items with a specific partition key
const { items } = await userEntity.send(new Query({
  key: {
    id: '123',
  },
}));

// Query with sort key condition
const { items, lastEvaluatedKey } = await userEntity.send(new Query({
  key: {
    id: '123',
  },
  sortKeyCondition: {
    SK: beginsWith('EMAIL#'),
  },
  limit: 10,
}));

// Paginate through results
for await (const page of userEntity.paginate(new Query({
  key: {
    id: '123',
  },
  pageSize: 50,
}))) {
  console.log(`Found ${page.count} items`);
  processItems(page.items);
}

Scan

Scan entire table (use sparingly, this an expensive operation):

import { Scan } from 'dynamo-document-builder';

// Full table scan
const { items, scannedCount } = await userEntity.send(new Scan());

// Scan with filter and limit
const { items, scannedCount } = await userEntity.send(new Scan({
  filter: {
    status: 'active',
  },
  limit: 100,
}));

// Parallel scan
async function parallelScan(totalSegments: number) {
  const scanPromises = Array.from({ length: totalSegments }, (_, i) =>
    userEntity.send(new Scan({
      segment: i,
      totalSegments: totalSegments,
    }))
  );
  const results = await Promise.all(scanPromises);
  return results.flatMap(r => r.items);
}

BatchGet

Retrieve multiple items by primary keys:

import { BatchGet } from 'dynamo-document-builder';

const { items, unprocessedKeys } = await userEntity.send(new BatchGet({
  keys: [
    { id: '123', email: 'johndoe@example.com' },
    { id: '456', email: 'janesmith@example.com' },
    { id: '789', email: 'bobwilson@example.com' },
  ],
}));

// Handle unprocessed keys
if (unprocessedKeys?.length) {
  // Retry
  await sleep(100);
  const { items: retryItems } = await userEntity.send(new BatchGet({
    keys: unprocessedKeys,
  }));
}

BatchWrite

Put and/or delete multiple items:

import { BatchWrite } from 'dynamo-document-builder';

const { unprocessedPuts, unprocessedDeletes } = await userEntity.send(new BatchWrite({
  items: [
    { id: '123', name: 'User 1', email: 'user1@example.com' },
    { id: '456', name: 'User 2', email: 'user2@example.com' },
  ],
  deletes: [
    { id: '789', email: 'user3@example.com' },
  ],
}));

TransactGet

Transactional read of multiple items:

import { TransactGet } from 'dynamo-document-builder';

const { items } = await userEntity.send(new TransactGet({
  keys: [
    { id: '123', email: 'johndoe@example.com' },
    { id: '456', email: 'janesmith@example.com' },
  ],
}));

// Items array has same order as keys
// Undefined if item not found
if (items[0]) {
  console.log('First user:', items[0].name);
}

TableBatchWrite

Put and/or delete multiple items across multiple entity types in a single request:

import { TableBatchWrite, BatchWrite } from 'dynamo-document-builder';

const { unprocessedPuts, unprocessedDeletes } = await myTable.send(
  new TableBatchWrite({
    writes: [
      userEntity.prepare(new BatchWrite({
        items: [
          { id: '1', name: 'Alice', email: 'alice@example.com' },
          { id: '2', name: 'Bob', email: 'bob@example.com' },
        ],
      })),
      orderEntity.prepare(new BatchWrite({
        items: [{ orderId: '100', userId: '1', total: 99.99 }],
        deletes: [{ orderId: '99' }],
      })),
    ],
  }),
);

// Tuple results typed per entity
const [userUnprocessedPuts, orderUnprocessedPuts] = unprocessedPuts;

TableBatchGet

Retrieve multiple items across multiple entity types in a single request:

import { TableBatchGet, BatchGet } from 'dynamo-document-builder';

const { items } = await myTable.send(
  new TableBatchGet({
    gets: [
      userEntity.prepare(new BatchGet({
        keys: [{ id: '1', email: 'alice@example.com' }],
      })),
      orderEntity.prepare(new BatchGet({
        keys: [{ orderId: '100' }, { orderId: '101' }],
      })),
    ],
  }),
);

// Tuple results typed per entity
const [users, orders] = items;
// users: User[]
// orders: Order[]

TableTransactWrite

Atomic multi-entity write transaction across multiple entity types:

import { TableTransactWrite } from 'dynamo-document-builder';
import { Put, Update, Delete } from 'dynamo-document-builder';

await myTable.send(
  new TableTransactWrite({
    transactions: [
      userEntity.prepare([
        new Put({ item: { id: '1', name: 'Alice', email: 'alice@example.com' } }),
      ]),
      orderEntity.prepare([
        new Update({ key: { orderId: '100' }, updates: { status: 'shipped' } }),
        new Delete({ key: { orderId: '99' } }),
      ]),
    ],
  }),
);

TableTransactGet

Transactional read across multiple entity types (all-or-nothing, strongly consistent):

import { TableTransactGet, TransactGet } from 'dynamo-document-builder';

const { items } = await myTable.send(
  new TableTransactGet({
    gets: [
      userEntity.prepare(new TransactGet({
        keys: [{ id: '1', email: 'alice@example.com' }],
      })),
      orderEntity.prepare(new TransactGet({
        keys: [{ orderId: '100' }, { orderId: '101' }],
      })),
    ],
  }),
);

// Tuple results typed per entity, undefined if not found
const [users, orders] = items;
import { TransactWrite, ConditionCheck } from 'dynamo-document-builder';
import { Put, Update, Delete } from 'dynamo-document-builder';
import { add, notExists } from 'dynamo-document-builder';

await userEntity.send(new TransactWrite({
  writes: [
    // Create new user
    new Put({ 
      item: { 
        id: '999', 
        name: 'New User', 
        email: 'newuser@example.com' 
      } 
    }),
    // Update existing user
    new Update({ 
      key: { id: '123', email: 'johndoe@example.com' },
      updates: { loginCount: add(1) }
    }),
    // Delete inactive user
    new Delete({ 
      key: { id: '456', email: 'janesmith@example.com' } 
    }),
    // Verify another user exists
    new ConditionCheck({
      key: { id: '789', email: 'admin@example.com' },
      condition: { role: 'admin' },
    }),
  ],
}));

About

DynamoDB single table design and data validation made easy using TypeScript and Zod ⚡️

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Contributors