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

breachie.core.dynaorm

v0.2.1

Published

A type-safe DynamoDB ORM

Readme

DynaORM - A Type-Safe DynamoDB Client for TypeScript

This package provides a lightweight, type-safe client for Amazon DynamoDB, simplifying common operations with a schema-first approach. By leveraging Zod for schema validation, it ensures your data conforms to a defined structure before interacting with the database. The library abstracts away the complexities of the AWS SDK, such as marshaling and unmarshaling data, and provides a fluent API for building queries.

Key Features

  • Schema-first: Define your table structure and indexes using a simple defineSchema function.
  • Type Safety: All operations are fully typed, leveraging TypeScript's generics to provide autocompletion and prevent runtime errors.
  • Zod Validation: Automatically validates data on create and upsert operations, ensuring data integrity.
  • Fluent Query Builder: Construct complex queries with a chainable query() API for intuitive read operations.
  • Index Query Builder: Type-safe onIndex() API with intellisense for GSI/LSI partition and sort keys, including multi-key GSI support.
  • Bulk Operations: Includes methods for efficient batchWrite, batchGet, transactWrite, and transactGet operations.
  • Atomic Operations: Support for increment, append, and addSet operations on update.
  • Throttling: Built-in support for throttling requests to manage DynamoDB throughput.
  • CDK Utilities: Helper to convert dynaorm schemas into CDK TablePropsV2 with multi-key GSI support.

Installation

npm install dynaorm zod @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb p-throttle

How to Use DynaORM

1. Define your Schema

import { defineSchema } from "dynaorm";
import { z } from "zod";

const userSchema = z.object({
  userId: z.string().uuid(),
  email: z.string().email(),
  username: z.string().min(3),
  createdAt: z.string().datetime(),
  status: z.enum(["active", "inactive", "suspended"]),
});

const userTableSchema = defineSchema({
  tableName: "UsersTable",
  fields: userSchema,
  partitionKey: "userId",
  globalSecondaryIndexes: {
    byEmail: { partitionKey: "email", projection: { type: "ALL" } },
    byStatus: {
      partitionKey: "status",
      sortKey: "createdAt",
      projection: { type: "KEYS_ONLY" },
    },
  },
});

Multi-Key GSI

For composite partition keys (2-4 fields), use partitionKeys array:

const eventTableSchema = defineSchema({
  tableName: "EventsTable",
  fields: z.object({
    eventId: z.string(),
    timestamp: z.string(),
    tenantId: z.string(),
    category: z.string(),
    severity: z.number(),
  }),
  partitionKey: "eventId",
  sortKey: "timestamp",
  globalSecondaryIndexes: {
    byTenantCategory: {
      partitionKeys: ["tenantId", "category"] as const,
      sortKey: "severity",
    },
  },
});

2. Create the Client

import { DynamoDBClientConfig } from "@aws-sdk/client-dynamodb";
import { createClient } from "dynaorm";

const config: DynamoDBClientConfig = {
  region: "us-east-1",
};

const client = createClient(
  { users: userTableSchema, events: eventTableSchema },
  {
    config,
    modelOptions: {
      throttle: { limit: 100, interval: 1000 },
    },
  },
);

const userModel = client.users;

3. Perform Operations

Create and Upsert

create performs a DynamoDB PutItem with an attribute_not_exists condition on the partition key — it will throw a ConditionalCheckFailedException if an item with the same key already exists. The item is validated against the Zod schema before being written.

upsert also performs a PutItem but without the existence check — it will create the item if it doesn't exist, or fully replace it if it does. An optional condition expression can be passed to guard the write.

const newUser = {
  userId: "some-uuid-1",
  email: "[email protected]",
  username: "testuser",
  createdAt: new Date().toISOString(),
  status: "active",
};

await userModel.create(newUser);

const updatedUser = { ...newUser, status: "inactive" };
await userModel.upsert(updatedUser);

Read Operations

findOne performs a DynamoDB GetItem — it requires the full primary key (partition key + sort key if the table has one) and returns the item or null. Supports optional attribute projection (return only specified fields) and consistentRead for strongly consistent reads (default is eventually consistent).

