@woltz/rich-domain 1.8.10 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,660 +1,660 @@
1
- # @woltz/rich-domain
2
-
3
- A TypeScript library for Domain-Driven Design with Standard Schema validation, automatic change tracking, and enterprise-ready repositories.
4
-
5
- [![npm version](https://img.shields.io/npm/v/@woltz/rich-domain.svg)](https://www.npmjs.com/package/@woltz/rich-domain)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- ## Features
9
-
10
- - 🎯 **Type-Safe DDD Building Blocks** - Entities, Aggregates, Value Objects with full TypeScript support
11
- - ✅ **Validation Agnostic** - Works with Zod, Valibot, ArkType, or any Standard Schema compatible library
12
- - 🔄 **Automatic Change Tracking** - Track changes across nested entities and collections without boilerplate
13
- - 🗄️ **ORM Independent** - Use with Prisma, TypeORM, Drizzle, or any persistence layer
14
- - 🔍 **Rich Query API** - Type-safe Criteria pattern with fluent API for complex queries
15
- - 📦 **Repository Pattern** - Abstract your persistence layer with built-in pagination and filtering
16
- - 🎭 **Domain Events** - Built-in event system for cross-aggregate communication
17
- - 🔐 **Lifecycle Hooks** - onCreate, onBeforeUpdate, and business rule validation
18
- - 🪝 **React Integration** - Ready-to-use hooks and components via [@woltz/react-rich-domain](https://www.npmjs.com/package/@woltz/react-rich-domain)
19
-
20
- ## Installation
21
-
22
- ```bash
23
- npm install @woltz/rich-domain
24
-
25
- # With your preferred validation library
26
- npm install zod # or valibot, arktype
27
- ```
28
-
29
- ## Quick Start
30
-
31
- ### 1. Define Your Domain Entities
32
-
33
- ```typescript
34
- import { Aggregate, Entity, Id } from "@woltz/rich-domain";
35
- import { z } from "zod";
36
-
37
- // Value Object
38
- class Email extends ValueObject<string> {
39
- protected static validation = {
40
- schema: z.string().email("Invalid email format"),
41
- };
42
-
43
- getDomain(): string {
44
- return this.value.split("@")[1];
45
- }
46
- }
47
-
48
- // Entity (child of Aggregate)
49
- const PostSchema = z.object({
50
- id: z.custom<Id>(),
51
- title: z.string().min(3),
52
- content: z.string(),
53
- published: z.boolean(),
54
- createdAt: z.date(),
55
- });
56
-
57
- class Post extends Entity<z.infer<typeof PostSchema>> {
58
- protected static validation = { schema: PostSchema };
59
-
60
- publish(): void {
61
- this.props.published = true;
62
- }
63
-
64
- get title() {
65
- return this.props.title;
66
- }
67
- }
68
-
69
- // Aggregate Root
70
- const UserSchema = z.object({
71
- id: z.custom<Id>(),
72
- email: z.custom<Email>(),
73
- name: z.string(),
74
- posts: z.array(z.instanceof(Post)),
75
- createdAt: z.date(),
76
- updatedAt: z.date(),
77
- });
78
-
79
- class User extends Aggregate<z.infer<typeof UserSchema>> {
80
- protected static validation = { schema: UserSchema };
81
-
82
- addPost(title: string, content: string): void {
83
- const post = new Post({
84
- title,
85
- content,
86
- published: false,
87
- createdAt: new Date(),
88
- });
89
- this.props.posts.push(post);
90
- }
91
-
92
- get email() {
93
- return this.props.email.value;
94
- }
95
-
96
- get posts() {
97
- return this.props.posts;
98
- }
99
- }
100
- ```
101
-
102
- ### 2. Automatic Change Tracking
103
-
104
- Changes are tracked automatically - no manual tracking needed:
105
-
106
- ```typescript
107
- // Load existing user
108
- const user = new User({
109
- id: Id.from("user-123"),
110
- email: new Email("john@example.com"),
111
- name: "John Doe",
112
- posts: [
113
- new Post({
114
- id: Id.from("post-1"),
115
- title: "First Post",
116
- content: "Content here",
117
- published: false,
118
- createdAt: new Date(),
119
- }),
120
- ],
121
- createdAt: new Date(),
122
- updatedAt: new Date(),
123
- });
124
-
125
- // Make changes
126
- user.addPost("Second Post", "More content"); // Create
127
- user.posts[0].publish(); // Update
128
- user.posts.splice(0, 1); // Delete
129
-
130
- // Get all changes automatically organized
131
- const changes = user.getChanges();
132
-
133
- console.log(changes.hasCreates()); // true
134
- console.log(changes.hasUpdates()); // true
135
- console.log(changes.hasDeletes()); // true
136
-
137
- // Changes are organized by depth for proper FK handling
138
- const batch = changes.toBatchOperations();
139
- // {
140
- // deletes: [{ entity: "Post", depth: 1, ids: ["post-1"] }],
141
- // creates: [{ entity: "Post", depth: 1, items: [...] }],
142
- // updates: [{ entity: "Post", depth: 1, items: [...] }]
143
- // }
144
- ```
145
-
146
- ### 3. Type-Safe Queries with Criteria
147
-
148
- Build complex queries with full type safety:
149
-
150
- ```typescript
151
- import { Criteria } from "@woltz/rich-domain";
152
-
153
- // Simple query
154
- const activePosts = Criteria.create<Post>()
155
- .where("published", "equals", true)
156
- .orderBy("createdAt", "desc")
157
- .limit(10);
158
-
159
- // Complex query with multiple filters
160
- const criteria = Criteria.create<User>()
161
- .where("name", "contains", "John")
162
- .where("email", "startsWith", "john")
163
- .where("createdAt", "greaterThan", new Date("2024-01-01"))
164
- .orderBy("name", "asc")
165
- .paginate(1, 20);
166
-
167
- // Use with repository
168
- const result = await userRepository.find(criteria);
169
- // result: PaginatedResult<User>
170
-
171
- console.log(result.data); // User[]
172
- console.log(result.meta); // { page: 1, pageSize: 20, total: 100, totalPages: 5 }
173
- ```
174
-
175
- ### 4. Repository Pattern
176
-
177
- Abstract your persistence layer:
178
-
179
- ```typescript
180
- import { IRepository, Criteria } from "@woltz/rich-domain";
181
-
182
- interface IUserRepository extends IRepository<User> {
183
- findByEmail(email: string): Promise<User | null>;
184
- findActiveUsers(): Promise<User[]>;
185
- }
186
-
187
- class UserRepository implements IUserRepository {
188
- async save(user: User): Promise<void> {
189
- // Your persistence logic
190
- const changes = user.getChanges();
191
-
192
- // Handle deletes (deepest first)
193
- for (const deletion of changes.toBatchOperations().deletes) {
194
- // Delete by entity and IDs
195
- }
196
-
197
- // Handle creates (root first)
198
- for (const creation of changes.toBatchOperations().creates) {
199
- // Create new entities
200
- }
201
-
202
- // Handle updates
203
- for (const update of changes.toBatchOperations().updates) {
204
- // Update only changed fields
205
- }
206
- }
207
-
208
- async findById(id: string): Promise<User | null> {
209
- // Your query logic
210
- }
211
-
212
- async find(criteria: Criteria<User>): Promise<PaginatedResult<User>> {
213
- // Transform criteria to your ORM query
214
- const filters = criteria.getFilters();
215
- const ordering = criteria.getOrdering();
216
- const pagination = criteria.getPagination();
217
-
218
- // Execute query and return paginated result
219
- }
220
-
221
- async findByEmail(email: string): Promise<User | null> {
222
- const criteria = Criteria.create<User>().where("email", "equals", email);
223
-
224
- const result = await this.find(criteria);
225
- return result.data[0] ?? null;
226
- }
227
- }
228
- ```
229
-
230
- ## Advanced Features
231
-
232
- ### Lifecycle Hooks
233
-
234
- Add validation and side effects at key points:
235
-
236
- ```typescript
237
- class Product extends Aggregate<ProductProps> {
238
- protected static validation = {
239
- schema: ProductSchema,
240
- };
241
-
242
- protected static hooks = {
243
- onBeforeCreate: (props) => {
244
- // Set default values before validation
245
- if (!props.createdAt) {
246
- props.createdAt = new Date();
247
- }
248
- },
249
-
250
- onCreate: (entity) => {
251
- console.log(`Product created: ${entity.name}`);
252
- },
253
-
254
- onBeforeUpdate: (entity, snapshot) => {
255
- // Prevent price changes on inactive products
256
- if (snapshot.status === "inactive" && entity.price !== snapshot.price) {
257
- return false; // Reject the change
258
- }
259
- return true;
260
- },
261
-
262
- rules: (entity) => {
263
- if (entity.price > 1000 && entity.stock === 0) {
264
- entity.addValidationIssue(
265
- "stock",
266
- "Premium products must have stock available"
267
- );
268
- // Or use throwValidationError() for fail-fast
269
- }
270
- },
271
- };
272
- }
273
- ```
274
-
275
- With `throwOnError: false`, use `entity.addValidationIssue(path, message)` in `rules` and read `entity.validationErrors?.getFormattedErrors()` for `{ path, message }[]` field errors. Use `lockMutationsWhenInvalid: true` to block mutations while errors exist.
276
-
277
- ### Optional Input Properties
278
-
279
- Make properties optional at construction but required in the entity:
280
-
281
- ```typescript
282
- const userSchema = z.object({
283
- id: z.custom<Id>(),
284
- email: z.string().email(),
285
- password: z.string().min(8), // Required in entity
286
- createdAt: z.date(), // Required in entity
287
- });
288
-
289
- type UserProps = z.infer<typeof userSchema>;
290
-
291
- // Second generic makes 'password' and 'createdAt' optional at input
292
- class User extends Aggregate<UserProps, "password" | "createdAt"> {
293
- protected static validation = { schema: userSchema };
294
-
295
- protected static hooks = {
296
- onBeforeCreate: (props) => {
297
- // Generate values before validation
298
- if (!props.password) {
299
- props.password = generateEncryptedPassword();
300
- }
301
- if (!props.createdAt) {
302
- props.createdAt = new Date();
303
- }
304
- },
305
- };
306
-
307
- get email() {
308
- return this.props.email;
309
- }
310
- }
311
-
312
- // ✅ Works without password and createdAt
313
- const user = new User({
314
- email: "user@example.com",
315
- });
316
-
317
- // ✅ Also works with explicit values
318
- const customUser = new User({
319
- email: "user@example.com",
320
- password: "custom-pass-12345678",
321
- });
322
- ```
323
-
324
- ### Domain Events
325
-
326
- Communicate across aggregate boundaries:
327
-
328
- ```typescript
329
- import { DomainEvent } from "@woltz/rich-domain";
330
-
331
- class OrderConfirmedEvent extends DomainEvent {
332
- constructor(
333
- aggregateId: Id,
334
- public readonly customerId: string,
335
- public readonly total: number
336
- ) {
337
- super(aggregateId);
338
- }
339
-
340
- protected getPayload() {
341
- return { customerId: this.customerId, total: this.total };
342
- }
343
- }
344
-
345
- class Order extends Aggregate<OrderProps> {
346
- confirm(): void {
347
- if (this.props.items.length === 0) {
348
- throw new DomainError("Cannot confirm empty order");
349
- }
350
-
351
- this.props.status = "confirmed";
352
-
353
- // Emit event
354
- this.addDomainEvent(
355
- new OrderConfirmedEvent(this.id, this.customerId, this.total)
356
- );
357
- }
358
- }
359
-
360
- // After saving
361
- await orderRepository.save(order);
362
- await order.dispatchAll(eventBus);
363
- order.clearEvents();
364
- ```
365
-
366
- ### Value Objects
367
-
368
- Immutable wrappers for primitive values with domain behavior:
369
-
370
- ```typescript
371
- import { ValueObject, VOHooks, throwValidationError } from "@woltz/rich-domain";
372
-
373
- class Price extends ValueObject<number> {
374
- protected static validation = {
375
- schema: z.number().positive("Price must be positive"),
376
- };
377
-
378
- protected static hooks: VOHooks<number, Price> = {
379
- rules: (price) => {
380
- if (price.value > 1000000) {
381
- throwValidationError("value", "Price cannot exceed 1,000,000");
382
- }
383
- },
384
- };
385
-
386
- addTax(taxRate: number): Price {
387
- return this.clone(this.value * (1 + taxRate));
388
- }
389
-
390
- discount(percentage: number): Price {
391
- return this.clone(this.value * (1 - percentage / 100));
392
- }
393
-
394
- format(currency: string = "USD"): string {
395
- return new Intl.NumberFormat("en-US", {
396
- style: "currency",
397
- currency,
398
- }).format(this.value);
399
- }
400
- }
401
-
402
- const price = new Price(99.99);
403
- const withTax = price.addTax(0.08);
404
- const discounted = price.discount(10);
405
-
406
- console.log(price.format()); // "$99.99"
407
- console.log(withTax.format()); // "$107.99"
408
- console.log(discounted.format()); // "$89.99"
409
- ```
410
-
411
- ## Integration with ORMs
412
-
413
- Rich Domain provides official adapters for popular ORMs:
414
-
415
- ### Prisma
416
-
417
- ```bash
418
- npm install @woltz/rich-domain-prisma
419
- ```
420
-
421
- ```typescript
422
- import {
423
- PrismaRepository,
424
- PrismaToPersistence,
425
- } from "@woltz/rich-domain-prisma";
426
-
427
- class UserToPersistence extends PrismaToPersistence<User> {
428
- protected readonly registry = schemaRegistry;
429
-
430
- protected async onCreate(user: User): Promise<void> {
431
- await this.context.user.create({
432
- data: {
433
- id: user.id.value,
434
- email: user.email,
435
- name: user.name,
436
- },
437
- });
438
- }
439
-
440
- protected async onUpdate(
441
- changes: AggregateChanges,
442
- user: User
443
- ): Promise<void> {
444
- // Automatic batch operations handling
445
- }
446
- }
447
- ```
448
-
449
- ### TypeORM
450
-
451
- ```bash
452
- npm install @woltz/rich-domain-typeorm
453
- ```
454
-
455
- ```typescript
456
- import { TypeORMRepository } from "@woltz/rich-domain-typeorm";
457
-
458
- class UserRepository extends TypeORMRepository<User, UserEntity> {
459
- // Automatic change tracking and batch operations
460
- }
461
- ```
462
-
463
- ## CLI Tool
464
-
465
- Bootstrap projects and generate domain code:
466
-
467
- ```bash
468
- npm install -g @woltz/rich-domain-cli
469
-
470
- # Initialize new project
471
- rich-domain init my-app --template fullstack
472
-
473
- # Generate domain from Prisma schema
474
- rich-domain generate --schema prisma/schema.prisma
475
-
476
- # Add entity manually
477
- rich-domain add User name:string email:string --with-repo
478
- ```
479
-
480
- ## API Reference
481
-
482
- ### Core Classes
483
-
484
- #### `Id`
485
-
486
- Unique identifier for entities:
487
-
488
- ```typescript
489
- const id = Id.create(); // Generate new UUID
490
- const existingId = Id.from("uuid-string"); // From existing value
491
-
492
- console.log(id.value); // string
493
- console.log(id.isNew()); // boolean
494
- console.log(id.equals(otherId)); // boolean
495
- ```
496
-
497
- #### `Entity<T>`
498
-
499
- Base class for entities:
500
-
501
- ```typescript
502
- abstract class Entity<T extends { id: Id }> {
503
- get id(): Id;
504
- isNew(): boolean;
505
- equals(other: Entity<T>): boolean;
506
- toJSON(): object;
507
- }
508
- ```
509
-
510
- #### `Aggregate<T>`
511
-
512
- Root entity with change tracking:
513
-
514
- ```typescript
515
- abstract class Aggregate<T extends { id: Id }> extends Entity<T> {
516
- getChanges(): AggregateChanges;
517
-
518
- // Domain Events
519
- protected addDomainEvent(event: IDomainEvent): void;
520
- getUncommittedEvents(): IDomainEvent[];
521
- clearEvents(): void;
522
- dispatchAll(bus: IDomainEventBus): Promise<void>;
523
- }
524
- ```
525
-
526
- #### `ValueObject<T>`
527
-
528
- Immutable object compared by value:
529
-
530
- ```typescript
531
- abstract class ValueObject<T> {
532
- readonly value: T;
533
-
534
- equals(other: ValueObject<T>): boolean;
535
- toJSON(): T;
536
- protected clone(value: T): this;
537
- }
538
- ```
539
-
540
- ### Criteria API
541
-
542
- ```typescript
543
- class Criteria<T> {
544
- static create<T>(): Criteria<T>;
545
-
546
- // Filters
547
- where<K extends FieldPath<T>>(
548
- field: K,
549
- operator: FilterOperator,
550
- value: any
551
- ): this;
552
-
553
- // Ordering
554
- orderBy<K extends FieldPath<T>>(field: K, direction: "asc" | "desc"): this;
555
-
556
- // Pagination
557
- limit(limit: number): this;
558
- offset(offset: number): this;
559
- paginate(page: number, pageSize: number): this;
560
-
561
- // Search
562
- search(term: string): this;
563
-
564
- // Getters
565
- getFilters(): Filter<T>[];
566
- getOrdering(): Order<T> | null;
567
- getPagination(): Pagination | null;
568
- getSearch(): string | null;
569
- }
570
- ```
571
-
572
- ### Exception Handling
573
-
574
- Rich Domain provides comprehensive exception types:
575
-
576
- ```typescript
577
- import {
578
- ValidationError,
579
- DomainError,
580
- EntityNotFoundError,
581
- DuplicateEntityError,
582
- ConcurrencyError,
583
- RepositoryError,
584
- } from "@woltz/rich-domain";
585
-
586
- try {
587
- const user = new User({
588
- /* invalid props */
589
- });
590
- } catch (error) {
591
- if (error instanceof ValidationError) {
592
- console.log(error.entity); // "User"
593
- console.log(error.field); // "email"
594
- console.log(error.message); // "Invalid email format"
595
- }
596
- }
597
- ```
598
-
599
- ## Package Format
600
-
601
- This library is published as a **dual package** supporting both CommonJS and ES Modules:
602
-
603
- ```javascript
604
- // CommonJS
605
- const { Id, Entity, Aggregate } = require("@woltz/rich-domain");
606
-
607
- // ES Modules
608
- import { Id, Entity, Aggregate } from "@woltz/rich-domain";
609
- ```
610
-
611
- Benefits:
612
-
613
- - ✅ Universal compatibility (Node.js, Vite, Webpack, etc.)
614
- - ✅ Tree-shaking support for modern bundlers
615
- - ✅ Full TypeScript support with type definitions
616
- - ✅ Zero configuration - automatically uses the correct format
617
-
618
- ## Documentation
619
-
620
- - 📚 [Full Documentation](https://woltz.mintlify.app)
621
- - 🚀 [Quick Start Guide](https://woltz.mintlify.app/quickstart)
622
- - 📖 [Core Concepts](https://woltz.mintlify.app/core/entities-and-aggregates)
623
- - 🔌 [Integrations](https://woltz.mintlify.app/integrations/prisma)
624
- - ⚛️ [React Components](https://woltz.mintlify.app/integrations/react)
625
-
626
- ## Examples
627
-
628
- Check out the [examples directory](./examples) for complete implementations:
629
-
630
- - Basic CRUD operations
631
- - Complex aggregate relationships
632
- - Custom validation rules
633
- - Domain events
634
- - Repository implementations for different ORMs
635
-
636
- ## Ecosystem
637
-
638
- | Package | Description | Version |
639
- | ------------------------------------------------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
640
- | [@woltz/rich-domain](https://www.npmjs.com/package/@woltz/rich-domain) | Core library | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain.svg)](https://www.npmjs.com/package/@woltz/rich-domain) |
641
- | [@woltz/rich-domain-prisma](https://www.npmjs.com/package/@woltz/rich-domain-prisma) | Prisma adapter | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain-prisma.svg)](https://www.npmjs.com/package/@woltz/rich-domain-prisma) |
642
- | [@woltz/rich-domain-typeorm](https://www.npmjs.com/package/@woltz/rich-domain-typeorm) | TypeORM adapter | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain-typeorm.svg)](https://www.npmjs.com/package/@woltz/rich-domain-typeorm) |
643
- | [@woltz/rich-domain-criteria-zod](https://www.npmjs.com/package/@woltz/rich-domain-criteria-zod) | Zod criteria builder | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain-criteria-zod.svg)](https://www.npmjs.com/package/@woltz/rich-domain-criteria-zod) |
644
- | [@woltz/rich-domain-cli](https://www.npmjs.com/package/@woltz/rich-domain-cli) | CLI tool | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain-cli.svg)](https://www.npmjs.com/package/@woltz/rich-domain-cli) |
645
-
646
- ## Contributing
647
-
648
- Contributions are welcome! Please read our [Contributing Guide](./CONTRIBUTING.md) for details.
649
-
650
- ## License
651
-
652
- MIT © [Tarcisio Andrade](https://github.com/tarcisioandrade)
653
-
654
- ## Links
655
-
656
- - [Documentation](https://woltz.mintlify.app)
657
- - [GitHub Repository](https://github.com/tarcisioandrade/rich-domain)
658
- - [npm Package](https://www.npmjs.com/package/@woltz/rich-domain)
659
- - [Issues](https://github.com/tarcisioandrade/rich-domain/issues)
660
- - [Changelog](./CHANGELOG.md)
1
+ # @woltz/rich-domain
2
+
3
+ A TypeScript library for Domain-Driven Design with Standard Schema validation, automatic change tracking, and enterprise-ready repositories.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@woltz/rich-domain.svg)](https://www.npmjs.com/package/@woltz/rich-domain)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - 🎯 **Type-Safe DDD Building Blocks** - Entities, Aggregates, Value Objects with full TypeScript support
11
+ - ✅ **Validation Agnostic** - Works with Zod, Valibot, ArkType, or any Standard Schema compatible library
12
+ - 🔄 **Automatic Change Tracking** - Track changes across nested entities and collections without boilerplate
13
+ - 🗄️ **ORM Independent** - Use with Prisma, TypeORM, Drizzle, or any persistence layer
14
+ - 🔍 **Rich Query API** - Type-safe Criteria pattern with fluent API for complex queries
15
+ - 📦 **Repository Pattern** - Abstract your persistence layer with built-in pagination and filtering
16
+ - 🎭 **Domain Events** - Built-in event system for cross-aggregate communication
17
+ - 🔐 **Lifecycle Hooks** - onCreate, onBeforeUpdate, and business rule validation
18
+ - 🪝 **React Integration** - Ready-to-use hooks and components via [@woltz/react-rich-domain](https://www.npmjs.com/package/@woltz/react-rich-domain)
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @woltz/rich-domain
24
+
25
+ # With your preferred validation library
26
+ npm install zod # or valibot, arktype
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Define Your Domain Entities
32
+
33
+ ```typescript
34
+ import { Aggregate, Entity, Id } from "@woltz/rich-domain";
35
+ import { z } from "zod";
36
+
37
+ // Value Object
38
+ class Email extends ValueObject<string> {
39
+ protected static validation = {
40
+ schema: z.string().email("Invalid email format"),
41
+ };
42
+
43
+ getDomain(): string {
44
+ return this.value.split("@")[1];
45
+ }
46
+ }
47
+
48
+ // Entity (child of Aggregate)
49
+ const PostSchema = z.object({
50
+ id: z.custom<Id>(),
51
+ title: z.string().min(3),
52
+ content: z.string(),
53
+ published: z.boolean(),
54
+ createdAt: z.date(),
55
+ });
56
+
57
+ class Post extends Entity<z.infer<typeof PostSchema>> {
58
+ protected static validation = { schema: PostSchema };
59
+
60
+ publish(): void {
61
+ this.props.published = true;
62
+ }
63
+
64
+ get title() {
65
+ return this.props.title;
66
+ }
67
+ }
68
+
69
+ // Aggregate Root
70
+ const UserSchema = z.object({
71
+ id: z.custom<Id>(),
72
+ email: z.custom<Email>(),
73
+ name: z.string(),
74
+ posts: z.array(z.instanceof(Post)),
75
+ createdAt: z.date(),
76
+ updatedAt: z.date(),
77
+ });
78
+
79
+ class User extends Aggregate<z.infer<typeof UserSchema>> {
80
+ protected static validation = { schema: UserSchema };
81
+
82
+ addPost(title: string, content: string): void {
83
+ const post = new Post({
84
+ title,
85
+ content,
86
+ published: false,
87
+ createdAt: new Date(),
88
+ });
89
+ this.props.posts.push(post);
90
+ }
91
+
92
+ get email() {
93
+ return this.props.email.value;
94
+ }
95
+
96
+ get posts() {
97
+ return this.props.posts;
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### 2. Automatic Change Tracking
103
+
104
+ Changes are tracked automatically - no manual tracking needed:
105
+
106
+ ```typescript
107
+ // Load existing user
108
+ const user = new User({
109
+ id: Id.from("user-123"),
110
+ email: new Email("john@example.com"),
111
+ name: "John Doe",
112
+ posts: [
113
+ new Post({
114
+ id: Id.from("post-1"),
115
+ title: "First Post",
116
+ content: "Content here",
117
+ published: false,
118
+ createdAt: new Date(),
119
+ }),
120
+ ],
121
+ createdAt: new Date(),
122
+ updatedAt: new Date(),
123
+ });
124
+
125
+ // Make changes
126
+ user.addPost("Second Post", "More content"); // Create
127
+ user.posts[0].publish(); // Update
128
+ user.posts.splice(0, 1); // Delete
129
+
130
+ // Get all changes automatically organized
131
+ const changes = user.getChanges();
132
+
133
+ console.log(changes.hasCreates()); // true
134
+ console.log(changes.hasUpdates()); // true
135
+ console.log(changes.hasDeletes()); // true
136
+
137
+ // Changes are organized by depth for proper FK handling
138
+ const batch = changes.toBatchOperations();
139
+ // {
140
+ // deletes: [{ entity: "Post", depth: 1, ids: ["post-1"] }],
141
+ // creates: [{ entity: "Post", depth: 1, items: [...] }],
142
+ // updates: [{ entity: "Post", depth: 1, items: [...] }]
143
+ // }
144
+ ```
145
+
146
+ ### 3. Type-Safe Queries with Criteria
147
+
148
+ Build complex queries with full type safety:
149
+
150
+ ```typescript
151
+ import { Criteria } from "@woltz/rich-domain";
152
+
153
+ // Simple query
154
+ const activePosts = Criteria.create<Post>()
155
+ .where("published", "equals", true)
156
+ .orderBy("createdAt", "desc")
157
+ .limit(10);
158
+
159
+ // Complex query with multiple filters
160
+ const criteria = Criteria.create<User>()
161
+ .where("name", "contains", "John")
162
+ .where("email", "startsWith", "john")
163
+ .where("createdAt", "greaterThan", new Date("2024-01-01"))
164
+ .orderBy("name", "asc")
165
+ .paginate(1, 20);
166
+
167
+ // Use with repository
168
+ const result = await userRepository.find(criteria);
169
+ // result: PaginatedResult<User>
170
+
171
+ console.log(result.data); // User[]
172
+ console.log(result.meta); // { page: 1, pageSize: 20, total: 100, totalPages: 5 }
173
+ ```
174
+
175
+ ### 4. Repository Pattern
176
+
177
+ Abstract your persistence layer:
178
+
179
+ ```typescript
180
+ import { IRepository, Criteria } from "@woltz/rich-domain";
181
+
182
+ interface IUserRepository extends IRepository<User> {
183
+ findByEmail(email: string): Promise<User | null>;
184
+ findActiveUsers(): Promise<User[]>;
185
+ }
186
+
187
+ class UserRepository implements IUserRepository {
188
+ async save(user: User): Promise<void> {
189
+ // Your persistence logic
190
+ const changes = user.getChanges();
191
+
192
+ // Handle deletes (deepest first)
193
+ for (const deletion of changes.toBatchOperations().deletes) {
194
+ // Delete by entity and IDs
195
+ }
196
+
197
+ // Handle creates (root first)
198
+ for (const creation of changes.toBatchOperations().creates) {
199
+ // Create new entities
200
+ }
201
+
202
+ // Handle updates
203
+ for (const update of changes.toBatchOperations().updates) {
204
+ // Update only changed fields
205
+ }
206
+ }
207
+
208
+ async findById(id: string): Promise<User | null> {
209
+ // Your query logic
210
+ }
211
+
212
+ async find(criteria: Criteria<User>): Promise<PaginatedResult<User>> {
213
+ // Transform criteria to your ORM query
214
+ const filters = criteria.getFilters();
215
+ const ordering = criteria.getOrdering();
216
+ const pagination = criteria.getPagination();
217
+
218
+ // Execute query and return paginated result
219
+ }
220
+
221
+ async findByEmail(email: string): Promise<User | null> {
222
+ const criteria = Criteria.create<User>().where("email", "equals", email);
223
+
224
+ const result = await this.find(criteria);
225
+ return result.data[0] ?? null;
226
+ }
227
+ }
228
+ ```
229
+
230
+ ## Advanced Features
231
+
232
+ ### Lifecycle Hooks
233
+
234
+ Add validation and side effects at key points:
235
+
236
+ ```typescript
237
+ class Product extends Aggregate<ProductProps> {
238
+ protected static validation = {
239
+ schema: ProductSchema,
240
+ };
241
+
242
+ protected static hooks = {
243
+ onBeforeCreate: (props) => {
244
+ // Set default values before validation
245
+ if (!props.createdAt) {
246
+ props.createdAt = new Date();
247
+ }
248
+ },
249
+
250
+ onCreate: (entity) => {
251
+ console.log(`Product created: ${entity.name}`);
252
+ },
253
+
254
+ onBeforeUpdate: (entity, snapshot) => {
255
+ // Prevent price changes on inactive products
256
+ if (snapshot.status === "inactive" && entity.price !== snapshot.price) {
257
+ return false; // Reject the change
258
+ }
259
+ return true;
260
+ },
261
+
262
+ rules: (entity) => {
263
+ if (entity.price > 1000 && entity.stock === 0) {
264
+ entity.addValidationIssue(
265
+ "stock",
266
+ "Premium products must have stock available"
267
+ );
268
+ // Or use throwValidationError() for fail-fast
269
+ }
270
+ },
271
+ };
272
+ }
273
+ ```
274
+
275
+ With `throwOnError: false`, use `entity.addValidationIssue(path, message)` in `rules` and read `entity.validationErrors?.getFormattedErrors()` for `{ path, message }[]` field errors. By default (`persistInvalidMutations: true`), updates apply even when invalid (dirty / form mode). Set `persistInvalidMutations: false` to freeze the entity and revert failed updates while errors exist.
276
+
277
+ ### Optional Input Properties
278
+
279
+ Make properties optional at construction but required in the entity:
280
+
281
+ ```typescript
282
+ const userSchema = z.object({
283
+ id: z.custom<Id>(),
284
+ email: z.string().email(),
285
+ password: z.string().min(8), // Required in entity
286
+ createdAt: z.date(), // Required in entity
287
+ });
288
+
289
+ type UserProps = z.infer<typeof userSchema>;
290
+
291
+ // Second generic makes 'password' and 'createdAt' optional at input
292
+ class User extends Aggregate<UserProps, "password" | "createdAt"> {
293
+ protected static validation = { schema: userSchema };
294
+
295
+ protected static hooks = {
296
+ onBeforeCreate: (props) => {
297
+ // Generate values before validation
298
+ if (!props.password) {
299
+ props.password = generateEncryptedPassword();
300
+ }
301
+ if (!props.createdAt) {
302
+ props.createdAt = new Date();
303
+ }
304
+ },
305
+ };
306
+
307
+ get email() {
308
+ return this.props.email;
309
+ }
310
+ }
311
+
312
+ // ✅ Works without password and createdAt
313
+ const user = new User({
314
+ email: "user@example.com",
315
+ });
316
+
317
+ // ✅ Also works with explicit values
318
+ const customUser = new User({
319
+ email: "user@example.com",
320
+ password: "custom-pass-12345678",
321
+ });
322
+ ```
323
+
324
+ ### Domain Events
325
+
326
+ Communicate across aggregate boundaries:
327
+
328
+ ```typescript
329
+ import { DomainEvent } from "@woltz/rich-domain";
330
+
331
+ class OrderConfirmedEvent extends DomainEvent {
332
+ constructor(
333
+ aggregateId: Id,
334
+ public readonly customerId: string,
335
+ public readonly total: number
336
+ ) {
337
+ super(aggregateId);
338
+ }
339
+
340
+ protected getPayload() {
341
+ return { customerId: this.customerId, total: this.total };
342
+ }
343
+ }
344
+
345
+ class Order extends Aggregate<OrderProps> {
346
+ confirm(): void {
347
+ if (this.props.items.length === 0) {
348
+ throw new DomainError("Cannot confirm empty order");
349
+ }
350
+
351
+ this.props.status = "confirmed";
352
+
353
+ // Emit event
354
+ this.addDomainEvent(
355
+ new OrderConfirmedEvent(this.id, this.customerId, this.total)
356
+ );
357
+ }
358
+ }
359
+
360
+ // After saving
361
+ await orderRepository.save(order);
362
+ await order.dispatchAll(eventBus);
363
+ order.clearEvents();
364
+ ```
365
+
366
+ ### Value Objects
367
+
368
+ Immutable wrappers for primitive values with domain behavior:
369
+
370
+ ```typescript
371
+ import { ValueObject, VOHooks, throwValidationError } from "@woltz/rich-domain";
372
+
373
+ class Price extends ValueObject<number> {
374
+ protected static validation = {
375
+ schema: z.number().positive("Price must be positive"),
376
+ };
377
+
378
+ protected static hooks: VOHooks<number, Price> = {
379
+ rules: (price) => {
380
+ if (price.value > 1000000) {
381
+ throwValidationError("value", "Price cannot exceed 1,000,000");
382
+ }
383
+ },
384
+ };
385
+
386
+ addTax(taxRate: number): Price {
387
+ return this.clone(this.value * (1 + taxRate));
388
+ }
389
+
390
+ discount(percentage: number): Price {
391
+ return this.clone(this.value * (1 - percentage / 100));
392
+ }
393
+
394
+ format(currency: string = "USD"): string {
395
+ return new Intl.NumberFormat("en-US", {
396
+ style: "currency",
397
+ currency,
398
+ }).format(this.value);
399
+ }
400
+ }
401
+
402
+ const price = new Price(99.99);
403
+ const withTax = price.addTax(0.08);
404
+ const discounted = price.discount(10);
405
+
406
+ console.log(price.format()); // "$99.99"
407
+ console.log(withTax.format()); // "$107.99"
408
+ console.log(discounted.format()); // "$89.99"
409
+ ```
410
+
411
+ ## Integration with ORMs
412
+
413
+ Rich Domain provides official adapters for popular ORMs:
414
+
415
+ ### Prisma
416
+
417
+ ```bash
418
+ npm install @woltz/rich-domain-prisma
419
+ ```
420
+
421
+ ```typescript
422
+ import {
423
+ PrismaRepository,
424
+ PrismaToPersistence,
425
+ } from "@woltz/rich-domain-prisma";
426
+
427
+ class UserToPersistence extends PrismaToPersistence<User> {
428
+ protected readonly registry = schemaRegistry;
429
+
430
+ protected async onCreate(user: User): Promise<void> {
431
+ await this.context.user.create({
432
+ data: {
433
+ id: user.id.value,
434
+ email: user.email,
435
+ name: user.name,
436
+ },
437
+ });
438
+ }
439
+
440
+ protected async onUpdate(
441
+ changes: AggregateChanges,
442
+ user: User
443
+ ): Promise<void> {
444
+ // Automatic batch operations handling
445
+ }
446
+ }
447
+ ```
448
+
449
+ ### TypeORM
450
+
451
+ ```bash
452
+ npm install @woltz/rich-domain-typeorm
453
+ ```
454
+
455
+ ```typescript
456
+ import { TypeORMRepository } from "@woltz/rich-domain-typeorm";
457
+
458
+ class UserRepository extends TypeORMRepository<User, UserEntity> {
459
+ // Automatic change tracking and batch operations
460
+ }
461
+ ```
462
+
463
+ ## CLI Tool
464
+
465
+ Bootstrap projects and generate domain code:
466
+
467
+ ```bash
468
+ npm install -g @woltz/rich-domain-cli
469
+
470
+ # Initialize new project
471
+ rich-domain init my-app --template fullstack
472
+
473
+ # Generate domain from Prisma schema
474
+ rich-domain generate --schema prisma/schema.prisma
475
+
476
+ # Add entity manually
477
+ rich-domain add User name:string email:string --with-repo
478
+ ```
479
+
480
+ ## API Reference
481
+
482
+ ### Core Classes
483
+
484
+ #### `Id`
485
+
486
+ Unique identifier for entities:
487
+
488
+ ```typescript
489
+ const id = Id.create(); // Generate new UUID
490
+ const existingId = Id.from("uuid-string"); // From existing value
491
+
492
+ console.log(id.value); // string
493
+ console.log(id.isNew()); // boolean
494
+ console.log(id.equals(otherId)); // boolean
495
+ ```
496
+
497
+ #### `Entity<T>`
498
+
499
+ Base class for entities:
500
+
501
+ ```typescript
502
+ abstract class Entity<T extends { id: Id }> {
503
+ get id(): Id;
504
+ isNew(): boolean;
505
+ equals(other: Entity<T>): boolean;
506
+ toJSON(): object;
507
+ }
508
+ ```
509
+
510
+ #### `Aggregate<T>`
511
+
512
+ Root entity with change tracking:
513
+
514
+ ```typescript
515
+ abstract class Aggregate<T extends { id: Id }> extends Entity<T> {
516
+ getChanges(): AggregateChanges;
517
+
518
+ // Domain Events
519
+ protected addDomainEvent(event: IDomainEvent): void;
520
+ getUncommittedEvents(): IDomainEvent[];
521
+ clearEvents(): void;
522
+ dispatchAll(bus: IDomainEventBus): Promise<void>;
523
+ }
524
+ ```
525
+
526
+ #### `ValueObject<T>`
527
+
528
+ Immutable object compared by value:
529
+
530
+ ```typescript
531
+ abstract class ValueObject<T> {
532
+ readonly value: T;
533
+
534
+ equals(other: ValueObject<T>): boolean;
535
+ toJSON(): T;
536
+ protected clone(value: T): this;
537
+ }
538
+ ```
539
+
540
+ ### Criteria API
541
+
542
+ ```typescript
543
+ class Criteria<T> {
544
+ static create<T>(): Criteria<T>;
545
+
546
+ // Filters
547
+ where<K extends FieldPath<T>>(
548
+ field: K,
549
+ operator: FilterOperator,
550
+ value: any
551
+ ): this;
552
+
553
+ // Ordering
554
+ orderBy<K extends FieldPath<T>>(field: K, direction: "asc" | "desc"): this;
555
+
556
+ // Pagination
557
+ limit(limit: number): this;
558
+ offset(offset: number): this;
559
+ paginate(page: number, pageSize: number): this;
560
+
561
+ // Search
562
+ search(term: string): this;
563
+
564
+ // Getters
565
+ getFilters(): Filter<T>[];
566
+ getOrdering(): Order<T> | null;
567
+ getPagination(): Pagination | null;
568
+ getSearch(): string | null;
569
+ }
570
+ ```
571
+
572
+ ### Exception Handling
573
+
574
+ Rich Domain provides comprehensive exception types:
575
+
576
+ ```typescript
577
+ import {
578
+ ValidationError,
579
+ DomainError,
580
+ EntityNotFoundError,
581
+ DuplicateEntityError,
582
+ ConcurrencyError,
583
+ RepositoryError,
584
+ } from "@woltz/rich-domain";
585
+
586
+ try {
587
+ const user = new User({
588
+ /* invalid props */
589
+ });
590
+ } catch (error) {
591
+ if (error instanceof ValidationError) {
592
+ console.log(error.entity); // "User"
593
+ console.log(error.field); // "email"
594
+ console.log(error.message); // "Invalid email format"
595
+ }
596
+ }
597
+ ```
598
+
599
+ ## Package Format
600
+
601
+ This library is published as a **dual package** supporting both CommonJS and ES Modules:
602
+
603
+ ```javascript
604
+ // CommonJS
605
+ const { Id, Entity, Aggregate } = require("@woltz/rich-domain");
606
+
607
+ // ES Modules
608
+ import { Id, Entity, Aggregate } from "@woltz/rich-domain";
609
+ ```
610
+
611
+ Benefits:
612
+
613
+ - ✅ Universal compatibility (Node.js, Vite, Webpack, etc.)
614
+ - ✅ Tree-shaking support for modern bundlers
615
+ - ✅ Full TypeScript support with type definitions
616
+ - ✅ Zero configuration - automatically uses the correct format
617
+
618
+ ## Documentation
619
+
620
+ - 📚 [Full Documentation](https://woltz.mintlify.app)
621
+ - 🚀 [Quick Start Guide](https://woltz.mintlify.app/quickstart)
622
+ - 📖 [Core Concepts](https://woltz.mintlify.app/core/entities-and-aggregates)
623
+ - 🔌 [Integrations](https://woltz.mintlify.app/integrations/prisma)
624
+ - ⚛️ [React Components](https://woltz.mintlify.app/integrations/react)
625
+
626
+ ## Examples
627
+
628
+ Check out the [examples directory](./examples) for complete implementations:
629
+
630
+ - Basic CRUD operations
631
+ - Complex aggregate relationships
632
+ - Custom validation rules
633
+ - Domain events
634
+ - Repository implementations for different ORMs
635
+
636
+ ## Ecosystem
637
+
638
+ | Package | Description | Version |
639
+ | ------------------------------------------------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
640
+ | [@woltz/rich-domain](https://www.npmjs.com/package/@woltz/rich-domain) | Core library | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain.svg)](https://www.npmjs.com/package/@woltz/rich-domain) |
641
+ | [@woltz/rich-domain-prisma](https://www.npmjs.com/package/@woltz/rich-domain-prisma) | Prisma adapter | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain-prisma.svg)](https://www.npmjs.com/package/@woltz/rich-domain-prisma) |
642
+ | [@woltz/rich-domain-typeorm](https://www.npmjs.com/package/@woltz/rich-domain-typeorm) | TypeORM adapter | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain-typeorm.svg)](https://www.npmjs.com/package/@woltz/rich-domain-typeorm) |
643
+ | [@woltz/rich-domain-criteria-zod](https://www.npmjs.com/package/@woltz/rich-domain-criteria-zod) | Zod criteria builder | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain-criteria-zod.svg)](https://www.npmjs.com/package/@woltz/rich-domain-criteria-zod) |
644
+ | [@woltz/rich-domain-cli](https://www.npmjs.com/package/@woltz/rich-domain-cli) | CLI tool | [![npm](https://img.shields.io/npm/v/@woltz/rich-domain-cli.svg)](https://www.npmjs.com/package/@woltz/rich-domain-cli) |
645
+
646
+ ## Contributing
647
+
648
+ Contributions are welcome! Please read our [Contributing Guide](./CONTRIBUTING.md) for details.
649
+
650
+ ## License
651
+
652
+ MIT © [Tarcisio Andrade](https://github.com/tarcisioandrade)
653
+
654
+ ## Links
655
+
656
+ - [Documentation](https://woltz.mintlify.app)
657
+ - [GitHub Repository](https://github.com/tarcisioandrade/rich-domain)
658
+ - [npm Package](https://www.npmjs.com/package/@woltz/rich-domain)
659
+ - [Issues](https://github.com/tarcisioandrade/rich-domain/issues)
660
+ - [Changelog](./CHANGELOG.md)