drimion 0.1.0
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/LICENSE +7 -0
- package/README.md +955 -0
- package/dist/cli/index.js +1045 -0
- package/dist/cli/templates/aggregate.ts.hbs +22 -0
- package/dist/cli/templates/entity.ts.hbs +16 -0
- package/dist/cli/templates/repository.ts.hbs +24 -0
- package/dist/cli/templates/use-case.ts.hbs +20 -0
- package/dist/cli/templates/value-object.ts.hbs +16 -0
- package/dist/kernel/core/aggregate.ts +234 -0
- package/dist/kernel/core/entity.ts +348 -0
- package/dist/kernel/core/id.ts +207 -0
- package/dist/kernel/core/index.ts +5 -0
- package/dist/kernel/core/repository.ts +81 -0
- package/dist/kernel/core/value-object.ts +309 -0
- package/dist/kernel/events/browser-event-manager.ts +241 -0
- package/dist/kernel/events/domain-event.ts +76 -0
- package/dist/kernel/events/event-bus.ts +158 -0
- package/dist/kernel/events/event-context.ts +95 -0
- package/dist/kernel/events/event-manager.ts +20 -0
- package/dist/kernel/events/event-utils.ts +19 -0
- package/dist/kernel/events/index.ts +7 -0
- package/dist/kernel/events/server-event-manager.ts +169 -0
- package/dist/kernel/helpers/auto-mapper.ts +222 -0
- package/dist/kernel/helpers/domain-classes.ts +162 -0
- package/dist/kernel/helpers/domain-error.ts +52 -0
- package/dist/kernel/helpers/getters-setters.ts +385 -0
- package/dist/kernel/helpers/index.ts +7 -0
- package/dist/kernel/index.ts +73 -0
- package/dist/kernel/libs/crypto.ts +33 -0
- package/dist/kernel/libs/index.ts +5 -0
- package/dist/kernel/libs/iterator.ts +298 -0
- package/dist/kernel/libs/result.ts +252 -0
- package/dist/kernel/libs/utils.ts +260 -0
- package/dist/kernel/libs/validator.ts +353 -0
- package/dist/kernel/types/adapter.types.ts +26 -0
- package/dist/kernel/types/command.types.ts +37 -0
- package/dist/kernel/types/entity.types.ts +60 -0
- package/dist/kernel/types/event.types.ts +129 -0
- package/dist/kernel/types/index.ts +26 -0
- package/dist/kernel/types/iterator.types.ts +39 -0
- package/dist/kernel/types/result.types.ts +122 -0
- package/dist/kernel/types/uid.types.ts +18 -0
- package/dist/kernel/types/utils.types.ts +120 -0
- package/dist/kernel/types/value-object.types.ts +20 -0
- package/dist/kernel/utils/date.utils.ts +111 -0
- package/dist/kernel/utils/index.ts +32 -0
- package/dist/kernel/utils/number.utils.ts +341 -0
- package/dist/kernel/utils/object.utils.ts +61 -0
- package/dist/kernel/utils/string.utils.ts +128 -0
- package/dist/kernel/utils/type.utils.ts +33 -0
- package/package.json +59 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { UUID } from "../libs/crypto";
|
|
2
|
+
import validator from "../libs/validator";
|
|
3
|
+
import type { UID } from "../types/uid.types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @description
|
|
7
|
+
* Represents a unique identifier for Entities or Aggregates, providing methods
|
|
8
|
+
* to generate and manipulate ID values, including the ability to create new IDs, convert them
|
|
9
|
+
* to a shorter format, clone them, and check for equality.
|
|
10
|
+
*/
|
|
11
|
+
export class ID implements UID<string | number> {
|
|
12
|
+
protected readonly __kind = "ID" as const;
|
|
13
|
+
|
|
14
|
+
readonly #maxSize: number = 16;
|
|
15
|
+
private _isNew: boolean;
|
|
16
|
+
private _value: string;
|
|
17
|
+
#createdAt: Date;
|
|
18
|
+
|
|
19
|
+
private constructor(id?: string | number) {
|
|
20
|
+
this.#createdAt = new Date();
|
|
21
|
+
|
|
22
|
+
if (typeof id === "undefined") {
|
|
23
|
+
const uuid = UUID();
|
|
24
|
+
this._value = uuid;
|
|
25
|
+
this._isNew = true;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!validator.isString(id) && !validator.isNumber(id)) {
|
|
30
|
+
throw new TypeError("ID must be a string or number");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this._value = String(id);
|
|
34
|
+
this._isNew = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @description
|
|
39
|
+
* Creates a new `ID` instance. If no `id` value is provided, a new
|
|
40
|
+
* UUID is generated. If an `id` is provided, that value is used directly.
|
|
41
|
+
*
|
|
42
|
+
* @param id Optional value to initialize the ID with. If not provided, a new UUID is created.
|
|
43
|
+
* @returns A `UID` instance.
|
|
44
|
+
*/
|
|
45
|
+
public static create(id?: string | number): UID<string> {
|
|
46
|
+
return new ID(id) as unknown as UID<string>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @description
|
|
51
|
+
* Creates a clone of the current `UID` instance with the same value
|
|
52
|
+
* and properties, but without marking it as new.
|
|
53
|
+
*
|
|
54
|
+
* @returns A cloned `UID` instance identical to the current one.
|
|
55
|
+
*/
|
|
56
|
+
public clone(): UID<string | number> {
|
|
57
|
+
return new ID(this._value) as unknown as UID<string | number>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @description
|
|
62
|
+
* Creates a new `UID` instance cloned from the current one, but marks
|
|
63
|
+
* the new clone as a new ID.
|
|
64
|
+
*
|
|
65
|
+
* @returns A cloned `UID` instance that is considered new.
|
|
66
|
+
*/
|
|
67
|
+
public cloneAsNew(): UID<string> {
|
|
68
|
+
const newUUID = new ID(this._value);
|
|
69
|
+
newUUID.setAsNew();
|
|
70
|
+
return newUUID;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @description
|
|
75
|
+
* Retrieves the creation date of this `ID` instance.
|
|
76
|
+
*
|
|
77
|
+
* @returns The `Date` object representing when this ID was created or last modified.
|
|
78
|
+
*/
|
|
79
|
+
public createdAt(): Date {
|
|
80
|
+
return this.#createdAt;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @description
|
|
85
|
+
* Performs a deep comparison by serializing both IDs and checking if
|
|
86
|
+
* the JSON representation is identical.
|
|
87
|
+
*
|
|
88
|
+
* @param id Another `UID` to compare to.
|
|
89
|
+
* @returns `true` if both IDs are deeply equal; otherwise `false`.
|
|
90
|
+
*/
|
|
91
|
+
public deepEqual(id: UID<unknown>): boolean {
|
|
92
|
+
const A = JSON.stringify(this);
|
|
93
|
+
const B = JSON.stringify(id);
|
|
94
|
+
return A === B;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @description
|
|
99
|
+
* Compares the current ID against another `UID` instance by value.
|
|
100
|
+
*
|
|
101
|
+
* @param id Another `UID` to compare to.
|
|
102
|
+
* @returns `true` if both IDs share the same value type and string value, otherwise `false`.
|
|
103
|
+
*/
|
|
104
|
+
public equal(id: UID<unknown>): boolean {
|
|
105
|
+
return (
|
|
106
|
+
typeof this._value === typeof id?.value() &&
|
|
107
|
+
(this._value as unknown) === id?.value()
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @description
|
|
113
|
+
* Alias for `equal`. Compares the current ID against another `UID` instance by value.
|
|
114
|
+
*
|
|
115
|
+
* @param id Another `UID` to compare to.
|
|
116
|
+
* @returns `true` if both IDs share the same value, otherwise `false`.
|
|
117
|
+
*/
|
|
118
|
+
public isEqual(id: UID<unknown>): boolean {
|
|
119
|
+
return this.equal(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @description
|
|
124
|
+
* Indicates whether this `ID` instance was created as a new,
|
|
125
|
+
* previously uninitialized ID (no value passed to `create()`), or if it was
|
|
126
|
+
* initialized from a provided value.
|
|
127
|
+
*
|
|
128
|
+
* @returns `true` if the ID is new; otherwise, `false`.
|
|
129
|
+
*/
|
|
130
|
+
public isNew(): boolean {
|
|
131
|
+
return this._isNew;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @description
|
|
136
|
+
* Checks if the current ID is short (16 bytes).
|
|
137
|
+
*
|
|
138
|
+
* @returns `true` if the ID is 16 bytes long, `false` otherwise.
|
|
139
|
+
*/
|
|
140
|
+
public isShort(): boolean {
|
|
141
|
+
return this._value.length === this.#maxSize;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* @description
|
|
146
|
+
* Marks the current ID as new.
|
|
147
|
+
*/
|
|
148
|
+
private setAsNew(): void {
|
|
149
|
+
this._isNew = true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @description
|
|
154
|
+
* Creates a new short (16-byte) ID. If no `id` value is provided,
|
|
155
|
+
* a new UUID is generated and then shortened.
|
|
156
|
+
*
|
|
157
|
+
* @param id An optional string or number to use as the base value.
|
|
158
|
+
* @returns A `UID` instance with a short, 16-byte value.
|
|
159
|
+
*/
|
|
160
|
+
public static short(id?: string | number): UID<string> {
|
|
161
|
+
const _id = new ID(id);
|
|
162
|
+
if (typeof id === "undefined") _id.setAsNew();
|
|
163
|
+
_id.toShort();
|
|
164
|
+
return _id;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @description
|
|
169
|
+
* Convert the current ID value into a shortened, 16-byte version.
|
|
170
|
+
*
|
|
171
|
+
* If the current ID value is shorter than 16 bytes, a UUID is prepended to ensure
|
|
172
|
+
* sufficient length.
|
|
173
|
+
*
|
|
174
|
+
* @returns An updated `ID` instance with a short, 16-byte value.
|
|
175
|
+
*/
|
|
176
|
+
public toShort(): UID<string> {
|
|
177
|
+
let short = "";
|
|
178
|
+
let longValue = this._value;
|
|
179
|
+
|
|
180
|
+
if (longValue.length < this.#maxSize) {
|
|
181
|
+
longValue = UUID() + longValue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
longValue = longValue.toUpperCase().replace(/-/g, "");
|
|
185
|
+
const chars = longValue.split("");
|
|
186
|
+
|
|
187
|
+
while (short.length < this.#maxSize) {
|
|
188
|
+
const lastChar = chars.pop();
|
|
189
|
+
short = lastChar + short;
|
|
190
|
+
}
|
|
191
|
+
this.#createdAt = new Date();
|
|
192
|
+
this._value = short;
|
|
193
|
+
return this as unknown as UID<string>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @description
|
|
198
|
+
* Retrieves the current ID value.
|
|
199
|
+
*
|
|
200
|
+
* @returns The ID value as a string.
|
|
201
|
+
*/
|
|
202
|
+
public value(): string {
|
|
203
|
+
return this._value;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export const Id = ID.create;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Entity } from "../core/entity";
|
|
2
|
+
import type { IEntityProps } from "../types/entity.types";
|
|
3
|
+
import type { IResult } from "../types/result.types";
|
|
4
|
+
import type { UID } from "../types/uid.types";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @description
|
|
8
|
+
* Abstract base class defining the standard contract for a domain repository.
|
|
9
|
+
*
|
|
10
|
+
* A repository abstracts the persistence layer, allowing domain code to remain
|
|
11
|
+
* ignorant of storage details. Each aggregate root should have its own repository
|
|
12
|
+
* implementation.
|
|
13
|
+
*
|
|
14
|
+
* Only the methods defined here are guaranteed by the contract. Implementations
|
|
15
|
+
* may expose additional query methods specific to their aggregate.
|
|
16
|
+
*
|
|
17
|
+
* @template T The entity or aggregate type managed by this repository.
|
|
18
|
+
* @template ID The type of the entity's identifier. Defaults to `string`.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* class UserRepository extends BaseRepository<User> {
|
|
23
|
+
* async findById(id: string): Promise<IResult<User, string>> {
|
|
24
|
+
* const row = await db.users.findOne({ id });
|
|
25
|
+
* if (!row) return Result.error('User not found');
|
|
26
|
+
* return User.create(row);
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* async save(user: User): Promise<IResult<void, string>> {
|
|
30
|
+
* await db.users.upsert(user.toObject());
|
|
31
|
+
* return Result.success();
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* async delete(id: string): Promise<IResult<void, string>> {
|
|
35
|
+
* await db.users.delete({ id });
|
|
36
|
+
* return Result.success();
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* async exists(id: string): Promise<boolean> {
|
|
40
|
+
* return db.users.exists({ id });
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export abstract class BaseRepository<T extends Entity<IEntityProps>, ID = UID> {
|
|
46
|
+
/**
|
|
47
|
+
* @description
|
|
48
|
+
* Finds an entity by its unique identifier.
|
|
49
|
+
*
|
|
50
|
+
* @param id The entity's unique identifier.
|
|
51
|
+
* @returns A `Result` containing the entity if found, or an error if not.
|
|
52
|
+
*/
|
|
53
|
+
abstract findById(id: ID): Promise<IResult<T, string>>;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @description
|
|
57
|
+
* Persists an entity — either inserting or updating as appropriate.
|
|
58
|
+
*
|
|
59
|
+
* @param entity The entity instance to save.
|
|
60
|
+
* @returns A `Result` indicating success or failure.
|
|
61
|
+
*/
|
|
62
|
+
abstract save(entity: T): Promise<IResult<void, string>>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @description
|
|
66
|
+
* Removes an entity by its unique identifier.
|
|
67
|
+
*
|
|
68
|
+
* @param id The unique identifier of the entity to delete.
|
|
69
|
+
* @returns A `Result` indicating success or failure.
|
|
70
|
+
*/
|
|
71
|
+
abstract delete(id: ID): Promise<IResult<void, string>>;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @description
|
|
75
|
+
* Checks whether an entity with the given identifier exists in the store.
|
|
76
|
+
*
|
|
77
|
+
* @param id The unique identifier to check.
|
|
78
|
+
* @returns `true` if the entity exists; `false` otherwise.
|
|
79
|
+
*/
|
|
80
|
+
abstract exists(id: ID): Promise<boolean>;
|
|
81
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/** biome-ignore-all lint/suspicious/noExplicitAny: TS cannot model private constructors with generic static factories. */
|
|
2
|
+
/** biome-ignore-all lint/complexity/noThisInStatic: Required for polymorphic `this` in base factory (DDD pattern). */
|
|
3
|
+
|
|
4
|
+
import { AutoMapper } from "../helpers/auto-mapper";
|
|
5
|
+
import { DomainError } from "../helpers/domain-error";
|
|
6
|
+
import { GettersAndSetters } from "../helpers/getters-setters";
|
|
7
|
+
import { Result } from "../libs/result";
|
|
8
|
+
import type { Adapter, IAdapter } from "../types/adapter.types";
|
|
9
|
+
import type { IResult } from "../types/result.types";
|
|
10
|
+
import type { UID } from "../types/uid.types";
|
|
11
|
+
import type { AnyObject } from "../types/utils.types";
|
|
12
|
+
import type {
|
|
13
|
+
IValueObjectSettings,
|
|
14
|
+
ValueObjectConstructor,
|
|
15
|
+
} from "../types/value-object.types";
|
|
16
|
+
import { DeepFreeze, StableStringify } from "../utils/object.utils";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @description
|
|
20
|
+
* Represents a domain object characterized entirely by its properties rather than
|
|
21
|
+
* a unique identifier. Two value objects with identical properties are considered equal.
|
|
22
|
+
*
|
|
23
|
+
* Value objects are immutable by design — mutations return new instances rather than
|
|
24
|
+
* modifying the existing one. They encapsulate domain validation logic, ensuring that
|
|
25
|
+
* only valid states can be represented.
|
|
26
|
+
*
|
|
27
|
+
* Extend this class to define your own value objects with custom business rules:
|
|
28
|
+
* - Override `isValidProps()` to define what constitutes a valid state.
|
|
29
|
+
* - Override `validation()` (inherited) to enforce per-key invariants on `set().to()`.
|
|
30
|
+
* - Use `create()` as the sole public factory — keep constructors `private`.
|
|
31
|
+
* - Use `init()` when you need a throwing factory (e.g., in seeding or tests).
|
|
32
|
+
*
|
|
33
|
+
* @template Props The shape of the value object's properties.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* interface MoneyProps { amount: number }
|
|
38
|
+
*
|
|
39
|
+
* class Money extends ValueObject<MoneyProps> {
|
|
40
|
+
* private constructor(props: MoneyProps) { super(props); }
|
|
41
|
+
*
|
|
42
|
+
* public static isValidProps({ amount }: MoneyProps): boolean {
|
|
43
|
+
* return amount >= 0;
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* public static create(amount: number): IResult<Money> {
|
|
47
|
+
* if (!this.isValidProps({ amount })) return Result.error('Amount must be positive');
|
|
48
|
+
* return Result.success(new Money({ amount }));
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export abstract class ValueObject<Props> extends GettersAndSetters<Props> {
|
|
54
|
+
/**
|
|
55
|
+
* @description
|
|
56
|
+
* Marker used internally by `Validator` to identify this instance as a `ValueObject`
|
|
57
|
+
* without requiring a direct `instanceof` check (avoiding circular imports).
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
protected readonly __kind = "ValueObject" as const;
|
|
62
|
+
private readonly autoMapper: AutoMapper;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @description
|
|
66
|
+
* Initializes a new `ValueObject` instance with the provided props and optional config.
|
|
67
|
+
*
|
|
68
|
+
* @param props The properties that define this value object's state.
|
|
69
|
+
* @param config Optional settings to disable getters or setters.
|
|
70
|
+
*/
|
|
71
|
+
constructor(props: Props, config?: IValueObjectSettings) {
|
|
72
|
+
super(props, "ValueObject", { ...config, disableSetters: true });
|
|
73
|
+
this.autoMapper = new AutoMapper();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @description
|
|
78
|
+
* Creates a deep clone of this value object, optionally overriding some properties.
|
|
79
|
+
*
|
|
80
|
+
* For object-based props, a shallow merge is performed before cloning — the result
|
|
81
|
+
* is always a new instance of the same subclass, not the base `ValueObject`.
|
|
82
|
+
*
|
|
83
|
+
* @param props Optional partial properties to override in the cloned instance.
|
|
84
|
+
* @returns A new instance of the same `ValueObject` subclass with the merged props.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const discounted = price.clone({ amount: price.get('amount') * 0.9 });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
public clone(props?: Props extends object ? Partial<Props> : never): this {
|
|
92
|
+
const proto = Reflect.getPrototypeOf(this);
|
|
93
|
+
const ctor = proto?.constructor ?? this.constructor;
|
|
94
|
+
if (
|
|
95
|
+
typeof this.props === "object" &&
|
|
96
|
+
!(this.props instanceof Date) &&
|
|
97
|
+
!Array.isArray(this.props)
|
|
98
|
+
) {
|
|
99
|
+
const merged = props ? { ...this.props, ...props } : { ...this.props };
|
|
100
|
+
return Reflect.construct(ctor, [merged, this.config]);
|
|
101
|
+
}
|
|
102
|
+
return Reflect.construct(ctor, [this.props, this.config]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @description
|
|
107
|
+
* Determines structural equality between this value object and another of the same type.
|
|
108
|
+
*
|
|
109
|
+
* Equality is determined by value, not reference. The comparison strategy varies by prop type:
|
|
110
|
+
* - Strings, booleans, numbers, bigints — compared by value.
|
|
111
|
+
* - Dates — compared by timestamp.
|
|
112
|
+
* - Arrays and functions — compared by JSON serialization.
|
|
113
|
+
* - IDs — compared by `.value()`.
|
|
114
|
+
* - Symbols — compared by `.description`.
|
|
115
|
+
* - Objects — compared by JSON serialization, excluding `createdAt` and `updatedAt`.
|
|
116
|
+
*
|
|
117
|
+
* @param other The value object to compare against.
|
|
118
|
+
* @returns `true` if both value objects are structurally equal; `false` otherwise.
|
|
119
|
+
*/
|
|
120
|
+
public isEqual(other: this): boolean {
|
|
121
|
+
if (!other || other.__kind !== this.__kind) return false;
|
|
122
|
+
const props = this.props;
|
|
123
|
+
const otherProps = other?.props;
|
|
124
|
+
|
|
125
|
+
const omitTimestamps = (obj: AnyObject): string => {
|
|
126
|
+
if (!obj) return "";
|
|
127
|
+
const { createdAt: _c, updatedAt: _u, ...rest } = obj;
|
|
128
|
+
return StableStringify(rest);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
if (this.validator.isArray(props) || this.validator.isFunction(props)) {
|
|
132
|
+
return StableStringify(props) === StableStringify(otherProps);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (this.validator.isBoolean(props)) return props === otherProps;
|
|
136
|
+
|
|
137
|
+
if (this.validator.isDate(props)) {
|
|
138
|
+
return (props as Date).getTime() === (otherProps as Date)?.getTime();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.validator.isID(props)) {
|
|
142
|
+
return (props as UID).value() === (otherProps as UID)?.value();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.validator.isNull(props) || this.validator.isUndefined(props)) {
|
|
146
|
+
return props === otherProps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (this.validator.isNumber(props) || typeof props === "bigint") {
|
|
150
|
+
return this.validator
|
|
151
|
+
.number(props as number)
|
|
152
|
+
.isEqualTo(otherProps as number);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (this.validator.isString(props)) {
|
|
156
|
+
return this.validator
|
|
157
|
+
.string(props as string)
|
|
158
|
+
.isEqual(otherProps as string);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (this.validator.isSymbol(props)) {
|
|
162
|
+
return (
|
|
163
|
+
(props as symbol).description === (otherProps as symbol)?.description
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
omitTimestamps(props as AnyObject) ===
|
|
169
|
+
omitTimestamps(otherProps as AnyObject)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @description
|
|
175
|
+
* Serializes this value object into a plain, deeply frozen object.
|
|
176
|
+
*
|
|
177
|
+
* If an `adapter` is provided, the adapter's transformation is applied instead
|
|
178
|
+
* of the default `AutoMapper` serialization:
|
|
179
|
+
* - `Adapter` (with `adaptOne`) — synchronous transformation.
|
|
180
|
+
* - `IAdapter` (with `build`) — Result-wrapped transformation, value is unwrapped.
|
|
181
|
+
*
|
|
182
|
+
* The default output is a deeply frozen `unknown` — cast to your expected shape
|
|
183
|
+
* at the call site if needed.
|
|
184
|
+
*
|
|
185
|
+
* @param adapter Optional adapter to transform the output into a custom shape.
|
|
186
|
+
* @returns A deeply frozen serialized representation of this value object.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* const plain = money.toObject();
|
|
191
|
+
* // { amount: 100 }
|
|
192
|
+
*
|
|
193
|
+
* const dto = money.toObject(new MoneyDtoAdapter());
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
public toObject<T = Props>(
|
|
197
|
+
adapter?: Adapter<this, T> | IAdapter<this, T>,
|
|
198
|
+
): Readonly<T> {
|
|
199
|
+
if (
|
|
200
|
+
adapter &&
|
|
201
|
+
typeof (adapter as Adapter<this, T>).adaptOne === "function"
|
|
202
|
+
) {
|
|
203
|
+
return (adapter as Adapter<this, T>).adaptOne(this);
|
|
204
|
+
}
|
|
205
|
+
if (adapter && typeof (adapter as IAdapter<this, T>).build === "function") {
|
|
206
|
+
return (adapter as IAdapter<this, T>).build(this).value();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const mapped = this.autoMapper.valueObjectToObj(this);
|
|
210
|
+
if (typeof mapped === "object" && mapped !== null) {
|
|
211
|
+
return DeepFreeze(mapped) as Readonly<T>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return mapped as Readonly<T>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* @description
|
|
219
|
+
* Creates a new `ValueObject` instance wrapped in a `Result`.
|
|
220
|
+
*
|
|
221
|
+
* Returns `Result.error()` if `isValidProps()` returns `false`.
|
|
222
|
+
* Subclasses should override both this method and `isValidProps()` to enforce
|
|
223
|
+
* domain-specific construction rules.
|
|
224
|
+
*
|
|
225
|
+
* @param props The properties to validate and construct the value object with.
|
|
226
|
+
* @returns A `Result` containing the new instance on success, or an error on failure.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* const result = Money.create(100);
|
|
231
|
+
* if (result.isSuccess()) {
|
|
232
|
+
* const money = result.value();
|
|
233
|
+
* }
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
public static create<Props, T extends ValueObject<Props>>(
|
|
237
|
+
this: ValueObjectConstructor<Props, T>,
|
|
238
|
+
props: Props,
|
|
239
|
+
): IResult<T> {
|
|
240
|
+
if (!this.isValidProps(props)) {
|
|
241
|
+
return Result.error(
|
|
242
|
+
`Invalid props for ${this.name}: failed domain validation.`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const instance = new (this as any)(props);
|
|
247
|
+
return Result.success(instance);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @description
|
|
252
|
+
* Initializes a new `ValueObject` instance directly, throwing a `DomainError` if
|
|
253
|
+
* the provided props are invalid.
|
|
254
|
+
*
|
|
255
|
+
* Prefer `create()` for production code. Use `init()` in tests, seeders, or contexts
|
|
256
|
+
* where you can guarantee the props are valid and prefer exceptions over `Result`.
|
|
257
|
+
*
|
|
258
|
+
* @param props The properties to validate and construct the value object with.
|
|
259
|
+
* @returns A new instance of this value object subclass.
|
|
260
|
+
* @throws {DomainError} If `isValidProps()` returns `false`.
|
|
261
|
+
*/
|
|
262
|
+
public static init<Props, T extends ValueObject<Props>>(
|
|
263
|
+
this: ValueObjectConstructor<Props, T>,
|
|
264
|
+
props: Props,
|
|
265
|
+
): T {
|
|
266
|
+
if (!this.isValidProps(props)) {
|
|
267
|
+
throw new DomainError(`Init: invalid props.`, { context: this.name });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const instance = new (this as any)(props);
|
|
271
|
+
return instance;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @description
|
|
276
|
+
* Alias for `isValidProps()`. Provided for consistency with the `Entity` API.
|
|
277
|
+
* Subclasses should override `isValidProps()` rather than this method.
|
|
278
|
+
*
|
|
279
|
+
* @param value The value to validate.
|
|
280
|
+
* @returns `true` if valid; `false` otherwise.
|
|
281
|
+
*/
|
|
282
|
+
public static isValid(value: unknown): boolean {
|
|
283
|
+
return this.isValidProps(value);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @description
|
|
288
|
+
* Validates the provided props before constructing a new instance.
|
|
289
|
+
*
|
|
290
|
+
* The base implementation accepts anything that is not `null` or `undefined`.
|
|
291
|
+
* Override this in your subclass to enforce domain-specific rules.
|
|
292
|
+
*
|
|
293
|
+
* @param props The props to validate.
|
|
294
|
+
* @returns `true` if the props represent a valid state; `false` otherwise.
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* public static isValidProps({ amount }: MoneyProps): boolean {
|
|
299
|
+
* return amount >= 0;
|
|
300
|
+
* }
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
public static isValidProps(props: unknown): boolean {
|
|
304
|
+
return (
|
|
305
|
+
!ValueObject.validator.isUndefined(props) &&
|
|
306
|
+
!ValueObject.validator.isNull(props)
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|