findByIndex queries a Global or Local Secondary Index. It accepts the index's key values and optional operators, filter, limit, and attributes options. It auto-paginates internally to collect all matching results up to the limit.

const user = await userModel.findOne({ userId: "some-uuid-1" });

// With projection
const partial = await userModel.findOne(
  { userId: "some-uuid-1" },
  { attributes: ["email", "status"], consistentRead: true },
);

// Find by index
const activeUsers = await userModel.findByIndex(
  "byStatus",
  { status: "active" },
  { limit: 10 },
);

Update and Delete

update performs a DynamoDB UpdateItem — it modifies specific attributes on an existing item without replacing the entire item. The key (partition + sort key) must be provided. Each field in the updates object generates a SET expression. If no update attributes are provided, it throws. An optional condition expression guards the update (e.g., attribute_exists(userId) to ensure the item exists first).

delete performs a DynamoDB DeleteItem. It does not throw if the item doesn't exist (DynamoDB delete is idempotent). An optional condition expression can be used to make the delete conditional.

await userModel.update({ userId: "some-uuid-1" }, { status: "suspended" });

// With condition expression
await userModel.update(
  { userId: "some-uuid-1" },
  { status: "active" },
  { condition: 'attribute_exists(userId)' },
);

await userModel.delete({ userId: "some-uuid-1" });

Atomic Operations

