@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,339 @@
1
+ // ============================================================================
2
+ // Example Tests - Demonstrating Standard Schema Validation with Zod
3
+ // ============================================================================
4
+
5
+ import { z } from "zod";
6
+ import {
7
+ Id,
8
+ BaseProps,
9
+ Aggregate,
10
+ EntityValidation,
11
+ EntityHooks,
12
+ ValidationError,
13
+ } from "../src";
14
+ import { Address } from "./utils";
15
+
16
+ interface UserProps extends BaseProps {
17
+ id: Id;
18
+ name: string;
19
+ email: string;
20
+ age: number;
21
+ status: "active" | "inactive";
22
+ }
23
+
24
+ const userSchema = z.object({
25
+ id: z.custom<Id>((val) => val instanceof Id, { message: "Invalid Id" }),
26
+ name: z.string().min(2, "Name must be at least 2 characters"),
27
+ email: z.string().email("Invalid email format"),
28
+ age: z.number().min(0, "Age cannot be negative").max(150, "Age is too high"),
29
+ status: z.enum(["active", "inactive"]),
30
+ });
31
+
32
+ class User extends Aggregate<UserProps> {
33
+ protected static validation: EntityValidation<UserProps> = {
34
+ schema: userSchema,
35
+ config: {
36
+ onCreate: true,
37
+ onUpdate: true,
38
+ throwOnError: true,
39
+ },
40
+ };
41
+
42
+ protected static hooks: EntityHooks<UserProps, User> = {
43
+ onCreate: (entity) => {},
44
+ onBeforeUpdate: (entity, snapshot) => {
45
+ if (snapshot.email !== entity.email) {
46
+ return false; // Block the update
47
+ }
48
+ return true;
49
+ },
50
+ rules: (entity) => {
51
+ if (entity.name.toLowerCase() === "admin") {
52
+ throw new Error("Name cannot be 'admin'");
53
+ }
54
+ },
55
+ };
56
+
57
+ get name(): string {
58
+ return this.props.name;
59
+ }
60
+
61
+ set name(value: string) {
62
+ this.props.name = value;
63
+ }
64
+
65
+ get email(): string {
66
+ return this.props.email;
67
+ }
68
+
69
+ set email(value: string) {
70
+ this.props.email = value;
71
+ }
72
+
73
+ get age(): number {
74
+ return this.props.age;
75
+ }
76
+
77
+ set age(value: number) {
78
+ this.props.age = value;
79
+ }
80
+
81
+ get status(): "active" | "inactive" {
82
+ return this.props.status;
83
+ }
84
+
85
+ deactivate(): void {
86
+ this.props.status = "inactive";
87
+ }
88
+
89
+ activate(): void {
90
+ this.props.status = "active";
91
+ }
92
+ }
93
+
94
+ // ============================================================================
95
+ // Example: User with throwOnError: false
96
+ // ============================================================================
97
+
98
+ class UserSafe extends Aggregate<UserProps> {
99
+ protected static validation: EntityValidation<UserProps> = {
100
+ schema: userSchema,
101
+ config: {
102
+ onCreate: true,
103
+ onUpdate: true,
104
+ throwOnError: false, // Does not throw, stores errors internally
105
+ },
106
+ };
107
+
108
+ protected static hooks: EntityHooks<UserProps, UserSafe> = {};
109
+
110
+ get name(): string {
111
+ return this.props.name;
112
+ }
113
+
114
+ get email(): string {
115
+ return this.props.email;
116
+ }
117
+ }
118
+
119
+ // ============================================================================
120
+ // Tests
121
+ // ============================================================================
122
+
123
+ describe("Rich Domain with Standard Schema Validation", () => {
124
+ describe("User Creation with Validation", () => {
125
+ it("should create user with valid data", () => {
126
+ const user = new User({
127
+ name: "John Doe",
128
+ email: "john@example.com",
129
+ age: 25,
130
+ status: "active",
131
+ });
132
+
133
+ expect(user).toBeInstanceOf(User);
134
+ expect(user.name).toBe("John Doe");
135
+ expect(user.email).toBe("john@example.com");
136
+ expect(user.age).toBe(25);
137
+ expect(user.isNew).toBe(true);
138
+ });
139
+
140
+ it("should throw on invalid email", () => {
141
+ expect(() => {
142
+ new User({
143
+ name: "John",
144
+ email: "invalid-email",
145
+ age: 30,
146
+ status: "active",
147
+ });
148
+ }).toThrow(ValidationError);
149
+ });
150
+
151
+ it("should throw on invalid name (too short)", () => {
152
+ expect(() => {
153
+ new User({
154
+ name: "J",
155
+ email: "john@example.com",
156
+ age: 30,
157
+ status: "active",
158
+ });
159
+ }).toThrow(ValidationError);
160
+ });
161
+
162
+ it("should throw on custom rule violation", () => {
163
+ expect(() => {
164
+ new User({
165
+ name: "admin",
166
+ email: "admin@example.com",
167
+ age: 30,
168
+ status: "active",
169
+ });
170
+ }).toThrow(Error);
171
+ });
172
+
173
+ it("should not throw when throwOnError is false", () => {
174
+ const user = new UserSafe({
175
+ name: "J", // Too short
176
+ email: "invalid",
177
+ age: 30,
178
+ status: "active",
179
+ });
180
+
181
+ expect(user).toBeInstanceOf(UserSafe);
182
+ expect(user.hasValidationErrors).toBe(true);
183
+ expect(user.validationErrors).toBeInstanceOf(ValidationError);
184
+ expect(user.validationErrors?.issues.length).toBeGreaterThan(0);
185
+ });
186
+
187
+ it("should not have errors when valid and throwOnError is false", () => {
188
+ const user = new UserSafe({
189
+ name: "John Doe",
190
+ email: "john@example.com",
191
+ age: 30,
192
+ status: "active",
193
+ });
194
+
195
+ expect(user).toBeInstanceOf(UserSafe);
196
+ expect(user.hasValidationErrors).toBe(false);
197
+ expect(user.validationErrors).toBeUndefined();
198
+ expect(user.name).toBe("John Doe");
199
+ });
200
+ });
201
+
202
+ describe("Update Validation", () => {
203
+ it("should validate on property update", () => {
204
+ const user = new User({
205
+ name: "John Doe",
206
+ email: "john@example.com",
207
+ age: 30,
208
+ status: "active",
209
+ });
210
+
211
+ expect(() => {
212
+ user.name = "J"; // Too short
213
+ }).toThrow(ValidationError);
214
+ });
215
+
216
+ it("should allow valid updates", () => {
217
+ const user = new User({
218
+ name: "John Doe",
219
+ email: "john@example.com",
220
+ age: 30,
221
+ status: "active",
222
+ });
223
+
224
+ user.name = "Jane Doe";
225
+ expect(user.name).toBe("Jane Doe");
226
+ });
227
+
228
+ it("should block update via onBeforeUpdate hook", () => {
229
+ const user = new User({
230
+ name: "John Doe",
231
+ email: "john@example.com",
232
+ age: 30,
233
+ status: "active",
234
+ });
235
+
236
+ // Email change should be blocked by onBeforeUpdate
237
+ user.email = "newemail@example.com";
238
+
239
+ // Email should remain unchanged
240
+ expect(user.email).toBe("john@example.com");
241
+ });
242
+
243
+ it("should validate custom rules on update", () => {
244
+ const user = new User({
245
+ name: "John Doe",
246
+ email: "john@example.com",
247
+ age: 30,
248
+ status: "active",
249
+ });
250
+
251
+ expect(() => {
252
+ user.name = "admin"; // Blocked by custom rule
253
+ }).toThrow(Error);
254
+ });
255
+ });
256
+
257
+ describe("Serialization", () => {
258
+ it("should serialize to JSON correctly", () => {
259
+ const user = new User({
260
+ name: "John Doe",
261
+ email: "john@example.com",
262
+ age: 30,
263
+ status: "active",
264
+ });
265
+
266
+ const json = user.toJson();
267
+
268
+ expect(json.name).toBe("John Doe");
269
+ expect(json.email).toBe("john@example.com");
270
+ expect(json.age).toBe(30);
271
+ expect(json.status).toBe("active");
272
+ expect(typeof json.id).toBe("string"); // Id converted to string
273
+ });
274
+ });
275
+
276
+ describe("History Tracking", () => {
277
+ it("should track property changes", () => {
278
+ const user = new User({
279
+ name: "John Doe",
280
+ email: "john@example.com",
281
+ age: 30,
282
+ status: "active",
283
+ });
284
+
285
+ user.name = "Jane Doe";
286
+ user.age = 25;
287
+
288
+ const history = user.getHistory();
289
+ expect(history.length).toBe(2);
290
+ expect(history[0].path).toBe("name");
291
+ expect(history[0].previousValue).toBe("John Doe");
292
+ expect(history[0].currentValue).toBe("Jane Doe");
293
+ });
294
+ });
295
+ });
296
+
297
+ // ============================================================================
298
+ // Example: Value Object with Validation
299
+ // ============================================================================
300
+
301
+ describe("Value Object", () => {
302
+ it("should be immutable", () => {
303
+ const address = new Address({
304
+ street: "123 Main St",
305
+ city: "New York",
306
+ zipCode: "10001",
307
+ });
308
+
309
+ expect(address.street).toBe("123 Main St");
310
+
311
+ // Props are frozen
312
+ expect(() => {
313
+ (address as any).props.street = "New Street";
314
+ }).toThrow();
315
+ });
316
+
317
+ it("should compare by value", () => {
318
+ const address1 = new Address({
319
+ street: "123 Main St",
320
+ city: "New York",
321
+ zipCode: "10001",
322
+ });
323
+
324
+ const address2 = new Address({
325
+ street: "123 Main St",
326
+ city: "New York",
327
+ zipCode: "10001",
328
+ });
329
+
330
+ const address3 = new Address({
331
+ street: "456 Oak Ave",
332
+ city: "Boston",
333
+ zipCode: "02101",
334
+ });
335
+
336
+ expect(address1.equals(address2)).toBe(true);
337
+ expect(address1.equals(address3)).toBe(false);
338
+ });
339
+ });
@@ -0,0 +1,33 @@
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
+ });