@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
package/src/deep-proxy.ts DELETED
@@ -1,447 +0,0 @@
1
- // ============================================================================
2
- // Deep Proxy - Core Change Tracking
3
- // ============================================================================
4
-
5
- import { Id } from "./id";
6
- import { HistoryEntry } from "./types";
7
-
8
- export class DeepProxy {
9
- private history: HistoryEntry[] = [];
10
- private subscribers: Map<string, Set<Function>> = new Map();
11
- private originalValues: Map<string, any> = new Map();
12
- private trackedArraysCloned: Map<string, any[]> = new Map();
13
- private trackedArraysOriginal: Map<string, any[]> = new Map();
14
- private globalSubscribers: Set<Function> = new Set();
15
- private updateInProgress: boolean = false;
16
- private pendingNotifications: Array<() => void> = [];
17
-
18
- constructor(
19
- private target: any,
20
- private path: string = "",
21
- private rootProxy?: DeepProxy
22
- ) {
23
- if (!rootProxy) this.rootProxy = this;
24
- }
25
-
26
- createProxy(): any {
27
- const handler: ProxyHandler<any> = {
28
- get: (target, prop, receiver) => {
29
- const value = Reflect.get(target, prop, receiver);
30
- if (!this.rootProxy) throw new Error("Root proxy is required");
31
-
32
- const currentPath = this.path
33
- ? `${this.path}.${String(prop)}`
34
- : String(prop);
35
-
36
- if (
37
- prop === "__isProxy" ||
38
- prop === "__originalTarget" ||
39
- prop === "__path" ||
40
- prop === "constructor" ||
41
- prop === "prototype"
42
- ) {
43
- return value;
44
- }
45
-
46
- if (typeof value === "function") return value.bind(target);
47
-
48
- if (Array.isArray(value)) {
49
- if (!this.rootProxy.trackedArraysCloned.has(currentPath)) {
50
- this.rootProxy.storeArrayState(currentPath, value);
51
- }
52
- return this.createArrayProxy(value, currentPath);
53
- }
54
-
55
- if (value && typeof value === "object" && !value.__isProxy) {
56
- const nestedProxy = new DeepProxy(value, currentPath, this.rootProxy);
57
- return nestedProxy.createProxy();
58
- }
59
-
60
- return value;
61
- },
62
-
63
- set: (target, prop, newValue, receiver) => {
64
- const currentPath = this.path
65
- ? `${this.path}.${String(prop)}`
66
- : String(prop);
67
- if (!this.rootProxy) throw new Error("Root proxy is required");
68
-
69
- const oldValue = Reflect.get(target, prop, receiver);
70
- const isArrayAssignment = Array.isArray(newValue);
71
-
72
- if (!isArrayAssignment && oldValue === newValue) return true;
73
-
74
- if (!this.rootProxy.originalValues.has(currentPath)) {
75
- this.rootProxy.originalValues.set(currentPath, oldValue);
76
- }
77
-
78
- this.rootProxy.history.push({
79
- path: currentPath,
80
- previousValue: oldValue,
81
- currentValue: newValue,
82
- timestamp: Date.now(),
83
- });
84
-
85
- const result = Reflect.set(target, prop, newValue, receiver);
86
-
87
- if (isArrayAssignment) {
88
- if (!this.rootProxy.trackedArraysCloned.has(currentPath)) {
89
- if (Array.isArray(oldValue)) {
90
- this.rootProxy.storeArrayState(currentPath, oldValue);
91
- } else {
92
- this.rootProxy.storeArrayState(currentPath, []);
93
- }
94
- }
95
- this.rootProxy.scheduleNotification(() =>
96
- this.rootProxy!.notifyArrayChange(currentPath, newValue)
97
- );
98
- } else {
99
- this.rootProxy.scheduleNotification(() =>
100
- this.rootProxy!.notifySubscribers(currentPath, oldValue, newValue)
101
- );
102
- }
103
-
104
- // Notify global subscribers
105
- this.rootProxy.scheduleNotification(() =>
106
- this.rootProxy!.notifyGlobalSubscribers()
107
- );
108
-
109
- this.rootProxy.flushNotifications();
110
-
111
- return result;
112
- },
113
- };
114
-
115
- const proxy = new Proxy(this.target, handler);
116
- Object.defineProperty(proxy, "__isProxy", { value: true, writable: false });
117
- return proxy;
118
- }
119
-
120
- private scheduleNotification(notification: () => void): void {
121
- this.pendingNotifications.push(notification);
122
- }
123
-
124
- private flushNotifications(): void {
125
- if (this.updateInProgress) return;
126
-
127
- this.updateInProgress = true;
128
- try {
129
- const notifications = [...this.pendingNotifications];
130
- this.pendingNotifications = [];
131
- notifications.forEach((notify) => notify());
132
- } finally {
133
- this.updateInProgress = false;
134
- }
135
- }
136
-
137
- private storeArrayState(path: string, arr: any[]): void {
138
- this.trackedArraysCloned.set(path, this.cloneArray(arr));
139
- this.trackedArraysOriginal.set(path, arr.slice());
140
- }
141
-
142
- private createArrayProxy(array: any[], path: string): any[] {
143
- const self = this;
144
- return new Proxy(array, {
145
- get(target, prop, receiver) {
146
- const value = Reflect.get(target, prop, receiver);
147
- if (!self.rootProxy) throw new Error("Root proxy is required");
148
-
149
- if (typeof value === "function") {
150
- const mutatingMethods = [
151
- "push",
152
- "pop",
153
- "shift",
154
- "unshift",
155
- "splice",
156
- "sort",
157
- "reverse",
158
- ];
159
- if (mutatingMethods.includes(String(prop))) {
160
- return function (...args: any[]) {
161
- // Capture state before mutation
162
- if (!self.rootProxy) throw new Error("Root proxy is required");
163
- const oldArray = target.slice();
164
-
165
- const result = value.apply(target, args);
166
-
167
- // Add to history
168
- self.rootProxy.history.push({
169
- path: path,
170
- previousValue: oldArray,
171
- currentValue: target.slice(),
172
- timestamp: Date.now(),
173
- });
174
-
175
- if (!self.rootProxy.trackedArraysCloned.has(path)) {
176
- self.rootProxy.storeArrayState(path, target);
177
- }
178
- self.rootProxy.notifyArrayChange(path, target);
179
- self.rootProxy.notifyGlobalSubscribers();
180
- return result;
181
- };
182
- }
183
- return value.bind(target);
184
- }
185
- if (typeof value === "object" && value !== null && !value.__isProxy) {
186
- const nestedPath = `${path}[${String(prop)}]`;
187
- const nestedProxy = new DeepProxy(value, nestedPath, self.rootProxy);
188
- return nestedProxy.createProxy();
189
- }
190
- return value;
191
- },
192
- set(target, prop, newValue, receiver) {
193
- if (!isNaN(Number(prop))) {
194
- if (!self.rootProxy) throw new Error("Root proxy is required");
195
- // Capture state before change
196
- const oldArray = target.slice();
197
-
198
- const result = Reflect.set(target, prop, newValue, receiver);
199
-
200
- // Add to history
201
- self.rootProxy.history.push({
202
- path: path,
203
- previousValue: oldArray,
204
- currentValue: target.slice(),
205
- timestamp: Date.now(),
206
- });
207
-
208
- if (!self.rootProxy.trackedArraysCloned.has(path)) {
209
- self.rootProxy.storeArrayState(path, target);
210
- }
211
- self.rootProxy.notifyArrayChange(path, target);
212
- self.rootProxy.notifyGlobalSubscribers();
213
- return result;
214
- }
215
- return Reflect.set(target, prop, newValue, receiver);
216
- },
217
- });
218
- }
219
-
220
- subscribe(path: string, callback: Function): void {
221
- if (!this.rootProxy) throw new Error("Root proxy is required");
222
-
223
- if (path === "*") {
224
- this.rootProxy.globalSubscribers.add(callback);
225
- return;
226
- }
227
-
228
- if (!this.rootProxy.subscribers.has(path)) {
229
- this.rootProxy.subscribers.set(path, new Set());
230
- }
231
-
232
- this.rootProxy.subscribers.get(path)!.add(callback);
233
-
234
- const actualValue = this.rootProxy.target[path];
235
- const isArray = Array.isArray(actualValue);
236
-
237
- // Fire callback immediately if there's a historical change for this path
238
- const lastChange = this.rootProxy.getLastChangeForPath(path);
239
- if (lastChange) {
240
- if (isArray) {
241
- // For arrays, we need to detect changes and fire with toCreate/toUpdate/toDelete
242
- const previousArray = Array.isArray(lastChange.previousValue)
243
- ? lastChange.previousValue
244
- : [];
245
- const currentArray = Array.isArray(lastChange.currentValue)
246
- ? lastChange.currentValue
247
- : actualValue;
248
-
249
- // Store the initial state before the change for comparison
250
- if (!this.rootProxy.trackedArraysCloned.has(path)) {
251
- this.rootProxy.storeArrayState(path, previousArray);
252
- }
253
-
254
- // Detect changes between previous and current
255
- const changes = this.rootProxy.detectArrayChanges(
256
- this.rootProxy.trackedArraysCloned.get(path) ||
257
- this.rootProxy.cloneArray(previousArray),
258
- previousArray,
259
- currentArray
260
- );
261
-
262
- callback({ ...changes, path });
263
-
264
- // Update tracked state to current
265
- this.rootProxy.storeArrayState(path, currentArray);
266
- } else {
267
- // For non-arrays, use the simple property change format
268
- callback({
269
- previous: lastChange.previousValue,
270
- current: lastChange.currentValue,
271
- path: lastChange.path,
272
- });
273
- }
274
- } else if (isArray && !this.rootProxy.trackedArraysCloned.has(path)) {
275
- // No historical changes, just store initial state
276
- this.rootProxy.storeArrayState(path, actualValue);
277
- }
278
- }
279
-
280
- unsubscribe(path: string, callback: Function): void {
281
- if (!this.rootProxy) throw new Error("Root proxy is required");
282
- if (path === "*") {
283
- this.rootProxy.globalSubscribers.delete(callback);
284
- return;
285
- }
286
-
287
- const subs = this.rootProxy.subscribers.get(path);
288
- if (subs) {
289
- subs.delete(callback);
290
- }
291
- }
292
-
293
- private notifySubscribers(path: string, oldValue: any, newValue: any): void {
294
- const subs = this.subscribers.get(path);
295
- if (subs) {
296
- subs.forEach((cb) => cb({ previous: oldValue, current: newValue, path }));
297
- }
298
- }
299
-
300
- private notifyGlobalSubscribers(): void {
301
- this.globalSubscribers.forEach((cb) => cb());
302
- }
303
-
304
- private notifyArrayChange(path: string, newArray: any[]): void {
305
- const subs = this.subscribers.get(path);
306
- if (!subs) return;
307
-
308
- const clonedInitial = this.trackedArraysCloned.get(path);
309
- const originalInitial = this.trackedArraysOriginal.get(path);
310
-
311
- if (!clonedInitial || !originalInitial) {
312
- this.storeArrayState(path, newArray);
313
- return;
314
- }
315
-
316
- const changes = this.detectArrayChanges(
317
- clonedInitial,
318
- originalInitial,
319
- newArray
320
- );
321
- subs.forEach((cb) => cb({ ...changes, path }));
322
- }
323
-
324
- private detectArrayChanges(
325
- oldCloned: any[],
326
- oldOriginal: any[],
327
- newArray: any[]
328
- ): { toCreate: any[]; toUpdate: any[]; toDelete: any[] } {
329
- const toCreate: any[] = [];
330
- const toUpdate: any[] = [];
331
- const toDelete: any[] = [];
332
-
333
- const oldMap = new Map<string, any>();
334
- const newMap = new Map<string, any>();
335
-
336
- oldCloned.forEach((item) => {
337
- const id = this.getItemId(item);
338
- if (id) oldMap.set(id, item);
339
- });
340
-
341
- newArray.forEach((item) => {
342
- const id = this.getItemId(item);
343
- if (id) newMap.set(id, item);
344
- });
345
-
346
- newArray.forEach((item) => {
347
- const id = this.getItemId(item);
348
- if (!id) {
349
- toCreate.push(item);
350
- } else if (!oldMap.has(id)) {
351
- toCreate.push(item);
352
- } else if (this.hasChanged(oldMap.get(id), item)) {
353
- toUpdate.push(item);
354
- }
355
- });
356
-
357
- oldOriginal.forEach((item) => {
358
- const id = this.getItemId(item);
359
- if (id && !newMap.has(id)) {
360
- toDelete.push(item);
361
- }
362
- });
363
-
364
- return { toCreate, toUpdate, toDelete };
365
- }
366
-
367
- private cloneArray(arr: any[]): any[] {
368
- return arr.map((item) => this.deepClone(item));
369
- }
370
-
371
- private deepClone(obj: any): any {
372
- if (obj === null || obj === undefined) return obj;
373
- if (typeof obj !== "object") return obj;
374
- if (obj instanceof Date) return new Date(obj.getTime());
375
- if (obj instanceof Id) return obj.value;
376
- if (obj.toJson && typeof obj.toJson === "function") return obj.toJson();
377
- if (Array.isArray(obj)) return obj.map((item) => this.deepClone(item));
378
- const cloned: any = {};
379
- for (const key in obj) {
380
- if (obj.hasOwnProperty(key)) cloned[key] = this.deepClone(obj[key]);
381
- }
382
- return cloned;
383
- }
384
-
385
- private getItemId(item: any): string | undefined {
386
- if (!item) return undefined;
387
- if (item.id instanceof Id) return item.id.value;
388
- if (item.id !== undefined) return String(item.id);
389
- if (typeof item === "object" && "id" in item) {
390
- const id = item.id;
391
- return id instanceof Id ? id.value : String(id);
392
- }
393
- return undefined;
394
- }
395
-
396
- private hasChanged(obj1: any, obj2: any): boolean {
397
- const json1 = this.normalizeAndStringify(this.deepClone(obj1));
398
- const json2 = this.normalizeAndStringify(this.deepClone(obj2));
399
- return json1 !== json2;
400
- }
401
-
402
- private normalizeAndStringify(obj: any): string {
403
- if (obj === null || typeof obj !== "object") {
404
- return JSON.stringify(obj);
405
- }
406
-
407
- if (Array.isArray(obj)) {
408
- const normalizedArray = obj.map((item) =>
409
- this.normalizeAndStringify(item)
410
- );
411
- return `[${normalizedArray.join(",")}]`;
412
- }
413
-
414
- const keys = Object.keys(obj).sort();
415
- const normalizedParts: string[] = [];
416
-
417
- for (const key of keys) {
418
- normalizedParts.push(`"${key}":${this.normalizeAndStringify(obj[key])}`);
419
- }
420
-
421
- return `{${normalizedParts.join(",")}}`;
422
- }
423
-
424
- getHistory(): HistoryEntry[] {
425
- return [...this.history];
426
- }
427
-
428
- getLastChangeForPath(path: string): HistoryEntry | undefined {
429
- for (let i = this.history.length - 1; i >= 0; i--) {
430
- if (this.history[i].path === path) {
431
- return this.history[i];
432
- }
433
- }
434
- return undefined;
435
- }
436
-
437
- clearHistory(): void {
438
- this.history = [];
439
- this.originalValues.clear();
440
- this.trackedArraysCloned.clear();
441
- this.trackedArraysOriginal.clear();
442
- }
443
-
444
- getTarget(): any {
445
- return this.target;
446
- }
447
- }
@@ -1,33 +0,0 @@
1
- // ==========================================================================
2
- // Basic Entity Tests
3
- // ==========================================================================
4
-
5
- import { Id } from "../src";
6
- import { Post } from "./utils";
7
-
8
- describe("Entity Basic Functionality", () => {
9
- it("should create an entity with id", () => {
10
- const id = new Id("1");
11
- const post = new Post({
12
- id,
13
- title: "First Post",
14
- content: "Hello World",
15
- likes: 0,
16
- });
17
-
18
- expect(post.id.value).toBe("1");
19
- expect(post.title).toBe("First Post");
20
- });
21
-
22
- it("should allow property modification", () => {
23
- const post = new Post({
24
- id: new Id("1"),
25
- title: "First Post",
26
- content: "Hello World",
27
- likes: 0,
28
- });
29
-
30
- post.title = "Updated Title";
31
- expect(post.title).toBe("Updated Title");
32
- });
33
- });