@shirudo/ddd-kit 1.0.0-rc.5 → 1.0.0-rc.6
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 +13 -16
- package/dist/index.d.ts +152 -133
- package/dist/index.js +61 -52
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -103,9 +103,9 @@ The Aggregate Root is an Entity (the parent Entity of the aggregate) that repres
|
|
|
103
103
|
|
|
104
104
|
The library provides:
|
|
105
105
|
|
|
106
|
-
- **`IAggregateRoot<TId
|
|
106
|
+
- **`IAggregateRoot<TId, TEvent?>`** - Interface for Aggregate Root Entities. The Aggregate Root is an Entity with identity (id), version for optimistic concurrency control, and a `pendingEvents` list of domain events recorded but not yet flushed. Both aggregate flavours (state-stored and event-sourced) expose `pendingEvents` under the same name, so a generic Repository.save() can harvest them uniformly.
|
|
107
107
|
|
|
108
|
-
- **`AggregateRoot<TState, TId, TEvent?>`** - Base class for creating Aggregate Root Entities without Event Sourcing. Implements `IAggregateRoot<TId>`. The optional `TEvent` parameter (defaults to `
|
|
108
|
+
- **`AggregateRoot<TState, TId, TEvent?>`** - Base class for creating Aggregate Root Entities without Event Sourcing. Implements `IAggregateRoot<TId, TEvent>`. The optional `TEvent` parameter (defaults to `never`) enables type-safe domain events — only aggregates that specify it can record events at all. Provides ID and version management, state management, pending-event tracking, and snapshot support.
|
|
109
109
|
|
|
110
110
|
- **`EventSourcedAggregate<TState, TEvent, TId>`** - Base class for Event-Sourced Aggregate Roots. Extends `Entity` directly (not `AggregateRoot`) so that state changes can only happen through event handlers via `apply()`. Provides event tracking, event validation, history replay, and snapshot support.
|
|
111
111
|
|
|
@@ -301,7 +301,7 @@ class Order extends AggregateRoot<OrderState, OrderId, OrderDomainEvent> {
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
// order.
|
|
304
|
+
// order.pendingEvents is ReadonlyArray<OrderDomainEvent> — no cast needed
|
|
305
305
|
// order.addDomainEvent({ type: "WrongEvent" }) → compile error
|
|
306
306
|
```
|
|
307
307
|
|
|
@@ -431,13 +431,10 @@ order.confirm();
|
|
|
431
431
|
order.ship("TRACK-789");
|
|
432
432
|
|
|
433
433
|
// Access pending events
|
|
434
|
-
console.log(order.pendingEvents);
|
|
435
|
-
|
|
436
|
-
//
|
|
437
|
-
console.log(order.
|
|
438
|
-
console.log(order.getEventCount()); // 3
|
|
439
|
-
console.log(order.getLatestEvent()?.type); // "OrderShipped"
|
|
440
|
-
console.log(order.version); // 3 (automatically bumped)
|
|
434
|
+
console.log(order.pendingEvents); // Array of events not yet persisted
|
|
435
|
+
console.log(order.pendingEvents.length); // 3
|
|
436
|
+
console.log(order.pendingEvents.at(-1)?.type); // "OrderShipped"
|
|
437
|
+
console.log(order.version); // 3 (automatically bumped)
|
|
441
438
|
```
|
|
442
439
|
|
|
443
440
|
### Aggregate Features: Snapshots and Configuration
|
|
@@ -675,7 +672,7 @@ const createOrderHandler: CommandHandler<CreateOrderCommand, string> = async (
|
|
|
675
672
|
|
|
676
673
|
return {
|
|
677
674
|
result: order.id,
|
|
678
|
-
|
|
675
|
+
aggregates: [order],
|
|
679
676
|
};
|
|
680
677
|
}
|
|
681
678
|
);
|
|
@@ -1139,9 +1136,9 @@ This package is written in TypeScript and provides full type definitions. All ty
|
|
|
1139
1136
|
Key exports include:
|
|
1140
1137
|
- `vo()`, `voEquals()`, `voEqualsExcept()`, `voWithValidation()` - Value Object utilities (`voWithValidation` is for the App-Service boundary; Domain construction goes through the `ValueObject` base class which throws via `validate()`)
|
|
1141
1138
|
- `IAggregateRoot<TId>` - Marker interface for Aggregate Root Entities
|
|
1142
|
-
- `AggregateRoot<TState, TId, TEvent?>` - Base class for creating Aggregate Root Entities without Event Sourcing (extends `Entity`, implements `IAggregateRoot<TId>`). Optional `TEvent` parameter enables type-safe domain events
|
|
1139
|
+
- `AggregateRoot<TState, TId, TEvent?>` - Base class for creating Aggregate Root Entities without Event Sourcing (extends `Entity`, implements `IAggregateRoot<TId, TEvent>`). Optional `TEvent` parameter enables type-safe domain events
|
|
1143
1140
|
- `EventSourcedAggregate<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Roots (extends `Entity`, implements `IEventSourcedAggregate<TId, TEvent>`)
|
|
1144
|
-
- `AggregateConfig
|
|
1141
|
+
- `AggregateConfig` - Configuration interface for `AggregateRoot` (controls per-call `setState` version-bump behavior)
|
|
1145
1142
|
- `AggregateSnapshot<TState>` - Snapshot interface for performance optimization
|
|
1146
1143
|
- `sameVersion()` - Optimistic concurrency check (same ID and version)
|
|
1147
1144
|
- `Entity<TState, TId>` - Base class for entities with state and business logic
|
|
@@ -1286,7 +1283,7 @@ class CreateOrderHandler implements CommandHandler<CreateOrderCommand, OrderId>
|
|
|
1286
1283
|
|
|
1287
1284
|
// 3. Save
|
|
1288
1285
|
await this.repository.save(order);
|
|
1289
|
-
await this.eventBus.publish(order.
|
|
1286
|
+
await this.eventBus.publish(order.pendingEvents);
|
|
1290
1287
|
|
|
1291
1288
|
return ok(order.id);
|
|
1292
1289
|
// 4. Aggregate is garbage collected when method returns
|
|
@@ -1432,7 +1429,7 @@ async function createOrderCommand(cmd: CreateOrderCommand) {
|
|
|
1432
1429
|
|
|
1433
1430
|
return {
|
|
1434
1431
|
result: order.id,
|
|
1435
|
-
|
|
1432
|
+
aggregates: [order], // withCommit harvests pendingEvents and dispatches
|
|
1436
1433
|
};
|
|
1437
1434
|
}); // Commits or rollbacks everything
|
|
1438
1435
|
}
|
|
@@ -1493,7 +1490,7 @@ class OrderService {
|
|
|
1493
1490
|
}
|
|
1494
1491
|
|
|
1495
1492
|
await this.repository.save(order);
|
|
1496
|
-
await this.eventBus.publish(order.
|
|
1493
|
+
await this.eventBus.publish(order.pendingEvents);
|
|
1497
1494
|
|
|
1498
1495
|
return ok(order.id);
|
|
1499
1496
|
// order is garbage collected here
|
package/dist/index.d.ts
CHANGED
|
@@ -189,6 +189,13 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
189
189
|
*/
|
|
190
190
|
metadata?: EventMetadata;
|
|
191
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Upper-bound alias for "any `DomainEvent` shape". Use as a generic
|
|
194
|
+
* constraint when a type parameter should accept any concrete event
|
|
195
|
+
* union. The `unknown` payload is the upper bound — concrete unions
|
|
196
|
+
* still narrow via `Extract<Evt, { type: K }>` at the use-site.
|
|
197
|
+
*/
|
|
198
|
+
type AnyDomainEvent = DomainEvent<string, unknown>;
|
|
192
199
|
/**
|
|
193
200
|
* Shared option bag for the `createDomainEvent*` factories.
|
|
194
201
|
*/
|
|
@@ -263,7 +270,7 @@ declare function createDomainEventWithMetadata<T extends string, P>(type: T, pay
|
|
|
263
270
|
* );
|
|
264
271
|
* ```
|
|
265
272
|
*/
|
|
266
|
-
declare function copyMetadata(sourceEvent:
|
|
273
|
+
declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
|
|
267
274
|
/**
|
|
268
275
|
* Merges multiple metadata objects into one.
|
|
269
276
|
* Later metadata objects override earlier ones for the same keys.
|
|
@@ -502,7 +509,7 @@ declare function sameEntity<TId extends Id<string>>(a: Identifiable<TId>, b: Ide
|
|
|
502
509
|
* // item is { id: itemId1, productId: "prod-1", quantity: 2 }
|
|
503
510
|
* ```
|
|
504
511
|
*/
|
|
505
|
-
declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
512
|
+
declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): T | undefined;
|
|
506
513
|
/**
|
|
507
514
|
* Checks if an entity with the given ID exists in the collection.
|
|
508
515
|
*
|
|
@@ -520,7 +527,7 @@ declare function findEntityById<TId extends Id<string>, T extends Identifiable<T
|
|
|
520
527
|
* hasEntityId(items, itemId2); // false
|
|
521
528
|
* ```
|
|
522
529
|
*/
|
|
523
|
-
declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
530
|
+
declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): boolean;
|
|
524
531
|
/**
|
|
525
532
|
* Removes an entity with the given ID from the collection.
|
|
526
533
|
* Returns a new array without the entity.
|
|
@@ -540,7 +547,7 @@ declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>
|
|
|
540
547
|
* // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
|
|
541
548
|
* ```
|
|
542
549
|
*/
|
|
543
|
-
declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
550
|
+
declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId): T[];
|
|
544
551
|
/**
|
|
545
552
|
* Updates an entity with the given ID in the collection.
|
|
546
553
|
* Returns a new array with the updated entity.
|
|
@@ -564,7 +571,7 @@ declare function removeEntityById<TId extends Id<string>, T extends Identifiable
|
|
|
564
571
|
* // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
|
|
565
572
|
* ```
|
|
566
573
|
*/
|
|
567
|
-
declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
574
|
+
declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId, updater: (entity: T) => T): T[];
|
|
568
575
|
/**
|
|
569
576
|
* Replaces an entity with the given ID in the collection.
|
|
570
577
|
* Returns a new array with the replaced entity.
|
|
@@ -588,7 +595,7 @@ declare function updateEntityById<TId extends Id<string>, T extends Identifiable
|
|
|
588
595
|
* });
|
|
589
596
|
* ```
|
|
590
597
|
*/
|
|
591
|
-
declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
598
|
+
declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>, id: TId, replacement: T): T[];
|
|
592
599
|
/**
|
|
593
600
|
* Extracts all IDs from a collection of entities.
|
|
594
601
|
*
|
|
@@ -606,7 +613,7 @@ declare function replaceEntityById<TId extends Id<string>, T extends Identifiabl
|
|
|
606
613
|
* // ids is [itemId1, itemId2]
|
|
607
614
|
* ```
|
|
608
615
|
*/
|
|
609
|
-
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: T
|
|
616
|
+
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>): TId[];
|
|
610
617
|
|
|
611
618
|
/**
|
|
612
619
|
* Marker interface for Aggregate Roots.
|
|
@@ -635,7 +642,7 @@ declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(
|
|
|
635
642
|
* }
|
|
636
643
|
* ```
|
|
637
644
|
*/
|
|
638
|
-
interface IAggregateRoot<TId extends Id<string
|
|
645
|
+
interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
|
|
639
646
|
/**
|
|
640
647
|
* Unique identifier of the aggregate root entity.
|
|
641
648
|
*/
|
|
@@ -646,11 +653,25 @@ interface IAggregateRoot<TId extends Id<string>> {
|
|
|
646
653
|
* This version applies to the entire aggregate, including all child entities.
|
|
647
654
|
*/
|
|
648
655
|
readonly version: Version;
|
|
656
|
+
/**
|
|
657
|
+
* Read-only list of domain events recorded on this aggregate that have
|
|
658
|
+
* not yet been flushed to the outbox / persistence layer. Both state-
|
|
659
|
+
* stored (`AggregateRoot`) and event-sourced (`EventSourcedAggregate`)
|
|
660
|
+
* aggregates expose them under the same name, so Repository.save() can
|
|
661
|
+
* harvest them uniformly without branching on the aggregate flavour.
|
|
662
|
+
*/
|
|
663
|
+
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
664
|
+
/**
|
|
665
|
+
* Clears the pending-event list. Called by `markPersisted` after a
|
|
666
|
+
* successful write — the events have been handed off to the outbox
|
|
667
|
+
* / event store and are no longer the aggregate's responsibility.
|
|
668
|
+
*/
|
|
669
|
+
clearPendingEvents(): void;
|
|
649
670
|
/**
|
|
650
671
|
* Post-save hook: a `Repository.save()` implementation calls this with
|
|
651
672
|
* the persisted version after a successful write to push the new
|
|
652
|
-
* version back into the aggregate and clear
|
|
653
|
-
*
|
|
673
|
+
* version back into the aggregate and clear pendingEvents (they are
|
|
674
|
+
* now safely on the write side / in the outbox).
|
|
654
675
|
*
|
|
655
676
|
* Required by the interface so a Repository implementation can call it
|
|
656
677
|
* via the published `IAggregateRoot` contract without taking the
|
|
@@ -665,17 +686,14 @@ interface IAggregateRoot<TId extends Id<string>> {
|
|
|
665
686
|
*/
|
|
666
687
|
interface AggregateConfig {
|
|
667
688
|
/**
|
|
668
|
-
* Whether `setState()` should bump the version automatically
|
|
689
|
+
* Whether `setState()` should bump the version automatically when the
|
|
690
|
+
* caller omits the per-call `bumpVersion` argument.
|
|
669
691
|
*
|
|
670
|
-
* Defaults to **`false`**
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
* (Contrast with `EventSourcedAggregate`, which defaults this to
|
|
677
|
-
* `true` because every event-sourced state change is per definition a
|
|
678
|
-
* versioned commit.)
|
|
692
|
+
* Defaults to **`false`** — `setState()` already takes an explicit
|
|
693
|
+
* `bumpVersion` argument per call, so the config is just the default
|
|
694
|
+
* the per-call argument falls back to. Set to `true` only if you have
|
|
695
|
+
* a subclass that never passes `bumpVersion` and you want every state
|
|
696
|
+
* change to advance the version anyway.
|
|
679
697
|
*/
|
|
680
698
|
autoVersionBump?: boolean;
|
|
681
699
|
}
|
|
@@ -701,7 +719,7 @@ interface AggregateConfig {
|
|
|
701
719
|
*
|
|
702
720
|
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
703
721
|
* @template TId - The type of the aggregate root identifier
|
|
704
|
-
* @template TEvent - The type of domain events recorded by this aggregate (
|
|
722
|
+
* @template TEvent - The type of domain events recorded by this aggregate. Defaults to `never` — aggregates without a declared event type cannot emit events (emitting any event becomes a compile error). Supply a concrete event union to opt in.
|
|
705
723
|
*
|
|
706
724
|
* @example
|
|
707
725
|
* ```typescript
|
|
@@ -717,28 +735,28 @@ interface AggregateConfig {
|
|
|
717
735
|
* }
|
|
718
736
|
* ```
|
|
719
737
|
*/
|
|
720
|
-
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId> {
|
|
738
|
+
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
|
|
721
739
|
private _version;
|
|
722
740
|
get version(): Version;
|
|
723
741
|
protected setVersion(version: Version): void;
|
|
724
742
|
private readonly _config;
|
|
725
743
|
private readonly _autoVersionBump;
|
|
726
|
-
private
|
|
744
|
+
private _pendingEvents;
|
|
727
745
|
/**
|
|
728
|
-
*
|
|
729
|
-
*
|
|
746
|
+
* Read-only list of domain events recorded on this aggregate that have
|
|
747
|
+
* not yet been flushed to the outbox / persistence layer.
|
|
730
748
|
*/
|
|
731
|
-
get
|
|
749
|
+
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
732
750
|
/**
|
|
733
|
-
* Clears the list
|
|
734
|
-
*
|
|
751
|
+
* Clears the pending-event list. Call this after the events have been
|
|
752
|
+
* dispatched (typically `markPersisted` handles it for you).
|
|
735
753
|
*/
|
|
736
|
-
|
|
754
|
+
clearPendingEvents(): void;
|
|
737
755
|
/**
|
|
738
756
|
* Post-save hook called by a `Repository.save()` implementation to push
|
|
739
|
-
* the persisted version back into the in-memory aggregate and clear
|
|
740
|
-
*
|
|
741
|
-
*
|
|
757
|
+
* the persisted version back into the in-memory aggregate and clear
|
|
758
|
+
* pendingEvents (they are now safely on the write side / in the
|
|
759
|
+
* outbox).
|
|
742
760
|
*
|
|
743
761
|
* Use this so `save()` can keep its `Promise<void>` return type: the
|
|
744
762
|
* caller holds the aggregate reference, which is up to date after this
|
|
@@ -972,11 +990,7 @@ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyC
|
|
|
972
990
|
* @template TId - The type of the aggregate root identifier
|
|
973
991
|
* @template TEvent - The union type of all domain events
|
|
974
992
|
*/
|
|
975
|
-
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends
|
|
976
|
-
/**
|
|
977
|
-
* Returns a read-only list of new, not-yet-persisted events.
|
|
978
|
-
*/
|
|
979
|
-
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
993
|
+
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
|
|
980
994
|
/**
|
|
981
995
|
* Reconstitutes the aggregate from an event history. Returns `Result`
|
|
982
996
|
* because event-stream corruption is an expected recoverable failure
|
|
@@ -984,45 +998,9 @@ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEv
|
|
|
984
998
|
*
|
|
985
999
|
* @param history - An ordered list of past events
|
|
986
1000
|
*/
|
|
987
|
-
loadFromHistory(history: TEvent
|
|
988
|
-
/**
|
|
989
|
-
* Clears the list of pending events.
|
|
990
|
-
*/
|
|
991
|
-
clearPendingEvents(): void;
|
|
992
|
-
/**
|
|
993
|
-
* Checks if the aggregate has any pending events.
|
|
994
|
-
*/
|
|
995
|
-
hasPendingEvents(): boolean;
|
|
996
|
-
/**
|
|
997
|
-
* Returns the number of pending events.
|
|
998
|
-
*/
|
|
999
|
-
getEventCount(): number;
|
|
1000
|
-
/**
|
|
1001
|
-
* Returns the latest pending event, if any.
|
|
1002
|
-
*/
|
|
1003
|
-
getLatestEvent(): TEvent | undefined;
|
|
1004
|
-
}
|
|
1005
|
-
type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
|
|
1006
|
-
/**
|
|
1007
|
-
* Configuration options for EventSourcedAggregate behavior.
|
|
1008
|
-
*/
|
|
1009
|
-
interface EventSourcedAggregateConfig {
|
|
1010
|
-
/**
|
|
1011
|
-
* Whether `apply()` should bump the version per event.
|
|
1012
|
-
*
|
|
1013
|
-
* Defaults to **`true`** for `EventSourcedAggregate` — each applied
|
|
1014
|
-
* event is by definition a versioned state change, so the canonical
|
|
1015
|
-
* event-sourcing pattern is "one event = one version bump". Set to
|
|
1016
|
-
* `false` only if your event store assigns version numbers itself
|
|
1017
|
-
* and you want the aggregate to track them via `bumpVersion()` /
|
|
1018
|
-
* `setVersion()` calls instead.
|
|
1019
|
-
*
|
|
1020
|
-
* (Contrast with `AggregateRoot`, which defaults this to `false`
|
|
1021
|
-
* because its `setState()` already takes a per-call `bumpVersion`
|
|
1022
|
-
* argument.)
|
|
1023
|
-
*/
|
|
1024
|
-
autoVersionBump?: boolean;
|
|
1001
|
+
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1025
1002
|
}
|
|
1003
|
+
type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
|
|
1026
1004
|
/**
|
|
1027
1005
|
* Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
|
|
1028
1006
|
*
|
|
@@ -1072,12 +1050,11 @@ interface EventSourcedAggregateConfig {
|
|
|
1072
1050
|
* }
|
|
1073
1051
|
* ```
|
|
1074
1052
|
*/
|
|
1075
|
-
declare abstract class EventSourcedAggregate<TState, TEvent extends
|
|
1053
|
+
declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends Entity<TState, TId> implements IEventSourcedAggregate<TId, TEvent> {
|
|
1076
1054
|
private _version;
|
|
1077
1055
|
get version(): Version;
|
|
1078
1056
|
private setVersion;
|
|
1079
1057
|
private _pendingEvents;
|
|
1080
|
-
private readonly _autoVersionBump;
|
|
1081
1058
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
1082
1059
|
clearPendingEvents(): void;
|
|
1083
1060
|
/**
|
|
@@ -1087,7 +1064,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1087
1064
|
* `save()` keep its `Promise<void>` return type.
|
|
1088
1065
|
*/
|
|
1089
1066
|
markPersisted(version: Version): void;
|
|
1090
|
-
protected constructor(id: TId, initialState: TState
|
|
1067
|
+
protected constructor(id: TId, initialState: TState);
|
|
1091
1068
|
/**
|
|
1092
1069
|
* Validates an event before it is applied. Default is no-op.
|
|
1093
1070
|
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
@@ -1123,11 +1100,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1123
1100
|
* resolved via the (statically-sound) `handlers` map.
|
|
1124
1101
|
*/
|
|
1125
1102
|
private dispatchAndCommit;
|
|
1126
|
-
/**
|
|
1127
|
-
* Manually bumps the aggregate version.
|
|
1128
|
-
* Only needed if `autoVersionBump` is disabled.
|
|
1129
|
-
*/
|
|
1130
|
-
protected bumpVersion(): void;
|
|
1131
1103
|
/**
|
|
1132
1104
|
* Reconstitutes the aggregate from an event history. Catches `DomainError`
|
|
1133
1105
|
* thrown during replay and returns it as an `Err` — this is the
|
|
@@ -1139,10 +1111,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1139
1111
|
* an aggregate already at v=1 (e.g. after a creation event) loading
|
|
1140
1112
|
* 2 events ends at v=3, not v=2.
|
|
1141
1113
|
*/
|
|
1142
|
-
loadFromHistory(history: TEvent
|
|
1143
|
-
hasPendingEvents(): boolean;
|
|
1144
|
-
getEventCount(): number;
|
|
1145
|
-
getLatestEvent(): TEvent | undefined;
|
|
1114
|
+
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1146
1115
|
/**
|
|
1147
1116
|
* Creates a snapshot of the current aggregate state.
|
|
1148
1117
|
*/
|
|
@@ -1157,7 +1126,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1157
1126
|
* aggregate is rolled back to its pre-call state + version. Partial
|
|
1158
1127
|
* restoration is never observable to the caller.
|
|
1159
1128
|
*/
|
|
1160
|
-
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent
|
|
1129
|
+
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1161
1130
|
/**
|
|
1162
1131
|
* A map of event types to their corresponding handlers.
|
|
1163
1132
|
* Subclasses MUST implement this property.
|
|
@@ -1456,9 +1425,7 @@ type EventHandler<Evt> = (event: Evt) => Promise<void> | void;
|
|
|
1456
1425
|
* await bus.publish([orderCreatedEvent, orderShippedEvent]);
|
|
1457
1426
|
* ```
|
|
1458
1427
|
*/
|
|
1459
|
-
interface EventBus<Evt extends {
|
|
1460
|
-
type: string;
|
|
1461
|
-
}> {
|
|
1428
|
+
interface EventBus<Evt extends AnyDomainEvent> {
|
|
1462
1429
|
/**
|
|
1463
1430
|
* Publishes events to all subscribed handlers.
|
|
1464
1431
|
*
|
|
@@ -1551,7 +1518,7 @@ interface OnceOptions {
|
|
|
1551
1518
|
* own `eventId`, generate its own UUID, use the row's auto-increment
|
|
1552
1519
|
* primary key, or whatever the storage layer prefers.
|
|
1553
1520
|
*/
|
|
1554
|
-
interface OutboxRecord<Evt> {
|
|
1521
|
+
interface OutboxRecord<Evt extends AnyDomainEvent> {
|
|
1555
1522
|
dispatchId: string;
|
|
1556
1523
|
event: Evt;
|
|
1557
1524
|
}
|
|
@@ -1571,7 +1538,7 @@ interface OutboxRecord<Evt> {
|
|
|
1571
1538
|
* that's already marked is a no-op, not an error. This lets the
|
|
1572
1539
|
* dispatcher safely retry on partial-failure.
|
|
1573
1540
|
*/
|
|
1574
|
-
interface Outbox<Evt> {
|
|
1541
|
+
interface Outbox<Evt extends AnyDomainEvent> {
|
|
1575
1542
|
/**
|
|
1576
1543
|
* Persists events. Called from inside `withCommit`'s transactional
|
|
1577
1544
|
* callback, atomically with the aggregate write.
|
|
@@ -1607,11 +1574,15 @@ interface Outbox<Evt> {
|
|
|
1607
1574
|
* and rolls back if it throws.
|
|
1608
1575
|
*
|
|
1609
1576
|
* `TCtx` is the persistence layer's transaction handle — Drizzle's `tx`,
|
|
1610
|
-
* Prisma's `tx`, Mongo's session,
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1577
|
+
* Prisma's `tx`, Mongo's session, etc. The scope opens the transaction
|
|
1578
|
+
* and passes the handle to `fn`; the use case binds its repositories to
|
|
1579
|
+
* that handle (typically by constructing a tx-scoped repo from the ctx).
|
|
1580
|
+
*
|
|
1581
|
+
* No default for `TCtx`: every implementor names their context type
|
|
1582
|
+
* explicitly. For genuinely context-free scopes (in-memory tests, naive
|
|
1583
|
+
* no-tx scopes) use `TransactionScope<undefined>` — that's a conscious
|
|
1584
|
+
* "there is nothing meaningful here" statement, not an accidental
|
|
1585
|
+
* `unknown` fallback.
|
|
1615
1586
|
*
|
|
1616
1587
|
* Intentionally **not** Fowler's full Unit of Work (no change tracking,
|
|
1617
1588
|
* no `registerDirty` / `registerNew` / `registerDeleted`, no commit-time
|
|
@@ -1631,11 +1602,11 @@ interface Outbox<Evt> {
|
|
|
1631
1602
|
* ```typescript
|
|
1632
1603
|
* await scope.transactional(async (tx) => {
|
|
1633
1604
|
* // Construct tx-bound repos from ctx (your factory / DI of choice)
|
|
1634
|
-
* const
|
|
1605
|
+
* const orderRepository = makeOrderRepository(tx);
|
|
1635
1606
|
*
|
|
1636
|
-
* const order = await
|
|
1607
|
+
* const order = await orderRepository.getByIdOrFail(orderId);
|
|
1637
1608
|
* order.confirm();
|
|
1638
|
-
* await
|
|
1609
|
+
* await orderRepository.save(order);
|
|
1639
1610
|
* });
|
|
1640
1611
|
* ```
|
|
1641
1612
|
*
|
|
@@ -1645,60 +1616,62 @@ interface Outbox<Evt> {
|
|
|
1645
1616
|
* (constructor injection, factory functions, `withTx` chains); pick one
|
|
1646
1617
|
* and keep it consistent.
|
|
1647
1618
|
*/
|
|
1648
|
-
interface TransactionScope<TCtx
|
|
1619
|
+
interface TransactionScope<TCtx> {
|
|
1649
1620
|
transactional<T>(fn: (ctx: TCtx) => Promise<T>): Promise<T>;
|
|
1650
1621
|
}
|
|
1651
1622
|
|
|
1652
1623
|
/**
|
|
1653
1624
|
* Helper for executing a write Use Case inside a transaction scope.
|
|
1654
1625
|
*
|
|
1626
|
+
* The use-case callback returns the aggregates it touched; `withCommit`
|
|
1627
|
+
* owns the post-save lifecycle (harvest, outbox, mark-persisted, publish).
|
|
1628
|
+
* This matches the Vernon / Axon / EventFlow unit-of-work pattern:
|
|
1629
|
+
* `Repository.save` is pure persistence; "this aggregate has been
|
|
1630
|
+
* committed" is the orchestrator's call to make, not the repo's.
|
|
1631
|
+
*
|
|
1655
1632
|
* Order of operations:
|
|
1656
1633
|
* 1. `fn(ctx)` runs inside `scope.transactional(...)` — domain mutations
|
|
1657
1634
|
* + repo writes happen here. `ctx` is whatever transaction handle the
|
|
1658
1635
|
* `scope` exposes (Drizzle `tx`, Prisma `tx`, Mongo session, or
|
|
1659
|
-
* `
|
|
1660
|
-
* 2.
|
|
1661
|
-
*
|
|
1662
|
-
* the state change.
|
|
1636
|
+
* `undefined` for context-free scopes).
|
|
1637
|
+
* 2. **Still inside the transaction**, `withCommit` harvests every
|
|
1638
|
+
* aggregate's `pendingEvents` and writes them via `outbox.add` (so
|
|
1639
|
+
* events persist atomically with the state change). Skipped when no
|
|
1640
|
+
* events were recorded.
|
|
1663
1641
|
* 3. The transaction commits.
|
|
1664
|
-
* 4. **After** the commit, `
|
|
1665
|
-
*
|
|
1642
|
+
* 4. **After** the commit, `aggregate.markPersisted(aggregate.version)`
|
|
1643
|
+
* fires on each returned aggregate — only now are pending events
|
|
1644
|
+
* considered flushed.
|
|
1645
|
+
* 5. `bus.publish(events)` fires for the in-process fast path (skipped
|
|
1646
|
+
* when no events or no `bus` is wired).
|
|
1666
1647
|
*
|
|
1667
1648
|
* Publishing AFTER commit prevents the classic "publish before commit"
|
|
1668
1649
|
* footgun: in-process subscribers can never react to events from a
|
|
1669
|
-
* transaction that later rolled back. If `bus.publish` itself
|
|
1650
|
+
* transaction that later rolled back. If `bus.publish` itself throws, the
|
|
1670
1651
|
* outbox still holds the events and an outbox-dispatcher will deliver
|
|
1671
1652
|
* them (eventual consistency).
|
|
1672
1653
|
*
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1675
|
-
* const result = await withCommit({ outbox, bus, scope }, async () => {
|
|
1676
|
-
* order.confirm();
|
|
1677
|
-
* await orderRepo.save(order);
|
|
1678
|
-
* return { result: order.id, events: order.domainEvents };
|
|
1679
|
-
* });
|
|
1680
|
-
* ```
|
|
1654
|
+
* If the transaction rolls back, `markPersisted` is **not** called — the
|
|
1655
|
+
* aggregate keeps its pending events, so the caller can retry or discard.
|
|
1681
1656
|
*
|
|
1682
1657
|
* @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
|
|
1683
1658
|
* ```typescript
|
|
1684
1659
|
* const result = await withCommit({ outbox, bus, scope }, async (tx) => {
|
|
1685
|
-
* const
|
|
1686
|
-
* const order = await
|
|
1660
|
+
* const orderRepository = makeOrderRepository(tx); // your factory binds tx to the repo
|
|
1661
|
+
* const order = await orderRepository.getByIdOrFail(orderId);
|
|
1687
1662
|
* order.confirm();
|
|
1688
|
-
* await
|
|
1689
|
-
* return { result: order.id,
|
|
1663
|
+
* await orderRepository.save(order); // pure persistence — does NOT call markPersisted
|
|
1664
|
+
* return { result: order.id, aggregates: [order] };
|
|
1690
1665
|
* });
|
|
1691
1666
|
* ```
|
|
1692
1667
|
*/
|
|
1693
|
-
declare function withCommit<Evt extends {
|
|
1694
|
-
type: string;
|
|
1695
|
-
}, R, TCtx = unknown>(deps: {
|
|
1668
|
+
declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: {
|
|
1696
1669
|
outbox: Outbox<Evt>;
|
|
1697
1670
|
bus?: EventBus<Evt>;
|
|
1698
1671
|
scope: TransactionScope<TCtx>;
|
|
1699
1672
|
}, fn: (ctx: TCtx) => Promise<{
|
|
1700
1673
|
result: R;
|
|
1701
|
-
|
|
1674
|
+
aggregates: ReadonlyArray<IAggregateRoot<Id<string>, Evt>>;
|
|
1702
1675
|
}>): Promise<R>;
|
|
1703
1676
|
|
|
1704
1677
|
/**
|
|
@@ -1933,7 +1906,7 @@ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQue
|
|
|
1933
1906
|
* // Both handlers will be called
|
|
1934
1907
|
* ```
|
|
1935
1908
|
*/
|
|
1936
|
-
declare class EventBusImpl<Evt extends
|
|
1909
|
+
declare class EventBusImpl<Evt extends AnyDomainEvent> implements EventBus<Evt> {
|
|
1937
1910
|
private readonly handlers;
|
|
1938
1911
|
subscribe<K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1939
1912
|
type: K;
|
|
@@ -1952,6 +1925,44 @@ declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements
|
|
|
1952
1925
|
publish(events: ReadonlyArray<Evt>): Promise<void>;
|
|
1953
1926
|
}
|
|
1954
1927
|
|
|
1928
|
+
/**
|
|
1929
|
+
* In-memory reference implementation of `Outbox<Evt>`.
|
|
1930
|
+
*
|
|
1931
|
+
* Intended for tests, single-process workers, and quick-start demos.
|
|
1932
|
+
* Uses the event's own `eventId` as the dispatch id — the common, clean
|
|
1933
|
+
* choice. Storage is a `Map<string, OutboxRecord<Evt>>` keyed by
|
|
1934
|
+
* `eventId`, so re-adding the same event is naturally idempotent (the
|
|
1935
|
+
* duplicate entry overwrites itself; `getPending` returns each event at
|
|
1936
|
+
* most once).
|
|
1937
|
+
*
|
|
1938
|
+
* For production, back the outbox with a transactional store so the
|
|
1939
|
+
* outbox row participates in the same transaction as the aggregate
|
|
1940
|
+
* write (see `TransactionScope` + `withCommit`). This class lives in
|
|
1941
|
+
* memory only — events are lost on process restart.
|
|
1942
|
+
*
|
|
1943
|
+
* @example
|
|
1944
|
+
* ```ts
|
|
1945
|
+
* import { InMemoryOutbox, EventBusImpl, withCommit } from "@shirudo/ddd-kit";
|
|
1946
|
+
*
|
|
1947
|
+
* const outbox = new InMemoryOutbox<OrderEvent>();
|
|
1948
|
+
* const bus = new EventBusImpl<OrderEvent>();
|
|
1949
|
+
*
|
|
1950
|
+
* await withCommit({ scope, outbox, bus }, async (tx) => {
|
|
1951
|
+
* const orderRepository = makeOrderRepository(tx);
|
|
1952
|
+
* const order = await orderRepository.getByIdOrFail(id);
|
|
1953
|
+
* order.confirm();
|
|
1954
|
+
* await orderRepository.save(order);
|
|
1955
|
+
* return { result: order.id, aggregates: [order] };
|
|
1956
|
+
* });
|
|
1957
|
+
* ```
|
|
1958
|
+
*/
|
|
1959
|
+
declare class InMemoryOutbox<Evt extends AnyDomainEvent> implements Outbox<Evt> {
|
|
1960
|
+
private readonly pending;
|
|
1961
|
+
add(events: ReadonlyArray<Evt>): Promise<void>;
|
|
1962
|
+
getPending(limit?: number): Promise<ReadonlyArray<OutboxRecord<Evt>>>;
|
|
1963
|
+
markDispatched(dispatchIds: ReadonlyArray<string>): Promise<void>;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1955
1966
|
/**
|
|
1956
1967
|
* Core repository contract for Aggregate Roots.
|
|
1957
1968
|
*
|
|
@@ -1988,17 +1999,25 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
1988
1999
|
*/
|
|
1989
2000
|
exists(id: TId): Promise<boolean>;
|
|
1990
2001
|
/**
|
|
1991
|
-
* Persists the aggregate (insert or update). Implementations
|
|
2002
|
+
* Persists the aggregate (insert or update). Implementations are
|
|
2003
|
+
* responsible for **persistence only** — they must NOT touch the
|
|
2004
|
+
* aggregate's in-memory state:
|
|
1992
2005
|
*
|
|
1993
2006
|
* 1. Throw `ConcurrencyConflictError` from `@shirudo/ddd-kit` when the
|
|
1994
2007
|
* aggregate's expected version does not match the version currently
|
|
1995
2008
|
* stored (optimistic concurrency).
|
|
1996
|
-
* 2.
|
|
1997
|
-
*
|
|
1998
|
-
*
|
|
2009
|
+
* 2. Write the aggregate to durable storage.
|
|
2010
|
+
*
|
|
2011
|
+
* Do **not** call `aggregate.markPersisted(...)` here. The library's
|
|
2012
|
+
* `withCommit` orchestrator handles the post-save lifecycle (harvest
|
|
2013
|
+
* pending events into the outbox, then mark persisted after commit).
|
|
2014
|
+
* Calling `markPersisted` inside `save` clears pending events too early
|
|
2015
|
+
* and breaks the harvest path — and is also why the Vernon/Axon/
|
|
2016
|
+
* EventFlow pattern separates persistence from commit-events.
|
|
1999
2017
|
*
|
|
2000
|
-
*
|
|
2001
|
-
*
|
|
2018
|
+
* If you are not using `withCommit` (custom orchestration), call
|
|
2019
|
+
* `aggregate.markPersisted(aggregate.version)` yourself **after** you
|
|
2020
|
+
* have harvested `aggregate.pendingEvents` for downstream dispatch.
|
|
2002
2021
|
*/
|
|
2003
2022
|
save(aggregate: TAgg): Promise<void>;
|
|
2004
2023
|
/**
|
|
@@ -2276,4 +2295,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
2276
2295
|
toJSON(): Readonly<T>;
|
|
2277
2296
|
}
|
|
2278
2297
|
|
|
2279
|
-
export { type AggregateConfig, AggregateNotFoundError, AggregateRoot, type AggregateSnapshot, type ClockFactory, type Command, CommandBus, type CommandHandler, ConcurrencyConflictError, type CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventIdFactory, type EventMetadata, EventSourcedAggregate, type
|
|
2298
|
+
export { type AggregateConfig, AggregateNotFoundError, AggregateRoot, type AggregateSnapshot, type AnyDomainEvent, type ClockFactory, type Command, CommandBus, type CommandHandler, ConcurrencyConflictError, type CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventIdFactory, type EventMetadata, EventSourcedAggregate, type IAggregateRoot, type ICommandBus, type IEntity, type IEventSourcedAggregate, type IQueryBus, type IQueryableRepository, type IRepository, type IValueObject, type Id, type IdGenerator, type Identifiable, InMemoryOutbox, InfrastructureError, MissingHandlerError, type OnceOptions, type Outbox, type OutboxRecord, type Query, QueryBus, type QueryHandler, type TransactionScope, type VO, ValueObject, type Version, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepFreeze, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withCommit };
|