@woltz/rich-domain 1.8.3 → 1.8.4

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.
Files changed (48) hide show
  1. package/README.md +659 -652
  2. package/dist/cjs/core/base-entity.d.ts.map +1 -1
  3. package/dist/cjs/core/base-entity.js.map +1 -1
  4. package/dist/cjs/core/change-tracker.d.ts.map +1 -1
  5. package/dist/cjs/core/change-tracker.js +6 -2
  6. package/dist/cjs/core/change-tracker.js.map +1 -1
  7. package/dist/cjs/core/value-object.d.ts.map +1 -1
  8. package/dist/cjs/core/value-object.js.map +1 -1
  9. package/dist/cjs/repository/mapper.d.ts +2 -1
  10. package/dist/cjs/repository/mapper.d.ts.map +1 -1
  11. package/dist/cjs/repository/mapper.js +3 -0
  12. package/dist/cjs/repository/mapper.js.map +1 -1
  13. package/dist/cjs/types/change-tracker.d.ts.map +1 -1
  14. package/dist/cjs/types/criteria.d.ts.map +1 -1
  15. package/dist/cjs/types/utils.d.ts.map +1 -1
  16. package/dist/cjs/validation-error.d.ts.map +1 -1
  17. package/dist/cjs/validation-error.js +24 -19
  18. package/dist/cjs/validation-error.js.map +1 -1
  19. package/dist/esm/core/base-entity.d.ts.map +1 -1
  20. package/dist/esm/core/base-entity.js.map +1 -1
  21. package/dist/esm/core/change-tracker.d.ts.map +1 -1
  22. package/dist/esm/core/change-tracker.js +6 -2
  23. package/dist/esm/core/change-tracker.js.map +1 -1
  24. package/dist/esm/core/value-object.d.ts.map +1 -1
  25. package/dist/esm/core/value-object.js.map +1 -1
  26. package/dist/esm/repository/mapper.d.ts +2 -1
  27. package/dist/esm/repository/mapper.d.ts.map +1 -1
  28. package/dist/esm/repository/mapper.js +3 -0
  29. package/dist/esm/repository/mapper.js.map +1 -1
  30. package/dist/esm/types/change-tracker.d.ts.map +1 -1
  31. package/dist/esm/types/criteria.d.ts.map +1 -1
  32. package/dist/esm/types/utils.d.ts.map +1 -1
  33. package/dist/esm/validation-error.d.ts.map +1 -1
  34. package/dist/esm/validation-error.js +24 -19
  35. package/dist/esm/validation-error.js.map +1 -1
  36. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  37. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  38. package/dist/tsconfig.types.tsbuildinfo +1 -1
  39. package/dist/types/core/base-entity.d.ts.map +1 -1
  40. package/dist/types/core/change-tracker.d.ts.map +1 -1
  41. package/dist/types/core/value-object.d.ts.map +1 -1
  42. package/dist/types/repository/mapper.d.ts +2 -1
  43. package/dist/types/repository/mapper.d.ts.map +1 -1
  44. package/dist/types/types/change-tracker.d.ts.map +1 -1
  45. package/dist/types/types/criteria.d.ts.map +1 -1
  46. package/dist/types/types/utils.d.ts.map +1 -1
  47. package/dist/types/validation-error.d.ts.map +1 -1
  48. package/package.json +70 -70
