@woltz/rich-domain 1.2.0 → 1.2.2

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 (143) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/aggregate-changes.d.ts +164 -0
  3. package/dist/aggregate-changes.d.ts.map +1 -0
  4. package/dist/aggregate-changes.js +281 -0
  5. package/dist/aggregate-changes.js.map +1 -0
  6. package/dist/base-entity.d.ts +32 -8
  7. package/dist/base-entity.d.ts.map +1 -1
  8. package/dist/base-entity.js +86 -93
  9. package/dist/base-entity.js.map +1 -1
  10. package/dist/change-tracker.d.ts +97 -0
  11. package/dist/change-tracker.d.ts.map +1 -0
  12. package/dist/change-tracker.js +758 -0
  13. package/dist/change-tracker.js.map +1 -0
  14. package/dist/constants.d.ts +7 -1
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js +65 -0
  17. package/dist/constants.js.map +1 -1
  18. package/dist/criteria.d.ts +3 -3
  19. package/dist/criteria.d.ts.map +1 -1
  20. package/dist/criteria.js +6 -4
  21. package/dist/criteria.js.map +1 -1
  22. package/dist/crypto.d.ts +3 -0
  23. package/dist/crypto.d.ts.map +1 -0
  24. package/dist/crypto.js +29 -0
  25. package/dist/crypto.js.map +1 -0
  26. package/dist/domain-event.d.ts.map +1 -1
  27. package/dist/domain-event.js +0 -3
  28. package/dist/domain-event.js.map +1 -1
  29. package/dist/entity-changes.d.ts +84 -0
  30. package/dist/entity-changes.d.ts.map +1 -0
  31. package/dist/entity-changes.js +131 -0
  32. package/dist/entity-changes.js.map +1 -0
  33. package/dist/entity-schema-registry.d.ts +148 -0
  34. package/dist/entity-schema-registry.d.ts.map +1 -0
  35. package/dist/entity-schema-registry.js +213 -0
  36. package/dist/entity-schema-registry.js.map +1 -0
  37. package/dist/entity.d.ts +0 -6
  38. package/dist/entity.d.ts.map +1 -1
  39. package/dist/entity.js +0 -9
  40. package/dist/entity.js.map +1 -1
  41. package/dist/id.d.ts +11 -10
  42. package/dist/id.d.ts.map +1 -1
  43. package/dist/id.js +4 -28
  44. package/dist/id.js.map +1 -1
  45. package/dist/index.d.ts +9 -5
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -11
  48. package/dist/index.js.map +1 -1
  49. package/dist/mapper.d.ts +1 -1
  50. package/dist/mapper.d.ts.map +1 -1
  51. package/dist/mapper.js.map +1 -1
  52. package/dist/paginated-result.d.ts.map +1 -1
  53. package/dist/paginated-result.js +0 -15
  54. package/dist/paginated-result.js.map +1 -1
  55. package/dist/repository/base-repository.d.ts +7 -33
  56. package/dist/repository/base-repository.d.ts.map +1 -1
  57. package/dist/repository/base-repository.js +0 -27
  58. package/dist/repository/base-repository.js.map +1 -1
  59. package/dist/repository/index.d.ts.map +1 -1
  60. package/dist/repository/index.js +0 -6
  61. package/dist/repository/index.js.map +1 -1
  62. package/dist/repository/unit-of-work.d.ts +0 -25
  63. package/dist/repository/unit-of-work.d.ts.map +1 -1
  64. package/dist/repository/unit-of-work.js +0 -28
  65. package/dist/repository/unit-of-work.js.map +1 -1
  66. package/dist/types/change-tracker.d.ts +196 -0
  67. package/dist/types/change-tracker.d.ts.map +1 -0
  68. package/dist/types/change-tracker.js +2 -0
  69. package/dist/types/change-tracker.js.map +1 -0
  70. package/dist/types/criteria.d.ts +5 -1
  71. package/dist/types/criteria.d.ts.map +1 -1
  72. package/dist/types/domain.d.ts +4 -6
  73. package/dist/types/domain.d.ts.map +1 -1
  74. package/dist/types/index.d.ts +1 -1
  75. package/dist/types/index.d.ts.map +1 -1
  76. package/dist/types/index.js +1 -1
  77. package/dist/types/index.js.map +1 -1
  78. package/dist/types/utils.d.ts +0 -1
  79. package/dist/types/utils.d.ts.map +1 -1
  80. package/dist/utils/criteria-operator-validation.d.ts +1 -0
  81. package/dist/utils/criteria-operator-validation.d.ts.map +1 -1
  82. package/dist/utils/criteria-operator-validation.js +39 -17
  83. package/dist/utils/criteria-operator-validation.js.map +1 -1
  84. package/dist/validation-error.d.ts.map +1 -1
  85. package/dist/validation-error.js +1 -6
  86. package/dist/validation-error.js.map +1 -1
  87. package/dist/value-object.d.ts +57 -8
  88. package/dist/value-object.d.ts.map +1 -1
  89. package/dist/value-object.js +49 -22
  90. package/dist/value-object.js.map +1 -1
  91. package/package.json +2 -1
  92. package/src/aggregate-changes.ts +335 -0
  93. package/src/base-entity.ts +102 -109
  94. package/src/change-tracker.ts +1062 -0
  95. package/src/constants.ts +75 -1
  96. package/src/criteria.ts +11 -4
  97. package/src/crypto.ts +31 -0
  98. package/src/domain-event.ts +0 -4
  99. package/src/entity-changes.ts +146 -0
  100. package/src/entity-schema-registry.ts +255 -0
  101. package/src/entity.ts +0 -11
  102. package/src/id.ts +17 -26
  103. package/src/index.ts +15 -19
  104. package/src/mapper.ts +4 -1
  105. package/src/paginated-result.ts +0 -21
  106. package/src/repository/base-repository.ts +7 -38
  107. package/src/repository/index.ts +0 -9
  108. package/src/repository/unit-of-work.ts +0 -29
  109. package/src/types/change-tracker.ts +233 -0
  110. package/src/types/criteria.ts +6 -1
  111. package/src/types/domain.ts +4 -8
  112. package/src/types/index.ts +1 -1
  113. package/src/types/utils.ts +0 -9
  114. package/src/utils/criteria-operator-validation.ts +57 -19
  115. package/src/validation-error.ts +1 -7
  116. package/src/value-object.ts +84 -24
  117. package/tests/aggregate-changes.test.ts +284 -0
  118. package/tests/criteria.test.ts +122 -161
  119. package/tests/entity-equality.test.ts +38 -61
  120. package/tests/entity-schema-registry.test.ts +382 -0
  121. package/tests/entity-validation.test.ts +7 -94
  122. package/tests/history-tracker.spec.ts +349 -617
  123. package/tests/id.test.ts +41 -44
  124. package/tests/load-test/data.json +346041 -0
  125. package/tests/load-test/entities.ts +97 -0
  126. package/tests/load-test/generate-data.ts +81 -0
  127. package/tests/load-test/lead-to-domain.mapper.ts +24 -0
  128. package/tests/load-test/load.test.ts +38 -0
  129. package/tests/repository.test.ts +30 -54
  130. package/tests/to-json.test.ts +14 -18
  131. package/tests/utils.ts +138 -102
  132. package/tests/value-objects.test.ts +57 -29
  133. package/dist/deep-proxy.d.ts +0 -36
  134. package/dist/deep-proxy.d.ts.map +0 -1
  135. package/dist/deep-proxy.js +0 -384
  136. package/dist/deep-proxy.js.map +0 -1
  137. package/dist/types/history-tracker.d.ts +0 -36
  138. package/dist/types/history-tracker.d.ts.map +0 -1
  139. package/dist/types/history-tracker.js +0 -2
  140. package/dist/types/history-tracker.js.map +0 -1
  141. package/src/deep-proxy.ts +0 -447
  142. package/src/types/history-tracker.ts +0 -45
  143. package/tests/entity.test.ts +0 -33
