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

dynamodb-zod

v0.1.8

Published

Entity mapping and query library for DynamoDB

Readme

dynamodb-zod

This is a fork of https://github.com/tschoffelen/turbine and changes will likely be upstreamed.

NPM License

Entity mapping and query helpers for DynamoDB using Zod schemas and the AWS SDK v3. Define your table and entities once, then put, get, update, and query with type-safe objects.

  • Small, direct, and type-friendly
  • Derive composite keys, defaults, and computed fields from your data
  • Type-safe expression objects for filters, conditions, and updates
  • Works with your existing DynamoDB DocumentClient

Getting started

npm install dynamodb-turbine zod
import z from "zod";
import { defineTable, defineEntity } from "dynamodb-turbine";

// 1) Define your table
const table = defineTable({
  name: "my-dynamodb-table",
  indexes: {
    table: { hashKey: "pk", rangeKey: "sk" },
    type_sk: { hashKey: "type", rangeKey: "sk" },
  },
});

// 2) Define an entity
const users = defineEntity({
  table,
  schema: z.object({
    id: z.string(),
    email: z.string().email(),
    name: z.string().optional(),
    pk: z.string(),
    sk: z.string(),
    type: z.string(),
    createdAt: z.string(),
    updatedAt: z.string(),
  }),
  computed: {
    pk: (u) => ["user", u.id],
    sk: (u) => u.email,
    type: () => "user",
    createdAt: (u) => u.createdAt || new Date().toISOString(),
    updatedAt: () => new Date().toISOString(),
  },
  immutable: ["createdAt"],
});

// 3) Use it
const user = await users.put({
  id: "user-001",
  email: "[email protected]",
});

await user.update({ name: "Alice" });

const found = await users.queryOne({
  pk: ["user", user.id],
  sk: { $beginsWith: "user@" },
});

Defining tables

const table = defineTable({
  name: "my-table",
  indexes: {
    table: { hashKey: "pk", rangeKey: "sk" },
    gsi1: { hashKey: "type", rangeKey: "createdAt" },
  },
  // Optional: set defaults applied to all operations
  options: {
    dynamodb: {
      consistentRead: true,
      returnConsumedCapacity: "TOTAL",
      returnItemCollectionMetrics: "SIZE",
    },
  },
  // Optional: pass your own client
  documentClient: myClient,
  // Optional: log all DynamoDB calls
  debug: true,
});

| Option | Description | | ---------------------------------------------- | ------------------------------------------------------------ | | name | DynamoDB table name | | indexes | Must include table (primary). Additional entries are GSIs. | | options.dynamodb.consistentRead | Applied to get and query | | options.dynamodb.returnConsumedCapacity | Applied to all operations | | options.dynamodb.returnItemCollectionMetrics | Applied to put, update, delete | | documentClient | Custom DynamoDBDocumentClient instance | | debug | Log all operations to console |

Defining entities

The schema defines the full stored item shape. Fields listed in computed are automatically excluded from put input requirements.

const posts = defineEntity({
  table,
  schema: z.object({
    id: z.string(),
    title: z.string(),
    content: z.string(),
    pk: z.string(),
    sk: z.string(),
    type: z.string(),
    createdAt: z.string(),
  }),
  computed: {
    pk: (p) => ["post", p.id],
    sk: (p) => ["post", p.createdAt, p.id],
    type: () => "post",
    createdAt: (p) => p.createdAt || new Date().toISOString(),
  },
  // Fields that use if_not_exists on update (never overwritten)
  immutable: ["createdAt"],
});

| Option | Description | | ----------- | ------------------------------------------------------------------------- | | schema | Zod object defining the full stored DynamoDB item shape | | computed | Functions that derive field values; these fields become optional in put | | immutable | Fields that are set on first write but never overwritten on update |

Operations

put

const post = await posts.put({ id: "123", title: "Hello", content: "World" });

get

const post = await posts.get({
  pk: ["post", "123"],
  sk: ["post", "2024-01-01", "123"],
});

update

await posts.update(
  { pk: ["post", "123"], sk: ["post", "2024-01-01", "123"] },
  { title: "Updated" },
);

query / queryOne / queryAll

