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

@woltz/rich-domain-drizzle

v0.1.2

Published

Drizzle integration for Rich Domain Library

Readme

@woltz/rich-domain-drizzle

Drizzle ORM adapter for @woltz/rich-domain. Provides plug and play integration between rich-domain and Drizzle ORM.

Installation

npm install @woltz/rich-domain @woltz/rich-domain-drizzle drizzle-orm

Features

  • Unit of Work with AsyncLocalStorage (request isolation)
  • DrizzleRepository base class with Criteria support
  • DrizzleToPersistence base class with change tracking
  • @Transactional decorator for automatic transactions
  • DrizzleBatchExecutor for batch change operations
  • EntitySchemaRegistry for owned (1:N) and reference (N:N) collections
  • Junction table support for explicit N:N mappings

Quick Start

1. Setup

import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./schema";
import { DrizzleUnitOfWork } from "@woltz/rich-domain-drizzle";

type DB = ReturnType<typeof drizzle<typeof schema>>;

const db: DB = drizzle(pool, { schema });
const uow = new DrizzleUnitOfWork(db);

2. Create Repository

import { DrizzleRepository, SearchableField } from "@woltz/rich-domain-drizzle";
import { users } from "./schema";

type DB = ReturnType<typeof drizzle<typeof schema>>;

class UserRepository extends DrizzleRepository<User, UserRecord, DB> {
  constructor(db: DB, uow: DrizzleUnitOfWork) {
    super({
      db,
      table: users,
      toDomainMapper: new UserToDomainMapper(),
      toPersistenceMapper: new UserToPersistenceMapper(db, uow),
      uow,
    });
  }

  protected get model() {
    return "users"; // key in db.query — matches Drizzle schema export name
  }

  protected getSearchableFields(): SearchableField<UserRecord>[] {
    return ["name", "email"];
  }

  protected getDefaultRelations() {
    return { posts: { with: { tags: { with: { tag: true } } } } };
  }
}

3. Use It

const userRepo = new UserRepository(uow);

// Create
const user = User.create({
  name: "John",
  email: "[email protected]",
  posts: [],
});
await userRepo.save(user);

// Find by ID
const found = await userRepo.findById(user.id.value);

// Find with Criteria
const criteria = Criteria.create<User>()
  .whereEquals("name", "John")
  .orderByAsc("createdAt")
  .paginate(1, 10);

const result = await userRepo.find(criteria);
// result.data → User[]
// result.toJSON().meta.total → total count

// Update (automatic change tracking)
found.updateName("John Updated");
await userRepo.save(found); // Detects and applies only what changed

// Delete
await userRepo.delete(found);
await userRepo.deleteById(userId);

API Reference

DrizzleUnitOfWork

Manages transactions with per-request isolation using AsyncLocalStorage.

const uow = new DrizzleUnitOfWork(db);

// Execute in transaction
await uow.transaction(async () => {
  await userRepo.save(user);
  await postRepo.save(post);
  // All or nothing — rolls back on failure
});

// Check if in transaction
uow.isInTransaction(); // boolean

// Get current context
uow.getCurrentContext(); // DrizzleTransactionContext | null

DrizzleRepository

Base class for repositories with full Criteria support.

abstract class DrizzleRepository<
  TDomain,
  TRecord,
  TDb extends DrizzleClient = DrizzleClient,
> {
  constructor(config: { db: TDb; table: any; toDomainMapper; toPersistenceMapper; uow }) { ... }

  // Required: key in db.query matching the Drizzle schema export name
  protected abstract get model(): string;

  // Optional: searchable columns for Criteria "search" operator
  protected getSearchableFields(): SearchableField<TRecord>[];

  // Optional: default relations to include in queries
  protected getDefaultRelations(): Record<string, any>;

  // Available: current context (transaction or plain db) — typed as TDb
  protected get context(): TDb;

  // Available methods
  async find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
  async findById(id: string): Promise<TDomain | null>;
  async findOne(criteria: Criteria<TDomain>): Promise<TDomain | null>;
  async count(criteria?: Criteria<TDomain>): Promise<number>;
  async exists(id: string): Promise<boolean>;
  async save(entity: TDomain): Promise<void>;
  async delete(entity: TDomain): Promise<void>;
  async deleteById(id: string): Promise<void>;
}

Note: Criteria filters and ordering only support top-level columns of the primary table. Use custom methods with explicit JOINs for cross-table queries.


DrizzleToPersistence

Base class for persistence mappers with change tracking support.

abstract class DrizzleToPersistence<
  TDomain,
  TDb extends DrizzleClient = DrizzleClient,
> extends Mapper<TDomain, void> {
  constructor(db: TDb, uow: DrizzleUnitOfWork) { ... }

  // Required: registry for entity/table/collection mapping
  protected abstract readonly registry: EntitySchemaRegistry;

  // Required: map entity names to Drizzle table objects (and junction tables)
  protected abstract readonly tableMap: Map<string, any>;

  // Required: implement creation
  protected abstract onCreate(entity: TDomain): Promise<void>;

  // Available: current context (transaction or plain db) — typed as TDb
  protected get context(): TDb;
}

