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

dinah

v0.16.1

Published

A dynamodb client designed for typescript

Readme

In my world, working with DynamoDb would be painless, safe, and fun. Dinah provides a type-safe, expressive API for interacting with DynamoDB, featuring schema-driven table definitions, a repository pattern with full type inference, MongoDB-like query syntax, and first-class support for batch operations, transactions, pagination, and GSIs. It is closer to a query builder than an ORM, and doesn't encourage single-table design. Nonsense? That's for you to decide.

Installation

npm install dinah @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

Quick Start

Create a Db Instance

Db wraps the AWS SDK v3 DynamoDB client. Pass it a DynamoDBClient, a DynamoDBClientConfig, or an existing client instance:

import { Db } from "dinah";

const db = new Db({ region: "us-east-1" });

Using the Db Class Directly

The Db class exposes low-level operations that work with any table by name:

// Put an item
await db.put({
  table: "users",
  item: {
    userId: "u1",
    email: "[email protected]",
    name: "Alice",
    role: "admin",
    createdAt: Date.now(),
  },
});

// Get an item
const user = await db.get<{ userId: string; name: string }>({
  table: "users",
  key: { userId: "u1" },
});

// Query
const users = await db.query<{ userId: string; name: string }>({
  table: "users",
  query: { role: "admin" },
  index: "byRole",
});

// Update
await db.update({
  table: "users",
  key: { userId: "u1" },
  update: { name: "Alice Smith", updatedAt: Date.now() },
});

// Delete
await db.delete({ table: "users", key: { userId: "u1" } });

Define a Table

Use TypeBox schemas to define your table's shape, then pass it to Table along with key configuration:

import { Type } from "typebox";
import { Table } from "dinah";

const UserTable = new Table(
  Type.Object({
    userId: Type.String(),
    email: Type.String(),
    name: Type.String(),
    role: Type.String(),
    createdAt: Type.Number(),
    updatedAt: Type.Optional(Type.Number()),
  }),
  {
    name: "users",
    partitionKey: "userId",
    billingMode: "PAY_PER_REQUEST",
    gsis: {
      byEmail: { partitionKey: "email" },
      byRole: { partitionKey: "role", sortKey: "createdAt" },
    },
  },
);

Using the Repository Class

Repo is the recommended way to interact with DynamoDB. Use db.makeRepo to create a repo inline, with or without configuration:

const userRepo = db.makeRepo(UserTable);

const userRepo = db.makeRepo(UserTable, {
  defaultCreateData: () => ({ createdAt: Date.now() }),
  defaultUpdateData: () => ({ updatedAt: Date.now() }),
});

To create a named, reusable repo class, use the standalone makeRepo (see Repository Configuration):

import { makeRepo } from "dinah";

class UserRepo extends makeRepo(UserTable, {
  defaultCreateData: () => ({ createdAt: Date.now() }),
  defaultUpdateData: () => ({ updatedAt: Date.now() }),
}) {}

const userRepo = new UserRepo(db);

CRUD

// Create (conditional put — fails if item already exists)
const user = await userRepo.create({
  userId: "u1",
  email: "[email protected]",
  name: "Alice",
  role: "admin",
  createdAt: Date.now(),
});

// Get
const alice = await userRepo.get({ userId: "u1" });

// Get with projection (return type narrows to projected fields)
const partial = await userRepo.get({ userId: "u1" }, { projection: ["name", "email"] });

// getOrThrow (throws if item not found)
const aliceOrThrow = await userRepo.getOrThrow({ userId: "u1" });

// Put (upsert)
await userRepo.put({
  userId: "u1",
  email: "[email protected]",
  name: "Alice",
  role: "admin",
  createdAt: Date.now(),
});

// Update (throws if item does not exist)
const updated = await userRepo.update(
  { userId: "u1" },
  { name: "Alice Smith", updatedAt: Date.now() },
);

