@woltz/rich-domain 0.2.1

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 (178) hide show
  1. package/.github/workflows/ci.yml +40 -0
  2. package/.husky/commit-msg +1 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.versionrc.json +21 -0
  5. package/.vscode/settings.json +3 -0
  6. package/CHANGELOG.md +81 -0
  7. package/LICENSE +21 -0
  8. package/README.md +712 -0
  9. package/commitlint.config.js +23 -0
  10. package/dist/base-entity.d.ts +67 -0
  11. package/dist/base-entity.d.ts.map +1 -0
  12. package/dist/base-entity.js +309 -0
  13. package/dist/base-entity.js.map +1 -0
  14. package/dist/constants.d.ts +3 -0
  15. package/dist/constants.d.ts.map +1 -0
  16. package/dist/constants.js +6 -0
  17. package/dist/constants.js.map +1 -0
  18. package/dist/criteria.d.ts +60 -0
  19. package/dist/criteria.d.ts.map +1 -0
  20. package/dist/criteria.js +214 -0
  21. package/dist/criteria.js.map +1 -0
  22. package/dist/deep-proxy.d.ts +34 -0
  23. package/dist/deep-proxy.d.ts.map +1 -0
  24. package/dist/deep-proxy.js +297 -0
  25. package/dist/deep-proxy.js.map +1 -0
  26. package/dist/domain-event-bus.d.ts +57 -0
  27. package/dist/domain-event-bus.d.ts.map +1 -0
  28. package/dist/domain-event-bus.js +112 -0
  29. package/dist/domain-event-bus.js.map +1 -0
  30. package/dist/domain-event.d.ts +55 -0
  31. package/dist/domain-event.d.ts.map +1 -0
  32. package/dist/domain-event.js +42 -0
  33. package/dist/domain-event.js.map +1 -0
  34. package/dist/entity.d.ts +13 -0
  35. package/dist/entity.d.ts.map +1 -0
  36. package/dist/entity.js +15 -0
  37. package/dist/entity.js.map +1 -0
  38. package/dist/filtering.d.ts +107 -0
  39. package/dist/filtering.d.ts.map +1 -0
  40. package/dist/filtering.js +202 -0
  41. package/dist/filtering.js.map +1 -0
  42. package/dist/id.d.ts +51 -0
  43. package/dist/id.d.ts.map +1 -0
  44. package/dist/id.js +84 -0
  45. package/dist/id.js.map +1 -0
  46. package/dist/index.d.ts +15 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +25 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/ordering.d.ts +93 -0
  51. package/dist/ordering.d.ts.map +1 -0
  52. package/dist/ordering.js +154 -0
  53. package/dist/ordering.js.map +1 -0
  54. package/dist/paginated-result.d.ts +62 -0
  55. package/dist/paginated-result.d.ts.map +1 -0
  56. package/dist/paginated-result.js +201 -0
  57. package/dist/paginated-result.js.map +1 -0
  58. package/dist/pagination.d.ts +218 -0
  59. package/dist/pagination.d.ts.map +1 -0
  60. package/dist/pagination.js +281 -0
  61. package/dist/pagination.js.map +1 -0
  62. package/dist/repository/base-repository.d.ts +77 -0
  63. package/dist/repository/base-repository.d.ts.map +1 -0
  64. package/dist/repository/base-repository.js +80 -0
  65. package/dist/repository/base-repository.js.map +1 -0
  66. package/dist/repository/in-memory-repository.d.ts +46 -0
  67. package/dist/repository/in-memory-repository.d.ts.map +1 -0
  68. package/dist/repository/in-memory-repository.js +85 -0
  69. package/dist/repository/in-memory-repository.js.map +1 -0
  70. package/dist/repository/index.d.ts +42 -0
  71. package/dist/repository/index.d.ts.map +1 -0
  72. package/dist/repository/index.js +47 -0
  73. package/dist/repository/index.js.map +1 -0
  74. package/dist/repository/mapper.d.ts +56 -0
  75. package/dist/repository/mapper.d.ts.map +1 -0
  76. package/dist/repository/mapper.js +15 -0
  77. package/dist/repository/mapper.js.map +1 -0
  78. package/dist/repository/types.d.ts +87 -0
  79. package/dist/repository/types.d.ts.map +1 -0
  80. package/dist/repository/types.js +6 -0
  81. package/dist/repository/types.js.map +1 -0
  82. package/dist/repository/unit-of-work.d.ts +70 -0
  83. package/dist/repository/unit-of-work.d.ts.map +1 -0
  84. package/dist/repository/unit-of-work.js +122 -0
  85. package/dist/repository/unit-of-work.js.map +1 -0
  86. package/dist/repository.d.ts +2 -0
  87. package/dist/repository.d.ts.map +1 -0
  88. package/dist/repository.js +21 -0
  89. package/dist/repository.js.map +1 -0
  90. package/dist/specification.d.ts +102 -0
  91. package/dist/specification.d.ts.map +1 -0
  92. package/dist/specification.js +187 -0
  93. package/dist/specification.js.map +1 -0
  94. package/dist/types/criteria.d.ts +35 -0
  95. package/dist/types/criteria.d.ts.map +1 -0
  96. package/dist/types/criteria.js +17 -0
  97. package/dist/types/criteria.js.map +1 -0
  98. package/dist/types/domain.d.ts +30 -0
  99. package/dist/types/domain.d.ts.map +1 -0
  100. package/dist/types/domain.js +2 -0
  101. package/dist/types/domain.js.map +1 -0
  102. package/dist/types/history-tracker.d.ts +36 -0
  103. package/dist/types/history-tracker.d.ts.map +1 -0
  104. package/dist/types/history-tracker.js +2 -0
  105. package/dist/types/history-tracker.js.map +1 -0
  106. package/dist/types/index.d.ts +8 -0
  107. package/dist/types/index.d.ts.map +1 -0
  108. package/dist/types/index.js +8 -0
  109. package/dist/types/index.js.map +1 -0
  110. package/dist/types/repository.d.ts +43 -0
  111. package/dist/types/repository.d.ts.map +1 -0
  112. package/dist/types/repository.js +2 -0
  113. package/dist/types/repository.js.map +1 -0
  114. package/dist/types/standard-schema.d.ts +15 -0
  115. package/dist/types/standard-schema.d.ts.map +1 -0
  116. package/dist/types/standard-schema.js +2 -0
  117. package/dist/types/standard-schema.js.map +1 -0
  118. package/dist/types/unit-of-work.d.ts +39 -0
  119. package/dist/types/unit-of-work.d.ts.map +1 -0
  120. package/dist/types/unit-of-work.js +2 -0
  121. package/dist/types/unit-of-work.js.map +1 -0
  122. package/dist/types/utils.d.ts +14 -0
  123. package/dist/types/utils.d.ts.map +1 -0
  124. package/dist/types/utils.js +2 -0
  125. package/dist/types/utils.js.map +1 -0
  126. package/dist/types.d.ts +88 -0
  127. package/dist/types.d.ts.map +1 -0
  128. package/dist/types.js +12 -0
  129. package/dist/types.js.map +1 -0
  130. package/dist/validation-error.d.ts +42 -0
  131. package/dist/validation-error.d.ts.map +1 -0
  132. package/dist/validation-error.js +73 -0
  133. package/dist/validation-error.js.map +1 -0
  134. package/dist/value-object.d.ts +47 -0
  135. package/dist/value-object.d.ts.map +1 -0
  136. package/dist/value-object.js +136 -0
  137. package/dist/value-object.js.map +1 -0
  138. package/eslint.config.js +51 -0
  139. package/jest.config.js +21 -0
  140. package/package.json +58 -0
  141. package/src/base-entity.ts +401 -0
  142. package/src/constants.ts +7 -0
  143. package/src/criteria.ts +291 -0
  144. package/src/deep-proxy.ts +339 -0
  145. package/src/domain-event-bus.ts +166 -0
  146. package/src/domain-event.ts +90 -0
  147. package/src/entity.ts +16 -0
  148. package/src/id.ts +94 -0
  149. package/src/index.ts +33 -0
  150. package/src/paginated-result.ts +274 -0
  151. package/src/repository/base-repository.ts +152 -0
  152. package/src/repository/in-memory-repository.ts +104 -0
  153. package/src/repository/index.ts +55 -0
  154. package/src/repository/mapper.ts +74 -0
  155. package/src/repository/unit-of-work.ts +148 -0
  156. package/src/types/criteria.ts +79 -0
  157. package/src/types/domain.ts +37 -0
  158. package/src/types/history-tracker.ts +45 -0
  159. package/src/types/index.ts +7 -0
  160. package/src/types/repository.ts +51 -0
  161. package/src/types/standard-schema.ts +19 -0
  162. package/src/types/unit-of-work.ts +46 -0
  163. package/src/types/utils.ts +29 -0
  164. package/src/validation-error.ts +97 -0
  165. package/src/value-object.ts +187 -0
  166. package/tests/criteria.test.ts +432 -0
  167. package/tests/domain-events.test.ts +445 -0
  168. package/tests/entity-equality.test.ts +487 -0
  169. package/tests/entity-validation.test.ts +339 -0
  170. package/tests/entity.test.ts +33 -0
  171. package/tests/history-tracker.spec.ts +667 -0
  172. package/tests/id.test.ts +341 -0
  173. package/tests/repository.test.ts +641 -0
  174. package/tests/to-json.test.ts +91 -0
  175. package/tests/utils.ts +151 -0
  176. package/tests/value-object-validation.test.ts +228 -0
  177. package/tests/value-objects.test.ts +52 -0
  178. package/tsconfig.json +31 -0
