@vytches/ddd-aggregates 0.26.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 +21 -0
- package/LLMGUIDE.md +185 -0
- package/README.md +1393 -0
- package/dist/aggregate-errors.d.ts +58 -0
- package/dist/aggregate-interfaces.d.ts +398 -0
- package/dist/capabilities/audit-capability.d.ts +95 -0
- package/dist/capabilities/event-sourcing-capability.d.ts +79 -0
- package/dist/capabilities/index.d.ts +4 -0
- package/dist/capabilities/snapshot-capability.d.ts +113 -0
- package/dist/capabilities/versioning-capability.d.ts +93 -0
- package/dist/core/aggregate-root.builder.d.ts +110 -0
- package/dist/core/aggregate-root.d.ts +201 -0
- package/dist/core/aggregate-utilities.d.ts +233 -0
- package/dist/core/entity.d.ts +76 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1283 -0
- package/package.json +69 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { IAggregateSnapshot, ISnapshotCapability, Capability } from '../../../contracts/src/index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Capability that enables snapshot creation and restoration on an aggregate
|
|
4
|
+
* — a performance optimization for event-sourced aggregates with long
|
|
5
|
+
* event histories. Instead of replaying all events to reconstitute state,
|
|
6
|
+
* load a recent snapshot and replay only events after it.
|
|
7
|
+
*
|
|
8
|
+
* Attach via {@link AggregateBuilder} (preferred) or directly with
|
|
9
|
+
* `aggregate.addCapability(new SnapshotCapability())`. The capability
|
|
10
|
+
* stores its state inside the aggregate's `CapabilityRegistry` — there is
|
|
11
|
+
* no separate persistence; *you* serialize/deserialize the aggregate's
|
|
12
|
+
* fields via callbacks.
|
|
13
|
+
*
|
|
14
|
+
* Two operations:
|
|
15
|
+
*
|
|
16
|
+
* - {@link createSnapshot} — produce an `IAggregateSnapshot<TState, TMeta>`
|
|
17
|
+
* capturing version, type, id, and a state slice you choose. Hand off to
|
|
18
|
+
* your snapshot store (database, blob, file).
|
|
19
|
+
* - {@link restoreFromSnapshot} — load a snapshot, validate id/type match,
|
|
20
|
+
* and restore the aggregate's version + reset uncommitted events. The
|
|
21
|
+
* `deserializer` callback is responsible for putting state back into
|
|
22
|
+
* the aggregate's private fields.
|
|
23
|
+
*
|
|
24
|
+
* @example Creating a snapshot from inside the aggregate
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import {
|
|
27
|
+
* AggregateRoot,
|
|
28
|
+
* AggregateBuilder,
|
|
29
|
+
* SnapshotCapability,
|
|
30
|
+
* } from '@vytches/ddd-aggregates';
|
|
31
|
+
*
|
|
32
|
+
* interface OrderState { amount: number; customerId: string; }
|
|
33
|
+
*
|
|
34
|
+
* class Order extends AggregateRoot<string> {
|
|
35
|
+
* private amount = 0;
|
|
36
|
+
* private customerId = '';
|
|
37
|
+
*
|
|
38
|
+
* takeSnapshot(): IAggregateSnapshot<OrderState> {
|
|
39
|
+
* const cap = this.getCapability(SnapshotCapability)!;
|
|
40
|
+
* return cap.createSnapshot<OrderState>(() => ({
|
|
41
|
+
* amount: this.amount,
|
|
42
|
+
* customerId: this.customerId,
|
|
43
|
+
* }));
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* restore(snap: IAggregateSnapshot<OrderState>): void {
|
|
47
|
+
* const cap = this.getCapability(SnapshotCapability)!;
|
|
48
|
+
* cap.restoreFromSnapshot(snap, state => {
|
|
49
|
+
* this.amount = state.amount;
|
|
50
|
+
* this.customerId = state.customerId;
|
|
51
|
+
* });
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
*
|
|
55
|
+
* const order = AggregateBuilder
|
|
56
|
+
* .create({ id: EntityId.create() })
|
|
57
|
+
* .withSnapshots()
|
|
58
|
+
* .build(Order);
|
|
59
|
+
*
|
|
60
|
+
* const snap = order.takeSnapshot();
|
|
61
|
+
* await snapshotStore.save(snap);
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* `restoreFromSnapshot` calls the aggregate's `_internal_setState` —
|
|
66
|
+
* this is intentional and required, but means snapshots must be the
|
|
67
|
+
* very first restoration step before any subsequent event replay.
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
* @stable
|
|
71
|
+
* @since 0.1.0
|
|
72
|
+
*/
|
|
73
|
+
export declare class SnapshotCapability<TState = unknown, TMeta = unknown> extends Capability<'snapshot'> implements ISnapshotCapability<TState, TMeta> {
|
|
74
|
+
readonly type: "snapshot";
|
|
75
|
+
static get capabilityType(): string;
|
|
76
|
+
private aggregate;
|
|
77
|
+
private _snapshot;
|
|
78
|
+
/**
|
|
79
|
+
* @param {unknown} aggregate - Aggregate to attach this capability to
|
|
80
|
+
*/
|
|
81
|
+
attach(aggregate: unknown): void;
|
|
82
|
+
detach?(): void;
|
|
83
|
+
/**
|
|
84
|
+
* @param {() => TState} serializer - Function to serialize aggregate state
|
|
85
|
+
* @param {() => TMeta} metadataCreator - Optional function to create snapshot metadata
|
|
86
|
+
* @returns {IAggregateSnapshot<TState, TMeta>} Created snapshot with state and metadata
|
|
87
|
+
*/
|
|
88
|
+
createSnapshot(serializer: () => TState, metadataCreator?: () => TMeta): IAggregateSnapshot<TState, TMeta>;
|
|
89
|
+
/**
|
|
90
|
+
* @param {IAggregateSnapshot<TState, TMeta>} snapshot - Snapshot to restore from
|
|
91
|
+
* @param {(state: TState) => void} deserializer - Function to deserialize state into aggregate
|
|
92
|
+
* @param {(metadata: TMeta) => void} metadataRestorer - Optional function to restore metadata
|
|
93
|
+
* @throws {AggregateError} When snapshot is invalid or IDs don't match
|
|
94
|
+
*/
|
|
95
|
+
restoreFromSnapshot(snapshot: IAggregateSnapshot<TState, TMeta>, deserializer: (state: TState) => void, metadataRestorer?: (metadata: TMeta) => void): void;
|
|
96
|
+
/**
|
|
97
|
+
* @param {TState} state - State to save temporarily
|
|
98
|
+
*/
|
|
99
|
+
saveTemporaryState?(state: TState): void;
|
|
100
|
+
/**
|
|
101
|
+
* @returns {Date | null} Timestamp of last snapshot or null if none exists
|
|
102
|
+
*/
|
|
103
|
+
getLastSnapshotTimestamp?(): Date | null;
|
|
104
|
+
/**
|
|
105
|
+
* @param {() => TState} serializer - Function to serialize aggregate state
|
|
106
|
+
* @param {() => TMeta} metadataCreator - Optional metadata creator function
|
|
107
|
+
*/
|
|
108
|
+
saveSnapshot(serializer: () => TState, metadataCreator?: () => TMeta): void;
|
|
109
|
+
/**
|
|
110
|
+
* @returns {IAggregateSnapshot<TState, TMeta> | null} Previous snapshot state if exists
|
|
111
|
+
*/
|
|
112
|
+
getPreviousState(): IAggregateSnapshot<TState, TMeta> | null;
|
|
113
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Capability, IVersioningCapability, IDomainEvent, IEventUpcaster } from '../../../contracts/src/index.ts';
|
|
2
|
+
import { IAggregateEventHandler } from '../aggregate-interfaces';
|
|
3
|
+
/**
|
|
4
|
+
* Capability that handles **event schema evolution** — when a stored event
|
|
5
|
+
* shape changes between releases, register an `IEventUpcaster` to migrate
|
|
6
|
+
* old payloads forward at replay time.
|
|
7
|
+
*
|
|
8
|
+
* Pattern: each event has an implicit version (default 1). Newer aggregate
|
|
9
|
+
* code expects newer-version payload shape. Old events from history are
|
|
10
|
+
* passed through registered upcasters in version order until they reach
|
|
11
|
+
* the current version, then the regular event handler runs.
|
|
12
|
+
*
|
|
13
|
+
* Without versioning, you must keep old event handlers around forever or
|
|
14
|
+
* write migration scripts. With versioning, code stays clean and history
|
|
15
|
+
* stays immutable.
|
|
16
|
+
*
|
|
17
|
+
* @example Adding a field with a default
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import {
|
|
20
|
+
* AggregateBuilder,
|
|
21
|
+
* VersioningCapability,
|
|
22
|
+
* } from '@vytches/ddd-aggregates';
|
|
23
|
+
*
|
|
24
|
+
* // v1 payload had no "currency"; v2 requires it.
|
|
25
|
+
* const order = AggregateBuilder
|
|
26
|
+
* .create({ id })
|
|
27
|
+
* .withVersioning()
|
|
28
|
+
* .build(Order);
|
|
29
|
+
*
|
|
30
|
+
* const versioning = order.getCapability(VersioningCapability)!;
|
|
31
|
+
* versioning.registerUpcaster('OrderCreated', 1, oldPayload => ({
|
|
32
|
+
* ...oldPayload,
|
|
33
|
+
* currency: 'USD', // sane default for legacy events
|
|
34
|
+
* }));
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example Renaming a field
|
|
38
|
+
* ```typescript
|
|
39
|
+
* versioning.registerUpcaster('CustomerUpdated', 1, oldPayload => {
|
|
40
|
+
* const { fullName, ...rest } = oldPayload as { fullName: string };
|
|
41
|
+
* return { ...rest, displayName: fullName };
|
|
42
|
+
* });
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @public
|
|
46
|
+
* @stable
|
|
47
|
+
* @since 0.1.0
|
|
48
|
+
*/
|
|
49
|
+
export declare class VersioningCapability extends Capability<'versioning'> implements IVersioningCapability {
|
|
50
|
+
readonly type: "versioning";
|
|
51
|
+
static get capabilityType(): string;
|
|
52
|
+
private aggregate;
|
|
53
|
+
private upcasters;
|
|
54
|
+
/**
|
|
55
|
+
* @param {unknown} aggregate - Aggregate to attach this capability to
|
|
56
|
+
*/
|
|
57
|
+
attach(aggregate: unknown): void;
|
|
58
|
+
detach?(): void;
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} eventType - Type of event to register upcaster for
|
|
61
|
+
* @param {number} sourceVersion - Source version number to upcast from
|
|
62
|
+
* @param {IEventUpcaster<TFrom, TTo>} upcaster - Upcaster instance to transform events
|
|
63
|
+
*/
|
|
64
|
+
registerUpcaster<TFrom = unknown, TTo = unknown>(eventType: string, sourceVersion: number, upcaster: IEventUpcaster<TFrom, TTo>): void;
|
|
65
|
+
/**
|
|
66
|
+
* @param {IDomainEvent} event - Event to process with version handling
|
|
67
|
+
* @param {Map<string, IAggregateEventHandler>} handlers - Map of event handlers
|
|
68
|
+
*/
|
|
69
|
+
handleVersionedEvent(event: IDomainEvent, handlers: Map<string, IAggregateEventHandler>): void;
|
|
70
|
+
/**
|
|
71
|
+
* @returns {string[]} Array of event types that have registered upcasters
|
|
72
|
+
*/
|
|
73
|
+
getRegisteredEventTypes(): string[];
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} eventType - Event type to check
|
|
76
|
+
* @param {number} version - Version number to check for upcaster
|
|
77
|
+
* @returns {boolean} True if upcaster exists for specified event type and version
|
|
78
|
+
*/
|
|
79
|
+
hasUpcaster(eventType: string, version: number): boolean;
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} eventType - Event type to get upcasters for
|
|
82
|
+
* @returns {Map<number, IEventUpcaster> | undefined} Map of upcasters by version or undefined
|
|
83
|
+
*/
|
|
84
|
+
getUpcastersForType(eventType: string): Map<number, IEventUpcaster> | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} eventType - Event type to clear upcasters for
|
|
87
|
+
*/
|
|
88
|
+
clearUpcastersForType(eventType: string): void;
|
|
89
|
+
/**
|
|
90
|
+
* @returns {number} Total count of all registered upcasters across all event types
|
|
91
|
+
*/
|
|
92
|
+
getTotalUpcasterCount(): number;
|
|
93
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Capability, IEventStore, EntityId } from '../../../contracts/src/index.ts';
|
|
2
|
+
import { IAggregateCapability, IAggregateConstructorParams } from '../aggregate-interfaces';
|
|
3
|
+
import { AggregateRoot } from './aggregate-root';
|
|
4
|
+
/**
|
|
5
|
+
* Fluent builder for assembling an aggregate root with a custom mix of
|
|
6
|
+
* capabilities (snapshots, audit, versioning, event sourcing) — without
|
|
7
|
+
* inheritance chains or constructor explosion.
|
|
8
|
+
*
|
|
9
|
+
* Why a builder? Capabilities have order-sensitive setup (audit must
|
|
10
|
+
* intercept `apply()` before any events are recorded) and many aggregates
|
|
11
|
+
* use only some of them. Hard-coding a base class with all four capabilities
|
|
12
|
+
* pulls in machinery you may not need; manual wiring is error-prone. The
|
|
13
|
+
* builder makes order correct by construction.
|
|
14
|
+
*
|
|
15
|
+
* @example Single capability
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { AggregateBuilder } from '@vytches/ddd-aggregates';
|
|
18
|
+
* import { EntityId } from '@vytches/ddd-value-objects';
|
|
19
|
+
*
|
|
20
|
+
* const order = AggregateBuilder
|
|
21
|
+
* .create({ id: EntityId.create() })
|
|
22
|
+
* .withSnapshots()
|
|
23
|
+
* .build(Order);
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* @example Multiple capabilities, ordered automatically
|
|
27
|
+
* ```typescript
|
|
28
|
+
* const order = AggregateBuilder
|
|
29
|
+
* .create({ id: EntityId.create() })
|
|
30
|
+
* .withVersioning()
|
|
31
|
+
* .withAudit()
|
|
32
|
+
* .withSnapshots()
|
|
33
|
+
* .withEventSourcing(myEventStore)
|
|
34
|
+
* .build(Order);
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example Shorthand — all four built-in capabilities
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const order = AggregateBuilder
|
|
40
|
+
* .create({ id })
|
|
41
|
+
* .buildWithAllCapabilities(Order);
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example Reconstituting from history with capabilities pre-attached
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const order = AggregateBuilder
|
|
47
|
+
* .create({ id, version: 0 })
|
|
48
|
+
* .withAudit()
|
|
49
|
+
* .build(Order);
|
|
50
|
+
* order.loadFromHistory(eventsFromStore);
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @public
|
|
54
|
+
* @stable
|
|
55
|
+
* @since 0.1.0
|
|
56
|
+
*/
|
|
57
|
+
export declare class AggregateBuilder<TId = string> {
|
|
58
|
+
private params;
|
|
59
|
+
private capabilities;
|
|
60
|
+
private eventStore?;
|
|
61
|
+
private constructor();
|
|
62
|
+
/**
|
|
63
|
+
* @returns Aggregate
|
|
64
|
+
*/
|
|
65
|
+
static create<TId = string>(params: {
|
|
66
|
+
id: TId | EntityId<TId>;
|
|
67
|
+
version?: number;
|
|
68
|
+
}): AggregateBuilder<TId>;
|
|
69
|
+
withSnapshots(): this;
|
|
70
|
+
/**
|
|
71
|
+
* @returns {this} Builder instance for method chaining
|
|
72
|
+
*/
|
|
73
|
+
withVersioning(): this;
|
|
74
|
+
/**
|
|
75
|
+
* @returns {this} Builder instance for method chaining
|
|
76
|
+
*/
|
|
77
|
+
withAudit(): this;
|
|
78
|
+
/**
|
|
79
|
+
* @param {IEventStore} [eventStore] - Optional event store for event sourcing
|
|
80
|
+
* @returns {this} Builder instance for method chaining
|
|
81
|
+
*/
|
|
82
|
+
withEventSourcing(eventStore?: IEventStore): this;
|
|
83
|
+
/**
|
|
84
|
+
* @param {T} capability - Custom capability instance to add
|
|
85
|
+
* @param {(cap: T) => void} configure - Optional configuration function for the capability
|
|
86
|
+
* @returns {this} Builder instance for method chaining
|
|
87
|
+
*/
|
|
88
|
+
withCapability<T extends Capability & IAggregateCapability>(capability: T, configure?: (cap: T) => void): this;
|
|
89
|
+
/**
|
|
90
|
+
* @param {IEventStore} eventStore - Event store instance for event sourcing capability
|
|
91
|
+
* @returns {this} Builder instance for method chaining
|
|
92
|
+
*/
|
|
93
|
+
setEventStore(eventStore: IEventStore): this;
|
|
94
|
+
build<TAgg extends AggregateRoot<TId> = AggregateRoot<TId>>(AggregateClass?: new (params: IAggregateConstructorParams<TId>) => TAgg): TAgg;
|
|
95
|
+
/**
|
|
96
|
+
* @param {new (params: IAggregateConstructorParams<TId>) => TAgg} AggregateClass - Optional aggregate class constructor
|
|
97
|
+
* @returns {TAgg} Fully configured aggregate with all standard capabilities
|
|
98
|
+
*/
|
|
99
|
+
buildWithAllCapabilities<TAgg extends AggregateRoot<TId> = AggregateRoot<TId>>(AggregateClass?: new (params: IAggregateConstructorParams<TId>) => TAgg): TAgg;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* @param {object} params - Parameters for creating aggregate builder
|
|
103
|
+
* @param {TId | EntityId<TId>} params.id - Entity identifier
|
|
104
|
+
* @param {number} [params.version] - Optional version number
|
|
105
|
+
* @returns {AggregateBuilder<TId>} New aggregate builder instance
|
|
106
|
+
*/
|
|
107
|
+
export declare function aggregateBuilder<TId = string>(params: {
|
|
108
|
+
id: TId | EntityId<TId>;
|
|
109
|
+
version?: number;
|
|
110
|
+
}): AggregateBuilder<TId>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { IAggregateCapability, IAggregateConstructorParams, IAggregateEventHandler, IAggregateRoot } from '../aggregate-interfaces';
|
|
2
|
+
import { Capability, CapabilityConstructor, EntityId, IDomainEvent, IEventMetadata } from '../../../contracts/src/index.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Base class for aggregate roots — the *transactional consistency boundary*
|
|
5
|
+
* of a Domain-Driven Design model. Per Eric Evans (Blue Book, ch. 6):
|
|
6
|
+
*
|
|
7
|
+
* > An AGGREGATE is a cluster of associated objects that we treat as a unit
|
|
8
|
+
* > for the purpose of data changes. Each AGGREGATE has a root and a
|
|
9
|
+
* > boundary. The root is a single, specific ENTITY contained in the
|
|
10
|
+
* > AGGREGATE. The root is the only member of the AGGREGATE that outside
|
|
11
|
+
* > objects are allowed to hold references to.
|
|
12
|
+
*
|
|
13
|
+
* `AggregateRoot<TId>` provides:
|
|
14
|
+
*
|
|
15
|
+
* - **Identity** via `EntityId<TId>` (`getId()`)
|
|
16
|
+
* - **Versioning** for optimistic locking (`getVersion()`,
|
|
17
|
+
* `getInitialVersion()`)
|
|
18
|
+
* - **Event emission** via `apply()` — append a `DomainEvent`, increment
|
|
19
|
+
* version, run the registered handler to mutate state
|
|
20
|
+
* - **Event sourcing reconstitution** via `loadFromHistory()` — replay
|
|
21
|
+
* events without accumulating new ones
|
|
22
|
+
* - **Capabilities** — composable, opt-in features (snapshots, audit,
|
|
23
|
+
* versioning, event-sourcing) attached without inheritance chains
|
|
24
|
+
* (see {@link AggregateBuilder})
|
|
25
|
+
*
|
|
26
|
+
* Two state-mutation rules:
|
|
27
|
+
*
|
|
28
|
+
* 1. **Never assign fields directly.** All state changes flow through
|
|
29
|
+
* `apply(eventType, payload)` so they are recorded as events. Direct
|
|
30
|
+
* assignment bypasses event recording and version tracking — the
|
|
31
|
+
* aggregate becomes inconsistent with its event stream.
|
|
32
|
+
* 2. **Register handlers BEFORE applying events.** Wire every event name
|
|
33
|
+
* in the constructor via `registerEventHandler('EventName', payload =>
|
|
34
|
+
* {...})` before any `apply()` call. Missing handlers do not throw —
|
|
35
|
+
* the event is recorded but state remains stale.
|
|
36
|
+
*
|
|
37
|
+
* For non-root domain entities (`OrderLine` inside `Order`), use
|
|
38
|
+
* {@link Entity} — it has identity but no version/event machinery.
|
|
39
|
+
*
|
|
40
|
+
* @example Minimal event-sourced aggregate
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { AggregateRoot } from '@vytches/ddd-aggregates';
|
|
43
|
+
* import { EntityId } from '@vytches/ddd-contracts';
|
|
44
|
+
*
|
|
45
|
+
* interface OrderCreatedPayload {
|
|
46
|
+
* customerId: string;
|
|
47
|
+
* amount: number;
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* class Order extends AggregateRoot<string> {
|
|
51
|
+
* private customerId = '';
|
|
52
|
+
* private amount = 0;
|
|
53
|
+
*
|
|
54
|
+
* constructor(params: IAggregateConstructorParams<string>) {
|
|
55
|
+
* super(params);
|
|
56
|
+
* this.registerEventHandler<OrderCreatedPayload>('OrderCreated', payload => {
|
|
57
|
+
* this.customerId = payload!.customerId;
|
|
58
|
+
* this.amount = payload!.amount;
|
|
59
|
+
* });
|
|
60
|
+
* }
|
|
61
|
+
*
|
|
62
|
+
* static create(customerId: string, amount: number): Order {
|
|
63
|
+
* const order = new Order({ id: EntityId.create(), version: 0 });
|
|
64
|
+
* order.apply('OrderCreated', { customerId, amount });
|
|
65
|
+
* return order;
|
|
66
|
+
* }
|
|
67
|
+
* }
|
|
68
|
+
*
|
|
69
|
+
* const order = Order.create('c-1', 500);
|
|
70
|
+
* order.getVersion(); // 1
|
|
71
|
+
* order.getDomainEvents(); // [{ eventName: 'OrderCreated', ... }]
|
|
72
|
+
* order.commit(); // clear after persisting
|
|
73
|
+
* order.hasChanges(); // false
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example Reconstituting from event history
|
|
77
|
+
* ```typescript
|
|
78
|
+
* class Order extends AggregateRoot<string> {
|
|
79
|
+
* static fromEvents(id: EntityId<string>, events: IDomainEvent[]): Order {
|
|
80
|
+
* const order = new Order({ id, version: 0 });
|
|
81
|
+
* order.loadFromHistory(events); // replays events, no new ones recorded
|
|
82
|
+
* return order;
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @example Bounded uncommitted-event count (REL-007 guard)
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const order = new Order({
|
|
90
|
+
* id: EntityId.create(),
|
|
91
|
+
* version: 0,
|
|
92
|
+
* maxEvents: 1000, // throw if uncommitted events exceed 1000
|
|
93
|
+
* });
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* @public
|
|
97
|
+
* @stable
|
|
98
|
+
* @since 0.1.0
|
|
99
|
+
*/
|
|
100
|
+
export declare class AggregateRoot<TId = string> implements IAggregateRoot<TId> {
|
|
101
|
+
private readonly _id;
|
|
102
|
+
private _version;
|
|
103
|
+
private _initialVersion;
|
|
104
|
+
private _domainEvents;
|
|
105
|
+
private _eventHandlers;
|
|
106
|
+
private _capabilities;
|
|
107
|
+
/**
|
|
108
|
+
* REL-007 (2026-05-08): optional advisory limit on uncommitted events.
|
|
109
|
+
* Undefined = no limit (default, backward-compat). When set, `apply()`
|
|
110
|
+
* throws if pushing would exceed the limit — guards against runaway
|
|
111
|
+
* loops or malicious replay payloads.
|
|
112
|
+
*/
|
|
113
|
+
private readonly _maxEvents;
|
|
114
|
+
constructor(params: IAggregateConstructorParams<TId>);
|
|
115
|
+
/**
|
|
116
|
+
* Get the unique identifier of this aggregate
|
|
117
|
+
*/
|
|
118
|
+
getId(): EntityId<TId>;
|
|
119
|
+
/**
|
|
120
|
+
*
|
|
121
|
+
* @returns {number} Current version number for optimistic locking
|
|
122
|
+
*/
|
|
123
|
+
getVersion(): number;
|
|
124
|
+
/**
|
|
125
|
+
*
|
|
126
|
+
* @returns {number} Initial version when aggregate was loaded
|
|
127
|
+
*/
|
|
128
|
+
getInitialVersion(): number;
|
|
129
|
+
/**
|
|
130
|
+
*
|
|
131
|
+
* @returns {boolean} True if aggregate has uncommitted domain events
|
|
132
|
+
*/
|
|
133
|
+
hasChanges(): boolean;
|
|
134
|
+
/**
|
|
135
|
+
*
|
|
136
|
+
* @returns {ReadonlyArray<IDomainEvent>} Array of uncommitted domain events
|
|
137
|
+
*/
|
|
138
|
+
getDomainEvents(): ReadonlyArray<IDomainEvent>;
|
|
139
|
+
/**
|
|
140
|
+
* Clear all uncommitted domain events and update initial version
|
|
141
|
+
*/
|
|
142
|
+
commit(): void;
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} eventType - Event type to register handler for
|
|
145
|
+
* @param {IAggregateEventHandler<T>} handler - Event handler function
|
|
146
|
+
* @returns {this} The aggregate instance for method chaining
|
|
147
|
+
*/
|
|
148
|
+
protected registerEventHandler<T>(eventType: string, handler: IAggregateEventHandler<T>): this;
|
|
149
|
+
/**
|
|
150
|
+
* Apply domain event to aggregate and add to pending events
|
|
151
|
+
* @param {string | IDomainEvent<P>} eventTypeOrEvent - Event type string or complete event object
|
|
152
|
+
* @param {P} payload - Event payload data
|
|
153
|
+
* @param {Partial<IEventMetadata>} metadata - Optional event metadata
|
|
154
|
+
*/
|
|
155
|
+
protected apply<P = unknown>(eventTypeOrEvent: string | IDomainEvent<P>, payload?: P, metadata?: Partial<IEventMetadata>): void;
|
|
156
|
+
private handleEvent;
|
|
157
|
+
/**
|
|
158
|
+
* Reconstruct aggregate state from historical domain events
|
|
159
|
+
* @param {IDomainEvent[]} events - Array of events to replay for state reconstruction
|
|
160
|
+
*/
|
|
161
|
+
protected loadFromHistory(events: IDomainEvent[]): void;
|
|
162
|
+
/**
|
|
163
|
+
* Add capability to enhance aggregate functionality
|
|
164
|
+
* @param {T} capability - Capability instance to add to the aggregate
|
|
165
|
+
* @returns {this} The aggregate instance for method chaining
|
|
166
|
+
*/
|
|
167
|
+
addCapability<T extends Capability & IAggregateCapability>(capability: T): this;
|
|
168
|
+
/**
|
|
169
|
+
* @param {CapabilityConstructor<T>} CapabilityClass - Constructor of the capability to retrieve
|
|
170
|
+
* @returns {T | undefined} The capability instance or undefined if not found
|
|
171
|
+
*/
|
|
172
|
+
getCapability<T extends Capability & IAggregateCapability>(CapabilityClass: CapabilityConstructor<T>): T | undefined;
|
|
173
|
+
/**
|
|
174
|
+
* @param {CapabilityConstructor<T>} CapabilityClass - Constructor of the capability to check
|
|
175
|
+
* @returns {boolean} True if the capability is present
|
|
176
|
+
*/
|
|
177
|
+
hasCapability<T extends Capability & IAggregateCapability>(CapabilityClass: CapabilityConstructor<T>): boolean;
|
|
178
|
+
/**
|
|
179
|
+
* @param {CapabilityConstructor<T>} CapabilityClass - Constructor of the capability to remove
|
|
180
|
+
* @returns {this} The aggregate instance for method chaining
|
|
181
|
+
*/
|
|
182
|
+
removeCapability<T extends Capability & IAggregateCapability>(CapabilityClass: CapabilityConstructor<T>): this;
|
|
183
|
+
/**
|
|
184
|
+
* @returns {Capability[]} Array of all registered capabilities
|
|
185
|
+
*/
|
|
186
|
+
getAllCapabilities(): Capability[];
|
|
187
|
+
/**
|
|
188
|
+
* @returns {string[]} Array of capability type names
|
|
189
|
+
*/
|
|
190
|
+
getCapabilityTypes(): string[];
|
|
191
|
+
private static sanitizeMetadata;
|
|
192
|
+
/**
|
|
193
|
+
* Internal method for capabilities to access private state
|
|
194
|
+
* Should only be used by capabilities, not directly by domain code
|
|
195
|
+
*/
|
|
196
|
+
_internal_setState(state: {
|
|
197
|
+
version: number;
|
|
198
|
+
initialVersion: number;
|
|
199
|
+
domainEvents: IDomainEvent[];
|
|
200
|
+
}): void;
|
|
201
|
+
}
|