package/README.md CHANGED
@@ -1,652 +1,659 @@
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>()
223
- .where("email", "equals", email);
224
-
225
- const result = await this.find(criteria);
226
- return result.data[0] ?? null;
227
- }
228
- }
229
- ```
230
-
231
- ## Advanced Features
232
-
233
- ### Lifecycle Hooks
234
-
235
- Add validation and side effects at key points:
236
-
237
- ```typescript
238
- class Product extends Aggregate<ProductProps> {
239
- protected static validation = {
240
- schema: ProductSchema,
241
- };
242
-
243
- protected static hooks = {
244
- onBeforeCreate: (props) => {
245
- // Set default values before validation
246
- if (!props.createdAt) {
247
- props.createdAt = new Date();
248
- }
249
- },
250
-
251
- onCreate: (entity) => {
252
- console.log(`Product created: ${entity.name}`);
253
- },
254
-
255
- onBeforeUpdate: (entity, snapshot) => {
256
- // Prevent price changes on inactive products
257
- if (snapshot.status === "inactive" && entity.price !== snapshot.price) {
258
- return false; // Reject the change
259
- }
260
- return true;
261
- },
262
-
263
- rules: (entity) => {
264
- if (entity.price > 1000 && entity.stock === 0) {
265
- throw new ValidationError([{
266
- path: ["stock"],
267
- message: "Premium products must have stock available"
268
- }]);
269
- }
270
- },
271
- };
272
- }
273
- ```
274
-
275
- ### Optional Input Properties
276
-
277
- Make properties optional at construction but required in the entity:
278
-
279
- ```typescript
280
- const userSchema = z.object({
281
- id: z.custom<Id>(),
282
- email: z.string().email(),
283
- password: z.string().min(8), // Required in entity
284
- createdAt: z.date(), // Required in entity
285
- });
286
-
287
- type UserProps = z.infer<typeof userSchema>;
288
-
289
- // Second generic makes 'password' and 'createdAt' optional at input
290
- class User extends Aggregate<UserProps, "password" | "createdAt"> {
291
- protected static validation = { schema: userSchema };
292
-
293
- protected static hooks = {
294
- onBeforeCreate: (props) => {
295
- // Generate values before validation
296
- if (!props.password) {
297
- props.password = generateEncryptedPassword();
298
- }
299
- if (!props.createdAt) {
300
- props.createdAt = new Date();
301
- }
302
- },
303
- };
304
-
305
- get email() {
306
- return this.props.email;
307
- }
308
- }
309
-
310
- // ✅ Works without password and createdAt
311
- const user = new User({
312
- email: "user@example.com",
313
- });
314
-
315
- // ✅ Also works with explicit values
316
- const customUser = new User({
317
- email: "user@example.com",
318
- password: "custom-pass-12345678",
319
- });
320
- ```
321
-
322
- ### Domain Events
323
-
324
- Communicate across aggregate boundaries:
325
-
326
- ```typescript
327
- import { DomainEvent } from "@woltz/rich-domain";
328
-
329
- class OrderConfirmedEvent extends DomainEvent {
330
- constructor(
331
- aggregateId: Id,
332
- public readonly customerId: string,
333
- public readonly total: number
334
- ) {
335
- super(aggregateId);
336
- }
337
-
338
- protected getPayload() {
339
- return { customerId: this.customerId, total: this.total };
340
- }
341
- }
342
-
343
- class Order extends Aggregate<OrderProps> {
344
- confirm(): void {
345
- if (this.props.items.length === 0) {
346
- throw new DomainError("Cannot confirm empty order");
347
- }
348
-
349
- this.props.status = "confirmed";
350
-
351
- // Emit event
352
- this.addDomainEvent(
353
- new OrderConfirmedEvent(this.id, this.customerId, this.total)
354
- );
355
- }
356
- }
357
-
358
- // After saving
359
- await orderRepository.save(order);
360
- await order.dispatchAll(eventBus);
361
- order.clearEvents();
362
- ```
363
-
364
- ### Value Objects
365
-
366
- Immutable wrappers for primitive values with domain behavior:
367
-
368
- ```typescript
369
- import { ValueObject, VOHooks, throwValidationError } from "@woltz/rich-domain";
370
-
371
- class Price extends ValueObject<number> {
372
- protected static validation = {
373
- schema: z.number().positive("Price must be positive"),
374
- };
375
-
376
- protected static hooks: VOHooks<number, Price> = {
377
- rules: (price) => {
378
- if (price.value > 1000000) {
379
- throwValidationError("value", "Price cannot exceed 1,000,000");
380
- }
381
- },
382
- };
383
-
384
- addTax(taxRate: number): Price {
385
- return this.clone(this.value * (1 + taxRate));
386
- }
387
-
388
- discount(percentage: number): Price {
389
- return this.clone(this.value * (1 - percentage / 100));
390
- }
391
-
392
- format(currency: string = "USD"): string {
393
- return new Intl.NumberFormat("en-US", {
394
- style: "currency",
395
- currency,
396
- }).format(this.value);
397
- }
398
- }
399
-
400
- const price = new Price(99.99);
401
- const withTax = price.addTax(0.08);
402
- const discounted = price.discount(10);
403
-
404
- console.log(price.format()); // "$99.99"
405
- console.log(withTax.format()); // "$107.99"
406
- console.log(discounted.format()); // "$89.99"
407
- ```
408
-
409
- ## Integration with ORMs
410
-
411
- Rich Domain provides official adapters for popular ORMs:
412
-
413
- ### Prisma
414
-
415
- ```bash
416
- npm install @woltz/rich-domain-prisma
417
- ```
418
-
419
- ```typescript
420
- import { PrismaRepository, PrismaToPersistence } from "@woltz/rich-domain-prisma";
421
-
422
- class UserToPersistence extends PrismaToPersistence<User> {
423
- protected readonly registry = schemaRegistry;
424
-
425
- protected async onCreate(user: User): Promise<void> {
426
- await this.context.user.create({
427
- data: {
428
- id: user.id.value,
429
- email: user.email,
430
- name: user.name,
431
- },
432
- });
433
- }
434
-
435
- protected async onUpdate(user: User, changes: AggregateChanges): Promise<void> {
436
- // Automatic batch operations handling
437
- }
438
- }
439
- ```
440
-
441
- ### TypeORM
442
-
443
- ```bash
444
- npm install @woltz/rich-domain-typeorm
445
- ```
446
-
447
- ```typescript
448
- import { TypeORMRepository } from "@woltz/rich-domain-typeorm";
449
-
450
- class UserRepository extends TypeORMRepository<User, UserEntity> {
451
- // Automatic change tracking and batch operations
452
- }
453
- ```
454
-
455
- ## CLI Tool
456
-
457
- Bootstrap projects and generate domain code:
458
-
459
- ```bash
460
- npm install -g @woltz/rich-domain-cli
461
-
462
- # Initialize new project
463
- rich-domain init my-app --template fullstack
464
-
465
- # Generate domain from Prisma schema
466
- rich-domain generate --schema prisma/schema.prisma
467
-
468
- # Add entity manually
469
- rich-domain add User name:string email:string --with-repo
470
- ```
471
-
472
- ## API Reference
473
-
474
- ### Core Classes
475
-
476
- #### `Id`
477
-
478
- Unique identifier for entities:
479
-
480
- ```typescript
481
- const id = Id.create(); // Generate new UUID
482
- const existingId = Id.from("uuid-string"); // From existing value
483
-
484
- console.log(id.value); // string
485
- console.log(id.isNew); // boolean
486
- console.log(id.equals(otherId)); // boolean
487
- ```
488
-
489
- #### `Entity<T>`
490
-
491
- Base class for entities:
492
-
493
- ```typescript
494
- abstract class Entity<T extends { id: Id }> {
495
- get id(): Id;
496
- get isNew(): boolean;
497
- equals(other: Entity<T>): boolean;
498
- toJSON(): object;
499
- }
500
- ```
501
-
502
- #### `Aggregate<T>`
503
-
504
- Root entity with change tracking:
505
-
506
- ```typescript
507
- abstract class Aggregate<T extends { id: Id }> extends Entity<T> {
508
- getChanges(): AggregateChanges;
509
-
510
- // Domain Events
511
- protected addDomainEvent(event: IDomainEvent): void;
512
- getUncommittedEvents(): IDomainEvent[];
513
- clearEvents(): void;
514
- dispatchAll(bus: DomainEventBus): Promise<void>;
515
- }
516
- ```
517
-
518
- #### `ValueObject<T>`
519
-
520
- Immutable object compared by value:
521
-
522
- ```typescript
523
- abstract class ValueObject<T> {
524
- protected readonly props: T;
525
-
526
- equals(other: ValueObject<T>): boolean;
527
- toJSON(): T;
528
- protected clone(updates: Partial<T>): this;
529
- }
530
- ```
531
-
532
- ### Criteria API
533
-
534
- ```typescript
535
- class Criteria<T> {
536
- static create<T>(): Criteria<T>;
537
-
538
- // Filters
539
- where<K extends FieldPath<T>>(
540
- field: K,
541
- operator: FilterOperator,
542
- value: any
543
- ): this;
544
-
545
- // Ordering
546
- orderBy<K extends FieldPath<T>>(
547
- field: K,
548
- direction: "asc" | "desc"
549
- ): this;
550
-
551
- // Pagination
552
- limit(limit: number): this;
553
- offset(offset: number): this;
554
- paginate(page: number, pageSize: number): this;
555
-
556
- // Search
557
- search(term: string): this;
558
-
559
- // Getters
560
- getFilters(): Filter<T>[];
561
- getOrdering(): Order<T> | null;
562
- getPagination(): Pagination | null;
563
- getSearch(): string | null;
564
- }
565
- ```
566
-
567
- ### Exception Handling
568
-
569
- Rich Domain provides comprehensive exception types:
570
-
571
- ```typescript
572
- import {
573
- ValidationError,
574
- DomainError,
575
- EntityNotFoundError,
576
- DuplicateEntityError,
577
- ConcurrencyError,
578
- RepositoryError
579
- } from "@woltz/rich-domain";
580
-
581
- try {
582
- const user = new User({ /* invalid props */ });
583
- } catch (error) {
584
- if (error instanceof ValidationError) {
585
- console.log(error.entity); // "User"
586
- console.log(error.field); // "email"
587
- console.log(error.message); // "Invalid email format"
588
- }
589
- }
590
- ```
591
-
592
- ## Package Format
593
-
594
- This library is published as a **dual package** supporting both CommonJS and ES Modules:
595
-
596
- ```javascript
597
- // CommonJS
598
- const { Id, Entity, Aggregate } = require('@woltz/rich-domain');
599
-
600
- // ES Modules
601
- import { Id, Entity, Aggregate } from '@woltz/rich-domain';
602
- ```
603
-
604
- Benefits:
605
- - ✅ Universal compatibility (Node.js, Vite, Webpack, etc.)
606
- - Tree-shaking support for modern bundlers
607
- - Full TypeScript support with type definitions
608
- - ✅ Zero configuration - automatically uses the correct format
609
-
610
- ## Documentation
611
-
612
- - 📚 [Full Documentation](https://woltz.mintlify.app)
613
- - 🚀 [Quick Start Guide](https://woltz.mintlify.app/quickstart)
614
- - 📖 [Core Concepts](https://woltz.mintlify.app/core/entities-and-aggregates)
615
- - 🔌 [Integrations](https://woltz.mintlify.app/integrations/prisma)
616
- - ⚛️ [React Components](https://woltz.mintlify.app/integrations/react)
617
-
618
- ## Examples
619
-
620
- Check out the [examples directory](./examples) for complete implementations:
621
-
622
- - Basic CRUD operations
623
- - Complex aggregate relationships
624
- - Custom validation rules
625
- - Domain events
626
- - Repository implementations for different ORMs
627
-
628
- ## Ecosystem
629
-
630
- | Package | Description | Version |
631
- |---------|-------------|---------|
632
- | [@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) |
633
- | [@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) |
634
- | [@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) |
635
- | [@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) |
636
- | [@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) |
637
-
638
- ## Contributing
639
-
640
- Contributions are welcome! Please read our [Contributing Guide](./CONTRIBUTING.md) for details.
641
-
642
- ## License
643
-
644
- MIT © [Tarcisio Andrade](https://github.com/tarcisioandrade)
645
-
646
- ## Links
647
-
648
- - [Documentation](https://woltz.mintlify.app)
649
- - [GitHub Repository](https://github.com/tarcisioandrade/rich-domain)
650
- - [npm Package](https://www.npmjs.com/package/@woltz/rich-domain)
651
- - [Issues](https://github.com/tarcisioandrade/rich-domain/issues)
652
- - [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
+ throw new ValidationError([
265
+ {
266
+ path: ["stock"],
267
+ message: "Premium products must have stock available",
268
+ },
269
+ ]);
270
+ }
271
+ },
272
+ };
273
+ }
274
+ ```
275
+
276
+ ### Optional Input Properties
277
+
278
+ Make properties optional at construction but required in the entity:
279
+
280
+ ```typescript
281
+ const userSchema = z.object({
282
+ id: z.custom<Id>(),
283
+ email: z.string().email(),
284
+ password: z.string().min(8), // Required in entity
285
+ createdAt: z.date(), // Required in entity
286
+ });
287
+
288
+ type UserProps = z.infer<typeof userSchema>;
289
+
290
+ // Second generic makes 'password' and 'createdAt' optional at input
291
+ class User extends Aggregate<UserProps, "password" | "createdAt"> {
292
+ protected static validation = { schema: userSchema };
293
+
294
+ protected static hooks = {
295
+ onBeforeCreate: (props) => {
296
+ // Generate values before validation
297
+ if (!props.password) {
298
+ props.password = generateEncryptedPassword();
299
+ }
300
+ if (!props.createdAt) {
301
+ props.createdAt = new Date();
302
+ }
303
+ },
304
+ };
305
+
306
+ get email() {
307
+ return this.props.email;
308
+ }
309
+ }
310
+
311
+ // Works without password and createdAt
312
+ const user = new User({
313
+ email: "user@example.com",
314
+ });
315
+
316
+ // Also works with explicit values
317
+ const customUser = new User({
318
+ email: "user@example.com",
319
+ password: "custom-pass-12345678",
320
+ });
321
+ ```
322
+
323
+ ### Domain Events
324
+
325
+ Communicate across aggregate boundaries:
326
+
327
+ ```typescript
328
+ import { DomainEvent } from "@woltz/rich-domain";
329
+
330
+ class OrderConfirmedEvent extends DomainEvent {
331
+ constructor(
332
+ aggregateId: Id,
333
+ public readonly customerId: string,
334
+ public readonly total: number
335
+ ) {
336
+ super(aggregateId);
337
+ }
338
+
339
+ protected getPayload() {
340
+ return { customerId: this.customerId, total: this.total };
341
+ }
342
+ }
343
+
344
+ class Order extends Aggregate<OrderProps> {
345
+ confirm(): void {
346
+ if (this.props.items.length === 0) {
347
+ throw new DomainError("Cannot confirm empty order");
348
+ }
349
+
350
+ this.props.status = "confirmed";
351
+
352
+ // Emit event
353
+ this.addDomainEvent(
354
+ new OrderConfirmedEvent(this.id, this.customerId, this.total)
355
+ );
356
+ }
357
+ }
358
+
359
+ // After saving
360
+ await orderRepository.save(order);
361
+ await order.dispatchAll(eventBus);
362
+ order.clearEvents();
363
+ ```
364
+
365
+ ### Value Objects
366
+
367
+ Immutable wrappers for primitive values with domain behavior:
368
+
369
+ ```typescript
370
+ import { ValueObject, VOHooks, throwValidationError } from "@woltz/rich-domain";
371
+
372
+ class Price extends ValueObject<number> {
373
+ protected static validation = {
374
+ schema: z.number().positive("Price must be positive"),
375
+ };
376
+
377
+ protected static hooks: VOHooks<number, Price> = {
378
+ rules: (price) => {
379
+ if (price.value > 1000000) {
380
+ throwValidationError("value", "Price cannot exceed 1,000,000");
381
+ }
382
+ },
383
+ };
384
+
385
+ addTax(taxRate: number): Price {
386
+ return this.clone(this.value * (1 + taxRate));
387
+ }
388
+
389
+ discount(percentage: number): Price {
390
+ return this.clone(this.value * (1 - percentage / 100));
391
+ }
392
+
393
+ format(currency: string = "USD"): string {
394
+ return new Intl.NumberFormat("en-US", {
395
+ style: "currency",
396
+ currency,
397
+ }).format(this.value);
398
+ }
399
+ }
400
+
401
+ const price = new Price(99.99);
402
+ const withTax = price.addTax(0.08);
403
+ const discounted = price.discount(10);
404
+
405
+ console.log(price.format()); // "$99.99"
406
+ console.log(withTax.format()); // "$107.99"
407
+ console.log(discounted.format()); // "$89.99"
408
+ ```
409
+
410
+ ## Integration with ORMs
411
+
412
+ Rich Domain provides official adapters for popular ORMs:
413
+
414
+ ### Prisma
415
+
416
+ ```bash
417
+ npm install @woltz/rich-domain-prisma
418
+ ```
419
+
420
+ ```typescript
421
+ import {
422
+ PrismaRepository,
423
+ PrismaToPersistence,
424
+ } from "@woltz/rich-domain-prisma";
425
+
426
+ class UserToPersistence extends PrismaToPersistence<User> {
427
+ protected readonly registry = schemaRegistry;
428
+
429
+ protected async onCreate(user: User): Promise<void> {
430
+ await this.context.user.create({
431
+ data: {
432
+ id: user.id.value,
433
+ email: user.email,
434
+ name: user.name,
435
+ },
436
+ });
437
+ }
438
+
439
+ protected async onUpdate(
440
+ user: User,
441
+ changes: AggregateChanges
442
+ ): Promise<void> {
443
+ // Automatic batch operations handling
444
+ }
445
+ }
446
+ ```
447
+
448
+ ### TypeORM
449
+
450
+ ```bash
451
+ npm install @woltz/rich-domain-typeorm
452
+ ```
453
+
454
+ ```typescript
455
+ import { TypeORMRepository } from "@woltz/rich-domain-typeorm";
456
+
457
+ class UserRepository extends TypeORMRepository<User, UserEntity> {
458
+ // Automatic change tracking and batch operations
459
+ }
460
+ ```
461
+
462
+ ## CLI Tool
463
+
464
+ Bootstrap projects and generate domain code:
465
+
466
+ ```bash
467
+ npm install -g @woltz/rich-domain-cli
468
+
469
+ # Initialize new project
470
+ rich-domain init my-app --template fullstack
471
+
472
+ # Generate domain from Prisma schema
473
+ rich-domain generate --schema prisma/schema.prisma
474
+
475
+ # Add entity manually
476
+ rich-domain add User name:string email:string --with-repo
477
+ ```
478
+
479
+ ## API Reference
480
+
481
+ ### Core Classes
482
+
483
+ #### `Id`
484
+
485
+ Unique identifier for entities:
486
+
487
+ ```typescript
488
+ const id = Id.create(); // Generate new UUID
489
+ const existingId = Id.from("uuid-string"); // From existing value
490
+
491
+ console.log(id.value); // string
492
+ console.log(id.isNew); // boolean
493
+ console.log(id.equals(otherId)); // boolean
494
+ ```
495
+
496
+ #### `Entity<T>`
497
+
498
+ Base class for entities:
499
+
500
+ ```typescript
501
+ abstract class Entity<T extends { id: Id }> {
502
+ get id(): Id;
503
+ get isNew(): boolean;
504
+ equals(other: Entity<T>): boolean;
505
+ toJSON(): object;
506
+ }
507
+ ```
508
+
509
+ #### `Aggregate<T>`
510
+
511
+ Root entity with change tracking:
512
+
513
+ ```typescript
514
+ abstract class Aggregate<T extends { id: Id }> extends Entity<T> {
515
+ getChanges(): AggregateChanges;
516
+
517
+ // Domain Events
518
+ protected addDomainEvent(event: IDomainEvent): void;
519
+ getUncommittedEvents(): IDomainEvent[];
520
+ clearEvents(): void;
521
+ dispatchAll(bus: DomainEventBus): Promise<void>;
522
+ }
523
+ ```
524
+
525
+ #### `ValueObject<T>`
526
+
527
+ Immutable object compared by value:
528
+
529
+ ```typescript
530
+ abstract class ValueObject<T> {
531
+ protected readonly props: T;
532
+
533
+ equals(other: ValueObject<T>): boolean;
534
+ toJSON(): T;
535
+ protected clone(updates: Partial<T>): this;
536
+ }
537
+ ```
538
+
539
+ ### Criteria API
540
+
541
+ ```typescript
542
+ class Criteria<T> {
543
+ static create<T>(): Criteria<T>;
544
+
545
+ // Filters
546
+ where<K extends FieldPath<T>>(
547
+ field: K,
548
+ operator: FilterOperator,
549
+ value: any
550
+ ): this;
551
+
552
+ // Ordering
553
+ orderBy<K extends FieldPath<T>>(field: K, direction: "asc" | "desc"): this;
554
+
555
+ // Pagination
556
+ limit(limit: number): this;
557
+ offset(offset: number): this;
558
+ paginate(page: number, pageSize: number): this;
559
+
560
+ // Search
561
+ search(term: string): this;
562
+
563
+ // Getters
564
+ getFilters(): Filter<T>[];
565
+ getOrdering(): Order<T> | null;
566
+ getPagination(): Pagination | null;
567
+ getSearch(): string | null;
568
+ }
569
+ ```
570
+
571
+ ### Exception Handling
572
+
573
+ Rich Domain provides comprehensive exception types:
574
+
575
+ ```typescript
576
+ import {
577
+ ValidationError,
578
+ DomainError,
579
+ EntityNotFoundError,
580
+ DuplicateEntityError,
581
+ ConcurrencyError,
582
+ RepositoryError,
583
+ } from "@woltz/rich-domain";
584
+
585
+ try {
586
+ const user = new User({
587
+ /* invalid props */
588
+ });
589
+ } catch (error) {
590
+ if (error instanceof ValidationError) {
591
+ console.log(error.entity); // "User"
592
+ console.log(error.field); // "email"
593
+ console.log(error.message); // "Invalid email format"
594
+ }
595
+ }
596
+ ```
597
+
598
+ ## Package Format
599
+
600
+ This library is published as a **dual package** supporting both CommonJS and ES Modules:
601
+
602
+ ```javascript
603
+ // CommonJS
604
+ const { Id, Entity, Aggregate } = require("@woltz/rich-domain");
605
+
606
+ // ES Modules
607
+ import { Id, Entity, Aggregate } from "@woltz/rich-domain";
608
+ ```
609
+
610
+ Benefits:
611
+
612
+ - Universal compatibility (Node.js, Vite, Webpack, etc.)
613
+ - Tree-shaking support for modern bundlers
614
+ - Full TypeScript support with type definitions
615
+ - Zero configuration - automatically uses the correct format
616
+
617
+ ## Documentation
618
+
619
+ - 📚 [Full Documentation](https://woltz.mintlify.app)
620
+ - 🚀 [Quick Start Guide](https://woltz.mintlify.app/quickstart)
621
+ - 📖 [Core Concepts](https://woltz.mintlify.app/core/entities-and-aggregates)
622
+ - 🔌 [Integrations](https://woltz.mintlify.app/integrations/prisma)
623
+ - ⚛️ [React Components](https://woltz.mintlify.app/integrations/react)
624
+
625
+ ## Examples
626
+
627
+ Check out the [examples directory](./examples) for complete implementations:
628
+
629
+ - Basic CRUD operations
630
+ - Complex aggregate relationships
631
+ - Custom validation rules
632
+ - Domain events
633
+ - Repository implementations for different ORMs
634
+
635
+ ## Ecosystem
636
+
637
+ | Package | Description | Version |
638
+ | ------------------------------------------------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
639
+ | [@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) |
640
+ | [@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) |
641
+ | [@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) |
642
+ | [@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) |
643
+ | [@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) |
644
+
645
+ ## Contributing
646
+
647
+ Contributions are welcome! Please read our [Contributing Guide](./CONTRIBUTING.md) for details.
648
+
649
+ ## License
650
+
651
+ MIT © [Tarcisio Andrade](https://github.com/tarcisioandrade)
652
+
653
+ ## Links
654
+
655
+ - [Documentation](https://woltz.mintlify.app)
656
+ - [GitHub Repository](https://github.com/tarcisioandrade/rich-domain)
657
+ - [npm Package](https://www.npmjs.com/package/@woltz/rich-domain)
658
+ - [Issues](https://github.com/tarcisioandrade/rich-domain/issues)
659
+ - [Changelog](./CHANGELOG.md)