@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.
- package/dist/aggregate-changes.d.ts +56 -14
- package/dist/aggregate-changes.d.ts.map +1 -1
- package/dist/aggregate-changes.js +103 -23
- package/dist/aggregate-changes.js.map +1 -1
- package/dist/base-entity.d.ts +1 -1
- package/dist/base-entity.d.ts.map +1 -1
- package/dist/base-entity.js +28 -13
- package/dist/base-entity.js.map +1 -1
- package/dist/change-tracker.d.ts +2 -1
- package/dist/change-tracker.d.ts.map +1 -1
- package/dist/change-tracker.js +61 -35
- package/dist/change-tracker.js.map +1 -1
- package/dist/criteria.d.ts +7 -15
- package/dist/criteria.d.ts.map +1 -1
- package/dist/criteria.js +105 -81
- package/dist/criteria.js.map +1 -1
- package/dist/domain-event-bus.js +4 -4
- package/dist/domain-event-bus.js.map +1 -1
- package/dist/domain-event.js +3 -0
- package/dist/domain-event.js.map +1 -1
- package/dist/entity-changes.js +1 -0
- package/dist/entity-changes.js.map +1 -1
- package/dist/entity-schema-registry.d.ts +137 -3
- package/dist/entity-schema-registry.d.ts.map +1 -1
- package/dist/entity-schema-registry.js +160 -7
- package/dist/entity-schema-registry.js.map +1 -1
- package/dist/exceptions.js +26 -1
- package/dist/exceptions.js.map +1 -1
- package/dist/id.js +2 -0
- package/dist/id.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/paginated-result.d.ts +4 -4
- package/dist/paginated-result.d.ts.map +1 -1
- package/dist/paginated-result.js +14 -19
- package/dist/paginated-result.js.map +1 -1
- package/dist/repository/unit-of-work.js +3 -7
- package/dist/repository/unit-of-work.js.map +1 -1
- package/dist/types/change-tracker.d.ts +30 -0
- package/dist/types/change-tracker.d.ts.map +1 -1
- package/dist/types/criteria.d.ts +1 -4
- package/dist/types/criteria.d.ts.map +1 -1
- package/dist/types/domain.d.ts +2 -1
- package/dist/types/domain.d.ts.map +1 -1
- package/dist/types/utils.d.ts +2 -2
- package/dist/utils/helpers.d.ts +1 -0
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/utils/helpers.js +23 -0
- package/dist/utils/helpers.js.map +1 -1
- package/dist/validation-error.d.ts +15 -1
- package/dist/validation-error.d.ts.map +1 -1
- package/dist/validation-error.js +46 -3
- package/dist/validation-error.js.map +1 -1
- package/dist/value-object.d.ts +1 -1
- package/dist/value-object.d.ts.map +1 -1
- package/dist/value-object.js +30 -2
- package/dist/value-object.js.map +1 -1
- package/package.json +17 -3
- package/src/aggregate-changes.ts +133 -24
- package/src/base-entity.ts +22 -11
- package/src/change-tracker.ts +113 -54
- package/src/criteria.ts +151 -109
- package/src/entity-schema-registry.ts +256 -6
- package/src/index.ts +1 -1
- package/src/paginated-result.ts +21 -29
- package/src/types/change-tracker.ts +31 -0
- package/src/types/criteria.ts +1 -4
- package/src/types/domain.ts +2 -1
- package/src/types/utils.ts +2 -2
- package/src/utils/helpers.ts +28 -0
- package/src/validation-error.ts +54 -4
- package/src/value-object.ts +6 -1
- package/.versionrc.json +0 -21
- package/CHANGELOG.md +0 -163
- package/tests/aggregate-changes.test.ts +0 -284
- package/tests/criteria.test.ts +0 -716
- package/tests/depth/deep-tracking.test.ts +0 -554
- package/tests/domain-events.test.ts +0 -431
- package/tests/entity-equality.test.ts +0 -464
- package/tests/entity-schema-registry.test.ts +0 -382
- package/tests/entity-validation.test.ts +0 -252
- package/tests/history-tracker.spec.ts +0 -439
- package/tests/id.test.ts +0 -338
- package/tests/load-test/data.json +0 -347211
- package/tests/load-test/entities.ts +0 -97
- package/tests/load-test/generate-data.ts +0 -81
- package/tests/load-test/lead-to-domain.mapper.ts +0 -24
- package/tests/load-test/load.test.ts +0 -38
- package/tests/repository.test.ts +0 -635
- package/tests/to-json.test.ts +0 -99
- package/tests/utils.ts +0 -290
- package/tests/value-object-validation.test.ts +0 -219
- package/tests/value-objects.test.ts +0 -80
- package/tsconfig.json +0 -9
package/src/aggregate-changes.ts
CHANGED
|
@@ -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');
|
|
35
|
+
* const postChanges = changes.for('Post');
|
|
35
36
|
* postChanges.creates.forEach(post => {
|
|
36
|
-
* console.log(post.title);
|
|
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>(
|
|
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
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
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<
|
|
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
|
-
|
|
184
|
-
|
|
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(
|
|
241
|
+
grouped.get(key)!.ids.push(op.id);
|
|
187
242
|
}
|
|
188
243
|
|
|
189
244
|
return Array.from(grouped.entries())
|
|
190
|
-
.map(([
|
|
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
|
-
{
|
|
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
|
-
|
|
206
|
-
|
|
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(
|
|
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(([
|
|
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
|
*/
|
package/src/base-entity.ts
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
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.
|
|
389
|
-
if (obj && typeof obj.
|
|
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) {
|
package/src/change-tracker.ts
CHANGED
|
@@ -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,
|
|
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(
|
|
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(
|
|
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
|
|
480
|
-
|
|
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(
|
|
490
|
+
for (const [propName, value] of Object.entries(propsToScan)) {
|
|
485
491
|
if (propName === "id") continue;
|
|
486
492
|
|
|
487
493
|
if (Array.isArray(value)) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
1002
|
-
return obj.
|
|
1060
|
+
if (typeof obj.toJSON === "function") {
|
|
1061
|
+
return obj.toJSON();
|
|
1003
1062
|
}
|
|
1004
1063
|
|
|
1005
1064
|
if (Array.isArray(obj)) {
|