@woltz/rich-domain 1.2.0 → 1.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/CHANGELOG.md +33 -0
- package/dist/aggregate-changes.d.ts +164 -0
- package/dist/aggregate-changes.d.ts.map +1 -0
- package/dist/aggregate-changes.js +281 -0
- package/dist/aggregate-changes.js.map +1 -0
- package/dist/base-entity.d.ts +32 -8
- package/dist/base-entity.d.ts.map +1 -1
- package/dist/base-entity.js +117 -86
- package/dist/base-entity.js.map +1 -1
- package/dist/criteria.d.ts +3 -3
- package/dist/criteria.d.ts.map +1 -1
- package/dist/criteria.js.map +1 -1
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +29 -0
- package/dist/crypto.js.map +1 -0
- package/dist/entity-changes.d.ts +84 -0
- package/dist/entity-changes.d.ts.map +1 -0
- package/dist/entity-changes.js +135 -0
- package/dist/entity-changes.js.map +1 -0
- package/dist/entity-schema-registry.d.ts +148 -0
- package/dist/entity-schema-registry.d.ts.map +1 -0
- package/dist/entity-schema-registry.js +219 -0
- package/dist/entity-schema-registry.js.map +1 -0
- package/dist/history-tracker.d.ts +97 -0
- package/dist/history-tracker.d.ts.map +1 -0
- package/dist/history-tracker.js +805 -0
- package/dist/history-tracker.js.map +1 -0
- package/dist/id.d.ts +11 -10
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +4 -28
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mapper.d.ts +1 -1
- package/dist/mapper.d.ts.map +1 -1
- package/dist/mapper.js.map +1 -1
- package/dist/repository/base-repository.d.ts +6 -32
- package/dist/repository/base-repository.d.ts.map +1 -1
- package/dist/repository/base-repository.js +0 -27
- package/dist/repository/base-repository.js.map +1 -1
- package/dist/repository/unit-of-work.d.ts +0 -25
- package/dist/repository/unit-of-work.d.ts.map +1 -1
- package/dist/repository/unit-of-work.js +0 -25
- package/dist/repository/unit-of-work.js.map +1 -1
- package/dist/types/change-tracker.d.ts +186 -0
- package/dist/types/change-tracker.d.ts.map +1 -0
- package/dist/types/change-tracker.js +2 -0
- package/dist/types/change-tracker.js.map +1 -0
- package/dist/types/criteria.d.ts +5 -1
- package/dist/types/criteria.d.ts.map +1 -1
- package/dist/types/history-tracker.d.ts +11 -0
- package/dist/types/history-tracker.d.ts.map +1 -1
- package/dist/types/utils.d.ts +0 -1
- package/dist/types/utils.d.ts.map +1 -1
- package/dist/validation-error.d.ts.map +1 -1
- package/dist/validation-error.js +0 -3
- package/dist/validation-error.js.map +1 -1
- package/dist/value-object.d.ts +57 -8
- package/dist/value-object.d.ts.map +1 -1
- package/dist/value-object.js +49 -21
- package/dist/value-object.js.map +1 -1
- package/package.json +2 -1
- package/src/aggregate-changes.ts +335 -0
- package/src/base-entity.ts +140 -100
- package/src/criteria.ts +2 -1
- package/src/crypto.ts +31 -0
- package/src/entity-changes.ts +151 -0
- package/src/entity-schema-registry.ts +275 -0
- package/src/history-tracker.ts +1114 -0
- package/src/id.ts +17 -26
- package/src/index.ts +1 -0
- package/src/mapper.ts +4 -1
- package/src/repository/base-repository.ts +6 -37
- package/src/repository/unit-of-work.ts +0 -25
- package/src/types/change-tracker.ts +221 -0
- package/src/types/criteria.ts +6 -1
- package/src/types/history-tracker.ts +13 -0
- package/src/types/utils.ts +0 -9
- package/src/validation-error.ts +0 -4
- package/src/value-object.ts +84 -23
- package/tests/aggregate-changes.test.ts +284 -0
- package/tests/criteria.test.ts +122 -161
- package/tests/entity-equality.test.ts +38 -61
- package/tests/entity-schema-registry.test.ts +382 -0
- package/tests/entity-validation.test.ts +7 -94
- package/tests/history-tracker.spec.ts +349 -617
- package/tests/id.test.ts +41 -44
- package/tests/load-test/data.json +346041 -0
- package/tests/load-test/entities.ts +97 -0
- package/tests/load-test/generate-data.ts +81 -0
- package/tests/load-test/lead-to-domain.mapper.ts +24 -0
- package/tests/load-test/load.test.ts +38 -0
- package/tests/repository.test.ts +30 -54
- package/tests/to-json.test.ts +14 -18
- package/tests/utils.ts +138 -102
- package/tests/value-objects.test.ts +57 -29
- package/dist/deep-proxy.d.ts +0 -36
- package/dist/deep-proxy.d.ts.map +0 -1
- package/dist/deep-proxy.js +0 -384
- package/dist/deep-proxy.js.map +0 -1
- package/src/deep-proxy.ts +0 -447
- package/tests/entity.test.ts +0 -33
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Operation,
|
|
3
|
+
CreateOperation,
|
|
4
|
+
UpdateOperation,
|
|
5
|
+
DeleteOperation,
|
|
6
|
+
BatchOperations,
|
|
7
|
+
BatchCreateItem,
|
|
8
|
+
BatchUpdateItem,
|
|
9
|
+
} from "./types/change-tracker";
|
|
10
|
+
import { EntityChanges } from "./entity-changes";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages and organizes the changes of an Aggregate.
|
|
14
|
+
*
|
|
15
|
+
* Responsibilities:
|
|
16
|
+
* - Stores all operations (create, update, delete)
|
|
17
|
+
* - Orders operations respecting FK dependencies
|
|
18
|
+
* - Groups operations by entity for batch execution
|
|
19
|
+
* - Provides query and iteration methods
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* // Define an entity map for type-safe operations
|
|
24
|
+
* type UserEntities = {
|
|
25
|
+
* User: User;
|
|
26
|
+
* Post: Post;
|
|
27
|
+
* Comment: Comment;
|
|
28
|
+
* };
|
|
29
|
+
*
|
|
30
|
+
* // Getting changes with types
|
|
31
|
+
* const changes = user.getChanges<UserEntities>();
|
|
32
|
+
*
|
|
33
|
+
* // Filtering by entity with autocompletion
|
|
34
|
+
* const postChanges = changes.for('Post'); // 'Post' autocompletes
|
|
35
|
+
* postChanges.creates.forEach(post => {
|
|
36
|
+
* console.log(post.title); // 'post' is typed as Post
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export class AggregateChanges<TEntityMap = Record<string, any>> {
|
|
41
|
+
private ops: Operation[] = [];
|
|
42
|
+
|
|
43
|
+
constructor(operations: Operation[] = []) {
|
|
44
|
+
this.ops = [...operations];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Adds a create operation.
|
|
49
|
+
*/
|
|
50
|
+
addCreate<T>(
|
|
51
|
+
entity: string,
|
|
52
|
+
data: T,
|
|
53
|
+
depth: number,
|
|
54
|
+
parentId?: string,
|
|
55
|
+
parentEntity?: string
|
|
56
|
+
): void {
|
|
57
|
+
this.ops.push({
|
|
58
|
+
type: "create",
|
|
59
|
+
entity,
|
|
60
|
+
data,
|
|
61
|
+
depth,
|
|
62
|
+
parentId,
|
|
63
|
+
parentEntity,
|
|
64
|
+
} as CreateOperation<T>);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Adds an update operation.
|
|
69
|
+
*/
|
|
70
|
+
addUpdate<T>(
|
|
71
|
+
entity: string,
|
|
72
|
+
id: string,
|
|
73
|
+
data: T,
|
|
74
|
+
changedFields: Record<string, any>,
|
|
75
|
+
depth: number
|
|
76
|
+
): void {
|
|
77
|
+
this.ops.push({
|
|
78
|
+
type: "update",
|
|
79
|
+
entity,
|
|
80
|
+
id,
|
|
81
|
+
data,
|
|
82
|
+
changedFields,
|
|
83
|
+
depth,
|
|
84
|
+
} as UpdateOperation<T>);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Adds a delete operation.
|
|
89
|
+
*/
|
|
90
|
+
addDelete<T>(entity: string, id: string, data: T, depth: number): void {
|
|
91
|
+
this.ops.push({
|
|
92
|
+
type: "delete",
|
|
93
|
+
entity,
|
|
94
|
+
id,
|
|
95
|
+
data,
|
|
96
|
+
depth,
|
|
97
|
+
} as DeleteOperation<T>);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Returns all create operations, sorted by ascending depth (root → leaf).
|
|
102
|
+
*/
|
|
103
|
+
creates(): CreateOperation[] {
|
|
104
|
+
return this.ops
|
|
105
|
+
.filter((op): op is CreateOperation => op.type === "create")
|
|
106
|
+
.sort((a, b) => a.depth - b.depth);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Returns all update operations.
|
|
111
|
+
*/
|
|
112
|
+
updates(): UpdateOperation[] {
|
|
113
|
+
return this.ops.filter((op): op is UpdateOperation => op.type === "update");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Returns all delete operations, sorted by descending depth (leaf → root).
|
|
118
|
+
*/
|
|
119
|
+
deletes(): DeleteOperation[] {
|
|
120
|
+
return this.ops
|
|
121
|
+
.filter((op): op is DeleteOperation => op.type === "delete")
|
|
122
|
+
.sort((a, b) => b.depth - a.depth);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Iterator that returns operations in the correct execution order:
|
|
127
|
+
* 1. Deletes (leaf → root)
|
|
128
|
+
* 2. Creates (root → leaf)
|
|
129
|
+
* 3. Updates
|
|
130
|
+
*/
|
|
131
|
+
*operations(): Generator<Operation> {
|
|
132
|
+
yield* this.deletes();
|
|
133
|
+
yield* this.creates();
|
|
134
|
+
yield* this.updates();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Returns all operations as an array in execution order.
|
|
139
|
+
*/
|
|
140
|
+
toArray(): Operation[] {
|
|
141
|
+
return [...this.operations()];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Converts the changes into BatchOperations for optimized execution.
|
|
146
|
+
*
|
|
147
|
+
* Groups operations by entity and sorts by depth:
|
|
148
|
+
* - Deletes: depth DESC (leaf → root)
|
|
149
|
+
* - Creates: depth ASC (root → leaf)
|
|
150
|
+
* - Updates: grouped by entity
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const batch = changes.toBatchOperations();
|
|
155
|
+
*
|
|
156
|
+
* // Run deletes
|
|
157
|
+
* for (const del of batch.deletes) {
|
|
158
|
+
* await tx[del.entity].deleteMany({ where: { id: { in: del.ids } } });
|
|
159
|
+
* }
|
|
160
|
+
*
|
|
161
|
+
* // Run creates
|
|
162
|
+
* for (const create of batch.creates) {
|
|
163
|
+
* await tx[create.entity].createMany({ data: create.items });
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
toBatchOperations(): BatchOperations {
|
|
168
|
+
return {
|
|
169
|
+
deletes: this.groupDeletes(),
|
|
170
|
+
creates: this.groupCreates(),
|
|
171
|
+
updates: this.groupUpdates(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Groups deletes by entity, sorted by descending depth.
|
|
177
|
+
*/
|
|
178
|
+
private groupDeletes(): BatchOperations["deletes"] {
|
|
179
|
+
const deleteOps = this.deletes();
|
|
180
|
+
const grouped = new Map<string, { depth: number; ids: string[] }>();
|
|
181
|
+
|
|
182
|
+
for (const op of deleteOps) {
|
|
183
|
+
if (!grouped.has(op.entity)) {
|
|
184
|
+
grouped.set(op.entity, { depth: op.depth, ids: [] });
|
|
185
|
+
}
|
|
186
|
+
grouped.get(op.entity)!.ids.push(op.id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Array.from(grouped.entries())
|
|
190
|
+
.map(([entity, { depth, ids }]) => ({ entity, depth, ids }))
|
|
191
|
+
.sort((a, b) => b.depth - a.depth);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Groups creates by entity, sorted by ascending depth.
|
|
196
|
+
*/
|
|
197
|
+
private groupCreates(): BatchOperations["creates"] {
|
|
198
|
+
const createOps = this.creates();
|
|
199
|
+
const grouped = new Map<
|
|
200
|
+
string,
|
|
201
|
+
{ depth: number; items: BatchCreateItem[] }
|
|
202
|
+
>();
|
|
203
|
+
|
|
204
|
+
for (const op of createOps) {
|
|
205
|
+
if (!grouped.has(op.entity)) {
|
|
206
|
+
grouped.set(op.entity, { depth: op.depth, items: [] });
|
|
207
|
+
}
|
|
208
|
+
grouped.get(op.entity)!.items.push({
|
|
209
|
+
data: op.data,
|
|
210
|
+
parentId: op.parentId,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return Array.from(grouped.entries())
|
|
215
|
+
.map(([entity, { depth, items }]) => ({ entity, depth, items }))
|
|
216
|
+
.sort((a, b) => a.depth - b.depth);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Groups updates by entity.
|
|
221
|
+
*/
|
|
222
|
+
private groupUpdates(): BatchOperations["updates"] {
|
|
223
|
+
const updateOps = this.updates();
|
|
224
|
+
const grouped = new Map<string, BatchUpdateItem[]>();
|
|
225
|
+
|
|
226
|
+
for (const op of updateOps) {
|
|
227
|
+
if (!grouped.has(op.entity)) {
|
|
228
|
+
grouped.set(op.entity, []);
|
|
229
|
+
}
|
|
230
|
+
grouped.get(op.entity)!.push({
|
|
231
|
+
id: op.id,
|
|
232
|
+
changedFields: op.changedFields,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return Array.from(grouped.entries()).map(([entity, items]) => ({
|
|
237
|
+
entity,
|
|
238
|
+
items,
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Filters changes by entity name.
|
|
244
|
+
*
|
|
245
|
+
* @param entityName - Name of the entity (e.g., 'Post', 'Comment')
|
|
246
|
+
* @returns EntityChanges containing only the operations for this entity
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const postChanges = changes.for('Post');
|
|
251
|
+
*
|
|
252
|
+
* if (postChanges.hasCreates()) {
|
|
253
|
+
* postChanges.creates.forEach(post => {
|
|
254
|
+
* console.log('New post:', post.title);
|
|
255
|
+
* });
|
|
256
|
+
* }
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
for<K extends keyof TEntityMap>(entityName: K): EntityChanges<TEntityMap[K]> {
|
|
260
|
+
const filtered = this.ops.filter((op) => op.entity === entityName);
|
|
261
|
+
return new EntityChanges<TEntityMap[K]>(filtered);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Checks if there are create operations.
|
|
266
|
+
*/
|
|
267
|
+
hasCreates(): boolean {
|
|
268
|
+
return this.ops.some((op) => op.type === "create");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Checks if there are update operations.
|
|
273
|
+
*/
|
|
274
|
+
hasUpdates(): boolean {
|
|
275
|
+
return this.ops.some((op) => op.type === "update");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Checks if there are delete operations.
|
|
280
|
+
*/
|
|
281
|
+
hasDeletes(): boolean {
|
|
282
|
+
return this.ops.some((op) => op.type === "delete");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Checks if there are any operations.
|
|
287
|
+
*/
|
|
288
|
+
hasChanges(): boolean {
|
|
289
|
+
return this.ops.length > 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Checks if there are no operations.
|
|
294
|
+
*/
|
|
295
|
+
isEmpty(): boolean {
|
|
296
|
+
return this.ops.length === 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Returns the total number of operations.
|
|
301
|
+
*/
|
|
302
|
+
get count(): number {
|
|
303
|
+
return this.ops.length;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Returns the raw operations (for debug/testing).
|
|
308
|
+
*/
|
|
309
|
+
get rawOperations(): Operation[] {
|
|
310
|
+
return [...this.ops];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Lists all entities that have changes.
|
|
315
|
+
*/
|
|
316
|
+
getAffectedEntities(): string[] {
|
|
317
|
+
const entities = new Set<string>();
|
|
318
|
+
this.ops.forEach((op) => entities.add(op.entity));
|
|
319
|
+
return Array.from(entities);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Clears all operations.
|
|
324
|
+
*/
|
|
325
|
+
clear(): void {
|
|
326
|
+
this.ops = [];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Creates a copy of the changes.
|
|
331
|
+
*/
|
|
332
|
+
clone(): AggregateChanges<TEntityMap> {
|
|
333
|
+
return new AggregateChanges<TEntityMap>([...this.ops]);
|
|
334
|
+
}
|
|
335
|
+
}
|