@vytches/ddd-aggregates 0.30.0 → 0.31.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/LLMGUIDE.md CHANGED
@@ -41,44 +41,61 @@ class Order extends AggregateRoot<string> {
41
41
  // Usage
42
42
  const order = Order.create('c-1', 500);
43
43
  const events = order.getDomainEvents(); // [{ eventName: 'OrderCreated', ... }]
44
- order.commit(); // clear pending events after saving
44
+ order.getDomainEvents() === events; // true — same array reference until the next mutation
45
+ order.commit(); // clear pending events after saving; getDomainEvents() now returns a new, empty array
45
46
  ```
46
47
 
47
48
  ## Key API
48
49
 
49
- | Export | Kind | Description |
50
- | -------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
- | `AggregateRoot<TId>` | class | Base aggregate root; manages version, events, capabilities — the _transactional_ root |
52
- | `Entity<TId>` | abstract class | Base for **non-root** domain entities (VF-CANON-001) — identity-based equality, no version/event machinery. Use for `OrderLine`, inner aggregate entities |
53
- | `AggregateBuilder<TId>` | class | Fluent builder for constructing aggregates with capabilities |
54
- | `aggregateBuilder(params)` | function | Shorthand factory for `AggregateBuilder.create(params)` |
55
- | `AggregateError` | class | Domain error for aggregate-level failures |
56
- | `SnapshotCapability` | class | Enables snapshot creation and restoration |
57
- | `AuditCapability` | class | Attaches an audit log to every event applied |
58
- | `VersioningCapability` | class | Registers event upcasters for schema evolution |
59
- | `EventSourcingCapability` | class | Integrates with `IEventStore` for persistence |
60
- | `asSnapshotAggregate(agg)` | function | Casts aggregate, throws if no `SnapshotCapability` |
61
- | `asAuditAggregate(agg)` | function | Casts aggregate, throws if no `AuditCapability` |
62
- | `hasAllCapabilities(agg, [...])` | function | Returns `true` if aggregate has every listed capability |
63
- | `getAggregateCapabilities(agg)` | function | Returns string array of capability type names |
64
- | `AggregateWithSnapshotCapability<TId>` | type | Type-safe alias for snapshot-capable aggregates |
65
- | `IAggregateRoot<TId>` | interface | Full contract for aggregates |
50
+ | Export | Kind | Description |
51
+ | ------------------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `AggregateRoot<TId>` | class | Base aggregate root; manages version, events, capabilities — the _transactional_ root |
53
+ | `Entity<TId>` | abstract class | Base for **non-root** domain entities (VF-CANON-001) — identity-based equality, no version/event machinery. Use for `OrderLine`, inner aggregate entities |
54
+ | `AggregateBuilder<TId>` | class | Fluent builder for constructing aggregates with capabilities |
55
+ | `aggregateBuilder(params)` | function | Shorthand factory for `AggregateBuilder.create(params)` |
56
+ | `AggregateError` | class | Domain error for aggregate-level failures |
57
+ | `SnapshotCapability` | class | Enables snapshot creation and restoration |
58
+ | `AuditCapability` | class | Attaches an audit log to every event applied |
59
+ | `VersioningCapability` | class | Registers event upcasters for schema evolution |
60
+ | `EventSourcingCapability` | class | Integrates with `IEventStore` for persistence |
61
+ | `asSnapshotAggregate(agg)` | function | Casts aggregate, throws if no `SnapshotCapability` |
62
+ | `asAuditAggregate(agg)` | function | Casts aggregate, throws if no `AuditCapability` |
63
+ | `asVersioningAggregate(agg)` | function | Casts aggregate, throws if no `VersioningCapability` |
64
+ | `asEventSourcingAggregate(agg)` | function | Casts aggregate, throws if no `EventSourcingCapability` |
65
+ | `tryAsSnapshotAggregate(agg)` | function | Non-throwing sibling of `asSnapshotAggregate`; returns `Result.ok(aggregate)` or `Result.fail(AggregateError)` instead of throwing |
66
+ | `tryAsAuditAggregate(agg)` | function | Non-throwing sibling of `asAuditAggregate`; returns a `Result<AggregateWithAuditCapability<TId>, AggregateError>` |
67
+ | `tryAsVersioningAggregate(agg)` | function | Non-throwing sibling of `asVersioningAggregate`; returns a `Result<AggregateWithVersioningCapability<TId>, AggregateError>` |
68
+ | `tryAsEventSourcingAggregate(agg)` | function | Non-throwing sibling of `asEventSourcingAggregate`; returns a `Result<AggregateWithEventSourcingCapability<TId>, AggregateError>` |
69
+ | `hasAllCapabilities(agg, [...])` | function | Returns `true` if aggregate has every listed capability |
70
+ | `getAggregateCapabilities(agg)` | function | Returns string array of capability type names |
71
+ | `AggregateWithSnapshotCapability<TId>` | type | Type-safe alias for snapshot-capable aggregates; return type of `asSnapshotAggregate`/`tryAsSnapshotAggregate` |
72
+ | `AggregateWithAuditCapability<TId>` | type | Type-safe alias for audit-capable aggregates; return type of `asAuditAggregate`/`tryAsAuditAggregate` |
73
+ | `AggregateWithVersioningCapability<TId>` | type | Type-safe alias for versioning-capable aggregates; return type of `asVersioningAggregate`/`tryAsVersioningAggregate` |
74
+ | `AggregateWithEventSourcingCapability<TId>` | type | Type-safe alias for event-sourcing-capable aggregates; return type of `asEventSourcingAggregate`/`tryAsEventSourcingAggregate` |
75
+ | `IAggregateRoot<TId>` | interface | Full contract for aggregates |
76
+ | `IAggregateCapability` | interface | Base contract every capability implements: `attach(aggregate)` (required) + `detach?()` (optional). `SnapshotCapability`, `AuditCapability`, `VersioningCapability`, `EventSourcingCapability` all satisfy it |
77
+ | `IAggregateEventHandler<T>` | interface | Callable shape `(payload: T, metadata?) => void` — the handler function type registered via `registerEventHandler(type, fn)` |
78
+
79
+ > **BREAKING (v0.31.0):** `IAggregateBuilder<TId>` was removed — it was exported
80
+ > but shape-incompatible with the concrete `AggregateBuilder` class. Use
81
+ > `AggregateBuilder` directly for `withSnapshots()`, `withVersioning()`,
82
+ > `withEventSourcing()`, `withAudit()`, `withCustomCapability()`, `build()`.
66
83
 
67
84
  ### `AggregateRoot` method reference
68
85
 
69
- | Method | Description |
70
- | --------------------------------------- | ------------------------------------------------------------------------- |
71
- | `apply(eventType, payload?, metadata?)` | Record a domain event, increment version, call handler |
72
- | `loadFromHistory(events)` | Replay historical events to restore state without accumulating new events |
73
- | `getDomainEvents()` | Return readonly array of uncommitted events since last `commit()` |
74
- | `commit()` | Clear uncommitted events; call after persisting to storage |
75
- | `hasChanges()` | `true` if there are uncommitted events |
76
- | `getVersion()` | Current version after all applied events |
77
- | `getInitialVersion()` | Version when last loaded from storage (for optimistic locking) |
78
- | `addCapability(cap)` | Attach a capability; calls `cap.attach(this)` |
79
- | `getCapability(CapClass)` | Retrieve capability by constructor, returns `undefined` if absent |
80
- | `hasCapability(CapClass)` | Type-safe capability presence check |
81
- | `registerEventHandler(type, fn)` | Protected; wire event name to state mutation function |
86
+ | Method | Description |
87
+ | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
+ | `apply(eventType, payload?, metadata?)` | Record a domain event, increment version, call handler |
89
+ | `loadFromHistory(events)` | Replay historical events to restore state without accumulating new events |
90
+ | `getDomainEvents()` | Return readonly, deep-frozen array of uncommitted events since last `commit()`. Same array reference on repeat calls — rebuilt only on the next mutation (`apply`/`commit`/`loadFromHistory`/`transformDomainEvents`) |
91
+ | `commit()` | Clear uncommitted events; call after persisting to storage |
92
+ | `hasChanges()` | `true` if there are uncommitted events |
93
+ | `getVersion()` | Current version after all applied events |
94
+ | `getInitialVersion()` | Version when last loaded from storage (for optimistic locking) |
95
+ | `addCapability(cap)` | Attach a capability; calls `cap.attach(this)` |
96
+ | `getCapability(CapClass)` | Retrieve capability by constructor, returns `undefined` if absent |
97
+ | `hasCapability(CapClass)` | Type-safe capability presence check |
98
+ | `registerEventHandler(type, fn)` | Protected; wire event name to state mutation function |
82
99
 
83
100
  ## Patterns
84
101
 
@@ -136,6 +153,57 @@ if (hasAllCapabilities(order, [AuditCapability])) {
136
153
  }
137
154
  ```
138
155
 
156
+ ### Composing multiple capabilities and narrowing with `tryAs*` helpers
157
+
158
+ Build one aggregate with two capabilities (versioning + event sourcing), then
159
+ narrow it back to each capability-specific type with the non-throwing `tryAs*`
160
+ variants — useful in application-layer code where a missing capability should be
161
+ handled as a `Result`, not an exception.
162
+
163
+ ```typescript
164
+ import {
165
+ AggregateBuilder,
166
+ VersioningCapability,
167
+ EventSourcingCapability,
168
+ tryAsVersioningAggregate,
169
+ tryAsEventSourcingAggregate,
170
+ type AggregateWithVersioningCapability,
171
+ type AggregateWithEventSourcingCapability,
172
+ } from '@vytches/ddd-aggregates';
173
+ import { EntityId } from '@vytches/ddd-value-objects';
174
+
175
+ const order = AggregateBuilder.create({ id: EntityId.create() })
176
+ .withVersioning()
177
+ .withEventSourcing()
178
+ .build(Order);
179
+
180
+ // Narrow to versioning-capable: register an upcaster for an old event shape.
181
+ const versioningResult = tryAsVersioningAggregate(order);
182
+ if (versioningResult.isSuccess) {
183
+ const versioned: AggregateWithVersioningCapability<string> =
184
+ versioningResult.value;
185
+ versioned
186
+ .getCapability(VersioningCapability)!
187
+ .registerUpcaster('OrderCreated', 1, {
188
+ upcast: (payload: { amount: number }) => ({
189
+ ...payload,
190
+ currency: 'USD',
191
+ }),
192
+ });
193
+ }
194
+
195
+ // Narrow to event-sourcing-capable: persist to the configured event store.
196
+ const eventSourcingResult = tryAsEventSourcingAggregate(order);
197
+ if (eventSourcingResult.isSuccess) {
198
+ const sourced: AggregateWithEventSourcingCapability<string> =
199
+ eventSourcingResult.value;
200
+ await sourced.getCapability(EventSourcingCapability)!.saveToEventStore();
201
+ } else {
202
+ // eventSourcingResult.error is an AggregateError — handle without throwing.
203
+ console.error(eventSourcingResult.error.message);
204
+ }
205
+ ```
206
+
139
207
  ## Anti-Patterns
140
208
 
141
209
  **Mutating state directly instead of using `apply()`.** Assigning
package/README.md CHANGED
@@ -181,7 +181,11 @@ class OrderLine extends Entity<string> {
181
181
  | `IAggregateCapability` | interface | Base capability contract |
182
182
  | `IAggregateConstructorParams<TId>` | interface | Constructor parameter shape for aggregates |
183
183
  | `IAggregateEventHandler` | interface | Event handler registration contract |
184
- | `IAggregateBuilder` | interface | Builder contract |
184
+
185
+ > **BREAKING (v0.31.0):** `IAggregateBuilder` was removed. It was
186
+ > shape-incompatible with the concrete `AggregateBuilder` class (a broken public
187
+ > interface is worse than none). Use the `AggregateBuilder` class directly for
188
+ > the builder's fluent contract.
185
189
 
186
190
  ### Utility Types
187
191
 
@@ -1,4 +1,4 @@
1
- import { Capability, CapabilityConstructor, EntityId, IAggregateSnapshot, IAuditEvent, IDomainEvent, IEventMetadata, IEventStore, IEventUpcaster } from '@vytches/ddd-contracts';
1
+ import { Capability, CapabilityConstructor, EntityId, IAggregateSnapshot, IAuditEvent, IDomainEvent, IEventMetadata, IEventUpcaster } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Core interface for aggregate root functionality.
4
4
  * Provides the foundation for domain aggregates with event sourcing, versioning, and capability management.
@@ -80,6 +80,21 @@ export interface IAggregateConstructorParams<TId = string> {
80
80
  * Leave undefined for "no limit" (preserves backward compatibility).
81
81
  */
82
82
  maxEvents?: number;
83
+ /**
84
+ * VF-023 (D-4, AC6, non-breaking addition): controls what happens when
85
+ * `apply()` (live) or `loadFromHistory()` (replay) processes an event for
86
+ * which no handler was registered via `registerEventHandler()`.
87
+ *
88
+ * - `"warn"` (default): the event is still recorded/replayed, but a
89
+ * warning is logged via the library's internal logger. Preserves the
90
+ * previous behavior's intent (missing handlers did not throw) without
91
+ * silently swallowing the condition.
92
+ * - `"throw"`: fail fast instead — useful in tests or strict environments
93
+ * where a missing handler indicates a real bug.
94
+ *
95
+ * Leave undefined for the default `"warn"` behavior (backward compatible).
96
+ */
97
+ onMissingHandler?: 'warn' | 'throw';
83
98
  }
84
99
  /**
85
100
  * Base interface for all aggregate capabilities.
@@ -174,36 +189,6 @@ export interface IMiddlewareCapability extends IAggregateCapability {
174
189
  */
175
190
  use(middleware: EventAggregateMiddleware): this;
176
191
  }
177
- /**
178
- * Builder interface for creating aggregates with capabilities.
179
- * Provides fluent API for configuring aggregate features before construction.
180
- */
181
- export interface IAggregateBuilder<TId> {
182
- /**
183
- * Adds snapshot capability
184
- */
185
- withSnapshots<TState = unknown, TMeta = unknown>(): this;
186
- /**
187
- * Adds versioning capability
188
- */
189
- withVersioning(): this;
190
- /**
191
- * Adds event sourcing capability
192
- */
193
- withEventSourcing(eventStore?: IEventStore): this;
194
- /**
195
- * Adds audit capability
196
- */
197
- withAudit(): this;
198
- /**
199
- * Adds custom capability
200
- */
201
- withCustomCapability<T extends IAggregateCapability>(name: string, capability: T): this;
202
- /**
203
- * Builds the aggregate with all configured capabilities
204
- */
205
- build(): IAggregateRoot<TId>;
206
- }
207
192
  /**
208
193
  * Handler function for processing aggregate events.
209
194
  * Used internally by aggregates to handle domain events.
@@ -243,156 +228,3 @@ export type AggregateIdType<T> = T extends IAggregateRoot<infer TId> ? TId : nev
243
228
  export type AggregateWithCapabilities<TId, TCapabilities extends CapabilityName[]> = IAggregateRoot<TId> & {
244
229
  [K in TCapabilities[number] as `getCapability`]: (name: K) => K extends 'snapshot' ? ISnapshotCapability : K extends 'versioning' ? IVersioningCapability : K extends 'eventSourcing' ? IEventSourcingCapability : K extends 'audit' ? IAuditCapability : K extends 'middleware' ? IMiddlewareCapability : IAggregateCapability;
245
230
  };
246
- /**
247
- * Factory interface for creating and rebuilding aggregates.
248
- * Provides standard methods for aggregate lifecycle management.
249
- * @template TId - The type of the aggregate ID value (string, number, etc.)
250
- */
251
- export interface IAggregateFactory<TId = string, TAggregate extends IAggregateRoot<TId> = IAggregateRoot<TId>> {
252
- /**
253
- * Creates a new aggregate instance
254
- */
255
- create(id: EntityId<TId>, ...args: unknown[]): TAggregate;
256
- /**
257
- * Rebuilds aggregate from events
258
- */
259
- fromEvents(id: EntityId<TId>, events: IDomainEvent[]): TAggregate;
260
- /**
261
- * Rebuilds aggregate from snapshot and subsequent events
262
- */
263
- fromSnapshot<TState, TMeta>(snapshot: IAggregateSnapshot<TState, TMeta>, events: IDomainEvent[]): TAggregate;
264
- }
265
- /**
266
- * Interface for validating aggregate state and operations.
267
- * Enables business rule validation at the aggregate level.
268
- */
269
- export interface IAggregateValidator<TAggregate extends IAggregateRoot<unknown>> {
270
- /**
271
- * Validates aggregate state
272
- */
273
- validate(aggregate: TAggregate): ValidationResult;
274
- /**
275
- * Validates aggregate before applying event
276
- */
277
- validateBeforeEvent(aggregate: TAggregate, event: IDomainEvent): ValidationResult;
278
- }
279
- /**
280
- * Result of aggregate validation operation.
281
- * Contains validation status and any error details.
282
- */
283
- export interface ValidationResult {
284
- /** Whether the validation passed */
285
- isValid: boolean;
286
- /** List of validation errors if any */
287
- errors: IValidationError[];
288
- }
289
- /**
290
- * Represents a single validation error.
291
- * Provides details about what failed validation and why.
292
- */
293
- export interface IValidationError {
294
- /** The field or property that failed validation */
295
- field: string;
296
- /** Human-readable error message */
297
- message: string;
298
- /** Optional error code for programmatic handling */
299
- code?: string;
300
- }
301
- /**
302
- * Capability for caching aggregate data.
303
- * Provides key-value storage with TTL support for performance optimization.
304
- */
305
- export interface ICachingCapability extends IAggregateCapability {
306
- /**
307
- * Gets cached value
308
- */
309
- get<T>(key: string): T | undefined;
310
- /**
311
- * Sets cached value
312
- */
313
- set<T>(key: string, value: T, ttl?: number): void;
314
- /**
315
- * Clears all cached values
316
- */
317
- clear(): void;
318
- /**
319
- * Invalidates specific cached value
320
- */
321
- invalidate(key: string): void;
322
- }
323
- /**
324
- * Capability for collecting aggregate metrics.
325
- * Enables monitoring and observability of aggregate operations.
326
- */
327
- export interface IMetricsCapability extends IAggregateCapability {
328
- /**
329
- * Records a metric
330
- */
331
- record(name: string, value: number, tags?: Record<string, string>): void;
332
- /**
333
- * Increments a counter
334
- */
335
- increment(name: string, tags?: Record<string, string>): void;
336
- /**
337
- * Records timing information
338
- */
339
- timing(name: string, duration: number, tags?: Record<string, string>): void;
340
- /**
341
- * Gets recorded metrics
342
- */
343
- getMetrics(): MetricData[];
344
- }
345
- /**
346
- * Represents a single metric data point.
347
- * Contains metric information with optional tags and timestamp.
348
- */
349
- export interface MetricData {
350
- /** Metric name */
351
- name: string;
352
- /** Metric value */
353
- value: number;
354
- /** Type of metric */
355
- type: 'counter' | 'gauge' | 'timing';
356
- /** Optional tags for metric categorization */
357
- tags?: Record<string, string>;
358
- /** When the metric was recorded */
359
- timestamp: Date;
360
- }
361
- /**
362
- * Capability for security features like encryption and authorization.
363
- * Provides security controls for sensitive aggregate operations.
364
- */
365
- export interface ISecurityCapability extends IAggregateCapability {
366
- /**
367
- * Encrypts sensitive data
368
- */
369
- encrypt(data: unknown): string;
370
- /**
371
- * Decrypts sensitive data
372
- */
373
- decrypt(encryptedData: string): unknown;
374
- /**
375
- * Checks if user has permission to perform action
376
- */
377
- hasPermission(action: string, user?: unknown): boolean;
378
- /**
379
- * Logs security events
380
- */
381
- logSecurityEvent(event: SecurityEvent): void;
382
- }
383
- /**
384
- * Represents a security-related event.
385
- * Used for audit trails and security monitoring.
386
- */
387
- export interface SecurityEvent {
388
- /** Type of security event */
389
- type: 'access' | 'modification' | 'failure';
390
- /** Action that was performed */
391
- action: string;
392
- /** User who performed the action */
393
- user?: unknown;
394
- /** When the event occurred */
395
- timestamp: Date;
396
- /** Additional event metadata */
397
- metadata?: Record<string, unknown>;
398
- }
@@ -1,4 +1,4 @@
1
- import { Capability, IAuditCapability, IAuditEvent, IDomainEvent } from '../../../contracts/src/index.ts';
1
+ import { Capability, IAuditCapability, IAuditEvent, IDomainEvent } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Capability that records every domain event applied to the aggregate as
4
4
  * an `IAuditEvent`. Useful for compliance, debugging, behavior analytics,
@@ -1,4 +1,4 @@
1
- import { IEventSourcingCapability, IEventStore, Capability } from '../../../contracts/src/index.ts';
1
+ import { IEventSourcingCapability, IEventStore, Capability } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Capability that integrates an aggregate with an external `IEventStore` —
4
4
  * the persistence boundary for an event-sourced system. Allows the
@@ -1,4 +1,4 @@
1
- import { IAggregateSnapshot, ISnapshotCapability, Capability } from '../../../contracts/src/index.ts';
1
+ import { IAggregateSnapshot, ISnapshotCapability, Capability } from '@vytches/ddd-contracts';
2
2
  /**
3
3
  * Capability that enables snapshot creation and restoration on an aggregate
4
4
  * — a performance optimization for event-sourced aggregates with long
@@ -1,4 +1,4 @@
1
- import { Capability, IVersioningCapability, IDomainEvent, IEventUpcaster } from '../../../contracts/src/index.ts';
1
+ import { Capability, IVersioningCapability, IDomainEvent, IEventUpcaster } from '@vytches/ddd-contracts';
2
2
  import { IAggregateEventHandler } from '../aggregate-interfaces';
3
3
  /**
4
4
  * Capability that handles **event schema evolution** — when a stored event
@@ -63,7 +63,20 @@ export declare class VersioningCapability extends Capability<'versioning'> imple
63
63
  */
64
64
  registerUpcaster<TFrom = unknown, TTo = unknown>(eventType: string, sourceVersion: number, upcaster: IEventUpcaster<TFrom, TTo>): void;
65
65
  /**
66
- * @param {IDomainEvent} event - Event to process with version handling
66
+ * Applies registered upcasters to bring `event` from its stored version up
67
+ * to the aggregate's current version, then invokes the matching handler.
68
+ *
69
+ * **Upcasting only activates if `event.metadata.targetVersion` is set.**
70
+ * This is NOT inferred automatically from registered upcasters or from
71
+ * any aggregate-level "current version" — the caller (typically your
72
+ * event-sourcing replay/load path) must explicitly stamp each historical
73
+ * event's metadata with `targetVersion` before calling this method. If
74
+ * `targetVersion` is absent, `currentVersion` falls back to `eventVersion`
75
+ * (i.e. no upcasting happens — the event is passed through unchanged even
76
+ * if upcasters are registered for it).
77
+ *
78
+ * @param {IDomainEvent} event - Event to process with version handling;
79
+ * set `event.metadata.targetVersion` to enable upcasting
67
80
  * @param {Map<string, IAggregateEventHandler>} handlers - Map of event handlers
68
81
  */
69
82
  handleVersionedEvent(event: IDomainEvent, handlers: Map<string, IAggregateEventHandler>): void;
@@ -1,4 +1,4 @@
1
- import { Capability, IEventStore, EntityId } from '../../../contracts/src/index.ts';
1
+ import { Capability, IEventStore, EntityId } from '@vytches/ddd-contracts';
2
2
  import { IAggregateCapability, IAggregateConstructorParams } from '../aggregate-interfaces';
3
3
  import { AggregateRoot } from './aggregate-root';
4
4
  /**