@woltz/rich-domain 1.9.0 → 1.9.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/cjs/core/aggregate-changes.d.ts +14 -0
- package/dist/cjs/core/aggregate-changes.d.ts.map +1 -1
- package/dist/cjs/core/aggregate-changes.js +18 -0
- package/dist/cjs/core/aggregate-changes.js.map +1 -1
- package/dist/cjs/core/base-entity.d.ts +2 -0
- package/dist/cjs/core/base-entity.d.ts.map +1 -1
- package/dist/cjs/core/base-entity.js +39 -41
- package/dist/cjs/core/base-entity.js.map +1 -1
- package/dist/cjs/core/change-tracker.d.ts +8 -0
- package/dist/cjs/core/change-tracker.d.ts.map +1 -1
- package/dist/cjs/core/change-tracker.js +36 -6
- package/dist/cjs/core/change-tracker.js.map +1 -1
- package/dist/cjs/core/domain-event.d.ts +3 -0
- package/dist/cjs/core/domain-event.d.ts.map +1 -1
- package/dist/cjs/core/domain-event.js +8 -1
- package/dist/cjs/core/domain-event.js.map +1 -1
- package/dist/cjs/core/value-object.d.ts.map +1 -1
- package/dist/cjs/core/value-object.js +3 -5
- package/dist/cjs/core/value-object.js.map +1 -1
- package/dist/cjs/criteria.d.ts +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/repository/entity-schema-registry.d.ts +56 -3
- package/dist/cjs/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/cjs/repository/entity-schema-registry.js +61 -6
- package/dist/cjs/repository/entity-schema-registry.js.map +1 -1
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/index.js +1 -0
- package/dist/cjs/types/index.js.map +1 -1
- package/dist/cjs/types/outbox-store.d.ts +91 -0
- package/dist/cjs/types/outbox-store.d.ts.map +1 -0
- package/dist/cjs/types/outbox-store.js +3 -0
- package/dist/cjs/types/outbox-store.js.map +1 -0
- package/dist/cjs/utils/helpers.d.ts +1 -0
- package/dist/cjs/utils/helpers.d.ts.map +1 -1
- package/dist/cjs/utils/helpers.js +4 -0
- package/dist/cjs/utils/helpers.js.map +1 -1
- package/dist/esm/core/aggregate-changes.d.ts +14 -0
- package/dist/esm/core/aggregate-changes.d.ts.map +1 -1
- package/dist/esm/core/aggregate-changes.js +18 -0
- package/dist/esm/core/aggregate-changes.js.map +1 -1
- package/dist/esm/core/base-entity.d.ts +2 -0
- package/dist/esm/core/base-entity.d.ts.map +1 -1
- package/dist/esm/core/base-entity.js +37 -39
- package/dist/esm/core/base-entity.js.map +1 -1
- package/dist/esm/core/change-tracker.d.ts +8 -0
- package/dist/esm/core/change-tracker.d.ts.map +1 -1
- package/dist/esm/core/change-tracker.js +36 -6
- package/dist/esm/core/change-tracker.js.map +1 -1
- package/dist/esm/core/domain-event.d.ts +3 -0
- package/dist/esm/core/domain-event.d.ts.map +1 -1
- package/dist/esm/core/domain-event.js +5 -1
- package/dist/esm/core/domain-event.js.map +1 -1
- package/dist/esm/core/value-object.d.ts.map +1 -1
- package/dist/esm/core/value-object.js +1 -3
- package/dist/esm/core/value-object.js.map +1 -1
- package/dist/esm/criteria.d.ts +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/repository/entity-schema-registry.d.ts +56 -3
- package/dist/esm/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/esm/repository/entity-schema-registry.js +61 -6
- package/dist/esm/repository/entity-schema-registry.js.map +1 -1
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/types/index.js.map +1 -1
- package/dist/esm/types/outbox-store.d.ts +91 -0
- package/dist/esm/types/outbox-store.d.ts.map +1 -0
- package/dist/esm/types/outbox-store.js +2 -0
- package/dist/esm/types/outbox-store.js.map +1 -0
- package/dist/esm/utils/helpers.d.ts +1 -0
- package/dist/esm/utils/helpers.d.ts.map +1 -1
- package/dist/esm/utils/helpers.js +3 -0
- package/dist/esm/utils/helpers.js.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/core/aggregate-changes.d.ts +14 -0
- package/dist/types/core/aggregate-changes.d.ts.map +1 -1
- package/dist/types/core/base-entity.d.ts +2 -0
- package/dist/types/core/base-entity.d.ts.map +1 -1
- package/dist/types/core/change-tracker.d.ts +8 -0
- package/dist/types/core/change-tracker.d.ts.map +1 -1
- package/dist/types/core/domain-event.d.ts +3 -0
- package/dist/types/core/domain-event.d.ts.map +1 -1
- package/dist/types/core/value-object.d.ts.map +1 -1
- package/dist/types/criteria.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/repository/entity-schema-registry.d.ts +56 -3
- package/dist/types/repository/entity-schema-registry.d.ts.map +1 -1
- package/dist/types/types/index.d.ts +1 -0
- package/dist/types/types/index.d.ts.map +1 -1
- package/dist/types/types/outbox-store.d.ts +91 -0
- package/dist/types/types/outbox-store.d.ts.map +1 -0
- package/dist/types/utils/helpers.d.ts +1 -0
- package/dist/types/utils/helpers.d.ts.map +1 -1
- package/package.json +68 -67
- package/src/constants.ts +82 -0
- package/src/core/aggregate-changes.ts +466 -0
- package/src/core/base-aggregate.ts +76 -0
- package/src/core/base-entity.ts +552 -0
- package/src/core/change-tracker.ts +1327 -0
- package/src/core/domain-event.ts +41 -0
- package/src/core/entity-changes.ts +146 -0
- package/src/core/entity.ts +13 -0
- package/src/core/id.ts +124 -0
- package/src/core/index.ts +9 -0
- package/src/core/value-object.ts +179 -0
- package/src/criteria.ts +574 -0
- package/src/exceptions.ts +549 -0
- package/src/index.ts +74 -0
- package/src/repository/base-repository.ts +81 -0
- package/src/repository/entity-schema-registry.ts +620 -0
- package/src/repository/index.ts +5 -0
- package/src/repository/mapper.ts +7 -0
- package/src/repository/paginated-result.ts +251 -0
- package/src/repository/unit-of-work.ts +76 -0
- package/src/types/change-tracker.ts +268 -0
- package/src/types/criteria.ts +197 -0
- package/src/types/domain-event.ts +29 -0
- package/src/types/domain.ts +41 -0
- package/src/types/event-bus.ts +17 -0
- package/src/types/index.ts +9 -0
- package/src/types/outbox-store.ts +97 -0
- package/src/types/standard-schema.ts +19 -0
- package/src/types/unit-of-work.ts +46 -0
- package/src/types/utils.ts +24 -0
- package/src/utils/criteria-operator-validation.ts +209 -0
- package/src/utils/crypto.ts +31 -0
- package/src/utils/helpers.ts +50 -0
- package/src/validation-error.ts +219 -0
- package/dist/cjs/t.d.ts +0 -2
- package/dist/cjs/t.d.ts.map +0 -1
- package/dist/cjs/t.js +0 -96
- package/dist/cjs/t.js.map +0 -1
- package/dist/esm/t.d.ts +0 -2
- package/dist/esm/t.d.ts.map +0 -1
- package/dist/esm/t.js +0 -94
- package/dist/esm/t.js.map +0 -1
- package/dist/types/t.d.ts +0 -2
- package/dist/types/t.d.ts.map +0 -1
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Operation,
|
|
3
|
+
CreateOperation,
|
|
4
|
+
UpdateOperation,
|
|
5
|
+
DeleteOperation,
|
|
6
|
+
} from "../types/change-tracker.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents the changes filtered for a specific entity.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const changes = user.getChanges();
|
|
14
|
+
* const postChanges = changes.of('Post');
|
|
15
|
+
*
|
|
16
|
+
* if (postChanges.hasCreates()) {
|
|
17
|
+
* console.log('Created posts:', postChanges.creates);
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* if (postChanges.hasUpdates()) {
|
|
21
|
+
* postChanges.updates.forEach(({ entity, changed }) => {
|
|
22
|
+
* console.log(`Post ${entity.id} has changed:`, changed);
|
|
23
|
+
* });
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class EntityChanges<T = any> {
|
|
28
|
+
constructor(private readonly operations: Operation<T>[]) {}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns all created entities
|
|
32
|
+
*/
|
|
33
|
+
get creates(): T[] {
|
|
34
|
+
return this.operations
|
|
35
|
+
.filter((op): op is CreateOperation<T> => op.type === "create")
|
|
36
|
+
.map((op) => op.data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Returns all updated entities with their changed fields
|
|
41
|
+
*/
|
|
42
|
+
get updates(): Array<{ entity: T; changed: Record<string, any> }> {
|
|
43
|
+
return this.operations
|
|
44
|
+
.filter((op): op is UpdateOperation<T> => op.type === "update")
|
|
45
|
+
.map((op) => ({
|
|
46
|
+
entity: op.data,
|
|
47
|
+
changed: op.changedFields,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Returns all deleted entities
|
|
53
|
+
*/
|
|
54
|
+
get deletes(): T[] {
|
|
55
|
+
return this.operations
|
|
56
|
+
.filter((op): op is DeleteOperation<T> => op.type === "delete")
|
|
57
|
+
.map((op) => op.data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns the IDs of the created entities
|
|
62
|
+
*/
|
|
63
|
+
get createIds(): string[] {
|
|
64
|
+
return this.operations
|
|
65
|
+
.filter((op): op is CreateOperation<T> => op.type === "create")
|
|
66
|
+
.map((op) => this.extractId(op.data))
|
|
67
|
+
.filter((id): id is string => id !== undefined);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns the IDs of the updated entities
|
|
72
|
+
*/
|
|
73
|
+
get updateIds(): string[] {
|
|
74
|
+
return this.operations
|
|
75
|
+
.filter((op): op is UpdateOperation<T> => op.type === "update")
|
|
76
|
+
.map((op) => op.id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns the IDs of the deleted entities
|
|
81
|
+
*/
|
|
82
|
+
get deleteIds(): string[] {
|
|
83
|
+
return this.operations
|
|
84
|
+
.filter((op): op is DeleteOperation<T> => op.type === "delete")
|
|
85
|
+
.map((op) => op.id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Checks if there are any creates
|
|
90
|
+
*/
|
|
91
|
+
hasCreates(): boolean {
|
|
92
|
+
return this.creates.length > 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks if there are any updates
|
|
97
|
+
*/
|
|
98
|
+
hasUpdates(): boolean {
|
|
99
|
+
return this.updates.length > 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks if there are any deletes
|
|
104
|
+
*/
|
|
105
|
+
hasDeletes(): boolean {
|
|
106
|
+
return this.deletes.length > 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Checks if there is any change
|
|
111
|
+
*/
|
|
112
|
+
hasChanges(): boolean {
|
|
113
|
+
return this.operations.length > 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if it is empty (no changes)
|
|
118
|
+
*/
|
|
119
|
+
isEmpty(): boolean {
|
|
120
|
+
return this.operations.length === 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns the total number of operations
|
|
125
|
+
*/
|
|
126
|
+
get count(): number {
|
|
127
|
+
return this.operations.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Returns the raw operations (for advanced use cases)
|
|
132
|
+
*/
|
|
133
|
+
get rawOperations(): Operation<T>[] {
|
|
134
|
+
return [...this.operations];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extracts the ID from an entity
|
|
139
|
+
*/
|
|
140
|
+
private extractId(entity: any): string | undefined {
|
|
141
|
+
if (!entity) return undefined;
|
|
142
|
+
if (entity.id?.value) return entity.id.value;
|
|
143
|
+
if (typeof entity.id === "string") return entity.id;
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +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> {}
|
package/src/core/id.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import UUID from "../utils/crypto.js";
|
|
2
|
+
|
|
3
|
+
export class Id {
|
|
4
|
+
private readonly _value: string;
|
|
5
|
+
private _isNew: boolean;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Create a new Id
|
|
9
|
+
* @param value - Optional existing ID value. If not provided, generates a new UUID.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // New entity (generates UUID)
|
|
13
|
+
* const newId = new Id();
|
|
14
|
+
* newuser.isNew() // true
|
|
15
|
+
*
|
|
16
|
+
* // Existing entity (uses provided ID)
|
|
17
|
+
* const existingId = new Id("550e8400-e29b-41d4-a716-446655440000");
|
|
18
|
+
* existinguser.isNew() // false
|
|
19
|
+
*/
|
|
20
|
+
constructor(value: string, isNew?: boolean);
|
|
21
|
+
constructor(value?: string);
|
|
22
|
+
constructor(value?: string, isNew?: boolean) {
|
|
23
|
+
if (value !== undefined) {
|
|
24
|
+
// ID was provided - this is an existing entity
|
|
25
|
+
this._value = value;
|
|
26
|
+
this._isNew = isNew ?? false;
|
|
27
|
+
} else {
|
|
28
|
+
// No ID provided - generate new one, this is a new entity
|
|
29
|
+
this._value = this.generateUUID();
|
|
30
|
+
this._isNew = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the string value of the ID
|
|
36
|
+
*/
|
|
37
|
+
get value(): string {
|
|
38
|
+
return this._value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if this ID represents a new entity
|
|
43
|
+
*/
|
|
44
|
+
public isNew(): boolean {
|
|
45
|
+
return this._isNew;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert to string (for JSON serialization and comparisons)
|
|
50
|
+
*/
|
|
51
|
+
toString(): string {
|
|
52
|
+
return this._value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Convert to JSON (returns the string value)
|
|
57
|
+
*/
|
|
58
|
+
toJSON(): string {
|
|
59
|
+
return this._value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check equality with another Id or string
|
|
64
|
+
*/
|
|
65
|
+
equals(other: Id | string): boolean {
|
|
66
|
+
if (other instanceof Id) {
|
|
67
|
+
return this._value === other._value;
|
|
68
|
+
}
|
|
69
|
+
return this._value === other;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generate a UUID v4
|
|
74
|
+
*/
|
|
75
|
+
private generateUUID(): string {
|
|
76
|
+
// Simple UUID v4 implementation
|
|
77
|
+
return UUID();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a new Id (convenience static method)
|
|
82
|
+
*/
|
|
83
|
+
static create(): Id {
|
|
84
|
+
return new Id();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Mark the Id as not new
|
|
89
|
+
*/
|
|
90
|
+
public markAsNotNew(): void {
|
|
91
|
+
this._isNew = false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Mark the Id as new
|
|
96
|
+
*/
|
|
97
|
+
public markAsNew(): void {
|
|
98
|
+
this._isNew = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create an Id from an existing value
|
|
103
|
+
*/
|
|
104
|
+
static from(value: string): Id {
|
|
105
|
+
return new Id(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Compose two IDs into a single ID
|
|
110
|
+
* @param a - The first ID
|
|
111
|
+
* @param b - The second ID
|
|
112
|
+
* @returns The composed ID
|
|
113
|
+
*/
|
|
114
|
+
static compose(a: string | Id, b: string | Id): string {
|
|
115
|
+
if (a instanceof Id) {
|
|
116
|
+
a = a.value;
|
|
117
|
+
}
|
|
118
|
+
if (b instanceof Id) {
|
|
119
|
+
b = b.value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `${a}-${b}`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +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 "./entity";
|
|
8
|
+
export * from "./id";
|
|
9
|
+
export * from "./value-object";
|
|
@@ -0,0 +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
|
+
}
|