@vytches/ddd-aggregates 0.26.0 → 0.28.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/README.md CHANGED
@@ -1,1393 +1,206 @@
1
1
  # @vytches/ddd-aggregates
2
2
 
3
+ > Aggregate root implementation with event sourcing, optimistic concurrency, and
4
+ > opt-in capabilities.
5
+
3
6
  [![npm version](https://badge.fury.io/js/%40vytches%2Fddd-aggregates.svg)](https://badge.fury.io/js/%40vytches%2Fddd-aggregates)
4
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
5
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
9
 
7
- > **Enterprise-grade aggregate root implementations for Domain-Driven Design**
8
-
9
- Complete aggregate root implementation with built-in event sourcing, capability
10
- system, and automatic event management. Designed for complex domain models with
11
- strong consistency boundaries.
12
-
13
- ## 📚 Detailed Documentation
14
-
15
- **NEW**: Enhanced Metadata System V2 with YAML Front Matter for AI-optimized
16
- documentation:
17
-
18
- - 🎯 **Generated JSDoc**: Full API documentation with hierarchical metadata
19
- system
20
- - 📊 **Live Documentation**: View at `/docs/api/aggregates/index.html` after
21
- running `pnpm jsdoc:publish`
22
- - 🧠 **AI-Optimized**: Structured metadata for LLM consumption and code
23
- generation
24
- - ⚡ **Performance**: ~12s initial build, ~2s incremental, ~0.5s no changes
25
-
26
- **Core Documentation:**
27
-
28
- - 📖 **AggregateRoot Class** - Base aggregate implementation with event sourcing
29
- - 🔧 **AggregateBuilder Class** - Fluent builder for aggregates with
30
- capabilities
31
- - 🛠️ **Utility Functions** - Type-safe capability management and utilities
32
- - 📋 **Error Classes** - Comprehensive aggregate-specific error handling
33
-
34
- > **Enhanced Metadata System V2** provides hierarchical documentation (Global →
35
- > Package → Class → Method) with format-specific content, custom tags, and
36
- > automatic example injection.
37
-
38
- ## 📋 Table of Contents
39
-
40
- - [Installation](#installation)
41
- - [Key Features](#key-features)
42
- - [Core Concepts](#core-concepts)
43
- - [Quick Start](#quick-start)
44
- - [Advanced Features](#advanced-features)
45
- - [API Reference](#api-reference)
46
- - [Patterns & Best Practices](#patterns--best-practices)
47
- - [Event Sourcing](#event-sourcing)
48
- - [Capabilities](#capabilities)
49
- - [Testing](#testing)
50
- - [Performance](#performance)
51
- - [Contributing](#contributing)
52
-
53
- ## 🚀 Installation
10
+ ## Installation
54
11
 
55
12
  ```bash
56
- # npm
57
- npm install @vytches/ddd-aggregates
58
-
59
- # yarn
60
- yarn add @vytches/ddd-aggregates
61
-
62
- # pnpm
63
13
  pnpm add @vytches/ddd-aggregates
64
14
  ```
65
15
 
66
- ### Peer Dependencies
67
-
68
- ```bash
69
- # Required for full functionality
70
- npm install @vytches/ddd-domain-primitives @vytches/ddd-events @vytches/ddd-utils
71
- ```
72
-
73
- ## ✨ Key Features
74
-
75
- ### Aggregate Root Foundation
76
-
77
- - **Event Sourcing**: Built-in event sourcing with automatic event management
78
- - **Capability System**: Extensible capabilities for cross-cutting concerns
79
- - **Concurrency Control**: Optimistic concurrency with version management
80
- - **Snapshot Support**: Automatic snapshots for performance optimization
81
-
82
- ### Enterprise Features
83
-
84
- - **Strong Consistency**: Aggregate consistency boundary enforcement
85
- - **Event Management**: Automatic event collection and publishing
86
- - **State Validation**: Built-in invariant validation
87
- - **Audit Trail**: Complete audit trail with event history
88
-
89
- ### Developer Experience
90
-
91
- - **Type Safety**: Full TypeScript support with generic typing
92
- - **Fluent API**: Intuitive API for domain modeling
93
- - **Testing Support**: Built-in testing utilities and builders
94
- - **Documentation**: Comprehensive documentation and examples
16
+ ## What's included
95
17
 
96
- ## 🎯 Core Concepts
18
+ - **`AggregateRoot`** — base class for aggregate roots with identity,
19
+ versioning, and domain events
20
+ - **`Entity`** — base class for non-root domain entities (identity-based
21
+ equality, no event machinery)
22
+ - **`AggregateBuilder` / `aggregateBuilder`** — fluent builder for constructing
23
+ aggregates with capabilities
24
+ - **Capabilities** — `AuditCapability`, `EventSourcingCapability`,
25
+ `SnapshotCapability`, `VersioningCapability`
26
+ - **Utility functions** — type-safe capability casting and introspection
27
+ - **`AggregateError`** — error class for aggregate-specific failures
97
28
 
98
- ### Aggregate Root
99
-
100
- An aggregate root is the only member of an aggregate that outside objects are
101
- allowed to hold references to. It's the gatekeeper for the aggregate, ensuring
102
- consistency and managing the lifecycle of the aggregate.
103
-
104
- ```typescript
105
- // Base aggregate root interface
106
- interface IAggregateRoot {
107
- readonly id: EntityId;
108
- readonly version: number;
109
-
110
- // Event management
111
- addDomainEvent(event: IDomainEvent): void;
112
- getUncommittedEvents(): IDomainEvent[];
113
- markEventsAsCommitted(): void;
114
-
115
- // Capability system
116
- addCapability<T extends IAggregateCapability>(capability: T): void;
117
- getCapability<T extends IAggregateCapability>(type: new () => T): T | null;
118
- hasCapability<T extends IAggregateCapability>(type: new () => T): boolean;
119
-
120
- // Lifecycle
121
- toSnapshot(): AggregateSnapshot;
122
- fromSnapshot(snapshot: AggregateSnapshot): void;
123
- }
124
- ```
125
-
126
- ### Event Sourcing
127
-
128
- Event sourcing is a pattern where state changes are stored as a sequence of
129
- events. The current state is derived by replaying these events.
130
-
131
- ```typescript
132
- // Event sourced aggregate
133
- abstract class EventSourcedAggregate extends AggregateRoot {
134
- // Apply event to current state
135
- protected apply(event: IDomainEvent): void;
136
-
137
- // Replay events to rebuild state
138
- static fromEventStream<T extends EventSourcedAggregate>(
139
- events: IDomainEvent[]
140
- ): T;
141
-
142
- // Event handlers (implemented by subclasses)
143
- protected abstract getEventHandlers(): EventHandlerMap;
144
- }
145
- ```
146
-
147
- ### Capabilities
148
-
149
- Capabilities are modular extensions that can be added to aggregates to provide
150
- cross-cutting concerns like auditing, caching, or validation.
29
+ ## Usage
151
30
 
152
31
  ```typescript
153
- // Capability interface
154
- interface IAggregateCapability {
155
- readonly name: string;
156
-
157
- // Lifecycle hooks
158
- onEventApplied?(event: IDomainEvent): void;
159
- onSnapshotCreated?(snapshot: AggregateSnapshot): void;
160
- beforeSave?(): void;
161
- afterSave?(): void;
162
- }
163
- ```
32
+ import { AggregateRoot } from '@vytches/ddd-aggregates';
33
+ import type { IAggregateConstructorParams } from '@vytches/ddd-aggregates';
34
+ import { EntityId } from '@vytches/ddd-contracts';
164
35
 
165
- ## 🚀 Quick Start
36
+ class Order extends AggregateRoot<string> {
37
+ private customerId = '';
38
+ private status: 'pending' | 'confirmed' = 'pending';
166
39
 
167
- ### 1. Basic Aggregate Root
168
-
169
- ```typescript
170
- import { AggregateRoot, EntityId } from '@vytches/ddd-aggregates';
171
- import { DomainEvent } from '@vytches/ddd-events';
172
- import { Result } from '@vytches/ddd-utils';
173
-
174
- // Domain events
175
- class OrderCreatedEvent extends DomainEvent<{
176
- orderId: string;
177
- customerId: string;
178
- totalAmount: number;
179
- currency: string;
180
- }> {
181
- constructor(payload: OrderCreatedData) {
182
- super(payload, undefined, 'OrderCreated');
183
- }
184
- }
185
-
186
- class OrderItemAddedEvent extends DomainEvent<{
187
- orderId: string;
188
- productId: string;
189
- quantity: number;
190
- price: number;
191
- }> {
192
- constructor(payload: OrderItemAddedData) {
193
- super(payload, undefined, 'OrderItemAdded');
194
- }
195
- }
196
-
197
- // Value objects
198
- class OrderStatus {
199
- static readonly PENDING = new OrderStatus('PENDING');
200
- static readonly CONFIRMED = new OrderStatus('CONFIRMED');
201
- static readonly SHIPPED = new OrderStatus('SHIPPED');
202
- static readonly DELIVERED = new OrderStatus('DELIVERED');
203
- static readonly CANCELLED = new OrderStatus('CANCELLED');
204
-
205
- private constructor(private readonly value: string) {}
206
-
207
- equals(other: OrderStatus): boolean {
208
- return this.value === other.value;
209
- }
210
-
211
- toString(): string {
212
- return this.value;
213
- }
214
- }
215
-
216
- // Aggregate root implementation
217
- class OrderAggregate extends AggregateRoot {
218
- private customerId: EntityId;
219
- private items: OrderItem[] = [];
220
- private status: OrderStatus = OrderStatus.PENDING;
221
- private totalAmount: number = 0;
222
- private currency: string = 'USD';
223
-
224
- private constructor(id: EntityId, customerId: EntityId) {
225
- super(id);
226
- this.customerId = customerId;
227
- }
228
-
229
- // Factory method
230
- static create(customerId: EntityId): OrderAggregate {
231
- const order = new OrderAggregate(EntityId.create(), customerId);
232
-
233
- // Add creation event
234
- order.addDomainEvent(
235
- new OrderCreatedEvent({
236
- orderId: order.id.value,
237
- customerId: customerId.value,
238
- totalAmount: 0,
239
- currency: 'USD',
240
- })
40
+ constructor(params: IAggregateConstructorParams<string>) {
41
+ super(params);
42
+ this.registerEventHandler<{ customerId: string }>(
43
+ 'OrderCreated',
44
+ payload => {
45
+ this.customerId = payload!.customerId;
46
+ }
241
47
  );
242
-
243
- return order;
48
+ this.registerEventHandler<void>('OrderConfirmed', () => {
49
+ this.status = 'confirmed';
50
+ });
244
51
  }
245
52
 
246
- // Business methods
247
- addItem(
248
- productId: EntityId,
249
- quantity: number,
250
- price: number
251
- ): Result<void, Error> {
252
- // Validate business rules
253
- if (quantity <= 0) {
254
- return Result.fail(new Error('Quantity must be positive'));
255
- }
256
-
257
- if (price < 0) {
258
- return Result.fail(new Error('Price cannot be negative'));
259
- }
260
-
261
- if (!this.status.equals(OrderStatus.PENDING)) {
262
- return Result.fail(new Error('Cannot add items to non-pending order'));
263
- }
264
-
265
- // Apply changes
266
- const item = new OrderItem(productId, quantity, price);
267
- this.items.push(item);
268
- this.totalAmount += quantity * price;
269
-
270
- // Add domain event
271
- this.addDomainEvent(
272
- new OrderItemAddedEvent({
273
- orderId: this.id.value,
274
- productId: productId.value,
275
- quantity,
276
- price,
277
- })
278
- );
279
-
280
- return Result.ok();
53
+ static create(customerId: string): Order {
54
+ const order = new Order({ id: EntityId.create(), version: 0 });
55
+ order.apply('OrderCreated', { customerId });
56
+ return order;
281
57
  }
282
58
 
283
- confirm(): Result<void, Error> {
284
- if (!this.status.equals(OrderStatus.PENDING)) {
285
- return Result.fail(new Error('Order must be pending to confirm'));
286
- }
287
-
288
- if (this.items.length === 0) {
289
- return Result.fail(new Error('Cannot confirm empty order'));
290
- }
291
-
292
- this.status = OrderStatus.CONFIRMED;
293
-
294
- this.addDomainEvent(
295
- new OrderConfirmedEvent({
296
- orderId: this.id.value,
297
- confirmedAt: new Date(),
298
- totalAmount: this.totalAmount,
299
- })
300
- );
301
-
302
- return Result.ok();
59
+ confirm(): void {
60
+ if (this.status !== 'pending') throw new Error('Order is not pending');
61
+ this.apply('OrderConfirmed', undefined);
303
62
  }
304
63
 
305
- // Getters
306
- getCustomerId(): EntityId {
64
+ getCustomerId(): string {
307
65
  return this.customerId;
308
66
  }
309
-
310
- getItems(): ReadonlyArray<OrderItem> {
311
- return this.items;
312
- }
313
-
314
- getStatus(): OrderStatus {
67
+ getStatus(): string {
315
68
  return this.status;
316
69
  }
317
-
318
- getTotalAmount(): number {
319
- return this.totalAmount;
320
- }
321
-
322
- getCurrency(): string {
323
- return this.currency;
324
- }
325
70
  }
326
71
 
327
- // Usage
328
- const order = OrderAggregate.create(EntityId.from('customer-123'));
329
-
330
- const addResult = order.addItem(EntityId.from('product-456'), 2, 29.99);
72
+ // Create an order
73
+ const order = Order.create('customer-123');
74
+ order.confirm();
331
75
 
332
- if (addResult.isSuccess()) {
333
- const confirmResult = order.confirm();
334
-
335
- if (confirmResult.isSuccess()) {
336
- console.log('Order confirmed:', order.id.value);
337
- console.log('Events:', order.getUncommittedEvents());
338
- }
339
- }
76
+ // Domain events are collected until committed
77
+ const events = order.getDomainEvents(); // [OrderCreated, OrderConfirmed]
78
+ order.commit(); // clears collected events
340
79
  ```
341
80
 
342
- ### 2. Event-Sourced Aggregate
81
+ ### With capabilities
343
82
 
344
83
  ```typescript
345
84
  import {
346
- EventSourcedAggregate,
347
- EventHandlerMap,
85
+ aggregateBuilder,
86
+ AuditCapability,
87
+ SnapshotCapability,
348
88
  } from '@vytches/ddd-aggregates';
349
89
 
350
- class BankAccountAggregate extends EventSourcedAggregate {
351
- private balance: number = 0;
352
- private status: AccountStatus = AccountStatus.ACTIVE;
353
- private overdraftLimit: number = 0;
354
-
355
- private constructor() {
356
- super();
357
- }
358
-
359
- // Factory method
360
- static create(
361
- accountId: EntityId,
362
- initialDeposit: number,
363
- overdraftLimit: number = 0
364
- ): BankAccountAggregate {
365
- const account = new BankAccountAggregate();
366
-
367
- // Apply creation event
368
- account.apply(
369
- new AccountOpenedEvent({
370
- accountId: accountId.value,
371
- initialDeposit,
372
- overdraftLimit,
373
- openedAt: new Date(),
374
- })
375
- );
376
-
377
- return account;
378
- }
379
-
380
- // Business methods
381
- deposit(amount: number): Result<void, Error> {
382
- if (amount <= 0) {
383
- return Result.fail(new Error('Deposit amount must be positive'));
384
- }
385
-
386
- if (this.status !== AccountStatus.ACTIVE) {
387
- return Result.fail(new Error('Account is not active'));
388
- }
389
-
390
- this.apply(
391
- new MoneyDepositedEvent({
392
- accountId: this.id.value,
393
- amount,
394
- balance: this.balance + amount,
395
- depositedAt: new Date(),
396
- })
397
- );
398
-
399
- return Result.ok();
400
- }
401
-
402
- withdraw(amount: number): Result<void, Error> {
403
- if (amount <= 0) {
404
- return Result.fail(new Error('Withdrawal amount must be positive'));
405
- }
406
-
407
- if (this.status !== AccountStatus.ACTIVE) {
408
- return Result.fail(new Error('Account is not active'));
409
- }
410
-
411
- const newBalance = this.balance - amount;
412
- if (newBalance < -this.overdraftLimit) {
413
- return Result.fail(new Error('Insufficient funds'));
414
- }
415
-
416
- this.apply(
417
- new MoneyWithdrawnEvent({
418
- accountId: this.id.value,
419
- amount,
420
- balance: newBalance,
421
- withdrawnAt: new Date(),
422
- })
423
- );
424
-
425
- return Result.ok();
426
- }
427
-
428
- // Event handlers
429
- protected getEventHandlers(): EventHandlerMap {
430
- return {
431
- AccountOpened: this.onAccountOpened.bind(this),
432
- MoneyDeposited: this.onMoneyDeposited.bind(this),
433
- MoneyWithdrawn: this.onMoneyWithdrawn.bind(this),
434
- };
435
- }
436
-
437
- private onAccountOpened(event: AccountOpenedEvent): void {
438
- this.id = EntityId.from(event.payload.accountId);
439
- this.balance = event.payload.initialDeposit;
440
- this.overdraftLimit = event.payload.overdraftLimit;
441
- this.status = AccountStatus.ACTIVE;
442
- }
443
-
444
- private onMoneyDeposited(event: MoneyDepositedEvent): void {
445
- this.balance = event.payload.balance;
446
- }
447
-
448
- private onMoneyWithdrawn(event: MoneyWithdrawnEvent): void {
449
- this.balance = event.payload.balance;
450
- }
451
-
452
- // Getters
453
- getBalance(): number {
454
- return this.balance;
455
- }
456
-
457
- getStatus(): AccountStatus {
458
- return this.status;
459
- }
460
-
461
- getOverdraftLimit(): number {
462
- return this.overdraftLimit;
463
- }
464
- }
465
-
466
- // Reconstitute from event stream
467
- const events = await eventStore.getEventStream('account-123');
468
- const account = BankAccountAggregate.fromEventStream(events);
469
-
470
- console.log('Current balance:', account.getBalance());
90
+ const order = aggregateBuilder(Order)
91
+ .withCapability(AuditCapability)
92
+ .withCapability(SnapshotCapability)
93
+ .build({ id: EntityId.create(), version: 0 });
471
94
  ```
472
95
 
473
- ### 3. Aggregate with Capabilities
96
+ ### Utility functions
474
97
 
475
98
  ```typescript
476
99
  import {
477
- AggregateRoot,
478
- AuditCapability,
479
- CacheCapability,
480
- ValidationCapability,
100
+ asSnapshotAggregate,
101
+ tryAsSnapshotAggregate,
102
+ hasAllCapabilities,
103
+ getAggregateCapabilities,
481
104
  } from '@vytches/ddd-aggregates';
105
+ import { SnapshotCapability, AuditCapability } from '@vytches/ddd-aggregates';
482
106
 
483
- class ProductAggregate extends AggregateRoot {
484
- private name: string;
485
- private price: number;
486
- private stock: number;
487
- private category: string;
107
+ // Type-safe capability access
108
+ const snapshotable = asSnapshotAggregate(order); // throws if missing
109
+ const maybeSnapshot = tryAsSnapshotAggregate(order); // returns null if missing
488
110
 
489
- private constructor(
490
- id: EntityId,
491
- name: string,
492
- price: number,
493
- stock: number,
494
- category: string
495
- ) {
496
- super(id);
497
- this.name = name;
498
- this.price = price;
499
- this.stock = stock;
500
- this.category = category;
501
- }
502
-
503
- static create(
504
- name: string,
505
- price: number,
506
- initialStock: number,
507
- category: string
508
- ): ProductAggregate {
509
- const product = new ProductAggregate(
510
- EntityId.create(),
511
- name,
512
- price,
513
- initialStock,
514
- category
515
- );
516
-
517
- // Add capabilities
518
- product.addCapability(new AuditCapability());
519
- product.addCapability(new CacheCapability({ ttl: 300 })); // 5 minutes
520
- product.addCapability(new ValidationCapability());
521
-
522
- product.addDomainEvent(
523
- new ProductCreatedEvent({
524
- productId: product.id.value,
525
- name,
526
- price,
527
- stock: initialStock,
528
- category,
529
- })
530
- );
531
-
532
- return product;
533
- }
534
-
535
- updatePrice(newPrice: number): Result<void, Error> {
536
- // Validation capability automatically validates
537
- if (newPrice < 0) {
538
- return Result.fail(new Error('Price cannot be negative'));
539
- }
540
-
541
- const oldPrice = this.price;
542
- this.price = newPrice;
543
-
544
- // Audit capability automatically logs changes
545
- this.addDomainEvent(
546
- new ProductPriceUpdatedEvent({
547
- productId: this.id.value,
548
- oldPrice,
549
- newPrice,
550
- updatedAt: new Date(),
551
- })
552
- );
553
-
554
- return Result.ok();
555
- }
556
-
557
- adjustStock(quantity: number): Result<void, Error> {
558
- const newStock = this.stock + quantity;
559
-
560
- if (newStock < 0) {
561
- return Result.fail(new Error('Insufficient stock'));
562
- }
563
-
564
- this.stock = newStock;
565
-
566
- this.addDomainEvent(
567
- new ProductStockAdjustedEvent({
568
- productId: this.id.value,
569
- adjustment: quantity,
570
- newStock,
571
- adjustedAt: new Date(),
572
- })
573
- );
574
-
575
- return Result.ok();
576
- }
577
-
578
- // Getters
579
- getName(): string {
580
- return this.name;
581
- }
582
-
583
- getPrice(): number {
584
- return this.price;
585
- }
586
-
587
- getStock(): number {
588
- return this.stock;
589
- }
590
-
591
- getCategory(): string {
592
- return this.category;
593
- }
594
- }
595
-
596
- // Usage with capabilities
597
- const product = ProductAggregate.create(
598
- 'Gaming Laptop',
599
- 1299.99,
600
- 50,
601
- 'Electronics'
602
- );
603
-
604
- // Audit capability tracks all changes
605
- product.updatePrice(1199.99);
606
- product.adjustStock(-5);
607
-
608
- // Cache capability optimizes repeated access
609
- const auditCapability = product.getCapability(AuditCapability);
610
- console.log('Audit trail:', auditCapability.getAuditTrail());
611
-
612
- // Validation capability ensures data integrity
613
- const validationCapability = product.getCapability(ValidationCapability);
614
- console.log('Validation rules:', validationCapability.getValidationRules());
111
+ // Introspection
112
+ const hasAll = hasAllCapabilities(order, [SnapshotCapability, AuditCapability]);
113
+ const capabilities = getAggregateCapabilities(order);
615
114
  ```
616
115
 
617
- ## 🔧 Advanced Features
618
-
619
- ### Optimistic Concurrency Control
116
+ ### Non-root entities
620
117
 
621
118
  ```typescript
622
- class OrderAggregate extends AggregateRoot {
623
- // Version is automatically managed by base class
624
-
625
- updateShippingAddress(newAddress: Address): Result<void, Error> {
626
- // Check if aggregate is in correct state
627
- if (this.status !== OrderStatus.CONFIRMED) {
628
- return Result.fail(new Error('Cannot update shipping address'));
629
- }
630
-
631
- // Apply change
632
- this.shippingAddress = newAddress;
119
+ import { Entity } from '@vytches/ddd-aggregates';
120
+ import { EntityId } from '@vytches/ddd-contracts';
633
121
 
634
- // Version is automatically incremented
635
- this.addDomainEvent(
636
- new OrderShippingAddressUpdatedEvent({
637
- orderId: this.id.value,
638
- newAddress: newAddress.toJSON(),
639
- version: this.version + 1,
640
- })
641
- );
642
-
643
- return Result.ok();
644
- }
645
- }
646
-
647
- // Repository handles concurrency conflicts
648
- class OrderRepository {
649
- async save(order: OrderAggregate): Promise<void> {
650
- try {
651
- await this.db.orders.update({
652
- where: {
653
- id: order.id.value,
654
- version: order.version - 1, // Expected version
655
- },
656
- data: {
657
- ...order.toSnapshot(),
658
- version: order.version,
659
- },
660
- });
661
- } catch (error) {
662
- if (error.code === 'P2025') {
663
- // Prisma: Record not found
664
- throw new OptimisticConcurrencyError(
665
- `Order ${order.id.value} was modified by another process`
666
- );
667
- }
668
- throw error;
669
- }
670
- }
671
- }
672
- ```
673
-
674
- ### Snapshot Support
675
-
676
- ```typescript
677
- class OrderAggregate extends AggregateRoot {
678
- // Snapshot creation
679
- toSnapshot(): AggregateSnapshot {
680
- return {
681
- id: this.id.value,
682
- version: this.version,
683
- data: {
684
- customerId: this.customerId.value,
685
- items: this.items.map(item => item.toJSON()),
686
- status: this.status.toString(),
687
- totalAmount: this.totalAmount,
688
- currency: this.currency,
689
- createdAt: this.createdAt.toISOString(),
690
- updatedAt: new Date().toISOString(),
691
- },
692
- };
693
- }
694
-
695
- // Snapshot restoration
696
- fromSnapshot(snapshot: AggregateSnapshot): void {
697
- this.id = EntityId.from(snapshot.id);
698
- this.version = snapshot.version;
699
- this.customerId = EntityId.from(snapshot.data.customerId);
700
- this.items = snapshot.data.items.map(item => OrderItem.fromJSON(item));
701
- this.status = OrderStatus.fromString(snapshot.data.status);
702
- this.totalAmount = snapshot.data.totalAmount;
703
- this.currency = snapshot.data.currency;
704
- this.createdAt = new Date(snapshot.data.createdAt);
705
- this.updatedAt = new Date(snapshot.data.updatedAt);
706
- }
707
- }
708
-
709
- // Automatic snapshot creation
710
- class OrderRepository {
711
- async save(order: OrderAggregate): Promise<void> {
712
- // Save aggregate snapshot
713
- await this.db.orders.upsert({
714
- where: { id: order.id.value },
715
- update: order.toSnapshot(),
716
- create: order.toSnapshot(),
717
- });
718
-
719
- // Save events
720
- const events = order.getUncommittedEvents();
721
- await this.eventStore.append(order.id.value, events);
722
-
723
- // Create snapshot if threshold reached
724
- if (events.length >= this.snapshotThreshold) {
725
- await this.snapshotStore.createSnapshot(
726
- order.id.value,
727
- order.toSnapshot()
728
- );
729
- }
730
- }
731
- }
732
- ```
733
-
734
- ### Invariant Validation
735
-
736
- ```typescript
737
- class OrderAggregate extends AggregateRoot {
738
- // Invariant validation
739
- private validateInvariants(): void {
740
- // Business rules that must always be true
741
- if (this.totalAmount < 0) {
742
- throw new InvariantViolationError('Total amount cannot be negative');
743
- }
744
-
745
- if (this.items.length === 0 && this.status !== OrderStatus.PENDING) {
746
- throw new InvariantViolationError('Non-pending orders must have items');
747
- }
748
-
749
- if (this.status === OrderStatus.CONFIRMED && this.totalAmount === 0) {
750
- throw new InvariantViolationError(
751
- 'Confirmed orders must have positive total'
752
- );
753
- }
754
-
755
- // Validate item consistency
756
- const calculatedTotal = this.items.reduce(
757
- (sum, item) => sum + item.quantity * item.price,
758
- 0
759
- );
760
-
761
- if (Math.abs(calculatedTotal - this.totalAmount) > 0.01) {
762
- throw new InvariantViolationError('Total amount does not match items');
763
- }
764
- }
765
-
766
- // Called automatically before events are applied
767
- protected beforeApplyEvent(event: IDomainEvent): void {
768
- this.validateInvariants();
769
- }
770
-
771
- // Called automatically after events are applied
772
- protected afterApplyEvent(event: IDomainEvent): void {
773
- this.validateInvariants();
774
- }
775
- }
776
- ```
777
-
778
- ## 📚 API Reference
779
-
780
- ### AggregateRoot
781
-
782
- **Base Class:**
783
-
784
- ```typescript
785
- abstract class AggregateRoot {
786
- protected readonly id: EntityId;
787
- protected version: number;
788
-
789
- // Event management
790
- addDomainEvent(event: IDomainEvent): void;
791
- getUncommittedEvents(): IDomainEvent[];
792
- markEventsAsCommitted(): void;
793
- clearEvents(): void;
794
-
795
- // Capability system
796
- addCapability<T extends IAggregateCapability>(capability: T): void;
797
- getCapability<T extends IAggregateCapability>(type: new () => T): T | null;
798
- hasCapability<T extends IAggregateCapability>(type: new () => T): boolean;
799
- removeCapability<T extends IAggregateCapability>(type: new () => T): void;
800
-
801
- // Snapshot support
802
- toSnapshot(): AggregateSnapshot;
803
- fromSnapshot(snapshot: AggregateSnapshot): void;
804
-
805
- // Lifecycle hooks
806
- protected beforeApplyEvent?(event: IDomainEvent): void;
807
- protected afterApplyEvent?(event: IDomainEvent): void;
808
- protected beforeSave?(): void;
809
- protected afterSave?(): void;
810
- }
811
- ```
812
-
813
- ### EventSourcedAggregate
814
-
815
- **Event Sourcing Support:**
816
-
817
- ```typescript
818
- abstract class EventSourcedAggregate extends AggregateRoot {
819
- // Event application
820
- protected apply(event: IDomainEvent): void;
821
-
822
- // Event stream reconstruction
823
- static fromEventStream<T extends EventSourcedAggregate>(
824
- events: IDomainEvent[]
825
- ): T;
826
-
827
- // Event handlers (must be implemented)
828
- protected abstract getEventHandlers(): EventHandlerMap;
829
-
830
- // Replay events
831
- protected replayEvents(events: IDomainEvent[]): void;
832
-
833
- // Get aggregate version from events
834
- protected getVersionFromEvents(events: IDomainEvent[]): number;
835
- }
836
- ```
837
-
838
- ### Built-in Capabilities
839
-
840
- **Audit Capability:**
841
-
842
- ```typescript
843
- class AuditCapability implements IAggregateCapability {
844
- readonly name = 'audit';
845
-
846
- // Configuration
847
- constructor(config?: AuditConfig) {}
848
-
849
- // Audit trail
850
- getAuditTrail(): AuditEntry[];
851
- clearAuditTrail(): void;
852
-
853
- // Hooks
854
- onEventApplied(event: IDomainEvent): void;
855
- onSnapshotCreated(snapshot: AggregateSnapshot): void;
856
- }
857
- ```
858
-
859
- **Cache Capability:**
860
-
861
- ```typescript
862
- class CacheCapability implements IAggregateCapability {
863
- readonly name = 'cache';
864
-
865
- // Configuration
866
- constructor(config: CacheConfig) {}
867
-
868
- // Cache management
869
- get<T>(key: string): T | undefined;
870
- set<T>(key: string, value: T): void;
871
- remove(key: string): void;
872
- clear(): void;
873
-
874
- // Statistics
875
- getStats(): CacheStats;
876
- }
877
- ```
878
-
879
- **Validation Capability:**
880
-
881
- ```typescript
882
- class ValidationCapability implements IAggregateCapability {
883
- readonly name = 'validation';
884
-
885
- // Configuration
886
- constructor(config?: ValidationConfig) {}
887
-
888
- // Validation rules
889
- addRule(rule: ValidationRule): void;
890
- removeRule(ruleName: string): void;
891
- getValidationRules(): ValidationRule[];
892
-
893
- // Validation
894
- validate(aggregate: AggregateRoot): ValidationResult;
895
-
896
- // Hooks
897
- beforeApplyEvent(event: IDomainEvent): void;
898
- }
899
- ```
900
-
901
- ## 🎯 Patterns & Best Practices
902
-
903
- ### Aggregate Design Principles
904
-
905
- 1. **Keep Aggregates Small**
906
-
907
- ```typescript
908
- // Good: Small, focused aggregate
909
- class OrderAggregate extends AggregateRoot {
910
- private items: OrderItem[] = [];
911
- private status: OrderStatus;
912
-
913
- // Only essential order operations
914
- addItem(item: OrderItem): Result<void, Error> {
915
- /* ... */
916
- }
917
- removeItem(itemId: string): Result<void, Error> {
918
- /* ... */
919
- }
920
- confirm(): Result<void, Error> {
921
- /* ... */
922
- }
923
- }
924
-
925
- // Avoid: Large, complex aggregates
926
- class OrderAggregate extends AggregateRoot {
927
- private items: OrderItem[] = [];
928
- private customer: Customer; // Should be separate aggregate
929
- private inventory: Inventory; // Should be separate aggregate
930
- private payments: Payment[]; // Should be separate aggregate
931
- }
932
- ```
933
-
934
- 2. **Use Eventual Consistency Between Aggregates**
935
-
936
- ```typescript
937
- // Good: Eventual consistency via domain events
938
- class OrderAggregate extends AggregateRoot {
939
- confirm(): Result<void, Error> {
940
- this.status = OrderStatus.CONFIRMED;
941
-
942
- // Notify other aggregates via events
943
- this.addDomainEvent(
944
- new OrderConfirmedEvent({
945
- orderId: this.id.value,
946
- customerId: this.customerId.value,
947
- items: this.items.map(item => item.toJSON()),
948
- })
949
- );
950
-
951
- return Result.ok();
952
- }
953
- }
954
-
955
- // Handler in inventory context
956
- @EventHandler(OrderConfirmedEvent)
957
- class InventoryHandler {
958
- async handle(event: OrderConfirmedEvent): Promise<void> {
959
- await this.inventoryService.reserveItems(event.payload.items);
960
- }
961
- }
962
- ```
963
-
964
- 3. **Enforce Invariants at Aggregate Boundaries**
965
-
966
- ```typescript
967
- class OrderAggregate extends AggregateRoot {
968
- private static readonly MAX_ITEMS = 100;
969
- private static readonly MIN_TOTAL = 10.0;
970
-
971
- addItem(item: OrderItem): Result<void, Error> {
972
- // Validate aggregate invariants
973
- if (this.items.length >= OrderAggregate.MAX_ITEMS) {
974
- return Result.fail(
975
- new Error(
976
- `Order cannot have more than ${OrderAggregate.MAX_ITEMS} items`
977
- )
978
- );
979
- }
980
-
981
- const newTotal = this.calculateTotalWithItem(item);
982
- if (
983
- this.status === OrderStatus.PENDING &&
984
- newTotal < OrderAggregate.MIN_TOTAL
985
- ) {
986
- return Result.fail(
987
- new Error(`Order total must be at least ${OrderAggregate.MIN_TOTAL}`)
988
- );
989
- }
990
-
991
- // Apply change
992
- this.items.push(item);
993
- this.recalculateTotal();
994
-
995
- return Result.ok();
996
- }
997
- }
998
- ```
999
-
1000
- ### Event Design Guidelines
1001
-
1002
- 1. **Use Past Tense for Domain Events**
1003
-
1004
- ```typescript
1005
- // Good: Past tense, describes what happened
1006
- class OrderCreatedEvent extends DomainEvent<OrderCreatedData> {}
1007
- class OrderConfirmedEvent extends DomainEvent<OrderConfirmedData> {}
1008
- class OrderCancelledEvent extends DomainEvent<OrderCancelledData> {}
1009
-
1010
- // Avoid: Present tense or imperative
1011
- class CreateOrderEvent extends DomainEvent<OrderData> {}
1012
- class ConfirmOrderEvent extends DomainEvent<OrderData> {}
1013
- ```
1014
-
1015
- 2. **Include Sufficient Data in Events**
1016
-
1017
- ```typescript
1018
- // Good: Rich event data
1019
- class OrderCreatedEvent extends DomainEvent<{
1020
- orderId: string;
1021
- customerId: string;
1022
- items: Array<{
1023
- productId: string;
1024
- quantity: number;
1025
- price: number;
1026
- }>;
1027
- totalAmount: number;
1028
- currency: string;
1029
- createdAt: Date;
1030
- metadata: {
1031
- channel: string;
1032
- promotionCode?: string;
1033
- };
1034
- }> {}
1035
-
1036
- // Avoid: Minimal event data
1037
- class OrderCreatedEvent extends DomainEvent<{
1038
- orderId: string;
1039
- }> {}
1040
- ```
1041
-
1042
- 3. **Version Events for Schema Evolution**
1043
-
1044
- ```typescript
1045
- // Event versioning
1046
- class OrderCreatedEvent extends DomainEvent<OrderCreatedData> {
1047
- static readonly VERSION = 2;
1048
-
1049
- constructor(payload: OrderCreatedData) {
1050
- super('OrderCreated', payload);
1051
- this.version = OrderCreatedEvent.VERSION;
1052
- }
1053
- }
1054
-
1055
- // Event upcasting for version compatibility
1056
- class OrderCreatedEventUpcaster {
1057
- upcast(event: IDomainEvent): IDomainEvent {
1058
- if (event.version === 1) {
1059
- return this.upcastV1ToV2(event);
1060
- }
1061
- return event;
1062
- }
1063
-
1064
- private upcastV1ToV2(event: IDomainEvent): IDomainEvent {
1065
- // Convert v1 to v2 format
1066
- return new OrderCreatedEvent({
1067
- ...event.payload,
1068
- currency: event.payload.currency || 'USD', // Default currency
1069
- });
1070
- }
1071
- }
1072
- ```
1073
-
1074
- ### Testing Strategies
1075
-
1076
- 1. **Unit Test Aggregate Behavior**
1077
-
1078
- ```typescript
1079
- describe('OrderAggregate', () => {
1080
- it('should create order with items', () => {
1081
- // Arrange
1082
- const customerId = EntityId.create();
1083
- const item = new OrderItem(EntityId.create(), 2, 29.99);
1084
-
1085
- // Act
1086
- const order = OrderAggregate.create(customerId);
1087
- const result = order.addItem(item);
1088
-
1089
- // Assert
1090
- expect(result.isSuccess()).toBe(true);
1091
- expect(order.getItems()).toHaveLength(1);
1092
- expect(order.getTotalAmount()).toBe(59.98);
1093
-
1094
- // Verify events
1095
- const events = order.getUncommittedEvents();
1096
- expect(events).toHaveLength(2);
1097
- expect(events[0]).toBeInstanceOf(OrderCreatedEvent);
1098
- expect(events[1]).toBeInstanceOf(OrderItemAddedEvent);
1099
- });
1100
-
1101
- it('should fail to add item with negative quantity', () => {
1102
- // Arrange
1103
- const order = OrderAggregate.create(EntityId.create());
1104
- const item = new OrderItem(EntityId.create(), -1, 29.99);
1105
-
1106
- // Act
1107
- const result = order.addItem(item);
1108
-
1109
- // Assert
1110
- expect(result.isFailure()).toBe(true);
1111
- expect(result.error?.message).toBe('Quantity must be positive');
1112
- expect(order.getItems()).toHaveLength(0);
1113
- });
1114
- });
1115
- ```
1116
-
1117
- 2. **Test Event Sourcing**
1118
-
1119
- ```typescript
1120
- describe('BankAccountAggregate Event Sourcing', () => {
1121
- it('should reconstitute from event stream', () => {
1122
- // Arrange
1123
- const events = [
1124
- new AccountOpenedEvent({
1125
- accountId: 'account-123',
1126
- initialDeposit: 1000,
1127
- overdraftLimit: 500,
1128
- }),
1129
- new MoneyDepositedEvent({
1130
- accountId: 'account-123',
1131
- amount: 250,
1132
- balance: 1250,
1133
- }),
1134
- new MoneyWithdrawnEvent({
1135
- accountId: 'account-123',
1136
- amount: 100,
1137
- balance: 1150,
1138
- }),
1139
- ];
1140
-
1141
- // Act
1142
- const account = BankAccountAggregate.fromEventStream(events);
1143
-
1144
- // Assert
1145
- expect(account.getBalance()).toBe(1150);
1146
- expect(account.getOverdraftLimit()).toBe(500);
1147
- expect(account.getStatus()).toBe(AccountStatus.ACTIVE);
1148
- });
1149
- });
1150
- ```
1151
-
1152
- 3. **Integration Testing with Repository**
1153
-
1154
- ```typescript
1155
- describe('OrderRepository Integration', () => {
1156
- it('should save and retrieve aggregate', async () => {
1157
- // Arrange
1158
- const order = OrderAggregate.create(EntityId.create());
1159
- order.addItem(new OrderItem(EntityId.create(), 1, 99.99));
1160
-
1161
- // Act
1162
- await orderRepository.save(order);
1163
- const retrievedOrder = await orderRepository.findById(order.id);
1164
-
1165
- // Assert
1166
- expect(retrievedOrder).toBeDefined();
1167
- expect(retrievedOrder?.getTotalAmount()).toBe(99.99);
1168
- expect(retrievedOrder?.getItems()).toHaveLength(1);
1169
- });
1170
- });
1171
- ```
1172
-
1173
- ## ⚡ Performance
1174
-
1175
- ### Optimization Strategies
1176
-
1177
- 1. **Snapshot Strategy**
1178
-
1179
- ```typescript
1180
- class OrderRepository {
1181
- private readonly snapshotThreshold = 20;
1182
-
1183
- async save(order: OrderAggregate): Promise<void> {
1184
- // Always save current state
1185
- await this.saveSnapshot(order);
1186
-
1187
- // Save events
1188
- const events = order.getUncommittedEvents();
1189
- await this.eventStore.append(order.id.value, events);
1190
-
1191
- // Create snapshot if threshold reached
1192
- const eventCount = await this.eventStore.getEventCount(order.id.value);
1193
- if (eventCount >= this.snapshotThreshold) {
1194
- await this.snapshotStore.createSnapshot(
1195
- order.id.value,
1196
- order.toSnapshot()
1197
- );
1198
-
1199
- // Optional: Archive old events
1200
- await this.eventStore.archiveEvents(order.id.value, eventCount - 10);
1201
- }
1202
- }
1203
- }
1204
- ```
1205
-
1206
- 2. **Lazy Loading**
1207
-
1208
- ```typescript
1209
- class OrderAggregate extends AggregateRoot {
1210
- private _items: OrderItem[] | null = null;
1211
-
1212
- getItems(): OrderItem[] {
1213
- if (this._items === null) {
1214
- this._items = this.loadItemsFromSnapshot();
1215
- }
1216
- return this._items;
1217
- }
1218
-
1219
- private loadItemsFromSnapshot(): OrderItem[] {
1220
- // Load from snapshot or reconstruct from events
1221
- return this.snapshotData.items.map(item => OrderItem.fromJSON(item));
1222
- }
1223
- }
1224
- ```
1225
-
1226
- 3. **Batching Operations**
1227
-
1228
- ```typescript
1229
- class OrderAggregate extends AggregateRoot {
1230
- addItems(items: OrderItem[]): Result<void, Error> {
1231
- // Validate all items first
1232
- for (const item of items) {
1233
- const validation = this.validateItem(item);
1234
- if (validation.isFailure()) {
1235
- return validation;
1236
- }
1237
- }
1238
-
1239
- // Apply all items in batch
1240
- for (const item of items) {
1241
- this.items.push(item);
1242
- this.totalAmount += item.quantity * item.price;
1243
- }
1244
-
1245
- // Single event for batch operation
1246
- this.addDomainEvent(
1247
- new OrderItemsBatchAddedEvent({
1248
- orderId: this.id.value,
1249
- items: items.map(item => item.toJSON()),
1250
- newTotal: this.totalAmount,
1251
- })
1252
- );
1253
-
1254
- return Result.ok();
1255
- }
1256
- }
1257
- ```
1258
-
1259
- ### Memory Management
1260
-
1261
- ```typescript
1262
- class AggregateRoot {
1263
- private eventCache: Map<string, IDomainEvent> = new Map();
1264
- private readonly maxCacheSize = 1000;
1265
-
1266
- addDomainEvent(event: IDomainEvent): void {
1267
- // Manage cache size
1268
- if (this.eventCache.size >= this.maxCacheSize) {
1269
- // Remove oldest events
1270
- const oldestKey = this.eventCache.keys().next().value;
1271
- this.eventCache.delete(oldestKey);
1272
- }
1273
-
1274
- this.eventCache.set(event.id, event);
122
+ // Use Entity for inner aggregate objects like OrderLine, Address
123
+ class OrderLine extends Entity<string> {
124
+ constructor(
125
+ id: EntityId<string>,
126
+ private sku: string,
127
+ private quantity: number
128
+ ) {
129
+ super({ id });
1275
130
  }
1276
-
1277
- getUncommittedEvents(): IDomainEvent[] {
1278
- return Array.from(this.eventCache.values());
131
+ getSku(): string {
132
+ return this.sku;
1279
133
  }
1280
-
1281
- markEventsAsCommitted(): void {
1282
- this.eventCache.clear();
134
+ getQuantity(): number {
135
+ return this.quantity;
1283
136
  }
1284
137
  }
1285
138
  ```
1286
139
 
1287
- ## 🧪 Testing
1288
-
1289
- ### Aggregate Testing Utilities
1290
-
1291
- ```typescript
1292
- import { AggregateTestBuilder } from '@vytches/ddd-aggregates/testing';
1293
-
1294
- // Aggregate builder for tests
1295
- const orderBuilder = new AggregateTestBuilder(OrderAggregate)
1296
- .withCustomer('customer-123')
1297
- .withItems([
1298
- { productId: 'product-1', quantity: 2, price: 29.99 },
1299
- { productId: 'product-2', quantity: 1, price: 49.99 },
1300
- ])
1301
- .withStatus(OrderStatus.PENDING)
1302
- .build();
1303
-
1304
- // Event stream testing
1305
- const account = new AggregateTestBuilder(BankAccountAggregate)
1306
- .fromEventStream([
1307
- new AccountOpenedEvent({ accountId: 'account-123', initialDeposit: 1000 }),
1308
- new MoneyDepositedEvent({
1309
- accountId: 'account-123',
1310
- amount: 500,
1311
- balance: 1500,
1312
- }),
1313
- ])
1314
- .build();
1315
-
1316
- // Snapshot testing
1317
- const order = new AggregateTestBuilder(OrderAggregate)
1318
- .fromSnapshot({
1319
- id: 'order-123',
1320
- version: 5,
1321
- data: {
1322
- /* snapshot data */
1323
- },
1324
- })
1325
- .build();
1326
- ```
1327
-
1328
- ### Event Testing
1329
-
1330
- ```typescript
1331
- import { EventTestHarness } from '@vytches/ddd-aggregates/testing';
1332
-
1333
- describe('Order Events', () => {
1334
- let eventHarness: EventTestHarness;
1335
-
1336
- beforeEach(() => {
1337
- eventHarness = new EventTestHarness();
1338
- });
1339
-
1340
- it('should emit correct events when adding item', () => {
1341
- // Arrange
1342
- const order = OrderAggregate.create(EntityId.create());
1343
- const item = new OrderItem(EntityId.create(), 1, 99.99);
1344
-
1345
- // Act
1346
- order.addItem(item);
1347
-
1348
- // Assert
1349
- eventHarness.assertEventEmitted(order, OrderItemAddedEvent, {
1350
- orderId: order.id.value,
1351
- productId: item.productId.value,
1352
- quantity: 1,
1353
- price: 99.99,
1354
- });
1355
- });
1356
- });
1357
- ```
1358
-
1359
- ## 🤝 Contributing
140
+ ## API Reference
1360
141
 
1361
- We welcome contributions! Please see our
1362
- [Contributing Guide](../../CONTRIBUTING.md) for details.
142
+ ### Core Classes
1363
143
 
1364
- ### Development Setup
144
+ | Export | Kind | Description |
145
+ | -------------------- | -------- | ------------------------------------------------------------------------------------------------------ |
146
+ | `AggregateRoot<TId>` | class | Base aggregate root; provides identity, versioning, event collection and event sourcing reconstitution |
147
+ | `Entity<TId>` | class | Base class for non-root domain entities; identity-based equality, no events |
148
+ | `AggregateBuilder` | class | Fluent builder for aggregates with capabilities |
149
+ | `aggregateBuilder` | function | Factory function shorthand for `AggregateBuilder` |
150
+ | `AggregateError` | class | Error thrown for aggregate-specific failures |
1365
151
 
1366
- ```bash
1367
- # Clone repository
1368
- git clone https://github.com/vytches/ddd.git
1369
- cd ddd
1370
-
1371
- # Install dependencies
1372
- pnpm install
1373
-
1374
- # Build aggregates package
1375
- pnpm build --filter=@vytches/ddd-aggregates
1376
-
1377
- # Run tests
1378
- pnpm test --filter=@vytches/ddd-aggregates
1379
-
1380
- # Run in development mode
1381
- pnpm dev --filter=@vytches/ddd-aggregates
1382
- ```
1383
-
1384
- ## 📄 License
1385
-
1386
- This project is licensed under the MIT License - see the
1387
- [LICENSE](../../LICENSE) file for details.
1388
-
1389
- ---
1390
-
1391
- **Part of the [@vytches/ddd-core](https://github.com/vytches/ddd) ecosystem**
152
+ ### Capabilities
1392
153
 
1393
- For more information, visit the [main documentation](../../README.md).
154
+ | Export | Kind | Description |
155
+ | ------------------------- | ----- | -------------------------------------------------------- |
156
+ | `AuditCapability` | class | Records who created/modified the aggregate and when |
157
+ | `EventSourcingCapability` | class | Enables event sourcing reconstitution from stored events |
158
+ | `SnapshotCapability` | class | Enables taking and restoring state snapshots |
159
+ | `VersioningCapability` | class | Manages optimistic concurrency version tracking |
160
+
161
+ ### Capability Utilities
162
+
163
+ | Export | Kind | Description |
164
+ | ---------------------------------- | -------- | ------------------------------------------------------------- |
165
+ | `asSnapshotAggregate(agg)` | function | Cast to snapshot-capable aggregate; throws if missing |
166
+ | `tryAsSnapshotAggregate(agg)` | function | Cast to snapshot-capable aggregate; returns `null` if missing |
167
+ | `asVersioningAggregate(agg)` | function | Cast to versioning-capable aggregate; throws if missing |
168
+ | `tryAsVersioningAggregate(agg)` | function | Returns `null` if versioning capability absent |
169
+ | `asAuditAggregate(agg)` | function | Cast to audit-capable aggregate; throws if missing |
170
+ | `tryAsAuditAggregate(agg)` | function | Returns `null` if audit capability absent |
171
+ | `asEventSourcingAggregate(agg)` | function | Cast to event-sourcing aggregate; throws if missing |
172
+ | `tryAsEventSourcingAggregate(agg)` | function | Returns `null` if event sourcing capability absent |
173
+ | `getAggregateCapabilities(agg)` | function | Returns all capabilities attached to an aggregate |
174
+ | `hasAllCapabilities(agg, caps)` | function | Returns `true` if aggregate has all listed capabilities |
175
+
176
+ ### Interfaces
177
+
178
+ | Export | Kind | Description |
179
+ | ---------------------------------- | --------- | ------------------------------------------ |
180
+ | `IAggregateRoot` | interface | Full contract for aggregate roots |
181
+ | `IAggregateCapability` | interface | Base capability contract |
182
+ | `IAggregateConstructorParams<TId>` | interface | Constructor parameter shape for aggregates |
183
+ | `IAggregateEventHandler` | interface | Event handler registration contract |
184
+ | `IAggregateBuilder` | interface | Builder contract |
185
+
186
+ ### Utility Types
187
+
188
+ | Export | Kind | Description |
189
+ | -------------------------------------- | ---- | ---------------------------------------------- |
190
+ | `AggregateWithSnapshotCapability` | type | Aggregate narrowed to snapshot-capable shape |
191
+ | `AggregateWithVersioningCapability` | type | Aggregate narrowed to versioning-capable shape |
192
+ | `AggregateWithAuditCapability` | type | Aggregate narrowed to audit-capable shape |
193
+ | `AggregateWithEventSourcingCapability` | type | Aggregate narrowed to event-sourcing shape |
194
+
195
+ ## Package boundaries
196
+
197
+ `@vytches/ddd-aggregates` depends on:
198
+
199
+ - `@vytches/ddd-contracts` — base interfaces and `EntityId`
200
+ - `@vytches/ddd-domain-primitives` — error types
201
+ - `@vytches/ddd-value-objects` — enhanced `EntityId`
202
+ - `@vytches/ddd-logging` — internal logging
203
+
204
+ ## License
205
+
206
+ MIT