@woltz/rich-domain 1.3.1 → 1.3.2
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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -11
- package/eslint.config.js +0 -57
- package/jest.config.js +0 -21
- package/src/aggregate-changes.ts +0 -444
- package/src/base-entity.ts +0 -410
- package/src/change-tracker.ts +0 -1123
- package/src/constants.ts +0 -81
- package/src/criteria.ts +0 -521
- package/src/crypto.ts +0 -31
- package/src/domain-event-bus.ts +0 -152
- package/src/domain-event.ts +0 -49
- package/src/entity-changes.ts +0 -146
- package/src/entity-schema-registry.ts +0 -505
- package/src/entity.ts +0 -5
- package/src/exceptions.ts +0 -435
- package/src/id.ts +0 -98
- package/src/index.ts +0 -52
- package/src/mapper.ts +0 -6
- package/src/paginated-result.ts +0 -250
- package/src/repository/base-repository.ts +0 -33
- package/src/repository/index.ts +0 -3
- package/src/repository/unit-of-work.ts +0 -76
- package/src/types/change-tracker.ts +0 -264
- package/src/types/criteria.ts +0 -159
- package/src/types/domain-event.ts +0 -38
- package/src/types/domain.ts +0 -33
- package/src/types/index.ts +0 -7
- package/src/types/standard-schema.ts +0 -19
- package/src/types/unit-of-work.ts +0 -46
- package/src/types/utils.ts +0 -20
- package/src/utils/criteria-operator-validation.ts +0 -209
- package/src/utils/helpers.ts +0 -34
- package/src/validation-error.ts +0 -141
- package/src/value-object.ts +0 -249
package/src/base-entity.ts
DELETED
|
@@ -1,410 +0,0 @@
|
|
|
1
|
-
import { Id } from "./id";
|
|
2
|
-
import { ValidationError } from "./validation-error";
|
|
3
|
-
import { IDomainEvent } from ".";
|
|
4
|
-
import {
|
|
5
|
-
BaseProps,
|
|
6
|
-
HistoryEntry,
|
|
7
|
-
DeepJsonResult,
|
|
8
|
-
EntityHooks,
|
|
9
|
-
ValidationConfig,
|
|
10
|
-
StandardSchema,
|
|
11
|
-
EntityValidation,
|
|
12
|
-
} from "./types";
|
|
13
|
-
import { DomainEventBus } from "./domain-event-bus";
|
|
14
|
-
import { DEFAULT_VALIDATION_CONFIG } from "./constants";
|
|
15
|
-
import { DomainError } from "./exceptions";
|
|
16
|
-
import { ChangeTracker } from "./change-tracker";
|
|
17
|
-
import { AggregateChanges } from "./aggregate-changes";
|
|
18
|
-
|
|
19
|
-
function getStaticProperty<T>(
|
|
20
|
-
instance: any,
|
|
21
|
-
propertyName: string
|
|
22
|
-
): T | undefined {
|
|
23
|
-
return instance.constructor[propertyName];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export abstract class BaseEntity<T extends BaseProps> {
|
|
27
|
-
private _props: T;
|
|
28
|
-
private tracker: ChangeTracker;
|
|
29
|
-
private proxiedProps: T;
|
|
30
|
-
private snapshot: T | null = null;
|
|
31
|
-
private validationConfig: Required<ValidationConfig>;
|
|
32
|
-
private entityHooks?: EntityHooks<T, any>;
|
|
33
|
-
private entitySchema?: StandardSchema<T>;
|
|
34
|
-
private domainEvents: IDomainEvent[] = [];
|
|
35
|
-
|
|
36
|
-
protected static validation?: EntityValidation<any>;
|
|
37
|
-
protected static hooks?: EntityHooks<any, any>;
|
|
38
|
-
|
|
39
|
-
constructor(props: Omit<T, "id"> & { id?: Id }) {
|
|
40
|
-
const validation = getStaticProperty<EntityValidation<T>>(
|
|
41
|
-
this,
|
|
42
|
-
"validation"
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const hooks = getStaticProperty<EntityHooks<T, any>>(this, "hooks");
|
|
46
|
-
|
|
47
|
-
if (!props.id) {
|
|
48
|
-
props.id = new Id();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (hooks?.onBeforeCreate) {
|
|
52
|
-
hooks.onBeforeCreate(props as T);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
this.entityHooks = hooks;
|
|
56
|
-
|
|
57
|
-
if (validation?.schema) {
|
|
58
|
-
this.entitySchema = validation.schema;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
this.validationConfig = {
|
|
62
|
-
...DEFAULT_VALIDATION_CONFIG,
|
|
63
|
-
...validation?.config,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
let finalProps = { ...props } as T;
|
|
67
|
-
|
|
68
|
-
if (this.entitySchema && this.validationConfig.onCreate) {
|
|
69
|
-
this.validateProps(finalProps);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this._props = finalProps;
|
|
73
|
-
this.tracker = new ChangeTracker(this._props, this.constructor.name);
|
|
74
|
-
|
|
75
|
-
if (this.validationConfig.onUpdate) {
|
|
76
|
-
this.setupUpdateValidation();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.proxiedProps = this.tracker.createProxy();
|
|
80
|
-
|
|
81
|
-
if (hooks?.rules) {
|
|
82
|
-
hooks.rules(this as any);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (hooks?.onCreate) {
|
|
86
|
-
hooks.onCreate(this as any);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
this.takeSnapshot();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private validateProps(props: T): void {
|
|
93
|
-
if (!this.entitySchema) return;
|
|
94
|
-
|
|
95
|
-
const result = this.entitySchema["~standard"].validate(props);
|
|
96
|
-
|
|
97
|
-
if (result instanceof Promise) {
|
|
98
|
-
throw new DomainError(
|
|
99
|
-
"Async validation not supported in constructor. Use sync validation schema."
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (result.issues && result.issues.length > 0) {
|
|
104
|
-
const validationError = new ValidationError(
|
|
105
|
-
result.issues.map((issue) => ({
|
|
106
|
-
path: issue.path?.map((p) => this.extractPathKey(p)) || [],
|
|
107
|
-
message: issue.message,
|
|
108
|
-
})),
|
|
109
|
-
{
|
|
110
|
-
entityName: this.constructor.name,
|
|
111
|
-
}
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
if (this.validationConfig.throwOnError) {
|
|
115
|
-
throw validationError;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
(this as any)._validationError = validationError;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private extractPathKey(pathSegment: unknown): string {
|
|
123
|
-
if (pathSegment === null || pathSegment === undefined) {
|
|
124
|
-
return "";
|
|
125
|
-
}
|
|
126
|
-
if (typeof pathSegment === "string" || typeof pathSegment === "number") {
|
|
127
|
-
return String(pathSegment);
|
|
128
|
-
}
|
|
129
|
-
if (typeof pathSegment === "symbol") {
|
|
130
|
-
return pathSegment.toString();
|
|
131
|
-
}
|
|
132
|
-
if (typeof pathSegment === "object" && "key" in pathSegment) {
|
|
133
|
-
return String((pathSegment as { key: unknown }).key);
|
|
134
|
-
}
|
|
135
|
-
return String(pathSegment);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Setup validation that runs on every property change.
|
|
140
|
-
* Uses the ChangeTracker's onChangeValidator callback.
|
|
141
|
-
*/
|
|
142
|
-
private setupUpdateValidation(): void {
|
|
143
|
-
const self = this;
|
|
144
|
-
|
|
145
|
-
this.tracker.setOnChangeValidator((path, newValue) => {
|
|
146
|
-
const originalValue = self._props[path as keyof T];
|
|
147
|
-
(self._props as any)[path] = newValue;
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
if (self.entityHooks?.onBeforeUpdate && self.snapshot) {
|
|
151
|
-
const shouldContinue = self.entityHooks.onBeforeUpdate(
|
|
152
|
-
self as any,
|
|
153
|
-
self.snapshot
|
|
154
|
-
);
|
|
155
|
-
if (!shouldContinue) {
|
|
156
|
-
(self._props as any)[path] = originalValue;
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (self.entitySchema) {
|
|
162
|
-
const result = self.entitySchema["~standard"].validate(self._props);
|
|
163
|
-
|
|
164
|
-
if (result instanceof Promise) {
|
|
165
|
-
console.warn(
|
|
166
|
-
"Async validation on update not supported. Consider using sync validation."
|
|
167
|
-
);
|
|
168
|
-
(self._props as any)[path] = originalValue;
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (result.issues && result.issues.length > 0) {
|
|
173
|
-
const validationError = new ValidationError(
|
|
174
|
-
result.issues.map((issue) => ({
|
|
175
|
-
path: issue.path?.map((p) => self.extractPathKey(p)) || [],
|
|
176
|
-
message: issue.message,
|
|
177
|
-
})),
|
|
178
|
-
{
|
|
179
|
-
entityName: self.constructor.name,
|
|
180
|
-
}
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
(self._props as any)[path] = originalValue;
|
|
184
|
-
|
|
185
|
-
if (self.validationConfig.throwOnError) {
|
|
186
|
-
throw validationError;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
console.error("Validation failed on update:", validationError);
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (self.entityHooks?.rules) {
|
|
195
|
-
try {
|
|
196
|
-
self.entityHooks.rules(self as any);
|
|
197
|
-
} catch (error) {
|
|
198
|
-
(self._props as any)[path] = originalValue;
|
|
199
|
-
|
|
200
|
-
if (self.validationConfig.throwOnError) {
|
|
201
|
-
throw error;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
console.error("Rules validation failed on update:", error);
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
(self._props as any)[path] = originalValue;
|
|
210
|
-
return true;
|
|
211
|
-
} catch (error) {
|
|
212
|
-
(self._props as any)[path] = originalValue;
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
private takeSnapshot(): void {
|
|
219
|
-
this.snapshot = this.deepCloneProps(this._props);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private deepCloneProps(obj: any, seen: WeakSet<object> = new WeakSet()): any {
|
|
223
|
-
if (obj === null || obj === undefined) return obj;
|
|
224
|
-
if (typeof obj !== "object") return obj;
|
|
225
|
-
if (obj instanceof Id) return obj;
|
|
226
|
-
if (obj instanceof Date) return new Date(obj.getTime());
|
|
227
|
-
|
|
228
|
-
if (seen.has(obj)) {
|
|
229
|
-
return obj;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (obj instanceof BaseEntity) {
|
|
233
|
-
return obj;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
obj.constructor &&
|
|
238
|
-
obj.constructor.name !== "Object" &&
|
|
239
|
-
obj.constructor.name !== "Array"
|
|
240
|
-
) {
|
|
241
|
-
if (
|
|
242
|
-
typeof obj.toJSON === "function" &&
|
|
243
|
-
typeof obj.equals === "function"
|
|
244
|
-
) {
|
|
245
|
-
return obj;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
seen.add(obj);
|
|
250
|
-
|
|
251
|
-
if (Array.isArray(obj)) {
|
|
252
|
-
return obj.map((item) => this.deepCloneProps(item, seen));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (obj.constructor === Object) {
|
|
256
|
-
try {
|
|
257
|
-
return structuredClone(obj);
|
|
258
|
-
} catch {
|
|
259
|
-
const cloned: any = {};
|
|
260
|
-
for (const key in obj) {
|
|
261
|
-
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
262
|
-
cloned[key] = this.deepCloneProps(obj[key], seen);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
return cloned;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return obj;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
get id(): Id {
|
|
273
|
-
return this._props.id;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
public isNew(): boolean {
|
|
277
|
-
return this._props.id.isNew();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Check equality with another entity by comparing IDs
|
|
282
|
-
*/
|
|
283
|
-
equals(other: BaseEntity<T> | Id | string): boolean {
|
|
284
|
-
if (!other) {
|
|
285
|
-
return false;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (other instanceof BaseEntity) {
|
|
289
|
-
return this.id.equals(other.id);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (other instanceof Id) {
|
|
293
|
-
return this.id.equals(other);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (typeof other === "string") {
|
|
297
|
-
return this.id.equals(other);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
public get props(): T {
|
|
304
|
-
return this.proxiedProps;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Check if entity has validation errors (when throwOnError is false)
|
|
309
|
-
*/
|
|
310
|
-
get hasValidationErrors(): boolean {
|
|
311
|
-
return !!(this as any)._validationError;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Get validation errors (when throwOnError is false)
|
|
316
|
-
*/
|
|
317
|
-
get validationErrors(): ValidationError | undefined {
|
|
318
|
-
return (this as any)._validationError;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Returns all detected changes as AggregateChanges.
|
|
323
|
-
*
|
|
324
|
-
* @example
|
|
325
|
-
* ```typescript
|
|
326
|
-
* const changes = user.getChanges();
|
|
327
|
-
* const batch = changes.toBatchOperations();
|
|
328
|
-
*
|
|
329
|
-
* for (const del of batch.deletes) { ... }
|
|
330
|
-
* for (const create of batch.creates) { ... }
|
|
331
|
-
* for (const upd of batch.updates) { ... }
|
|
332
|
-
* ```
|
|
333
|
-
*/
|
|
334
|
-
getChanges<TEntityMap = Record<string, any>>(): AggregateChanges<TEntityMap> {
|
|
335
|
-
return this.tracker.getChanges<TEntityMap>();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Returns the change history (for debugging).
|
|
340
|
-
*/
|
|
341
|
-
getHistory(): HistoryEntry[] {
|
|
342
|
-
return this.tracker.getHistory();
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Clears history and marks entity as "clean".
|
|
347
|
-
* Call this after successfully persisting to the database.
|
|
348
|
-
*/
|
|
349
|
-
markAsClean(): void {
|
|
350
|
-
this.tracker.markAsClean();
|
|
351
|
-
this.takeSnapshot();
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Add a domain event to this entity
|
|
356
|
-
*/
|
|
357
|
-
protected addDomainEvent(event: IDomainEvent): void {
|
|
358
|
-
this.domainEvents.push(event);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Dispatch all events through the event bus
|
|
363
|
-
*/
|
|
364
|
-
public async dispatchAll(bus: DomainEventBus): Promise<void> {
|
|
365
|
-
await bus.publishAll(this.getUncommittedEvents());
|
|
366
|
-
this.clearEvents();
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Get all uncommitted domain events
|
|
371
|
-
*/
|
|
372
|
-
getUncommittedEvents(): IDomainEvent[] {
|
|
373
|
-
return [...this.domainEvents];
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Clear all domain events (call after publishing)
|
|
378
|
-
*/
|
|
379
|
-
clearEvents(): void {
|
|
380
|
-
this.domainEvents = [];
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Check if entity has uncommitted events
|
|
385
|
-
*/
|
|
386
|
-
hasUncommittedEvents(): boolean {
|
|
387
|
-
return this.domainEvents.length > 0;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
toJSON(): DeepJsonResult<T> {
|
|
391
|
-
return this.deepToJson(this._props) as DeepJsonResult<T>;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
private deepToJson(obj: any): any {
|
|
395
|
-
if (obj === null || obj === undefined) return obj;
|
|
396
|
-
if (obj instanceof Id) return obj.value;
|
|
397
|
-
if (obj instanceof Date) return obj.toISOString();
|
|
398
|
-
if (Array.isArray(obj)) return obj.map((item) => this.deepToJson(item));
|
|
399
|
-
if (obj instanceof BaseEntity) return obj.toJSON();
|
|
400
|
-
if (obj && typeof obj.toJSON === "function") return obj.toJSON();
|
|
401
|
-
if (typeof obj === "object") {
|
|
402
|
-
const result: any = {};
|
|
403
|
-
for (const key in obj) {
|
|
404
|
-
if (obj.hasOwnProperty(key)) result[key] = this.deepToJson(obj[key]);
|
|
405
|
-
}
|
|
406
|
-
return result;
|
|
407
|
-
}
|
|
408
|
-
return obj;
|
|
409
|
-
}
|
|
410
|
-
}
|