@woltz/rich-domain 0.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 (178) hide show
  1. package/.github/workflows/ci.yml +40 -0
  2. package/.husky/commit-msg +1 -0
  3. package/.husky/pre-commit +1 -0
  4. package/.versionrc.json +21 -0
  5. package/.vscode/settings.json +3 -0
  6. package/CHANGELOG.md +81 -0
  7. package/LICENSE +21 -0
  8. package/README.md +712 -0
  9. package/commitlint.config.js +23 -0
  10. package/dist/base-entity.d.ts +67 -0
  11. package/dist/base-entity.d.ts.map +1 -0
  12. package/dist/base-entity.js +309 -0
  13. package/dist/base-entity.js.map +1 -0
  14. package/dist/constants.d.ts +3 -0
  15. package/dist/constants.d.ts.map +1 -0
  16. package/dist/constants.js +6 -0
  17. package/dist/constants.js.map +1 -0
  18. package/dist/criteria.d.ts +60 -0
  19. package/dist/criteria.d.ts.map +1 -0
  20. package/dist/criteria.js +214 -0
  21. package/dist/criteria.js.map +1 -0
  22. package/dist/deep-proxy.d.ts +34 -0
  23. package/dist/deep-proxy.d.ts.map +1 -0
  24. package/dist/deep-proxy.js +297 -0
  25. package/dist/deep-proxy.js.map +1 -0
  26. package/dist/domain-event-bus.d.ts +57 -0
  27. package/dist/domain-event-bus.d.ts.map +1 -0
  28. package/dist/domain-event-bus.js +112 -0
  29. package/dist/domain-event-bus.js.map +1 -0
  30. package/dist/domain-event.d.ts +55 -0
  31. package/dist/domain-event.d.ts.map +1 -0
  32. package/dist/domain-event.js +42 -0
  33. package/dist/domain-event.js.map +1 -0
  34. package/dist/entity.d.ts +13 -0
  35. package/dist/entity.d.ts.map +1 -0
  36. package/dist/entity.js +15 -0
  37. package/dist/entity.js.map +1 -0
  38. package/dist/filtering.d.ts +107 -0
  39. package/dist/filtering.d.ts.map +1 -0
  40. package/dist/filtering.js +202 -0
  41. package/dist/filtering.js.map +1 -0
  42. package/dist/id.d.ts +51 -0
  43. package/dist/id.d.ts.map +1 -0
  44. package/dist/id.js +84 -0
  45. package/dist/id.js.map +1 -0
  46. package/dist/index.d.ts +15 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +25 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/ordering.d.ts +93 -0
  51. package/dist/ordering.d.ts.map +1 -0
  52. package/dist/ordering.js +154 -0
  53. package/dist/ordering.js.map +1 -0
  54. package/dist/paginated-result.d.ts +62 -0
  55. package/dist/paginated-result.d.ts.map +1 -0
  56. package/dist/paginated-result.js +201 -0
  57. package/dist/paginated-result.js.map +1 -0
  58. package/dist/pagination.d.ts +218 -0
  59. package/dist/pagination.d.ts.map +1 -0
  60. package/dist/pagination.js +281 -0
  61. package/dist/pagination.js.map +1 -0
  62. package/dist/repository/base-repository.d.ts +77 -0
  63. package/dist/repository/base-repository.d.ts.map +1 -0
  64. package/dist/repository/base-repository.js +80 -0
  65. package/dist/repository/base-repository.js.map +1 -0
  66. package/dist/repository/in-memory-repository.d.ts +46 -0
  67. package/dist/repository/in-memory-repository.d.ts.map +1 -0
  68. package/dist/repository/in-memory-repository.js +85 -0
  69. package/dist/repository/in-memory-repository.js.map +1 -0
  70. package/dist/repository/index.d.ts +42 -0
  71. package/dist/repository/index.d.ts.map +1 -0
  72. package/dist/repository/index.js +47 -0
  73. package/dist/repository/index.js.map +1 -0
  74. package/dist/repository/mapper.d.ts +56 -0
  75. package/dist/repository/mapper.d.ts.map +1 -0
  76. package/dist/repository/mapper.js +15 -0
  77. package/dist/repository/mapper.js.map +1 -0
  78. package/dist/repository/types.d.ts +87 -0
  79. package/dist/repository/types.d.ts.map +1 -0
  80. package/dist/repository/types.js +6 -0
  81. package/dist/repository/types.js.map +1 -0
  82. package/dist/repository/unit-of-work.d.ts +70 -0
  83. package/dist/repository/unit-of-work.d.ts.map +1 -0
  84. package/dist/repository/unit-of-work.js +122 -0
  85. package/dist/repository/unit-of-work.js.map +1 -0
  86. package/dist/repository.d.ts +2 -0
  87. package/dist/repository.d.ts.map +1 -0
  88. package/dist/repository.js +21 -0
  89. package/dist/repository.js.map +1 -0
  90. package/dist/specification.d.ts +102 -0
  91. package/dist/specification.d.ts.map +1 -0
  92. package/dist/specification.js +187 -0
  93. package/dist/specification.js.map +1 -0
  94. package/dist/types/criteria.d.ts +35 -0
  95. package/dist/types/criteria.d.ts.map +1 -0
  96. package/dist/types/criteria.js +17 -0
  97. package/dist/types/criteria.js.map +1 -0
  98. package/dist/types/domain.d.ts +30 -0
  99. package/dist/types/domain.d.ts.map +1 -0
  100. package/dist/types/domain.js +2 -0
  101. package/dist/types/domain.js.map +1 -0
  102. package/dist/types/history-tracker.d.ts +36 -0
  103. package/dist/types/history-tracker.d.ts.map +1 -0
  104. package/dist/types/history-tracker.js +2 -0
  105. package/dist/types/history-tracker.js.map +1 -0
  106. package/dist/types/index.d.ts +8 -0
  107. package/dist/types/index.d.ts.map +1 -0
  108. package/dist/types/index.js +8 -0
  109. package/dist/types/index.js.map +1 -0
  110. package/dist/types/repository.d.ts +43 -0
  111. package/dist/types/repository.d.ts.map +1 -0
  112. package/dist/types/repository.js +2 -0
  113. package/dist/types/repository.js.map +1 -0
  114. package/dist/types/standard-schema.d.ts +15 -0
  115. package/dist/types/standard-schema.d.ts.map +1 -0
  116. package/dist/types/standard-schema.js +2 -0
  117. package/dist/types/standard-schema.js.map +1 -0
  118. package/dist/types/unit-of-work.d.ts +39 -0
  119. package/dist/types/unit-of-work.d.ts.map +1 -0
  120. package/dist/types/unit-of-work.js +2 -0
  121. package/dist/types/unit-of-work.js.map +1 -0
  122. package/dist/types/utils.d.ts +14 -0
  123. package/dist/types/utils.d.ts.map +1 -0
  124. package/dist/types/utils.js +2 -0
  125. package/dist/types/utils.js.map +1 -0
  126. package/dist/types.d.ts +88 -0
  127. package/dist/types.d.ts.map +1 -0
  128. package/dist/types.js +12 -0
  129. package/dist/types.js.map +1 -0
  130. package/dist/validation-error.d.ts +42 -0
  131. package/dist/validation-error.d.ts.map +1 -0
  132. package/dist/validation-error.js +73 -0
  133. package/dist/validation-error.js.map +1 -0
  134. package/dist/value-object.d.ts +47 -0
  135. package/dist/value-object.d.ts.map +1 -0
  136. package/dist/value-object.js +136 -0
  137. package/dist/value-object.js.map +1 -0
  138. package/eslint.config.js +51 -0
  139. package/jest.config.js +21 -0
  140. package/package.json +58 -0
  141. package/src/base-entity.ts +401 -0
  142. package/src/constants.ts +7 -0
  143. package/src/criteria.ts +291 -0
  144. package/src/deep-proxy.ts +339 -0
  145. package/src/domain-event-bus.ts +166 -0
  146. package/src/domain-event.ts +90 -0
  147. package/src/entity.ts +16 -0
  148. package/src/id.ts +94 -0
  149. package/src/index.ts +33 -0
  150. package/src/paginated-result.ts +274 -0
  151. package/src/repository/base-repository.ts +152 -0
  152. package/src/repository/in-memory-repository.ts +104 -0
  153. package/src/repository/index.ts +55 -0
  154. package/src/repository/mapper.ts +74 -0
  155. package/src/repository/unit-of-work.ts +148 -0
  156. package/src/types/criteria.ts +79 -0
  157. package/src/types/domain.ts +37 -0
  158. package/src/types/history-tracker.ts +45 -0
  159. package/src/types/index.ts +7 -0
  160. package/src/types/repository.ts +51 -0
  161. package/src/types/standard-schema.ts +19 -0
  162. package/src/types/unit-of-work.ts +46 -0
  163. package/src/types/utils.ts +29 -0
  164. package/src/validation-error.ts +97 -0
  165. package/src/value-object.ts +187 -0
  166. package/tests/criteria.test.ts +432 -0
  167. package/tests/domain-events.test.ts +445 -0
  168. package/tests/entity-equality.test.ts +487 -0
  169. package/tests/entity-validation.test.ts +339 -0
  170. package/tests/entity.test.ts +33 -0
  171. package/tests/history-tracker.spec.ts +667 -0
  172. package/tests/id.test.ts +341 -0
  173. package/tests/repository.test.ts +641 -0
  174. package/tests/to-json.test.ts +91 -0
  175. package/tests/utils.ts +151 -0
  176. package/tests/value-object-validation.test.ts +228 -0
  177. package/tests/value-objects.test.ts +52 -0
  178. package/tsconfig.json +31 -0
