npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ftschopp/dynatable-core

v2.3.2

Published

Core library for DynamoDB single table design

Readme

@ftschopp/dynatable-core

A type-safe, functional programming library for AWS DynamoDB with Single Table Design support. Built with TypeScript and designed to make DynamoDB development elegant and productive.

Features

  • 🔐 Type-Safe - Full TypeScript support with end-to-end type inference
  • 🎯 Single Table Design - Built-in support for DynamoDB best practices
  • 🔄 Functional API - Chainable, composable operations with immutable builders
  • ⚡️ Auto-generated IDs - ULID/UUID generation for unique identifiers
  • 🕒 Automatic Timestamps - Auto-manage createdAt and updatedAt
  • 🔒 Transactions - Atomic operations with TransactWrite and TransactGet
  • 📦 Batch Operations - Efficient BatchGet and BatchWrite operations
  • 🎨 Query Builder - Intuitive, type-safe API for complex queries
  • Validation - Built-in Zod schema validation
  • 🧪 Testable - Easy to mock and test with AWS SDK client mock support

Installation

npm install @ftschopp/dynatable-core
# or
yarn add @ftschopp/dynatable-core
# or
pnpm add @ftschopp/dynatable-core

Quick Start

import { Table } from '@ftschopp/dynatable-core';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

// Define your schema
const schema = {
  format: 'dynatable:1.0.0',
  version: '1.0.0',
  indexes: {
    primary: { hash: 'PK', sort: 'SK' },
  },
  models: {
    User: {
      key: {
        PK: { type: String, value: 'USER#${username}' },
        SK: { type: String, value: 'USER#${username}' },
      },
      attributes: {
        username: { type: String, required: true },
        name: { type: String, required: true },
        email: { type: String, required: true },
      },
    },
  },
  params: {
    timestamps: true,
  },
} as const;

// Create table instance — pass the raw DynamoDBClient.
// Internally Dynatable uses lib-dynamodb commands which marshal/unmarshal
// JS values automatically; you do NOT need to wrap with DynamoDBDocumentClient.
const table = new Table({
  name: 'MyTable',
  client: new DynamoDBClient({ region: 'us-east-1' }),
  schema,
});

// Use it!
async function example() {
  // Create
  const user = await table.entities.User.put({
    username: 'alice',
    name: 'Alice Smith',
    email: '[email protected]',
  }).execute();

  // Read
  const retrieved = await table.entities.User.get({
    username: 'alice',
  }).execute();

  // Update
  await table.entities.User.update({ username: 'alice' }).set('name', 'Alice Johnson').execute();

  // Query
  const users = await table.entities.User.query()
    .where((attr, op) => op.eq(attr.username, 'alice'))
    .execute();

  // Delete
  await table.entities.User.delete({ username: 'alice' }).execute();
}

Core Concepts

Schema Definition

Define your data models with full type inference:

const schema = {
  format: 'dynatable:1.0.0',
  version: '1.0.0',

  indexes: {
    primary: { hash: 'PK', sort: 'SK' },
    gsi1: { hash: 'GSI1PK', sort: 'GSI1SK' },
  },

  models: {
    User: {
      key: {
        PK: { type: String, value: 'USER#${username}' },
        SK: { type: String, value: 'USER#${username}' },
      },
      index: {
        GSI1PK: { type: String, value: 'USER' },
        GSI1SK: { type: String, value: 'USER#${username}' },
      },
      attributes: {
        username: { type: String, required: true },
        name: { type: String, required: true },
        email: { type: String },
        userId: { type: String, generate: 'ulid' },
        followerCount: { type: Number, default: 0 },
        // Nested object with typed schema
        address: {
          type: Object,
          schema: {
            street: { type: String },
            city: { type: String, required: true },
            country: { type: String, required: true },
          },
        },
        // Typed array with item schema
        tags: {
          type: Array,
          default: [],
          items: { type: String },
        },
      },
    },
  },

  params: {
    timestamps: true, // Auto createdAt/updatedAt as ISO 8601 strings
    cleanInternalKeys: false, // Hide PK/SK from results
  },
} as const;

