Skip to content

stacksjs/dynamodb-tooling

Social Card of this repo

DynamoDB Tooling

npm version GitHub Actions Commitizen friendly

A comprehensive DynamoDB toolkit for TypeScript/JavaScript with automatic single-table design, Laravel-style ORM, migrations, and CLI tools.

Features

  • Zero-Config Single-Table Design - Automatically generates pk/sk patterns and GSIs from your model definitions
  • Laravel-Style ORM - Familiar query builder with where(), with(), orderBy(), and more
  • Automated Migrations - Schema generation, diffing, and migrations from your models
  • Factory System - Generate fake data for testing with states and sequences
  • Seeding System - Populate your database with test data
  • CLI Tools - Manage tables, run migrations, seed data, and more
  • DynamoDB Local - Built-in support for local development
  • TypeScript First - Full type safety and IntelliSense support

Installation

bun install dynamodb-tooling
# or
npm install dynamodb-tooling

Quick Start

1. Configure DynamoDB

Create a dynamodb.config.ts file:

import type { DynamoDBConfig } from 'dynamodb-tooling'

export default {
  region: 'us-east-1',
  defaultTableName: 'MyApp',

  singleTableDesign: {
    enabled: true,
    partitionKeyName: 'pk',
    sortKeyName: 'sk',
    keyDelimiter: '#',
    gsiCount: 5,
  },

  local: {
    enabled: true,
    port: 8000,
  },
} satisfies DynamoDBConfig

2. Define Models

Create models in your app/models directory:

// app/models/User.ts
import { DynamoDBModel } from 'dynamodb-tooling'

export class User extends DynamoDBModel {
  static table = 'users'
  static primaryKey = 'id'
  static timestamps = true
  static softDeletes = true

  // Attributes
  attributes = {
    id: { type: 'string', required: true },
    email: { type: 'string', required: true, unique: true },
    name: { type: 'string', required: true },
    age: { type: 'number' },
  }

  // Relationships
  relationships = {
    posts: { type: 'hasMany', model: 'Post', foreignKey: 'userId' },
    profile: { type: 'hasOne', model: 'Profile', foreignKey: 'userId' },
  }
}

3. Use the Query Builder

import { User } from './app/models/User'

// Find by ID
const user = await User.find('123')

// Query with conditions
const users = await User.query()
  .where('status', 'active')
  .where('age', '>=', 18)
  .orderByDesc('createdAt')
  .limit(10)
  .get()

// With eager loading
const usersWithPosts = await User.query()
  .with('posts')
  .get()

// Create
const newUser = await User.create({
  email: 'john@example.com',
  name: 'John Doe',
})

// Update
await user.update({ name: 'Jane Doe' })

// Soft delete
await user.delete()

// Force delete
await user.forceDelete()

CLI Commands

The CLI is available as dbtooling:

# DynamoDB Local
dbtooling start              # Start DynamoDB Local
dbtooling stop               # Stop DynamoDB Local
dbtooling status             # Show running instances
dbtooling install            # Install DynamoDB Local

# Migrations
dbtooling migrate            # Run migrations
dbtooling migrate:status     # Show migration status
dbtooling migrate:rollback   # Rollback last migration
dbtooling migrate:fresh      # Drop and re-migrate
dbtooling migrate:generate   # Generate migration from models

# Tables
dbtooling table:create       # Create table from models
dbtooling table:describe     # Describe a table
dbtooling table:list         # List all tables
dbtooling table:delete       # Delete a table

# Seeding
dbtooling seed               # Run seeders
dbtooling make:seeder User   # Generate a seeder
dbtooling make:factory User  # Generate a factory
dbtooling db:fresh           # Drop, migrate, and seed

# Queries
dbtooling query --pk USER#1  # Query by partition key
dbtooling scan               # Scan table
dbtooling get --pk X --sk Y  # Get single item

# Utilities
dbtooling access-patterns    # Show access patterns
dbtooling export             # Export table data
dbtooling import             # Import data
dbtooling ci:validate        # Validate models for CI

Query Builder API

Basic Queries

// Select specific columns
User.query().select('id', 'name', 'email').get()

// Where conditions
User.query().where('status', 'active').get()
User.query().where('age', '>=', 18).get()
User.query().whereIn('role', ['admin', 'moderator']).get()
User.query().whereBetween('age', 18, 65).get()
User.query().whereNull('deletedAt').get()
User.query().whereBeginsWith('sk', 'USER#').get()
User.query().whereContains('tags', 'featured').get()

// Ordering
User.query().orderBy('name').get()
User.query().orderByDesc('createdAt').get()
User.query().latest().get()  // orderBy('createdAt', 'desc')
User.query().oldest().get()  // orderBy('createdAt', 'asc')

// Limiting
User.query().limit(10).get()
User.query().take(10).get()

// Pagination
const page1 = await User.query().paginate(20)
const page2 = await User.query().cursorPaginate(cursor, 20)

// Chunking for large datasets
await User.query().chunk(100, async (users) => {
  for (const user of users) {
    // Process user
  }
})

Relationships

