@woltz/rich-domain 0.2.2 → 1.1.0

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 (148) hide show
  1. package/CHANGELOG.md +23 -75
  2. package/LICENSE +20 -20
  3. package/README.md +37 -20
  4. package/dist/base-entity.d.ts +2 -2
  5. package/dist/base-entity.d.ts.map +1 -1
  6. package/dist/base-entity.js +6 -4
  7. package/dist/base-entity.js.map +1 -1
  8. package/dist/criteria.d.ts +5 -11
  9. package/dist/criteria.d.ts.map +1 -1
  10. package/dist/criteria.js +4 -3
  11. package/dist/criteria.js.map +1 -1
  12. package/dist/deep-proxy.d.ts +3 -1
  13. package/dist/deep-proxy.d.ts.map +1 -1
  14. package/dist/deep-proxy.js +116 -29
  15. package/dist/deep-proxy.js.map +1 -1
  16. package/dist/domain-event-bus.d.ts +5 -6
  17. package/dist/domain-event-bus.d.ts.map +1 -1
  18. package/dist/domain-event-bus.js +3 -11
  19. package/dist/domain-event-bus.js.map +1 -1
  20. package/dist/domain-event.d.ts +1 -31
  21. package/dist/domain-event.d.ts.map +1 -1
  22. package/dist/domain-event.js +2 -1
  23. package/dist/domain-event.js.map +1 -1
  24. package/dist/entity.d.ts +2 -2
  25. package/dist/entity.js +1 -1
  26. package/dist/exceptions.d.ts +251 -0
  27. package/dist/exceptions.d.ts.map +1 -0
  28. package/dist/exceptions.js +321 -0
  29. package/dist/exceptions.js.map +1 -0
  30. package/dist/id.d.ts +3 -3
  31. package/dist/id.d.ts.map +1 -1
  32. package/dist/id.js +15 -4
  33. package/dist/id.js.map +1 -1
  34. package/dist/index.d.ts +2 -5
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +2 -8
  37. package/dist/index.js.map +1 -1
  38. package/dist/paginated-result.d.ts.map +1 -1
  39. package/dist/paginated-result.js +12 -1
  40. package/dist/paginated-result.js.map +1 -1
  41. package/dist/repository/index.d.ts +2 -39
  42. package/dist/repository/index.d.ts.map +1 -1
  43. package/dist/repository/index.js +2 -39
  44. package/dist/repository/index.js.map +1 -1
  45. package/dist/repository/unit-of-work.d.ts +0 -11
  46. package/dist/repository/unit-of-work.d.ts.map +1 -1
  47. package/dist/repository/unit-of-work.js +0 -35
  48. package/dist/repository/unit-of-work.js.map +1 -1
  49. package/dist/types/criteria.d.ts +6 -2
  50. package/dist/types/criteria.d.ts.map +1 -1
  51. package/dist/types/criteria.js +1 -1
  52. package/dist/types/criteria.js.map +1 -1
  53. package/dist/types/domain-event.d.ts +32 -0
  54. package/dist/types/domain-event.d.ts.map +1 -0
  55. package/dist/types/domain-event.js +2 -0
  56. package/dist/types/domain-event.js.map +1 -0
  57. package/dist/types/domain.d.ts +2 -2
  58. package/dist/types/domain.d.ts.map +1 -1
  59. package/dist/types/history-tracker.d.ts +1 -1
  60. package/dist/types/history-tracker.d.ts.map +1 -1
  61. package/dist/types/index.d.ts +1 -0
  62. package/dist/types/index.d.ts.map +1 -1
  63. package/dist/types/index.js +1 -0
  64. package/dist/types/index.js.map +1 -1
  65. package/dist/value-object.d.ts +1 -1
  66. package/dist/value-object.d.ts.map +1 -1
  67. package/dist/value-object.js +2 -5
  68. package/dist/value-object.js.map +1 -1
  69. package/eslint.config.js +3 -3
  70. package/jest.config.js +1 -1
  71. package/package.json +14 -20
  72. package/src/base-entity.ts +6 -5
  73. package/src/criteria.ts +11 -11
  74. package/src/deep-proxy.ts +447 -339
  75. package/src/domain-event-bus.ts +152 -166
  76. package/src/domain-event.ts +53 -90
  77. package/src/entity.ts +16 -16
  78. package/src/exceptions.ts +435 -0
  79. package/src/id.ts +107 -94
  80. package/src/index.ts +26 -9
  81. package/src/paginated-result.ts +14 -1
  82. package/src/repository/index.ts +2 -44
  83. package/src/repository/unit-of-work.ts +1 -44
  84. package/src/types/criteria.ts +7 -2
  85. package/src/types/domain-event.ts +38 -0
  86. package/src/types/domain.ts +2 -3
  87. package/src/types/history-tracker.ts +1 -1
  88. package/src/types/index.ts +1 -0
  89. package/src/validation-error.ts +97 -97
  90. package/src/value-object.ts +3 -6
  91. package/tests/criteria.test.ts +8 -0
  92. package/tests/domain-events.test.ts +431 -445
  93. package/tests/entity-validation.test.ts +2 -2
  94. package/tests/entity.test.ts +33 -33
  95. package/tests/history-tracker.spec.ts +57 -17
  96. package/tests/id.test.ts +341 -341
  97. package/tests/repository.test.ts +8 -4
  98. package/tests/to-json.test.ts +103 -91
  99. package/tests/utils.ts +254 -151
  100. package/tests/value-object-validation.test.ts +0 -9
  101. package/tests/value-objects.test.ts +52 -52
  102. package/tsconfig.json +2 -24
  103. package/.github/workflows/ci.yml +0 -40
  104. package/.husky/commit-msg +0 -1
  105. package/.husky/pre-commit +0 -1
  106. package/.vscode/settings.json +0 -3
  107. package/commitlint.config.js +0 -23
  108. package/dist/filtering.d.ts +0 -107
  109. package/dist/filtering.d.ts.map +0 -1
  110. package/dist/filtering.js +0 -202
  111. package/dist/filtering.js.map +0 -1
  112. package/dist/ordering.d.ts +0 -93
  113. package/dist/ordering.d.ts.map +0 -1
  114. package/dist/ordering.js +0 -154
  115. package/dist/ordering.js.map +0 -1
  116. package/dist/pagination.d.ts +0 -218
  117. package/dist/pagination.d.ts.map +0 -1
  118. package/dist/pagination.js +0 -281
  119. package/dist/pagination.js.map +0 -1
  120. package/dist/repository/in-memory-repository.d.ts +0 -50
  121. package/dist/repository/in-memory-repository.d.ts.map +0 -1
  122. package/dist/repository/in-memory-repository.js +0 -93
  123. package/dist/repository/in-memory-repository.js.map +0 -1
  124. package/dist/repository/mapper.d.ts +0 -56
  125. package/dist/repository/mapper.d.ts.map +0 -1
  126. package/dist/repository/mapper.js +0 -15
  127. package/dist/repository/mapper.js.map +0 -1
  128. package/dist/repository/types.d.ts +0 -87
  129. package/dist/repository/types.d.ts.map +0 -1
  130. package/dist/repository/types.js +0 -6
  131. package/dist/repository/types.js.map +0 -1
  132. package/dist/repository.d.ts +0 -2
  133. package/dist/repository.d.ts.map +0 -1
  134. package/dist/repository.js +0 -21
  135. package/dist/repository.js.map +0 -1
  136. package/dist/specification.d.ts +0 -102
  137. package/dist/specification.d.ts.map +0 -1
  138. package/dist/specification.js +0 -187
  139. package/dist/specification.js.map +0 -1
  140. package/dist/types/repository.d.ts +0 -43
  141. package/dist/types/repository.d.ts.map +0 -1
  142. package/dist/types/repository.js +0 -2
  143. package/dist/types/repository.js.map +0 -1
  144. package/dist/types.d.ts +0 -88
  145. package/dist/types.d.ts.map +0 -1
  146. package/dist/types.js +0 -12
  147. package/dist/types.js.map +0 -1
  148. package/src/repository/in-memory-repository.ts +0 -116