Atomic operations are special update expressions that DynamoDB executes server-side without read-modify-write race conditions. They are passed as update values with an __operation field:

  • increment — Adds a numeric value to an existing number attribute (or starts from 0 if the attribute doesn't exist). Use negative values to decrement.
  • append — Appends elements to the end of a list attribute using list_append.
  • addSet — Adds elements to a Set attribute using the DynamoDB ADD expression. The attribute must be a Set type (not a List).

Atomic operations can be mixed with regular SET updates in the same update() call.

await userModel.update(
  { userId: "some-uuid-1" },
  {
    loginCount: { __operation: "increment", value: 1 },
    tags: { __operation: "append", value: ["new-tag"] },
    roles: { __operation: "addSet", value: new Set(["admin"]) },
  },
);

Query Builder (Table)

The QueryBuilder constructs a DynamoDB Query operation against the table's primary key. It uses KeyConditionExpression for partition/sort key conditions (server-side, efficient) and FilterExpression for post-read filtering (applied after items are read from the index, so filtered items still consume read capacity).

exec() returns one page of results (up to limit items) plus a lastKey for pagination. It auto-paginates internally when items span multiple DynamoDB pages until the limit is reached. paginate() is an async generator that yields successive pages. execAll() collects all pages into a single array.

The orderBy(boolean) method controls ScanIndexForwardtrue (default) returns items in ascending sort key order, false for descending.

// Query by partition key
const result = await userModel.query()
  .wherePK("some-uuid-1")
  .exec();

// With sort key condition
const result = await userModel.query()
  .wherePK("some-uuid-1")
  .whereSK("begins_with", "2024-")
  .exec();

// With filters, ordering, limit
const result = await userModel.query()
  .wherePK("some-uuid-1")
  .whereSK(">=", "2024-01-01")
  .filter("status", "=", "active")
  .filter("username", "contains", "test", "AND")
  .orderBy(false) // descending
  .limit(10)
  .project(["userId", "email", "status"])
  .consistentRead()
  .exec();

// Pagination
const { items, lastKey } = await userModel.query()
  .wherePK("some-uuid-1")
  .limit(25)
  .exec();

// Resume from lastKey
const nextPage = await userModel.query()
  .wherePK("some-uuid-1")
  .limit(25)
  .startKey(lastKey)
  .exec();

// Async generator pagination
for await (const page of userModel.query().wherePK("some-uuid-1").limit(25).paginate()) {
  console.log(page);
}

// Get all results
const allItems = await userModel.query()
  .wherePK("some-uuid-1")
  .execAll();

Index Query Builder (GSI/LSI)

Calling query().onIndex(name) returns a separate IndexQueryBuilder class that is typed to the specific index. Unlike the table QueryBuilder (which always uses the table's PK/SK), the IndexQueryBuilder knows the index's partition key(s) and sort key, providing correct intellisense and type checking.

For single-key GSIs, wherePK accepts either the raw value or an object. For multi-key GSIs (defined with partitionKeys array), wherePK accepts an object with all keys, or can be chained with key-value pairs. The whereSK method is typed to the index's sort key field.

GSI queries are always eventually consistent (DynamoDB limitation). LSI queries can use consistentRead() since they share the table's partition.

// Single-key GSI — single value
const result = await userModel.query()
  .onIndex("byEmail")
  .wherePK("[email protected]")
  .exec();

// Single-key GSI with sort key
const result = await userModel.query()
  .onIndex("byStatus")
  .wherePK("active")
  .whereSK("begins_with", "2024-")
  .exec();

// Single-key GSI — object form
const result = await userModel.query()
  .onIndex("byStatus")
  .wherePK({ status: "active" })
  .whereSK(">=", "2024-01-01")
  .exec();

// Multi-key GSI — object form
const result = await client.events.query()
  .onIndex("byTenantCategory")
  .wherePK({ tenantId: "t1", category: "alerts" })
  .whereSK(">=", 5)
  .exec();

// Multi-key GSI — chained key-value pairs
const result = await client.events.query()
  .onIndex("byTenantCategory")
  .wherePK("tenantId", "t1")
  .wherePK("category", "alerts")
  .whereSK(">=", 5)
  .exec();

// Index query with filter, limit, projection
const result = await userModel.query()
  .onIndex("byStatus")
  .wherePK("active")
  .filter("username", "begins_with", "test")
  .limit(10)
  .project(["userId", "email"])
  .execAll();

Filter Operators

Filter expressions are applied after DynamoDB reads items from the index — they do not reduce the amount of data read (and therefore read capacity consumed), but they do reduce the data returned to the client. Filters can be chained with AND or OR join operators (default is AND).

Available filter operators for filter():

| Operator | Description | |----------|-------------| | = | Equal | | <> | Not equal | | <, >, <=, >= | Comparison | | contains | String/set contains value | | begins_with | String starts with prefix | | attribute_exists | Attribute exists (no value arg) | | attribute_not_exists | Attribute does not exist (no value arg) | | IN | Value in array |

// attribute_exists / attribute_not_exists (no value parameter)
.filter("email", "attribute_exists")
.filter("deletedAt", "attribute_not_exists")

// IN operator (array of values)
.filter("status", "IN", ["active", "inactive"])

// Multiple filters with join
.filter("status", "=", "active")
.filter("age", ">=", 18, "AND")
.filter("role", "=", "admin", "OR")

Bulk Operations

batchWrite sends multiple put and/or delete operations in a single request. DynamoDB limits each request to 25 items — DynaORM automatically chunks larger arrays into multiple requests. If any items fail (returned as UnprocessedItems), they are retried with exponential backoff. Note: batch writes are NOT atomic — some items may succeed while others fail across chunks.

batchGet retrieves multiple items by their primary keys in a single request (up to 100 keys per request, auto-chunked). Unprocessed keys are retried automatically.

upsertMany is a convenience wrapper that calls batchWrite with all items as put operations.

await userModel.batchWrite([
  { type: "put", item: newUser },
  { type: "delete", item: { userId: "uuid-3" } },
]);

const items = await userModel.batchGet([
  { userId: "uuid-1" },
  { userId: "uuid-2" },
]);

await userModel.upsertMany([user1, user2, user3]);

Transactional Operations

Transactions provide all-or-nothing atomicity — if any operation in the batch fails (e.g., a condition expression is not met), the entire transaction is rolled back and no items are modified. DynamoDB supports up to 100 items per transaction. DynaORM automatically chunks larger sets into separate 100-item transactions (note: atomicity only applies within each chunk, not across chunks).

transactWrite supports mixed put, update, and delete operations with optional condition expressions per item. transactGet performs a strongly consistent read of multiple items in a single request.

updateMany is a convenience wrapper around transactWrite that issues update operations for each item (chunked to 25 per transaction). deleteMany queries by partition key (and optional sort key condition), then deletes all matching items — atomically via transactWrite for ≤100 items, falling back to batchWrite (non-atomic) for larger sets.

await userModel.transactWrite([
  { type: "put", item: newUser },
  { type: "update", key: { userId: "uuid-2" }, updates: { status: "active" } },
  { type: "delete", key: { userId: "uuid-3" } },
  { type: "put", item: anotherUser, condition: "attribute_not_exists(userId)" },
]);

const results = await userModel.transactGet([
  { userId: "uuid-1" },
  { userId: "uuid-2" },
]);

// Update many (uses transactWrite internally)
await userModel.updateMany([
  { key: { userId: "uuid-1" }, updates: { status: "active" } },
  { key: { userId: "uuid-2" }, updates: { status: "inactive" } },
]);

// Delete many by partition key
await userModel.deleteMany("partition-value");
await userModel.deleteMany("partition-value", { operator: ">=", value: "sort-key-value" });

Scan Operations

scanAll reads every item in the table (a full table scan). This is expensive and slow for large tables — prefer queries when possible. It supports optional filter expressions, limit, attribute projection, and startKey for resuming a previous scan.

For large tables, use parallelism to split the scan into multiple concurrent segments (DynamoDB parallel scan). Each segment scans a portion of the table independently. The optional onSegmentData callback is invoked with each batch of items as they arrive, useful for streaming processing without holding all results in memory.

getItemCount performs a scan with Select: "COUNT" — it counts items matching an optional filter without returning the actual data.

const { items, lastKey } = await userModel.scanAll({
  filter: { status: "active" },
  limit: 100,
  attributes: ["userId", "email"],
});

// Parallel scan
const { items } = await userModel.scanAll({ parallelism: 4 });

// With callback per segment
await userModel.scanAll({
  parallelism: 4,
  onSegmentData: async (items) => {
    console.log(`Got ${items.length} items`);
  },
});

// Count items
const count = await userModel.getItemCount({
  filter: { status: "active" },
});

Index Operations

These convenience methods combine a findByIndex query with a bulk mutation. They first query the index to find all matching items, then perform the update or delete using the items' primary keys.

updateByIndex finds all items matching the index query and applies the same updates to each (supports atomic operations). deleteByIndex finds all matching items and deletes them — atomically via transactWrite for ≤100 items, falling back to batchWrite for larger result sets.

// Find, update, and delete by index
const items = await userModel.findByIndex("byStatus", { status: "active" });

await userModel.updateByIndex("byStatus", { status: "inactive" }, { status: "archived" });

await userModel.deleteByIndex("byStatus", { status: "archived" });

CDK Utilities

buildDynamoDbTableFromZodSchema converts a dynaorm schema into AWS CDK TablePropsV2 — including partition key, sort key, GSIs, LSIs, TTL detection (looks for an expiresOn field), and attribute type inference from Zod (string → S, number → N).

For multi-key GSIs (partitionKeys arrays), CDK's L2 construct only supports one HASH + one RANGE key per index. The utility works around this by creating the GSI with the first key as PK and second as SK at the CDK level, then returning compositeKeyOverrides that patch the generated CloudFormation via cfnTable.addPropertyOverride() to include the full multi-key KeySchema.

import { buildDynamoDbTableFromZodSchema } from "dynaorm/cdk/utils";
import { TableV2, CfnTable, Billing } from "aws-cdk-lib/aws-dynamodb";

const { tableProps, compositeKeyOverrides } = buildDynamoDbTableFromZodSchema(schema);

const table = new TableV2(stack, "MyTable", {
  ...tableProps,
  billing: Billing.onDemand(),
});

// Apply multi-key GSI overrides
const cfnTable = table.node.defaultChild as CfnTable;
for (const override of compositeKeyOverrides) {
  cfnTable.addPropertyOverride(
    `GlobalSecondaryIndexes.${override.gsiIndex}.KeySchema`,
    override.keySchema.map((k) => ({ AttributeName: k.attributeName, KeyType: k.keyType })),
  );
}

API Reference

createClient(schemas, options)

Initializes the DynamoDB client and attaches models for each schema.

  • schemas: A record mapping model names to schemas.
  • options: Contains config (DynamoDBClientConfig), optional modelOptions, and optional perModelOptions.

defineSchema(schema)

Defines a new DynamoDB table schema with strong typing.

  • tableName: DynamoDB table name.
  • fields: Zod object for item structure.
  • partitionKey: Partition key field name.
  • sortKey?: Optional sort key.
  • globalSecondaryIndexes?: Optional GSIs. Each can use partitionKey (single) or partitionKeys (array of 2-4 for multi-key).
  • localSecondaryIndexes?: Optional LSIs.

Model<S>

Provides methods for interacting with a DynamoDB table.

CRUD Operations

  • create(item) — PutItem with attribute_not_exists condition (prevents overwrite)
  • upsert(item, options?) — PutItem (creates or overwrites), optional condition
  • update(key, updates, options?) — UpdateItem, supports atomic ops, optional condition
  • delete(key, options?) — DeleteItem, optional condition
  • findOne(key, options?) — GetItem, optional attributes projection and consistentRead

Query and Scan

  • query() — Returns QueryBuilder for table queries
  • findMany(partitionKeyValue, sortKeyCondition?, options?) — Query with auto-pagination
  • findByIndex(indexName, keyValues, options?) — Query a GSI/LSI
  • scanAll(options?) — Full table scan with filter, limit, parallelism
  • getItemCount(options?) — COUNT scan

Bulk & Transactional Operations

  • batchWrite(items) — Up to 25 per request, auto-chunks, retries unprocessed
  • batchGet(keys) — Up to 100 per request, auto-chunks, retries unprocessed
  • transactWrite(items) — Atomic multi-item write (put/update/delete), max 100
  • transactGet(keys) — Consistent multi-item read, max 100
  • upsertMany(items) — Batch put via batchWrite
  • updateMany(items) — Multi-item update via transactWrite
  • deleteMany(partitionKeyValue, sortKeyCondition?) — Query and delete matching items

Index Operations

  • deleteByIndex(indexName, keyValues) — Find by index and delete all matches
  • updateByIndex(indexName, keyValues, updates) — Find by index and update all matches

QueryBuilder<S>

Fluent API for building DynamoDB Query operations on the table's primary key.

  • wherePK(value) — Set partition key value (typed to schema's PK field)
  • whereSK(operator, value) — Set sort key condition
  • filter(key, operator, value, join?) — Add filter expression
  • limit(count) — Limit results per page
  • orderBy(asc)true for ascending, false for descending
  • startKey(key) — Resume from pagination key
  • project(attrs) — Select specific attributes
  • onIndex(index) — Switch to IndexQueryBuilder for the named GSI/LSI
  • consistentRead() — Use strongly consistent reads

Execution Methods

  • exec() — Returns { items, lastKey? }
  • paginate() — Async generator yielding pages
  • execAll() — Collects all pages into a single array

IndexQueryBuilder<S, I>

Type-safe fluent API for querying GSIs and LSIs. Returned by query().onIndex(name).

wherePK (three calling styles)

// Single value (single-key GSIs only)
.wherePK("value")

// Key-value pair (chainable for multi-key GSIs)
.wherePK("fieldName", "value")

// Object (all partition keys at once)
.wherePK({ field1: "value1", field2: "value2" })
  • whereSK(operator, value) — Sort key condition (typed to the index's SK)
  • filter(key, operator, value, join?) — Add filter expression
  • limit(count) — Limit results
  • orderBy(asc) — Sort order
  • startKey(key) — Pagination resume
  • project(attrs) — Attribute projection
  • consistentRead() — Consistent reads

Execution Methods

  • exec() — Returns { items, lastKey? }
  • paginate() — Async generator
  • execAll() — All pages as array