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.12.0

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, {
  defaultPutData: () => ({ 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, {
  defaultPutData: () => ({ 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
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, {
  defaultPutData: () => ({ createdAt: Date.now() }),
  defaultUpdateData: () => ({ updatedAt: Date.now() }),
}) {}

defaultPutData is merged under every put / create / batchWrite put / trxPut / trxCreate. defaultUpdateData is merged under every update / batchUpdate / trxUpdate. Caller-provided values always win.

transformInput / transformOutput

transformInput runs on every write (after defaults are merged) and receives a partial of the schema. transformOutput runs on every read and maps the stored shape to your desired return type:

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

Transforms are skipped when a projection option is provided, since only a subset of fields is available.

derivedAttributes / immutableAttributes

derivedAttributes lists fields that are computed by transformInput and should never be written directly by the caller. They are stripped from put and update inputs:

class UserRepo extends makeRepo(UserTable, {
  transformInput: (item) => ({
    ...item,
    emailDomain: item.email ? item.email.split("@")[1] : undefined,
  }),
  derivedAttributes: ["emailDomain"],
}) {}

immutableAttributes lists fields that may be set on create but must not be changed by updates:

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

Both arrays are inferred as literal types — no as const needed.

License

MIT