package/tests/id.test.ts CHANGED
@@ -1,341 +1,341 @@
1
- import { Id } from '../src';
2
- import { Address, Post, User } from './utils';
3
-
4
- // ============================================================================
5
- // Id Class Tests
6
- // ============================================================================
7
-
8
- describe('Id Class', () => {
9
- describe('Construction', () => {
10
- it('should generate UUID when no value provided', () => {
11
- const id = new Id();
12
-
13
- expect(id.value).toBeDefined();
14
- expect(typeof id.value).toBe('string');
15
- expect(id.value.length).toBeGreaterThan(0);
16
- // UUID v4 format check
17
- expect(id.value).toMatch(
18
- /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
19
- );
20
- });
21
-
22
- it('should mark as new when no value provided', () => {
23
- const id = new Id();
24
-
25
- expect(id.isNew).toBe(true);
26
- });
27
-
28
- it('should use provided value', () => {
29
- const existingId = '550e8400-e29b-41d4-a716-446655440000';
30
- const id = new Id(existingId);
31
-
32
- expect(id.value).toBe(existingId);
33
- });
34
-
35
- it('should mark as NOT new when value provided', () => {
36
- const id = new Id('existing-id');
37
-
38
- expect(id.isNew).toBe(false);
39
- });
40
-
41
- it('should generate unique IDs', () => {
42
- const id1 = new Id();
43
- const id2 = new Id();
44
- const id3 = new Id();
45
-
46
- expect(id1.value).not.toBe(id2.value);
47
- expect(id2.value).not.toBe(id3.value);
48
- expect(id1.value).not.toBe(id3.value);
49
- });
50
- });
51
-
52
- describe('Static Methods', () => {
53
- it('should create new Id with Id.create()', () => {
54
- const id = Id.create();
55
-
56
- expect(id.isNew).toBe(true);
57
- expect(id.value).toBeDefined();
58
- });
59
-
60
- it('should create existing Id with Id.from()', () => {
61
- const id = Id.from('existing-id');
62
-
63
- expect(id.isNew).toBe(false);
64
- expect(id.value).toBe('existing-id');
65
- });
66
- });
67
-
68
- describe('String Conversion', () => {
69
- it('should convert to string with toString()', () => {
70
- const id = new Id('test-id');
71
-
72
- expect(id.toString()).toBe('test-id');
73
- expect(String(id)).toBe('test-id');
74
- });
75
-
76
- it('should convert to JSON', () => {
77
- const id = new Id('test-id');
78
-
79
- expect(id.toJSON()).toBe('test-id');
80
- expect(JSON.stringify(id)).toBe('"test-id"');
81
- });
82
- });
83
-
84
- describe('Equality', () => {
85
- it('should compare with another Id', () => {
86
- const id1 = new Id('same-id');
87
- const id2 = new Id('same-id');
88
- const id3 = new Id('different-id');
89
-
90
- expect(id1.equals(id2)).toBe(true);
91
- expect(id1.equals(id3)).toBe(false);
92
- });
93
-
94
- it('should compare with string', () => {
95
- const id = new Id('test-id');
96
-
97
- expect(id.equals('test-id')).toBe(true);
98
- expect(id.equals('other-id')).toBe(false);
99
- });
100
- });
101
- });
102
-
103
- // ============================================================================
104
- // Entity with Id Tests
105
- // ============================================================================
106
-
107
- describe('Entity with Id Class', () => {
108
- describe('New Entity', () => {
109
- it('should be new when Id is auto-generated', () => {
110
- const post = new Post({
111
- id: new Id(), // No value = new
112
- title: 'New Post',
113
- content: 'Content',
114
- likes: 0,
115
- });
116
-
117
- expect(post.isNew).toBe(true);
118
- expect(post.id.isNew).toBe(true);
119
- });
120
-
121
- it('should auto-generate unique IDs', () => {
122
- const post1 = new Post({
123
- id: new Id(),
124
- title: 'Post 1',
125
- content: 'Content',
126
- likes: 0,
127
- });
128
-
129
- const post2 = new Post({
130
- id: new Id(),
131
- title: 'Post 2',
132
- content: 'Content',
133
- likes: 0,
134
- });
135
-
136
- expect(post1.id.value).not.toBe(post2.id.value);
137
- expect(post1.isNew).toBe(true);
138
- expect(post2.isNew).toBe(true);
139
- });
140
-
141
- it('should work with Id.create()', () => {
142
- const post = new Post({
143
- id: Id.create(),
144
- title: 'New Post',
145
- content: 'Content',
146
- likes: 0,
147
- });
148
-
149
- expect(post.isNew).toBe(true);
150
- });
151
- });
152
-
153
- describe('Existing Entity', () => {
154
- it('should NOT be new when Id value is provided', () => {
155
- const post = new Post({
156
- id: new Id('existing-post-id'), // Value provided = not new
157
- title: 'Existing Post',
158
- content: 'Content',
159
- likes: 10,
160
- });
161
-
162
- expect(post.isNew).toBe(false);
163
- expect(post.id.isNew).toBe(false);
164
- });
165
-
166
- it('should work with Id.from()', () => {
167
- const post = new Post({
168
- id: Id.from('existing-post-id'),
169
- title: 'Existing Post',
170
- content: 'Content',
171
- likes: 10,
172
- });
173
-
174
- expect(post.isNew).toBe(false);
175
- });
176
- });
177
-
178
- describe('toJson()', () => {
179
- it('should serialize Id to string', () => {
180
- const post = new Post({
181
- id: new Id('post-123'),
182
- title: 'Test Post',
183
- content: 'Content',
184
- likes: 5,
185
- });
186
-
187
- const json = post.toJson();
188
-
189
- expect(json.id).toBe('post-123');
190
- expect(typeof json.id).toBe('string');
191
- });
192
- });
193
-
194
- describe('Id Comparison in Arrays', () => {
195
- it('should detect changes in arrays using Id', done => {
196
- const user = new User({
197
- id: new Id('user-1'),
198
- name: 'John',
199
- email: 'john@example.com',
200
- posts: [],
201
- address: new Address({
202
- street: 'Main St',
203
- city: 'NYC',
204
- zipCode: '10001',
205
- }),
206
- comments: [],
207
- });
208
-
209
- user.subscribe({
210
- posts: {
211
- onChange: ({ toCreate, toDelete }) => {
212
- expect(toCreate).toHaveLength(2);
213
- expect(toDelete).toHaveLength(0);
214
- done();
215
- },
216
- },
217
- });
218
-
219
- user.addManyPosts([
220
- new Post({
221
- id: new Id(),
222
- title: 'Post 1',
223
- content: 'Content 1',
224
- likes: 0,
225
- }),
226
- new Post({
227
- id: new Id(),
228
- title: 'Post 2',
229
- content: 'Content 2',
230
- likes: 0,
231
- }),
232
- ]);
233
- });
234
-
235
- it('should track deletes correctly with Id', done => {
236
- const postId = new Id('post-to-delete');
237
-
238
- const user = new User({
239
- id: new Id('user-1'),
240
- name: 'John',
241
- email: 'john@example.com',
242
- posts: [
243
- new Post({
244
- id: postId,
245
- title: 'Post 1',
246
- content: 'Content 1',
247
- likes: 0,
248
- }),
249
- ],
250
- address: new Address({
251
- street: 'Main St',
252
- city: 'NYC',
253
- zipCode: '10001',
254
- }),
255
- comments: [],
256
- });
257
-
258
- user.subscribe({
259
- posts: {
260
- onChange: ({ toCreate, toDelete }) => {
261
- expect(toCreate).toHaveLength(0);
262
- expect(toDelete).toHaveLength(1);
263
- expect(toDelete[0].id.value).toBe(postId.value);
264
- done();
265
- },
266
- },
267
- });
268
-
269
- user.removePostById(postId.value);
270
- });
271
- });
272
- });
273
-
274
- // ============================================================================
275
- // Aggregate with Id Tests
276
- // ============================================================================
277
-
278
- describe('Aggregate with Id Class', () => {
279
- it('should be new when Id is auto-generated', () => {
280
- const user = new User({
281
- id: new Id(),
282
- name: 'John',
283
- email: 'john@example.com',
284
- posts: [],
285
- address: new Address({
286
- street: 'Main St',
287
- city: 'NYC',
288
- zipCode: '10001',
289
- }),
290
- comments: [],
291
- });
292
-
293
- expect(user.isNew).toBe(true);
294
- });
295
-
296
- it('should NOT be new when Id value is provided', () => {
297
- const user = new User({
298
- id: new Id('existing-user'),
299
- name: 'John',
300
- email: 'john@example.com',
301
- posts: [],
302
- address: new Address({
303
- street: 'Main St',
304
- city: 'NYC',
305
- zipCode: '10001',
306
- }),
307
- comments: [],
308
- });
309
-
310
- expect(user.isNew).toBe(false);
311
- });
312
-
313
- it('should serialize Id in nested entities', () => {
314
- const user = new User({
315
- id: new Id('user-1'),
316
- name: 'John',
317
- email: 'john@example.com',
318
- posts: [
319
- new Post({
320
- id: new Id('post-1'),
321
- title: 'Post 1',
322
- content: 'Content',
323
- likes: 0,
324
- }),
325
- ],
326
- comments: [],
327
- address: new Address({
328
- street: 'Main St',
329
- city: 'NYC',
330
- zipCode: '10001',
331
- }),
332
- });
333
-
334
- const json = user.toJson();
335
-
336
- expect(json.id).toBe('user-1');
337
- expect(json.posts[0].id).toBe('post-1');
338
- expect(typeof json.id).toBe('string');
339
- expect(typeof json.posts[0].id).toBe('string');
340
- });
341
- });
1
+ import { Id } from "../src";
2
+ import { Address, Post, User } from "./utils";
3
+
4
+ // ============================================================================
5
+ // Id Class Tests
6
+ // ============================================================================
7
+
8
+ describe("Id Class", () => {
9
+ describe("Construction", () => {
10
+ it("should generate UUID when no value provided", () => {
11
+ const id = new Id();
12
+
13
+ expect(id.value).toBeDefined();
14
+ expect(typeof id.value).toBe("string");
15
+ expect(id.value.length).toBeGreaterThan(0);
16
+ // UUID v4 format check
17
+ expect(id.value).toMatch(
18
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
19
+ );
20
+ });
21
+
22
+ it("should mark as new when no value provided", () => {
23
+ const id = new Id();
24
+
25
+ expect(id.isNew()).toBe(true);
26
+ });
27
+
28
+ it("should use provided value", () => {
29
+ const existingId = "550e8400-e29b-41d4-a716-446655440000";
30
+ const id = new Id(existingId);
31
+
32
+ expect(id.value).toBe(existingId);
33
+ });
34
+
35
+ it("should mark as NOT new when value provided", () => {
36
+ const id = new Id("existing-id");
37
+
38
+ expect(id.isNew()).toBe(false);
39
+ });
40
+
41
+ it("should generate unique IDs", () => {
42
+ const id1 = new Id();
43
+ const id2 = new Id();
44
+ const id3 = new Id();
45
+
46
+ expect(id1.value).not.toBe(id2.value);
47
+ expect(id2.value).not.toBe(id3.value);
48
+ expect(id1.value).not.toBe(id3.value);
49
+ });
50
+ });
51
+
52
+ describe("Static Methods", () => {
53
+ it("should create new Id with Id.create()", () => {
54
+ const id = Id.create();
55
+
56
+ expect(id.isNew()).toBe(true);
57
+ expect(id.value).toBeDefined();
58
+ });
59
+
60
+ it("should create existing Id with Id.from()", () => {
61
+ const id = Id.from("existing-id");
62
+
63
+ expect(id.isNew()).toBe(false);
64
+ expect(id.value).toBe("existing-id");
65
+ });
66
+ });
67
+
68
+ describe("String Conversion", () => {
69
+ it("should convert to string with toString()", () => {
70
+ const id = new Id("test-id");
71
+
72
+ expect(id.toString()).toBe("test-id");
73
+ expect(String(id)).toBe("test-id");
74
+ });
75
+
76
+ it("should convert to JSON", () => {
77
+ const id = new Id("test-id");
78
+
79
+ expect(id.toJSON()).toBe("test-id");
80
+ expect(JSON.stringify(id)).toBe('"test-id"');
81
+ });
82
+ });
83
+
84
+ describe("Equality", () => {
85
+ it("should compare with another Id", () => {
86
+ const id1 = new Id("same-id");
87
+ const id2 = new Id("same-id");
88
+ const id3 = new Id("different-id");
89
+
90
+ expect(id1.equals(id2)).toBe(true);
91
+ expect(id1.equals(id3)).toBe(false);
92
+ });
93
+
94
+ it("should compare with string", () => {
95
+ const id = new Id("test-id");
96
+
97
+ expect(id.equals("test-id")).toBe(true);
98
+ expect(id.equals("other-id")).toBe(false);
99
+ });
100
+ });
101
+ });
102
+
103
+ // ============================================================================
104
+ // Entity with Id Tests
105
+ // ============================================================================
106
+
107
+ describe("Entity with Id Class", () => {
108
+ describe("New Entity", () => {
109
+ it("should be new when Id is auto-generated", () => {
110
+ const post = new Post({
111
+ id: new Id(), // No value = new
112
+ title: "New Post",
113
+ content: "Content",
114
+ likes: 0,
115
+ });
116
+
117
+ expect(post.isNew()).toBe(true);
118
+ expect(post.id.isNew()).toBe(true);
119
+ });
120
+
121
+ it("should auto-generate unique IDs", () => {
122
+ const post1 = new Post({
123
+ id: new Id(),
124
+ title: "Post 1",
125
+ content: "Content",
126
+ likes: 0,
127
+ });
128
+
129
+ const post2 = new Post({
130
+ id: new Id(),
131
+ title: "Post 2",
132
+ content: "Content",
133
+ likes: 0,
134
+ });
135
+
136
+ expect(post1.id.value).not.toBe(post2.id.value);
137
+ expect(post1.isNew()).toBe(true);
138
+ expect(post2.isNew()).toBe(true);
139
+ });
140
+
141
+ it("should work with Id.create()", () => {
142
+ const post = new Post({
143
+ id: Id.create(),
144
+ title: "New Post",
145
+ content: "Content",
146
+ likes: 0,
147
+ });
148
+
149
+ expect(post.isNew()).toBe(true);
150
+ });
151
+ });
152
+
153
+ describe("Existing Entity", () => {
154
+ it("should NOT be new when Id value is provided", () => {
155
+ const post = new Post({
156
+ id: new Id("existing-post-id"), // Value provided = not new
157
+ title: "Existing Post",
158
+ content: "Content",
159
+ likes: 10,
160
+ });
161
+
162
+ expect(post.isNew()).toBe(false);
163
+ expect(post.id.isNew()).toBe(false);
164
+ });
165
+
166
+ it("should work with Id.from()", () => {
167
+ const post = new Post({
168
+ id: Id.from("existing-post-id"),
169
+ title: "Existing Post",
170
+ content: "Content",
171
+ likes: 10,
172
+ });
173
+
174
+ expect(post.isNew()).toBe(false);
175
+ });
176
+ });
177
+
178
+ describe("toJson()", () => {
179
+ it("should serialize Id to string", () => {
180
+ const post = new Post({
181
+ id: new Id("post-123"),
182
+ title: "Test Post",
183
+ content: "Content",
184
+ likes: 5,
185
+ });
186
+
187
+ const json = post.toJson();
188
+
189
+ expect(json.id).toBe("post-123");
190
+ expect(typeof json.id).toBe("string");
191
+ });
192
+ });
193
+
194
+ describe("Id Comparison in Arrays", () => {
195
+ it("should detect changes in arrays using Id", (done) => {
196
+ const user = new User({
197
+ id: new Id("user-1"),
198
+ name: "John",
199
+ email: "john@example.com",
200
+ posts: [],
201
+ address: new Address({
202
+ street: "Main St",
203
+ city: "NYC",
204
+ zipCode: "10001",
205
+ }),
206
+ comments: [],
207
+ });
208
+
209
+ user.subscribe({
210
+ posts: {
211
+ onChange: ({ toCreate, toDelete }) => {
212
+ expect(toCreate).toHaveLength(2);
213
+ expect(toDelete).toHaveLength(0);
214
+ done();
215
+ },
216
+ },
217
+ });
218
+
219
+ user.addManyPosts([
220
+ new Post({
221
+ id: new Id(),
222
+ title: "Post 1",
223
+ content: "Content 1",
224
+ likes: 0,
225
+ }),
226
+ new Post({
227
+ id: new Id(),
228
+ title: "Post 2",
229
+ content: "Content 2",
230
+ likes: 0,
231
+ }),
232
+ ]);
233
+ });
234
+
235
+ it("should track deletes correctly with Id", (done) => {
236
+ const postId = new Id("post-to-delete");
237
+
238
+ const user = new User({
239
+ id: new Id("user-1"),
240
+ name: "John",
241
+ email: "john@example.com",
242
+ posts: [
243
+ new Post({
244
+ id: postId,
245
+ title: "Post 1",
246
+ content: "Content 1",
247
+ likes: 0,
248
+ }),
249
+ ],
250
+ address: new Address({
251
+ street: "Main St",
252
+ city: "NYC",
253
+ zipCode: "10001",
254
+ }),
255
+ comments: [],
256
+ });
257
+
258
+ user.subscribe({
259
+ posts: {
260
+ onChange: ({ toCreate, toDelete }) => {
261
+ expect(toCreate).toHaveLength(0);
262
+ expect(toDelete).toHaveLength(1);
263
+ expect(toDelete[0].id.value).toBe(postId.value);
264
+ done();
265
+ },
266
+ },
267
+ });
268
+
269
+ user.removePostById(postId.value);
270
+ });
271
+ });
272
+ });
273
+
274
+ // ============================================================================
275
+ // Aggregate with Id Tests
276
+ // ============================================================================
277
+
278
+ describe("Aggregate with Id Class", () => {
279
+ it("should be new when Id is auto-generated", () => {
280
+ const user = new User({
281
+ id: new Id(),
282
+ name: "John",
283
+ email: "john@example.com",
284
+ posts: [],
285
+ address: new Address({
286
+ street: "Main St",
287
+ city: "NYC",
288
+ zipCode: "10001",
289
+ }),
290
+ comments: [],
291
+ });
292
+
293
+ expect(user.isNew()).toBe(true);
294
+ });
295
+
296
+ it("should NOT be new when Id value is provided", () => {
297
+ const user = new User({
298
+ id: new Id("existing-user"),
299
+ name: "John",
300
+ email: "john@example.com",
301
+ posts: [],
302
+ address: new Address({
303
+ street: "Main St",
304
+ city: "NYC",
305
+ zipCode: "10001",
306
+ }),
307
+ comments: [],
308
+ });
309
+
310
+ expect(user.isNew()).toBe(false);
311
+ });
312
+
313
+ it("should serialize Id in nested entities", () => {
314
+ const user = new User({
315
+ id: new Id("user-1"),
316
+ name: "John",
317
+ email: "john@example.com",
318
+ posts: [
319
+ new Post({
320
+ id: new Id("post-1"),
321
+ title: "Post 1",
322
+ content: "Content",
323
+ likes: 0,
324
+ }),
325
+ ],
326
+ comments: [],
327
+ address: new Address({
328
+ street: "Main St",
329
+ city: "NYC",
330
+ zipCode: "10001",
331
+ }),
332
+ });
333
+
334
+ const json = user.toJson();
335
+
336
+ expect(json.id).toBe("user-1");
337
+ expect(json.posts[0].id).toBe("post-1");
338
+ expect(typeof json.id).toBe("string");
339
+ expect(typeof json.posts[0].id).toBe("string");
340
+ });
341
+ });