// Delete (returns old item or undefined)
const deleted = await userRepo.delete({ userId: "u1" });

// deleteOrThrow (throws if item not found)
const item = await userRepo.deleteOrThrow({ userId: "u1" });

Update Expressions

The update argument supports MongoDB-style operators:

await userRepo.update(
  { userId: "u1" },
  {
    name: "Alice", // set
    age: undefined, // remove
    score: { $plus: 10 }, // increment
    score: { $minus: 5 }, // decrement
    score: { $ifNotExists: 0 }, // set only if missing
    tags: { $append: "vip" }, // list_append to end
    tags: { $prepend: "featured" }, // list_append to front
    followers: { $setAdd: "user-99" }, // ADD to DynamoDB set
    followers: { $setDel: "user-99" }, // DELETE from DynamoDB set
  },
);

Querying

Queries use a MongoDB-like syntax with operators like $gt, $between, $prefix, and more:

// Query by partition key
const posts = await postRepo.query({ authorId: "u1" });

// Query with sort key condition (sort-key operators go in the query object)
const recent = await postRepo.query({ authorId: "u1", postId: { $gte: "2024-" } });

// Query a GSI
const adminsByDate = await userRepo.queryGsi("byRole", {
  role: "admin",
  createdAt: { $gt: 1700000000000 },
});

// Paginated query (async generator, one page at a time)
for await (const page of postRepo.queryPaged({ authorId: "u1" }, { limit: 20 })) {
  console.log(page);
}

// Paginated GSI query
for await (const page of userRepo.queryGsiPaged("byRole", { role: "admin" })) {
  console.log(page);
}

// Scan with filters
const recentUsers = await userRepo.scan({
  filter: { createdAt: { $gte: 1700000000000 } },
});

// Scan a GSI
const allByStatus = await postRepo.scanGsi("byStatus");

// Check existence (uses query or scan, no data returned)
const hasAdmins = await userRepo.existsGsi("byRole", { query: { role: "admin" } });
const exists = await postRepo.exists({ query: { authorId: "u1" } });

Filter operators: $eq, $ne, $gt, $gte, $lt, $lte, $between, $in, $nin, $prefix, $includes, $exists, $size, $type.

Batch Operations

// Batch get
const { items, unprocessed } = await userRepo.batchGet([{ userId: "u1" }, { userId: "u2" }]);

// Batch get (throws on missing or unprocessed)
const items = await userRepo.batchGetOrThrow([{ userId: "u1" }, { userId: "u2" }]);

// Batch write (puts and deletes mixed)
await userRepo.batchWrite([
  {
    type: "PUT",
    item: { userId: "u3", email: "[email protected]", name: "Carol", role: "user", createdAt: Date.now() },
  },
  { type: "DELETE", key: { userId: "u2" } },
]);

// Batch update (same update applied to multiple keys via PartiQL)
await userRepo.batchUpdate([{ userId: "u1" }, { userId: "u3" }], { role: "admin" });

Transactions

trxWrite accepts plain request objects and executes them as a single DynamoDB transaction:

await userRepo.trxWrite(
  {
    type: "PUT",
    item: {
      userId: "u5",
      email: "[email protected]",
      name: "Eve",
      role: "user",
      createdAt: Date.now(),
    },
  },
  { type: "UPDATE", key: { userId: "u1" }, update: { role: "superadmin" } },
  { type: "DELETE", key: { userId: "u2" } },
  { type: "CONDITION", key: { userId: "u3" }, condition: { role: "admin" } },
);

Convenience methods operate on multiple keys/items atomically:

// Transactional get
const [u1, u2] = await userRepo.trxGet([{ userId: "u1" }, { userId: "u2" }]);
const items = await userRepo.trxGetOrThrow([{ userId: "u1" }, { userId: "u2" }]);