@@ -0,0 +1,641 @@
1
+ import { Id, Aggregate, Criteria, BaseProps, PaginatedResult } from "../src";
2
+ import {
3
+ InMemoryRepository,
4
+ BaseRepository,
5
+ BaseMapper,
6
+ } from "../src/repository";
7
+
8
+ // ============================================================================
9
+ // Test Domain Models
10
+ // ============================================================================
11
+
12
+ interface UserProps extends BaseProps {
13
+ name: string;
14
+ email: string;
15
+ age: number;
16
+ status: "active" | "inactive";
17
+ }
18
+
19
+ class User extends Aggregate<UserProps> {
20
+ get name() {
21
+ return this.props.name;
22
+ }
23
+
24
+ get email() {
25
+ return this.props.email;
26
+ }
27
+
28
+ get age() {
29
+ return this.props.age;
30
+ }
31
+
32
+ get status() {
33
+ return this.props.status;
34
+ }
35
+
36
+ setName(name: string) {
37
+ this.props.name = name;
38
+ }
39
+
40
+ activate() {
41
+ this.props.status = "active";
42
+ }
43
+
44
+ deactivate() {
45
+ this.props.status = "inactive";
46
+ }
47
+
48
+ static create(props: Omit<UserProps, "id"> & { id?: Id }): User {
49
+ return new User({
50
+ id: props.id || new Id(),
51
+ name: props.name,
52
+ email: props.email,
53
+ age: props.age,
54
+ status: props.status,
55
+ });
56
+ }
57
+ }
58
+
59
+ // ============================================================================
60
+ // Mock Persistence Model
61
+ // ============================================================================
62
+
63
+ type UserPersistence = {
64
+ id: string;
65
+ name: string;
66
+ email: string;
67
+ age: number;
68
+ status: string;
69
+ createdAt: Date;
70
+ updatedAt: Date;
71
+ };
72
+
73
+ // ============================================================================
74
+ // Mapper Implementation
75
+ // ============================================================================
76
+
77
+ class UserMapper extends BaseMapper<User, UserPersistence> {
78
+ toDomain(persistence: UserPersistence): User {
79
+ return User.create({
80
+ id: Id.from(persistence.id),
81
+ name: persistence.name,
82
+ email: persistence.email,
83
+ age: persistence.age,
84
+ status: persistence.status as "active" | "inactive",
85
+ });
86
+ }
87
+
88
+ toPersistence(domain: User): UserPersistence {
89
+ return {
90
+ id: domain.id.value,
91
+ name: domain.name,
92
+ email: domain.email,
93
+ age: domain.age,
94
+ status: domain.status,
95
+ createdAt: new Date(),
96
+ updatedAt: new Date(),
97
+ };
98
+ }
99
+ }
100
+
101
+ // ============================================================================
102
+ // Mock Repository Implementation
103
+ // ============================================================================
104
+
105
+ class MockUserRepository extends BaseRepository<User, UserPersistence> {
106
+ private store: Map<string, UserPersistence> = new Map();
107
+
108
+ constructor() {
109
+ super(new UserMapper());
110
+ }
111
+
112
+ protected async insertOne(data: UserPersistence): Promise<UserPersistence> {
113
+ this.store.set(data.id, data);
114
+ return data;
115
+ }
116
+
117
+ protected async updateOne(
118
+ id: string,
119
+ data: UserPersistence
120
+ ): Promise<UserPersistence> {
121
+ this.store.set(id, { ...data, updatedAt: new Date() });
122
+ return data;
123
+ }
124
+
125
+ protected async deleteOne(id: string): Promise<void> {
126
+ this.store.delete(id);
127
+ }
128
+
129
+ protected async findOneById(id: string): Promise<User | null> {
130
+ const persistence = this.store.get(id);
131
+ if (!persistence) return null;
132
+ return this.mapper.toDomain(persistence);
133
+ }
134
+
135
+ protected async findMany(): Promise<User[]> {
136
+ const persistence = Array.from(this.store.values());
137
+ return this.mapper.toDomainList
138
+ ? this.mapper.toDomainList(persistence)
139
+ : persistence.map((p) => this.mapper.toDomain(p));
140
+ }
141
+
142
+ protected async applyCriteria(criteria: Criteria<User>) {
143
+ let results = Array.from(this.store.values());
144
+
145
+ // Apply filters
146
+ for (const filter of criteria.getFilters()) {
147
+ results = results.filter((item: any) => {
148
+ const value = item[filter.field];
149
+ switch (filter.operator) {
150
+ case "equals":
151
+ return value === filter.value;
152
+ case "greaterThan":
153
+ return value > (filter.value as any);
154
+ case "lessThan":
155
+ return value < (filter.value as any);
156
+ case "contains":
157
+ return String(value)
158
+ .toLowerCase()
159
+ .includes(String(filter.value).toLowerCase());
160
+ default:
161
+ return true;
162
+ }
163
+ });
164
+ }
165
+
166
+ const total = results.length;
167
+
168
+ // Apply ordering
169
+ for (const order of criteria.getOrders()) {
170
+ results.sort((a: any, b: any) => {
171
+ const aVal = a[order.field];
172
+ const bVal = b[order.field];
173
+ const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
174
+ return order.direction === "desc" ? -comparison : comparison;
175
+ });
176
+ }
177
+
178
+ // Apply pagination
179
+ const pagination = criteria.getPagination();
180
+
181
+ results = results.slice(
182
+ pagination.offset,
183
+ pagination.offset + pagination.limit
184
+ );
185
+
186
+ const domains = this.mapper.toDomainList
187
+ ? this.mapper.toDomainList(results)
188
+ : results.map((p) => this.mapper.toDomain(p));
189
+
190
+ return PaginatedResult.create(domains, pagination, total);
191
+ }
192
+
193
+ protected async countByCriteria(criteria?: Criteria<User>): Promise<number> {
194
+ if (!criteria) {
195
+ return this.store.size;
196
+ }
197
+
198
+ const result = await this.applyCriteria(criteria);
199
+ return result.meta.total;
200
+ }
201
+
202
+ protected async existsById(id: string): Promise<boolean> {
203
+ return this.store.has(id);
204
+ }
205
+
206
+ // Test helper
207
+ clear() {
208
+ this.store.clear();
209
+ }
210
+ }
211
+
212
+ // ============================================================================
213
+ // Tests
214
+ // ============================================================================
215
+
216
+ describe("Repository", () => {
217
+ describe("InMemoryRepository", () => {
218
+ let repository: InMemoryRepository<User>;
219
+ let user1: User;
220
+ let user2: User;
221
+ let user3: User;
222
+
223
+ beforeEach(() => {
224
+ repository = new InMemoryRepository<User>();
225
+
226
+ user1 = User.create({
227
+ name: "Alice",
228
+ email: "alice@example.com",
229
+ age: 25,
230
+ status: "active",
231
+ });
232
+
233
+ user2 = User.create({
234
+ name: "Bob",
235
+ email: "bob@example.com",
236
+ age: 30,
237
+ status: "active",
238
+ });
239
+
240
+ user3 = User.create({
241
+ name: "Charlie",
242
+ email: "charlie@example.com",
243
+ age: 35,
244
+ status: "inactive",
245
+ });
246
+ });
247
+
248
+ afterEach(() => {
249
+ repository.clear();
250
+ });
251
+
252
+ describe("save and findById", () => {
253
+ it("should save and retrieve user", async () => {
254
+ await repository.save(user1);
255
+ const found = await repository.findById(user1.id);
256
+
257
+ expect(found).toBeDefined();
258
+ expect(found?.id.equals(user1.id)).toBe(true);
259
+ expect(found?.name).toBe("Alice");
260
+ });
261
+
262
+ it("should return null for non-existent id", async () => {
263
+ const found = await repository.findById(new Id());
264
+ expect(found).toBeNull();
265
+ });
266
+
267
+ it("should update existing user", async () => {
268
+ await repository.save(user1);
269
+ user1.setName("Alice Updated");
270
+ await repository.save(user1);
271
+
272
+ const found = await repository.findById(user1.id);
273
+ expect(found?.name).toBe("Alice Updated");
274
+ });
275
+ });
276
+
277
+ describe("findAll", () => {
278
+ it("should return empty array when no users", async () => {
279
+ const users = await repository.findAll();
280
+ expect(users).toHaveLength(0);
281
+ });
282
+
283
+ it("should return all users", async () => {
284
+ await repository.save(user1);
285
+ await repository.save(user2);
286
+ await repository.save(user3);
287
+
288
+ const users = await repository.findAll();
289
+ expect(users).toHaveLength(3);
290
+ });
291
+
292
+ it("should return users matching criteria", async () => {
293
+ await repository.save(user1);
294
+ await repository.save(user2);
295
+ await repository.save(user3);
296
+
297
+ const criteria = Criteria.create<User>().whereEquals(
298
+ "status",
299
+ "active"
300
+ );
301
+ const users = await repository.findAll(criteria);
302
+
303
+ expect(users).toHaveLength(2);
304
+ expect(users.every((u) => u.status === "active")).toBe(true);
305
+ });
306
+ });
307
+
308
+ describe("find with Criteria", () => {
309
+ beforeEach(async () => {
310
+ await repository.save(user1);
311
+ await repository.save(user2);
312
+ await repository.save(user3);
313
+ });
314
+
315
+ it("should filter by equals", async () => {
316
+ const criteria = Criteria.create<User>().whereEquals(
317
+ "status",
318
+ "active"
319
+ );
320
+ const result = await repository.find(criteria);
321
+
322
+ expect(result.data).toHaveLength(2);
323
+ expect(result.meta.total).toBe(2);
324
+ });
325
+
326
+ it("should order by field", async () => {
327
+ const criteria = Criteria.create<User>().orderByDesc("age");
328
+ const result = await repository.find(criteria);
329
+
330
+ expect(result.data[0].age).toBe(35);
331
+ expect(result.data[1].age).toBe(30);
332
+ expect(result.data[2].age).toBe(25);
333
+ });
334
+
335
+ it("should paginate results", async () => {
336
+ const criteria = Criteria.create<User>()
337
+ .orderByAsc("age")
338
+ .paginate(1, 2);
339
+
340
+ const result = await repository.find(criteria);
341
+
342
+ expect(result.data).toHaveLength(2);
343
+ expect(result.meta.page).toBe(1);
344
+ expect(result.meta.limit).toBe(2);
345
+ expect(result.meta.total).toBe(3);
346
+ expect(result.meta.totalPages).toBe(2);
347
+ expect(result.meta.hasNext).toBe(true);
348
+ expect(result.meta.hasPrevious).toBe(false);
349
+ });
350
+
351
+ it("should combine filter, order, and pagination", async () => {
352
+ const criteria = Criteria.create<User>()
353
+ .whereEquals("status", "active")
354
+ .orderByDesc("age")
355
+ .paginate(1, 1);
356
+
357
+ const result = await repository.find(criteria);
358
+
359
+ expect(result.data).toHaveLength(1);
360
+ expect(result.data[0].age).toBe(30); // Bob (highest age among active)
361
+ expect(result.meta.total).toBe(2); // Total active users
362
+ });
363
+ });
364
+
365
+ describe("findOne", () => {
366
+ it("should find first matching user", async () => {
367
+ await repository.save(user1);
368
+ await repository.save(user2);
369
+
370
+ const criteria = Criteria.create<User>().whereEquals(
371
+ "status",
372
+ "active"
373
+ );
374
+ const found = await repository.findOne(criteria);
375
+
376
+ expect(found).toBeDefined();
377
+ expect(found?.status).toBe("active");
378
+ });
379
+
380
+ it("should return null when no match", async () => {
381
+ const criteria = Criteria.create<User>().whereEquals(
382
+ "status",
383
+ "active"
384
+ );
385
+ const found = await repository.findOne(criteria);
386
+
387
+ expect(found).toBeNull();
388
+ });
389
+ });
390
+
391
+ describe("delete", () => {
392
+ it("should delete by aggregate", async () => {
393
+ await repository.save(user1);
394
+ await repository.delete(user1);
395
+
396
+ const found = await repository.findById(user1.id);
397
+ expect(found).toBeNull();
398
+ });
399
+
400
+ it("should delete by id", async () => {
401
+ await repository.save(user1);
402
+ await repository.deleteById(user1.id);
403
+
404
+ const found = await repository.findById(user1.id);
405
+ expect(found).toBeNull();
406
+ });
407
+ });
408
+
409
+ describe("exists", () => {
410
+ it("should return true when user exists", async () => {
411
+ await repository.save(user1);
412
+ const exists = await repository.exists(user1.id);
413
+ expect(exists).toBe(true);
414
+ });
415
+
416
+ it("should return false when user does not exist", async () => {
417
+ const exists = await repository.exists(new Id());
418
+ expect(exists).toBe(false);
419
+ });
420
+ });
421
+
422
+ describe("count", () => {
423
+ beforeEach(async () => {
424
+ await repository.save(user1);
425
+ await repository.save(user2);
426
+ await repository.save(user3);
427
+ });
428
+
429
+ it("should count all users", async () => {
430
+ const count = await repository.count();
431
+ expect(count).toBe(3);
432
+ });
433
+
434
+ it("should count users matching criteria", async () => {
435
+ const criteria = Criteria.create<User>().whereEquals(
436
+ "status",
437
+ "active"
438
+ );
439
+ const count = await repository.count(criteria);
440
+ expect(count).toBe(2);
441
+ });
442
+ });
443
+
444
+ describe("saveMany", () => {
445
+ it("should save multiple users", async () => {
446
+ await repository.saveMany([user1, user2, user3]);
447
+
448
+ const users = await repository.findAll();
449
+ expect(users).toHaveLength(3);
450
+ });
451
+ });
452
+ });
453
+
454
+ describe("BaseRepository with Mapper", () => {
455
+ let repository: MockUserRepository;
456
+ let user: User;
457
+
458
+ beforeEach(() => {
459
+ repository = new MockUserRepository();
460
+ user = User.create({
461
+ name: "John",
462
+ email: "john@example.com",
463
+ age: 28,
464
+ status: "active",
465
+ });
466
+ });
467
+
468
+ afterEach(() => {
469
+ repository.clear();
470
+ });
471
+
472
+ it("should save new user (insert)", async () => {
473
+ expect(user.isNew).toBe(true);
474
+ await repository.save(user);
475
+
476
+ const found = await repository.findById(user.id);
477
+ expect(found).toBeDefined();
478
+ expect(found?.name).toBe("John");
479
+ });
480
+
481
+ it("should update existing user", async () => {
482
+ await repository.save(user);
483
+
484
+ // Simulate existing user
485
+ const existingUser = User.create({
486
+ id: user.id, // Same ID
487
+ name: "John Updated",
488
+ email: "john@example.com",
489
+ age: 29,
490
+ status: "inactive",
491
+ });
492
+
493
+ await repository.save(existingUser);
494
+
495
+ const found = await repository.findById(user.id);
496
+ expect(found?.name).toBe("John Updated");
497
+ expect(found?.age).toBe(29);
498
+ });
499
+
500
+ it("should convert between domain and persistence", async () => {
501
+ await repository.save(user);
502
+ const found = await repository.findById(user.id);
503
+
504
+ // Should be a domain object
505
+ expect(found).toBeInstanceOf(User);
506
+ expect(found?.id).toBeInstanceOf(Id);
507
+ });
508
+
509
+ it("should apply criteria correctly", async () => {
510
+ const user1 = User.create({
511
+ name: "Alice",
512
+ email: "alice@example.com",
513
+ age: 25,
514
+ status: "active",
515
+ });
516
+ const user2 = User.create({
517
+ name: "Bob",
518
+ email: "bob@example.com",
519
+ age: 30,
520
+ status: "inactive",
521
+ });
522
+
523
+ await Promise.all([repository.save(user1), repository.save(user2)]);
524
+
525
+ const result = await repository.find(
526
+ Criteria.create<User>()
527
+ .whereEquals("status", "active")
528
+ .orderByDesc("age")
529
+ .paginate(1, 10)
530
+ );
531
+
532
+ expect(result.data).toHaveLength(1);
533
+ expect(result.data[0].name).toBe("Alice");
534
+ });
535
+ });
536
+
537
+ describe("Mapper", () => {
538
+ let mapper: UserMapper;
539
+
540
+ beforeEach(() => {
541
+ mapper = new UserMapper();
542
+ });
543
+
544
+ it("should convert from persistence to domain", () => {
545
+ const persistence: UserPersistence = {
546
+ id: "123",
547
+ name: "John",
548
+ email: "john@example.com",
549
+ age: 30,
550
+ status: "active",
551
+ createdAt: new Date(),
552
+ updatedAt: new Date(),
553
+ };
554
+
555
+ const domain = mapper.toDomain(persistence);
556
+
557
+ expect(domain).toBeInstanceOf(User);
558
+ expect(domain.id.value).toBe("123");
559
+ expect(domain.name).toBe("John");
560
+ expect(domain.email).toBe("john@example.com");
561
+ expect(domain.age).toBe(30);
562
+ expect(domain.status).toBe("active");
563
+ });
564
+
565
+ it("should convert from domain to persistence", () => {
566
+ const domain = User.create({
567
+ id: Id.from("123"),
568
+ name: "John",
569
+ email: "john@example.com",
570
+ age: 30,
571
+ status: "active",
572
+ });
573
+
574
+ const persistence = mapper.toPersistence(domain);
575
+
576
+ expect(persistence.id).toBe("123");
577
+ expect(persistence.name).toBe("John");
578
+ expect(persistence.email).toBe("john@example.com");
579
+ expect(persistence.age).toBe(30);
580
+ expect(persistence.status).toBe("active");
581
+ expect(persistence.createdAt).toBeInstanceOf(Date);
582
+ expect(persistence.updatedAt).toBeInstanceOf(Date);
583
+ });
584
+
585
+ it("should convert list from persistence to domain", () => {
586
+ const persistenceList: UserPersistence[] = [
587
+ {
588
+ id: "1",
589
+ name: "Alice",
590
+ email: "alice@example.com",
591
+ age: 25,
592
+ status: "active",
593
+ createdAt: new Date(),
594
+ updatedAt: new Date(),
595
+ },
596
+ {
597
+ id: "2",
598
+ name: "Bob",
599
+ email: "bob@example.com",
600
+ age: 30,
601
+ status: "inactive",
602
+ createdAt: new Date(),
603
+ updatedAt: new Date(),
604
+ },
605
+ ];
606
+
607
+ const domainList = mapper.toDomainList(persistenceList);
608
+
609
+ expect(domainList).toHaveLength(2);
610
+ expect(domainList[0]).toBeInstanceOf(User);
611
+ expect(domainList[0].name).toBe("Alice");
612
+ expect(domainList[1].name).toBe("Bob");
613
+ });
614
+
615
+ it("should convert list from domain to persistence", () => {
616
+ const domainList = [
617
+ User.create({
618
+ id: Id.from("1"),
619
+ name: "Alice",
620
+ email: "alice@example.com",
621
+ age: 25,
622
+ status: "active",
623
+ }),
624
+ User.create({
625
+ id: Id.from("2"),
626
+ name: "Bob",
627
+ email: "bob@example.com",
628
+ age: 30,
629
+ status: "inactive",
630
+ }),
631
+ ];
632
+
633
+ const persistenceList = mapper.toPersistenceList(domainList);
634
+
635
+ expect(persistenceList).toHaveLength(2);
636
+ expect(persistenceList[0].id).toBe("1");
637
+ expect(persistenceList[0].name).toBe("Alice");
638
+ expect(persistenceList[1].name).toBe("Bob");
639
+ });
640
+ });
641
+ });