Example

import {
  DrizzleToPersistence,
  DrizzleUnitOfWork,
  Transactional,
} from "@woltz/rich-domain-drizzle";
import { EntitySchemaRegistry } from "@woltz/rich-domain";
import { users, posts, tags, postsToTags } from "./schema";

const userRegistry = new EntitySchemaRegistry()
  .register({
    entity: "User",
    table: "users",
    collections: {
      posts: { type: "owned", entity: "Post" },
    },
  })
  .register({
    entity: "Post",
    table: "posts",
    parentFk: { field: "authorId", parentEntity: "User" },
    collections: {
      tags: {
        type: "reference",
        entity: "Tag",
        junction: {
          table: "posts_to_tags", // must match tableMap key
          sourceKey: "postId",
          targetKey: "tagId",
        },
      },
    },
  })
  .register({ entity: "Tag", table: "tags" });

type DB = ReturnType<typeof drizzle<typeof schema>>;

class UserToPersistenceMapper extends DrizzleToPersistence<User, DB> {
  protected readonly registry = userRegistry;

  protected readonly tableMap = new Map<string, any>([
    ["User", users],
    ["Post", posts],
    ["Tag", tags],
    ["posts_to_tags", postsToTags], // junction table key must match registry
  ]);

  constructor(db: DB, uow: DrizzleUnitOfWork) {
    super(db, uow);
  }

  @Transactional()
  protected async onCreate(user: User): Promise<void> {
    await this.context.insert(users).values({
      id: user.id.value,
      email: user.email,
      name: user.name,
      createdAt: user.createdAt,
      updatedAt: user.updatedAt,
    });

    if (user.posts.length > 0) {
      await this.context.insert(posts).values(
        user.posts.map((p) => ({
          id: p.id.value,
          title: p.title,
          content: p.content,
          published: p.published,
          authorId: user.id.value,
          createdAt: p.createdAt,
          updatedAt: p.updatedAt,
        }))
      );
    }
  }
  // onUpdate is handled automatically by DrizzleBatchExecutor
}

EntitySchemaRegistry — Collection Types

| Type | Behavior | Junction required? | | ----------- | ------------------------------------------------------ | ------------------ | | owned | 1:N — child rows belong to the parent; deletes cascade | No | | reference | N:N — rows in a junction table | Yes — always |

Unlike Prisma, Drizzle does not manage implicit junction tables. Every reference collection must provide a junction config. Omitting it throws MissingJunctionConfigError.


@Transactional Decorator

Wraps a method in a transaction automatically.

import { Transactional } from "@woltz/rich-domain-drizzle";

class CreateUserUseCase {
  constructor(
    private readonly userRepo: UserRepository,
    private readonly uow: DrizzleUnitOfWork // Required!
  ) {}

  @Transactional()
  async execute(input: CreateUserInput): Promise<User> {
    const user = User.create({ ...input, posts: [] });
    await this.userRepo.save(user);
    return user;
  }
}

| Scenario | Behavior | | ---------------------- | ----------------------- | | Direct call | Creates new transaction | | Already in transaction | Reuses existing one | | Error thrown | Automatic rollback |

The class must expose a uow property of type DrizzleUnitOfWork for the decorator to find it.


DrizzleBatchExecutor

Executes batch operations from AggregateChanges in the correct order for referential integrity:

  1. Deletes — leaf → root (depth DESC)
  2. Creates — root → leaf (depth ASC)
  3. Updates — any order

onUpdate in DrizzleToPersistence is called automatically; you do not need to instantiate the executor directly.


Connect / Disconnect (N:N)

// Connect — adds a row to the junction table
const post = await postRepo.findById(postId);
post.addTag(new Tag({ id: new Id(tagId) }));
await postRepo.save(post);

// Disconnect — removes the row from the junction table
post.removeTag(new Tag({ id: new Id(tagId) }));
await postRepo.save(post);

Transactions

await uow.transaction(async () => {
  const user = User.create({ name: "Alice", email: "[email protected]", posts: [] });
  await userRepo.save(user);

  const post = Post.restore({ ... });
  await postRepo.save(post);
  // Both committed atomically, or both rolled back on error
});

Limitations

  • Criteria dot-field paths are not supported. Filters and ordering must reference top-level columns of the primary table. "posts.title" or "profile.name" throws DrizzleAdapterError. Add custom repository methods with explicit JOINs instead.
  • contains / startsWith / endsWith use ILIKE — PostgreSQL only.

Error Reference

| Error | When thrown | | ---------------------------- | ------------------------------------------------------------------ | | TableNotFoundError | tableMap key not found for an entity or junction name | | MissingJunctionConfigError | reference collection has no junction configured | | BatchOperationError | DB error during a batch create, update, or delete | | NoRecordsAffectedError | delete() / deleteById() matched 0 rows | | DrizzleAdapterError | Unsupported Criteria operator, dot-field path, or column not found |


Full Example

See the fastify-with-drizzle example for a complete working application.


License

MIT