@woltz/rich-domain 1.9.2 → 1.9.3
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/cjs/constants.d.ts +1 -1
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/core/base-entity.d.ts +2 -1
- package/dist/cjs/core/base-entity.d.ts.map +1 -1
- package/dist/cjs/core/base-entity.js +9 -7
- package/dist/cjs/core/base-entity.js.map +1 -1
- package/dist/cjs/core/change-tracker.d.ts +1 -0
- package/dist/cjs/core/change-tracker.d.ts.map +1 -1
- package/dist/cjs/core/change-tracker.js +24 -15
- package/dist/cjs/core/change-tracker.js.map +1 -1
- package/dist/cjs/core/domain-event.d.ts +1 -1
- package/dist/cjs/core/domain-event.d.ts.map +1 -1
- package/dist/cjs/core/domain-event.js +2 -2
- package/dist/cjs/core/domain-event.js.map +1 -1
- package/dist/cjs/core/entities.d.ts +8 -0
- package/dist/cjs/core/entities.d.ts.map +1 -0
- package/dist/cjs/core/entities.js +12 -0
- package/dist/cjs/core/entities.js.map +1 -0
- package/dist/cjs/core/index.d.ts +9 -9
- package/dist/cjs/core/index.d.ts.map +1 -1
- package/dist/cjs/core/index.js +9 -9
- package/dist/cjs/core/index.js.map +1 -1
- package/dist/cjs/repository/base-repository.d.ts +1 -1
- package/dist/cjs/repository/base-repository.d.ts.map +1 -1
- package/dist/cjs/repository/entity-schema-registry.d.ts +1 -1
- package/dist/cjs/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/cjs/repository/entity-schema-registry.js +5 -5
- package/dist/cjs/repository/entity-schema-registry.js.map +1 -1
- package/dist/cjs/types/change-tracker.d.ts +1 -1
- package/dist/cjs/types/change-tracker.d.ts.map +1 -1
- package/dist/cjs/types/domain.d.ts +1 -1
- package/dist/cjs/types/domain.d.ts.map +1 -1
- package/dist/cjs/types/event-bus.d.ts +1 -1
- package/dist/cjs/types/event-bus.d.ts.map +1 -1
- package/dist/cjs/types/unit-of-work.d.ts +2 -2
- package/dist/cjs/types/unit-of-work.d.ts.map +1 -1
- package/dist/cjs/types/utils.d.ts +1 -1
- package/dist/cjs/types/utils.d.ts.map +1 -1
- package/dist/esm/constants.d.ts +1 -1
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/core/base-entity.d.ts +2 -1
- package/dist/esm/core/base-entity.d.ts.map +1 -1
- package/dist/esm/core/base-entity.js +3 -1
- package/dist/esm/core/base-entity.js.map +1 -1
- package/dist/esm/core/change-tracker.d.ts +1 -0
- package/dist/esm/core/change-tracker.d.ts.map +1 -1
- package/dist/esm/core/change-tracker.js +24 -15
- package/dist/esm/core/change-tracker.js.map +1 -1
- package/dist/esm/core/domain-event.d.ts +1 -1
- package/dist/esm/core/domain-event.d.ts.map +1 -1
- package/dist/esm/core/domain-event.js +1 -1
- package/dist/esm/core/domain-event.js.map +1 -1
- package/dist/esm/core/entities.d.ts +8 -0
- package/dist/esm/core/entities.d.ts.map +1 -0
- package/dist/esm/core/entities.js +7 -0
- package/dist/esm/core/entities.js.map +1 -0
- package/dist/esm/core/index.d.ts +9 -9
- package/dist/esm/core/index.d.ts.map +1 -1
- package/dist/esm/core/index.js +9 -9
- package/dist/esm/core/index.js.map +1 -1
- package/dist/esm/repository/base-repository.d.ts +1 -1
- package/dist/esm/repository/base-repository.d.ts.map +1 -1
- package/dist/esm/repository/entity-schema-registry.d.ts +1 -1
- package/dist/esm/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/esm/repository/entity-schema-registry.js +1 -1
- package/dist/esm/repository/entity-schema-registry.js.map +1 -1
- package/dist/esm/types/change-tracker.d.ts +1 -1
- package/dist/esm/types/change-tracker.d.ts.map +1 -1
- package/dist/esm/types/domain.d.ts +1 -1
- package/dist/esm/types/domain.d.ts.map +1 -1
- package/dist/esm/types/event-bus.d.ts +1 -1
- package/dist/esm/types/event-bus.d.ts.map +1 -1
- package/dist/esm/types/unit-of-work.d.ts +2 -2
- package/dist/esm/types/unit-of-work.d.ts.map +1 -1
- package/dist/esm/types/utils.d.ts +1 -1
- package/dist/esm/types/utils.d.ts.map +1 -1
- package/dist/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/tsconfig.types.tsbuildinfo +1 -1
- package/dist/types/constants.d.ts +1 -1
- package/dist/types/constants.d.ts.map +1 -1
- package/dist/types/core/base-entity.d.ts +2 -1
- package/dist/types/core/base-entity.d.ts.map +1 -1
- package/dist/types/core/change-tracker.d.ts +1 -0
- package/dist/types/core/change-tracker.d.ts.map +1 -1
- package/dist/types/core/domain-event.d.ts +1 -1
- package/dist/types/core/domain-event.d.ts.map +1 -1
- package/dist/types/core/entities.d.ts +8 -0
- package/dist/types/core/entities.d.ts.map +1 -0
- package/dist/types/core/index.d.ts +9 -9
- package/dist/types/core/index.d.ts.map +1 -1
- package/dist/types/repository/base-repository.d.ts +1 -1
- package/dist/types/repository/base-repository.d.ts.map +1 -1
- package/dist/types/repository/entity-schema-registry.d.ts +1 -1
- package/dist/types/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/types/types/change-tracker.d.ts +1 -1
- package/dist/types/types/change-tracker.d.ts.map +1 -1
- package/dist/types/types/domain.d.ts +1 -1
- package/dist/types/types/domain.d.ts.map +1 -1
- package/dist/types/types/event-bus.d.ts +1 -1
- package/dist/types/types/event-bus.d.ts.map +1 -1
- package/dist/types/types/unit-of-work.d.ts +2 -2
- package/dist/types/types/unit-of-work.d.ts.map +1 -1
- package/dist/types/types/utils.d.ts +1 -1
- package/dist/types/types/utils.d.ts.map +1 -1
- package/package.json +68 -68
- package/src/constants.ts +82 -82
- package/src/core/aggregate-changes.ts +466 -466
- package/src/core/base-entity.ts +4 -1
- package/src/core/change-tracker.ts +30 -16
- package/src/core/domain-event.ts +41 -41
- package/src/core/{entity.ts → entities.ts} +13 -13
- package/src/core/index.ts +9 -9
- package/src/core/value-object.ts +179 -179
- package/src/repository/base-repository.ts +81 -81
- package/src/repository/entity-schema-registry.ts +1 -1
- package/src/types/change-tracker.ts +268 -268
- package/src/types/domain.ts +41 -41
- package/src/types/event-bus.ts +17 -17
- package/src/types/unit-of-work.ts +46 -46
- package/src/types/utils.ts +24 -24
- package/src/utils/helpers.ts +50 -50
package/src/core/base-entity.ts
CHANGED
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
} from "../types/index.js";
|
|
15
15
|
import { DEFAULT_VALIDATION_CONFIG } from "../constants.js";
|
|
16
16
|
import { DomainError } from "../exceptions.js";
|
|
17
|
-
import {
|
|
17
|
+
import { AggregateChanges } from "./aggregate-changes.js";
|
|
18
|
+
import { ChangeTracker } from "./change-tracker.js";
|
|
19
|
+
import { Id } from "./id.js";
|
|
20
|
+
import { ValueObject } from "./value-object.js";
|
|
18
21
|
import { getStaticProperty } from "../utils/helpers.js";
|
|
19
22
|
|
|
20
23
|
export abstract class BaseEntity<
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Id } from "./id.js";
|
|
2
|
-
import {
|
|
2
|
+
import { BaseEntity } from "./base-entity.js";
|
|
3
3
|
import { ValueObject } from "./value-object.js";
|
|
4
4
|
import { ArrayState, HistoryEntry, TrackedItem } from "../types/index.js";
|
|
5
5
|
import { EntityChangeState } from "../types/change-tracker.js";
|
|
@@ -97,7 +97,7 @@ export class ChangeTracker {
|
|
|
97
97
|
|
|
98
98
|
if (Array.isArray(value)) {
|
|
99
99
|
this.captureArrayState(value, propPath, depth + 1, id, entityName);
|
|
100
|
-
} else if (value instanceof
|
|
100
|
+
} else if (value instanceof BaseEntity) {
|
|
101
101
|
const nestedName = this.getEntityName(value);
|
|
102
102
|
this.captureEntityState(
|
|
103
103
|
value,
|
|
@@ -137,7 +137,7 @@ export class ChangeTracker {
|
|
|
137
137
|
// Only track individual items for non-primitive arrays
|
|
138
138
|
if (!isPrimitive) {
|
|
139
139
|
arr.forEach((item, index) => {
|
|
140
|
-
if (item instanceof
|
|
140
|
+
if (item instanceof BaseEntity) {
|
|
141
141
|
const itemPath = `${path}[${index}]`;
|
|
142
142
|
this.captureEntityState(
|
|
143
143
|
item,
|
|
@@ -175,7 +175,7 @@ export class ChangeTracker {
|
|
|
175
175
|
return this.createArrayProxy(value, currentPath);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
if (value instanceof
|
|
178
|
+
if (value instanceof BaseEntity) {
|
|
179
179
|
const nestedTracker = new ChangeTracker(
|
|
180
180
|
value,
|
|
181
181
|
this.getEntityName(value),
|
|
@@ -230,7 +230,10 @@ export class ChangeTracker {
|
|
|
230
230
|
|
|
231
231
|
if (Array.isArray(newValue)) {
|
|
232
232
|
this.handleArrayAssignment(currentPath, oldValue);
|
|
233
|
-
} else if (
|
|
233
|
+
} else if (
|
|
234
|
+
(newValue instanceof BaseEntity && !this.isSelfReference(newValue)) ||
|
|
235
|
+
(oldValue instanceof BaseEntity && !this.isSelfReference(oldValue))
|
|
236
|
+
) {
|
|
234
237
|
this.handleEntityChange(currentPath, oldValue, newValue);
|
|
235
238
|
}
|
|
236
239
|
|
|
@@ -277,7 +280,7 @@ export class ChangeTracker {
|
|
|
277
280
|
|
|
278
281
|
const result = Reflect.deleteProperty(target, prop);
|
|
279
282
|
|
|
280
|
-
if (oldValue instanceof
|
|
283
|
+
if (oldValue instanceof BaseEntity && !this.isSelfReference(oldValue)) {
|
|
281
284
|
this.handleEntityChange(currentPath, oldValue, undefined);
|
|
282
285
|
}
|
|
283
286
|
|
|
@@ -359,7 +362,7 @@ export class ChangeTracker {
|
|
|
359
362
|
return value.bind(target);
|
|
360
363
|
}
|
|
361
364
|
|
|
362
|
-
if (!isNaN(Number(prop)) && value instanceof
|
|
365
|
+
if (!isNaN(Number(prop)) && value instanceof BaseEntity) {
|
|
363
366
|
const nestedPath = `${path}[${String(prop)}]`;
|
|
364
367
|
const nestedTracker = new ChangeTracker(
|
|
365
368
|
value,
|
|
@@ -454,7 +457,12 @@ export class ChangeTracker {
|
|
|
454
457
|
|
|
455
458
|
// 1:1 entity relations are tracked by analyzeEntityChanges
|
|
456
459
|
if (rootTracker.trackedEntities.has(path)) continue;
|
|
457
|
-
if (
|
|
460
|
+
if (
|
|
461
|
+
(originalValue instanceof BaseEntity &&
|
|
462
|
+
!this.isSelfReference(originalValue)) ||
|
|
463
|
+
(currentValue instanceof BaseEntity &&
|
|
464
|
+
!this.isSelfReference(currentValue))
|
|
465
|
+
) {
|
|
458
466
|
continue;
|
|
459
467
|
}
|
|
460
468
|
|
|
@@ -605,7 +613,7 @@ export class ChangeTracker {
|
|
|
605
613
|
const relationField = propName;
|
|
606
614
|
|
|
607
615
|
for (const child of value) {
|
|
608
|
-
if (child instanceof
|
|
616
|
+
if (child instanceof BaseEntity) {
|
|
609
617
|
const childEntityName = this.getEntityName(child);
|
|
610
618
|
changes.addCreate(
|
|
611
619
|
childEntityName,
|
|
@@ -618,7 +626,7 @@ export class ChangeTracker {
|
|
|
618
626
|
this.markNestedItemsAsCreated(child, parentDepth + 1, changes);
|
|
619
627
|
}
|
|
620
628
|
}
|
|
621
|
-
} else if (value instanceof
|
|
629
|
+
} else if (value instanceof BaseEntity) {
|
|
622
630
|
const childEntityName = this.getEntityName(value);
|
|
623
631
|
changes.addCreate(
|
|
624
632
|
childEntityName,
|
|
@@ -667,8 +675,8 @@ export class ChangeTracker {
|
|
|
667
675
|
nestedItem,
|
|
668
676
|
parentDepth + 1,
|
|
669
677
|
relationField,
|
|
670
|
-
|
|
671
|
-
|
|
678
|
+
parentId,
|
|
679
|
+
parentEntity
|
|
672
680
|
);
|
|
673
681
|
|
|
674
682
|
this.markNestedJsonItemAsDeleted(
|
|
@@ -753,7 +761,7 @@ export class ChangeTracker {
|
|
|
753
761
|
originalArray: any[]
|
|
754
762
|
): string | undefined {
|
|
755
763
|
for (const originalItem of originalArray) {
|
|
756
|
-
if (originalItem instanceof
|
|
764
|
+
if (originalItem instanceof BaseEntity) {
|
|
757
765
|
const originalJson = this.deepClone(originalItem);
|
|
758
766
|
if (JSON.stringify(originalJson) === JSON.stringify(jsonItem)) {
|
|
759
767
|
const key = this.getItemKey(originalItem);
|
|
@@ -783,7 +791,7 @@ export class ChangeTracker {
|
|
|
783
791
|
|
|
784
792
|
if (Array.isArray(value)) {
|
|
785
793
|
value.forEach((item, index) => {
|
|
786
|
-
if (item instanceof
|
|
794
|
+
if (item instanceof BaseEntity) {
|
|
787
795
|
this.collectNestedArrays(
|
|
788
796
|
item,
|
|
789
797
|
`${propPath}[${index}]`,
|
|
@@ -792,7 +800,7 @@ export class ChangeTracker {
|
|
|
792
800
|
);
|
|
793
801
|
}
|
|
794
802
|
});
|
|
795
|
-
} else if (value instanceof
|
|
803
|
+
} else if (value instanceof BaseEntity) {
|
|
796
804
|
this.collectNestedArrays(value, propPath, allArrays, processedArrays);
|
|
797
805
|
}
|
|
798
806
|
}
|
|
@@ -1000,7 +1008,7 @@ export class ChangeTracker {
|
|
|
1000
1008
|
continue;
|
|
1001
1009
|
}
|
|
1002
1010
|
|
|
1003
|
-
if (currValue instanceof
|
|
1011
|
+
if (currValue instanceof BaseEntity) {
|
|
1004
1012
|
continue;
|
|
1005
1013
|
}
|
|
1006
1014
|
|
|
@@ -1053,6 +1061,12 @@ export class ChangeTracker {
|
|
|
1053
1061
|
return this.rootTracker || this;
|
|
1054
1062
|
}
|
|
1055
1063
|
|
|
1064
|
+
private isSelfReference(value: BaseEntity<any>): boolean {
|
|
1065
|
+
const targetId = this.getEntityId(this.target);
|
|
1066
|
+
const valueId = this.getEntityId(value);
|
|
1067
|
+
return !!targetId && !!valueId && targetId === valueId;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1056
1070
|
private buildPath(prop: string): string {
|
|
1057
1071
|
return this.path ? `${this.path}.${prop}` : prop;
|
|
1058
1072
|
}
|
package/src/core/domain-event.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { IDomainEvent } from "
|
|
2
|
-
import UUID from "../utils/crypto";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Base class for domain events
|
|
6
|
-
*/
|
|
7
|
-
export abstract class DomainEvent<P> implements IDomainEvent<P> {
|
|
8
|
-
public readonly eventId: string;
|
|
9
|
-
public readonly occurredOn: Date;
|
|
10
|
-
public readonly payload: P;
|
|
11
|
-
static readonly queueName?: string;
|
|
12
|
-
|
|
13
|
-
constructor(payload: P) {
|
|
14
|
-
this.eventId = this.generateEventId();
|
|
15
|
-
this.occurredOn = new Date();
|
|
16
|
-
this.payload = payload;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Get the event name (defaults to class name)
|
|
21
|
-
*/
|
|
22
|
-
get eventName(): string {
|
|
23
|
-
return this.constructor.name;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Generate a UUID v4
|
|
28
|
-
*/
|
|
29
|
-
private generateEventId(): string {
|
|
30
|
-
return UUID();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
toJSON() {
|
|
34
|
-
return {
|
|
35
|
-
eventId: this.eventId,
|
|
36
|
-
eventName: this.eventName,
|
|
37
|
-
occurredOn: this.occurredOn.toISOString(),
|
|
38
|
-
payload: this.payload,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
import type { IDomainEvent } from "../types/domain-event.js";
|
|
2
|
+
import UUID from "../utils/crypto.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base class for domain events
|
|
6
|
+
*/
|
|
7
|
+
export abstract class DomainEvent<P> implements IDomainEvent<P> {
|
|
8
|
+
public readonly eventId: string;
|
|
9
|
+
public readonly occurredOn: Date;
|
|
10
|
+
public readonly payload: P;
|
|
11
|
+
static readonly queueName?: string;
|
|
12
|
+
|
|
13
|
+
constructor(payload: P) {
|
|
14
|
+
this.eventId = this.generateEventId();
|
|
15
|
+
this.occurredOn = new Date();
|
|
16
|
+
this.payload = payload;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the event name (defaults to class name)
|
|
21
|
+
*/
|
|
22
|
+
get eventName(): string {
|
|
23
|
+
return this.constructor.name;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a UUID v4
|
|
28
|
+
*/
|
|
29
|
+
private generateEventId(): string {
|
|
30
|
+
return UUID();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toJSON() {
|
|
34
|
+
return {
|
|
35
|
+
eventId: this.eventId,
|
|
36
|
+
eventName: this.eventName,
|
|
37
|
+
occurredOn: this.occurredOn.toISOString(),
|
|
38
|
+
payload: this.payload,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { BaseEntity } from "./base-entity.js";
|
|
2
|
-
import { BaseAggregate } from "./base-aggregate.js";
|
|
3
|
-
import { BaseProps } from "../types/index.js";
|
|
4
|
-
|
|
5
|
-
export class Entity<
|
|
6
|
-
T extends BaseProps,
|
|
7
|
-
TOptionalInput extends keyof T = never,
|
|
8
|
-
> extends BaseEntity<T, TOptionalInput> {}
|
|
9
|
-
|
|
10
|
-
export class Aggregate<
|
|
11
|
-
T extends BaseProps,
|
|
12
|
-
TOptionalInput extends keyof T = never,
|
|
13
|
-
> extends BaseAggregate<T, TOptionalInput> {}
|
|
1
|
+
import { BaseEntity } from "./base-entity.js";
|
|
2
|
+
import { BaseAggregate } from "./base-aggregate.js";
|
|
3
|
+
import { BaseProps } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
export class Entity<
|
|
6
|
+
T extends BaseProps,
|
|
7
|
+
TOptionalInput extends keyof T = never,
|
|
8
|
+
> extends BaseEntity<T, TOptionalInput> {}
|
|
9
|
+
|
|
10
|
+
export class Aggregate<
|
|
11
|
+
T extends BaseProps,
|
|
12
|
+
TOptionalInput extends keyof T = never,
|
|
13
|
+
> extends BaseAggregate<T, TOptionalInput> {}
|
package/src/core/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export * from "./base-entity";
|
|
2
|
-
export * from "./base-aggregate";
|
|
3
|
-
export * from "./aggregate-changes";
|
|
4
|
-
export * from "./change-tracker";
|
|
5
|
-
export * from "./domain-event";
|
|
6
|
-
export * from "./entity-changes";
|
|
7
|
-
export * from "./
|
|
8
|
-
export * from "./id";
|
|
9
|
-
export * from "./value-object";
|
|
1
|
+
export * from "./base-entity.js";
|
|
2
|
+
export * from "./base-aggregate.js";
|
|
3
|
+
export * from "./aggregate-changes.js";
|
|
4
|
+
export * from "./change-tracker.js";
|
|
5
|
+
export * from "./domain-event.js";
|
|
6
|
+
export * from "./entity-changes.js";
|
|
7
|
+
export * from "./entities.js";
|
|
8
|
+
export * from "./id.js";
|
|
9
|
+
export * from "./value-object.js";
|
package/src/core/value-object.ts
CHANGED
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ValidationError,
|
|
3
|
-
ValidationIssue,
|
|
4
|
-
ValidationIssueCollector,
|
|
5
|
-
} from "../validation-error.js";
|
|
6
|
-
import {
|
|
7
|
-
VOHooks,
|
|
8
|
-
ValidationConfig,
|
|
9
|
-
StandardSchema,
|
|
10
|
-
EntityValidation,
|
|
11
|
-
Primitive,
|
|
12
|
-
} from "../types/index.js";
|
|
13
|
-
import { DEFAULT_VALIDATION_CONFIG } from "../constants.js";
|
|
14
|
-
import { DomainError } from "../exceptions.js";
|
|
15
|
-
import { getStaticProperty } from "../utils/helpers.js";
|
|
16
|
-
|
|
17
|
-
export abstract class ValueObject<T extends Primitive> {
|
|
18
|
-
public readonly value!: T;
|
|
19
|
-
private validationConfig: Required<ValidationConfig>;
|
|
20
|
-
private domainHooks?: VOHooks<T, any>;
|
|
21
|
-
private domainSchema?: StandardSchema<T>;
|
|
22
|
-
private readonly issueCollector = new ValidationIssueCollector();
|
|
23
|
-
|
|
24
|
-
protected static validation?: EntityValidation<any>;
|
|
25
|
-
protected static hooks?: VOHooks<any, any>;
|
|
26
|
-
|
|
27
|
-
constructor(value: T) {
|
|
28
|
-
const validation = getStaticProperty<EntityValidation<T>>(
|
|
29
|
-
this,
|
|
30
|
-
"validation"
|
|
31
|
-
);
|
|
32
|
-
const hooks = getStaticProperty<VOHooks<T, any>>(this, "hooks");
|
|
33
|
-
|
|
34
|
-
if (hooks?.onBeforeCreate) {
|
|
35
|
-
hooks.onBeforeCreate(value);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
this.domainHooks = hooks;
|
|
39
|
-
|
|
40
|
-
if (validation?.schema) {
|
|
41
|
-
this.domainSchema = validation.schema;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
this.validationConfig = {
|
|
45
|
-
...DEFAULT_VALIDATION_CONFIG,
|
|
46
|
-
...validation?.config,
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
if (this.domainSchema && this.validationConfig.onCreate) {
|
|
50
|
-
this.validateValue(value);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
this.value = value;
|
|
54
|
-
|
|
55
|
-
if (hooks?.rules) {
|
|
56
|
-
this.runRulesHook();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
Object.freeze(this);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Add a validation issue during rules hook execution (non-throwing mode).
|
|
64
|
-
*/
|
|
65
|
-
public addValidationIssue(path: string | string[], message: string): void {
|
|
66
|
-
this.issueCollector.add(path, message);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private beginValidationCycle(): void {
|
|
70
|
-
this.issueCollector.clear();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
private finalizeValidation(collectedIssues: ValidationIssue[] = []): void {
|
|
74
|
-
const existing = (this as any)._validationError as
|
|
75
|
-
| ValidationError
|
|
76
|
-
| undefined;
|
|
77
|
-
const merged = ValidationError.merge(existing, collectedIssues);
|
|
78
|
-
|
|
79
|
-
if (!merged) {
|
|
80
|
-
delete (this as any)._validationError;
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (this.validationConfig.throwOnError) {
|
|
85
|
-
throw merged;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
(this as any)._validationError = merged;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
private runRulesHook(): void {
|
|
92
|
-
if (!this.domainHooks?.rules) return;
|
|
93
|
-
|
|
94
|
-
this.beginValidationCycle();
|
|
95
|
-
this.domainHooks.rules(this as any);
|
|
96
|
-
this.finalizeValidation([...this.issueCollector.getIssues()]);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private validateValue(value: T): void {
|
|
100
|
-
const schemaError = this.validateSchema(value);
|
|
101
|
-
if (!schemaError) return;
|
|
102
|
-
|
|
103
|
-
if (this.validationConfig.throwOnError) {
|
|
104
|
-
throw schemaError;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
(this as any)._validationError = schemaError;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private validateSchema(value: T): ValidationError | null {
|
|
111
|
-
if (!this.domainSchema) return null;
|
|
112
|
-
|
|
113
|
-
const result = this.domainSchema["~standard"].validate(value);
|
|
114
|
-
|
|
115
|
-
if (result instanceof Promise) {
|
|
116
|
-
throw new DomainError(
|
|
117
|
-
"Async validation not supported in constructor. Use sync validation schema."
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (result.issues && result.issues.length > 0) {
|
|
122
|
-
return new ValidationError(
|
|
123
|
-
result.issues.map((issue) => ({
|
|
124
|
-
path: issue.path?.map((p) => this.extractPathKey(p)) || [],
|
|
125
|
-
message: issue.message,
|
|
126
|
-
}))
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private extractPathKey(pathSegment: unknown): string {
|
|
134
|
-
if (pathSegment === null || pathSegment === undefined) {
|
|
135
|
-
return "";
|
|
136
|
-
}
|
|
137
|
-
if (typeof pathSegment === "string" || typeof pathSegment === "number") {
|
|
138
|
-
return String(pathSegment);
|
|
139
|
-
}
|
|
140
|
-
if (typeof pathSegment === "symbol") {
|
|
141
|
-
return pathSegment.toString();
|
|
142
|
-
}
|
|
143
|
-
if (typeof pathSegment === "object" && "key" in pathSegment) {
|
|
144
|
-
return String((pathSegment as { key: unknown }).key);
|
|
145
|
-
}
|
|
146
|
-
return String(pathSegment);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Returns true if the value object has validation errors (when throwOnError is false).
|
|
151
|
-
*/
|
|
152
|
-
get hasValidationErrors(): boolean {
|
|
153
|
-
return !!(this as any)._validationError;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Returns the validation errors (when throwOnError is false).
|
|
158
|
-
*/
|
|
159
|
-
get validationErrors(): ValidationError | undefined {
|
|
160
|
-
return (this as any)._validationError;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Compare this ValueObject with another for equality based on their properties.
|
|
165
|
-
*/
|
|
166
|
-
equals(other: ValueObject<T>): boolean {
|
|
167
|
-
if (!other || !(other instanceof ValueObject)) return false;
|
|
168
|
-
return this.value === other.value;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Creates a new ValueObject with updated value.
|
|
173
|
-
* ValueObjects are immutable, so this returns a new instance.
|
|
174
|
-
*/
|
|
175
|
-
protected clone(value: T): this {
|
|
176
|
-
const Constructor = this.constructor as new (value: T) => this;
|
|
177
|
-
return new Constructor(value);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
ValidationError,
|
|
3
|
+
ValidationIssue,
|
|
4
|
+
ValidationIssueCollector,
|
|
5
|
+
} from "../validation-error.js";
|
|
6
|
+
import {
|
|
7
|
+
VOHooks,
|
|
8
|
+
ValidationConfig,
|
|
9
|
+
StandardSchema,
|
|
10
|
+
EntityValidation,
|
|
11
|
+
Primitive,
|
|
12
|
+
} from "../types/index.js";
|
|
13
|
+
import { DEFAULT_VALIDATION_CONFIG } from "../constants.js";
|
|
14
|
+
import { DomainError } from "../exceptions.js";
|
|
15
|
+
import { getStaticProperty } from "../utils/helpers.js";
|
|
16
|
+
|
|
17
|
+
export abstract class ValueObject<T extends Primitive> {
|
|
18
|
+
public readonly value!: T;
|
|
19
|
+
private validationConfig: Required<ValidationConfig>;
|
|
20
|
+
private domainHooks?: VOHooks<T, any>;
|
|
21
|
+
private domainSchema?: StandardSchema<T>;
|
|
22
|
+
private readonly issueCollector = new ValidationIssueCollector();
|
|
23
|
+
|
|
24
|
+
protected static validation?: EntityValidation<any>;
|
|
25
|
+
protected static hooks?: VOHooks<any, any>;
|
|
26
|
+
|
|
27
|
+
constructor(value: T) {
|
|
28
|
+
const validation = getStaticProperty<EntityValidation<T>>(
|
|
29
|
+
this,
|
|
30
|
+
"validation"
|
|
31
|
+
);
|
|
32
|
+
const hooks = getStaticProperty<VOHooks<T, any>>(this, "hooks");
|
|
33
|
+
|
|
34
|
+
if (hooks?.onBeforeCreate) {
|
|
35
|
+
hooks.onBeforeCreate(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.domainHooks = hooks;
|
|
39
|
+
|
|
40
|
+
if (validation?.schema) {
|
|
41
|
+
this.domainSchema = validation.schema;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.validationConfig = {
|
|
45
|
+
...DEFAULT_VALIDATION_CONFIG,
|
|
46
|
+
...validation?.config,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (this.domainSchema && this.validationConfig.onCreate) {
|
|
50
|
+
this.validateValue(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.value = value;
|
|
54
|
+
|
|
55
|
+
if (hooks?.rules) {
|
|
56
|
+
this.runRulesHook();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Object.freeze(this);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Add a validation issue during rules hook execution (non-throwing mode).
|
|
64
|
+
*/
|
|
65
|
+
public addValidationIssue(path: string | string[], message: string): void {
|
|
66
|
+
this.issueCollector.add(path, message);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private beginValidationCycle(): void {
|
|
70
|
+
this.issueCollector.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private finalizeValidation(collectedIssues: ValidationIssue[] = []): void {
|
|
74
|
+
const existing = (this as any)._validationError as
|
|
75
|
+
| ValidationError
|
|
76
|
+
| undefined;
|
|
77
|
+
const merged = ValidationError.merge(existing, collectedIssues);
|
|
78
|
+
|
|
79
|
+
if (!merged) {
|
|
80
|
+
delete (this as any)._validationError;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (this.validationConfig.throwOnError) {
|
|
85
|
+
throw merged;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
(this as any)._validationError = merged;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private runRulesHook(): void {
|
|
92
|
+
if (!this.domainHooks?.rules) return;
|
|
93
|
+
|
|
94
|
+
this.beginValidationCycle();
|
|
95
|
+
this.domainHooks.rules(this as any);
|
|
96
|
+
this.finalizeValidation([...this.issueCollector.getIssues()]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private validateValue(value: T): void {
|
|
100
|
+
const schemaError = this.validateSchema(value);
|
|
101
|
+
if (!schemaError) return;
|
|
102
|
+
|
|
103
|
+
if (this.validationConfig.throwOnError) {
|
|
104
|
+
throw schemaError;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
(this as any)._validationError = schemaError;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private validateSchema(value: T): ValidationError | null {
|
|
111
|
+
if (!this.domainSchema) return null;
|
|
112
|
+
|
|
113
|
+
const result = this.domainSchema["~standard"].validate(value);
|
|
114
|
+
|
|
115
|
+
if (result instanceof Promise) {
|
|
116
|
+
throw new DomainError(
|
|
117
|
+
"Async validation not supported in constructor. Use sync validation schema."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (result.issues && result.issues.length > 0) {
|
|
122
|
+
return new ValidationError(
|
|
123
|
+
result.issues.map((issue) => ({
|
|
124
|
+
path: issue.path?.map((p) => this.extractPathKey(p)) || [],
|
|
125
|
+
message: issue.message,
|
|
126
|
+
}))
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private extractPathKey(pathSegment: unknown): string {
|
|
134
|
+
if (pathSegment === null || pathSegment === undefined) {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
if (typeof pathSegment === "string" || typeof pathSegment === "number") {
|
|
138
|
+
return String(pathSegment);
|
|
139
|
+
}
|
|
140
|
+
if (typeof pathSegment === "symbol") {
|
|
141
|
+
return pathSegment.toString();
|
|
142
|
+
}
|
|
143
|
+
if (typeof pathSegment === "object" && "key" in pathSegment) {
|
|
144
|
+
return String((pathSegment as { key: unknown }).key);
|
|
145
|
+
}
|
|
146
|
+
return String(pathSegment);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns true if the value object has validation errors (when throwOnError is false).
|
|
151
|
+
*/
|
|
152
|
+
get hasValidationErrors(): boolean {
|
|
153
|
+
return !!(this as any)._validationError;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Returns the validation errors (when throwOnError is false).
|
|
158
|
+
*/
|
|
159
|
+
get validationErrors(): ValidationError | undefined {
|
|
160
|
+
return (this as any)._validationError;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Compare this ValueObject with another for equality based on their properties.
|
|
165
|
+
*/
|
|
166
|
+
equals(other: ValueObject<T>): boolean {
|
|
167
|
+
if (!other || !(other instanceof ValueObject)) return false;
|
|
168
|
+
return this.value === other.value;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a new ValueObject with updated value.
|
|
173
|
+
* ValueObjects are immutable, so this returns a new instance.
|
|
174
|
+
*/
|
|
175
|
+
protected clone(value: T): this {
|
|
176
|
+
const Constructor = this.constructor as new (value: T) => this;
|
|
177
|
+
return new Constructor(value);
|
|
178
|
+
}
|
|
179
|
+
}
|