@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.
Files changed (105) hide show
  1. package/CHANGELOG.md +33 -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 +117 -86
  9. package/dist/base-entity.js.map +1 -1
  10. package/dist/criteria.d.ts +3 -3
  11. package/dist/criteria.d.ts.map +1 -1
  12. package/dist/criteria.js.map +1 -1
  13. package/dist/crypto.d.ts +3 -0
  14. package/dist/crypto.d.ts.map +1 -0
  15. package/dist/crypto.js +29 -0
  16. package/dist/crypto.js.map +1 -0
  17. package/dist/entity-changes.d.ts +84 -0
  18. package/dist/entity-changes.d.ts.map +1 -0
  19. package/dist/entity-changes.js +135 -0
  20. package/dist/entity-changes.js.map +1 -0
  21. package/dist/entity-schema-registry.d.ts +148 -0
  22. package/dist/entity-schema-registry.d.ts.map +1 -0
  23. package/dist/entity-schema-registry.js +219 -0
  24. package/dist/entity-schema-registry.js.map +1 -0
  25. package/dist/history-tracker.d.ts +97 -0
  26. package/dist/history-tracker.d.ts.map +1 -0
  27. package/dist/history-tracker.js +805 -0
  28. package/dist/history-tracker.js.map +1 -0
  29. package/dist/id.d.ts +11 -10
  30. package/dist/id.d.ts.map +1 -1
  31. package/dist/id.js +4 -28
  32. package/dist/id.js.map +1 -1
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/mapper.d.ts +1 -1
  38. package/dist/mapper.d.ts.map +1 -1
  39. package/dist/mapper.js.map +1 -1
  40. package/dist/repository/base-repository.d.ts +6 -32
  41. package/dist/repository/base-repository.d.ts.map +1 -1
  42. package/dist/repository/base-repository.js +0 -27
  43. package/dist/repository/base-repository.js.map +1 -1
  44. package/dist/repository/unit-of-work.d.ts +0 -25
  45. package/dist/repository/unit-of-work.d.ts.map +1 -1
  46. package/dist/repository/unit-of-work.js +0 -25
  47. package/dist/repository/unit-of-work.js.map +1 -1
  48. package/dist/types/change-tracker.d.ts +186 -0
  49. package/dist/types/change-tracker.d.ts.map +1 -0
  50. package/dist/types/change-tracker.js +2 -0
  51. package/dist/types/change-tracker.js.map +1 -0
  52. package/dist/types/criteria.d.ts +5 -1
  53. package/dist/types/criteria.d.ts.map +1 -1
  54. package/dist/types/history-tracker.d.ts +11 -0
  55. package/dist/types/history-tracker.d.ts.map +1 -1
  56. package/dist/types/utils.d.ts +0 -1
  57. package/dist/types/utils.d.ts.map +1 -1
  58. package/dist/validation-error.d.ts.map +1 -1
  59. package/dist/validation-error.js +0 -3
  60. package/dist/validation-error.js.map +1 -1
  61. package/dist/value-object.d.ts +57 -8
  62. package/dist/value-object.d.ts.map +1 -1
  63. package/dist/value-object.js +49 -21
  64. package/dist/value-object.js.map +1 -1
  65. package/package.json +2 -1
  66. package/src/aggregate-changes.ts +335 -0
  67. package/src/base-entity.ts +140 -100
  68. package/src/criteria.ts +2 -1
  69. package/src/crypto.ts +31 -0
  70. package/src/entity-changes.ts +151 -0
  71. package/src/entity-schema-registry.ts +275 -0
  72. package/src/history-tracker.ts +1114 -0
  73. package/src/id.ts +17 -26
  74. package/src/index.ts +1 -0
  75. package/src/mapper.ts +4 -1
  76. package/src/repository/base-repository.ts +6 -37
  77. package/src/repository/unit-of-work.ts +0 -25
  78. package/src/types/change-tracker.ts +221 -0
  79. package/src/types/criteria.ts +6 -1
  80. package/src/types/history-tracker.ts +13 -0
  81. package/src/types/utils.ts +0 -9
  82. package/src/validation-error.ts +0 -4
  83. package/src/value-object.ts +84 -23
  84. package/tests/aggregate-changes.test.ts +284 -0
  85. package/tests/criteria.test.ts +122 -161
  86. package/tests/entity-equality.test.ts +38 -61
  87. package/tests/entity-schema-registry.test.ts +382 -0
  88. package/tests/entity-validation.test.ts +7 -94
  89. package/tests/history-tracker.spec.ts +349 -617
  90. package/tests/id.test.ts +41 -44
  91. package/tests/load-test/data.json +346041 -0
  92. package/tests/load-test/entities.ts +97 -0
  93. package/tests/load-test/generate-data.ts +81 -0
  94. package/tests/load-test/lead-to-domain.mapper.ts +24 -0
  95. package/tests/load-test/load.test.ts +38 -0
  96. package/tests/repository.test.ts +30 -54
  97. package/tests/to-json.test.ts +14 -18
  98. package/tests/utils.ts +138 -102
  99. package/tests/value-objects.test.ts +57 -29
  100. package/dist/deep-proxy.d.ts +0 -36
  101. package/dist/deep-proxy.d.ts.map +0 -1
  102. package/dist/deep-proxy.js +0 -384
  103. package/dist/deep-proxy.js.map +0 -1
  104. package/src/deep-proxy.ts +0 -447
  105. package/tests/entity.test.ts +0 -33
