Skip to main content
Coming Soon — The Drizzle adapter is currently in development. This page outlines the planned features and architecture.

Overview

@woltz/rich-domain-drizzle will provide integration between rich-domain and Drizzle ORM. Due to Drizzle’s different architecture (no built-in nested writes or automatic Unit of Work), this adapter will require more manual configuration than the Prisma adapter.
# Coming soon
npm install @woltz/rich-domain @woltz/rich-domain-drizzle

Planned Features

Unit of Work

Manual transaction management with AsyncLocalStorage

Repository Base Class

DrizzleRepository with Criteria support

Change Tracking

DrizzleToPersistence with manual batch writes

Query Builder

Criteria to Drizzle query translation

Architecture Differences

Unlike Prisma, Drizzle doesn’t have built-in nested writes or automatic transaction management. This means:
FeaturePrismaDrizzle
Nested writesBuilt-inManual
Unit of WorkAutomaticManual with AsyncLocalStorage
Transaction API$transaction()db.transaction()
Relation loadingincludeManual joins or with

Planned API

DrizzleUnitOfWork

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

const db = drizzle(pool);
const uow = new DrizzleUnitOfWork(db);

// Execute in transaction
await uow.transaction(async () => {
  await userRepository.save(user);
  await orderRepository.save(order);
});

DrizzleRepository

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

class UserRepository extends DrizzleRepository<User, typeof users> {
  protected readonly table = users;

  constructor(db: DrizzleDB, uow: DrizzleUnitOfWork) {
    super(
      new UserToPersistenceMapper(db, uow),
      new UserToDomainMapper(),
      db,
      uow
    );
  }

  // Relations must be loaded manually
  async findByIdWithPosts(id: string): Promise<User | null> {
    const result = await this.context
      .select()
      .from(users)
      .leftJoin(posts, eq(posts.authorId, users.id))
      .where(eq(users.id, id));

    return result.length ? this.mapperToDomain.build(result) : null;
  }
}

DrizzleToPersistence

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

class UserToPersistenceMapper extends DrizzleToPersistence<User> {
  protected readonly registry = new EntitySchemaRegistry()
    .register({ entity: "User", table: "users" })
    .register({ entity: "Post", table: "posts" });

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

    // Insert posts manually (no nested writes)
    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,
          authorId: user.id.value,
        }))
      );
    }
  }

  protected async onUpdate(
    user: User,
    changes: AggregateChanges
  ): Promise<void> {
    const batch = changes.toBatchOperations();

    // Process deletes
    for (const del of batch.deletes) {
      const table = this.getTable(del.entity);
      await this.context.delete(table).where(inArray(table.id, del.ids));
    }

    // Process creates
    for (const create of batch.creates) {
      const table = this.getTable(create.entity);
      const records = create.items.map((item) => this.mapToRecord(item));
      await this.context.insert(table).values(records);
    }

    // Process updates
    for (const upd of batch.updates) {
      for (const item of upd.items) {
        const table = this.getTable(upd.entity);
        await this.context
          .update(table)
          .set(item.changedFields)
          .where(eq(table.id, item.id));
      }
    }
  }
}

Criteria Translation

Planned support for translating Criteria to Drizzle queries:
import { criteriaToWhere } from "@woltz/rich-domain-drizzle";

const criteria = Criteria.create<User>()
  .whereEquals("status", "active")
  .where("age", "greaterThan", 18)
  .orderByDesc("createdAt")
  .paginate(1, 10);

// Translate to Drizzle
const whereClause = criteriaToWhere(criteria, users);
const orderClause = criteriaToOrder(criteria, users);
const pagination = criteria.getPagination();

const result = await db
  .select()
  .from(users)
  .where(whereClause)
  .orderBy(orderClause)
  .limit(pagination.limit)
  .offset(pagination.offset);

Timeline

1

Research & Design

Analyze Drizzle’s API and plan integration architecture
2

Core Implementation

DrizzleUnitOfWork and DrizzleRepository base classes
3

Criteria Translation

Build Criteria to Drizzle query translator
4

Testing & Documentation

Comprehensive tests and documentation

Want to Contribute?

We welcome contributions! If you’re interested in helping build the Drizzle adapter:

Alternative: Manual Integration

While waiting for the official adapter, you can integrate Drizzle manually:
import { Repository, Criteria, PaginatedResult } from "@woltz/rich-domain";
import { drizzle } from "drizzle-orm/node-postgres";
import { eq, and, or, like, gt, lt, gte, lte, inArray } from "drizzle-orm";

class UserRepository extends Repository<User> {
  constructor(private db: ReturnType<typeof drizzle>) {
    super();
  }

  async findById(id: string): Promise<User | null> {
    const [record] = await this.db
      .select()
      .from(users)
      .where(eq(users.id, id));

    return record ? this.toDomain(record) : null;
  }

  async find(criteria: Criteria<User>): Promise<PaginatedResult<User>> {
    const whereClause = this.buildWhere(criteria);
    const pagination = criteria.getPagination();

    const [data, countResult] = await Promise.all([
      this.db
        .select()
        .from(users)
        .where(whereClause)
        .limit(pagination.limit)
        .offset(pagination.offset),
      this.db.select({ count: sql`count(*)` }).from(users).where(whereClause),
    ]);

    const total = Number(countResult[0].count);
    return PaginatedResult.create(
      data.map((d) => this.toDomain(d)),
      pagination,
      total
    );
  }

  async save(user: User): Promise<void> {
    if (user.isNew()) {
      await this.db.insert(users).values({
        id: user.id.value,
        name: user.name,
        email: user.email,
      });
    } else {
      await this.db
        .update(users)
        .set({ name: user.name, email: user.email })
        .where(eq(users.id, user.id.value));
    }
  }

  async delete(user: User): Promise<void> {
    await this.db.delete(users).where(eq(users.id, user.id.value));
  }

  private buildWhere(criteria: Criteria<User>) {
    const filters = criteria.getFilters();
    const conditions = filters.map((f) => {
      switch (f.operator) {
        case "equals":
          return eq(users[f.field], f.value);
        case "contains":
          return like(users[f.field], `%${f.value}%`);
        case "greaterThan":
          return gt(users[f.field], f.value);
        // ... other operators
      }
    });

    return conditions.length > 0 ? and(...conditions) : undefined;
  }

  private toDomain(record: UserRecord): User {
    return new User({
      id: Id.from(record.id),
      name: record.name,
      email: record.email,
    });
  }
}
This manual approach doesn’t include change tracking or Unit of Work. For full DDD support, wait for the official adapter or use the Prisma adapter.