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

@joyful-tools/drizzle

v0.0.3

Published

A lean Result-flavored wrapper around a [drizzle-orm](https://orm.drizzle.team) database instance.

Readme

@joyful/drizzle

A lean Result-flavored wrapper around a drizzle-orm database instance.

Wrap the db once, lift queries into AsyncResult, and roll back transactions when a callback returns Err. No Proxy, no AsyncLocalStorage, no custom interpreter — a plain class with bound run / tx.

Installation

drizzle-orm is required as a peer.

# npm
npm install @joyful-tools/drizzle drizzle-orm

# pnpm
pnpm add @joyful-tools/drizzle drizzle-orm

# bun
bun add @joyful-tools/drizzle drizzle-orm

# npm (from JSR)
npx jsr add @joyful/drizzle

# pnpm 10.9+ (from JSR)
pnpm add jsr:@joyful/drizzle

# yarn 4.9+ (from JSR)
yarn add jsr:@joyful/drizzle

# deno
deno add jsr:@joyful/drizzle

The examples below use @joyful/drizzle and @joyful/result (the JSR names) in imports. If you installed from npm, use @joyful-tools/drizzle and @joyful-tools/result instead.

Why

  • Queries are data. run turns drizzle thenables into AsyncResult<T, DatabaseError> so failures compose with the rest of @joyful/result instead of throwing.
  • Err rolls back. tx commits on Ok and rolls back when the callback returns Err — including nested savepoints.
  • Session-first composition. Helpers take s: Session first, so the same function runs as myQuery(db, ...) or db.tx(myQuery, ...).
  • Tiny surface. jdb, run, tx, matches. Dialect-agnostic: the package imports no concrete drivers.

Usage

Setup

import { drizzle } from "drizzle-orm/libsql";
import { jdb, type SessionOf } from "@joyful/drizzle";
import * as schema from "./schema.ts";

export const db = jdb(drizzle(client, { schema }));
export type Session = SessionOf<typeof db>;

db and the s received in a tx callback are both assignable to Session.

If you prefer drizzle's table.$inferSelect style, typeof db.$inferSession is the same type (type-only — undefined at runtime):

export type Session = typeof db.$inferSession;

Lazy initialization

jdb also accepts a zero-arg initializer. It runs on first use (first query, tx, or $client / $driver access) and the result is memoized — handy when building the client needs config that isn't available at import time:

export const db = jdb(() => {
  const { databaseUrl, databaseAuthToken } = getCoreConfig();
  const client = createClient({
    url: databaseUrl,
    authToken: databaseAuthToken,
  });
  return drizzle(client, { schema });
});
export type Session = typeof db.$inferSession;

The inferred type is the same JoyDb as the eager form. A throw inside the initializer surfaces as DatabaseError from whichever run / tx touched the db first.

run — lift a query

The callback form is the one to use day-to-day. The raw drizzle instance is scoped to the call, and sync throws during builder construction become DatabaseError too:

const rows = await db.run((d) =>
  d.select().from(boards).where(eq(boards.workspaceId, wsId))
);
// AsyncResult<Board[], DatabaseError>

You can also pass a thenable directly (db.run(d.select().from(boards))), but construction then happens outside the try boundary.

Variadic refiners

When extra arguments are present, the base builder is switched to .$dynamic() and each (qb) => qb is applied left-to-right. The result type comes from the last refiner:

const page = await db.run(
  (d) => d.select().from(boards),
  (qb) => qb.where(eq(boards.workspaceId, wsId)),
  (qb) => qb.limit(10).offset(20),
);

matches

AND together equality filters, skipping undefined entries. null values become IS NULL (a plain = NULL never matches in SQL). Returns undefined when nothing remains, so it nests inside drizzle's and() / or():

import { and, eq } from "drizzle-orm";
import { matches } from "@joyful/drizzle";

db.run((d) =>
  d.select().from(boards).where(and(
    matches(boards, { workspaceId, id: boardId }),
    eq(boards.archived, false),
  ))
);

tx — Result-aware transactions

The callback either returns Result / AsyncResult / Promise<Result>, or is an async function* — the generator is driven through Result.run for you. Ok commits; Err (returned or yield*ed) rolls back and is returned as-is. Driver throws become DatabaseError. Extra arguments after the function are passed through:

const created = await db.tx(async function* (s) {
  yield* s.run((d) => d.insert(workspaces).values({ id, name }));
  const [board] = yield* s.run((d) =>
    d.insert(boards).values({ id: boardId, workspaceId: id, title })
      .returning()
  );
  return board;
});

await db.tx(createBoard, workspaceId, input);

The explicit form db.tx((s) => Result.run(async function* () { ... })) still works and is equivalent.

Nesting / savepoints

JoyDb<Tx>.tx calls tx.transaction(...), which is drizzle's savepoint. There is no special nesting API — inner Err rolls back the inner writes only, and if the outer callback still returns Ok the outer work commits:

await db.tx(async (s) => {
  const outer = await s.run((d) => d.insert(workspaces).values(workspace));
  if (outer.isErr()) return outer;
  const inner = await s.tx(async (s2) => {
    const wrote = await s2.run((d) => d.insert(boards).values(board));
    if (wrote.isErr()) return wrote;
    return Result.err(new Conflict({}));
  });
  // inner writes are gone; workspace remains if we return Ok
  return Result.ok("handled");
});

Session-first convention

Partial / composable queries are plain functions with s: Session as the first parameter:

  • Plain execution: myQuery(db, ...args)
  • Atomic: db.tx(myQuery, ...args) or db.tx((s) => myQuery(s, ...args))
  • Composition: yield* subQuery(s, ...) inside Result.run
import { Result, taggedError } from "@joyful/result";
import type { Session } from "./db.ts";

class NotFound extends taggedError("NotFound") {}

function getWorkspace(s: Session, id: string) {
  return Result.run(async function* () {
    const [row] = yield* s.run((d) =>
      d.select().from(workspaces).where(eq(workspaces.id, id))
    );
    if (!row) yield* new NotFound({});
    return row;
  });
}

function createBoard(s: Session, workspaceId: string, title: string) {
  return Result.run(async function* () {
    const workspace = yield* getWorkspace(s, workspaceId);
    const [board] = yield* s.run((d) =>
      d.insert(boards).values({
        id: crypto.randomUUID(),
        workspaceId: workspace.id,
        title,
      }).returning()
    );
    return board;
  });
}

// not atomic: a later failure leaves the workspace write in place
await createBoard(db, wsId, "Roadmap");

// atomic: any Err rolls back every write in the helper
await db.tx(createBoard, wsId, "Roadmap");

Helpers can also be async generators — db.tx drives them through Result.run, and they compose into other generators with yield*:

async function* createBoard(s: Session, workspaceId: string, title: string) {
  const workspace = yield* getWorkspace(s, workspaceId);
  const [board] = yield* s.run((d) =>
    d.insert(boards).values({
      id: crypto.randomUUID(),
      workspaceId: workspace.id,
      title,
    }).returning()
  );
  return board;
}

// atomic: db.tx runs the generator inside the transaction
await db.tx(createBoard, wsId, "Roadmap");

// composes into a larger transaction
await db.tx(async function* (s) {
  const board = yield* createBoard(s, wsId, "Roadmap");
  yield* s.run((d) => d.insert(items).values(seedItems(board.id)));
  return board;
});

// plain (non-atomic) execution needs the Result.run wrapper
await Result.run(() => createBoard(db, wsId, "Roadmap"));

run and tx are bound, so helpers can also destructure a session:

function listBoards({ run }: Session, workspaceId: string) {
  return run((d) =>
    d.select().from(boards).where(eq(boards.workspaceId, workspaceId))
  );
}

$client / $driver

Escape hatches for APIs this wrapper does not cover:

  • $client — the wrapped drizzle instance (root db or the current transaction)
  • $driver — drizzle's own .$client (libsql Client, pg Pool, …). never on a transaction session
await db.$driver.execute("select 1");
await db.$client.query.boards.findMany({
  where: eq(boards.workspaceId, wsId),
});

API

  • jdb(client) — wrap a drizzle database or transaction. Also takes a lazy () => client initializer, resolved and memoized on first use.
  • JoyDb — the wrapper class. run and tx are bound arrow properties.
  • DatabaseError — tagged error for driver / query failures; original error is cause.
  • matches(table, values) — AND of eq for defined keys (null → IS NULL); undefined if empty.
  • SessionOf<typeof db> — session type shared by the root db and tx callbacks. Also available as typeof db.$inferSession (type-only property).
  • TxOf<C> — transaction client of C (TxOf<Tx> = Tx).
  • DriverOf<C> — drizzle .$client driver handle, or never on a transaction.

License

MIT