Nested Objects and Arrays

Attributes support typed nested schemas for full TypeScript inference:

Story: {
  attributes: {
    // Typed array of objects
    frames: {
      type: Array,
      default: [],
      items: {
        type: Object,
        schema: {
          url: { type: String, required: true },
          duration: { type: Number },
          mediaType: { type: String },
        },
      },
    },
    // Nested object with a typed schema
    location: {
      type: Object,
      schema: {
        city: { type: String },
        country: { type: String },
        lat: { type: Number },
        lng: { type: Number },
      },
    },
  },
}

Use ArrayItem<T> to extract item types from array attributes:

import type { InferModelFromSchema, ArrayItem } from '@ftschopp/dynatable-core';

type StoryEntity = InferModelFromSchema<typeof schema, 'Story'>;
type StoryFrame = ArrayItem<StoryEntity['frames']>;
// → { url: string; duration?: number; mediaType?: string }

Type Inference

Extract types from your schema:

import type {
  InferModel,
  InferInput,
  InferKeyInput,
  InferModelFromSchema,
  InferInputFromSchema,
  ArrayItem,
} from '@ftschopp/dynatable-core';

// Preferred: infer from the full schema (timestamps included automatically)
type User = InferModelFromSchema<typeof schema, 'User'>;
// { username: string; name: string; email?: string; userId: string; followerCount: number; createdAt: string; updatedAt: string }

type UserInput = InferInputFromSchema<typeof schema, 'User'>;
// { username: string; name: string; email?: string; followerCount?: number }

// Low-level: infer directly from a ModelDefinition.
// Prefer InferModelFromSchema in user code; use this when you only have
// the model in hand (e.g. utilities that take a ModelDefinition).
// Note: does not honor schema-level `params.timestamps`.
type UserModel = InferModel<typeof schema.models.User>;

// Low-level input variant. Prefer InferInputFromSchema in user code.
type UserInputRaw = InferInput<typeof schema.models.User>;

// Key input type (only key template variables)
type UserKey = InferKeyInput<typeof schema.models.User>;
// { username: string }

// Extract item type from an array attribute
type StoryEntity = InferModelFromSchema<typeof schema, 'Story'>;
type StoryFrame = ArrayItem<StoryEntity['frames']>;
// → { url: string; duration?: number; mediaType?: string }

Builder Operations

All operations use the immutable builder pattern:

// GET - Retrieve item
const user = await table.entities.User.get({ username: 'alice' })
  .select(['name', 'email'])
  .consistentRead()
  .execute();

// PUT - Insert/replace item
await table.entities.User.put({
  username: 'alice',
  name: 'Alice',
  email: '[email protected]',
})
  .ifNotExists()
  .returning('ALL_OLD')
  .execute();

// UPDATE - Modify attributes
await table.entities.User.update({ username: 'alice' })
  .set('name', 'Alice Johnson')
  .setIfNotExists('createdAt', new Date().toISOString()) // write only on first insert
  .add('followerCount', 1)
  .remove('email')
  .returning('ALL_NEW')
  .where((attr, op) => op.gt(attr.followerCount, 0))
  .execute();

// Note: `.set()`, `.setIfNotExists()`, `.add()` and `.delete()` reject
// `undefined` values up front. DynamoDB can't encode `undefined` in
// `ExpressionAttributeValues`, so the builder throws with the offending
// keys instead of letting the request fail server-side. Use `.remove(attr)`
// to clear an attribute, or filter `undefined` out of your payload before
// calling `.set()`. `null` is allowed — it writes the DynamoDB `NULL` type.

