@woltz/rich-domain 1.2.4 → 1.3.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 (94) hide show
  1. package/dist/aggregate-changes.d.ts +56 -14
  2. package/dist/aggregate-changes.d.ts.map +1 -1
  3. package/dist/aggregate-changes.js +103 -23
  4. package/dist/aggregate-changes.js.map +1 -1
  5. package/dist/base-entity.d.ts +1 -1
  6. package/dist/base-entity.d.ts.map +1 -1
  7. package/dist/base-entity.js +28 -13
  8. package/dist/base-entity.js.map +1 -1
  9. package/dist/change-tracker.d.ts +2 -1
  10. package/dist/change-tracker.d.ts.map +1 -1
  11. package/dist/change-tracker.js +61 -35
  12. package/dist/change-tracker.js.map +1 -1
  13. package/dist/criteria.d.ts +7 -15
  14. package/dist/criteria.d.ts.map +1 -1
  15. package/dist/criteria.js +105 -81
  16. package/dist/criteria.js.map +1 -1
  17. package/dist/domain-event-bus.js +4 -4
  18. package/dist/domain-event-bus.js.map +1 -1
  19. package/dist/domain-event.js +3 -0
  20. package/dist/domain-event.js.map +1 -1
  21. package/dist/entity-changes.js +1 -0
  22. package/dist/entity-changes.js.map +1 -1
  23. package/dist/entity-schema-registry.d.ts +137 -3
  24. package/dist/entity-schema-registry.d.ts.map +1 -1
  25. package/dist/entity-schema-registry.js +160 -7
  26. package/dist/entity-schema-registry.js.map +1 -1
  27. package/dist/exceptions.js +26 -1
  28. package/dist/exceptions.js.map +1 -1
  29. package/dist/id.js +2 -0
  30. package/dist/id.js.map +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/paginated-result.d.ts +4 -4
  34. package/dist/paginated-result.d.ts.map +1 -1
  35. package/dist/paginated-result.js +14 -19
  36. package/dist/paginated-result.js.map +1 -1
  37. package/dist/repository/unit-of-work.js +3 -7
  38. package/dist/repository/unit-of-work.js.map +1 -1
  39. package/dist/types/change-tracker.d.ts +30 -0
  40. package/dist/types/change-tracker.d.ts.map +1 -1
  41. package/dist/types/criteria.d.ts +1 -4
  42. package/dist/types/criteria.d.ts.map +1 -1
  43. package/dist/types/domain.d.ts +2 -1
  44. package/dist/types/domain.d.ts.map +1 -1
  45. package/dist/types/utils.d.ts +2 -2
  46. package/dist/utils/helpers.d.ts +1 -0
  47. package/dist/utils/helpers.d.ts.map +1 -1
  48. package/dist/utils/helpers.js +23 -0
  49. package/dist/utils/helpers.js.map +1 -1
  50. package/dist/validation-error.d.ts +15 -1
  51. package/dist/validation-error.d.ts.map +1 -1
  52. package/dist/validation-error.js +46 -3
  53. package/dist/validation-error.js.map +1 -1
  54. package/dist/value-object.d.ts +1 -1
  55. package/dist/value-object.d.ts.map +1 -1
  56. package/dist/value-object.js +30 -2
  57. package/dist/value-object.js.map +1 -1
  58. package/package.json +17 -3
  59. package/src/aggregate-changes.ts +133 -24
  60. package/src/base-entity.ts +22 -11
  61. package/src/change-tracker.ts +113 -54
  62. package/src/criteria.ts +151 -109
  63. package/src/entity-schema-registry.ts +256 -6
  64. package/src/index.ts +1 -1
  65. package/src/paginated-result.ts +21 -29
  66. package/src/types/change-tracker.ts +31 -0
  67. package/src/types/criteria.ts +1 -4
  68. package/src/types/domain.ts +2 -1
  69. package/src/types/utils.ts +2 -2
  70. package/src/utils/helpers.ts +28 -0
  71. package/src/validation-error.ts +54 -4
  72. package/src/value-object.ts +6 -1
  73. package/.versionrc.json +0 -21
  74. package/CHANGELOG.md +0 -163
  75. package/tests/aggregate-changes.test.ts +0 -284
  76. package/tests/criteria.test.ts +0 -716
  77. package/tests/depth/deep-tracking.test.ts +0 -554
  78. package/tests/domain-events.test.ts +0 -431
  79. package/tests/entity-equality.test.ts +0 -464
  80. package/tests/entity-schema-registry.test.ts +0 -382
  81. package/tests/entity-validation.test.ts +0 -252
  82. package/tests/history-tracker.spec.ts +0 -439
  83. package/tests/id.test.ts +0 -338
  84. package/tests/load-test/data.json +0 -347211
  85. package/tests/load-test/entities.ts +0 -97
  86. package/tests/load-test/generate-data.ts +0 -81
  87. package/tests/load-test/lead-to-domain.mapper.ts +0 -24
  88. package/tests/load-test/load.test.ts +0 -38
  89. package/tests/repository.test.ts +0 -635
  90. package/tests/to-json.test.ts +0 -99
  91. package/tests/utils.ts +0 -290
  92. package/tests/value-object-validation.test.ts +0 -219
  93. package/tests/value-objects.test.ts +0 -80
  94. package/tsconfig.json +0 -9