@@ -0,0 +1,805 @@
1
+ import { Id } from "./id";
2
+ import { Entity } from "./entity";
3
+ import { ValueObject } from "./value-object";
4
+ import { AggregateChanges } from "./aggregate-changes";
5
+ /**
6
+ * Tracks changes in Aggregates using Proxy.
7
+ *
8
+ * Features:
9
+ * - Tracks changes in primitive properties
10
+ * - Tracks changes in nested entities (1:1)
11
+ * - Tracks changes in collections (1:N)
12
+ * - Supports Value Objects with identityKey
13
+ * - Calculates depth automatically
14
+ * - Generates AggregateChanges for persistence
15
+ * - Supports validation on change via onChangeValidator
16
+ */
17
+ export class HistoryTracker {
18
+ constructor(target, rootEntityName, path = "", depth = 0, parentId, parentEntity, rootTracker) {
19
+ this.target = target;
20
+ this.rootEntityName = rootEntityName;
21
+ this.path = path;
22
+ this.depth = depth;
23
+ this.parentId = parentId;
24
+ this.parentEntity = parentEntity;
25
+ this.rootTracker = rootTracker;
26
+ this.history = [];
27
+ this.originalValues = new Map();
28
+ this.trackedArrays = new Map();
29
+ this.trackedEntities = new Map();
30
+ if (!rootTracker) {
31
+ this.rootTracker = this;
32
+ }
33
+ this.captureInitialState();
34
+ }
35
+ /**
36
+ * Sets a validator callback that will be called on every property change.
37
+ * The validator can:
38
+ * - Return false to reject the change (value will be reverted)
39
+ * - Throw an error to reject the change with an error
40
+ * - Return true/undefined to accept the change
41
+ */
42
+ setOnChangeValidator(validator) {
43
+ this.getRootTracker().onChangeValidator = validator;
44
+ }
45
+ // ============================================================================
46
+ // Initial State Capture
47
+ // ============================================================================
48
+ captureInitialState() {
49
+ if (this.depth > 0)
50
+ return;
51
+ this.captureEntityState(this.target, this.rootEntityName, "", 0);
52
+ }
53
+ captureEntityState(obj, entityName, path, depth, parentId, parentEntity) {
54
+ if (!obj || typeof obj !== "object")
55
+ return;
56
+ const id = this.getEntityId(obj);
57
+ const key = path || "root";
58
+ this.trackedEntities.set(key, {
59
+ entity: obj,
60
+ metadata: {
61
+ entityName,
62
+ depth,
63
+ parentId,
64
+ parentEntity,
65
+ path,
66
+ },
67
+ originalState: this.deepClone(obj),
68
+ });
69
+ const propsToScan = obj.props || obj;
70
+ for (const [propName, value] of Object.entries(propsToScan)) {
71
+ if (propName === "id")
72
+ continue;
73
+ const propPath = path ? `${path}.${propName}` : propName;
74
+ if (Array.isArray(value)) {
75
+ this.captureArrayState(value, propPath, depth + 1, id, entityName);
76
+ }
77
+ else if (this.isEntityOrVO(value)) {
78
+ const nestedName = this.getEntityName(value);
79
+ this.captureEntityState(value, nestedName, propPath, depth + 1, id, entityName);
80
+ }
81
+ }
82
+ }
83
+ captureArrayState(arr, path, depth, parentId, parentEntity) {
84
+ const entityName = arr.length > 0 ? this.getEntityName(arr[0]) : "Unknown";
85
+ this.trackedArrays.set(path, {
86
+ cloned: this.cloneArray(arr),
87
+ original: arr.slice(),
88
+ metadata: {
89
+ entityName,
90
+ depth,
91
+ parentId,
92
+ parentEntity,
93
+ path,
94
+ },
95
+ });
96
+ arr.forEach((item, index) => {
97
+ if (this.isEntityOrVO(item)) {
98
+ const itemPath = `${path}[${index}]`;
99
+ this.captureEntityState(item, this.getEntityName(item), itemPath, depth, parentId, parentEntity);
100
+ }
101
+ });
102
+ }
103
+ // ============================================================================
104
+ // Proxy Creation
105
+ // ============================================================================
106
+ createProxy() {
107
+ const handler = {
108
+ get: (target, prop, receiver) => {
109
+ const value = Reflect.get(target, prop, receiver);
110
+ if (this.shouldSkipProperty(prop)) {
111
+ return value;
112
+ }
113
+ if (typeof value === "function") {
114
+ return value.bind(target);
115
+ }
116
+ const currentPath = this.buildPath(String(prop));
117
+ if (Array.isArray(value)) {
118
+ return this.createArrayProxy(value, currentPath);
119
+ }
120
+ if (this.isEntityOrVO(value)) {
121
+ const nestedTracker = new HistoryTracker(value, this.getEntityName(value), currentPath, this.depth + 1, this.getEntityId(this.target), this.rootEntityName, this.rootTracker);
122
+ return nestedTracker.createProxy();
123
+ }
124
+ return value;
125
+ },
126
+ set: (target, prop, newValue, receiver) => {
127
+ const currentPath = this.buildPath(String(prop));
128
+ const oldValue = Reflect.get(target, prop, receiver);
129
+ if (!Array.isArray(newValue) && oldValue === newValue) {
130
+ return true;
131
+ }
132
+ // Call validator before making the change
133
+ const rootTracker = this.getRootTracker();
134
+ if (rootTracker.onChangeValidator) {
135
+ try {
136
+ const result = rootTracker.onChangeValidator(currentPath, oldValue, newValue);
137
+ if (result === false) {
138
+ // Validator rejected the change
139
+ return true; // Return true to not throw, but don't apply change
140
+ }
141
+ }
142
+ catch (error) {
143
+ // Validator threw an error - propagate it
144
+ throw error;
145
+ }
146
+ }
147
+ // Store original value
148
+ if (!rootTracker.originalValues.has(currentPath)) {
149
+ rootTracker.originalValues.set(currentPath, oldValue);
150
+ }
151
+ // Record in history
152
+ rootTracker.history.push({
153
+ path: currentPath,
154
+ previousValue: oldValue,
155
+ currentValue: newValue,
156
+ timestamp: Date.now(),
157
+ });
158
+ const result = Reflect.set(target, prop, newValue, receiver);
159
+ // Handle special cases
160
+ if (Array.isArray(newValue)) {
161
+ this.handleArrayAssignment(currentPath, oldValue);
162
+ }
163
+ else if (this.isEntityOrVO(newValue) || this.isEntityOrVO(oldValue)) {
164
+ this.handleEntityChange(currentPath, oldValue, newValue);
165
+ }
166
+ return result;
167
+ },
168
+ };
169
+ const proxy = new Proxy(this.target, handler);
170
+ Object.defineProperty(proxy, "__isProxy", { value: true, writable: false });
171
+ return proxy;
172
+ }
173
+ createArrayProxy(array, path) {
174
+ const tracker = this;
175
+ const rootTracker = this.getRootTracker();
176
+ if (!rootTracker.trackedArrays.has(path)) {
177
+ const parentId = this.getEntityId(this.target);
178
+ rootTracker.captureArrayState(array, path, this.depth + 1, parentId, this.rootEntityName);
179
+ }
180
+ return new Proxy(array, {
181
+ get(target, prop, receiver) {
182
+ const value = Reflect.get(target, prop, receiver);
183
+ if (typeof value === "function") {
184
+ const mutatingMethods = [
185
+ "push",
186
+ "pop",
187
+ "shift",
188
+ "unshift",
189
+ "splice",
190
+ "sort",
191
+ "reverse",
192
+ ];
193
+ if (mutatingMethods.includes(String(prop))) {
194
+ return function (...args) {
195
+ const oldArray = target.slice();
196
+ // Call validator before array mutation
197
+ if (rootTracker.onChangeValidator) {
198
+ try {
199
+ const result = rootTracker.onChangeValidator(path, oldArray, [...oldArray, ...args] // Preview of change
200
+ );
201
+ if (result === false) {
202
+ return undefined;
203
+ }
204
+ }
205
+ catch (error) {
206
+ throw error;
207
+ }
208
+ }
209
+ const result = value.apply(target, args);
210
+ rootTracker.history.push({
211
+ path,
212
+ previousValue: oldArray,
213
+ currentValue: target.slice(),
214
+ timestamp: Date.now(),
215
+ });
216
+ return result;
217
+ };
218
+ }
219
+ return value.bind(target);
220
+ }
221
+ if (!isNaN(Number(prop)) && tracker.isEntityOrVO(value)) {
222
+ const nestedPath = `${path}[${String(prop)}]`;
223
+ const nestedTracker = new HistoryTracker(value, tracker.getEntityName(value), nestedPath, tracker.depth + 1, tracker.getEntityId(tracker.target), tracker.rootEntityName, rootTracker);
224
+ return nestedTracker.createProxy();
225
+ }
226
+ return value;
227
+ },
228
+ set(target, prop, newValue, receiver) {
229
+ if (!isNaN(Number(prop))) {
230
+ const oldArray = target.slice();
231
+ // Call validator before array item change
232
+ if (rootTracker.onChangeValidator) {
233
+ try {
234
+ const result = rootTracker.onChangeValidator(path, oldArray, newValue);
235
+ if (result === false) {
236
+ return true;
237
+ }
238
+ }
239
+ catch (error) {
240
+ throw error;
241
+ }
242
+ }
243
+ const result = Reflect.set(target, prop, newValue, receiver);
244
+ rootTracker.history.push({
245
+ path,
246
+ previousValue: oldArray,
247
+ currentValue: target.slice(),
248
+ timestamp: Date.now(),
249
+ });
250
+ return result;
251
+ }
252
+ return Reflect.set(target, prop, newValue, receiver);
253
+ },
254
+ });
255
+ }
256
+ // ============================================================================
257
+ // getChanges() - Main Method
258
+ // ============================================================================
259
+ /**
260
+ * Returns all detected changes as AggregateChanges.
261
+ */
262
+ getChanges() {
263
+ const changes = new AggregateChanges();
264
+ const rootTracker = this.getRootTracker();
265
+ this.analyzeRootChanges(changes, rootTracker);
266
+ this.analyzeCollectionChanges(changes, rootTracker);
267
+ this.analyzeEntityChanges(changes, rootTracker);
268
+ return changes;
269
+ }
270
+ analyzeRootChanges(changes, rootTracker) {
271
+ const changedFields = {};
272
+ let hasChanges = false;
273
+ for (const [path, originalValue] of rootTracker.originalValues) {
274
+ if (path.includes(".") || path.includes("["))
275
+ continue;
276
+ const currentValue = this.target[path];
277
+ if (!this.isEqual(originalValue, currentValue)) {
278
+ changedFields[path] = currentValue;
279
+ hasChanges = true;
280
+ }
281
+ }
282
+ if (hasChanges) {
283
+ const id = this.getEntityId(this.target);
284
+ if (id) {
285
+ changes.addUpdate(this.rootEntityName, id, this.target, changedFields, 0);
286
+ }
287
+ }
288
+ }
289
+ analyzeCollectionChanges(changes, rootTracker) {
290
+ const allTrackedArrays = new Map();
291
+ const processedArrays = new Set();
292
+ for (const [path, arrayState] of rootTracker.trackedArrays) {
293
+ const currentArray = this.getValueAtPath(this.target, path);
294
+ if (Array.isArray(currentArray) && !processedArrays.has(currentArray)) {
295
+ allTrackedArrays.set(path, arrayState);
296
+ processedArrays.add(currentArray);
297
+ }
298
+ }
299
+ this.collectNestedArrays(this.target, "", allTrackedArrays, processedArrays);
300
+ for (const [path, arrayState] of allTrackedArrays) {
301
+ const currentArray = this.getValueAtPath(this.target, path);
302
+ if (!Array.isArray(currentArray))
303
+ continue;
304
+ const { created, updated, deleted } = this.detectArrayChanges(arrayState.cloned, arrayState.original, currentArray);
305
+ const { depth, parentId, parentEntity } = arrayState.metadata;
306
+ for (const item of created) {
307
+ const itemEntityName = this.getEntityName(item);
308
+ changes.addCreate(itemEntityName, item, depth, parentId, parentEntity);
309
+ // Recursively mark nested items as created
310
+ this.markNestedItemsAsCreated(item, depth, changes);
311
+ }
312
+ for (const item of updated) {
313
+ const id = this.getEntityId(item);
314
+ if (id) {
315
+ const original = arrayState.cloned.find((o) => this.getEntityId(o) === id);
316
+ const changedFields = this.detectChangedFields(original, item);
317
+ if (Object.keys(changedFields).length > 0) {
318
+ const itemEntityName = this.getEntityName(item);
319
+ changes.addUpdate(itemEntityName, id, item, changedFields, depth);
320
+ }
321
+ }
322
+ }
323
+ for (const item of deleted) {
324
+ const id = this.getEntityId(item);
325
+ const key = this.getItemKey(item);
326
+ if (id || key) {
327
+ const itemEntityName = this.getEntityName(item);
328
+ const deleteId = id || key;
329
+ changes.addDelete(itemEntityName, deleteId, item, depth);
330
+ // Recursively mark nested items as deleted using ORIGINAL state
331
+ this.markNestedItemsAsDeleted(item, depth, changes, rootTracker);
332
+ }
333
+ }
334
+ }
335
+ }
336
+ /**
337
+ * Recursively marks all nested items as created when a parent is created.
338
+ */
339
+ markNestedItemsAsCreated(item, parentDepth, changes) {
340
+ if (!item || typeof item !== "object")
341
+ return;
342
+ const itemId = this.getEntityId(item);
343
+ if (!itemId)
344
+ return;
345
+ const props = item.props || item;
346
+ for (const [propName, value] of Object.entries(props)) {
347
+ if (propName === "id")
348
+ continue;
349
+ if (Array.isArray(value)) {
350
+ // Process all items in the array
351
+ for (const nestedItem of value) {
352
+ if (this.isEntityOrVO(nestedItem)) {
353
+ const nestedId = this.getEntityId(nestedItem);
354
+ const nestedKey = this.getItemKey(nestedItem);
355
+ if (nestedId || nestedKey) {
356
+ const entityName = this.getEntityName(nestedItem);
357
+ changes.addCreate(entityName, nestedItem, parentDepth + 1, itemId, this.getEntityName(item));
358
+ // Recursively process nested items
359
+ this.markNestedItemsAsCreated(nestedItem, parentDepth + 1, changes);
360
+ }
361
+ }
362
+ }
363
+ }
364
+ }
365
+ }
366
+ /**
367
+ * Recursively marks all nested items as deleted when a parent is deleted.
368
+ * Uses the original captured state to find nested items.
369
+ */
370
+ markNestedItemsAsDeleted(item, parentDepth, changes, rootTracker) {
371
+ if (!item || typeof item !== "object")
372
+ return;
373
+ // Get the ID to look up the original state
374
+ const itemId = this.getEntityId(item);
375
+ if (!itemId)
376
+ return;
377
+ // Look through all tracked arrays to find nested items
378
+ for (const [, arrayState] of rootTracker.trackedArrays) {
379
+ // Check if this array belongs to our deleted item
380
+ if (arrayState.metadata.parentId === itemId) {
381
+ // Use the CLONED (original) state to get the items
382
+ // Note: cloned items are JSON objects, not Entity/VO instances
383
+ for (const nestedItem of arrayState.cloned) {
384
+ // Cloned items are JSON objects with an 'id' property
385
+ const id = typeof nestedItem === "object" && nestedItem !== null
386
+ ? nestedItem.id
387
+ : undefined;
388
+ if (id) {
389
+ const entityName = arrayState.metadata.entityName;
390
+ changes.addDelete(entityName, id, nestedItem, parentDepth + 1);
391
+ // Recursively process this item's nested arrays
392
+ this.markNestedJsonItemAsDeleted(id, parentDepth + 1, changes, rootTracker);
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+ /**
399
+ * Recursively marks nested items as deleted from a JSON object.
400
+ * This is used when processing cloned (JSON) state.
401
+ */
402
+ markNestedJsonItemAsDeleted(itemId, parentDepth, changes, rootTracker) {
403
+ // Look through all tracked arrays to find nested items of this parent
404
+ for (const [, arrayState] of rootTracker.trackedArrays) {
405
+ if (arrayState.metadata.parentId === itemId) {
406
+ // Process all items in this nested array
407
+ for (const nestedJsonItem of arrayState.cloned) {
408
+ if (typeof nestedJsonItem !== "object" || nestedJsonItem === null)
409
+ continue;
410
+ const nestedId = nestedJsonItem.id;
411
+ const entityName = arrayState.metadata.entityName;
412
+ if (nestedId) {
413
+ changes.addDelete(entityName, nestedId, nestedJsonItem, parentDepth + 1);
414
+ // Recursively process further nesting
415
+ this.markNestedJsonItemAsDeleted(nestedId, parentDepth + 1, changes, rootTracker);
416
+ }
417
+ else {
418
+ // Value object - try to extract identity key
419
+ const key = this.extractIdentityKeyFromJson(nestedJsonItem, arrayState.original);
420
+ if (key) {
421
+ changes.addDelete(entityName, key, nestedJsonItem, parentDepth + 1);
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
427
+ }
428
+ /**
429
+ * Extracts identity key from a JSON object by looking at the original ValueObject instances.
430
+ */
431
+ extractIdentityKeyFromJson(jsonItem, originalArray) {
432
+ // Try to find the original ValueObject to get its identity key
433
+ for (const originalItem of originalArray) {
434
+ if (this.isEntityOrVO(originalItem)) {
435
+ const originalJson = this.deepClone(originalItem);
436
+ // Check if this matches our JSON item (rough comparison)
437
+ if (JSON.stringify(originalJson) === JSON.stringify(jsonItem)) {
438
+ // Found the matching original item - extract its identity key
439
+ const key = this.getItemKey(originalItem);
440
+ if (key)
441
+ return key;
442
+ }
443
+ }
444
+ }
445
+ // Fallback: if it has an id, use that
446
+ if (jsonItem.id)
447
+ return jsonItem.id;
448
+ return undefined;
449
+ }
450
+ collectNestedArrays(obj, basePath, allArrays, processedArrays) {
451
+ if (!obj || typeof obj !== "object")
452
+ return;
453
+ for (const [propName, value] of Object.entries(obj)) {
454
+ if (propName === "id" || propName === "proxy" || propName === "_props")
455
+ continue;
456
+ const propPath = basePath ? `${basePath}.${propName}` : propName;
457
+ if (Array.isArray(value)) {
458
+ value.forEach((item, index) => {
459
+ if (this.isEntityOrVO(item)) {
460
+ this.collectNestedArrays(item, `${propPath}[${index}]`, allArrays, processedArrays);
461
+ }
462
+ });
463
+ }
464
+ else if (this.isEntityOrVO(value)) {
465
+ this.collectNestedArrays(value, propPath, allArrays, processedArrays);
466
+ }
467
+ }
468
+ }
469
+ analyzeEntityChanges(changes, rootTracker) {
470
+ for (const [path, trackedItem] of rootTracker.trackedEntities) {
471
+ if (path === "root")
472
+ continue;
473
+ if (path.includes("["))
474
+ continue;
475
+ const currentValue = this.getValueAtPath(this.target, path);
476
+ const originalValue = trackedItem.originalState;
477
+ const originalEntity = trackedItem.entity;
478
+ const { entityName, depth, parentId, parentEntity } = trackedItem.metadata;
479
+ const state = this.detectEntityChangeState(originalValue, currentValue);
480
+ switch (state) {
481
+ case "created":
482
+ changes.addCreate(entityName, currentValue, depth, parentId, parentEntity);
483
+ break;
484
+ case "deleted":
485
+ const id = this.getEntityId(originalValue);
486
+ if (id) {
487
+ // Use originalEntity instead of originalValue to preserve entity instance
488
+ changes.addDelete(entityName, id, originalEntity, depth);
489
+ }
490
+ break;
491
+ case "replaced":
492
+ const oldId = this.getEntityId(originalValue);
493
+ if (oldId) {
494
+ // Use originalEntity instead of originalValue to preserve entity instance
495
+ changes.addDelete(entityName, oldId, originalEntity, depth);
496
+ }
497
+ changes.addCreate(entityName, currentValue, depth, parentId, parentEntity);
498
+ break;
499
+ case "updated":
500
+ const updateId = this.getEntityId(currentValue);
501
+ if (updateId) {
502
+ const changedFields = this.detectChangedFields(originalValue, currentValue);
503
+ if (Object.keys(changedFields).length > 0) {
504
+ changes.addUpdate(entityName, updateId, currentValue, changedFields, depth);
505
+ }
506
+ }
507
+ break;
508
+ }
509
+ }
510
+ }
511
+ // ============================================================================
512
+ // Change Detection Helpers
513
+ // ============================================================================
514
+ detectEntityChangeState(previous, current) {
515
+ if (previous === null && current !== null) {
516
+ return "created";
517
+ }
518
+ if (previous !== null && current === null) {
519
+ return "deleted";
520
+ }
521
+ if (previous !== null && current !== null) {
522
+ const prevId = this.getEntityId(previous);
523
+ const currId = this.getEntityId(current);
524
+ if (prevId && currId && prevId === currId) {
525
+ return this.hasChanged(previous, current) ? "updated" : "unchanged";
526
+ }
527
+ else {
528
+ return "replaced";
529
+ }
530
+ }
531
+ return "unchanged";
532
+ }
533
+ detectArrayChanges(oldCloned, oldOriginal, newArray) {
534
+ const created = [];
535
+ const updated = [];
536
+ const deleted = [];
537
+ const oldMap = new Map();
538
+ const newMap = new Map();
539
+ oldCloned.forEach((item) => {
540
+ const key = this.getItemKey(item);
541
+ if (key)
542
+ oldMap.set(key, item);
543
+ });
544
+ newArray.forEach((item) => {
545
+ const key = this.getItemKey(item);
546
+ if (key)
547
+ newMap.set(key, item);
548
+ });
549
+ newArray.forEach((item) => {
550
+ const key = this.getItemKey(item);
551
+ if (!key) {
552
+ created.push(item);
553
+ }
554
+ else if (!oldMap.has(key)) {
555
+ created.push(item);
556
+ }
557
+ else if (this.hasChanged(oldMap.get(key), item)) {
558
+ updated.push(item);
559
+ }
560
+ });
561
+ oldOriginal.forEach((item) => {
562
+ const key = this.getItemKey(item);
563
+ if (key && !newMap.has(key)) {
564
+ deleted.push(item);
565
+ }
566
+ });
567
+ return { created, updated, deleted };
568
+ }
569
+ detectChangedFields(original, current) {
570
+ const changes = {};
571
+ if (!original || !current)
572
+ return changes;
573
+ const origProps = original.props || original;
574
+ const currProps = current.props || current;
575
+ for (const key of Object.keys(currProps)) {
576
+ if (key === "id")
577
+ continue;
578
+ const origValue = origProps[key];
579
+ const currValue = currProps[key];
580
+ if (Array.isArray(currValue) || this.isEntityOrVO(currValue)) {
581
+ continue;
582
+ }
583
+ if (!this.isEqual(origValue, currValue)) {
584
+ changes[key] = currValue;
585
+ }
586
+ }
587
+ return changes;
588
+ }
589
+ // ============================================================================
590
+ // Internal Handlers
591
+ // ============================================================================
592
+ handleArrayAssignment(path, oldValue) {
593
+ const rootTracker = this.getRootTracker();
594
+ if (!rootTracker.trackedArrays.has(path)) {
595
+ const parentId = this.getEntityId(this.target);
596
+ rootTracker.captureArrayState(Array.isArray(oldValue) ? oldValue : [], path, this.depth + 1, parentId, this.rootEntityName);
597
+ }
598
+ }
599
+ handleEntityChange(path, oldValue, newValue) {
600
+ const rootTracker = this.getRootTracker();
601
+ const entityName = newValue
602
+ ? this.getEntityName(newValue)
603
+ : this.getEntityName(oldValue);
604
+ const existingTracked = rootTracker.trackedEntities.get(path);
605
+ rootTracker.trackedEntities.set(path, {
606
+ // Preserve original entity, or use oldValue if this is the first change
607
+ entity: existingTracked?.entity || oldValue,
608
+ metadata: {
609
+ entityName,
610
+ depth: this.depth + 1,
611
+ parentId: this.getEntityId(this.target),
612
+ parentEntity: this.rootEntityName,
613
+ path,
614
+ },
615
+ // Preserve original state
616
+ originalState: existingTracked?.originalState,
617
+ });
618
+ }
619
+ // ============================================================================
620
+ // Utility Methods
621
+ // ============================================================================
622
+ getRootTracker() {
623
+ return this.rootTracker || this;
624
+ }
625
+ buildPath(prop) {
626
+ return this.path ? `${this.path}.${prop}` : prop;
627
+ }
628
+ shouldSkipProperty(prop) {
629
+ const skipProps = [
630
+ "__isProxy",
631
+ "__tracker",
632
+ "__originalTarget",
633
+ "__path",
634
+ "constructor",
635
+ "prototype",
636
+ ];
637
+ return skipProps.includes(String(prop));
638
+ }
639
+ getValueAtPath(obj, path) {
640
+ if (!path)
641
+ return obj;
642
+ const parts = path.split(/[.\[\]]+/).filter(Boolean);
643
+ let current = obj;
644
+ for (const part of parts) {
645
+ if (current === null || current === undefined)
646
+ return undefined;
647
+ current = current[part];
648
+ }
649
+ return current;
650
+ }
651
+ getItemKey(item) {
652
+ const id = this.getEntityId(item);
653
+ if (id)
654
+ return id;
655
+ if (item instanceof ValueObject && item.hasIdentityKey()) {
656
+ return item.getIdentityKey() || undefined;
657
+ }
658
+ return undefined;
659
+ }
660
+ getEntityId(item) {
661
+ if (!item)
662
+ return undefined;
663
+ if (item.id instanceof Id)
664
+ return item.id.value;
665
+ if (item.id !== undefined)
666
+ return String(item.id);
667
+ return undefined;
668
+ }
669
+ getEntityName(item) {
670
+ if (!item)
671
+ return "Unknown";
672
+ return item.constructor?.name || "Unknown";
673
+ }
674
+ isEntityOrVO(value) {
675
+ if (value === null || value === undefined)
676
+ return false;
677
+ return value instanceof Entity || value instanceof ValueObject;
678
+ }
679
+ isEqual(a, b) {
680
+ if (a === b)
681
+ return true;
682
+ if (a instanceof Id && b instanceof Id)
683
+ return a.value === b.value;
684
+ if (a instanceof Date && b instanceof Date)
685
+ return a.getTime() === b.getTime();
686
+ try {
687
+ return this.hasChanged(a, b) === false;
688
+ }
689
+ catch {
690
+ return this.deepEqual(a, b);
691
+ }
692
+ }
693
+ deepEqual(a, b) {
694
+ if (a === b)
695
+ return true;
696
+ if (a == null || b == null)
697
+ return a === b;
698
+ if (typeof a !== typeof b)
699
+ return false;
700
+ if (typeof a !== "object")
701
+ return a === b;
702
+ if (Array.isArray(a) !== Array.isArray(b))
703
+ return false;
704
+ if (Array.isArray(a)) {
705
+ if (a.length !== b.length)
706
+ return false;
707
+ for (let i = 0; i < a.length; i++) {
708
+ if (!this.deepEqual(a[i], b[i]))
709
+ return false;
710
+ }
711
+ return true;
712
+ }
713
+ const keysA = Object.keys(a).filter((key) => {
714
+ const value = a[key];
715
+ return (typeof value !== "object" ||
716
+ value instanceof Date ||
717
+ value instanceof Id ||
718
+ value === null);
719
+ });
720
+ const keysB = Object.keys(b).filter((key) => {
721
+ const value = b[key];
722
+ return (typeof value !== "object" ||
723
+ value instanceof Date ||
724
+ value instanceof Id ||
725
+ value === null);
726
+ });
727
+ if (keysA.length !== keysB.length)
728
+ return false;
729
+ for (const key of keysA) {
730
+ if (!keysB.includes(key))
731
+ return false;
732
+ if (!this.isEqual(a[key], b[key]))
733
+ return false;
734
+ }
735
+ return true;
736
+ }
737
+ hasChanged(obj1, obj2) {
738
+ const json1 = this.normalizeAndStringify(this.deepClone(obj1));
739
+ const json2 = this.normalizeAndStringify(this.deepClone(obj2));
740
+ return json1 !== json2;
741
+ }
742
+ cloneArray(arr) {
743
+ return arr.map((item) => this.deepClone(item));
744
+ }
745
+ deepClone(obj) {
746
+ if (obj === null || obj === undefined || typeof obj !== "object") {
747
+ return obj;
748
+ }
749
+ if (obj instanceof Id) {
750
+ return obj.value;
751
+ }
752
+ if (typeof obj.toJson === "function") {
753
+ return obj.toJson();
754
+ }
755
+ if (Array.isArray(obj)) {
756
+ return obj.map((item) => this.deepClone(item));
757
+ }
758
+ if (obj instanceof Date) {
759
+ return new Date(obj.getTime());
760
+ }
761
+ try {
762
+ return structuredClone(obj);
763
+ }
764
+ catch {
765
+ const cloned = {};
766
+ for (const key in obj) {
767
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
768
+ cloned[key] = this.deepClone(obj[key]);
769
+ }
770
+ }
771
+ return cloned;
772
+ }
773
+ }
774
+ normalizeAndStringify(obj) {
775
+ if (obj === null || typeof obj !== "object") {
776
+ return JSON.stringify(obj);
777
+ }
778
+ if (Array.isArray(obj)) {
779
+ return `[${obj
780
+ .map((item) => this.normalizeAndStringify(item))
781
+ .join(",")}]`;
782
+ }
783
+ const keys = Object.keys(obj).sort();
784
+ const parts = keys.map((key) => `"${key}":${this.normalizeAndStringify(obj[key])}`);
785
+ return `{${parts.join(",")}}`;
786
+ }
787
+ getHistory() {
788
+ return [...this.getRootTracker().history];
789
+ }
790
+ clearHistory() {
791
+ const rootTracker = this.getRootTracker();
792
+ rootTracker.history = [];
793
+ rootTracker.originalValues.clear();
794
+ rootTracker.trackedArrays.clear();
795
+ rootTracker.trackedEntities.clear();
796
+ this.captureInitialState();
797
+ }
798
+ markAsClean() {
799
+ this.clearHistory();
800
+ }
801
+ getTarget() {
802
+ return this.target;
803
+ }
804
+ }
805
+ //# sourceMappingURL=history-tracker.js.map