const page = await posts.query({ pk: ["post", "123"] }, { Limit: 10 });
const one = await posts.queryOne({
  pk: ["post", "123"],
  sk: { $beginsWith: "post#2024" },
});
const all = await posts.queryAll({ pk: ["post", "123"] });

Query a GSI by specifying the index name — TypeScript constrains the key shape to match that index:

// Query by type (GSI with hashKey: "type", rangeKey: "sk")
const allUsers = await users.queryAll({
  index: "type_sk",
  type: "user",
});

// Query by type with range condition
const recentPosts = await posts.query(
  { index: "type_sk", type: "post", sk: { $beginsWith: "post#2024" } },
  { Limit: 20, ScanIndexForward: false },
);

delete

await posts.delete({ pk: ["post", "123"], sk: ["post", "2024-01-01", "123"] });

Expression objects

All expressions are plain $-prefixed objects — no function imports needed. They are fully type-safe.

Filter & condition expressions

Used in query filters and put/update/delete conditions.

// Filters on query
const results = await posts.queryAll(
  { pk: ["user", userId] },
  { filters: { title: { $beginsWith: "Hello" }, type: { $equals: "post" } } },
);

// Logical groups
const results = await posts.queryAll(
  { pk: ["user", userId] },
  {
    filters: {
      $or: [
        { title: { $beginsWith: "Hello" } },
        { title: { $beginsWith: "World" } },
      ],
    },
  },
);

// Conditions on writes
await posts.put(data, { conditions: { pk: { $notExists: true } } });
await posts.delete(key, {
  conditions: {
    $and: [{ type: { $equals: "post" } }, { title: { $exists: true } }],
  },
});

| Expression | DynamoDB expression | Value type | | ------------------------------- | ---------------------------- | -------------- | | { $equals: val } | = :val | any | | { $notEquals: val } | <> :val | any | | { $greaterThan: val } | > :val | string, number | | { $lessThan: val } | < :val | string, number | | { $greaterThanOrEquals: val } | >= :val | string, number | | { $lessThanOrEquals: val } | <= :val | string, number | | { $between: [lo, hi] } | BETWEEN :lo AND :hi | string, number | | { $beginsWith: prefix } | begins_with(path, :val) | string | | { $contains: val } | contains(path, :val) | string, number | | { $notContains: val } | NOT contains(path, :val) | string, number | | { $exists: true } | attribute_exists(path) | — | | { $notExists: true } | attribute_not_exists(path) | — | | { $and: [...conditions] } | cond AND cond | filter groups | | { $or: [...conditions] } | cond OR cond | filter groups |

Update expressions

Used in update patches alongside plain values.

await users.update(
  { pk: ["user", id], sk: email },
  {
    name: "Alice", // plain SET
    loginCount: { $increment: 1 }, // SET path = path + :val
    credits: { $decrement: 5 }, // SET path = path - :val
    tags: { $append: ["new-tag"] }, // SET path = list_append(path, :val)
    history: { $prepend: ["latest"] }, // SET path = list_append(:val, path)
    bio: { $ifNotExists: "No bio yet" }, // SET path = if_not_exists(path, :val)
    roles: { $addToSet: new Set(["admin"]) }, // ADD path :val
    oldRoles: { $deleteFromSet: new Set(["guest"]) }, // DELETE path :val
    temp: { $remove: true }, // REMOVE path
  },
);

| Expression | DynamoDB action | Value type | | ------------------------- | -------------------------------------- | ---------- | | { $increment: n } | SET path = path + :val | number | | { $decrement: n } | SET path = path - :val | number | | { $append: list } | SET path = list_append(path, :val) | array | | { $prepend: list } | SET path = list_append(:val, path) | array | | { $ifNotExists: val } | SET path = if_not_exists(path, :val) | any | | { $addToSet: set } | ADD path :val | Set | | { $deleteFromSet: set } | DELETE path :val | Set | | { $remove: true } | REMOVE path | — | | null | REMOVE path | — |

Dot-notation is supported for nested fields: { "stats.views": { $increment: 1 } }.

Types and validation

  • Inputs are validated by your Zod schema.
  • Returned instances are typed as the full schema and include an update(patch) helper.
  • Expression objects are type-restricted to their valid DynamoDB operand types.

Error handling

Invalid input or unresolved computed fields throw an error. DynamoDB errors (e.g. ConditionalCheckFailedException) are passed through.