@@ -17,6 +17,7 @@ import { EntityChanges } from "./entity-changes";
17
17
  * - Orders operations respecting FK dependencies
18
18
  * - Groups operations by entity for batch execution
19
19
  * - Provides query and iteration methods
20
+ * - Includes relationField, parentId, parentEntity for N:N support
20
21
  *
21
22
  * @example
22
23
  * ```typescript
@@ -31,9 +32,9 @@ import { EntityChanges } from "./entity-changes";
31
32
  * const changes = user.getChanges<UserEntities>();
32
33
  *
33
34
  * // Filtering by entity with autocompletion
34
- * const postChanges = changes.for('Post'); // 'Post' autocompletes
35
+ * const postChanges = changes.for('Post');
35
36
  * postChanges.creates.forEach(post => {
36
- * console.log(post.title); // 'post' is typed as Post
37
+ * console.log(post.title);
37
38
  * });
38
39
  * ```
39
40
  */
@@ -46,13 +47,21 @@ export class AggregateChanges<TEntityMap = Record<string, any>> {
46
47
 
47
48
  /**
48
49
  * Adds a create operation.
50
+ *
51
+ * @param entity - Entity name
52
+ * @param data - Entity data
53
+ * @param depth - Depth in the aggregate tree
54
+ * @param parentId - Parent entity ID (for FK)
55
+ * @param parentEntity - Parent entity name
56
+ * @param relationField - Name of the relation field in parent (e.g., 'tags', 'comments')
49
57
  */
50
58
  addCreate<T>(
51
59
  entity: string,
52
60
  data: T,
53
61
  depth: number,
54
62
  parentId?: string,
55
- parentEntity?: string
63
+ parentEntity?: string,
64
+ relationField?: string
56
65
  ): void {
57
66
  this.ops.push({
58
67
  type: "create",
@@ -61,6 +70,7 @@ export class AggregateChanges<TEntityMap = Record<string, any>> {
61
70
  depth,
62
71
  parentId,
63
72
  parentEntity,
73
+ relationField,
64
74
  } as CreateOperation<T>);
65
75
  }
66
76
 
@@ -86,14 +96,33 @@ export class AggregateChanges<TEntityMap = Record<string, any>> {
86
96
 
87
97
  /**
88
98
  * Adds a delete operation.
99
+ *
100
+ * @param entity - Entity name
101
+ * @param id - Entity ID
102
+ * @param data - Entity data (for reference)
103
+ * @param depth - Depth in the aggregate tree
104
+ * @param relationField - Name of the relation field in parent (e.g., 'tags', 'comments')
105
+ * @param parentId - Parent entity ID (for N:N disconnect)
106
+ * @param parentEntity - Parent entity name (for N:N disconnect)
89
107
  */
90
- addDelete<T>(entity: string, id: string, data: T, depth: number): void {
108
+ addDelete<T>(
109
+ entity: string,
110
+ id: string,
111
+ data: T,
112
+ depth: number,
113
+ relationField?: string,
114
+ parentId?: string,
115
+ parentEntity?: string
116
+ ): void {
91
117
  this.ops.push({
92
118
  type: "delete",
93
119
  entity,
94
120
  id,
95
121
  data,
96
122
  depth,
123
+ relationField,
124
+ parentId,
125
+ parentEntity,
97
126
  } as DeleteOperation<T>);
98
127
  }
99
128
 
@@ -145,8 +174,8 @@ export class AggregateChanges<TEntityMap = Record<string, any>> {
145
174
  * Converts the changes into BatchOperations for optimized execution.
146
175
  *
147
176
  * Groups operations by entity and sorts by depth:
148
- * - Deletes: depth DESC (leaf → root)
149
- * - Creates: depth ASC (root → leaf)
177
+ * - Deletes: depth DESC (leaf → root), grouped by entity + relationField + parentId
178
+ * - Creates: depth ASC (root → leaf), grouped by entity + relationField
150
179
  * - Updates: grouped by entity
151
180
  *
152
181
  * @example
@@ -155,12 +184,16 @@ export class AggregateChanges<TEntityMap = Record<string, any>> {
155
184
  *
156
185
  * // Run deletes
157
186
  * for (const del of batch.deletes) {
158
- * await tx[del.entity].deleteMany({ where: { id: { in: del.ids } } });
159
- * }
160
- *
161
- * // Run creates
162
- * for (const create of batch.creates) {
163
- * await tx[create.entity].createMany({ data: create.items });
187
+ * if (registry.isReferenceCollection(del.parentEntity, del.relationField)) {
188
+ * // N:N - disconnect only
189
+ * await prisma[del.parentEntity].update({
190
+ * where: { id: del.parentId },
191
+ * data: { [del.relationField]: { disconnect: del.ids.map(id => ({ id })) } }
192
+ * });
193
+ * } else {
194
+ * // 1:N - delete entities
195
+ * await prisma[del.entity].deleteMany({ where: { id: { in: del.ids } } });
196
+ * }
164
197
  * }
165
198
  * ```
166
199
  */
@@ -173,46 +206,90 @@ export class AggregateChanges<TEntityMap = Record<string, any>> {
173
206
  }
174
207
 
175
208
  /**
176
- * Groups deletes by entity, sorted by descending depth.
209
+ * Groups deletes by entity + relationField + parentId, sorted by descending depth.
210
+ *
211
+ * For N:N relations, we need to group by parentId because disconnect
212
+ * operations are performed on the parent entity.
177
213
  */
178
214
  private groupDeletes(): BatchOperations["deletes"] {
179
215
  const deleteOps = this.deletes();
180
- const grouped = new Map<string, { depth: number; ids: string[] }>();
216
+ const grouped = new Map<
217
+ string,
218
+ {
219
+ depth: number;
220
+ ids: string[];
221
+ relationField?: string;
222
+ parentEntity?: string;
223
+ parentId?: string;
224
+ }
225
+ >();
181
226
 
182
227
  for (const op of deleteOps) {
183
- if (!grouped.has(op.entity)) {
184
- grouped.set(op.entity, { depth: op.depth, ids: [] });
228
+ // Group by entity + relationField + parentId
229
+ // This ensures N:N disconnects are grouped per parent
230
+ const key = `${op.entity}:${op.relationField ?? ""}:${op.parentId ?? ""}`;
231
+
232
+ if (!grouped.has(key)) {
233
+ grouped.set(key, {
234
+ depth: op.depth,
235
+ ids: [],
236
+ relationField: op.relationField,
237
+ parentEntity: op.parentEntity,
238
+ parentId: op.parentId,
239
+ });
185
240
  }
186
- grouped.get(op.entity)!.ids.push(op.id);
241
+ grouped.get(key)!.ids.push(op.id);
187
242
  }
188
243
 
189
244
  return Array.from(grouped.entries())
190
- .map(([entity, { depth, ids }]) => ({ entity, depth, ids }))
245
+ .map(([key, { depth, ids, relationField, parentEntity, parentId }]) => {
246
+ const entity = key.split(":")[0];
247
+ return { entity, depth, ids, parentId, relationField, parentEntity };
248
+ })
191
249
  .sort((a, b) => b.depth - a.depth);
192
250
  }
193
251
 
194
252
  /**
195
- * Groups creates by entity, sorted by ascending depth.
253
+ * Groups creates by entity + relationField, sorted by ascending depth.
254
+ *
255
+ * Preserves parentEntity for N:N connect operations.
196
256
  */
197
257
  private groupCreates(): BatchOperations["creates"] {
198
258
  const createOps = this.creates();
199
259
  const grouped = new Map<
200
260
  string,
201
- { depth: number; items: BatchCreateItem[] }
261
+ {
262
+ depth: number;
263
+ items: BatchCreateItem[];
264
+ relationField?: string;
265
+ parentEntity?: string;
266
+ }
202
267
  >();
203
268
 
204
269
  for (const op of createOps) {
205
- if (!grouped.has(op.entity)) {
206
- grouped.set(op.entity, { depth: op.depth, items: [] });
270
+ const key = `${op.entity}:${op.relationField ?? ""}`;
271
+
272
+ if (!grouped.has(key)) {
273
+ grouped.set(key, {
274
+ depth: op.depth,
275
+ items: [],
276
+ relationField: op.relationField,
277
+ parentEntity: op.parentEntity,
278
+ });
207
279
  }
208
- grouped.get(op.entity)!.items.push({
280
+ grouped.get(key)!.items.push({
209
281
  data: op.data,
210
282
  parentId: op.parentId,
283
+ parentEntity: op.parentEntity,
284
+ relationField: op.relationField,
211
285
  });
212
286
  }
213
287
 
214
288
  return Array.from(grouped.entries())
215
- .map(([entity, { depth, items }]) => ({ entity, depth, items }))
289
+ .map(([key, { depth, items, relationField, parentEntity }]) => {
290
+ const entity = key.split(":")[0];
291
+ return { entity, depth, items, relationField, parentEntity };
292
+ })
216
293
  .sort((a, b) => a.depth - b.depth);
217
294
  }
218
295
 
@@ -261,6 +338,25 @@ export class AggregateChanges<TEntityMap = Record<string, any>> {
261
338
  return new EntityChanges<TEntityMap[K]>(filtered);
262
339
  }
263
340
 
341
+ /**
342
+ * Filters changes by relation field.
343
+ *
344
+ * @param relationField - Name of the relation field (e.g., 'tags', 'comments')
345
+ * @returns New AggregateChanges containing only operations for this relation
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * const tagChanges = changes.forRelation('tags');
350
+ * // Contains only creates/deletes for the 'tags' relation
351
+ * ```
352
+ */
353
+ forRelation(relationField: string): AggregateChanges<TEntityMap> {
354
+ const filtered = this.ops.filter(
355
+ (op) => op.relationField === relationField
356
+ );
357
+ return new AggregateChanges<TEntityMap>(filtered);
358
+ }
359
+
264
360
  /**
265
361
  * Checks if there are create operations.
266
362
  */
@@ -319,6 +415,19 @@ export class AggregateChanges<TEntityMap = Record<string, any>> {
319
415
  return Array.from(entities);
320
416
  }
321
417
 
418
+ /**
419
+ * Lists all relation fields that have changes.
420
+ */
421
+ getAffectedRelations(): string[] {
422
+ const relations = new Set<string>();
423
+ this.ops.forEach((op) => {
424
+ if (op.relationField) {
425
+ relations.add(op.relationField);
426
+ }
427
+ });
428
+ return Array.from(relations);
429
+ }
430
+
322
431
  /**
323
432
  * Clears all operations.
324
433
  */
@@ -41,8 +41,17 @@ export abstract class BaseEntity<T extends BaseProps> {
41
41
  this,
42
42
  "validation"
43
43
  );
44
+
44
45
  const hooks = getStaticProperty<EntityHooks<T, any>>(this, "hooks");
45
46
 
47
+ if (!props.id) {
48
+ props.id = new Id();
49
+ }
50
+
51
+ if (hooks?.onBeforeCreate) {
52
+ hooks.onBeforeCreate(props as T);
53
+ }
54
+
46
55
  this.entityHooks = hooks;
47
56
 
48
57
  if (validation?.schema) {
@@ -56,10 +65,6 @@ export abstract class BaseEntity<T extends BaseProps> {
56
65
 
57
66
  let finalProps = { ...props } as T;
58
67
 
59
- if (!finalProps.id) {
60
- finalProps.id = new Id();
61
- }
62
-
63
68
  if (this.entitySchema && this.validationConfig.onCreate) {
64
69
  this.validateProps(finalProps);
65
70
  }
@@ -100,7 +105,10 @@ export abstract class BaseEntity<T extends BaseProps> {
100
105
  result.issues.map((issue) => ({
101
106
  path: issue.path?.map((p) => this.extractPathKey(p)) || [],
102
107
  message: issue.message,
103
- }))
108
+ })),
109
+ {
110
+ entityName: this.constructor.name,
111
+ }
104
112
  );
105
113
 
106
114
  if (this.validationConfig.throwOnError) {
@@ -134,7 +142,7 @@ export abstract class BaseEntity<T extends BaseProps> {
134
142
  private setupUpdateValidation(): void {
135
143
  const self = this;
136
144
 
137
- this.tracker.setOnChangeValidator((path, oldValue, newValue) => {
145
+ this.tracker.setOnChangeValidator((path, newValue) => {
138
146
  const originalValue = self._props[path as keyof T];
139
147
  (self._props as any)[path] = newValue;
140
148
 
@@ -166,7 +174,10 @@ export abstract class BaseEntity<T extends BaseProps> {
166
174
  result.issues.map((issue) => ({
167
175
  path: issue.path?.map((p) => self.extractPathKey(p)) || [],
168
176
  message: issue.message,
169
- }))
177
+ })),
178
+ {
179
+ entityName: self.constructor.name,
180
+ }
170
181
  );
171
182
 
172
183
  (self._props as any)[path] = originalValue;
@@ -228,7 +239,7 @@ export abstract class BaseEntity<T extends BaseProps> {
228
239
  obj.constructor.name !== "Array"
229
240
  ) {
230
241
  if (
231
- typeof obj.toJson === "function" &&
242
+ typeof obj.toJSON === "function" &&
232
243
  typeof obj.equals === "function"
233
244
  ) {
234
245
  return obj;
@@ -376,7 +387,7 @@ export abstract class BaseEntity<T extends BaseProps> {
376
387
  return this.domainEvents.length > 0;
377
388
  }
378
389
 
379
- toJson(): DeepJsonResult<T> {
390
+ toJSON(): DeepJsonResult<T> {
380
391
  return this.deepToJson(this._props) as DeepJsonResult<T>;
381
392
  }
382
393
 
@@ -385,8 +396,8 @@ export abstract class BaseEntity<T extends BaseProps> {
385
396
  if (obj instanceof Id) return obj.value;
386
397
  if (obj instanceof Date) return obj.toISOString();
387
398
  if (Array.isArray(obj)) return obj.map((item) => this.deepToJson(item));
388
- if (obj instanceof BaseEntity) return obj.toJson();
389
- if (obj && typeof obj.toJson === "function") return obj.toJson();
399
+ if (obj instanceof BaseEntity) return obj.toJSON();
400
+ if (obj && typeof obj.toJSON === "function") return obj.toJSON();
390
401
  if (typeof obj === "object") {
391
402
  const result: any = {};
392
403
  for (const key in obj) {
@@ -9,11 +9,7 @@ import { AggregateChanges } from "./aggregate-changes";
9
9
  * Callback for validation on property change.
10
10
  * Return false to reject the change, or throw an error.
11
11
  */
12
- export type OnChangeValidator = (
13
- path: string,
14
- oldValue: any,
15
- newValue: any
16
- ) => boolean | void;
12
+ export type OnChangeValidator = (path: string, newValue: any) => boolean | void;
17
13
 
18
14
  /**
19
15
  * Tracks changes in Aggregates using Proxy.
@@ -39,7 +35,9 @@ export class ChangeTracker {
39
35
  private rootEntityName: string,
40
36
  private path: string = "",
41
37
  private depth: number = 0,
38
+ // @ts-expect-error - This is a private property
42
39
  private parentId?: string,
40
+ // @ts-expect-error - This is a private property
43
41
  private parentEntity?: string,
44
42
  private rootTracker?: ChangeTracker
45
43
  ) {
@@ -195,11 +193,7 @@ export class ChangeTracker {
195
193
  const rootTracker = this.getRootTracker();
196
194
  if (rootTracker.onChangeValidator) {
197
195
  try {
198
- const result = rootTracker.onChangeValidator(
199
- currentPath,
200
- oldValue,
201
- newValue
202
- );
196
+ const result = rootTracker.onChangeValidator(currentPath, newValue);
203
197
  if (result === false) {
204
198
  return true;
205
199
  }
@@ -272,7 +266,7 @@ export class ChangeTracker {
272
266
 
273
267
  if (rootTracker.onChangeValidator) {
274
268
  try {
275
- const result = rootTracker.onChangeValidator(path, oldArray, [
269
+ const result = rootTracker.onChangeValidator(path, [
276
270
  ...oldArray,
277
271
  ...args,
278
272
  ]);
@@ -322,11 +316,7 @@ export class ChangeTracker {
322
316
 
323
317
  if (rootTracker.onChangeValidator) {
324
318
  try {
325
- const result = rootTracker.onChangeValidator(
326
- path,
327
- oldArray,
328
- newValue
329
- );
319
+ const result = rootTracker.onChangeValidator(path, newValue);
330
320
  if (result === false) {
331
321
  return true;
332
322
  }
@@ -431,9 +421,18 @@ export class ChangeTracker {
431
421
 
432
422
  const { depth, parentId, parentEntity } = arrayState.metadata;
433
423
 
424
+ const relationField = this.extractRelationField(path);
425
+
434
426
  for (const item of created) {
435
427
  const itemEntityName = this.getEntityName(item);
436
- changes.addCreate(itemEntityName, item, depth, parentId, parentEntity);
428
+ changes.addCreate(
429
+ itemEntityName,
430
+ item,
431
+ depth,
432
+ parentId,
433
+ parentEntity,
434
+ relationField
435
+ );
437
436
 
438
437
  this.markNestedItemsAsCreated(item, depth, changes);
439
438
  }
@@ -458,7 +457,15 @@ export class ChangeTracker {
458
457
  if (id || key) {
459
458
  const itemEntityName = this.getEntityName(item);
460
459
  const deleteId = id || key!;
461
- changes.addDelete(itemEntityName, deleteId, item, depth);
460
+ changes.addDelete(
461
+ itemEntityName,
462
+ deleteId,
463
+ item,
464
+ depth,
465
+ relationField,
466
+ parentId,
467
+ parentEntity
468
+ );
462
469
 
463
470
  this.markNestedItemsAsDeleted(item, depth, changes, rootTracker);
464
471
  }
@@ -476,37 +483,41 @@ export class ChangeTracker {
476
483
  ): void {
477
484
  if (!item || typeof item !== "object") return;
478
485
 
479
- const itemId = this.getEntityId(item);
480
- if (!itemId) return;
481
-
482
- const props = item.props || item;
486
+ const propsToScan = item.props || item;
487
+ const parentId = this.getEntityId(item);
488
+ const parentEntity = this.getEntityName(item);
483
489
 
484
- for (const [propName, value] of Object.entries(props)) {
490
+ for (const [propName, value] of Object.entries(propsToScan)) {
485
491
  if (propName === "id") continue;
486
492
 
487
493
  if (Array.isArray(value)) {
488
- for (const nestedItem of value) {
489
- if (this.isEntityOrVO(nestedItem)) {
490
- const nestedId = this.getEntityId(nestedItem);
491
- const nestedKey = this.getItemKey(nestedItem);
492
- if (nestedId || nestedKey) {
493
- const entityName = this.getEntityName(nestedItem);
494
- changes.addCreate(
495
- entityName,
496
- nestedItem,
497
- parentDepth + 1,
498
- itemId,
499
- this.getEntityName(item)
500
- );
501
-
502
- this.markNestedItemsAsCreated(
503
- nestedItem,
504
- parentDepth + 1,
505
- changes
506
- );
507
- }
494
+ const relationField = propName;
495
+
496
+ for (const child of value) {
497
+ if (this.isEntityOrVO(child)) {
498
+ const childEntityName = this.getEntityName(child);
499
+ changes.addCreate(
500
+ childEntityName,
501
+ child,
502
+ parentDepth + 1,
503
+ parentId,
504
+ parentEntity,
505
+ relationField
506
+ );
507
+ this.markNestedItemsAsCreated(child, parentDepth + 1, changes);
508
508
  }
509
509
  }
510
+ } else if (this.isEntityOrVO(value)) {
511
+ const childEntityName = this.getEntityName(value);
512
+ changes.addCreate(
513
+ childEntityName,
514
+ value,
515
+ parentDepth + 1,
516
+ parentId,
517
+ parentEntity,
518
+ propName
519
+ );
520
+ this.markNestedItemsAsCreated(value, parentDepth + 1, changes);
510
521
  }
511
522
  }
512
523
  }
@@ -526,8 +537,12 @@ export class ChangeTracker {
526
537
  const itemId = this.getEntityId(item);
527
538
  if (!itemId) return;
528
539
 
529
- for (const [, arrayState] of rootTracker.trackedArrays) {
540
+ for (const [path, arrayState] of rootTracker.trackedArrays) {
530
541
  if (arrayState.metadata.parentId === itemId) {
542
+ const relationField = this.extractRelationField(path);
543
+ const parentEntity = arrayState.metadata.parentEntity;
544
+ const parentId = arrayState.metadata.parentId;
545
+
531
546
  for (const nestedItem of arrayState.cloned) {
532
547
  const id =
533
548
  typeof nestedItem === "object" && nestedItem !== null
@@ -535,7 +550,15 @@ export class ChangeTracker {
535
550
  : undefined;
536
551
  if (id) {
537
552
  const entityName = arrayState.metadata.entityName;
538
- changes.addDelete(entityName, id, nestedItem, parentDepth + 1);
553
+ changes.addDelete(
554
+ entityName,
555
+ id,
556
+ nestedItem,
557
+ parentDepth + 1,
558
+ relationField,
559
+ parentEntity,
560
+ parentId
561
+ );
539
562
 
540
563
  this.markNestedJsonItemAsDeleted(
541
564
  id,
@@ -559,21 +582,28 @@ export class ChangeTracker {
559
582
  changes: AggregateChanges<any>,
560
583
  rootTracker: ChangeTracker
561
584
  ): void {
562
- for (const [, arrayState] of rootTracker.trackedArrays) {
585
+ for (const [path, arrayState] of rootTracker.trackedArrays) {
563
586
  if (arrayState.metadata.parentId === itemId) {
587
+ const relationField = this.extractRelationField(path);
588
+
564
589
  for (const nestedJsonItem of arrayState.cloned) {
565
590
  if (typeof nestedJsonItem !== "object" || nestedJsonItem === null)
566
591
  continue;
567
592
 
568
593
  const nestedId = nestedJsonItem.id;
569
594
  const entityName = arrayState.metadata.entityName;
595
+ const parentEntity = arrayState.metadata.parentEntity;
596
+ const parentId = arrayState.metadata.parentId;
570
597
 
571
598
  if (nestedId) {
572
599
  changes.addDelete(
573
600
  entityName,
574
601
  nestedId,
575
602
  nestedJsonItem,
576
- parentDepth + 1
603
+ parentDepth + 1,
604
+ relationField,
605
+ parentId,
606
+ parentEntity
577
607
  );
578
608
 
579
609
  this.markNestedJsonItemAsDeleted(
@@ -592,7 +622,10 @@ export class ChangeTracker {
592
622
  entityName,
593
623
  key,
594
624
  nestedJsonItem,
595
- parentDepth + 1
625
+ parentDepth + 1,
626
+ relationField,
627
+ parentId,
628
+ parentEntity
596
629
  );
597
630
  }
598
631
  }
@@ -668,6 +701,8 @@ export class ChangeTracker {
668
701
  const { entityName, depth, parentId, parentEntity } =
669
702
  trackedItem.metadata;
670
703
 
704
+ const relationField = this.extractRelationField(path);
705
+
671
706
  const state = this.detectEntityChangeState(originalValue, currentValue);
672
707
 
673
708
  switch (state) {
@@ -677,28 +712,46 @@ export class ChangeTracker {
677
712
  currentValue,
678
713
  depth,
679
714
  parentId,
680
- parentEntity
715
+ parentEntity,
716
+ relationField
681
717
  );
682
718
  break;
683
719
 
684
720
  case "deleted":
685
721
  const id = this.getEntityId(originalValue);
686
722
  if (id) {
687
- changes.addDelete(entityName, id, originalEntity, depth);
723
+ changes.addDelete(
724
+ entityName,
725
+ id,
726
+ originalEntity,
727
+ depth,
728
+ relationField,
729
+ parentId,
730
+ parentEntity
731
+ );
688
732
  }
689
733
  break;
690
734
 
691
735
  case "replaced":
692
736
  const oldId = this.getEntityId(originalValue);
693
737
  if (oldId) {
694
- changes.addDelete(entityName, oldId, originalEntity, depth);
738
+ changes.addDelete(
739
+ entityName,
740
+ oldId,
741
+ originalEntity,
742
+ depth,
743
+ relationField,
744
+ parentId,
745
+ parentEntity
746
+ );
695
747
  }
696
748
  changes.addCreate(
697
749
  entityName,
698
750
  currentValue,
699
751
  depth,
700
752
  parentId,
701
- parentEntity
753
+ parentEntity,
754
+ relationField
702
755
  );
703
756
  break;
704
757
 
@@ -894,6 +947,12 @@ export class ChangeTracker {
894
947
  return current;
895
948
  }
896
949
 
950
+ private extractRelationField(path: string): string {
951
+ const withoutIndices = path.replace(/\[\d+\]/g, "");
952
+ const parts = withoutIndices.split(".");
953
+ return parts[parts.length - 1];
954
+ }
955
+
897
956
  private getItemKey(item: any): string | undefined {
898
957
  const id = this.getEntityId(item);
899
958
  if (id) return id;
@@ -998,8 +1057,8 @@ export class ChangeTracker {
998
1057
  return obj.value;
999
1058
  }
1000
1059
 
1001
- if (typeof obj.toJson === "function") {
1002
- return obj.toJson();
1060
+ if (typeof obj.toJSON === "function") {
1061
+ return obj.toJSON();
1003
1062
  }
1004
1063
 
1005
1064
  if (Array.isArray(obj)) {