@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
+ // 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
+ const currentPath = this.path
31
+ ? `${this.path}.${String(prop)}`
32
+ : String(prop);
33
+
34
+ if (
35
+ prop === '__isProxy' ||
36
+ prop === '__originalTarget' ||
37
+ prop === '__path' ||
38
+ prop === 'constructor' ||
39
+ prop === 'prototype'
40
+ ) {
41
+ return value;
42
+ }
43
+
44
+ if (typeof value === 'function') return value.bind(target);
45
+
46
+ if (Array.isArray(value)) {
47
+ if (!this.rootProxy!.trackedArraysCloned.has(currentPath)) {
48
+ this.rootProxy!.storeArrayState(currentPath, value);
49
+ }
50
+ return this.createArrayProxy(value, currentPath);
51
+ }
52
+
53
+ if (value && typeof value === 'object' && !value.__isProxy) {
54
+ const nestedProxy = new DeepProxy(value, currentPath, this.rootProxy);
55
+ return nestedProxy.createProxy();
56
+ }
57
+
58
+ return value;
59
+ },
60
+
61
+ set: (target, prop, newValue, receiver) => {
62
+ const currentPath = this.path
63
+ ? `${this.path}.${String(prop)}`
64
+ : String(prop);
65
+ const oldValue = Reflect.get(target, prop, receiver);
66
+ const isArrayAssignment = Array.isArray(newValue);
67
+
68
+ if (!isArrayAssignment && oldValue === newValue) return true;
69
+
70
+ if (!this.rootProxy!.originalValues.has(currentPath)) {
71
+ this.rootProxy!.originalValues.set(currentPath, oldValue);
72
+ }
73
+
74
+ this.rootProxy!.history.push({
75
+ path: currentPath,
76
+ previousValue: oldValue,
77
+ currentValue: newValue,
78
+ timestamp: Date.now(),
79
+ });
80
+
81
+ const result = Reflect.set(target, prop, newValue, receiver);
82
+
83
+ if (isArrayAssignment) {
84
+ if (!this.rootProxy!.trackedArraysCloned.has(currentPath)) {
85
+ if (Array.isArray(oldValue)) {
86
+ this.rootProxy!.storeArrayState(currentPath, oldValue);
87
+ } else {
88
+ this.rootProxy!.storeArrayState(currentPath, []);
89
+ }
90
+ }
91
+ this.rootProxy!.scheduleNotification(() =>
92
+ this.rootProxy!.notifyArrayChange(currentPath, newValue)
93
+ );
94
+ } else {
95
+ this.rootProxy!.scheduleNotification(() =>
96
+ this.rootProxy!.notifySubscribers(currentPath, oldValue, newValue)
97
+ );
98
+ }
99
+
100
+ // Notify global subscribers
101
+ this.rootProxy!.scheduleNotification(() =>
102
+ this.rootProxy!.notifyGlobalSubscribers()
103
+ );
104
+
105
+ this.rootProxy!.flushNotifications();
106
+
107
+ return result;
108
+ },
109
+ };
110
+
111
+ const proxy = new Proxy(this.target, handler);
112
+ Object.defineProperty(proxy, '__isProxy', { value: true, writable: false });
113
+ return proxy;
114
+ }
115
+
116
+ private scheduleNotification(notification: () => void): void {
117
+ this.pendingNotifications.push(notification);
118
+ }
119
+
120
+ private flushNotifications(): void {
121
+ if (this.updateInProgress) return;
122
+
123
+ this.updateInProgress = true;
124
+ try {
125
+ const notifications = [...this.pendingNotifications];
126
+ this.pendingNotifications = [];
127
+ notifications.forEach((notify) => notify());
128
+ } finally {
129
+ this.updateInProgress = false;
130
+ }
131
+ }
132
+
133
+ private storeArrayState(path: string, arr: any[]): void {
134
+ this.trackedArraysCloned.set(path, this.cloneArray(arr));
135
+ this.trackedArraysOriginal.set(path, arr.slice());
136
+ }
137
+
138
+ private createArrayProxy(array: any[], path: string): any[] {
139
+ const self = this;
140
+ return new Proxy(array, {
141
+ get(target, prop, receiver) {
142
+ const value = Reflect.get(target, prop, receiver);
143
+ if (typeof value === 'function') {
144
+ const mutatingMethods = [
145
+ 'push',
146
+ 'pop',
147
+ 'shift',
148
+ 'unshift',
149
+ 'splice',
150
+ 'sort',
151
+ 'reverse',
152
+ ];
153
+ if (mutatingMethods.includes(String(prop))) {
154
+ return function (...args: any[]) {
155
+ const result = value.apply(target, args);
156
+ if (!self.rootProxy!.trackedArraysCloned.has(path)) {
157
+ self.rootProxy!.storeArrayState(path, target);
158
+ }
159
+ self.rootProxy!.notifyArrayChange(path, target);
160
+ self.rootProxy!.notifyGlobalSubscribers();
161
+ return result;
162
+ };
163
+ }
164
+ return value.bind(target);
165
+ }
166
+ if (typeof value === 'object' && value !== null && !value.__isProxy) {
167
+ const nestedPath = `${path}[${String(prop)}]`;
168
+ const nestedProxy = new DeepProxy(value, nestedPath, self.rootProxy);
169
+ return nestedProxy.createProxy();
170
+ }
171
+ return value;
172
+ },
173
+ set(target, prop, newValue, receiver) {
174
+ if (!isNaN(Number(prop))) {
175
+ const result = Reflect.set(target, prop, newValue, receiver);
176
+ if (!self.rootProxy!.trackedArraysCloned.has(path)) {
177
+ self.rootProxy!.storeArrayState(path, target);
178
+ }
179
+ self.rootProxy!.notifyArrayChange(path, target);
180
+ self.rootProxy!.notifyGlobalSubscribers();
181
+ return result;
182
+ }
183
+ return Reflect.set(target, prop, newValue, receiver);
184
+ },
185
+ });
186
+ }
187
+
188
+ subscribe(path: string, callback: Function): void {
189
+ if (path === '*') {
190
+ this.rootProxy!.globalSubscribers.add(callback);
191
+ return;
192
+ }
193
+
194
+ if (!this.rootProxy!.subscribers.has(path)) {
195
+ this.rootProxy!.subscribers.set(path, new Set());
196
+ }
197
+ this.rootProxy!.subscribers.get(path)!.add(callback);
198
+
199
+ const actualValue = this.rootProxy!.target[path];
200
+ if (
201
+ Array.isArray(actualValue) &&
202
+ !this.rootProxy!.trackedArraysCloned.has(path)
203
+ ) {
204
+ this.rootProxy!.storeArrayState(path, actualValue);
205
+ }
206
+ }
207
+
208
+ unsubscribe(path: string, callback: Function): void {
209
+ if (path === '*') {
210
+ this.rootProxy!.globalSubscribers.delete(callback);
211
+ return;
212
+ }
213
+
214
+ const subs = this.rootProxy!.subscribers.get(path);
215
+ if (subs) {
216
+ subs.delete(callback);
217
+ }
218
+ }
219
+
220
+ private notifySubscribers(path: string, oldValue: any, newValue: any): void {
221
+ const subs = this.subscribers.get(path);
222
+ if (subs) {
223
+ subs.forEach((cb) => cb({ previous: oldValue, current: newValue, path }));
224
+ }
225
+ }
226
+
227
+ private notifyGlobalSubscribers(): void {
228
+ this.globalSubscribers.forEach((cb) => cb());
229
+ }
230
+
231
+ private notifyArrayChange(path: string, newArray: any[]): void {
232
+ const subs = this.subscribers.get(path);
233
+ if (!subs) return;
234
+
235
+ const clonedInitial = this.trackedArraysCloned.get(path);
236
+ const originalInitial = this.trackedArraysOriginal.get(path);
237
+
238
+ if (!clonedInitial || !originalInitial) {
239
+ this.storeArrayState(path, newArray);
240
+ return;
241
+ }
242
+
243
+ const changes = this.detectArrayChanges(clonedInitial, originalInitial, newArray);
244
+ subs.forEach((cb) => cb({ ...changes, path }));
245
+ }
246
+
247
+ private detectArrayChanges(
248
+ oldCloned: any[],
249
+ oldOriginal: any[],
250
+ newArray: any[]
251
+ ): { toCreate: any[]; toUpdate: any[]; toDelete: any[] } {
252
+ const toCreate: any[] = [];
253
+ const toUpdate: any[] = [];
254
+ const toDelete: any[] = [];
255
+
256
+ const oldMap = new Map<string, any>();
257
+ const newMap = new Map<string, any>();
258
+
259
+ oldCloned.forEach((item) => {
260
+ const id = this.getItemId(item);
261
+ if (id) oldMap.set(id, item);
262
+ });
263
+
264
+ newArray.forEach((item) => {
265
+ const id = this.getItemId(item);
266
+ if (id) newMap.set(id, item);
267
+ });
268
+
269
+ newArray.forEach((item) => {
270
+ const id = this.getItemId(item);
271
+ if (!id) {
272
+ toCreate.push(item);
273
+ } else if (!oldMap.has(id)) {
274
+ toCreate.push(item);
275
+ } else if (this.hasChanged(oldMap.get(id), item)) {
276
+ toUpdate.push(item);
277
+ }
278
+ });
279
+
280
+ oldOriginal.forEach((item) => {
281
+ const id = this.getItemId(item);
282
+ if (id && !newMap.has(id)) {
283
+ toDelete.push(item);
284
+ }
285
+ });
286
+
287
+ return { toCreate, toUpdate, toDelete };
288
+ }
289
+
290
+ private cloneArray(arr: any[]): any[] {
291
+ return arr.map((item) => this.deepClone(item));
292
+ }
293
+
294
+ private deepClone(obj: any): any {
295
+ if (obj === null || obj === undefined) return obj;
296
+ if (typeof obj !== 'object') return obj;
297
+ if (obj instanceof Date) return new Date(obj.getTime());
298
+ if (obj instanceof Id) return obj.value;
299
+ if (obj.toJson && typeof obj.toJson === 'function') return obj.toJson();
300
+ if (Array.isArray(obj)) return obj.map((item) => this.deepClone(item));
301
+ const cloned: any = {};
302
+ for (const key in obj) {
303
+ if (obj.hasOwnProperty(key)) cloned[key] = this.deepClone(obj[key]);
304
+ }
305
+ return cloned;
306
+ }
307
+
308
+ private getItemId(item: any): string | undefined {
309
+ if (!item) return undefined;
310
+ if (item.id instanceof Id) return item.id.value;
311
+ if (item.id !== undefined) return String(item.id);
312
+ if (typeof item === 'object' && 'id' in item) {
313
+ const id = item.id;
314
+ return id instanceof Id ? id.value : String(id);
315
+ }
316
+ return undefined;
317
+ }
318
+
319
+ private hasChanged(obj1: any, obj2: any): boolean {
320
+ const json1 = this.deepClone(obj1);
321
+ const json2 = this.deepClone(obj2);
322
+ return JSON.stringify(json1) !== JSON.stringify(json2);
323
+ }
324
+
325
+ getHistory(): HistoryEntry[] {
326
+ return [...this.history];
327
+ }
328
+
329
+ clearHistory(): void {
330
+ this.history = [];
331
+ this.originalValues.clear();
332
+ this.trackedArraysCloned.clear();
333
+ this.trackedArraysOriginal.clear();
334
+ }
335
+
336
+ getTarget(): any {
337
+ return this.target;
338
+ }
339
+ }
@@ -0,0 +1,166 @@
1
+ // ============================================================================
2
+ // Domain Event Bus - Pub/Sub for Domain Events
3
+ // ============================================================================
4
+
5
+ import {
6
+ IDomainEvent,
7
+ DomainEventHandler,
8
+ IDomainEventHandler,
9
+ } from "./domain-event";
10
+
11
+ type EventConstructor<T extends IDomainEvent = IDomainEvent> = new (
12
+ ...args: any[]
13
+ ) => T;
14
+
15
+ /**
16
+ * Domain Event Bus - Singleton pattern for event pub/sub
17
+ */
18
+ export class DomainEventBus {
19
+ private static instance: DomainEventBus;
20
+ private handlers: Map<
21
+ string,
22
+ Set<DomainEventHandler<any> | IDomainEventHandler<any>>
23
+ > = new Map();
24
+ private wildcardHandlers: Set<
25
+ DomainEventHandler<any> | IDomainEventHandler<any>
26
+ > = new Set();
27
+
28
+ private constructor() {}
29
+
30
+ /**
31
+ * Get the singleton instance
32
+ */
33
+ static getInstance(): DomainEventBus {
34
+ if (!DomainEventBus.instance) {
35
+ DomainEventBus.instance = new DomainEventBus();
36
+ }
37
+ return DomainEventBus.instance;
38
+ }
39
+
40
+ /**
41
+ * Subscribe to a specific event type
42
+ */
43
+ subscribe<T extends IDomainEvent>(
44
+ eventType: EventConstructor<T> | string,
45
+ handler: DomainEventHandler<T> | IDomainEventHandler<T>
46
+ ): void {
47
+ const eventName =
48
+ typeof eventType === "string" ? eventType : eventType.name;
49
+
50
+ if (!this.handlers.has(eventName)) {
51
+ this.handlers.set(eventName, new Set());
52
+ }
53
+
54
+ this.handlers.get(eventName)!.add(handler);
55
+ }
56
+
57
+ /**
58
+ * Subscribe to all events (wildcard)
59
+ */
60
+ subscribeAll(
61
+ handler: DomainEventHandler<IDomainEvent> | IDomainEventHandler<IDomainEvent>
62
+ ): void {
63
+ this.wildcardHandlers.add(handler);
64
+ }
65
+
66
+ /**
67
+ * Unsubscribe from a specific event type
68
+ */
69
+ unsubscribe<T extends IDomainEvent>(
70
+ eventType: EventConstructor<T> | string,
71
+ handler: DomainEventHandler<T> | IDomainEventHandler<T>
72
+ ): void {
73
+ const eventName =
74
+ typeof eventType === "string" ? eventType : eventType.name;
75
+ const handlers = this.handlers.get(eventName);
76
+
77
+ if (handlers) {
78
+ handlers.delete(handler);
79
+ if (handlers.size === 0) {
80
+ this.handlers.delete(eventName);
81
+ }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Unsubscribe from all events
87
+ */
88
+ unsubscribeAll(
89
+ handler: DomainEventHandler<IDomainEvent> | IDomainEventHandler<IDomainEvent>
90
+ ): void {
91
+ this.wildcardHandlers.delete(handler);
92
+ }
93
+
94
+ /**
95
+ * Publish a single event
96
+ */
97
+ async publish<T extends IDomainEvent>(event: T): Promise<void> {
98
+ const eventName = event.eventName;
99
+ const handlers = this.handlers.get(eventName) || new Set();
100
+
101
+ // Execute specific handlers
102
+ const specificPromises = Array.from(handlers).map((handler) =>
103
+ this.executeHandler(handler, event)
104
+ );
105
+
106
+ // Execute wildcard handlers
107
+ const wildcardPromises = Array.from(this.wildcardHandlers).map((handler) =>
108
+ this.executeHandler(handler, event)
109
+ );
110
+
111
+ await Promise.all([...specificPromises, ...wildcardPromises]);
112
+ }
113
+
114
+ /**
115
+ * Publish multiple events
116
+ */
117
+ async publishAll(events: IDomainEvent[]): Promise<void> {
118
+ await Promise.all(events.map((event) => this.publish(event)));
119
+ }
120
+
121
+ /**
122
+ * Clear all handlers (useful for testing)
123
+ */
124
+ clearAllHandlers(): void {
125
+ this.handlers.clear();
126
+ this.wildcardHandlers.clear();
127
+ }
128
+
129
+ /**
130
+ * Get count of handlers for an event type
131
+ */
132
+ getHandlerCount(eventType: EventConstructor | string): number {
133
+ const eventName =
134
+ typeof eventType === "string" ? eventType : eventType.name;
135
+ return this.handlers.get(eventName)?.size || 0;
136
+ }
137
+
138
+ /**
139
+ * Execute a handler (function or class)
140
+ */
141
+ private async executeHandler(
142
+ handler: DomainEventHandler<any> | IDomainEventHandler<any>,
143
+ event: IDomainEvent
144
+ ): Promise<void> {
145
+ try {
146
+ if (typeof handler === "function") {
147
+ await handler(event);
148
+ } else {
149
+ await handler.handle(event);
150
+ }
151
+ } catch (error) {
152
+ console.error(
153
+ `Error handling event ${event.eventName}:`,
154
+ error
155
+ );
156
+ // Don't throw - we don't want one handler failure to break others
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Convenience function to get the event bus instance
163
+ */
164
+ export function getEventBus(): DomainEventBus {
165
+ return DomainEventBus.getInstance();
166
+ }
@@ -0,0 +1,90 @@
1
+ // ============================================================================
2
+ // Domain Events - Event-Driven Architecture Support
3
+ // ============================================================================
4
+
5
+ import { Id } from "./id";
6
+
7
+ /**
8
+ * Interface for all domain events
9
+ */
10
+ export interface IDomainEvent {
11
+ /**
12
+ * Unique identifier for this event occurrence
13
+ */
14
+ readonly eventId: string;
15
+
16
+ /**
17
+ * When the event occurred
18
+ */
19
+ readonly occurredOn: Date;
20
+
21
+ /**
22
+ * Name/type of the event (e.g., "UserCreated", "OrderPlaced")
23
+ */
24
+ readonly eventName: string;
25
+
26
+ /**
27
+ * ID of the aggregate that raised this event
28
+ */
29
+ readonly aggregateId: string;
30
+ }
31
+
32
+ /**
33
+ * Base class for domain events
34
+ */
35
+ export abstract class DomainEvent implements IDomainEvent {
36
+ public readonly eventId: string;
37
+ public readonly occurredOn: Date;
38
+ public readonly aggregateId: string;
39
+
40
+ constructor(aggregateId: Id | string) {
41
+ this.eventId = this.generateEventId();
42
+ this.occurredOn = new Date();
43
+ this.aggregateId = aggregateId instanceof Id ? aggregateId.value : aggregateId;
44
+ }
45
+
46
+ /**
47
+ * Get the event name (defaults to class name)
48
+ */
49
+ get eventName(): string {
50
+ return this.constructor.name;
51
+ }
52
+
53
+ private generateEventId(): string {
54
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
55
+ }
56
+
57
+ /**
58
+ * Convert event to JSON
59
+ */
60
+ toJSON(): Record<string, any> {
61
+ return {
62
+ eventId: this.eventId,
63
+ eventName: this.eventName,
64
+ occurredOn: this.occurredOn.toISOString(),
65
+ aggregateId: this.aggregateId,
66
+ ...this.getPayload(),
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Override this to provide event-specific data
72
+ */
73
+ protected getPayload(): Record<string, any> {
74
+ return {};
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Event handler function type
80
+ */
81
+ export type DomainEventHandler<T extends IDomainEvent = IDomainEvent> = (
82
+ event: T
83
+ ) => void | Promise<void>;
84
+
85
+ /**
86
+ * Event handler class type
87
+ */
88
+ export interface IDomainEventHandler<T extends IDomainEvent = IDomainEvent> {
89
+ handle(event: T): void | Promise<void>;
90
+ }
package/src/entity.ts ADDED
@@ -0,0 +1,16 @@
1
+ // ============================================================================
2
+ // Entity & Aggregate Classes
3
+ // ============================================================================
4
+
5
+ import { BaseEntity } from './base-entity';
6
+ import { BaseProps } from './types';
7
+
8
+ /**
9
+ * Entity - Has identity and lifecycle, but is not an aggregate root
10
+ */
11
+ export class Entity<T extends BaseProps> extends BaseEntity<T> {}
12
+
13
+ /**
14
+ * Aggregate - Aggregate root that manages a consistency boundary
15
+ */
16
+ export class Aggregate<T extends BaseProps> extends BaseEntity<T> {}
package/src/id.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { randomUUID } from 'crypto';
2
+ // ============================================================================
3
+ // Id Class - Smart Identity Management
4
+ // ============================================================================
5
+
6
+ export class Id {
7
+ private readonly _value: string;
8
+ private readonly _isNew: boolean;
9
+
10
+ /**
11
+ * Create a new Id
12
+ * @param value - Optional existing ID value. If not provided, generates a new UUID.
13
+ *
14
+ * @example
15
+ * // New entity (generates UUID)
16
+ * const newId = new Id();
17
+ * newId.isNew // true
18
+ *
19
+ * // Existing entity (uses provided ID)
20
+ * const existingId = new Id("550e8400-e29b-41d4-a716-446655440000");
21
+ * existingId.isNew // false
22
+ */
23
+ constructor(value?: string) {
24
+ if (value !== undefined) {
25
+ // ID was provided - this is an existing entity
26
+ this._value = value;
27
+ this._isNew = false;
28
+ } else {
29
+ // No ID provided - generate new one, this is a new entity
30
+ this._value = this.generateUUID();
31
+ this._isNew = true;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get the string value of the ID
37
+ */
38
+ get value(): string {
39
+ return this._value;
40
+ }
41
+
42
+ /**
43
+ * Check if this ID represents a new entity
44
+ */
45
+ get isNew(): boolean {
46
+ return this._isNew;
47
+ }
48
+
49
+ /**
50
+ * Convert to string (for JSON serialization and comparisons)
51
+ */
52
+ toString(): string {
53
+ return this._value;
54
+ }
55
+
56
+ /**
57
+ * Convert to JSON (returns the string value)
58
+ */
59
+ toJSON(): string {
60
+ return this._value;
61
+ }
62
+
63
+ /**
64
+ * Check equality with another Id or string
65
+ */
66
+ equals(other: Id | string): boolean {
67
+ if (other instanceof Id) {
68
+ return this._value === other._value;
69
+ }
70
+ return this._value === other;
71
+ }
72
+
73
+ /**
74
+ * Generate a UUID v4
75
+ */
76
+ private generateUUID(): string {
77
+ // Simple UUID v4 implementation
78
+ return randomUUID();
79
+ }
80
+
81
+ /**
82
+ * Create a new Id (convenience static method)
83
+ */
84
+ static create(): Id {
85
+ return new Id();
86
+ }
87
+
88
+ /**
89
+ * Create an Id from an existing value
90
+ */
91
+ static from(value: string): Id {
92
+ return new Id(value);
93
+ }
94
+ }