Overview
@woltz/rich-domain-cli is a command-line tool that generates domain entities, aggregates, repositories, and mappers from your Prisma schema. It analyzes relationships to automatically classify models as Aggregates or Entities.
npm install -D @woltz/rich-domain-cli
# Or run directly with npx
npx @woltz/rich-domain-cli generate
Prisma Introspection Reads schema using @prisma/internals DMMF
Smart Classification Auto-detects Aggregates vs Entities from relationships
Dependency Resolution Topological sort ensures correct generation order
Full Stack Generation Schemas, entities, repositories, and mappers
Quick Start
# Generate from default prisma/schema.prisma
npx rich-domain generate
# Specify schema path
npx rich-domain generate --schema prisma/schema.prisma
# Generate to custom directory
npx rich-domain generate --output src/domain
# Use specific validation library
npx rich-domain generate --validation zod
# Generate only specific models
npx rich-domain generate --models User,Post,Comment
# Preview without writing files
npx rich-domain generate --dry-run
Command Reference
generate
Generate domain files from Prisma schema.
npx rich-domain generate [options]
Options
Option Alias Default Description --schema <path>-sAuto-detect Path to Prisma schema file --output <path>-osrc/domainOutput directory --validation <type>-vAuto-detect Validation library: zod, valibot, arktype --models <names>-mAll models Comma-separated list of models to generate --dry-run- falsePreview without writing files --force-ffalseSkip confirmation prompts
Examples
# Basic usage
npx rich-domain generate
# Full options
npx rich-domain generate \
--schema ./prisma/schema.prisma \
--output ./src/domain \
--validation zod \
--models User,Post,Comment
# Preview mode
npx rich-domain generate --dry-run
Package Detection
The CLI automatically detects installed packages and adjusts generation:
🔍 Rich Domain Generator
Reading schema from prisma/schema.prisma
Detected packages:
• @woltz/rich-domain-prisma: ✓ installed
• Validation library: zod
Package Effect @woltz/rich-domain-prismaGenerates repositories and mappers zodUses Zod for schema validation valibotUses Valibot for schema validation arktypeUses ArkType for schema validation None Generates TypeScript interfaces only
Repository and mapper files are only generated when @woltz/rich-domain-prisma is installed.
Generated Structure
For a Prisma schema with User, Post, and Comment models:
src/domain/
├── shared/
│ └── enums.ts # All Prisma enums
├── user/
│ ├── index.ts # Barrel export
│ ├── user.schema.ts # Zod schema
│ ├── user.aggregate.ts # Aggregate class
│ ├── user.repository.ts # Repository (if prisma adapter installed)
│ ├── user-to-domain.mapper.ts # Domain mapper
│ └── user-to-persistence.mapper.ts # Persistence mapper
├── post/
│ ├── index.ts
│ ├── post.schema.ts
│ ├── post.aggregate.ts # Aggregate (has children)
│ ├── post.repository.ts
│ ├── post-to-domain.mapper.ts
│ └── post-to-persistence.mapper.ts
└── comment/
├── index.ts
├── comment.schema.ts
└── comment.entity.ts # Entity (owned by Post)
Classification Logic
The CLI classifies models as Aggregates or Entities based on their relationships.
Classification Rules
Condition Classification Reason Referenced by others via FK Aggregate It’s a parent/root Has list relations (1:N) Aggregate It owns children Has N:N relations Aggregate Both sides are roots Only has FKs, not referenced Entity It’s a child/owned No relations Aggregate Standalone root
Example Analysis
model User {
id String @id
posts Post [] // Has 1:N → Aggregate
}
model Post {
id String @id
authorId String
author User @relation ( fields : [ authorId ], references : [ id ] )
comments Comment [] // Has 1:N AND FK → Aggregate
}
model Comment {
id String @id
postId String
post Post @relation ( fields : [ postId ], references : [ id ] )
// Only has FK, not referenced → Entity
}
model Tag {
id String @id
name String
// No relations → Aggregate (standalone)
}
Results:
User → Aggregate (referenced by Post, has list)
Post → Aggregate (referenced by Comment, has list)
Comment → Entity (only has FK, not referenced)
Tag → Aggregate (standalone)
Generated Code Examples
model User {
id String @id @default ( uuid ())
email String @unique
name String
status UserStatus @default ( ACTIVE )
posts Post []
createdAt DateTime @default ( now ())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default ( uuid ())
title String
content String
published Boolean @default ( false )
authorId String
author User @relation ( fields : [ authorId ], references : [ id ] )
createdAt DateTime @default ( now ())
}
enum UserStatus {
ACTIVE
INACTIVE
PENDING
}
Output: Zod Schema
// src/domain/user/user.schema.ts
import { z } from "zod" ;
import { Id } from "@woltz/rich-domain" ;
import { Post } from "../post/post.aggregate.js" ;
export const UserStatus = {
ACTIVE: "ACTIVE" ,
INACTIVE: "INACTIVE" ,
PENDING: "PENDING" ,
} as const ;
export type UserStatus = ( typeof UserStatus )[ keyof typeof UserStatus ];
export const userSchema = z . object ({
id: z . custom < Id >(( v ) => v instanceof Id ),
email: z . string (). email (),
name: z . string (),
status: z . nativeEnum ( UserStatus ),
posts: z . array ( z . custom < Post >(( v ) => v instanceof Post )),
createdAt: z . date (),
updatedAt: z . date (),
});
export type UserProps = z . infer < typeof userSchema >;
Output: Aggregate Class
// src/domain/user/user.aggregate.ts
import { Aggregate , Id , EntityValidation } from "@woltz/rich-domain" ;
import { userSchema , UserProps , UserStatus } from "./user.schema.js" ;
import { Post } from "../post/post.aggregate.js" ;
export class User extends Aggregate < UserProps > {
protected static validation : EntityValidation < UserProps > = {
schema: userSchema ,
};
get email () {
return this . props . email ;
}
get name () {
return this . props . name ;
}
set name ( value : string ) {
this . props . name = value ;
}
get status () {
return this . props . status ;
}
get posts () {
return this . props . posts ;
}
get createdAt () {
return this . props . createdAt ;
}
get updatedAt () {
return this . props . updatedAt ;
}
// Generated collection methods
addPost ( post : Post ) : void {
this . props . posts . push ( post );
}
removePost ( postId : Id ) : void {
const index = this . props . posts . findIndex (( p ) => p . id . equals ( postId ));
if ( index !== - 1 ) {
this . props . posts . splice ( index , 1 );
}
}
}
Output: Entity Class
// src/domain/comment/comment.entity.ts
import { Entity , Id , EntityValidation } from "@woltz/rich-domain" ;
import { commentSchema , CommentProps } from "./comment.schema.js" ;
export class Comment extends Entity < CommentProps > {
protected static validation : EntityValidation < CommentProps > = {
schema: commentSchema ,
};
get text () {
return this . props . text ;
}
set text ( value : string ) {
this . props . text = value ;
}
get createdAt () {
return this . props . createdAt ;
}
}
Output: Repository
// src/domain/user/user.repository.ts
import { PrismaClient } from "@prisma/client" ;
import { PrismaRepository , PrismaUnitOfWork } from "@woltz/rich-domain-prisma" ;
import { User } from "./user.aggregate.js" ;
import { UserToDomainMapper } from "./user-to-domain.mapper.js" ;
import { UserToPersistenceMapper } from "./user-to-persistence.mapper.js" ;
export class UserRepository extends PrismaRepository < User > {
protected readonly model = "user" ;
protected readonly includes = {
posts: true ,
};
constructor ( prisma : PrismaClient , uow : PrismaUnitOfWork ) {
super (
new UserToPersistenceMapper ( prisma , uow ),
new UserToDomainMapper (),
prisma ,
uow
);
}
protected generateSearchQuery ( search : string ) : any [] {
return [
{ email: { contains: search , mode: "insensitive" } },
{ name: { contains: search , mode: "insensitive" } },
];
}
}
Output: Domain Mapper
// src/domain/user/user-to-domain.mapper.ts
import { Mapper , Id } from "@woltz/rich-domain" ;
import { User } from "./user.aggregate.js" ;
import { Post } from "../post/post.aggregate.js" ;
type PrismaUser = {
id : string ;
email : string ;
name : string ;
status : string ;
createdAt : Date ;
updatedAt : Date ;
};
type PrismaUserWithRelations = PrismaUser & {
posts ?: PrismaPost [];
};
export class UserToDomainMapper extends Mapper < PrismaUserWithRelations , User > {
build ( raw : PrismaUserWithRelations ) : User {
return new User ({
id: Id . from ( raw . id ),
email: raw . email ,
name: raw . name ,
status: raw . status as UserStatus ,
posts: raw . posts ?. map (( item ) => this . mapPost ( item )) ?? [],
createdAt: raw . createdAt ,
updatedAt: raw . updatedAt ,
});
}
private mapPost ( raw : PrismaPost ) : Post {
return new Post ({
id: Id . from ( raw . id ),
title: raw . title ,
content: raw . content ,
published: raw . published ,
createdAt: raw . createdAt ,
});
}
}
Output: Persistence Mapper
// src/domain/user/user-to-persistence.mapper.ts
import { PrismaClient } from "@prisma/client" ;
import { PrismaToPersistence , PrismaUnitOfWork } from "@woltz/rich-domain-prisma" ;
import { User } from "./user.aggregate.js" ;
export class UserToPersistenceMapper extends PrismaToPersistence < User > {
constructor ( prisma : PrismaClient , uow : PrismaUnitOfWork ) {
super ( prisma , uow , {
posts: { type: "entity" , model: "post" },
});
}
protected toPrismaCreate ( entity : User ) {
return {
id: entity . id . value ,
email: entity . email ,
name: entity . name ,
status: entity . status ,
createdAt: entity . createdAt ,
updatedAt: entity . updatedAt ,
};
}
protected toPrismaUpdate ( entity : User ) {
return {
name: entity . name ,
status: entity . status ,
updatedAt: new Date (),
};
}
}
Dependency Resolution
The CLI uses topological sorting to generate files in the correct order:
Standalone models (no relations) - Generated first
Entities (owned by aggregates) - Generated second
Aggregates (owners) - Generated last
This ensures that when using z.instanceof(Entity) in schemas, the referenced class already exists.
Generation Order:
1. Tag (standalone)
2. Comment (entity, owned by Post)
3. Post (aggregate, owns Comment)
4. User (aggregate, owns Post)
Bidirectional relations (User → Post[], Post → User) are normal in Prisma and handled correctly. They are not treated as problematic cycles.
Validation Libraries
Zod (Default)
// user.schema.ts
import { z } from "zod" ;
export const userSchema = z . object ({
id: z . custom < Id >(( v ) => v instanceof Id ),
email: z . string (). email (),
name: z . string (),
age: z . number (). int (). min ( 0 ),
isActive: z . boolean (),
createdAt: z . date (),
});
Valibot
// user.schema.ts
import * as v from "valibot" ;
export const userSchema = v . object ({
id: v . custom < Id >(( v ) => v instanceof Id ),
email: v . pipe ( v . string (), v . email ()),
name: v . string (),
age: v . pipe ( v . number (), v . integer (), v . minValue ( 0 )),
isActive: v . boolean (),
createdAt: v . date (),
});
ArkType
// user.schema.ts
import { type } from "arktype" ;
export const userSchema = type ({
id: "instanceof Id" ,
email: "string.email" ,
name: "string" ,
age: "integer >= 0" ,
isActive: "boolean" ,
createdAt: "Date" ,
});
None (Interfaces Only)
// user.schema.ts
import { Id } from "@woltz/rich-domain" ;
export interface UserProps {
id : Id ;
email : string ;
name : string ;
age : number ;
isActive : boolean ;
createdAt : Date ;
}
Best Practices
After Generation
Review classifications - Adjust Aggregate/Entity based on actual domain boundaries
Add business logic - The generated code is a starting point
Add validation rules - Customize schemas with business constraints
Configure hooks - Add lifecycle hooks as needed
When to Re-generate
After changing Prisma schema structure
After adding new models
Use --models flag to regenerate specific models only
Use --dry-run first to preview changes before overwriting files.
Manual Adjustments
Some scenarios require manual adjustment:
Scenario Adjustment Needed Lookup tables (Tag, Category) May want to be Value Objects Self-referential relations Review aggregate boundaries Complex N:N Decide which side owns the relation
Requirements
Node.js >= 20
Prisma schema file
@woltz/rich-domain (required)
@woltz/rich-domain-prisma (optional, for repositories/mappers)
Validation library (optional, for runtime validation)