Skip to main content

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

OptionAliasDefaultDescription
--schema <path>-sAuto-detectPath to Prisma schema file
--output <path>-osrc/domainOutput directory
--validation <type>-vAuto-detectValidation library: zod, valibot, arktype
--models <names>-mAll modelsComma-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
PackageEffect
@woltz/rich-domain-prismaGenerates repositories and mappers
zodUses Zod for schema validation
valibotUses Valibot for schema validation
arktypeUses ArkType for schema validation
NoneGenerates 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

ConditionClassificationReason
Referenced by others via FKAggregateIt’s a parent/root
Has list relations (1:N)AggregateIt owns children
Has N:N relationsAggregateBoth sides are roots
Only has FKs, not referencedEntityIt’s a child/owned
No relationsAggregateStandalone 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:
  • UserAggregate (referenced by Post, has list)
  • PostAggregate (referenced by Comment, has list)
  • CommentEntity (only has FK, not referenced)
  • TagAggregate (standalone)

Generated Code Examples

Input: Prisma Schema

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:
  1. Standalone models (no relations) - Generated first
  2. Entities (owned by aggregates) - Generated second
  3. 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

  1. Review classifications - Adjust Aggregate/Entity based on actual domain boundaries
  2. Add business logic - The generated code is a starting point
  3. Add validation rules - Customize schemas with business constraints
  4. 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:
ScenarioAdjustment Needed
Lookup tables (Tag, Category)May want to be Value Objects
Self-referential relationsReview aggregate boundaries
Complex N:NDecide 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)