@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.
- package/.github/workflows/ci.yml +40 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.versionrc.json +21 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/README.md +712 -0
- package/commitlint.config.js +23 -0
- package/dist/base-entity.d.ts +67 -0
- package/dist/base-entity.d.ts.map +1 -0
- package/dist/base-entity.js +309 -0
- package/dist/base-entity.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -0
- package/dist/criteria.d.ts +60 -0
- package/dist/criteria.d.ts.map +1 -0
- package/dist/criteria.js +214 -0
- package/dist/criteria.js.map +1 -0
- package/dist/deep-proxy.d.ts +34 -0
- package/dist/deep-proxy.d.ts.map +1 -0
- package/dist/deep-proxy.js +297 -0
- package/dist/deep-proxy.js.map +1 -0
- package/dist/domain-event-bus.d.ts +57 -0
- package/dist/domain-event-bus.d.ts.map +1 -0
- package/dist/domain-event-bus.js +112 -0
- package/dist/domain-event-bus.js.map +1 -0
- package/dist/domain-event.d.ts +55 -0
- package/dist/domain-event.d.ts.map +1 -0
- package/dist/domain-event.js +42 -0
- package/dist/domain-event.js.map +1 -0
- package/dist/entity.d.ts +13 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +15 -0
- package/dist/entity.js.map +1 -0
- package/dist/filtering.d.ts +107 -0
- package/dist/filtering.d.ts.map +1 -0
- package/dist/filtering.js +202 -0
- package/dist/filtering.js.map +1 -0
- package/dist/id.d.ts +51 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +84 -0
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/ordering.d.ts +93 -0
- package/dist/ordering.d.ts.map +1 -0
- package/dist/ordering.js +154 -0
- package/dist/ordering.js.map +1 -0
- package/dist/paginated-result.d.ts +62 -0
- package/dist/paginated-result.d.ts.map +1 -0
- package/dist/paginated-result.js +201 -0
- package/dist/paginated-result.js.map +1 -0
- package/dist/pagination.d.ts +218 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +281 -0
- package/dist/pagination.js.map +1 -0
- package/dist/repository/base-repository.d.ts +77 -0
- package/dist/repository/base-repository.d.ts.map +1 -0
- package/dist/repository/base-repository.js +80 -0
- package/dist/repository/base-repository.js.map +1 -0
- package/dist/repository/in-memory-repository.d.ts +46 -0
- package/dist/repository/in-memory-repository.d.ts.map +1 -0
- package/dist/repository/in-memory-repository.js +85 -0
- package/dist/repository/in-memory-repository.js.map +1 -0
- package/dist/repository/index.d.ts +42 -0
- package/dist/repository/index.d.ts.map +1 -0
- package/dist/repository/index.js +47 -0
- package/dist/repository/index.js.map +1 -0
- package/dist/repository/mapper.d.ts +56 -0
- package/dist/repository/mapper.d.ts.map +1 -0
- package/dist/repository/mapper.js +15 -0
- package/dist/repository/mapper.js.map +1 -0
- package/dist/repository/types.d.ts +87 -0
- package/dist/repository/types.d.ts.map +1 -0
- package/dist/repository/types.js +6 -0
- package/dist/repository/types.js.map +1 -0
- package/dist/repository/unit-of-work.d.ts +70 -0
- package/dist/repository/unit-of-work.d.ts.map +1 -0
- package/dist/repository/unit-of-work.js +122 -0
- package/dist/repository/unit-of-work.js.map +1 -0
- package/dist/repository.d.ts +2 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +21 -0
- package/dist/repository.js.map +1 -0
- package/dist/specification.d.ts +102 -0
- package/dist/specification.d.ts.map +1 -0
- package/dist/specification.js +187 -0
- package/dist/specification.js.map +1 -0
- package/dist/types/criteria.d.ts +35 -0
- package/dist/types/criteria.d.ts.map +1 -0
- package/dist/types/criteria.js +17 -0
- package/dist/types/criteria.js.map +1 -0
- package/dist/types/domain.d.ts +30 -0
- package/dist/types/domain.d.ts.map +1 -0
- package/dist/types/domain.js +2 -0
- package/dist/types/domain.js.map +1 -0
- package/dist/types/history-tracker.d.ts +36 -0
- package/dist/types/history-tracker.d.ts.map +1 -0
- package/dist/types/history-tracker.js +2 -0
- package/dist/types/history-tracker.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/repository.d.ts +43 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/repository.js +2 -0
- package/dist/types/repository.js.map +1 -0
- package/dist/types/standard-schema.d.ts +15 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/types/unit-of-work.d.ts +39 -0
- package/dist/types/unit-of-work.d.ts.map +1 -0
- package/dist/types/unit-of-work.js +2 -0
- package/dist/types/unit-of-work.js.map +1 -0
- package/dist/types/utils.d.ts +14 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/utils.js +2 -0
- package/dist/types/utils.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/validation-error.d.ts +42 -0
- package/dist/validation-error.d.ts.map +1 -0
- package/dist/validation-error.js +73 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/value-object.d.ts +47 -0
- package/dist/value-object.d.ts.map +1 -0
- package/dist/value-object.js +136 -0
- package/dist/value-object.js.map +1 -0
- package/eslint.config.js +51 -0
- package/jest.config.js +21 -0
- package/package.json +58 -0
- package/src/base-entity.ts +401 -0
- package/src/constants.ts +7 -0
- package/src/criteria.ts +291 -0
- package/src/deep-proxy.ts +339 -0
- package/src/domain-event-bus.ts +166 -0
- package/src/domain-event.ts +90 -0
- package/src/entity.ts +16 -0
- package/src/id.ts +94 -0
- package/src/index.ts +33 -0
- package/src/paginated-result.ts +274 -0
- package/src/repository/base-repository.ts +152 -0
- package/src/repository/in-memory-repository.ts +104 -0
- package/src/repository/index.ts +55 -0
- package/src/repository/mapper.ts +74 -0
- package/src/repository/unit-of-work.ts +148 -0
- package/src/types/criteria.ts +79 -0
- package/src/types/domain.ts +37 -0
- package/src/types/history-tracker.ts +45 -0
- package/src/types/index.ts +7 -0
- package/src/types/repository.ts +51 -0
- package/src/types/standard-schema.ts +19 -0
- package/src/types/unit-of-work.ts +46 -0
- package/src/types/utils.ts +29 -0
- package/src/validation-error.ts +97 -0
- package/src/value-object.ts +187 -0
- package/tests/criteria.test.ts +432 -0
- package/tests/domain-events.test.ts +445 -0
- package/tests/entity-equality.test.ts +487 -0
- package/tests/entity-validation.test.ts +339 -0
- package/tests/entity.test.ts +33 -0
- package/tests/history-tracker.spec.ts +667 -0
- package/tests/id.test.ts +341 -0
- package/tests/repository.test.ts +641 -0
- package/tests/to-json.test.ts +91 -0
- package/tests/utils.ts +151 -0
- package/tests/value-object-validation.test.ts +228 -0
- package/tests/value-objects.test.ts +52 -0
- 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)
|