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

v1.2.0

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';
import { DynamoDBDocumentClient } from '@aws-sdk/lib-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,
    isoDates: true,
  },
} as const;

// Create DynamoDB client
const ddbClient = new DynamoDBClient({ region: 'us-east-1' });
const client = DynamoDBDocumentClient.from(ddbClient);

// Create table instance
const table = new Table({
  name: 'MyTable',
  client,
  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
    isoDates: true,        // Use ISO 8601 dates
    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 }

// Full model type (deprecated — use InferModelFromSchema)
type UserLegacy = InferModel<typeof schema.models.User>;

// Input type (deprecated — use InferInputFromSchema)
type UserInputLegacy = 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')
  .add('followerCount', 1)
  .remove('email')
  .returning('ALL_NEW')
  .where((attr, op) => op.gt(attr.followerCount, 0))
  .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()
  .where((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
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
const result = await table.transactGet()
  .addGet(table.entities.User.get({ username: 'alice' }).dbParams())
  .addGet(table.entities.Photo.get({ photoId: 'photo1' }).dbParams())
  .execute();

const [user, photo] = result.items;

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()
  .where((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 (deprecated)
  type InferInput,                 // Infer input type (deprecated)
  type InferKeyInput,              // Infer key type
  type InferModelFromSchema,       // Infer from full schema (preferred)
  type InferInputFromSchema,       // Infer input from full schema (preferred)
  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 >= 18
  • 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