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:
Feature Prisma Drizzle Nested writes Built-in Manual Unit of Work Automatic Manual with AsyncLocalStorage Transaction API $transaction()db.transaction()Relation loading includeManual 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
Research & Design
Analyze Drizzle’s API and plan integration architecture
Core Implementation
DrizzleUnitOfWork and DrizzleRepository base classes
Criteria Translation
Build Criteria to Drizzle query translator
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 .