@@ -1,14 +1,8 @@
1
- // ============================================================================
2
- // Base Entity Class with Standard Schema Validation
3
- // ============================================================================
4
-
5
1
  import { Id } from "./id";
6
- import { DeepProxy } from "./deep-proxy";
7
2
  import { ValidationError } from "./validation-error";
8
3
  import { IDomainEvent } from ".";
9
4
  import {
10
5
  BaseProps,
11
- SubscriptionConfig,
12
6
  HistoryEntry,
13
7
  DeepJsonResult,
14
8
  EntityHooks,
@@ -19,8 +13,9 @@ import {
19
13
  import { DomainEventBus } from "./domain-event-bus";
20
14
  import { DEFAULT_VALIDATION_CONFIG } from "./constants";
21
15
  import { DomainError } from "./exceptions";
16
+ import { ChangeTracker } from "./change-tracker";
17
+ import { AggregateChanges } from "./aggregate-changes";
22
18
 
23
- // Helper to get static properties from constructor
24
19
  function getStaticProperty<T>(
25
20
  instance: any,
26
21
  propertyName: string
@@ -30,7 +25,7 @@ function getStaticProperty<T>(
30
25
 
31
26
  export abstract class BaseEntity<T extends BaseProps> {
32
27
  private _props: T;
33
- private proxy: DeepProxy;
28
+ private tracker: ChangeTracker;
34
29
  private proxiedProps: T;
35
30
  private snapshot: T | null = null;
36
31
  private validationConfig: Required<ValidationConfig>;
@@ -38,12 +33,10 @@ export abstract class BaseEntity<T extends BaseProps> {
38
33
  private entitySchema?: StandardSchema<T>;
39
34
  private domainEvents: IDomainEvent[] = [];
40
35
 
41
- // Static properties that subclasses can override
42
36
  protected static validation?: EntityValidation<any>;
43
37
  protected static hooks?: EntityHooks<any, any>;
44
38
 
45
39
  constructor(props: Omit<T, "id"> & { id?: Id }) {
46
- // Get static configuration from subclass
47
40
  const validation = getStaticProperty<EntityValidation<T>>(
48
41
  this,
49
42
  "validation"
@@ -63,36 +56,31 @@ export abstract class BaseEntity<T extends BaseProps> {
63
56
 
64
57
  let finalProps = { ...props } as T;
65
58
 
66
- // Generate ID if not provided
67
59
  if (!finalProps.id) {
68
60
  finalProps.id = new Id();
69
61
  }
70
62
 
71
- // Validate schema on creation
72
63
  if (this.entitySchema && this.validationConfig.onCreate) {
73
64
  this.validateProps(finalProps);
74
65
  }
75
66
 
76
67
  this._props = finalProps;
77
- this.proxy = new DeepProxy(this._props);
78
- this.proxiedProps = this.proxy.createProxy();
68
+ this.tracker = new ChangeTracker(this._props, this.constructor.name);
69
+
70
+ if (this.validationConfig.onUpdate) {
71
+ this.setupUpdateValidation();
72
+ }
73
+
74
+ this.proxiedProps = this.tracker.createProxy();
79
75
 
80
- // Execute rules (custom validations)
81
76
  if (hooks?.rules) {
82
77
  hooks.rules(this as any);
83
78
  }
84
79
 
85
- // Hook onCreate
86
80
  if (hooks?.onCreate) {
87
81
  hooks.onCreate(this as any);
88
82
  }
89
83
 
90
- // Setup update validation
91
- if (this.entitySchema && this.validationConfig.onUpdate) {
92
- this.setupUpdateValidation();
93
- }
94
-
95
- // Take initial snapshot for onBeforeUpdate
96
84
  this.takeSnapshot();
97
85
  }
98
86
 
@@ -119,7 +107,6 @@ export abstract class BaseEntity<T extends BaseProps> {
119
107
  throw validationError;
120
108
  }
121
109
 
122
- // If not throwing, store error for later retrieval
123
110
  (this as any)._validationError = validationError;
124
111
  }
125
112
  }
@@ -128,90 +115,93 @@ export abstract class BaseEntity<T extends BaseProps> {
128
115
  if (pathSegment === null || pathSegment === undefined) {
129
116
  return "";
130
117
  }
131
- // Handle PropertyKey (string | number | symbol)
132
118
  if (typeof pathSegment === "string" || typeof pathSegment === "number") {
133
119
  return String(pathSegment);
134
120
  }
135
121
  if (typeof pathSegment === "symbol") {
136
122
  return pathSegment.toString();
137
123
  }
138
- // Handle object with 'key' property (Zod's PathSegment)
139
124
  if (typeof pathSegment === "object" && "key" in pathSegment) {
140
125
  return String((pathSegment as { key: unknown }).key);
141
126
  }
142
- // Fallback
143
127
  return String(pathSegment);
144
128
  }
145
129
 
130
+ /**
131
+ * Setup validation that runs on every property change.
132
+ * Uses the ChangeTracker's onChangeValidator callback.
133
+ */
146
134
  private setupUpdateValidation(): void {
147
135
  const self = this;
148
136
 
149
- const validateOnChange = () => {
150
- // Check onBeforeUpdate hook
151
- if (self.entityHooks?.onBeforeUpdate && self.snapshot) {
152
- const shouldContinue = self.entityHooks.onBeforeUpdate(
153
- self as any,
154
- self.snapshot
155
- );
156
- if (!shouldContinue) {
157
- // Revert changes
158
- Object.assign(self._props, self.snapshot);
159
- return;
160
- }
161
- }
137
+ this.tracker.setOnChangeValidator((path, oldValue, newValue) => {
138
+ const originalValue = self._props[path as keyof T];
139
+ (self._props as any)[path] = newValue;
162
140
 
163
- // Validate with schema
164
- if (self.entitySchema) {
165
- const result = self.entitySchema["~standard"].validate(self._props);
166
-
167
- if (result instanceof Promise) {
168
- console.warn(
169
- "Async validation on update not supported. Consider using sync validation."
141
+ try {
142
+ if (self.entityHooks?.onBeforeUpdate && self.snapshot) {
143
+ const shouldContinue = self.entityHooks.onBeforeUpdate(
144
+ self as any,
145
+ self.snapshot
170
146
  );
171
- return;
147
+ if (!shouldContinue) {
148
+ (self._props as any)[path] = originalValue;
149
+ return false;
150
+ }
172
151
  }
173
152
 
174
- if (result.issues && result.issues.length > 0) {
175
- const validationError = new ValidationError(
176
- result.issues.map((issue) => ({
177
- path: issue.path?.map((p) => self.extractPathKey(p)) || [],
178
- message: issue.message,
179
- }))
180
- );
153
+ if (self.entitySchema) {
154
+ const result = self.entitySchema["~standard"].validate(self._props);
155
+
156
+ if (result instanceof Promise) {
157
+ console.warn(
158
+ "Async validation on update not supported. Consider using sync validation."
159
+ );
160
+ (self._props as any)[path] = originalValue;
161
+ return true;
162
+ }
163
+
164
+ if (result.issues && result.issues.length > 0) {
165
+ const validationError = new ValidationError(
166
+ result.issues.map((issue) => ({
167
+ path: issue.path?.map((p) => self.extractPathKey(p)) || [],
168
+ message: issue.message,
169
+ }))
170
+ );
181
171
 
182
- if (self.validationConfig.throwOnError) {
183
- // Revert changes before throwing
184
- if (self.snapshot) {
185
- Object.assign(self._props, self.snapshot);
172
+ (self._props as any)[path] = originalValue;
173
+
174
+ if (self.validationConfig.throwOnError) {
175
+ throw validationError;
186
176
  }
187
- throw validationError;
177
+
178
+ console.error("Validation failed on update:", validationError);
179
+ return false;
188
180
  }
189
- console.error("Validation failed on update:", validationError);
190
181
  }
191
- }
192
182
 
193
- // Execute rules after schema validation
194
- if (self.entityHooks?.rules) {
195
- try {
196
- self.entityHooks.rules(self as any);
197
- } catch (error) {
198
- if (self.validationConfig.throwOnError) {
199
- // Revert changes before throwing
200
- if (self.snapshot) {
201
- Object.assign(self._props, self.snapshot);
183
+ if (self.entityHooks?.rules) {
184
+ try {
185
+ self.entityHooks.rules(self as any);
186
+ } catch (error) {
187
+ (self._props as any)[path] = originalValue;
188
+
189
+ if (self.validationConfig.throwOnError) {
190
+ throw error;
202
191
  }
203
- throw error;
192
+
193
+ console.error("Rules validation failed on update:", error);
194
+ return false;
204
195
  }
205
- console.error("Rules validation failed on update:", error);
206
196
  }
207
- }
208
-
209
- // Update snapshot after successful validation
210
- self.takeSnapshot();
211
- };
212
197
 
213
- // Subscribe to all changes
214
- this.proxy.subscribe("*", validateOnChange);
198
+ (self._props as any)[path] = originalValue;
199
+ return true;
200
+ } catch (error) {
201
+ (self._props as any)[path] = originalValue;
202
+ throw error;
203
+ }
204
+ });
215
205
  }
216
206
 
217
207
  private takeSnapshot(): void {
@@ -220,36 +210,28 @@ export abstract class BaseEntity<T extends BaseProps> {
220
210
 
221
211
  private deepCloneProps(obj: any, seen: WeakSet<object> = new WeakSet()): any {
222
212
  if (obj === null || obj === undefined) return obj;
223
-
224
- // Primitives
225
213
  if (typeof obj !== "object") return obj;
226
-
227
- // Special cases - don't clone these, just return the reference
228
214
  if (obj instanceof Id) return obj;
229
215
  if (obj instanceof Date) return new Date(obj.getTime());
230
216
 
231
- // Check for circular references
232
217
  if (seen.has(obj)) {
233
- return obj; // Return reference to avoid infinite loop
218
+ return obj;
234
219
  }
235
220
 
236
- // Handle BaseEntity instances - just keep the reference
237
221
  if (obj instanceof BaseEntity) {
238
222
  return obj;
239
223
  }
240
224
 
241
- // Handle ValueObject instances - just keep the reference (they're immutable)
242
225
  if (
243
226
  obj.constructor &&
244
227
  obj.constructor.name !== "Object" &&
245
228
  obj.constructor.name !== "Array"
246
229
  ) {
247
- // Check if it has toJson method (likely a ValueObject or similar)
248
230
  if (
249
231
  typeof obj.toJson === "function" &&
250
232
  typeof obj.equals === "function"
251
233
  ) {
252
- return obj; // Keep reference for ValueObjects
234
+ return obj;
253
235
  }
254
236
  }
255
237
 
@@ -259,7 +241,6 @@ export abstract class BaseEntity<T extends BaseProps> {
259
241
  return obj.map((item) => this.deepCloneProps(item, seen));
260
242
  }
261
243
 
262
- // Plain objects only
263
244
  if (obj.constructor === Object) {
264
245
  const cloned: any = {};
265
246
  for (const key in obj) {
@@ -270,7 +251,6 @@ export abstract class BaseEntity<T extends BaseProps> {
270
251
  return cloned;
271
252
  }
272
253
 
273
- // For other object types (custom classes), just keep the reference
274
254
  return obj;
275
255
  }
276
256
 
@@ -284,27 +264,20 @@ export abstract class BaseEntity<T extends BaseProps> {
284
264
 
285
265
  /**
286
266
  * Check equality with another entity by comparing IDs
287
- * Entities are equal if they have the same ID, regardless of other properties
288
- *
289
- * @param other - Another entity or an ID to compare with
290
- * @returns true if entities have the same ID
291
267
  */
292
268
  equals(other: BaseEntity<T> | Id | string): boolean {
293
269
  if (!other) {
294
270
  return false;
295
271
  }
296
272
 
297
- // Compare with another entity
298
273
  if (other instanceof BaseEntity) {
299
274
  return this.id.equals(other.id);
300
275
  }
301
276
 
302
- // Compare with an ID
303
277
  if (other instanceof Id) {
304
278
  return this.id.equals(other);
305
279
  }
306
280
 
307
- // Compare with a string ID
308
281
  if (typeof other === "string") {
309
282
  return this.id.equals(other);
310
283
  }
@@ -330,21 +303,37 @@ export abstract class BaseEntity<T extends BaseProps> {
330
303
  return (this as any)._validationError;
331
304
  }
332
305
 
333
- subscribe(config: SubscriptionConfig<T>): void {
334
- Object.keys(config).forEach((key) => {
335
- const sub = config[key as keyof T];
336
- if (sub && "onChange" in sub) {
337
- this.proxy.subscribe(key, sub.onChange);
338
- }
339
- });
306
+ /**
307
+ * Returns all detected changes as AggregateChanges.
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * const changes = user.getChanges();
312
+ * const batch = changes.toBatchOperations();
313
+ *
314
+ * for (const del of batch.deletes) { ... }
315
+ * for (const create of batch.creates) { ... }
316
+ * for (const upd of batch.updates) { ... }
317
+ * ```
318
+ */
319
+ getChanges<TEntityMap = Record<string, any>>(): AggregateChanges<TEntityMap> {
320
+ return this.tracker.getChanges<TEntityMap>();
340
321
  }
341
322
 
323
+ /**
324
+ * Returns the change history (for debugging).
325
+ */
342
326
  getHistory(): HistoryEntry[] {
343
- return this.proxy.getHistory();
327
+ return this.tracker.getHistory();
344
328
  }
345
329
 
346
- clearHistory(): void {
347
- this.proxy.clearHistory();
330
+ /**
331
+ * Clears history and marks entity as "clean".
332
+ * Call this after successfully persisting to the database.
333
+ */
334
+ markAsClean(): void {
335
+ this.tracker.markAsClean();
336
+ this.takeSnapshot();
348
337
  }
349
338
 
350
339
  /**
@@ -354,8 +343,12 @@ export abstract class BaseEntity<T extends BaseProps> {
354
343
  this.domainEvents.push(event);
355
344
  }
356
345
 
357
- public async dispatchAll(bus: DomainEventBus) {
346
+ /**
347
+ * Dispatch all events through the event bus
348
+ */
349
+ public async dispatchAll(bus: DomainEventBus): Promise<void> {
358
350
  await bus.publishAll(this.getUncommittedEvents());
351
+ this.clearEvents();
359
352
  }
360
353
 
361
354
  /**