// UPSERT via setDefined - sync from an external system where undefined
// means "this attribute no longer applies". Defined values go to SET,
// undefined keys go to REMOVE. Internally composes `.set()` + `.remove()`,
// so all guards (PK template, GSI templates, dedup, GSI key recomputation)
// apply identically.
await table.entities.User.update({ username: 'alice' })
  .setDefined({
    name: tamsRecord.name,           // SET
    email: tamsRecord.email,         // may be undefined → REMOVE
    lastSeenAt: new Date().toISOString(),
  })
  .setIfNotExists('createdAt', new Date().toISOString())
  .execute();

// DELETE - Remove item
await table.entities.User.delete({ username: 'alice' })
  .returning('ALL_OLD')
  .where((attr, op) => op.exists(attr.email))
  .execute();

// QUERY - Query with conditions
const photos = await table.entities.Photo.query()
  .where((attr, op) => op.and(op.eq(attr.username, 'alice'), op.gt(attr.likesCount, 10)))
  .limit(20)
  .scanIndexForward(false)
  .execute();

// SCAN - Full table scan with filter
const activeUsers = await table.entities.User.scan()
  .filter((attr, op) => op.gt(attr.followerCount, 1000))
  .limit(50)
  .execute();

// BATCH GET - Retrieve multiple items
const users = await table.entities.User.batchGet([
  { username: 'alice' },
  { username: 'bob' },
  { username: 'charlie' },
]).execute();

// BATCH WRITE - Write multiple items
//
// `batchWrite` accepts any number of items. Internally `execute()` chunks
// them into sub-requests of at most 25 items (DynamoDB's hard
// `BatchWriteItem` limit) and retries UnprocessedItems with exponential
// backoff (default 3 retries; configurable via `.maxRetries(n)` and
// `.retryBackoffMs(ms)`). If items remain unprocessed after the retry
// budget, `BatchUnprocessedError` is thrown.
await table.entities.User.batchWrite([
  { username: 'alice', name: 'Alice', email: '[email protected]' },
  { username: 'bob', name: 'Bob', email: '[email protected]' },
]).execute();

Transactions

Atomic operations across multiple items:

// TransactWrite - Atomic writes
await table
  .transactWrite()
  .addPut(
    table.entities.Like.put({
      photoId: 'photo1',
      likingUsername: 'alice',
    })
      .ifNotExists()
      .dbParams()
  )
  .addUpdate(table.entities.Photo.update({ photoId: 'photo1' }).add('likesCount', 1).dbParams())
  .execute();

// TransactGet - Atomic reads. execute() resolves to a flat array of
// items in the same order as the addGet() calls.
const [user, photo] = await table
  .transactGet()
  .addGet(table.entities.User.get({ username: 'alice' }).dbParams())
  .addGet(table.entities.Photo.get({ photoId: 'photo1' }).dbParams())
  .execute();

Available Operators

Build complex conditions with type-safe operators:

Comparison

  • eq(attr, value) - Equals
  • ne(attr, value) - Not equals
  • lt(attr, value) - Less than
  • lte(attr, value) - Less than or equal
  • gt(attr, value) - Greater than
  • gte(attr, value) - Greater than or equal
  • between(attr, low, high) - Between values

String

  • beginsWith(attr, prefix) - Begins with prefix
  • contains(attr, value) - Contains value (strings, sets, lists)

Existence

  • exists(attr) - Attribute exists
  • notExists(attr) - Attribute doesn't exist

Advanced

  • attributeType(attr, type) - Check attribute type ('S', 'N', 'M', 'L', etc.)
  • in(attr, values[]) - Value in array
  • size(attr) - Get size, returns object with .eq(), .gt(), etc.

Logical

  • and(...conditions) - Combine with AND
  • or(...conditions) - Combine with OR
  • not(condition) - Negate condition

Examples

// Exists check
await table.entities.User.update({ username: 'alice' })
  .set('email', '[email protected]')
  .where((attr, op) => op.notExists(attr.email))
  .execute();

// Contains
const users = await table.entities.User.query()
  .where((attr, op) => op.and(op.eq(attr.username, 'alice'), op.contains(attr.tags, 'premium')))
  .execute();

