@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.
@@ -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
+ }