@shirudo/ddd-kit 1.0.0-rc.4 → 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 +210 -157
- package/dist/index.js +71 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
669
|
-
*
|
|
670
|
-
* Defaults to **`false`** for `AggregateRoot` — because `setState()`
|
|
671
|
-
* already takes an explicit `bumpVersion` argument per call, so adding
|
|
672
|
-
* an "always bump" config on top would be redundant. Keep it `false`
|
|
673
|
-
* unless you have a subclass that never passes `bumpVersion` and you
|
|
674
|
-
* want every state change to advance the version anyway.
|
|
689
|
+
* Whether `setState()` should bump the version automatically when the
|
|
690
|
+
* caller omits the per-call `bumpVersion` argument.
|
|
675
691
|
*
|
|
676
|
-
*
|
|
677
|
-
* `
|
|
678
|
-
*
|
|
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
|
|
@@ -880,14 +898,11 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = ne
|
|
|
880
898
|
* they typically map to HTTP 400 / business-rule responses.
|
|
881
899
|
*
|
|
882
900
|
* The library itself does **not** ship any concrete `DomainError`
|
|
883
|
-
* subclass — the kit can't know your invariants.
|
|
884
|
-
* {@link AggregateNotFoundError}, and {@link ConcurrencyConflictError}
|
|
885
|
-
* deliberately sit on other branches of the hierarchy (see below) because
|
|
886
|
-
* they are not invariant violations.
|
|
901
|
+
* subclass — the kit can't know your invariants.
|
|
887
902
|
*
|
|
888
|
-
* Extends `BaseError<Name
|
|
889
|
-
*
|
|
890
|
-
*
|
|
903
|
+
* Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
|
|
904
|
+
* surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
|
|
905
|
+
* `isRetryable`, …).
|
|
891
906
|
*/
|
|
892
907
|
declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
|
|
893
908
|
}
|
|
@@ -899,11 +914,8 @@ declare abstract class DomainError<Name extends string = string> extends BaseErr
|
|
|
899
914
|
* broken); they describe race conditions and missing rows at the
|
|
900
915
|
* storage boundary.
|
|
901
916
|
*
|
|
902
|
-
* Library-internal concrete subclasses:
|
|
903
|
-
*
|
|
904
|
-
* - {@link ConcurrencyConflictError}
|
|
905
|
-
*
|
|
906
|
-
* Extends `BaseError<Name>` from `@shirudo/base-error`.
|
|
917
|
+
* Library-internal concrete subclasses: {@link AggregateNotFoundError},
|
|
918
|
+
* {@link ConcurrencyConflictError}.
|
|
907
919
|
*/
|
|
908
920
|
declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
|
|
909
921
|
}
|
|
@@ -926,7 +938,7 @@ declare abstract class InfrastructureError<Name extends string = string> extends
|
|
|
926
938
|
*/
|
|
927
939
|
declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
|
|
928
940
|
readonly eventType: string;
|
|
929
|
-
constructor(eventType: string);
|
|
941
|
+
constructor(eventType: string, cause?: unknown);
|
|
930
942
|
}
|
|
931
943
|
/**
|
|
932
944
|
* Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
|
|
@@ -934,13 +946,17 @@ declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
|
|
|
934
946
|
* boundary, not a business rule, decided the row is absent. Use the
|
|
935
947
|
* nullable variant `getById()` if "not found" is a valid outcome.
|
|
936
948
|
*
|
|
937
|
-
*
|
|
938
|
-
*
|
|
949
|
+
* Accepts an optional `cause` so a `Repository.save()` implementation
|
|
950
|
+
* can wrap a lower-level "row not found" / driver-level error without
|
|
951
|
+
* losing context. Cause-chain helpers (`getRootCause`,
|
|
952
|
+
* `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
|
|
953
|
+
*
|
|
954
|
+
* Not retryable — retrying won't make the row appear.
|
|
939
955
|
*/
|
|
940
956
|
declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
|
|
941
957
|
readonly aggregateType: string;
|
|
942
958
|
readonly id: string;
|
|
943
|
-
constructor(aggregateType: string, id: string);
|
|
959
|
+
constructor(aggregateType: string, id: string, cause?: unknown);
|
|
944
960
|
}
|
|
945
961
|
/**
|
|
946
962
|
* Thrown by `IRepository.save()` when the aggregate's expected version
|
|
@@ -964,7 +980,7 @@ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyC
|
|
|
964
980
|
* the use case, and retry on this exception.
|
|
965
981
|
*/
|
|
966
982
|
readonly retryable: true;
|
|
967
|
-
constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number);
|
|
983
|
+
constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
|
|
968
984
|
}
|
|
969
985
|
|
|
970
986
|
/**
|
|
@@ -974,11 +990,7 @@ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyC
|
|
|
974
990
|
* @template TId - The type of the aggregate root identifier
|
|
975
991
|
* @template TEvent - The union type of all domain events
|
|
976
992
|
*/
|
|
977
|
-
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends
|
|
978
|
-
/**
|
|
979
|
-
* Returns a read-only list of new, not-yet-persisted events.
|
|
980
|
-
*/
|
|
981
|
-
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
993
|
+
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
|
|
982
994
|
/**
|
|
983
995
|
* Reconstitutes the aggregate from an event history. Returns `Result`
|
|
984
996
|
* because event-stream corruption is an expected recoverable failure
|
|
@@ -986,45 +998,9 @@ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEv
|
|
|
986
998
|
*
|
|
987
999
|
* @param history - An ordered list of past events
|
|
988
1000
|
*/
|
|
989
|
-
loadFromHistory(history: TEvent
|
|
990
|
-
/**
|
|
991
|
-
* Clears the list of pending events.
|
|
992
|
-
*/
|
|
993
|
-
clearPendingEvents(): void;
|
|
994
|
-
/**
|
|
995
|
-
* Checks if the aggregate has any pending events.
|
|
996
|
-
*/
|
|
997
|
-
hasPendingEvents(): boolean;
|
|
998
|
-
/**
|
|
999
|
-
* Returns the number of pending events.
|
|
1000
|
-
*/
|
|
1001
|
-
getEventCount(): number;
|
|
1002
|
-
/**
|
|
1003
|
-
* Returns the latest pending event, if any.
|
|
1004
|
-
*/
|
|
1005
|
-
getLatestEvent(): TEvent | undefined;
|
|
1006
|
-
}
|
|
1007
|
-
type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
|
|
1008
|
-
/**
|
|
1009
|
-
* Configuration options for EventSourcedAggregate behavior.
|
|
1010
|
-
*/
|
|
1011
|
-
interface EventSourcedAggregateConfig {
|
|
1012
|
-
/**
|
|
1013
|
-
* Whether `apply()` should bump the version per event.
|
|
1014
|
-
*
|
|
1015
|
-
* Defaults to **`true`** for `EventSourcedAggregate` — each applied
|
|
1016
|
-
* event is by definition a versioned state change, so the canonical
|
|
1017
|
-
* event-sourcing pattern is "one event = one version bump". Set to
|
|
1018
|
-
* `false` only if your event store assigns version numbers itself
|
|
1019
|
-
* and you want the aggregate to track them via `bumpVersion()` /
|
|
1020
|
-
* `setVersion()` calls instead.
|
|
1021
|
-
*
|
|
1022
|
-
* (Contrast with `AggregateRoot`, which defaults this to `false`
|
|
1023
|
-
* because its `setState()` already takes a per-call `bumpVersion`
|
|
1024
|
-
* argument.)
|
|
1025
|
-
*/
|
|
1026
|
-
autoVersionBump?: boolean;
|
|
1001
|
+
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1027
1002
|
}
|
|
1003
|
+
type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
|
|
1028
1004
|
/**
|
|
1029
1005
|
* Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
|
|
1030
1006
|
*
|
|
@@ -1074,12 +1050,11 @@ interface EventSourcedAggregateConfig {
|
|
|
1074
1050
|
* }
|
|
1075
1051
|
* ```
|
|
1076
1052
|
*/
|
|
1077
|
-
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> {
|
|
1078
1054
|
private _version;
|
|
1079
1055
|
get version(): Version;
|
|
1080
1056
|
private setVersion;
|
|
1081
1057
|
private _pendingEvents;
|
|
1082
|
-
private readonly _autoVersionBump;
|
|
1083
1058
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
1084
1059
|
clearPendingEvents(): void;
|
|
1085
1060
|
/**
|
|
@@ -1089,7 +1064,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1089
1064
|
* `save()` keep its `Promise<void>` return type.
|
|
1090
1065
|
*/
|
|
1091
1066
|
markPersisted(version: Version): void;
|
|
1092
|
-
protected constructor(id: TId, initialState: TState
|
|
1067
|
+
protected constructor(id: TId, initialState: TState);
|
|
1093
1068
|
/**
|
|
1094
1069
|
* Validates an event before it is applied. Default is no-op.
|
|
1095
1070
|
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
@@ -1125,11 +1100,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1125
1100
|
* resolved via the (statically-sound) `handlers` map.
|
|
1126
1101
|
*/
|
|
1127
1102
|
private dispatchAndCommit;
|
|
1128
|
-
/**
|
|
1129
|
-
* Manually bumps the aggregate version.
|
|
1130
|
-
* Only needed if `autoVersionBump` is disabled.
|
|
1131
|
-
*/
|
|
1132
|
-
protected bumpVersion(): void;
|
|
1133
1103
|
/**
|
|
1134
1104
|
* Reconstitutes the aggregate from an event history. Catches `DomainError`
|
|
1135
1105
|
* thrown during replay and returns it as an `Err` — this is the
|
|
@@ -1141,10 +1111,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1141
1111
|
* an aggregate already at v=1 (e.g. after a creation event) loading
|
|
1142
1112
|
* 2 events ends at v=3, not v=2.
|
|
1143
1113
|
*/
|
|
1144
|
-
loadFromHistory(history: TEvent
|
|
1145
|
-
hasPendingEvents(): boolean;
|
|
1146
|
-
getEventCount(): number;
|
|
1147
|
-
getLatestEvent(): TEvent | undefined;
|
|
1114
|
+
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1148
1115
|
/**
|
|
1149
1116
|
* Creates a snapshot of the current aggregate state.
|
|
1150
1117
|
*/
|
|
@@ -1159,7 +1126,7 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
1159
1126
|
* aggregate is rolled back to its pre-call state + version. Partial
|
|
1160
1127
|
* restoration is never observable to the caller.
|
|
1161
1128
|
*/
|
|
1162
|
-
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent
|
|
1129
|
+
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1163
1130
|
/**
|
|
1164
1131
|
* A map of event types to their corresponding handlers.
|
|
1165
1132
|
* Subclasses MUST implement this property.
|
|
@@ -1458,9 +1425,7 @@ type EventHandler<Evt> = (event: Evt) => Promise<void> | void;
|
|
|
1458
1425
|
* await bus.publish([orderCreatedEvent, orderShippedEvent]);
|
|
1459
1426
|
* ```
|
|
1460
1427
|
*/
|
|
1461
|
-
interface EventBus<Evt extends {
|
|
1462
|
-
type: string;
|
|
1463
|
-
}> {
|
|
1428
|
+
interface EventBus<Evt extends AnyDomainEvent> {
|
|
1464
1429
|
/**
|
|
1465
1430
|
* Publishes events to all subscribed handlers.
|
|
1466
1431
|
*
|
|
@@ -1553,7 +1518,7 @@ interface OnceOptions {
|
|
|
1553
1518
|
* own `eventId`, generate its own UUID, use the row's auto-increment
|
|
1554
1519
|
* primary key, or whatever the storage layer prefers.
|
|
1555
1520
|
*/
|
|
1556
|
-
interface OutboxRecord<Evt> {
|
|
1521
|
+
interface OutboxRecord<Evt extends AnyDomainEvent> {
|
|
1557
1522
|
dispatchId: string;
|
|
1558
1523
|
event: Evt;
|
|
1559
1524
|
}
|
|
@@ -1573,7 +1538,7 @@ interface OutboxRecord<Evt> {
|
|
|
1573
1538
|
* that's already marked is a no-op, not an error. This lets the
|
|
1574
1539
|
* dispatcher safely retry on partial-failure.
|
|
1575
1540
|
*/
|
|
1576
|
-
interface Outbox<Evt> {
|
|
1541
|
+
interface Outbox<Evt extends AnyDomainEvent> {
|
|
1577
1542
|
/**
|
|
1578
1543
|
* Persists events. Called from inside `withCommit`'s transactional
|
|
1579
1544
|
* callback, atomically with the aggregate write.
|
|
@@ -1604,67 +1569,109 @@ interface Outbox<Evt> {
|
|
|
1604
1569
|
* Transaction-scope abstraction.
|
|
1605
1570
|
*
|
|
1606
1571
|
* Wraps a block of work so it runs inside the persistence layer's native
|
|
1607
|
-
* transaction (Postgres `BEGIN`/`COMMIT`, Mongo session,
|
|
1608
|
-
* commits when the callback resolves
|
|
1609
|
-
*
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1572
|
+
* transaction (Postgres `BEGIN`/`COMMIT`, Mongo session, Drizzle / Prisma
|
|
1573
|
+
* `$transaction`, etc.). The block commits when the callback resolves
|
|
1574
|
+
* and rolls back if it throws.
|
|
1575
|
+
*
|
|
1576
|
+
* `TCtx` is the persistence layer's transaction handle — Drizzle's `tx`,
|
|
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.
|
|
1586
|
+
*
|
|
1587
|
+
* Intentionally **not** Fowler's full Unit of Work (no change tracking,
|
|
1588
|
+
* no `registerDirty` / `registerNew` / `registerDeleted`, no commit-time
|
|
1589
|
+
* flush). Change tracking is the ORM's job.
|
|
1590
|
+
*
|
|
1591
|
+
* @example Drizzle implementation
|
|
1592
|
+
* ```typescript
|
|
1593
|
+
* class DrizzleScope implements TransactionScope<DrizzleTx> {
|
|
1594
|
+
* constructor(private db: DrizzleDb) {}
|
|
1595
|
+
* async transactional<T>(fn: (tx: DrizzleTx) => Promise<T>): Promise<T> {
|
|
1596
|
+
* return this.db.transaction((tx) => fn(tx));
|
|
1597
|
+
* }
|
|
1598
|
+
* }
|
|
1599
|
+
* ```
|
|
1615
1600
|
*
|
|
1616
|
-
* @example
|
|
1601
|
+
* @example Use site — bind repos to the live transaction
|
|
1617
1602
|
* ```typescript
|
|
1618
|
-
* await scope.transactional(async () => {
|
|
1619
|
-
*
|
|
1603
|
+
* await scope.transactional(async (tx) => {
|
|
1604
|
+
* // Construct tx-bound repos from ctx (your factory / DI of choice)
|
|
1605
|
+
* const orderRepository = makeOrderRepository(tx);
|
|
1606
|
+
*
|
|
1607
|
+
* const order = await orderRepository.getByIdOrFail(orderId);
|
|
1620
1608
|
* order.confirm();
|
|
1621
|
-
* await
|
|
1609
|
+
* await orderRepository.save(order);
|
|
1622
1610
|
* });
|
|
1623
1611
|
* ```
|
|
1612
|
+
*
|
|
1613
|
+
* `IRepository`'s contract takes the id / aggregate only — the tx handle
|
|
1614
|
+
* is wired into a concrete repository at construction time, not threaded
|
|
1615
|
+
* through every call. Different ORMs have different idioms for that
|
|
1616
|
+
* (constructor injection, factory functions, `withTx` chains); pick one
|
|
1617
|
+
* and keep it consistent.
|
|
1624
1618
|
*/
|
|
1625
|
-
interface TransactionScope {
|
|
1626
|
-
transactional<T>(fn: () => Promise<T>): Promise<T>;
|
|
1619
|
+
interface TransactionScope<TCtx> {
|
|
1620
|
+
transactional<T>(fn: (ctx: TCtx) => Promise<T>): Promise<T>;
|
|
1627
1621
|
}
|
|
1628
1622
|
|
|
1629
1623
|
/**
|
|
1630
|
-
* Helper for executing a write Use Case inside a
|
|
1624
|
+
* Helper for executing a write Use Case inside a transaction scope.
|
|
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
1631
|
*
|
|
1632
1632
|
* Order of operations:
|
|
1633
|
-
* 1. `fn()` runs inside `
|
|
1634
|
-
* writes happen here.
|
|
1635
|
-
*
|
|
1636
|
-
*
|
|
1633
|
+
* 1. `fn(ctx)` runs inside `scope.transactional(...)` — domain mutations
|
|
1634
|
+
* + repo writes happen here. `ctx` is whatever transaction handle the
|
|
1635
|
+
* `scope` exposes (Drizzle `tx`, Prisma `tx`, Mongo session, or
|
|
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.
|
|
1637
1641
|
* 3. The transaction commits.
|
|
1638
|
-
* 4. **After** the commit, `
|
|
1639
|
-
*
|
|
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).
|
|
1640
1647
|
*
|
|
1641
1648
|
* Publishing AFTER commit prevents the classic "publish before commit"
|
|
1642
1649
|
* footgun: in-process subscribers can never react to events from a
|
|
1643
|
-
* transaction that later rolled back. If `bus.publish` itself
|
|
1644
|
-
* outbox still holds the events and an outbox-dispatcher will deliver
|
|
1645
|
-
* (eventual consistency).
|
|
1650
|
+
* transaction that later rolled back. If `bus.publish` itself throws, the
|
|
1651
|
+
* outbox still holds the events and an outbox-dispatcher will deliver
|
|
1652
|
+
* them (eventual consistency).
|
|
1646
1653
|
*
|
|
1647
|
-
*
|
|
1654
|
+
* If the transaction rolls back, `markPersisted` is **not** called — the
|
|
1655
|
+
* aggregate keeps its pending events, so the caller can retry or discard.
|
|
1656
|
+
*
|
|
1657
|
+
* @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
|
|
1648
1658
|
* ```typescript
|
|
1649
|
-
* const result = await withCommit(
|
|
1650
|
-
*
|
|
1651
|
-
*
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1656
|
-
* );
|
|
1659
|
+
* const result = await withCommit({ outbox, bus, scope }, async (tx) => {
|
|
1660
|
+
* const orderRepository = makeOrderRepository(tx); // your factory binds tx to the repo
|
|
1661
|
+
* const order = await orderRepository.getByIdOrFail(orderId);
|
|
1662
|
+
* order.confirm();
|
|
1663
|
+
* await orderRepository.save(order); // pure persistence — does NOT call markPersisted
|
|
1664
|
+
* return { result: order.id, aggregates: [order] };
|
|
1665
|
+
* });
|
|
1657
1666
|
* ```
|
|
1658
1667
|
*/
|
|
1659
|
-
declare function withCommit<Evt extends {
|
|
1660
|
-
type: string;
|
|
1661
|
-
}, R>(deps: {
|
|
1668
|
+
declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: {
|
|
1662
1669
|
outbox: Outbox<Evt>;
|
|
1663
1670
|
bus?: EventBus<Evt>;
|
|
1664
|
-
scope: TransactionScope
|
|
1665
|
-
}, fn: () => Promise<{
|
|
1671
|
+
scope: TransactionScope<TCtx>;
|
|
1672
|
+
}, fn: (ctx: TCtx) => Promise<{
|
|
1666
1673
|
result: R;
|
|
1667
|
-
|
|
1674
|
+
aggregates: ReadonlyArray<IAggregateRoot<Id<string>, Evt>>;
|
|
1668
1675
|
}>): Promise<R>;
|
|
1669
1676
|
|
|
1670
1677
|
/**
|
|
@@ -1899,7 +1906,7 @@ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQue
|
|
|
1899
1906
|
* // Both handlers will be called
|
|
1900
1907
|
* ```
|
|
1901
1908
|
*/
|
|
1902
|
-
declare class EventBusImpl<Evt extends
|
|
1909
|
+
declare class EventBusImpl<Evt extends AnyDomainEvent> implements EventBus<Evt> {
|
|
1903
1910
|
private readonly handlers;
|
|
1904
1911
|
subscribe<K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1905
1912
|
type: K;
|
|
@@ -1918,6 +1925,44 @@ declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements
|
|
|
1918
1925
|
publish(events: ReadonlyArray<Evt>): Promise<void>;
|
|
1919
1926
|
}
|
|
1920
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
|
+
|
|
1921
1966
|
/**
|
|
1922
1967
|
* Core repository contract for Aggregate Roots.
|
|
1923
1968
|
*
|
|
@@ -1954,17 +1999,25 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
1954
1999
|
*/
|
|
1955
2000
|
exists(id: TId): Promise<boolean>;
|
|
1956
2001
|
/**
|
|
1957
|
-
* 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:
|
|
1958
2005
|
*
|
|
1959
2006
|
* 1. Throw `ConcurrencyConflictError` from `@shirudo/ddd-kit` when the
|
|
1960
2007
|
* aggregate's expected version does not match the version currently
|
|
1961
2008
|
* stored (optimistic concurrency).
|
|
1962
|
-
* 2.
|
|
1963
|
-
*
|
|
1964
|
-
*
|
|
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.
|
|
1965
2017
|
*
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
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.
|
|
1968
2021
|
*/
|
|
1969
2022
|
save(aggregate: TAgg): Promise<void>;
|
|
1970
2023
|
/**
|
|
@@ -2242,4 +2295,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
2242
2295
|
toJSON(): Readonly<T>;
|
|
2243
2296
|
}
|
|
2244
2297
|
|
|
2245
|
-
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 };
|