// Eager loading
User.query().with('posts').get()
User.query().with('posts', 'profile').get()
User.query().with('posts.comments').get()  // Nested

// Relationship counts
User.query().withCount('posts').get()

// Relationship existence
User.query().has('posts').get()
User.query().doesntHave('posts').get()
User.query().whereHas('posts', q => q.where('published', true)).get()

Aggregations

await User.query().count()
await User.query().sum('balance')
await User.query().avg('age')
await User.query().min('age')
await User.query().max('age')
await User.query().exists()
await User.query().doesntExist()

Soft Deletes

// Include soft deleted
User.query().withTrashed().get()

// Only soft deleted
User.query().onlyTrashed().get()

// Restore
await user.restore()

// Force delete (permanent)
await user.forceDelete()

Scopes

// Define global scopes
User.addGlobalScope('active', (query) => {
  return query.where('status', 'active')
})

// Use scopes
User.query().scope('active').get()

Factory System

Create factories for generating test data:

// factories/UserFactory.ts
import { Factory, uniqueEmail, randomInt } from 'dynamodb-tooling'

Factory.define('User', {
  entityType: 'USER',
  definition: () => ({
    id: crypto.randomUUID(),
    email: uniqueEmail(),
    name: 'Test User',
    age: randomInt(18, 65),
  }),
  states: {
    admin: { role: 'admin' },
    inactive: { status: 'inactive' },
  },
})

// Usage
const users = await Factory.for('User').count(10).create()
const admin = await Factory.for('User').state('admin').createOne()
const fakeUsers = Factory.for('User').count(5).make()  // No persist

Seeder System

Create seeders to populate your database:

// seeders/UserSeeder.ts
import { Seeder, SeederContext } from 'dynamodb-tooling'

export class UserSeeder extends Seeder {
  static order = 1

  async run(ctx: SeederContext): Promise<void> {
    await ctx.factory('USER', {
      attributes: () => ({
        id: crypto.randomUUID(),
        email: `user${Date.now()}@example.com`,
        name: 'Test User',
      }),
    }).count(50).create()
  }
}

Single-Table Design

The toolkit automatically generates single-table design patterns from your models:

Key Patterns

User:
  pk: USER#{id}
  sk: USER#{id}

Post:
  pk: POST#{id}
  sk: POST#{id}
  gsi1pk: USER#{userId}  (for querying posts by user)
  gsi1sk: POST#{id}

Access Patterns Generated

  1. Get User by ID
  2. Get Post by ID
  3. Get all Posts by User (via GSI1)
  4. Get User's Profile (via relationship)

View patterns with:

dbtooling access-patterns --format markdown

Migration System

Migrations are automatically generated from your models:

# Generate migration
dbtooling migrate:generate

# Preview changes
dbtooling migrate --dry-run

# Apply migration
dbtooling migrate

# Rollback
dbtooling migrate:rollback

The migration system handles:

  • Table creation with pk/sk
  • GSI creation/deletion
  • LSI configuration
  • TTL settings
  • Stream configuration

Configuration Reference

interface DynamoDBConfig {
  // AWS Settings
  region?: string
  endpoint?: string
  credentials?: {
    accessKeyId: string
    secretAccessKey: string
  }

  // Table Settings
  defaultTableName: string
  tableNamePrefix?: string
  tableNameSuffix?: string

  // Single-Table Design
  singleTableDesign: {
    enabled: boolean
    partitionKeyName: string  // default: 'pk'
    sortKeyName: string       // default: 'sk'
    keyDelimiter: string      // default: '#'
    entityTypeAttribute: string  // default: '_type'
    gsiCount: number          // default: 5
  }

  // Query Builder
  queryBuilder: {
    modelsPath: string
    timestampFormat: 'iso' | 'unix' | 'unix_ms'
    softDeletes: {
      enabled: boolean
      attribute: string
    }
  }

  // Capacity
  capacity: {
    billingMode: 'PAY_PER_REQUEST' | 'PROVISIONED'
    read?: number
    write?: number
  }

  // DynamoDB Local
  local: {
    enabled: boolean
    port: number
    installPath: string
  }
}

DynamoDB Local

Start local development:

import { dynamoDb } from 'dynamodb-tooling'

// Start
await dynamoDb.launch({ port: 8000 })

// With persistent storage
await dynamoDb.launch({
  port: 8000,
  dbPath: './data/dynamodb',
})

// Stop
dynamoDb.stop(8000)

Or via CLI:

dbtooling start --port 8000
dbtooling start --db-path ./data  # Persistent
dbtooling stop

Testing

bun test

Changelog

Please see our releases page for more information on what has changed recently.

Contributing

Please review the Contributing Guide for details.

Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

Discussions on GitHub

For casual chit-chat with others using this package:

Join the Stacks Discord Server

Postcardware

Stacks OSS will always stay open-sourced, and we will always love to receive postcards from wherever Stacks is used! And we also publish them on our website. Thank you, Spatie.

Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094, United States 🌎

Sponsors

We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.

Credits

License

The MIT License (MIT). Please see LICENSE for more information.

Made with πŸ’™

About

πŸ› οΈ A simple local DynamoDB API. Without the need for Docker.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •