@woltz/rich-domain 1.9.0 → 1.9.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 (144) hide show
  1. package/dist/cjs/core/aggregate-changes.d.ts +14 -0
  2. package/dist/cjs/core/aggregate-changes.d.ts.map +1 -1
  3. package/dist/cjs/core/aggregate-changes.js +18 -0
  4. package/dist/cjs/core/aggregate-changes.js.map +1 -1
  5. package/dist/cjs/core/base-entity.d.ts +2 -0
  6. package/dist/cjs/core/base-entity.d.ts.map +1 -1
  7. package/dist/cjs/core/base-entity.js +39 -41
  8. package/dist/cjs/core/base-entity.js.map +1 -1
  9. package/dist/cjs/core/change-tracker.d.ts +8 -0
  10. package/dist/cjs/core/change-tracker.d.ts.map +1 -1
  11. package/dist/cjs/core/change-tracker.js +36 -6
  12. package/dist/cjs/core/change-tracker.js.map +1 -1
  13. package/dist/cjs/core/domain-event.d.ts +3 -0
  14. package/dist/cjs/core/domain-event.d.ts.map +1 -1
  15. package/dist/cjs/core/domain-event.js +8 -1
  16. package/dist/cjs/core/domain-event.js.map +1 -1
  17. package/dist/cjs/core/value-object.d.ts.map +1 -1
  18. package/dist/cjs/core/value-object.js +3 -5
  19. package/dist/cjs/core/value-object.js.map +1 -1
  20. package/dist/cjs/criteria.d.ts +1 -1
  21. package/dist/cjs/index.d.ts +1 -1
  22. package/dist/cjs/index.d.ts.map +1 -1
  23. package/dist/cjs/repository/entity-schema-registry.d.ts +56 -3
  24. package/dist/cjs/repository/entity-schema-registry.d.ts.map +1 -1
  25. package/dist/cjs/repository/entity-schema-registry.js +61 -6
  26. package/dist/cjs/repository/entity-schema-registry.js.map +1 -1
  27. package/dist/cjs/types/index.d.ts +1 -0
  28. package/dist/cjs/types/index.d.ts.map +1 -1
  29. package/dist/cjs/types/index.js +1 -0
  30. package/dist/cjs/types/index.js.map +1 -1
  31. package/dist/cjs/types/outbox-store.d.ts +91 -0
  32. package/dist/cjs/types/outbox-store.d.ts.map +1 -0
  33. package/dist/cjs/types/outbox-store.js +3 -0
  34. package/dist/cjs/types/outbox-store.js.map +1 -0
  35. package/dist/cjs/utils/helpers.d.ts +1 -0
  36. package/dist/cjs/utils/helpers.d.ts.map +1 -1
  37. package/dist/cjs/utils/helpers.js +4 -0
  38. package/dist/cjs/utils/helpers.js.map +1 -1
  39. package/dist/esm/core/aggregate-changes.d.ts +14 -0
  40. package/dist/esm/core/aggregate-changes.d.ts.map +1 -1
  41. package/dist/esm/core/aggregate-changes.js +18 -0
  42. package/dist/esm/core/aggregate-changes.js.map +1 -1
  43. package/dist/esm/core/base-entity.d.ts +2 -0
  44. package/dist/esm/core/base-entity.d.ts.map +1 -1
  45. package/dist/esm/core/base-entity.js +37 -39
  46. package/dist/esm/core/base-entity.js.map +1 -1
  47. package/dist/esm/core/change-tracker.d.ts +8 -0
  48. package/dist/esm/core/change-tracker.d.ts.map +1 -1
  49. package/dist/esm/core/change-tracker.js +36 -6
  50. package/dist/esm/core/change-tracker.js.map +1 -1
  51. package/dist/esm/core/domain-event.d.ts +3 -0
  52. package/dist/esm/core/domain-event.d.ts.map +1 -1
  53. package/dist/esm/core/domain-event.js +5 -1
  54. package/dist/esm/core/domain-event.js.map +1 -1
  55. package/dist/esm/core/value-object.d.ts.map +1 -1
  56. package/dist/esm/core/value-object.js +1 -3
  57. package/dist/esm/core/value-object.js.map +1 -1
  58. package/dist/esm/criteria.d.ts +1 -1
  59. package/dist/esm/index.d.ts +1 -1
  60. package/dist/esm/index.d.ts.map +1 -1
  61. package/dist/esm/repository/entity-schema-registry.d.ts +56 -3
  62. package/dist/esm/repository/entity-schema-registry.d.ts.map +1 -1
  63. package/dist/esm/repository/entity-schema-registry.js +61 -6
  64. package/dist/esm/repository/entity-schema-registry.js.map +1 -1
  65. package/dist/esm/types/index.d.ts +1 -0
  66. package/dist/esm/types/index.d.ts.map +1 -1
  67. package/dist/esm/types/index.js +1 -0
  68. package/dist/esm/types/index.js.map +1 -1
  69. package/dist/esm/types/outbox-store.d.ts +91 -0
  70. package/dist/esm/types/outbox-store.d.ts.map +1 -0
  71. package/dist/esm/types/outbox-store.js +2 -0
  72. package/dist/esm/types/outbox-store.js.map +1 -0
  73. package/dist/esm/utils/helpers.d.ts +1 -0
  74. package/dist/esm/utils/helpers.d.ts.map +1 -1
  75. package/dist/esm/utils/helpers.js +3 -0
  76. package/dist/esm/utils/helpers.js.map +1 -1
  77. package/dist/tsconfig.cjs.tsbuildinfo +1 -1
  78. package/dist/tsconfig.esm.tsbuildinfo +1 -1
  79. package/dist/tsconfig.types.tsbuildinfo +1 -1
  80. package/dist/types/core/aggregate-changes.d.ts +14 -0
  81. package/dist/types/core/aggregate-changes.d.ts.map +1 -1
  82. package/dist/types/core/base-entity.d.ts +2 -0
  83. package/dist/types/core/base-entity.d.ts.map +1 -1
  84. package/dist/types/core/change-tracker.d.ts +8 -0
  85. package/dist/types/core/change-tracker.d.ts.map +1 -1
  86. package/dist/types/core/domain-event.d.ts +3 -0
  87. package/dist/types/core/domain-event.d.ts.map +1 -1
  88. package/dist/types/core/value-object.d.ts.map +1 -1
  89. package/dist/types/criteria.d.ts +1 -1
  90. package/dist/types/index.d.ts +1 -1
  91. package/dist/types/index.d.ts.map +1 -1
  92. package/dist/types/repository/entity-schema-registry.d.ts +56 -3
  93. package/dist/types/repository/entity-schema-registry.d.ts.map +1 -1
  94. package/dist/types/types/index.d.ts +1 -0
  95. package/dist/types/types/index.d.ts.map +1 -1
  96. package/dist/types/types/outbox-store.d.ts +91 -0
  97. package/dist/types/types/outbox-store.d.ts.map +1 -0
  98. package/dist/types/utils/helpers.d.ts +1 -0
  99. package/dist/types/utils/helpers.d.ts.map +1 -1
  100. package/package.json +68 -67
  101. package/src/constants.ts +82 -0
  102. package/src/core/aggregate-changes.ts +466 -0
  103. package/src/core/base-aggregate.ts +76 -0
  104. package/src/core/base-entity.ts +552 -0
  105. package/src/core/change-tracker.ts +1327 -0
  106. package/src/core/domain-event.ts +41 -0
  107. package/src/core/entity-changes.ts +146 -0
  108. package/src/core/entity.ts +13 -0
  109. package/src/core/id.ts +124 -0
  110. package/src/core/index.ts +9 -0
  111. package/src/core/value-object.ts +179 -0
  112. package/src/criteria.ts +574 -0
  113. package/src/exceptions.ts +549 -0
  114. package/src/index.ts +74 -0
  115. package/src/repository/base-repository.ts +81 -0
  116. package/src/repository/entity-schema-registry.ts +620 -0
  117. package/src/repository/index.ts +5 -0
  118. package/src/repository/mapper.ts +7 -0
  119. package/src/repository/paginated-result.ts +251 -0
  120. package/src/repository/unit-of-work.ts +76 -0
  121. package/src/types/change-tracker.ts +268 -0
  122. package/src/types/criteria.ts +197 -0
  123. package/src/types/domain-event.ts +29 -0
  124. package/src/types/domain.ts +41 -0
  125. package/src/types/event-bus.ts +17 -0
  126. package/src/types/index.ts +9 -0
  127. package/src/types/outbox-store.ts +97 -0
  128. package/src/types/standard-schema.ts +19 -0
  129. package/src/types/unit-of-work.ts +46 -0
  130. package/src/types/utils.ts +24 -0
  131. package/src/utils/criteria-operator-validation.ts +209 -0
  132. package/src/utils/crypto.ts +31 -0
  133. package/src/utils/helpers.ts +50 -0
  134. package/src/validation-error.ts +219 -0
  135. package/dist/cjs/t.d.ts +0 -2
  136. package/dist/cjs/t.d.ts.map +0 -1
  137. package/dist/cjs/t.js +0 -96
  138. package/dist/cjs/t.js.map +0 -1
  139. package/dist/esm/t.d.ts +0 -2
  140. package/dist/esm/t.d.ts.map +0 -1
  141. package/dist/esm/t.js +0 -94
  142. package/dist/esm/t.js.map +0 -1
  143. package/dist/types/t.d.ts +0 -2
  144. package/dist/types/t.d.ts.map +0 -1
@@ -0,0 +1,1327 @@
1
+ import { Id } from "./id.js";
2
+ import { Entity } from "./entity.js";
3
+ import { ValueObject } from "./value-object.js";
4
+ import { ArrayState, HistoryEntry, TrackedItem } from "../types/index.js";
5
+ import { EntityChangeState } from "../types/change-tracker.js";
6
+ import { AggregateChanges } from "./aggregate-changes.js";
7
+ import { DomainError } from "../exceptions.js";
8
+
9
+ /**
10
+ * Callback for validation on property change.
11
+ * Return false to reject the change, or throw an error.
12
+ */
13
+ export type OnChangeValidator = (path: string, newValue: any) => boolean | void;
14
+
15
+ /**
16
+ * Tracks changes in Aggregates using Proxy.
17
+ *
18
+ * Features:
19
+ * - Tracks changes in primitive properties
20
+ * - Tracks changes in nested entities (1:1)
21
+ * - Tracks changes in collections (1:N)
22
+ * - Calculates depth automatically
23
+ * - Generates AggregateChanges for persistence
24
+ * - Supports validation on change via onChangeValidator
25
+ */
26
+ export class ChangeTracker {
27
+ private history: HistoryEntry[] = [];
28
+ private originalValues: Map<string, any> = new Map();
29
+ private trackedArrays: Map<string, ArrayState> = new Map();
30
+ private trackedEntities: Map<string, TrackedItem> = new Map();
31
+ private onChangeValidator?: OnChangeValidator;
32
+
33
+ constructor(
34
+ private target: any,
35
+ private rootEntityName: string,
36
+ private path: string = "",
37
+ private depth: number = 0,
38
+ // @ts-expect-error - This is a private property
39
+ private parentId?: string,
40
+ // @ts-expect-error - This is a private property
41
+ private parentEntity?: string,
42
+ private rootTracker?: ChangeTracker
43
+ ) {
44
+ if (!rootTracker) {
45
+ this.rootTracker = this;
46
+ }
47
+ this.captureInitialState();
48
+ }
49
+
50
+ /**
51
+ * Sets a validator callback that will be called on every property change.
52
+ * The validator can:
53
+ * - Return false to reject the change (value will be reverted)
54
+ * - Throw an error to reject the change with an error
55
+ * - Return true/undefined to accept the change
56
+ */
57
+ setOnChangeValidator(validator: OnChangeValidator): void {
58
+ this.getRootTracker().onChangeValidator = validator;
59
+ }
60
+
61
+ private captureInitialState(): void {
62
+ if (this.depth > 0) return;
63
+ this.captureEntityState(this.target, this.rootEntityName, "", 0);
64
+ }
65
+
66
+ private captureEntityState(
67
+ obj: any,
68
+ entityName: string,
69
+ path: string,
70
+ depth: number,
71
+ parentId?: string,
72
+ parentEntity?: string
73
+ ): void {
74
+ if (!obj || typeof obj !== "object") return;
75
+
76
+ const id = this.getEntityId(obj);
77
+ const key = path || "root";
78
+
79
+ this.trackedEntities.set(key, {
80
+ entity: obj,
81
+ metadata: {
82
+ entityName,
83
+ depth,
84
+ parentId,
85
+ parentEntity,
86
+ path,
87
+ },
88
+ originalState: this.deepClone(obj),
89
+ });
90
+
91
+ const propsToScan = obj.props || obj;
92
+
93
+ for (const [propName, value] of Object.entries(propsToScan)) {
94
+ if (propName === "id") continue;
95
+
96
+ const propPath = path ? `${path}.${propName}` : propName;
97
+
98
+ if (Array.isArray(value)) {
99
+ this.captureArrayState(value, propPath, depth + 1, id, entityName);
100
+ } else if (value instanceof Entity) {
101
+ const nestedName = this.getEntityName(value);
102
+ this.captureEntityState(
103
+ value,
104
+ nestedName,
105
+ propPath,
106
+ depth + 1,
107
+ id,
108
+ entityName
109
+ );
110
+ }
111
+ }
112
+ }
113
+
114
+ private captureArrayState(
115
+ arr: any[],
116
+ path: string,
117
+ depth: number,
118
+ parentId?: string,
119
+ parentEntity?: string
120
+ ): void {
121
+ const isPrimitive = this.isPrimitiveArray(arr);
122
+ const entityName = arr.length > 0 ? this.getEntityName(arr[0]) : "Unknown";
123
+
124
+ this.trackedArrays.set(path, {
125
+ cloned: this.cloneArray(arr),
126
+ original: arr.slice(),
127
+ metadata: {
128
+ entityName,
129
+ depth,
130
+ parentId,
131
+ parentEntity,
132
+ path,
133
+ },
134
+ isPrimitiveArray: isPrimitive,
135
+ });
136
+
137
+ // Only track individual items for non-primitive arrays
138
+ if (!isPrimitive) {
139
+ arr.forEach((item, index) => {
140
+ if (item instanceof Entity) {
141
+ const itemPath = `${path}[${index}]`;
142
+ this.captureEntityState(
143
+ item,
144
+ this.getEntityName(item),
145
+ itemPath,
146
+ depth,
147
+ parentId,
148
+ parentEntity
149
+ );
150
+ }
151
+ });
152
+ }
153
+ }
154
+
155
+ createProxy(): any {
156
+ const handler: ProxyHandler<any> = {
157
+ get: (target, prop, receiver) => {
158
+ const value = Reflect.get(target, prop, receiver);
159
+
160
+ if (typeof prop === "symbol") {
161
+ return value;
162
+ }
163
+
164
+ if (this.shouldSkipProperty(prop)) {
165
+ return value;
166
+ }
167
+
168
+ if (typeof value === "function") {
169
+ return value.bind(target);
170
+ }
171
+
172
+ const currentPath = this.buildPath(String(prop));
173
+
174
+ if (Array.isArray(value)) {
175
+ return this.createArrayProxy(value, currentPath);
176
+ }
177
+
178
+ if (value instanceof Entity) {
179
+ const nestedTracker = new ChangeTracker(
180
+ value,
181
+ this.getEntityName(value),
182
+ currentPath,
183
+ this.depth + 1,
184
+ this.getEntityId(this.target),
185
+ this.rootEntityName,
186
+ this.rootTracker
187
+ );
188
+ return nestedTracker.createProxy();
189
+ }
190
+
191
+ return value;
192
+ },
193
+
194
+ set: (target, prop, newValue, receiver) => {
195
+ if (typeof prop === "symbol") {
196
+ return Reflect.set(target, prop, newValue, receiver);
197
+ }
198
+
199
+ const currentPath = this.buildPath(String(prop));
200
+ const oldValue = Reflect.get(target, prop, receiver);
201
+
202
+ if (!Array.isArray(newValue) && oldValue === newValue) {
203
+ return true;
204
+ }
205
+
206
+ const rootTracker = this.getRootTracker();
207
+ if (rootTracker.onChangeValidator) {
208
+ try {
209
+ const result = rootTracker.onChangeValidator(currentPath, newValue);
210
+ if (result === false) {
211
+ return true;
212
+ }
213
+ } catch (error) {
214
+ throw error;
215
+ }
216
+ }
217
+
218
+ if (!rootTracker.originalValues.has(currentPath)) {
219
+ rootTracker.originalValues.set(currentPath, oldValue);
220
+ }
221
+
222
+ rootTracker.history.push({
223
+ path: currentPath,
224
+ previousValue: oldValue,
225
+ currentValue: newValue,
226
+ timestamp: Date.now(),
227
+ });
228
+
229
+ const result = Reflect.set(target, prop, newValue, receiver);
230
+
231
+ if (Array.isArray(newValue)) {
232
+ this.handleArrayAssignment(currentPath, oldValue);
233
+ } else if (newValue instanceof Entity || oldValue instanceof Entity) {
234
+ this.handleEntityChange(currentPath, oldValue, newValue);
235
+ }
236
+
237
+ return result;
238
+ },
239
+
240
+ deleteProperty: (target, prop) => {
241
+ if (typeof prop === "symbol") {
242
+ return Reflect.deleteProperty(target, prop);
243
+ }
244
+
245
+ const currentPath = this.buildPath(String(prop));
246
+ const oldValue = Reflect.get(target, prop);
247
+
248
+ if (!(prop in target)) {
249
+ return true;
250
+ }
251
+
252
+ const rootTracker = this.getRootTracker();
253
+ if (rootTracker.onChangeValidator) {
254
+ try {
255
+ const result = rootTracker.onChangeValidator(
256
+ currentPath,
257
+ undefined
258
+ );
259
+ if (result === false) {
260
+ return true;
261
+ }
262
+ } catch (error) {
263
+ throw error;
264
+ }
265
+ }
266
+
267
+ if (!rootTracker.originalValues.has(currentPath)) {
268
+ rootTracker.originalValues.set(currentPath, oldValue);
269
+ }
270
+
271
+ rootTracker.history.push({
272
+ path: currentPath,
273
+ previousValue: oldValue,
274
+ currentValue: undefined,
275
+ timestamp: Date.now(),
276
+ });
277
+
278
+ const result = Reflect.deleteProperty(target, prop);
279
+
280
+ if (oldValue instanceof Entity) {
281
+ this.handleEntityChange(currentPath, oldValue, undefined);
282
+ }
283
+
284
+ return result;
285
+ },
286
+ };
287
+
288
+ const proxy = new Proxy(this.target, handler);
289
+ Object.defineProperty(proxy, "__isProxy", { value: true, writable: false });
290
+ return proxy;
291
+ }
292
+
293
+ private createArrayProxy(array: any[], path: string): any[] {
294
+ const tracker = this;
295
+ const rootTracker = this.getRootTracker();
296
+
297
+ if (!rootTracker.trackedArrays.has(path)) {
298
+ const parentId = this.getEntityId(this.target);
299
+ rootTracker.captureArrayState(
300
+ array,
301
+ path,
302
+ this.depth + 1,
303
+ parentId,
304
+ this.rootEntityName
305
+ );
306
+ }
307
+
308
+ return new Proxy(array, {
309
+ get(target, prop, receiver) {
310
+ const value = Reflect.get(target, prop, receiver);
311
+
312
+ if (typeof prop === "symbol") {
313
+ return value;
314
+ }
315
+
316
+ if (typeof value === "function") {
317
+ const mutatingMethods = [
318
+ "push",
319
+ "pop",
320
+ "shift",
321
+ "unshift",
322
+ "splice",
323
+ "sort",
324
+ "reverse",
325
+ ];
326
+
327
+ if (mutatingMethods.includes(String(prop))) {
328
+ return function (...args: any[]) {
329
+ const oldArray = target.slice();
330
+ const result = value.apply(target, args);
331
+ const newArray = target.slice();
332
+
333
+ if (rootTracker.onChangeValidator) {
334
+ try {
335
+ const validatorResult = rootTracker.onChangeValidator(
336
+ path,
337
+ newArray
338
+ );
339
+ if (validatorResult === false) {
340
+ target.splice(0, target.length, ...oldArray);
341
+ return undefined;
342
+ }
343
+ } catch (error) {
344
+ target.splice(0, target.length, ...oldArray);
345
+ throw error;
346
+ }
347
+ }
348
+
349
+ rootTracker.history.push({
350
+ path,
351
+ previousValue: oldArray,
352
+ currentValue: newArray,
353
+ timestamp: Date.now(),
354
+ });
355
+
356
+ return result;
357
+ };
358
+ }
359
+ return value.bind(target);
360
+ }
361
+
362
+ if (!isNaN(Number(prop)) && value instanceof Entity) {
363
+ const nestedPath = `${path}[${String(prop)}]`;
364
+ const nestedTracker = new ChangeTracker(
365
+ value,
366
+ tracker.getEntityName(value),
367
+ nestedPath,
368
+ tracker.depth + 1,
369
+ tracker.getEntityId(tracker.target),
370
+ tracker.rootEntityName,
371
+ rootTracker
372
+ );
373
+ return nestedTracker.createProxy();
374
+ }
375
+
376
+ return value;
377
+ },
378
+
379
+ set(target, prop, newValue, receiver) {
380
+ if (typeof prop === "symbol") {
381
+ return Reflect.set(target, prop, newValue, receiver);
382
+ }
383
+
384
+ if (!isNaN(Number(prop))) {
385
+ const oldArray = target.slice();
386
+
387
+ if (rootTracker.onChangeValidator) {
388
+ try {
389
+ const result = rootTracker.onChangeValidator(path, newValue);
390
+ if (result === false) {
391
+ return true;
392
+ }
393
+ } catch (error) {
394
+ throw error;
395
+ }
396
+ }
397
+
398
+ const result = Reflect.set(target, prop, newValue, receiver);
399
+
400
+ rootTracker.history.push({
401
+ path,
402
+ previousValue: oldArray,
403
+ currentValue: target.slice(),
404
+ timestamp: Date.now(),
405
+ });
406
+
407
+ return result;
408
+ }
409
+ return Reflect.set(target, prop, newValue, receiver);
410
+ },
411
+ });
412
+ }
413
+
414
+ /**
415
+ * Returns all detected changes as AggregateChanges.
416
+ */
417
+ getChanges<TEntityMap = Record<string, any>>(): AggregateChanges<TEntityMap> {
418
+ const changes = new AggregateChanges<TEntityMap>();
419
+ const rootTracker = this.getRootTracker();
420
+
421
+ // Collect all root-level changes (primitive fields + primitive arrays)
422
+ const rootChangedFields = this.collectRootChanges(rootTracker);
423
+
424
+ if (Object.keys(rootChangedFields).length > 0) {
425
+ const id = this.getEntityId(this.target);
426
+ if (id) {
427
+ changes.addUpdate(
428
+ this.rootEntityName,
429
+ id,
430
+ this.target,
431
+ rootChangedFields,
432
+ 0
433
+ );
434
+ }
435
+ }
436
+
437
+ this.analyzeCollectionChanges(changes, rootTracker);
438
+ this.analyzeEntityChanges(changes, rootTracker);
439
+
440
+ return changes;
441
+ }
442
+
443
+ /**
444
+ * Collects all root-level changes: primitive properties and primitive arrays.
445
+ */
446
+ private collectRootChanges(rootTracker: ChangeTracker): Record<string, any> {
447
+ const changedFields: Record<string, any> = {};
448
+
449
+ // Collect primitive property changes
450
+ for (const [path, originalValue] of rootTracker.originalValues) {
451
+ if (path.includes(".") || path.includes("[")) continue;
452
+
453
+ const currentValue = this.target[path];
454
+
455
+ // 1:1 entity relations are tracked by analyzeEntityChanges
456
+ if (rootTracker.trackedEntities.has(path)) continue;
457
+ if (originalValue instanceof Entity || currentValue instanceof Entity) {
458
+ continue;
459
+ }
460
+
461
+ if (!this.isEqual(originalValue, currentValue)) {
462
+ changedFields[path] =
463
+ currentValue instanceof ValueObject
464
+ ? currentValue.value
465
+ : currentValue;
466
+ }
467
+ }
468
+
469
+ // Collect primitive array changes
470
+ for (const [path, arrayState] of rootTracker.trackedArrays) {
471
+ if (path.includes(".")) continue; // Only root-level arrays
472
+
473
+ const currentArray = this.getValueAtPath(this.target, path);
474
+ if (!Array.isArray(currentArray)) continue;
475
+ if (!this.shouldTreatArrayAsPrimitive(currentArray, arrayState)) continue;
476
+
477
+ const originalArray = arrayState.cloned;
478
+
479
+ if (!this.arraysEqual(originalArray, currentArray)) {
480
+ changedFields[path] = currentArray.slice();
481
+ }
482
+ }
483
+
484
+ return changedFields;
485
+ }
486
+
487
+ /**
488
+ * Compares two arrays for equality (shallow comparison for primitives).
489
+ */
490
+ private arraysEqual(a: any[], b: any[]): boolean {
491
+ if (a.length !== b.length) return false;
492
+ for (let i = 0; i < a.length; i++) {
493
+ if (a[i] !== b[i]) return false;
494
+ }
495
+ return true;
496
+ }
497
+
498
+ private analyzeCollectionChanges(
499
+ changes: AggregateChanges<any>,
500
+ rootTracker: ChangeTracker
501
+ ): void {
502
+ const allTrackedArrays = new Map<string, ArrayState>();
503
+ const processedArrays = new Set<any>();
504
+
505
+ for (const [path, arrayState] of rootTracker.trackedArrays) {
506
+ const currentArray = this.getValueAtPath(this.target, path);
507
+ if (Array.isArray(currentArray) && !processedArrays.has(currentArray)) {
508
+ allTrackedArrays.set(path, arrayState);
509
+ processedArrays.add(currentArray);
510
+ }
511
+ }
512
+
513
+ this.collectNestedArrays(
514
+ this.target,
515
+ "",
516
+ allTrackedArrays,
517
+ processedArrays
518
+ );
519
+
520
+ for (const [path, arrayState] of allTrackedArrays) {
521
+ const currentArray = this.getValueAtPath(this.target, path);
522
+ if (!Array.isArray(currentArray)) continue;
523
+
524
+ // Skip primitive arrays - they are handled as property changes on the parent entity
525
+ if (this.shouldTreatArrayAsPrimitive(currentArray, arrayState)) continue;
526
+
527
+ const { created, updated, deleted } = this.detectArrayChanges(
528
+ arrayState.cloned,
529
+ arrayState.original,
530
+ currentArray
531
+ );
532
+
533
+ const { depth, parentId, parentEntity } = arrayState.metadata;
534
+
535
+ const relationField = this.extractRelationField(path);
536
+
537
+ for (const item of created) {
538
+ const itemEntityName = this.getEntityName(item);
539
+ changes.addCreate(
540
+ itemEntityName,
541
+ item,
542
+ depth,
543
+ parentId,
544
+ parentEntity,
545
+ relationField
546
+ );
547
+
548
+ this.markNestedItemsAsCreated(item, depth, changes);
549
+ }
550
+
551
+ for (const item of updated) {
552
+ const id = this.getEntityId(item);
553
+ if (id) {
554
+ const original = arrayState.cloned.find(
555
+ (o) => this.getEntityId(o) === id
556
+ );
557
+ const changedFields = this.detectChangedFields(original, item);
558
+ if (Object.keys(changedFields).length > 0) {
559
+ const itemEntityName = this.getEntityName(item);
560
+ changes.addUpdate(itemEntityName, id, item, changedFields, depth);
561
+ }
562
+ }
563
+ }
564
+
565
+ for (const item of deleted) {
566
+ const id = this.getEntityId(item);
567
+ const key = this.getItemKey(item);
568
+ if (id || key) {
569
+ const itemEntityName = this.getEntityName(item);
570
+ const deleteId = id || key!;
571
+ changes.addDelete(
572
+ itemEntityName,
573
+ deleteId,
574
+ item,
575
+ depth,
576
+ relationField,
577
+ parentId,
578
+ parentEntity
579
+ );
580
+
581
+ this.markNestedItemsAsDeleted(item, depth, changes, rootTracker);
582
+ }
583
+ }
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Recursively marks all nested items as created when a parent is created.
589
+ */
590
+ private markNestedItemsAsCreated(
591
+ item: any,
592
+ parentDepth: number,
593
+ changes: AggregateChanges<any>
594
+ ): void {
595
+ if (!item || typeof item !== "object") return;
596
+
597
+ const propsToScan = item.props || item;
598
+ const parentId = this.getEntityId(item);
599
+ const parentEntity = this.getEntityName(item);
600
+
601
+ for (const [propName, value] of Object.entries(propsToScan)) {
602
+ if (propName === "id") continue;
603
+
604
+ if (Array.isArray(value)) {
605
+ const relationField = propName;
606
+
607
+ for (const child of value) {
608
+ if (child instanceof Entity) {
609
+ const childEntityName = this.getEntityName(child);
610
+ changes.addCreate(
611
+ childEntityName,
612
+ child,
613
+ parentDepth + 1,
614
+ parentId,
615
+ parentEntity,
616
+ relationField
617
+ );
618
+ this.markNestedItemsAsCreated(child, parentDepth + 1, changes);
619
+ }
620
+ }
621
+ } else if (value instanceof Entity) {
622
+ const childEntityName = this.getEntityName(value);
623
+ changes.addCreate(
624
+ childEntityName,
625
+ value,
626
+ parentDepth + 1,
627
+ parentId,
628
+ parentEntity,
629
+ propName
630
+ );
631
+ this.markNestedItemsAsCreated(value, parentDepth + 1, changes);
632
+ }
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Recursively marks all nested items as deleted when a parent is deleted.
638
+ * Uses the original captured state to find nested items.
639
+ */
640
+ private markNestedItemsAsDeleted(
641
+ item: any,
642
+ parentDepth: number,
643
+ changes: AggregateChanges<any>,
644
+ rootTracker: ChangeTracker
645
+ ): void {
646
+ if (!item || typeof item !== "object") return;
647
+
648
+ const itemId = this.getEntityId(item);
649
+ if (!itemId) return;
650
+
651
+ for (const [path, arrayState] of rootTracker.trackedArrays) {
652
+ if (arrayState.metadata.parentId === itemId) {
653
+ const relationField = this.extractRelationField(path);
654
+ const parentEntity = arrayState.metadata.parentEntity;
655
+ const parentId = arrayState.metadata.parentId;
656
+
657
+ for (const nestedItem of arrayState.cloned) {
658
+ const id =
659
+ typeof nestedItem === "object" && nestedItem !== null
660
+ ? nestedItem.id
661
+ : undefined;
662
+ if (id) {
663
+ const entityName = arrayState.metadata.entityName;
664
+ changes.addDelete(
665
+ entityName,
666
+ id,
667
+ nestedItem,
668
+ parentDepth + 1,
669
+ relationField,
670
+ parentEntity,
671
+ parentId
672
+ );
673
+
674
+ this.markNestedJsonItemAsDeleted(
675
+ id,
676
+ parentDepth + 1,
677
+ changes,
678
+ rootTracker
679
+ );
680
+ }
681
+ }
682
+ }
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Recursively marks nested items as deleted from a JSON object.
688
+ * This is used when processing cloned (JSON) state.
689
+ */
690
+ private markNestedJsonItemAsDeleted(
691
+ itemId: string,
692
+ parentDepth: number,
693
+ changes: AggregateChanges<any>,
694
+ rootTracker: ChangeTracker
695
+ ): void {
696
+ for (const [path, arrayState] of rootTracker.trackedArrays) {
697
+ if (arrayState.metadata.parentId === itemId) {
698
+ const relationField = this.extractRelationField(path);
699
+
700
+ for (const nestedJsonItem of arrayState.cloned) {
701
+ if (typeof nestedJsonItem !== "object" || nestedJsonItem === null)
702
+ continue;
703
+
704
+ const nestedId = nestedJsonItem.id;
705
+ const entityName = arrayState.metadata.entityName;
706
+ const parentEntity = arrayState.metadata.parentEntity;
707
+ const parentId = arrayState.metadata.parentId;
708
+
709
+ if (nestedId) {
710
+ changes.addDelete(
711
+ entityName,
712
+ nestedId,
713
+ nestedJsonItem,
714
+ parentDepth + 1,
715
+ relationField,
716
+ parentId,
717
+ parentEntity
718
+ );
719
+
720
+ this.markNestedJsonItemAsDeleted(
721
+ nestedId,
722
+ parentDepth + 1,
723
+ changes,
724
+ rootTracker
725
+ );
726
+ } else {
727
+ const key = this.extractIdentityKeyFromJson(
728
+ nestedJsonItem,
729
+ arrayState.original
730
+ );
731
+ if (key) {
732
+ changes.addDelete(
733
+ entityName,
734
+ key,
735
+ nestedJsonItem,
736
+ parentDepth + 1,
737
+ relationField,
738
+ parentId,
739
+ parentEntity
740
+ );
741
+ }
742
+ }
743
+ }
744
+ }
745
+ }
746
+ }
747
+
748
+ /**
749
+ * Extracts identity key from a JSON object by looking at the original Entity instances.
750
+ */
751
+ private extractIdentityKeyFromJson(
752
+ jsonItem: any,
753
+ originalArray: any[]
754
+ ): string | undefined {
755
+ for (const originalItem of originalArray) {
756
+ if (originalItem instanceof Entity) {
757
+ const originalJson = this.deepClone(originalItem);
758
+ if (JSON.stringify(originalJson) === JSON.stringify(jsonItem)) {
759
+ const key = this.getItemKey(originalItem);
760
+ if (key) return key;
761
+ }
762
+ }
763
+ }
764
+
765
+ if (jsonItem.id) return jsonItem.id;
766
+
767
+ return undefined;
768
+ }
769
+
770
+ private collectNestedArrays(
771
+ obj: any,
772
+ basePath: string,
773
+ allArrays: Map<string, ArrayState>,
774
+ processedArrays: Set<any>
775
+ ): void {
776
+ if (!obj || typeof obj !== "object") return;
777
+
778
+ for (const [propName, value] of Object.entries(obj)) {
779
+ if (propName === "id" || propName === "proxy" || propName === "_props")
780
+ continue;
781
+
782
+ const propPath = basePath ? `${basePath}.${propName}` : propName;
783
+
784
+ if (Array.isArray(value)) {
785
+ value.forEach((item, index) => {
786
+ if (item instanceof Entity) {
787
+ this.collectNestedArrays(
788
+ item,
789
+ `${propPath}[${index}]`,
790
+ allArrays,
791
+ processedArrays
792
+ );
793
+ }
794
+ });
795
+ } else if (value instanceof Entity) {
796
+ this.collectNestedArrays(value, propPath, allArrays, processedArrays);
797
+ }
798
+ }
799
+ }
800
+
801
+ private analyzeEntityChanges(
802
+ changes: AggregateChanges<any>,
803
+ rootTracker: ChangeTracker
804
+ ): void {
805
+ for (const [path, trackedItem] of rootTracker.trackedEntities) {
806
+ if (path === "root") continue;
807
+ if (path.includes("[")) continue;
808
+
809
+ const currentValue = this.getValueAtPath(this.target, path);
810
+ const originalValue = trackedItem.originalState;
811
+ const originalEntity = trackedItem.entity;
812
+ const { entityName, depth, parentId, parentEntity } =
813
+ trackedItem.metadata;
814
+
815
+ const relationField = this.extractRelationField(path);
816
+
817
+ const state = this.detectEntityChangeState(originalValue, currentValue);
818
+
819
+ switch (state) {
820
+ case "created":
821
+ changes.addCreate(
822
+ entityName,
823
+ currentValue,
824
+ depth,
825
+ parentId,
826
+ parentEntity,
827
+ relationField
828
+ );
829
+ break;
830
+
831
+ case "deleted":
832
+ const id = this.getEntityId(originalValue);
833
+ if (id) {
834
+ changes.addDelete(
835
+ entityName,
836
+ id,
837
+ originalEntity,
838
+ depth,
839
+ relationField,
840
+ parentId,
841
+ parentEntity
842
+ );
843
+ }
844
+ break;
845
+
846
+ case "replaced":
847
+ const oldId = this.getEntityId(originalValue);
848
+ if (oldId) {
849
+ changes.addDelete(
850
+ entityName,
851
+ oldId,
852
+ originalEntity,
853
+ depth,
854
+ relationField,
855
+ parentId,
856
+ parentEntity
857
+ );
858
+ }
859
+ if (!this.isAbsent(currentValue)) {
860
+ changes.addCreate(
861
+ entityName,
862
+ currentValue,
863
+ depth,
864
+ parentId,
865
+ parentEntity,
866
+ relationField
867
+ );
868
+ }
869
+ break;
870
+
871
+ case "updated":
872
+ const updateId = this.getEntityId(currentValue);
873
+ if (updateId) {
874
+ const changedFields = this.detectChangedFields(
875
+ originalValue,
876
+ currentValue
877
+ );
878
+ if (Object.keys(changedFields).length > 0) {
879
+ changes.addUpdate(
880
+ entityName,
881
+ updateId,
882
+ currentValue,
883
+ changedFields,
884
+ depth
885
+ );
886
+ }
887
+ }
888
+ break;
889
+ }
890
+ }
891
+ }
892
+
893
+ private detectEntityChangeState(
894
+ previous: any,
895
+ current: any
896
+ ): EntityChangeState {
897
+ const hadPrevious = !this.isAbsent(previous);
898
+ const hasCurrent = !this.isAbsent(current);
899
+
900
+ if (!hadPrevious && hasCurrent) {
901
+ return "created";
902
+ }
903
+
904
+ if (hadPrevious && !hasCurrent) {
905
+ return "deleted";
906
+ }
907
+
908
+ if (hadPrevious && hasCurrent) {
909
+ const prevId = this.getEntityId(previous);
910
+ const currId = this.getEntityId(current);
911
+
912
+ if (prevId && currId && prevId === currId) {
913
+ return this.hasChanged(previous, current) ? "updated" : "unchanged";
914
+ } else {
915
+ return "replaced";
916
+ }
917
+ }
918
+
919
+ return "unchanged";
920
+ }
921
+
922
+ private isAbsent(value: any): boolean {
923
+ return value === null || value === undefined;
924
+ }
925
+
926
+ private detectArrayChanges(
927
+ oldCloned: any[],
928
+ oldOriginal: any[],
929
+ newArray: any[]
930
+ ): { created: any[]; updated: any[]; deleted: any[] } {
931
+ const created: any[] = [];
932
+ const updated: any[] = [];
933
+ const deleted: any[] = [];
934
+
935
+ const oldMap = new Map<string, any>();
936
+ const newMap = new Map<string, any>();
937
+
938
+ oldCloned.forEach((item) => {
939
+ const key = this.getItemKey(item);
940
+ if (key) oldMap.set(key, item);
941
+ });
942
+
943
+ newArray.forEach((item) => {
944
+ const key = this.getItemKey(item);
945
+ if (key) newMap.set(key, item);
946
+ });
947
+
948
+ newArray.forEach((item) => {
949
+ if (this.isPrimitiveValue(item)) return;
950
+
951
+ const key = this.getItemKey(item);
952
+ if (!key) {
953
+ created.push(item);
954
+ } else if (!oldMap.has(key)) {
955
+ created.push(item);
956
+ } else if (this.hasChanged(oldMap.get(key), item)) {
957
+ updated.push(item);
958
+ }
959
+ });
960
+
961
+ oldOriginal.forEach((item) => {
962
+ if (this.isPrimitiveValue(item)) return;
963
+
964
+ const key = this.getItemKey(item);
965
+ if (key && !newMap.has(key)) {
966
+ deleted.push(item);
967
+ }
968
+ });
969
+
970
+ return { created, updated, deleted };
971
+ }
972
+
973
+ private detectChangedFields(
974
+ original: any,
975
+ current: any
976
+ ): Record<string, any> {
977
+ const changes: Record<string, any> = {};
978
+
979
+ if (!original || !current) return changes;
980
+
981
+ const origProps = original.props || original;
982
+ const currProps = current.props || current;
983
+
984
+ for (const key of Object.keys(currProps)) {
985
+ if (key === "id") continue;
986
+
987
+ const origValue = origProps[key];
988
+ const currValue = currProps[key];
989
+
990
+ if (Array.isArray(currValue)) {
991
+ const origArray = Array.isArray(origValue) ? origValue : [];
992
+ if (
993
+ this.isPrimitiveArray(currValue) ||
994
+ this.isPrimitiveArray(origArray)
995
+ ) {
996
+ if (!this.arraysEqual(origArray, currValue)) {
997
+ changes[key] = currValue.slice();
998
+ }
999
+ }
1000
+ continue;
1001
+ }
1002
+
1003
+ if (currValue instanceof Entity) {
1004
+ continue;
1005
+ }
1006
+
1007
+ if (!this.isEqual(origValue, currValue)) {
1008
+ changes[key] =
1009
+ currValue instanceof ValueObject ? currValue.value : currValue;
1010
+ }
1011
+ }
1012
+
1013
+ return changes;
1014
+ }
1015
+
1016
+ private handleArrayAssignment(path: string, oldValue: any): void {
1017
+ const rootTracker = this.getRootTracker();
1018
+
1019
+ if (!rootTracker.trackedArrays.has(path)) {
1020
+ const parentId = this.getEntityId(this.target);
1021
+ rootTracker.captureArrayState(
1022
+ Array.isArray(oldValue) ? oldValue : [],
1023
+ path,
1024
+ this.depth + 1,
1025
+ parentId,
1026
+ this.rootEntityName
1027
+ );
1028
+ }
1029
+ }
1030
+
1031
+ private handleEntityChange(path: string, oldValue: any, newValue: any): void {
1032
+ const rootTracker = this.getRootTracker();
1033
+ const entityName = newValue
1034
+ ? this.getEntityName(newValue)
1035
+ : this.getEntityName(oldValue);
1036
+
1037
+ const existingTracked = rootTracker.trackedEntities.get(path);
1038
+
1039
+ rootTracker.trackedEntities.set(path, {
1040
+ entity: existingTracked?.entity || oldValue,
1041
+ metadata: {
1042
+ entityName,
1043
+ depth: this.depth + 1,
1044
+ parentId: this.getEntityId(this.target),
1045
+ parentEntity: this.rootEntityName,
1046
+ path,
1047
+ },
1048
+ originalState: existingTracked?.originalState,
1049
+ });
1050
+ }
1051
+
1052
+ private getRootTracker(): ChangeTracker {
1053
+ return this.rootTracker || this;
1054
+ }
1055
+
1056
+ private buildPath(prop: string): string {
1057
+ return this.path ? `${this.path}.${prop}` : prop;
1058
+ }
1059
+
1060
+ private shouldSkipProperty(prop: string | symbol): boolean {
1061
+ const skipProps = [
1062
+ "__isProxy",
1063
+ "__tracker",
1064
+ "__originalTarget",
1065
+ "__path",
1066
+ "constructor",
1067
+ "prototype",
1068
+ ];
1069
+ return skipProps.includes(String(prop));
1070
+ }
1071
+
1072
+ private getValueAtPath(obj: any, path: string): any {
1073
+ if (!path) return obj;
1074
+
1075
+ const parts = path.split(/[.\[\]]+/).filter(Boolean);
1076
+ let current = obj;
1077
+
1078
+ for (const part of parts) {
1079
+ if (current === null || current === undefined) return undefined;
1080
+
1081
+ const propsToAccess = current.props || current;
1082
+ current = propsToAccess[part];
1083
+ }
1084
+
1085
+ return current;
1086
+ }
1087
+
1088
+ private extractRelationField(path: string): string {
1089
+ const withoutIndices = path.replace(/\[\d+\]/g, "");
1090
+ const parts = withoutIndices.split(".");
1091
+ return parts[parts.length - 1];
1092
+ }
1093
+
1094
+ private getItemKey(item: any): string | undefined {
1095
+ const id = this.getEntityId(item);
1096
+ if (id) return id;
1097
+
1098
+ return undefined;
1099
+ }
1100
+
1101
+ private getEntityId(item: any): string | undefined {
1102
+ if (!item) return undefined;
1103
+ if (item.id instanceof Id) return item.id.value;
1104
+ if (item.id !== undefined) return String(item.id);
1105
+ return undefined;
1106
+ }
1107
+
1108
+ private getEntityName(item: any): string {
1109
+ if (!item) return "Unknown";
1110
+ return item.constructor?.name || "Unknown";
1111
+ }
1112
+
1113
+ /**
1114
+ * Checks if a value is a primitive (string, number, boolean, null, undefined, symbol, bigint).
1115
+ */
1116
+ private isPrimitiveValue(value: any): boolean {
1117
+ if (value === null || value === undefined) return true;
1118
+ const type = typeof value;
1119
+ return (
1120
+ type === "string" ||
1121
+ type === "number" ||
1122
+ type === "boolean" ||
1123
+ type === "symbol" ||
1124
+ type === "bigint"
1125
+ );
1126
+ }
1127
+
1128
+ /**
1129
+ * Checks if an array contains only primitive values.
1130
+ * Empty arrays are not treated as primitive at capture time since their
1131
+ * element type is not yet known.
1132
+ */
1133
+ private isPrimitiveArray(arr: any[]): boolean {
1134
+ if (arr.length === 0) return false;
1135
+ return arr.every((item) => this.isPrimitiveValue(item));
1136
+ }
1137
+
1138
+ /**
1139
+ * Determines whether an array should be tracked as a primitive property
1140
+ * during change analysis. Uses current contents when available, otherwise
1141
+ * falls back to the originally captured clone.
1142
+ */
1143
+ private shouldTreatArrayAsPrimitive(
1144
+ currentArray: any[],
1145
+ arrayState: ArrayState
1146
+ ): boolean {
1147
+ if (currentArray.length > 0) {
1148
+ return currentArray.every((item) => this.isPrimitiveValue(item));
1149
+ }
1150
+ if (arrayState.cloned.length > 0) {
1151
+ return arrayState.cloned.every((item) => this.isPrimitiveValue(item));
1152
+ }
1153
+ return false;
1154
+ }
1155
+
1156
+ private isEqual(a: any, b: any): boolean {
1157
+ if (a === b) return true;
1158
+ if (a instanceof Id && b instanceof Id) return a.equals(b);
1159
+ if (a instanceof ValueObject && b instanceof ValueObject)
1160
+ return a.equals(b);
1161
+ if (a instanceof Date && b instanceof Date)
1162
+ return a.getTime() === b.getTime();
1163
+
1164
+ try {
1165
+ return this.hasChanged(a, b) === false;
1166
+ } catch (error) {
1167
+ if (error instanceof DomainError) throw error;
1168
+ return this.deepEqual(a, b);
1169
+ }
1170
+ }
1171
+
1172
+ private deepEqual(a: any, b: any): boolean {
1173
+ if (a === b) return true;
1174
+ if (a == null || b == null) return a === b;
1175
+ if (typeof a !== typeof b) return false;
1176
+ if (typeof a !== "object") return a === b;
1177
+
1178
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
1179
+ if (Array.isArray(a)) {
1180
+ if (a.length !== b.length) return false;
1181
+ for (let i = 0; i < a.length; i++) {
1182
+ if (!this.deepEqual(a[i], b[i])) return false;
1183
+ }
1184
+ return true;
1185
+ }
1186
+
1187
+ const keysA = Object.keys(a).filter((key) => {
1188
+ const value = a[key];
1189
+ return (
1190
+ typeof value !== "object" ||
1191
+ value instanceof Date ||
1192
+ value instanceof Id ||
1193
+ value === null
1194
+ );
1195
+ });
1196
+ const keysB = Object.keys(b).filter((key) => {
1197
+ const value = b[key];
1198
+ return (
1199
+ typeof value !== "object" ||
1200
+ value instanceof Date ||
1201
+ value instanceof Id ||
1202
+ value === null
1203
+ );
1204
+ });
1205
+
1206
+ if (keysA.length !== keysB.length) return false;
1207
+
1208
+ for (const key of keysA) {
1209
+ if (!keysB.includes(key)) return false;
1210
+ if (!this.isEqual(a[key], b[key])) return false;
1211
+ }
1212
+
1213
+ return true;
1214
+ }
1215
+
1216
+ private hasChanged(obj1: any, obj2: any): boolean {
1217
+ return this.toCanonicalString(obj1) !== this.toCanonicalString(obj2);
1218
+ }
1219
+
1220
+ private toCanonicalString(obj: any, visited = new WeakSet()): string {
1221
+ if (obj === null || typeof obj !== "object") {
1222
+ return JSON.stringify(obj);
1223
+ }
1224
+ if (visited.has(obj)) {
1225
+ this.throwCircularReferenceError(obj);
1226
+ }
1227
+
1228
+ visited.add(obj);
1229
+
1230
+ if (obj instanceof Id || obj instanceof ValueObject) {
1231
+ return JSON.stringify(obj.value);
1232
+ }
1233
+
1234
+ if (typeof obj.toJSON === "function") {
1235
+ try {
1236
+ return this.toCanonicalString(obj.toJSON(), visited);
1237
+ } catch (error) {
1238
+ if (error instanceof DomainError) throw error;
1239
+ this.throwCircularReferenceError(obj);
1240
+ }
1241
+ }
1242
+
1243
+ if (Array.isArray(obj)) {
1244
+ const entries = obj.map((item) => this.toCanonicalString(item, visited));
1245
+ return `[${entries.join(",")}]`;
1246
+ }
1247
+
1248
+ const sortedKeys = Object.keys(obj).sort();
1249
+ const resultParts = sortedKeys.map((key) => {
1250
+ const value = this.toCanonicalString(obj[key], visited);
1251
+ return `${JSON.stringify(key)}:${value}`;
1252
+ });
1253
+
1254
+ return `{${resultParts.join(",")}}`;
1255
+ }
1256
+
1257
+ private throwCircularReferenceError(obj: any): void {
1258
+ const className = obj.constructor?.name || "Unknown";
1259
+ throw new DomainError(
1260
+ `Circular reference detected in object comparison: ${className}`,
1261
+ "CIRCULAR_REFERENCE_ERROR"
1262
+ );
1263
+ }
1264
+
1265
+ private cloneArray(arr: any[]): any[] {
1266
+ return arr.map((item) => this.deepClone(item));
1267
+ }
1268
+
1269
+ private deepClone(obj: any): any {
1270
+ if (obj === null || obj === undefined || typeof obj !== "object") {
1271
+ return obj;
1272
+ }
1273
+
1274
+ if (obj instanceof Id) {
1275
+ return obj.value;
1276
+ }
1277
+
1278
+ if (obj instanceof ValueObject) {
1279
+ return obj.value;
1280
+ }
1281
+
1282
+ if (typeof obj.toJSON === "function") {
1283
+ return obj.toJSON();
1284
+ }
1285
+
1286
+ if (Array.isArray(obj)) {
1287
+ return obj.map((item) => this.deepClone(item));
1288
+ }
1289
+
1290
+ if (obj instanceof Date) {
1291
+ return new Date(obj.getTime());
1292
+ }
1293
+
1294
+ try {
1295
+ return structuredClone(obj);
1296
+ } catch {
1297
+ const cloned: any = {};
1298
+ for (const key in obj) {
1299
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
1300
+ cloned[key] = this.deepClone(obj[key]);
1301
+ }
1302
+ }
1303
+ return cloned;
1304
+ }
1305
+ }
1306
+
1307
+ getHistory(): HistoryEntry[] {
1308
+ return [...this.getRootTracker().history];
1309
+ }
1310
+
1311
+ clearHistory(): void {
1312
+ const rootTracker = this.getRootTracker();
1313
+ rootTracker.history = [];
1314
+ rootTracker.originalValues.clear();
1315
+ rootTracker.trackedArrays.clear();
1316
+ rootTracker.trackedEntities.clear();
1317
+ this.captureInitialState();
1318
+ }
1319
+
1320
+ markAsClean(): void {
1321
+ this.clearHistory();
1322
+ }
1323
+
1324
+ getTarget(): any {
1325
+ return this.target;
1326
+ }
1327
+ }