// IN operator
const activeUsers = await table.entities.User.scan()
  .filter((attr, op) => op.in(attr.status, ['active', 'pending']))
  .execute();

// Size function
const posts = await table.entities.Post.query()
  .where((attr, op) => op.and(op.eq(attr.userId, 'alice'), op.size(attr.tags).gte(3)))
  .execute();

// Complex nested conditions
await table.entities.Photo.query()
  .where((attr, op) =>
    op.and(
      op.eq(attr.username, 'alice'),
      op.or(
        op.gt(attr.likesCount, 100),
        op.and(op.gt(attr.commentCount, 50), op.exists(attr.featured))
      )
    )
  )
  .execute();

DynamoDB Logger

Debug your DynamoDB operations:

import { createDynamoDBLogger } from '@ftschopp/dynatable-core';

const logger = createDynamoDBLogger({
  enabled: true,
  logParams: true,
  logResponse: false,
});

const table = new Table({
  name: 'MyTable',
  client,
  schema,
  logger, // Attach logger
});

// All operations now logged to console

Pagination

Built-in pagination support:

// Execute with pagination
const page1 = await table.entities.Post.query()
  .where((attr, op) => op.eq(attr.userId, 'alice'))
  .limit(20)
  .executeWithPagination();

// Get next page
if (page1.lastEvaluatedKey) {
  const page2 = await table.entities.Post.query()
    .where((attr, op) => op.eq(attr.userId, 'alice'))
    .startFrom(page1.lastEvaluatedKey)
    .limit(20)
    .executeWithPagination();
}

API Reference

Core Exports

export {
  Table, // Main Table class
  type SchemaDefinition, // Schema type
  type ModelDefinition, // Model type
  type AttributeDefinition, // Union of all attribute types
  type ScalarAttributeDefinition, // String/Number/Boolean/Date attribute
  type ObjectAttributeDefinition, // Nested object attribute with schema
  type ArrayAttributeDefinition, // Typed array attribute with items
  type PrimaryKeyDefinition, // PK + SK key definition
  type KeyDefinition, // Single key definition
  type IndexDefinition, // Index (hash + optional sort)
  type IndexesDefinition, // All table indexes
  type SchemaParams, // Global schema params
  type InferModel, // Infer model type from a ModelDefinition (low-level)
  type InferInput, // Infer input type from a ModelDefinition (low-level)
  type InferKeyInput, // Infer key type
  type InferModelFromSchema, // Infer from full schema (preferred for user code)
  type InferInputFromSchema, // Infer input from full schema (preferred for user code)
  type TimestampFields, // createdAt/updatedAt fields type
  type ArrayItem, // Extract item type from array attribute
  createDynamoDBLogger, // Logger factory
  type DynamoDBLogger, // Logger type
  type DynamoDBLoggerConfig, // Logger config
};

Builder Types

Each builder type is exported for advanced use cases:

  • GetBuilder, PutBuilder, UpdateBuilder, DeleteBuilder
  • QueryBuilder, ScanBuilder
  • BatchGetBuilder, BatchWriteBuilder
  • TransactWriteBuilder, TransactGetBuilder
  • Condition, OpBuilder, AttrBuilder, SizeRef

Requirements

  • Node.js >= 22
  • TypeScript >= 5.0 (recommended)
  • AWS SDK v3 (@aws-sdk/client-dynamodb, @aws-sdk/lib-dynamodb)

Dependencies

  • @aws-sdk/client-dynamodb ^3.965.0
  • @aws-sdk/lib-dynamodb ^3.965.0
  • zod ^4.3.5 - Runtime validation
  • ulid ^3.0.2 - ULID generation
  • ramda ^0.32.0 - Functional utilities

Documentation

For complete documentation, examples, and guides, visit the main repository.

Testing

The library includes comprehensive test coverage with Jest:

npm test

License

MIT

Contributing

Contributions are welcome! Please see the main repository for contribution guidelines.

Related Packages