package/README.md ADDED
@@ -0,0 +1,712 @@
1
+ # Rich Domain
2
+
3
+ Uma biblioteca TypeScript para Domain-Driven Design (DDD) com suporte a validação via Standard Schema, rastreamento automático de mudanças, sistema de eventos, e repositories enterprise-ready.
4
+
5
+ ## Características
6
+
7
+ - 🏗️ **Entities & Aggregates** - Classes base com identidade e ciclo de vida
8
+ - 💎 **Value Objects** - Objetos imutáveis comparados por valor
9
+ - ✅ **Standard Schema Validation** - Integração com Zod, ArkType, Valibot e outras libs
10
+ - 📜 **Change Tracking** - Histórico automático de todas as mudanças
11
+ - 🔔 **Subscriptions** - Sistema de eventos para observar mudanças
12
+ - 🎯 **Hooks** - Interceptação de criação e atualização de entidades
13
+ - 🆔 **Smart IDs** - Identificadores que sabem se a entidade é nova ou existente
14
+ - 🔍 **Criteria Pattern** - Query builder type-safe com filtros, ordenação e paginação
15
+ - 📦 **Repository Pattern** - Abstrações para persistência com suporte a Prisma, TypeORM, etc.
16
+ - 🔄 **Unit of Work** - Gerenciamento de transações cross-repository
17
+ - 📊 **Paginated Results** - Resultados paginados com serialização profunda
18
+
19
+ ## Instalação
20
+
21
+ ```bash
22
+ npm install rich-domain
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Definindo um Aggregate com Validação
28
+
29
+ ```typescript
30
+ import { z } from "zod";
31
+ import {
32
+ Id,
33
+ Aggregate,
34
+ EntityValidation,
35
+ EntityHooks,
36
+ BaseProps,
37
+ throwValidationError,
38
+ } from "rich-domain";
39
+
40
+ // Define as propriedades
41
+ interface UserProps extends BaseProps {
42
+ name: string;
43
+ email: string;
44
+ age: number;
45
+ status: "active" | "inactive";
46
+ }
47
+
48
+ // Define o schema de validação (Zod, ArkType, Valibot, etc.)
49
+ const userSchema = z.object({
50
+ id: z.custom<Id>((val) => val instanceof Id),
51
+ name: z.string().min(2, "Nome deve ter pelo menos 2 caracteres"),
52
+ email: z.string().email("Email inválido"),
53
+ age: z.number().min(0).max(150),
54
+ status: z.enum(["active", "inactive"]),
55
+ });
56
+
57
+ // Cria o Aggregate
58
+ class User extends Aggregate<UserProps> {
59
+ // Configuração de validação
60
+ protected static validation: EntityValidation<UserProps> = {
61
+ schema: userSchema,
62
+ config: {
63
+ onCreate: true,
64
+ onUpdate: true,
65
+ throwOnError: true,
66
+ },
67
+ };
68
+
69
+ // Hooks de ciclo de vida
70
+ protected static hooks: EntityHooks<UserProps, User> = {
71
+ onCreate: (entity) => {
72
+ console.log(`Usuário criado: ${entity.name}`);
73
+ },
74
+
75
+ onBeforeUpdate: (entity, snapshot) => {
76
+ // Bloquear mudança de email
77
+ if (snapshot.email !== entity.email) {
78
+ return false;
79
+ }
80
+ return true;
81
+ },
82
+
83
+ rules: (entity) => {
84
+ if (entity.name.toLowerCase() === "admin") {
85
+ throwValidationError("name", 'Nome não pode ser "admin"');
86
+ }
87
+ },
88
+ };
89
+
90
+ get name() { return this.props.name; }
91
+ set name(value: string) { this.props.name = value; }
92
+
93
+ get email() { return this.props.email; }
94
+ get age() { return this.props.age; }
95
+ get status() { return this.props.status; }
96
+
97
+ activate() { this.props.status = "active"; }
98
+ deactivate() { this.props.status = "inactive"; }
99
+ }
100
+ ```
101
+
102
+ ### 2. Repository Pattern
103
+
104
+ #### In-Memory (Para Testes)
105
+
106
+ ```typescript
107
+ import { InMemoryRepository, Criteria } from "rich-domain";
108
+
109
+ const userRepo = new InMemoryRepository<User>();
110
+
111
+ // Salvar
112
+ const user = new User({
113
+ name: "João Silva",
114
+ email: "joao@example.com",
115
+ age: 30,
116
+ status: "active",
117
+ });
118
+ await userRepo.save(user);
119
+
120
+ // Buscar por ID
121
+ const found = await userRepo.findById(user.id);
122
+
123
+ // Buscar com Criteria (type-safe!)
124
+ const result = await userRepo.find(
125
+ Criteria.create<User>()
126
+ .whereEquals("status", "active")
127
+ .where("age", "greaterThan", 18)
128
+ .orderByDesc("age")
129
+ .paginate(1, 10)
130
+ );
131
+
132
+ console.log(result.data); // Array de User
133
+ console.log(result.meta); // { page, limit, total, totalPages, hasNext, hasPrevious }
134
+
135
+ // Serializar para API
136
+ const json = result.toJSON(); // Deep serialization de todos os agregados
137
+ res.json(json);
138
+ ```
139
+
140
+ #### Production (Prisma, TypeORM, etc)
141
+
142
+ ```typescript
143
+ import { BaseRepository, BaseMapper } from "rich-domain";
144
+
145
+ // Mapper: Domain ↔ Persistence
146
+ class UserMapper extends BaseMapper<User, PrismaUser> {
147
+ toDomain(persistence: PrismaUser): User {
148
+ return new User({
149
+ id: Id.from(persistence.id),
150
+ name: persistence.name,
151
+ email: persistence.email,
152
+ age: persistence.age,
153
+ status: persistence.status as "active" | "inactive",
154
+ });
155
+ }
156
+
157
+ toPersistence(domain: User): PrismaUser {
158
+ return {
159
+ id: domain.id.value,
160
+ name: domain.name,
161
+ email: domain.email,
162
+ age: domain.age,
163
+ status: domain.status,
164
+ };
165
+ }
166
+ }
167
+
168
+ // Repository
169
+ class UserRepository extends BaseRepository<User, PrismaUser> {
170
+ constructor(private prisma: PrismaClient) {
171
+ super(new UserMapper());
172
+ }
173
+
174
+ protected async insertOne(data: PrismaUser) {
175
+ return this.prisma.user.create({ data });
176
+ }
177
+
178
+ protected async updateOne(id: string, data: PrismaUser) {
179
+ return this.prisma.user.update({ where: { id }, data });
180
+ }
181
+
182
+ protected async deleteOne(id: string) {
183
+ await this.prisma.user.delete({ where: { id } });
184
+ }
185
+
186
+ protected async findOneById(id: string) {
187
+ const persistence = await this.prisma.user.findUnique({ where: { id } });
188
+ return persistence ? this.mapper.toDomain(persistence) : null;
189
+ }
190
+
191
+ protected async findMany() {
192
+ const persistence = await this.prisma.user.findMany();
193
+ return this.mapper.toDomainList!(persistence);
194
+ }
195
+
196
+ protected async applyCriteria(criteria: Criteria<User>) {
197
+ const where = this.buildWhereClause(criteria);
198
+ const orderBy = this.buildOrderBy(criteria);
199
+ const pagination = criteria.getPagination();
200
+
201
+ const [data, total] = await Promise.all([
202
+ this.prisma.user.findMany({
203
+ where,
204
+ orderBy,
205
+ skip: pagination.offset,
206
+ take: pagination.limit,
207
+ }),
208
+ this.prisma.user.count({ where }),
209
+ ]);
210
+
211
+ return [data, total];
212
+ }
213
+
214
+ protected async countByCriteria(criteria?: Criteria<User>) {
215
+ const where = criteria ? this.buildWhereClause(criteria) : {};
216
+ return this.prisma.user.count({ where });
217
+ }
218
+
219
+ protected async existsById(id: string) {
220
+ const count = await this.prisma.user.count({ where: { id } });
221
+ return count > 0;
222
+ }
223
+
224
+ // Helpers para converter Criteria → Prisma
225
+ private buildWhereClause(criteria: Criteria<User>) {
226
+ // Ver src/repository/examples/prisma-repository.example.ts
227
+ }
228
+
229
+ private buildOrderBy(criteria: Criteria<User>) {
230
+ // Ver src/repository/examples/prisma-repository.example.ts
231
+ }
232
+ }
233
+
234
+ // Uso
235
+ const prisma = new PrismaClient();
236
+ const userRepo = new UserRepository(prisma);
237
+
238
+ const result = await userRepo.find(
239
+ Criteria.create<User>()
240
+ .whereEquals("status", "active")
241
+ .orderByDesc("createdAt")
242
+ .paginate(1, 10)
243
+ );
244
+ ```
245
+
246
+ ### 3. Criteria Pattern (Type-Safe Queries)
247
+
248
+ ```typescript
249
+ import { Criteria } from "rich-domain";
250
+
251
+ // Criar criteria
252
+ const criteria = Criteria.create<User>()
253
+ // Filtros
254
+ .whereEquals("status", "active")
255
+ .where("age", "greaterThan", 18)
256
+ .where("age", "lessThan", 65)
257
+ .whereContains("name", "silva")
258
+ .whereIn("status", ["active", "pending"])
259
+ .whereBetween("age", 18, 65)
260
+ .whereNull("deletedAt")
261
+ .whereNotNull("email")
262
+
263
+ // Ordenação
264
+ .orderBy("name", "asc")
265
+ .orderByDesc("createdAt")
266
+
267
+ // Paginação
268
+ .paginate(1, 10)
269
+ .limit(20);
270
+
271
+ // Busca em múltiplos campos
272
+ criteria.search(["name", "email"], "joão");
273
+
274
+ // Serialização
275
+ const json = criteria.toJSON();
276
+
277
+ // Clone
278
+ const cloned = criteria.clone();
279
+
280
+ // From query params (para APIs)
281
+ const criteriaFromUrl = Criteria.fromQueryParams<User>({
282
+ "status:equals": "active",
283
+ "age:greaterThan": "18",
284
+ orderBy: "name:asc,createdAt:desc",
285
+ page: "1",
286
+ limit: "10",
287
+ search: "joão",
288
+ searchFields: "name,email",
289
+ });
290
+ ```
291
+
292
+ ### 4. Unit of Work (Transações)
293
+
294
+ ```typescript
295
+ import { UnitOfWork } from "rich-domain";
296
+
297
+ // Executar múltiplas operações em transação
298
+ await uow.transaction(async (ctx) => {
299
+ const userRepo = uow.getRepository(UserRepository);
300
+ const orderRepo = uow.getRepository(OrderRepository);
301
+
302
+ await userRepo.save(user);
303
+ await orderRepo.save(order);
304
+
305
+ // Auto-commit on success
306
+ // Auto-rollback on error
307
+ });
308
+
309
+ // Controle manual
310
+ const ctx = await uow.begin();
311
+ try {
312
+ await userRepo.save(user);
313
+ await orderRepo.save(order);
314
+ await ctx.commit();
315
+ } catch (error) {
316
+ await ctx.rollback();
317
+ throw error;
318
+ }
319
+ ```
320
+
321
+ ### 5. Paginated Results com Deep Serialization
322
+
323
+ ```typescript
324
+ // Com Entities/Aggregates
325
+ const users = await userRepo.find(criteria);
326
+ // users: PaginatedResult<User>
327
+
328
+ // Serialização profunda (Ids, nested entities, value objects)
329
+ const json = users.toJSON();
330
+ // {
331
+ // data: [
332
+ // { id: "123", name: "João", ... }, // IDs serializados para string
333
+ // { id: "456", name: "Maria", ... }
334
+ // ],
335
+ // meta: {
336
+ // page: 1,
337
+ // limit: 10,
338
+ // total: 100,
339
+ // totalPages: 10,
340
+ // hasNext: true,
341
+ // hasPrevious: false
342
+ // }
343
+ // }
344
+
345
+ // Utilitários
346
+ users.isEmpty; // boolean
347
+ users.hasMore; // boolean
348
+ users.map(user => user.name); // Transforma cada item
349
+ ```
350
+
351
+ ## Value Objects
352
+
353
+ ```typescript
354
+ import { ValueObject } from "rich-domain";
355
+
356
+ interface AddressProps {
357
+ street: string;
358
+ city: string;
359
+ zipCode: string;
360
+ }
361
+
362
+ class Address extends ValueObject<AddressProps> {
363
+ get street() { return this.props.street; }
364
+ get city() { return this.props.city; }
365
+
366
+ changeCity(newCity: string): Address {
367
+ return this.clone({ city: newCity });
368
+ }
369
+ }
370
+
371
+ const addr1 = new Address({
372
+ street: "Av. Paulista",
373
+ city: "São Paulo",
374
+ zipCode: "01310-100",
375
+ });
376
+
377
+ const addr2 = new Address({
378
+ street: "Av. Paulista",
379
+ city: "São Paulo",
380
+ zipCode: "01310-100",
381
+ });
382
+
383
+ addr1.equals(addr2); // true (comparação por valor)
384
+
385
+ const addr3 = addr1.changeCity("Rio de Janeiro");
386
+ addr1.city; // São Paulo (imutável)
387
+ addr3.city; // Rio de Janeiro
388
+ ```
389
+
390
+ ## Sistema de IDs
391
+
392
+ ```typescript
393
+ import { Id } from "rich-domain";
394
+
395
+ // Nova entidade - gera UUID
396
+ const newId = new Id();
397
+ console.log(newId.isNew); // true
398
+
399
+ // Entidade existente
400
+ const existingId = new Id("user-123");
401
+ console.log(existingId.isNew); // false
402
+
403
+ // Comparação
404
+ newId.equals(existingId); // false
405
+ existingId.equals("user-123"); // true
406
+
407
+ // Static methods
408
+ const id1 = Id.create(); // Novo
409
+ const id2 = Id.from("abc-123"); // Existente
410
+ ```
411
+
412
+ ## Change Tracking & Subscriptions
413
+
414
+ ```typescript
415
+ const user = new User({
416
+ name: "João",
417
+ email: "joao@example.com",
418
+ });
419
+
420
+ // Subscribe
421
+ user.subscribe({
422
+ name: {
423
+ onChange: ({ previous, current }) => {
424
+ console.log(`Nome: ${previous} → ${current}`);
425
+ },
426
+ },
427
+ });
428
+
429
+ user.name = "Maria"; // Trigger onChange
430
+
431
+ // History
432
+ const history = user.getHistory();
433
+ // [{ path: 'name', previousValue: 'João', currentValue: 'Maria', timestamp: ... }]
434
+
435
+ user.clearHistory();
436
+ ```
437
+
438
+ ## Domain Events
439
+
440
+ ```typescript
441
+ import { DomainEvent, DomainEventBus } from "rich-domain";
442
+
443
+ // Definir evento
444
+ class UserCreatedEvent extends DomainEvent {
445
+ constructor(
446
+ public readonly userId: Id,
447
+ public readonly userName: string
448
+ ) {
449
+ super("UserCreated", userId);
450
+ }
451
+ }
452
+
453
+ // Criar aggregate
454
+ class User extends Aggregate<UserProps> {
455
+ static create(props: Omit<UserProps, "id">) {
456
+ const user = new User({ ...props, id: new Id() });
457
+
458
+ // Adicionar evento
459
+ user.addDomainEvent(
460
+ new UserCreatedEvent(user.id, user.name)
461
+ );
462
+
463
+ return user;
464
+ }
465
+ }
466
+
467
+ // Handler
468
+ class SendWelcomeEmailHandler {
469
+ async handle(event: UserCreatedEvent) {
470
+ await sendEmail(event.userName);
471
+ }
472
+ }
473
+
474
+ // Registrar handler
475
+ const bus = DomainEventBus.getInstance();
476
+ bus.subscribe(UserCreatedEvent, new SendWelcomeEmailHandler());
477
+
478
+ // Publicar eventos
479
+ const user = User.create({ name: "João", email: "joao@example.com" });
480
+ await user.dispatchAll(bus);
481
+ ```
482
+
483
+ ## Validation
484
+
485
+ ### Com Throw
486
+
487
+ ```typescript
488
+ const user = new User({
489
+ name: "J", // Muito curto
490
+ email: "invalid",
491
+ });
492
+ // throws ValidationError
493
+ ```
494
+
495
+ ### Sem Throw
496
+
497
+ ```typescript
498
+ class UserSafe extends Aggregate<UserProps> {
499
+ protected static validation = {
500
+ schema: userSchema,
501
+ config: { throwOnError: false },
502
+ };
503
+ }
504
+
505
+ const user = new UserSafe({
506
+ name: "J",
507
+ email: "invalid",
508
+ });
509
+
510
+ if (user.hasValidationErrors) {
511
+ console.log(user.validationErrors!.getMessages());
512
+ // ['Nome deve ter pelo menos 2 caracteres', 'Email inválido']
513
+ }
514
+ ```
515
+
516
+ ## Compatibilidade Standard Schema
517
+
518
+ ### Zod
519
+ ```typescript
520
+ import { z } from "zod";
521
+ const schema = z.object({ ... });
522
+ ```
523
+
524
+ ### Valibot
525
+ ```typescript
526
+ import * as v from "valibot";
527
+ const schema = v.object({ ... });
528
+ ```
529
+
530
+ ### ArkType
531
+ ```typescript
532
+ import { type } from "arktype";
533
+ const schema = type({ ... });
534
+ ```
535
+
536
+ ## Estrutura de Arquivos
537
+
538
+ Para exemplos completos, veja:
539
+ - `src/repository/examples/prisma-repository.example.ts` - Implementação Prisma completa
540
+ - `src/repository/examples/README.md` - Documentação detalhada
541
+ - `tests/` - Testes completos de todos os recursos
542
+
543
+ ## API Reference
544
+
545
+ ### Repository
546
+
547
+ ```typescript
548
+ interface IRepository<TDomain> {
549
+ findById(id: Id): Promise<TDomain | null>;
550
+ find(criteria: Criteria<TDomain>): Promise<PaginatedResult<TDomain>>;
551
+ findAll(criteria?: Criteria<TDomain>): Promise<TDomain[]>;
552
+ findOne(criteria: Criteria<TDomain>): Promise<TDomain | null>;
553
+ save(aggregate: TDomain): Promise<void>;
554
+ saveMany(aggregates: TDomain[]): Promise<void>;
555
+ delete(aggregate: TDomain): Promise<void>;
556
+ deleteById(id: Id): Promise<void>;
557
+ exists(id: Id): Promise<boolean>;
558
+ count(criteria?: Criteria<TDomain>): Promise<number>;
559
+ }
560
+ ```
561
+
562
+ ### Criteria
563
+
564
+ ```typescript
565
+ class Criteria<T> {
566
+ static create<T>(): Criteria<T>;
567
+
568
+ // Filters
569
+ where(field, operator, value?): this;
570
+ whereEquals(field, value): this;
571
+ whereContains(field, value): this;
572
+ whereIn(field, values): this;
573
+ whereBetween(field, min, max): this;
574
+ whereNull(field): this;
575
+ whereNotNull(field): this;
576
+
577
+ // Ordering
578
+ orderBy(field, direction?): this;
579
+ orderByAsc(field): this;
580
+ orderByDesc(field): this;
581
+
582
+ // Pagination
583
+ paginate(page, limit): this;
584
+ limit(limit): this;
585
+
586
+ // Search
587
+ search(fields, value): this;
588
+
589
+ // Utils
590
+ clone(): Criteria<T>;
591
+ toJSON(): object;
592
+
593
+ static fromObject<T>(obj): Criteria<T>;
594
+ static fromQueryParams<T>(query): Criteria<T>;
595
+ }
596
+ ```
597
+
598
+ ### PaginatedResult
599
+
600
+ ```typescript
601
+ class PaginatedResult<T> {
602
+ readonly data: T[];
603
+ readonly meta: PaginationMeta;
604
+
605
+ static create<T>(data, pagination, total): PaginatedResult<T>;
606
+ static createMeta(pagination, total): PaginationMeta;
607
+ static fromArray<T>(items, criteria): PaginatedResult<T>;
608
+
609
+ toJSON(): PaginatedJsonResult<T>; // Deep serialization
610
+ map<U>(fn): PaginatedResult<U>;
611
+
612
+ get isEmpty(): boolean;
613
+ get hasMore(): boolean;
614
+ }
615
+ ```
616
+
617
+ ### Id
618
+
619
+ ```typescript
620
+ class Id {
621
+ constructor(value?: string);
622
+
623
+ get value(): string;
624
+ get isNew(): boolean;
625
+
626
+ toString(): string;
627
+ toJSON(): string;
628
+ equals(other: Id | string): boolean;
629
+
630
+ static create(): Id;
631
+ static from(value: string): Id;
632
+ }
633
+ ```
634
+
635
+ ### BaseEntity
636
+
637
+ ```typescript
638
+ abstract class BaseEntity<T extends BaseProps> {
639
+ get id(): Id;
640
+ get isNew(): boolean;
641
+ get hasValidationErrors(): boolean;
642
+ get validationErrors(): ValidationError | undefined;
643
+
644
+ subscribe(config: SubscriptionConfig<T>): void;
645
+ getHistory(): HistoryEntry[];
646
+ clearHistory(): void;
647
+ toJson(): DeepJsonResult<T>;
648
+
649
+ // Domain Events
650
+ protected addDomainEvent(event: IDomainEvent): void;
651
+ getUncommittedEvents(): IDomainEvent[];
652
+ clearEvents(): void;
653
+ async dispatchAll(bus: DomainEventBus): Promise<void>;
654
+ }
655
+ ```
656
+
657
+ ### ValueObject
658
+
659
+ ```typescript
660
+ abstract class ValueObject<T> {
661
+ protected readonly props: T;
662
+
663
+ equals(other: ValueObject<T>): boolean;
664
+ toJson(): T;
665
+ protected clone(updates: Partial<T>): this;
666
+ }
667
+ ```
668
+
669
+ ## Testing
670
+
671
+ ```typescript
672
+ import { InMemoryRepository } from "rich-domain";
673
+
674
+ describe("UserService", () => {
675
+ const userRepo = new InMemoryRepository<User>();
676
+ const service = new UserService(userRepo);
677
+
678
+ beforeEach(() => userRepo.clear());
679
+
680
+ it("should create user", async () => {
681
+ const user = await service.createUser({
682
+ name: "João",
683
+ email: "joao@example.com",
684
+ });
685
+
686
+ expect(user.id.isNew).toBe(false);
687
+ expect(await userRepo.exists(user.id)).toBe(true);
688
+ });
689
+ });
690
+ ```
691
+
692
+ ## Roadmap
693
+
694
+ - [ ] TypeORM repository example
695
+ - [ ] MongoDB repository example
696
+ - [ ] Drizzle ORM repository example
697
+ - [ ] GraphQL integration utilities
698
+ - [ ] Advanced caching strategies
699
+
700
+ ## Contribuindo
701
+
702
+ Contribuições são bem-vindas! Veja [CONTRIBUTING.md](CONTRIBUTING.md) para detalhes.
703
+
704
+ ## Licença
705
+
706
+ MIT
707
+
708
+ ## Links
709
+
710
+ - [Documentação Completa](https://github.com/yourusername/rich-domain)
711
+ - [Exemplos](./src/repository/examples)
712
+ - [Issues](https://github.com/yourusername/rich-domain/issues)