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,22 @@
|
|
|
1
|
+
import { Aggregate } from "{{importAlias}}";
|
|
2
|
+
|
|
3
|
+
interface {{className}}Props {
|
|
4
|
+
// define props
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class {{className}} extends Aggregate<{{className}}Props> {
|
|
8
|
+
private constructor(props: {{className}}Props) {
|
|
9
|
+
super(props);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// example behavior
|
|
13
|
+
public doSomething(): void {
|
|
14
|
+
// this.change("field", value);
|
|
15
|
+
// this.emit({ type: "{{lowerName}}:event", payload: {} });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public static override isValidProps(props: {{className}}Props): boolean {
|
|
19
|
+
// use {{className}}.validator helpers if needed
|
|
20
|
+
throw new Error("Method not implemented")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Entity } from "{{importAlias}}";
|
|
2
|
+
|
|
3
|
+
interface {{className}}Props {
|
|
4
|
+
// define props
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class {{className}} extends Entity<{{className}}Props> {
|
|
8
|
+
private constructor(props: {{className}}Props) {
|
|
9
|
+
super(props);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public static override isValidProps(props: {{className}}Props): boolean {
|
|
13
|
+
// use {{className}}.validator helpers if needed
|
|
14
|
+
throw new Error("Method not implemented")
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BaseRepository } from "{{importAlias}}";
|
|
2
|
+
// import { {{className}} } from "./path-to-your-{{lowerName}}";
|
|
3
|
+
|
|
4
|
+
export class {{className}}Repository extends BaseRepository<{{className}}> {
|
|
5
|
+
async findById(id: string): Promise<IResult<{{className}}, string>> {
|
|
6
|
+
// implement fetch logic
|
|
7
|
+
throw new Error("Method not implemented")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async save(entity: {{className}}): Promise<IResult<void, string>> {
|
|
11
|
+
// implement persistence
|
|
12
|
+
throw new Error("Method not implemented")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async delete(id: string): Promise<IResult<void, string>> {
|
|
16
|
+
// implement delete
|
|
17
|
+
throw new Error("Method not implemented")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async exists(id: string): Promise<boolean> {
|
|
21
|
+
// implement existence check
|
|
22
|
+
throw new Error("Method not implemented")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IUseCase, IResult } from "{{importAlias}}";
|
|
2
|
+
|
|
3
|
+
interface {{className}}Input {
|
|
4
|
+
// define input
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface {{className}}Output {
|
|
8
|
+
// define output
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface {{className}}Metadata {
|
|
12
|
+
// define output or assign void to the generic instead
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class {{className}} implements IUseCase<{{className}}Input, {{className}}Output> {
|
|
16
|
+
async execute(input: {{className}}Input): Promise<IResult<{{className}}Output, {{className}}Metadata>> {
|
|
17
|
+
// implement use case
|
|
18
|
+
throw new Error("Method not implemented")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ValueObject } from "{{importAlias}}";
|
|
2
|
+
|
|
3
|
+
interface {{className}}Props {
|
|
4
|
+
// define props
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class {{className}} extends ValueObject<{{className}}Props> {
|
|
8
|
+
private constructor(props: {{className}}Props) {
|
|
9
|
+
super(props);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public static override isValidProps(props: {{className}}Props): boolean {
|
|
13
|
+
// use {{className}}.validator helpers if needed
|
|
14
|
+
throw new Error("Method not implemented")
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { IEntityProps, IEntitySettings } from "../types/entity.types";
|
|
2
|
+
import type { DomainEvent } from "../types/event.types";
|
|
3
|
+
import type { UID } from "../types/uid.types";
|
|
4
|
+
import { Entity } from "./entity";
|
|
5
|
+
import { ID } from "./id";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @description
|
|
9
|
+
* Represents an aggregate root — the consistency boundary of a domain model.
|
|
10
|
+
*
|
|
11
|
+
* An `Aggregate` extends `Entity` with domain event collection. It is the
|
|
12
|
+
* single entry point for mutations within its boundary: domain methods mutate
|
|
13
|
+
* state and record **what happened** as `DomainEvent` objects via `emit()`.
|
|
14
|
+
* Events are plain data — they carry no handlers and no knowledge of how they
|
|
15
|
+
* will be consumed.
|
|
16
|
+
*
|
|
17
|
+
* After a successful `repository.save()`, the application layer drains the
|
|
18
|
+
* event queue with `pullEvents()` and hands the events off to whatever
|
|
19
|
+
* transport it uses (`EventBus`, Redis Streams, BullMQ, etc.).
|
|
20
|
+
*
|
|
21
|
+
* **Lifecycle**
|
|
22
|
+
* ```
|
|
23
|
+
* [Domain method] → emit(event) records the fact
|
|
24
|
+
* [App layer] → repo.save(aggregate) persists state
|
|
25
|
+
* [App layer] → pullEvents() drains the queue
|
|
26
|
+
* [App layer] → bus.publishAll(...) delivers the facts
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @template Props The shape of the aggregate's user-defined domain properties.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* interface OrderProps { customerId: string; status: string; total: number }
|
|
34
|
+
*
|
|
35
|
+
* class Order extends Aggregate<OrderProps> {
|
|
36
|
+
* private constructor(props: EntityProps<OrderProps>) { super(props); }
|
|
37
|
+
*
|
|
38
|
+
* public place(): void {
|
|
39
|
+
* this.change('status', 'placed');
|
|
40
|
+
* this.emit({
|
|
41
|
+
* type: 'order:placed',
|
|
42
|
+
* payload: { total: this.get('total') },
|
|
43
|
+
* });
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* public static override isValidProps(props: unknown): boolean {
|
|
47
|
+
* return (
|
|
48
|
+
* typeof (props as OrderProps)?.customerId === 'string' &&
|
|
49
|
+
* typeof (props as OrderProps)?.total === 'number'
|
|
50
|
+
* );
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* // Application layer
|
|
55
|
+
* const order = Order.init({ customerId: 'c-1', status: 'pending', total: 100 });
|
|
56
|
+
* order.place();
|
|
57
|
+
*
|
|
58
|
+
* await orderRepository.save(order); // persist first
|
|
59
|
+
* await eventBus.publishAll(order.pullEvents()); // then publish
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export abstract class Aggregate<
|
|
63
|
+
Props extends IEntityProps,
|
|
64
|
+
> extends Entity<Props> {
|
|
65
|
+
/**
|
|
66
|
+
* @description
|
|
67
|
+
* Marker used by `Validator.isAggregate()` to distinguish aggregates from
|
|
68
|
+
* plain entities without requiring an `instanceof` check.
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
protected readonly __aggregate = true as const;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @description Pending domain events waiting to be pulled and published.
|
|
75
|
+
*/
|
|
76
|
+
private _domainEvents: DomainEvent[] = [];
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @description
|
|
80
|
+
* Initialises a new `Aggregate` instance.
|
|
81
|
+
*
|
|
82
|
+
* @param props The domain properties for this aggregate.
|
|
83
|
+
* @param config Optional settings to disable getters or setters.
|
|
84
|
+
*/
|
|
85
|
+
protected constructor(props: Props, config?: IEntitySettings) {
|
|
86
|
+
super(props, config);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @description
|
|
91
|
+
* Records a domain event on this aggregate.
|
|
92
|
+
*
|
|
93
|
+
* Call `emit()` inside domain methods after a successful state change to
|
|
94
|
+
* capture the fact as a `DomainEvent`. The `aggregateId`, `aggregateName`,
|
|
95
|
+
* and `occurredAt` fields are filled in automatically — only `type` and
|
|
96
|
+
* `payload` are required from the caller.
|
|
97
|
+
*
|
|
98
|
+
* Events are held in memory until `pullEvents()` is called.
|
|
99
|
+
*
|
|
100
|
+
* @param event A partial `DomainEvent` — `type` is required, `payload` is optional.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* // Inline (most common)
|
|
105
|
+
* this.emit({ type: 'order:placed', payload: { total: 100 } });
|
|
106
|
+
*
|
|
107
|
+
* // Via BaseDomainEvent subclass (OOP style, optional)
|
|
108
|
+
* this.emit(new OrderPlacedEvent(this.id.value(), { total: 100 }));
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
protected emit<TPayload = unknown>(
|
|
112
|
+
event:
|
|
113
|
+
| Pick<DomainEvent<TPayload>, "type" | "payload">
|
|
114
|
+
| DomainEvent<TPayload>,
|
|
115
|
+
): void {
|
|
116
|
+
const aggregateName =
|
|
117
|
+
Reflect.getPrototypeOf(this)?.constructor?.name ?? "Aggregate";
|
|
118
|
+
|
|
119
|
+
const full: DomainEvent<TPayload> = Object.freeze({
|
|
120
|
+
...(event as DomainEvent<TPayload>),
|
|
121
|
+
aggregateId: this.id.value(),
|
|
122
|
+
aggregateName,
|
|
123
|
+
occurredAt: new Date(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this._domainEvents.push(full as DomainEvent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @description
|
|
131
|
+
* Returns all pending domain events and clears the internal queue.
|
|
132
|
+
*
|
|
133
|
+
* **Always call this after `repository.save()` succeeds**, never before —
|
|
134
|
+
* pulling before persist risks delivering events for a state that was never
|
|
135
|
+
* committed (phantom events).
|
|
136
|
+
*
|
|
137
|
+
* The returned array is frozen. If you need to inspect events without
|
|
138
|
+
* consuming them, use `peekEvents()` instead.
|
|
139
|
+
*
|
|
140
|
+
* @returns A frozen, ordered snapshot of all pending `DomainEvent` objects.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* // Application layer (use case / command handler)
|
|
145
|
+
* await repo.save(order);
|
|
146
|
+
* await bus.publishAll(order.pullEvents());
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
public pullEvents(): ReadonlyArray<DomainEvent> {
|
|
150
|
+
const snapshot = Object.freeze([...this._domainEvents]);
|
|
151
|
+
this._domainEvents = [];
|
|
152
|
+
return snapshot;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @description
|
|
157
|
+
* Returns a snapshot of pending events **without** clearing the queue.
|
|
158
|
+
*
|
|
159
|
+
* Useful for logging, testing assertions, or conditional logic that needs
|
|
160
|
+
* to inspect events before deciding whether to publish them.
|
|
161
|
+
*
|
|
162
|
+
* @returns A frozen, ordered snapshot of the current pending events.
|
|
163
|
+
*/
|
|
164
|
+
public peekEvents(): ReadonlyArray<DomainEvent> {
|
|
165
|
+
return Object.freeze([...this._domainEvents]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @description
|
|
170
|
+
* Returns the number of domain events currently pending in the queue.
|
|
171
|
+
*/
|
|
172
|
+
public get eventCount(): number {
|
|
173
|
+
return this._domainEvents.length;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @description
|
|
178
|
+
* Discards all pending domain events without publishing them.
|
|
179
|
+
*
|
|
180
|
+
* Use sparingly — in most cases events should be published, not discarded.
|
|
181
|
+
* Useful in tests or when an operation is rolled back before persist.
|
|
182
|
+
*
|
|
183
|
+
* @returns The number of events cleared.
|
|
184
|
+
*/
|
|
185
|
+
public clearEvents(): number {
|
|
186
|
+
const count = this._domainEvents.length;
|
|
187
|
+
this._domainEvents = [];
|
|
188
|
+
return count;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @description
|
|
193
|
+
* Creates a deep clone of this aggregate, optionally overriding some properties.
|
|
194
|
+
*
|
|
195
|
+
* Pending domain events are **not** copied to the clone by default — events
|
|
196
|
+
* belong to the instance that produced them. Pass `{ withEvents: true }` to
|
|
197
|
+
* carry them over (e.g. when re-trying a failed publish).
|
|
198
|
+
*
|
|
199
|
+
* @param props Optional partial properties and clone options.
|
|
200
|
+
* @returns A new instance of the same `Aggregate` subclass.
|
|
201
|
+
*/
|
|
202
|
+
public override clone(
|
|
203
|
+
props?: Partial<Props> & { withEvents?: boolean },
|
|
204
|
+
): this {
|
|
205
|
+
const proto = Reflect.getPrototypeOf(this);
|
|
206
|
+
const ctor = proto?.constructor ?? this.constructor;
|
|
207
|
+
|
|
208
|
+
const { withEvents, ...domainProps } = (props ?? {}) as Partial<Props> & {
|
|
209
|
+
withEvents?: boolean;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const merged = { ...this.props, ...domainProps };
|
|
213
|
+
const clone = Reflect.construct(ctor, [merged, this.config]) as this;
|
|
214
|
+
|
|
215
|
+
if (withEvents) {
|
|
216
|
+
clone._domainEvents = [...this._domainEvents];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return clone;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @description
|
|
224
|
+
* Generates a hash code for this aggregate combining its class name and ID.
|
|
225
|
+
*
|
|
226
|
+
* Format: `[Aggregate@ClassName]:UUID`
|
|
227
|
+
*
|
|
228
|
+
* @returns A `UID<string>` representing this aggregate's hash code.
|
|
229
|
+
*/
|
|
230
|
+
public override hashCode(): UID<string> {
|
|
231
|
+
const name = Reflect.getPrototypeOf(this)?.constructor?.name ?? "Aggregate";
|
|
232
|
+
return ID.create(`[Aggregate@${name}]:${this.id.value()}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
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 {
|
|
10
|
+
EntityConstructor,
|
|
11
|
+
EntityProps,
|
|
12
|
+
IEntityProps,
|
|
13
|
+
IEntitySettings,
|
|
14
|
+
} from "../types/entity.types";
|
|
15
|
+
import type { IResult } from "../types/result.types";
|
|
16
|
+
import type { UID } from "../types/uid.types";
|
|
17
|
+
import type { AnyObject } from "../types/utils.types";
|
|
18
|
+
import { DeepFreeze, StableStringify } from "../utils/object.utils";
|
|
19
|
+
import { InvalidPropsType } from "../utils/type.utils";
|
|
20
|
+
import { ID } from "./id";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @description
|
|
24
|
+
* Represents a domain entity — a domain object with a stable unique identity.
|
|
25
|
+
*
|
|
26
|
+
* Unlike value objects, two entities are not equal simply because their properties match.
|
|
27
|
+
* Identity is determined by the `id` field. Two entities are equal only when they share
|
|
28
|
+
* the same `id` AND belong to the same class AND share the same property values
|
|
29
|
+
* (excluding lifecycle timestamps).
|
|
30
|
+
*
|
|
31
|
+
* Entities automatically track `createdAt` and `updatedAt` timestamps. Mutations via
|
|
32
|
+
* `set().to()` or `change()` automatically refresh `updatedAt`.
|
|
33
|
+
*
|
|
34
|
+
* Extend this class to define your own entities with custom business rules:
|
|
35
|
+
* - Override `isValidProps()` to define construction constraints.
|
|
36
|
+
* - Override `validation()` (inherited) to enforce per-key invariants.
|
|
37
|
+
* - Use `create()` as the sole public factory — keep constructors `private`.
|
|
38
|
+
* - Use `init()` when you need a throwing factory (e.g., in tests or seeders).
|
|
39
|
+
*
|
|
40
|
+
* @template Props The shape of the entity's user-defined domain properties.
|
|
41
|
+
* Do **not** include `id`, `createdAt`, or `updatedAt` here — they are added
|
|
42
|
+
* automatically via `EntityProps<Props>` in the factory methods.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* interface UserProps { name: string; email: string; }
|
|
47
|
+
*
|
|
48
|
+
* class User extends Entity<UserProps> {
|
|
49
|
+
* private constructor(props: EntityProps<UserProps>) { super(props); }
|
|
50
|
+
*
|
|
51
|
+
* public static override isValidProps(props: unknown): boolean {
|
|
52
|
+
* return (
|
|
53
|
+
* this.validator.isObject(props) &&
|
|
54
|
+
* 'name' in props && this.validator.isString(props.name) &&
|
|
55
|
+
* 'email' in props && this.validator.isString(props.email)
|
|
56
|
+
* );
|
|
57
|
+
* }
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* // Autocomplete: name, email, id?, createdAt?, updatedAt?
|
|
61
|
+
* const result = User.create({ name: 'Alice', email: 'alice@example.com' });
|
|
62
|
+
* // get() autocomplete: 'name' | 'email' | 'id' | 'createdAt' | 'updatedAt'
|
|
63
|
+
* result.value().get('name');
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export abstract class Entity<
|
|
67
|
+
Props extends IEntityProps,
|
|
68
|
+
> extends GettersAndSetters<EntityProps<Props>> {
|
|
69
|
+
/**
|
|
70
|
+
* @description
|
|
71
|
+
* Marker used internally by `Validator` to identify this instance as an `Entity`
|
|
72
|
+
* without requiring a direct `instanceof` check (avoiding circular imports).
|
|
73
|
+
*
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
protected readonly __kind = "Entity" as const;
|
|
77
|
+
private readonly autoMapper: AutoMapper;
|
|
78
|
+
private _id: UID<string>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @description
|
|
82
|
+
* Initializes a new `Entity` instance.
|
|
83
|
+
*
|
|
84
|
+
* Props are merged with default `createdAt` and `updatedAt` timestamps.
|
|
85
|
+
* The `id` field is resolved from props if present (as string, number, or UID),
|
|
86
|
+
* or a new UUID is generated automatically.
|
|
87
|
+
*
|
|
88
|
+
* @param props The domain properties for this entity, including optional implicit fields.
|
|
89
|
+
* @param config Optional settings to disable getters or setters.
|
|
90
|
+
*
|
|
91
|
+
* @throws {DomainError} If `props` is not a plain object.
|
|
92
|
+
*/
|
|
93
|
+
constructor(props: Props, config?: IEntitySettings) {
|
|
94
|
+
if (!Entity.isPlainProps(props)) {
|
|
95
|
+
const name = new.target.name;
|
|
96
|
+
throw new DomainError(
|
|
97
|
+
`Construct: props must be a plain object (received: ${InvalidPropsType(props)}).`,
|
|
98
|
+
{ context: name },
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const merged = Object.assign(
|
|
103
|
+
{},
|
|
104
|
+
{ createdAt: new Date(), updatedAt: new Date() },
|
|
105
|
+
props,
|
|
106
|
+
) as Props;
|
|
107
|
+
|
|
108
|
+
super(merged, "Entity", config);
|
|
109
|
+
this.autoMapper = new AutoMapper();
|
|
110
|
+
|
|
111
|
+
const rawId = (props as AnyObject).id;
|
|
112
|
+
const isUID = this.validator.isID(rawId);
|
|
113
|
+
const isStringOrNumber =
|
|
114
|
+
this.validator.isString(rawId) || this.validator.isNumber(rawId);
|
|
115
|
+
|
|
116
|
+
this._id = isStringOrNumber
|
|
117
|
+
? ID.create(rawId as string | number)
|
|
118
|
+
: isUID
|
|
119
|
+
? (rawId as UID<string>)
|
|
120
|
+
: ID.create();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @description
|
|
125
|
+
* Creates a deep clone of this entity, optionally overriding some properties.
|
|
126
|
+
*
|
|
127
|
+
* If `props` contains an `id`, it is used as the clone's identity.
|
|
128
|
+
* Otherwise, the clone inherits the original entity's `id`.
|
|
129
|
+
*
|
|
130
|
+
* @param props Optional partial properties to override in the cloned instance.
|
|
131
|
+
* @returns A new instance of the same `Entity` subclass.
|
|
132
|
+
*/
|
|
133
|
+
public clone(props?: Partial<Props>): this {
|
|
134
|
+
const proto = Reflect.getPrototypeOf(this);
|
|
135
|
+
const ctor = proto?.constructor ?? this.constructor;
|
|
136
|
+
const merged = props ? { ...this.props, ...props } : { ...this.props };
|
|
137
|
+
return Reflect.construct(ctor, [merged, this.config]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @description
|
|
142
|
+
* Generates a hash code identifier for this entity combining its class name and ID.
|
|
143
|
+
* Useful for logging, caching, or deduplication.
|
|
144
|
+
*
|
|
145
|
+
* Format: `[Entity@ClassName]:UUID`
|
|
146
|
+
*
|
|
147
|
+
* @returns A `UID<string>` representing this entity's hash code.
|
|
148
|
+
*/
|
|
149
|
+
public hashCode(): UID<string> {
|
|
150
|
+
const proto = Reflect.getPrototypeOf(this);
|
|
151
|
+
const name = proto?.constructor?.name ?? "Entity";
|
|
152
|
+
return ID.create(`[Entity@${name}]:${this._id.value()}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @description
|
|
157
|
+
* The unique identifier of this entity.
|
|
158
|
+
*/
|
|
159
|
+
public get id(): UID<string> {
|
|
160
|
+
return this._id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @description
|
|
165
|
+
* Determines structural and identity equality between this entity and another.
|
|
166
|
+
*
|
|
167
|
+
* Two entities are considered equal when **all three** conditions hold:
|
|
168
|
+
* 1. They are instances of the same concrete class (same `constructor`).
|
|
169
|
+
* 2. Their `id` values are equal.
|
|
170
|
+
* 3. Their domain properties (excluding `id`, `createdAt`, `updatedAt`) are deeply equal.
|
|
171
|
+
*
|
|
172
|
+
* **Why class-identity check matters (critical DDD rule):**
|
|
173
|
+
* Without the constructor guard, a `User` entity and an `Admin` entity that happen
|
|
174
|
+
* to share the same `id` (e.g. when restored from the same DB row) would be
|
|
175
|
+
* considered equal — a silent, hard-to-trace bug. In DDD, entity identity is
|
|
176
|
+
* scoped to its aggregate boundary and class type.
|
|
177
|
+
*
|
|
178
|
+
* @param other The entity to compare against.
|
|
179
|
+
* @returns `true` if both entities are fully equal; `false` otherwise.
|
|
180
|
+
*/
|
|
181
|
+
public isEqual(other: this): boolean {
|
|
182
|
+
if (
|
|
183
|
+
!other ||
|
|
184
|
+
Reflect.getPrototypeOf(this)?.constructor !==
|
|
185
|
+
Reflect.getPrototypeOf(other)?.constructor
|
|
186
|
+
) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const omit = (obj: AnyObject) => {
|
|
191
|
+
const { id: _i, createdAt: _c, updatedAt: _u, ...rest } = obj;
|
|
192
|
+
return rest;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const currentProps = omit(this.props as AnyObject);
|
|
196
|
+
const otherProps = omit((other?.props ?? {}) as AnyObject);
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
this._id.isEqual(other?.id) &&
|
|
200
|
+
StableStringify(currentProps) === StableStringify(otherProps)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @description
|
|
206
|
+
* Returns `true` if this entity's ID was freshly generated (not restored from persistence).
|
|
207
|
+
*/
|
|
208
|
+
public isNew(): boolean {
|
|
209
|
+
return this._id.isNew();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @description
|
|
214
|
+
* Serializes this entity into a plain, deeply frozen object.
|
|
215
|
+
*
|
|
216
|
+
* If an `adapter` is provided, the adapter's transformation is applied instead
|
|
217
|
+
* of the default `AutoMapper` serialization.
|
|
218
|
+
*
|
|
219
|
+
* @param adapter Optional adapter to transform the output into a custom shape.
|
|
220
|
+
* @returns A deeply frozen serialized representation of this entity.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* const plain = user.toObject();
|
|
225
|
+
* // { id: '...', name: 'Alice', createdAt: Date, updatedAt: Date }
|
|
226
|
+
*
|
|
227
|
+
* const dto = user.toObject(new UserDtoAdapter());
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
public toObject<T>(
|
|
231
|
+
adapter?: Adapter<this, T> | IAdapter<this, T>,
|
|
232
|
+
): Readonly<T> {
|
|
233
|
+
if (
|
|
234
|
+
adapter &&
|
|
235
|
+
typeof (adapter as Adapter<this, T>).adaptOne === "function"
|
|
236
|
+
) {
|
|
237
|
+
return (adapter as Adapter<this, T>).adaptOne(this);
|
|
238
|
+
}
|
|
239
|
+
if (adapter && typeof (adapter as IAdapter<this, T>).build === "function") {
|
|
240
|
+
return (adapter as IAdapter<this, T>).build(this).value();
|
|
241
|
+
}
|
|
242
|
+
return DeepFreeze(
|
|
243
|
+
this.autoMapper.entityToObj(this) as object,
|
|
244
|
+
) as Readonly<T>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @description
|
|
249
|
+
* Creates a new `Entity` instance wrapped in a `Result`.
|
|
250
|
+
*
|
|
251
|
+
* The `this: EntityConstructor<Props, T>` typing mirrors the pattern used by
|
|
252
|
+
* `ValueObject.create()`, ensuring that when called on a concrete subclass,
|
|
253
|
+
* TypeScript infers the correct `Props` and instance type `T` automatically —
|
|
254
|
+
* so subclasses do **not** need to override `create()` just for types.
|
|
255
|
+
*
|
|
256
|
+
* Returns `Result.error()` if `isValidProps()` returns `false`.
|
|
257
|
+
*
|
|
258
|
+
* @param props The properties to validate and construct the entity with.
|
|
259
|
+
* Includes both user-defined domain props and optional implicit fields
|
|
260
|
+
* (`id`, `createdAt`, `updatedAt`).
|
|
261
|
+
* @returns A `Result` containing the new instance on success, or an error on failure.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* // No override needed — types are inferred from EntityConstructor<UserProps, User>
|
|
266
|
+
* const result = User.create({ name: 'Alice', email: 'alice@example.com' });
|
|
267
|
+
* // ^^ autocomplete works here
|
|
268
|
+
* if (result.isSuccess()) {
|
|
269
|
+
* result.value().get('name'); // 'Alice' — autocomplete works here too
|
|
270
|
+
* }
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
public static create<Props extends object, T extends Entity<Props>>(
|
|
274
|
+
this: EntityConstructor<Props, T>,
|
|
275
|
+
props: EntityProps<Props>,
|
|
276
|
+
): IResult<T, string> {
|
|
277
|
+
if (!this.isValidProps(props)) {
|
|
278
|
+
return Result.error(
|
|
279
|
+
`Invalid props for ${this.name}: failed domain validation.`,
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const instance = new (this as any)(props);
|
|
284
|
+
return Result.success(instance);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @description
|
|
289
|
+
* Initializes a new `Entity` instance directly, throwing a `DomainError` if
|
|
290
|
+
* the provided props are invalid.
|
|
291
|
+
*
|
|
292
|
+
* Prefer `create()` for production code. Use `init()` in tests, seeders, or contexts
|
|
293
|
+
* where you can guarantee the props are valid and prefer exceptions over `Result`.
|
|
294
|
+
*
|
|
295
|
+
* Subclasses do **not** need to override this — types are inferred from
|
|
296
|
+
* `EntityConstructor<Props, T>` the same way as `create()`.
|
|
297
|
+
*
|
|
298
|
+
* @param props The properties to validate and construct the entity with.
|
|
299
|
+
* @returns A new instance of this entity subclass.
|
|
300
|
+
* @throws {DomainError} If `isValidProps()` returns `false`.
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```typescript
|
|
304
|
+
* // In tests or seeders where you want to throw on invalid data:
|
|
305
|
+
* const user = User.init({ name: 'Alice', email: 'alice@example.com' });
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
public static init<Props extends object, T extends Entity<Props>>(
|
|
309
|
+
this: EntityConstructor<Props, T>,
|
|
310
|
+
props: EntityProps<Props>,
|
|
311
|
+
): T {
|
|
312
|
+
if (!this.isValidProps(props)) {
|
|
313
|
+
throw new DomainError(`Init: invalid props.`, { context: this.name });
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const instance = new (this as any)(props);
|
|
317
|
+
return instance;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @description Alias for `isValidProps()`.
|
|
322
|
+
*/
|
|
323
|
+
public static isValid(value: unknown): boolean {
|
|
324
|
+
return this.isValidProps(value);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* @description
|
|
329
|
+
* Validates the provided props before constructing a new instance.
|
|
330
|
+
* Base implementation rejects `null` and `undefined`.
|
|
331
|
+
* Override in subclasses to enforce domain-specific rules.
|
|
332
|
+
*
|
|
333
|
+
* @param props The props to validate.
|
|
334
|
+
* @returns `true` if valid; `false` otherwise.
|
|
335
|
+
*/
|
|
336
|
+
public static isValidProps(props: unknown): boolean {
|
|
337
|
+
return (
|
|
338
|
+
!Entity.validator.isUndefined(props) && !Entity.validator.isNull(props)
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private static isPlainProps(props: unknown): props is AnyObject {
|
|
343
|
+
if (props === null || typeof props !== "object") return false;
|
|
344
|
+
if (props instanceof Date || Array.isArray(props)) return false;
|
|
345
|
+
const proto = Object.getPrototypeOf(props);
|
|
346
|
+
return proto === Object.prototype || proto === null;
|
|
347
|
+
}
|
|
348
|
+
}
|