// Transactional writes
await userRepo.trxPut([item1, item2]);
await userRepo.trxUpdate([{ userId: "u1" }, { userId: "u2" }], { role: "admin" });
await userRepo.trxDelete([{ userId: "u1" }, { userId: "u2" }]);
await userRepo.trxCreate([item1, item2]); // fails if any item already exists

To build cross-repo transactions, use the *Request methods to produce request objects and pass them to db.trxWrite:

await db.trxWrite(
  userRepo.trxPutRequest(userItem),
  postRepo.trxDeleteRequest({ authorId: "u1", postId: "p1" }),
);

Repository Configuration

makeRepo accepts a config object that controls defaults, transforms, and attribute rules. Extend the result to create a named repo class:

class UserRepo extends makeRepo(UserTable, {
  defaultCreateData: () => ({ createdAt: Date.now() }),
  defaultUpdateData: () => ({ updatedAt: Date.now() }),
}) {}

defaultCreateData is merged under every create / trxCreate. defaultUpdateData is merged under every update / batchUpdate / trxUpdate. Caller-provided values always win, and any key covered by a default becomes optional at the call site. (put / batchWrite puts / trxPut are raw upserts and do not apply these.)

transformAttributes / transformOutput

transformAttributes is a per-field map of normalizers applied to write inputs (on create and update). transformOutput runs on every non-projected read and maps the stored shape to your desired return type:

class UserRepo extends makeRepo(UserTable, {
  transformAttributes: {
    email: (email) => email.toLowerCase(),
  },
  transformOutput: (item): UserWithDisplayName => ({
    ...item,
    displayName: `${item.name} <${item.email}>`,
  }),
}) {}

transformOutput is skipped when a projection option is provided, or when reading a GSI with a KEYS_ONLY / INCLUDE projection, since only a subset of fields is available.

computedAttributes

computedAttributes declaratively derives a field from one or more other fields. Computed fields are recomputed on create / update and cannot be set directly by the caller (attempting to do so throws):

class UserRepo extends makeRepo(UserTable, {
  computedAttributes: {
    // single source field
    emailDomain: { from: "email", compute: (email) => email?.split("@")[1] },
    // multiple source fields
    displayName: { from: ["name", "email"], compute: (name, email) => name || email },
  },
}) {}

immutableAttributes

immutableAttributes lists fields that may be set on create but must not be changed by updates (attempting to update one throws):

class UserRepo extends makeRepo(UserTable, {
  immutableAttributes: ["createdAt"],
}) {}

Both computedAttributes keys and immutableAttributes are inferred as literal types — no as const needed.

discriminator / resourceName

discriminator names a schema field that tags a discriminated-union item type; Dinah uses it to narrow update inputs and results to the matching variant and to guard writes. resourceName sets the label used in thrown DinahErrors (defaults to the class name without the Repo suffix, else the table name).

class EventRepo extends makeRepo(EventTable, {
  discriminator: "kind",
  resourceName: "Event",
}) {}

Seeding

The optional dinah/seed subpath (zero runtime dependencies) is a minimal, deterministic way to define fixture data for a table and write it in a batched PUT. Items are typed against the table's schema.

import { createSeed } from "dinah/seed";

const users = createSeed(UserTable, [
  { userId: "u1", email: "[email protected]", name: "Alice", role: "admin", createdAt: 0 },
  { userId: "u2", email: "[email protected]", name: "Bob", role: "user", createdAt: 0 },
]);

await users.put(db); // batched PUT — overwrites by key

Reference another seed's .items directly — an already-defined seed resolves eagerly, so a plain array is enough:

const posts = createSeed(PostTable, [
  { postId: "p1", authorId: users.items[0].userId, title: "Hello" },
]);

await users.put(db);
await posts.put(db);

Use a thunk (() => Item[]) only when a reference can't be resolved at definition time — forward or mutual references, where the other seed is declared later — so it's evaluated lazily. A reference cycle between seeds then throws a clear error rather than looping.

License

MIT