@shirudo/ddd-kit 1.0.0-rc.7 → 1.0.0-rc.9
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/dist/index.d.ts +549 -555
- package/dist/index.js +165 -190
- package/dist/index.js.map +1 -1
- package/package.json +72 -71
package/dist/index.d.ts
CHANGED
|
@@ -52,6 +52,101 @@ interface IdGenerator<Tag extends string> {
|
|
|
52
52
|
next: () => Id<Tag>;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Abstract base for **domain-invariant violations**. Domain methods
|
|
57
|
+
* (aggregates, entity validation hooks, value-object constructors)
|
|
58
|
+
* throw `DomainError`-derived exceptions when a business rule is
|
|
59
|
+
* violated. Consumers derive their own concrete errors — e.g.
|
|
60
|
+
* `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
|
|
61
|
+
* for `instanceof`-style catching at the App-Service boundary, where
|
|
62
|
+
* they typically map to HTTP 400 / business-rule responses.
|
|
63
|
+
*
|
|
64
|
+
* The library itself does **not** ship any concrete `DomainError`
|
|
65
|
+
* subclass — the kit can't know your invariants.
|
|
66
|
+
*
|
|
67
|
+
* Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
|
|
68
|
+
* surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
|
|
69
|
+
* `isRetryable`, …).
|
|
70
|
+
*/
|
|
71
|
+
declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Abstract base for **infrastructure / persistence failures** that the
|
|
75
|
+
* App-Service can recover from — typically by retrying, by returning
|
|
76
|
+
* HTTP 404 / 409, or by surfacing a "please try again" UX. These are
|
|
77
|
+
* not domain-invariant violations (the business rules were not
|
|
78
|
+
* broken); they describe race conditions and missing rows at the
|
|
79
|
+
* storage boundary.
|
|
80
|
+
*
|
|
81
|
+
* Library-internal concrete subclasses: {@link AggregateNotFoundError},
|
|
82
|
+
* {@link ConcurrencyConflictError}.
|
|
83
|
+
*/
|
|
84
|
+
declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Thrown by `EventSourcedAggregate.apply()` when no handler is
|
|
88
|
+
* registered for the event's type. This means the aggregate's subclass
|
|
89
|
+
* forgot to add an entry to its `handlers` map — a programming /
|
|
90
|
+
* configuration bug, not a domain or infrastructure failure.
|
|
91
|
+
*
|
|
92
|
+
* Deliberately **not** on `DomainError` or `InfrastructureError` —
|
|
93
|
+
* a generic `catch (e instanceof DomainError)` handler at the App
|
|
94
|
+
* layer must not mask a forgotten handler; this should crash loud and
|
|
95
|
+
* fail the calling Use Case so the bug surfaces in development. The
|
|
96
|
+
* replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
|
|
97
|
+
* also let it propagate uncaught instead of wrapping it in `Result.Err`.
|
|
98
|
+
*
|
|
99
|
+
* Use `isBaseError(e)` from `@shirudo/base-error` to detect
|
|
100
|
+
* "any structured error from the kit or any other BaseError-using
|
|
101
|
+
* library" at the App boundary.
|
|
102
|
+
*/
|
|
103
|
+
declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
|
|
104
|
+
readonly eventType: string;
|
|
105
|
+
constructor(eventType: string, cause?: unknown);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
|
|
109
|
+
* given id does not exist. `InfrastructureError` because the storage
|
|
110
|
+
* boundary, not a business rule, decided the row is absent. Use the
|
|
111
|
+
* nullable variant `getById()` if "not found" is a valid outcome.
|
|
112
|
+
*
|
|
113
|
+
* Accepts an optional `cause` so a `Repository.save()` implementation
|
|
114
|
+
* can wrap a lower-level "row not found" / driver-level error without
|
|
115
|
+
* losing context. Cause-chain helpers (`getRootCause`,
|
|
116
|
+
* `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
|
|
117
|
+
*
|
|
118
|
+
* Not retryable — retrying won't make the row appear.
|
|
119
|
+
*/
|
|
120
|
+
declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
|
|
121
|
+
readonly aggregateType: string;
|
|
122
|
+
readonly id: string;
|
|
123
|
+
constructor(aggregateType: string, id: string, cause?: unknown);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Thrown by `IRepository.save()` when the aggregate's expected version
|
|
127
|
+
* does not match the version currently persisted — i.e. another writer
|
|
128
|
+
* updated the aggregate concurrently. The canonical optimistic-
|
|
129
|
+
* concurrency signal; the App-Service typically reloads, re-applies
|
|
130
|
+
* the use case, and retries, or surfaces HTTP 409 to the caller.
|
|
131
|
+
*
|
|
132
|
+
* `InfrastructureError` because the persistence layer (not a domain
|
|
133
|
+
* rule) detects the race. Marks itself as `retryable: true` so the
|
|
134
|
+
* `isRetryable` predicate from `@shirudo/base-error` picks it up.
|
|
135
|
+
*/
|
|
136
|
+
declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
|
|
137
|
+
readonly aggregateType: string;
|
|
138
|
+
readonly aggregateId: string;
|
|
139
|
+
readonly expectedVersion: number;
|
|
140
|
+
readonly actualVersion: number;
|
|
141
|
+
/**
|
|
142
|
+
* Marks this error as retryable so `isRetryable(err)` returns
|
|
143
|
+
* true. The canonical OCC pattern is to reload the aggregate, re-apply
|
|
144
|
+
* the use case, and retry on this exception.
|
|
145
|
+
*/
|
|
146
|
+
readonly retryable: true;
|
|
147
|
+
constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
|
|
148
|
+
}
|
|
149
|
+
|
|
55
150
|
/**
|
|
56
151
|
* Factory function producing a fresh, unique event identifier for each call.
|
|
57
152
|
*
|
|
@@ -314,9 +409,25 @@ interface CreateDomainEventOptions {
|
|
|
314
409
|
* Creates a domain event with default values.
|
|
315
410
|
* Sets occurredAt to current date and version to 1 if not provided.
|
|
316
411
|
*
|
|
412
|
+
* **For aggregate-internal events, prefer `this.recordEvent(...)` on
|
|
413
|
+
* `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
|
|
414
|
+
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
415
|
+
* aggregate's declared `aggregateType` property), which downstream
|
|
416
|
+
* consumers — outbox dispatchers, projection handlers, audit logs —
|
|
417
|
+
* route by. The `withCommit` harvest boundary now validates both fields
|
|
418
|
+
* are present and throws if they're missing, so a direct
|
|
419
|
+
* `createDomainEvent(...)` call inside an aggregate that forgets the
|
|
420
|
+
* options is caught at runtime.
|
|
421
|
+
*
|
|
422
|
+
* Use `createDomainEvent(...)` directly for events that don't belong to
|
|
423
|
+
* an aggregate: system events, integration events, configuration events,
|
|
424
|
+
* test fixtures. For those, set `aggregateId` / `aggregateType` in
|
|
425
|
+
* `options` if downstream consumers expect routing metadata.
|
|
426
|
+
*
|
|
317
427
|
* @param type - The event type
|
|
318
428
|
* @param payload - The event payload
|
|
319
|
-
* @param options - Optional event configuration
|
|
429
|
+
* @param options - Optional event configuration (including `aggregateId`
|
|
430
|
+
* and `aggregateType` for routing)
|
|
320
431
|
* @returns A domain event
|
|
321
432
|
*
|
|
322
433
|
* @example
|
|
@@ -369,6 +480,93 @@ declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?:
|
|
|
369
480
|
*/
|
|
370
481
|
declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
|
|
371
482
|
|
|
483
|
+
type Version = number & {
|
|
484
|
+
readonly __v: true;
|
|
485
|
+
};
|
|
486
|
+
/**
|
|
487
|
+
* Snapshot of an aggregate state at a specific point in time.
|
|
488
|
+
* Used for optimizing event replay by starting from a snapshot
|
|
489
|
+
* instead of replaying all events from the beginning.
|
|
490
|
+
*
|
|
491
|
+
* @template TState - The type of the aggregate state
|
|
492
|
+
*/
|
|
493
|
+
interface AggregateSnapshot<TState> {
|
|
494
|
+
/**
|
|
495
|
+
* The state of the aggregate at the time of the snapshot.
|
|
496
|
+
*/
|
|
497
|
+
state: TState;
|
|
498
|
+
/**
|
|
499
|
+
* The version of the aggregate when the snapshot was taken.
|
|
500
|
+
*/
|
|
501
|
+
version: Version;
|
|
502
|
+
/**
|
|
503
|
+
* Timestamp when the snapshot was created.
|
|
504
|
+
*/
|
|
505
|
+
snapshotAt: Date;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Public contract every Aggregate Root satisfies. Implemented by
|
|
509
|
+
* `BaseAggregate` and inherited by both `AggregateRoot` and
|
|
510
|
+
* `EventSourcedAggregate`. Repository implementations type their
|
|
511
|
+
* `save(aggregate)` parameter against this interface rather than the
|
|
512
|
+
* concrete classes, so the repo layer does not take a compile-time
|
|
513
|
+
* dependency on the aggregate hierarchy.
|
|
514
|
+
*
|
|
515
|
+
* Full per-member documentation lives on the concrete `BaseAggregate`
|
|
516
|
+
* class; the interface is intentionally terse to avoid drift.
|
|
517
|
+
*
|
|
518
|
+
* @template TId - The aggregate root identifier (branded via `Id<Tag>`)
|
|
519
|
+
* @template TEvent - The domain-event union, defaults to `never`
|
|
520
|
+
*/
|
|
521
|
+
interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
|
|
522
|
+
readonly id: TId;
|
|
523
|
+
readonly version: Version;
|
|
524
|
+
readonly persistedVersion: Version | undefined;
|
|
525
|
+
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
526
|
+
clearPendingEvents(): void;
|
|
527
|
+
markPersisted(version: Version): void;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Public contract for Event-Sourced Aggregate Roots. Extends
|
|
531
|
+
* `IAggregateRoot` with the replay-from-history boundary.
|
|
532
|
+
*
|
|
533
|
+
* @template TId - The aggregate root identifier
|
|
534
|
+
* @template TEvent - The union type of all domain events
|
|
535
|
+
*/
|
|
536
|
+
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
|
|
537
|
+
/**
|
|
538
|
+
* Reconstitutes the aggregate from an event history. Returns
|
|
539
|
+
* `Result` because event-stream corruption is an expected
|
|
540
|
+
* recoverable failure at the infrastructure boundary.
|
|
541
|
+
*/
|
|
542
|
+
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Checks if two aggregates are at the same version (same ID and version).
|
|
546
|
+
* Useful for optimistic concurrency control checks.
|
|
547
|
+
*
|
|
548
|
+
* Note: Two aggregates with the same ID ARE the same aggregate (identity).
|
|
549
|
+
* This function checks if they are at the same version — i.e., no concurrent modification.
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```typescript
|
|
553
|
+
* const before = await repository.getById(id);
|
|
554
|
+
* // ... some operations ...
|
|
555
|
+
* const after = await repository.getById(id);
|
|
556
|
+
*
|
|
557
|
+
* if (!sameVersion(before, after)) {
|
|
558
|
+
* throw new Error("Aggregate was modified by another process");
|
|
559
|
+
* }
|
|
560
|
+
* ```
|
|
561
|
+
*/
|
|
562
|
+
declare function sameVersion<TId extends Id<string>>(a: {
|
|
563
|
+
id: TId;
|
|
564
|
+
version: Version;
|
|
565
|
+
}, b: {
|
|
566
|
+
id: TId;
|
|
567
|
+
version: Version;
|
|
568
|
+
}): boolean;
|
|
569
|
+
|
|
372
570
|
/**
|
|
373
571
|
* Entity utilities and interfaces for Domain-Driven Design.
|
|
374
572
|
*
|
|
@@ -696,491 +894,109 @@ declare function replaceEntityById<TId extends Id<string>, T extends Identifiabl
|
|
|
696
894
|
* // ids is [itemId1, itemId2]
|
|
697
895
|
* ```
|
|
698
896
|
*/
|
|
699
|
-
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>): TId[];
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
*
|
|
703
|
-
*
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
* -
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
* @template TId
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
*
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
*
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
*/
|
|
746
|
-
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
747
|
-
/**
|
|
748
|
-
* Clears the pending-event list. Called by `markPersisted` after a
|
|
749
|
-
* successful write — the events have been handed off to the outbox
|
|
750
|
-
* / event store and are no longer the aggregate's responsibility.
|
|
751
|
-
*/
|
|
752
|
-
clearPendingEvents(): void;
|
|
753
|
-
/**
|
|
754
|
-
* Post-save hook: a `Repository.save()` implementation calls this with
|
|
755
|
-
* the persisted version after a successful write to push the new
|
|
756
|
-
* version back into the aggregate and clear pendingEvents (they are
|
|
757
|
-
* now safely on the write side / in the outbox).
|
|
758
|
-
*
|
|
759
|
-
* Required by the interface so a Repository implementation can call it
|
|
760
|
-
* via the published `IAggregateRoot` contract without taking the
|
|
761
|
-
* abstract class as a compile-time dependency.
|
|
762
|
-
*
|
|
763
|
-
* @param version - The version assigned by the persistence layer
|
|
764
|
-
*/
|
|
765
|
-
markPersisted(version: Version): void;
|
|
766
|
-
}
|
|
767
|
-
/**
|
|
768
|
-
* Configuration options for AggregateRoot behavior.
|
|
769
|
-
*/
|
|
770
|
-
interface AggregateConfig {
|
|
771
|
-
/**
|
|
772
|
-
* Whether `setState()` should bump the version automatically when the
|
|
773
|
-
* caller omits the per-call `bumpVersion` argument.
|
|
774
|
-
*
|
|
775
|
-
* Defaults to **`false`** — `setState()` already takes an explicit
|
|
776
|
-
* `bumpVersion` argument per call, so the config is just the default
|
|
777
|
-
* the per-call argument falls back to. Set to `true` only if you have
|
|
778
|
-
* a subclass that never passes `bumpVersion` and you want every state
|
|
779
|
-
* change to advance the version anyway.
|
|
780
|
-
*/
|
|
781
|
-
autoVersionBump?: boolean;
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Base class for Aggregate Roots without Event Sourcing.
|
|
785
|
-
*
|
|
786
|
-
* In DDD (Evans), an Aggregate is a cluster of objects — root entity, child entities,
|
|
787
|
-
* and value objects — treated as a unit for consistency. The **Aggregate Root** is the
|
|
788
|
-
* root entity that represents the aggregate externally and is the only entry point
|
|
789
|
-
* for external code. This class serves as both: it IS the root entity and it contains
|
|
790
|
-
* the aggregate state (`TState`) which holds child entities and value objects.
|
|
791
|
-
*
|
|
792
|
-
* Provides:
|
|
793
|
-
* - Identity (id) and state management (via `Entity`)
|
|
794
|
-
* - Version management for optimistic concurrency control
|
|
795
|
-
* - Domain event tracking for side-effects
|
|
796
|
-
* - Snapshot support for performance optimization
|
|
797
|
-
*
|
|
798
|
-
* All changes to child entities within `TState` are versioned through this root.
|
|
799
|
-
* Use `setState()` for state mutations to ensure invariant validation.
|
|
800
|
-
*
|
|
801
|
-
* For event sourcing, use `EventSourcedAggregate` instead.
|
|
802
|
-
*
|
|
803
|
-
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
804
|
-
* @template TId - The type of the aggregate root identifier
|
|
805
|
-
* @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.
|
|
806
|
-
*
|
|
807
|
-
* @example
|
|
808
|
-
* ```typescript
|
|
809
|
-
* // Order is an Aggregate Root (an Entity with version)
|
|
810
|
-
* class Order extends AggregateRoot<OrderState, OrderId> {
|
|
811
|
-
* constructor(id: OrderId, initialState: OrderState) {
|
|
812
|
-
* super(id, initialState);
|
|
813
|
-
* }
|
|
814
|
-
*
|
|
815
|
-
* confirm(): void {
|
|
816
|
-
* this.setState({ ...this.state, status: "confirmed" }, true);
|
|
817
|
-
* }
|
|
818
|
-
* }
|
|
819
|
-
* ```
|
|
820
|
-
*/
|
|
821
|
-
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
|
|
822
|
-
private _version;
|
|
823
|
-
get version(): Version;
|
|
824
|
-
protected setVersion(version: Version): void;
|
|
825
|
-
private readonly _config;
|
|
826
|
-
private readonly _autoVersionBump;
|
|
827
|
-
private _pendingEvents;
|
|
828
|
-
/**
|
|
829
|
-
* Read-only list of domain events recorded on this aggregate that have
|
|
830
|
-
* not yet been flushed to the outbox / persistence layer.
|
|
831
|
-
*/
|
|
832
|
-
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
833
|
-
/**
|
|
834
|
-
* Clears the pending-event list. Call this after the events have been
|
|
835
|
-
* dispatched (typically `markPersisted` handles it for you).
|
|
836
|
-
*/
|
|
837
|
-
clearPendingEvents(): void;
|
|
838
|
-
/**
|
|
839
|
-
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
840
|
-
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
841
|
-
* to push the persisted version back into the in-memory aggregate and
|
|
842
|
-
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
843
|
-
* subclasses **should not** override this method directly.
|
|
844
|
-
*
|
|
845
|
-
* Overriding without calling `super.markPersisted(version)` silently
|
|
846
|
-
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
847
|
-
* through the outbox, double-emitting events. This bug has been hit
|
|
848
|
-
* in production by consumers; the {@link onPersisted} hook below is
|
|
849
|
-
* the safer extension point.
|
|
850
|
-
*
|
|
851
|
-
* If you must override (legitimate cases are very rare), call
|
|
852
|
-
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
853
|
-
* runs, then add your logic afterwards.
|
|
854
|
-
*
|
|
855
|
-
* @param version - The version assigned by the persistence layer
|
|
856
|
-
* @see onPersisted — the safe extension point for subclasses
|
|
857
|
-
*/
|
|
858
|
-
markPersisted(version: Version): void;
|
|
859
|
-
/**
|
|
860
|
-
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
861
|
-
* updated the version and cleared `pendingEvents`. Override this for
|
|
862
|
-
* post-persist logging, metrics, or cache-eviction without risk of
|
|
863
|
-
* breaking the framework's pendingEvents cleanup.
|
|
864
|
-
*
|
|
865
|
-
* The default implementation is a no-op. Subclasses do NOT need to
|
|
866
|
-
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
867
|
-
* implementation to preserve.
|
|
868
|
-
*
|
|
869
|
-
* **`onPersisted` deliberately receives only the version, not the
|
|
870
|
-
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
871
|
-
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
872
|
-
* subscribers or the outbox dispatcher — that is the proper
|
|
873
|
-
* Aggregate-Boundary separation. Building event-aware logic into
|
|
874
|
-
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
875
|
-
* recreates the boundary problems Vernon's aggregate discipline is
|
|
876
|
-
* meant to prevent.
|
|
877
|
-
*
|
|
878
|
-
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
879
|
-
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
880
|
-
* permissive `void` will accept an `async`-override returning
|
|
881
|
-
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
882
|
-
* any rejection becomes an unhandled rejection and `withCommit`
|
|
883
|
-
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
884
|
-
* relevant domain event on the `EventBus` instead; that is the
|
|
885
|
-
* properly awaited extension point.
|
|
886
|
-
*
|
|
887
|
-
* @param version - The version that was just persisted
|
|
888
|
-
*/
|
|
889
|
-
protected onPersisted(_version: Version): void;
|
|
890
|
-
/**
|
|
891
|
-
* Mutates state and records the resulting domain events in the
|
|
892
|
-
* **canonical record-after-mutation order**. Use this instead of calling
|
|
893
|
-
* `setState` + `addDomainEvent` separately and you cannot trip the
|
|
894
|
-
* "event for a fact that never happened" footgun.
|
|
895
|
-
*
|
|
896
|
-
* Order of operations:
|
|
897
|
-
* 1. `setState(newState, true)` — runs `validateState` first.
|
|
898
|
-
* If it throws, the method propagates and **no event is recorded
|
|
899
|
-
* and no version is bumped**.
|
|
900
|
-
* 2. Each event in `events` is appended via `addDomainEvent`.
|
|
901
|
-
*
|
|
902
|
-
* `commit()` **always bumps the version**, regardless of the aggregate's
|
|
903
|
-
* `autoVersionBump` config. Recording a domain event implies "something
|
|
904
|
-
* happened that the outside world cares about", and optimistic-
|
|
905
|
-
* concurrency callers must see a fresh version every time. The config
|
|
906
|
-
* still governs the un-coupled `setState` path. If you need to mutate
|
|
907
|
-
* state without bumping (e.g. cosmetic caches), call `setState(newState,
|
|
908
|
-
* false)` and skip `commit` entirely.
|
|
909
|
-
*
|
|
910
|
-
* `events` accepts a single event or an array. Omit it (or pass `[]`)
|
|
911
|
-
* for state-only mutations.
|
|
912
|
-
*
|
|
913
|
-
* @example
|
|
914
|
-
* ```ts
|
|
915
|
-
* confirm(): void {
|
|
916
|
-
* if (this.state.status === "confirmed") {
|
|
917
|
-
* throw new OrderAlreadyConfirmedError(this.id);
|
|
918
|
-
* }
|
|
919
|
-
* this.commit(
|
|
920
|
-
* { ...this.state, status: "confirmed" },
|
|
921
|
-
* { type: "OrderConfirmed", orderId: this.id },
|
|
922
|
-
* );
|
|
923
|
-
* }
|
|
924
|
-
* ```
|
|
925
|
-
*
|
|
926
|
-
* `EventSourcedAggregate.apply()` enforces the same ordering
|
|
927
|
-
* structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
|
|
928
|
-
* where `setState` and `addDomainEvent` are otherwise decoupled and the
|
|
929
|
-
* ordering is convention-only.
|
|
930
|
-
*
|
|
931
|
-
* @param newState - The new state (validated by `validateState`)
|
|
932
|
-
* @param events - One event, an array of events, or none (default)
|
|
933
|
-
*/
|
|
934
|
-
protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
|
|
935
|
-
protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
|
|
936
|
-
/**
|
|
937
|
-
* Records a domain event for later publication.
|
|
938
|
-
*
|
|
939
|
-
* **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
|
|
940
|
-
* explicit: a domain event describes something that has just happened
|
|
941
|
-
* to the aggregate — its existence implies the state change already
|
|
942
|
-
* occurred. Concretely:
|
|
943
|
-
*
|
|
944
|
-
* ```ts
|
|
945
|
-
* confirm(): void {
|
|
946
|
-
* if (this.state.status === "confirmed") {
|
|
947
|
-
* throw new OrderAlreadyConfirmedError(this.id);
|
|
948
|
-
* }
|
|
949
|
-
* this.setState({ ...this.state, status: "confirmed" }, true);
|
|
950
|
-
* this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
|
|
951
|
-
* // ↑ post-mutation. The event represents the committed fact.
|
|
952
|
-
* }
|
|
953
|
-
* ```
|
|
954
|
-
*
|
|
955
|
-
* Recording before mutation is a footgun: if a subsequent invariant
|
|
956
|
-
* check throws, the event has already been queued but the state never
|
|
957
|
-
* actually changed — consumers see an event for a fact that did not
|
|
958
|
-
* happen.
|
|
959
|
-
*
|
|
960
|
-
* `EventSourcedAggregate.apply()` enforces this ordering structurally;
|
|
961
|
-
* `AggregateRoot` leaves it as a convention because the state-mutation
|
|
962
|
-
* path (`setState`) is decoupled from event recording.
|
|
963
|
-
*
|
|
964
|
-
* @param event - The domain event to record
|
|
965
|
-
*/
|
|
966
|
-
protected addDomainEvent(event: TEvent): void;
|
|
967
|
-
/**
|
|
968
|
-
* Manually bumps the aggregate version.
|
|
969
|
-
* Call this after state changes for Optimistic Concurrency Control.
|
|
970
|
-
*
|
|
971
|
-
* If `autoVersionBump` is enabled, this is called automatically
|
|
972
|
-
* when using `setState()`.
|
|
973
|
-
*/
|
|
974
|
-
protected bumpVersion(): void;
|
|
975
|
-
/**
|
|
976
|
-
* Sets the state and optionally bumps the version automatically.
|
|
977
|
-
* This is a convenience method for state mutations.
|
|
978
|
-
* Automatically validates the newState using `validateState()`.
|
|
979
|
-
* Overrides Entity.setState to add version bumping.
|
|
980
|
-
*
|
|
981
|
-
* @param newState - The new state
|
|
982
|
-
* @param bumpVersion - Whether to bump the version (defaults to autoVersionBump config)
|
|
983
|
-
*/
|
|
984
|
-
protected setState(newState: TState, bumpVersion?: boolean): void;
|
|
985
|
-
/**
|
|
986
|
-
* Creates a snapshot of the current aggregate state.
|
|
987
|
-
* Useful for performance optimization, backup/restore, and audit trails.
|
|
988
|
-
*
|
|
989
|
-
* @returns A snapshot containing the current state and version
|
|
990
|
-
*
|
|
991
|
-
* @example
|
|
992
|
-
* ```typescript
|
|
993
|
-
* const snapshot = aggregate.createSnapshot();
|
|
994
|
-
* await snapshotRepository.save(aggregate.id, snapshot);
|
|
995
|
-
* ```
|
|
996
|
-
*/
|
|
997
|
-
createSnapshot(): AggregateSnapshot<TState>;
|
|
998
|
-
/**
|
|
999
|
-
* Restores the aggregate from a snapshot.
|
|
1000
|
-
* This is useful for loading aggregates from snapshots instead of
|
|
1001
|
-
* rebuilding them from scratch.
|
|
1002
|
-
* Validates the restored state.
|
|
1003
|
-
*
|
|
1004
|
-
* @param snapshot - The snapshot to restore from
|
|
1005
|
-
*
|
|
1006
|
-
* @example
|
|
1007
|
-
* ```typescript
|
|
1008
|
-
* const snapshot = await snapshotRepository.getLatest(aggregateId);
|
|
1009
|
-
* aggregate.restoreFromSnapshot(snapshot);
|
|
1010
|
-
* ```
|
|
1011
|
-
*/
|
|
1012
|
-
restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
/**
|
|
1016
|
-
* Abstract base for **domain-invariant violations**. Domain methods
|
|
1017
|
-
* (aggregates, entity validation hooks, value-object constructors)
|
|
1018
|
-
* throw `DomainError`-derived exceptions when a business rule is
|
|
1019
|
-
* violated. Consumers derive their own concrete errors — e.g.
|
|
1020
|
-
* `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
|
|
1021
|
-
* for `instanceof`-style catching at the App-Service boundary, where
|
|
1022
|
-
* they typically map to HTTP 400 / business-rule responses.
|
|
1023
|
-
*
|
|
1024
|
-
* The library itself does **not** ship any concrete `DomainError`
|
|
1025
|
-
* subclass — the kit can't know your invariants.
|
|
1026
|
-
*
|
|
1027
|
-
* Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
|
|
1028
|
-
* surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
|
|
1029
|
-
* `isRetryable`, …).
|
|
1030
|
-
*/
|
|
1031
|
-
declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
|
|
1032
|
-
}
|
|
1033
|
-
/**
|
|
1034
|
-
* Abstract base for **infrastructure / persistence failures** that the
|
|
1035
|
-
* App-Service can recover from — typically by retrying, by returning
|
|
1036
|
-
* HTTP 404 / 409, or by surfacing a "please try again" UX. These are
|
|
1037
|
-
* not domain-invariant violations (the business rules were not
|
|
1038
|
-
* broken); they describe race conditions and missing rows at the
|
|
1039
|
-
* storage boundary.
|
|
1040
|
-
*
|
|
1041
|
-
* Library-internal concrete subclasses: {@link AggregateNotFoundError},
|
|
1042
|
-
* {@link ConcurrencyConflictError}.
|
|
1043
|
-
*/
|
|
1044
|
-
declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Thrown by `EventSourcedAggregate.apply()` when no handler is
|
|
1048
|
-
* registered for the event's type. This means the aggregate's subclass
|
|
1049
|
-
* forgot to add an entry to its `handlers` map — a programming /
|
|
1050
|
-
* configuration bug, not a domain or infrastructure failure.
|
|
1051
|
-
*
|
|
1052
|
-
* Deliberately **not** on `DomainError` or `InfrastructureError` —
|
|
1053
|
-
* a generic `catch (e instanceof DomainError)` handler at the App
|
|
1054
|
-
* layer must not mask a forgotten handler; this should crash loud and
|
|
1055
|
-
* fail the calling Use Case so the bug surfaces in development. The
|
|
1056
|
-
* replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
|
|
1057
|
-
* also let it propagate uncaught instead of wrapping it in `Result.Err`.
|
|
1058
|
-
*
|
|
1059
|
-
* Use `isBaseError(e)` from `@shirudo/base-error` to detect
|
|
1060
|
-
* "any structured error from the kit or any other BaseError-using
|
|
1061
|
-
* library" at the App boundary.
|
|
1062
|
-
*/
|
|
1063
|
-
declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
|
|
1064
|
-
readonly eventType: string;
|
|
1065
|
-
constructor(eventType: string, cause?: unknown);
|
|
1066
|
-
}
|
|
1067
|
-
/**
|
|
1068
|
-
* Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
|
|
1069
|
-
* given id does not exist. `InfrastructureError` because the storage
|
|
1070
|
-
* boundary, not a business rule, decided the row is absent. Use the
|
|
1071
|
-
* nullable variant `getById()` if "not found" is a valid outcome.
|
|
1072
|
-
*
|
|
1073
|
-
* Accepts an optional `cause` so a `Repository.save()` implementation
|
|
1074
|
-
* can wrap a lower-level "row not found" / driver-level error without
|
|
1075
|
-
* losing context. Cause-chain helpers (`getRootCause`,
|
|
1076
|
-
* `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
|
|
1077
|
-
*
|
|
1078
|
-
* Not retryable — retrying won't make the row appear.
|
|
1079
|
-
*/
|
|
1080
|
-
declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
|
|
1081
|
-
readonly aggregateType: string;
|
|
1082
|
-
readonly id: string;
|
|
1083
|
-
constructor(aggregateType: string, id: string, cause?: unknown);
|
|
1084
|
-
}
|
|
1085
|
-
/**
|
|
1086
|
-
* Thrown by `IRepository.save()` when the aggregate's expected version
|
|
1087
|
-
* does not match the version currently persisted — i.e. another writer
|
|
1088
|
-
* updated the aggregate concurrently. The canonical optimistic-
|
|
1089
|
-
* concurrency signal; the App-Service typically reloads, re-applies
|
|
1090
|
-
* the use case, and retries, or surfaces HTTP 409 to the caller.
|
|
1091
|
-
*
|
|
1092
|
-
* `InfrastructureError` because the persistence layer (not a domain
|
|
1093
|
-
* rule) detects the race. Marks itself as `retryable: true` so the
|
|
1094
|
-
* `isRetryable` predicate from `@shirudo/base-error` picks it up.
|
|
1095
|
-
*/
|
|
1096
|
-
declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
|
|
1097
|
-
readonly aggregateType: string;
|
|
1098
|
-
readonly aggregateId: string;
|
|
1099
|
-
readonly expectedVersion: number;
|
|
1100
|
-
readonly actualVersion: number;
|
|
1101
|
-
/**
|
|
1102
|
-
* Marks this error as retryable so `isRetryable(err)` returns
|
|
1103
|
-
* true. The canonical OCC pattern is to reload the aggregate, re-apply
|
|
1104
|
-
* the use case, and retry on this exception.
|
|
1105
|
-
*/
|
|
1106
|
-
readonly retryable: true;
|
|
1107
|
-
constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
/**
|
|
1111
|
-
* Interface for Event-Sourced Aggregate Roots.
|
|
1112
|
-
* Defines the contract for aggregates that manage state changes via event sourcing.
|
|
1113
|
-
*
|
|
1114
|
-
* @template TId - The type of the aggregate root identifier
|
|
1115
|
-
* @template TEvent - The union type of all domain events
|
|
1116
|
-
*/
|
|
1117
|
-
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
|
|
1118
|
-
/**
|
|
1119
|
-
* Reconstitutes the aggregate from an event history. Returns `Result`
|
|
1120
|
-
* because event-stream corruption is an expected recoverable failure
|
|
1121
|
-
* at the infrastructure boundary.
|
|
1122
|
-
*
|
|
1123
|
-
* @param history - An ordered list of past events
|
|
1124
|
-
*/
|
|
1125
|
-
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1126
|
-
}
|
|
1127
|
-
type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
|
|
1128
|
-
/**
|
|
1129
|
-
* Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
|
|
1130
|
-
*
|
|
1131
|
-
* Like `AggregateRoot`, this is both the root entity and the aggregate boundary.
|
|
1132
|
-
* The difference is persistence: state is derived from events, not stored directly.
|
|
1133
|
-
* Events are the single source of truth — all state changes go through `apply()` → handler.
|
|
1134
|
-
*
|
|
1135
|
-
* Extends `Entity` directly (not `AggregateRoot`) so that `setState()` and
|
|
1136
|
-
* `addDomainEvent()` are not available. This enforces the event sourcing pattern
|
|
1137
|
-
* at the type level — there is no way to mutate state without going through an event handler.
|
|
1138
|
-
*
|
|
1139
|
-
* `apply()` and `validateEvent()` throw `DomainError`-derived exceptions on
|
|
1140
|
-
* invariant violations. Subclasses override `validateEvent()` to throw their
|
|
1141
|
-
* own concrete subclasses (e.g. `OrderAlreadyConfirmedError`). Only the
|
|
1142
|
-
* infrastructure-boundary methods (`loadFromHistory`,
|
|
1143
|
-
* `restoreFromSnapshotWithEvents`) return `Result` — they catch `DomainError`
|
|
1144
|
-
* during replay so callers can react to corrupted event streams without
|
|
1145
|
-
* try/catch.
|
|
1146
|
-
*
|
|
1147
|
-
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
1148
|
-
* @template TEvent - The union type of all domain events
|
|
1149
|
-
* @template TId - The type of the aggregate root identifier
|
|
1150
|
-
*
|
|
1151
|
-
* @example
|
|
1152
|
-
* ```typescript
|
|
1153
|
-
* class OrderAlreadyConfirmedError extends DomainError {
|
|
1154
|
-
* constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
|
|
1155
|
-
* }
|
|
1156
|
-
*
|
|
1157
|
-
* class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
1158
|
-
* confirm(): void {
|
|
1159
|
-
* this.apply(createDomainEvent("OrderConfirmed", {}));
|
|
1160
|
-
* }
|
|
1161
|
-
*
|
|
1162
|
-
* protected validateEvent(event: OrderEvent): void {
|
|
1163
|
-
* if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
|
|
1164
|
-
* throw new OrderAlreadyConfirmedError(this.id);
|
|
1165
|
-
* }
|
|
1166
|
-
* }
|
|
1167
|
-
*
|
|
1168
|
-
* protected readonly handlers = {
|
|
1169
|
-
* OrderConfirmed: (state: OrderState): OrderState => ({
|
|
1170
|
-
* ...state,
|
|
1171
|
-
* status: "confirmed",
|
|
1172
|
-
* }),
|
|
1173
|
-
* };
|
|
1174
|
-
* }
|
|
1175
|
-
* ```
|
|
1176
|
-
*/
|
|
1177
|
-
declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends Entity<TState, TId> implements IEventSourcedAggregate<TId, TEvent> {
|
|
897
|
+
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: ReadonlyArray<T>): TId[];
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Shared base for both `AggregateRoot` (state-stored) and
|
|
901
|
+
* `EventSourcedAggregate`. Carries the lifecycle machinery that's
|
|
902
|
+
* identical across the two flavours: version + persistedVersion
|
|
903
|
+
* tracking, pending events buffer, the `markRestored` (Post-Load) /
|
|
904
|
+
* `markPersisted` (Post-Save) lifecycle markers, and the
|
|
905
|
+
* `recordEvent` helper that auto-injects `aggregateId` +
|
|
906
|
+
* `aggregateType` on every event the aggregate emits.
|
|
907
|
+
*
|
|
908
|
+
* Consumers do NOT extend this class directly — extend
|
|
909
|
+
* `AggregateRoot` for state-stored aggregates or
|
|
910
|
+
* `EventSourcedAggregate` for event-sourced ones. The split between
|
|
911
|
+
* those two reflects the canonical Vernon §8 (state-stored) /
|
|
912
|
+
* Vernon §11 + Greg Young (event-sourced) distinction in how state
|
|
913
|
+
* is represented; the lifecycle machinery is the same for both.
|
|
914
|
+
*
|
|
915
|
+
* @template TState - The type of the aggregate state
|
|
916
|
+
* @template TId - The aggregate root identifier
|
|
917
|
+
* @template TEvent - The domain-event union. Defaults to `never` so
|
|
918
|
+
* aggregates without a declared event type cannot emit events
|
|
919
|
+
* (emitting any event becomes a compile error).
|
|
920
|
+
*/
|
|
921
|
+
declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
|
|
922
|
+
/**
|
|
923
|
+
* The aggregate's domain type as a string, used to populate
|
|
924
|
+
* `aggregateType` on events recorded via {@link recordEvent}.
|
|
925
|
+
*
|
|
926
|
+
* Subclasses MUST declare this as a string literal:
|
|
927
|
+
*
|
|
928
|
+
* ```ts
|
|
929
|
+
* class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
|
|
930
|
+
* protected readonly aggregateType = "Order";
|
|
931
|
+
* }
|
|
932
|
+
* ```
|
|
933
|
+
*
|
|
934
|
+
* The string is *the* identifier downstream consumers (outbox
|
|
935
|
+
* dispatchers, projection handlers, audit logs) use to route by
|
|
936
|
+
* aggregate kind. Use the same canonical name across your system —
|
|
937
|
+
* matching the class name is the obvious choice, but the value
|
|
938
|
+
* comes from this explicit declaration, not `constructor.name`
|
|
939
|
+
* (which is fragile under minification, bundler transforms, and
|
|
940
|
+
* subclass renaming).
|
|
941
|
+
*/
|
|
942
|
+
protected abstract readonly aggregateType: string;
|
|
1178
943
|
private _version;
|
|
1179
|
-
|
|
1180
|
-
|
|
944
|
+
/**
|
|
945
|
+
* DB-baseline version. `undefined` until the aggregate has been
|
|
946
|
+
* persisted or restored at least once. Repository implementations
|
|
947
|
+
* route INSERT vs UPDATE on this field and use it as the OCC
|
|
948
|
+
* baseline. See `IRepository.save` JSDoc.
|
|
949
|
+
*
|
|
950
|
+
* Distinct from {@link version}, which is the in-memory
|
|
951
|
+
* post-mutation value. Mutations bump `_version` but never touch
|
|
952
|
+
* `_persistedVersion` — that field only moves on {@link markRestored}
|
|
953
|
+
* (Post-Load) and {@link markPersisted} (Post-Save).
|
|
954
|
+
*/
|
|
955
|
+
private _persistedVersion;
|
|
1181
956
|
private _pendingEvents;
|
|
957
|
+
get version(): Version;
|
|
958
|
+
get persistedVersion(): Version | undefined;
|
|
959
|
+
/**
|
|
960
|
+
* Read-only list of domain events recorded on this aggregate that
|
|
961
|
+
* have not yet been flushed to the outbox / persistence layer.
|
|
962
|
+
*/
|
|
1182
963
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
964
|
+
/**
|
|
965
|
+
* Clears the pending-event list. Called by `markPersisted` after a
|
|
966
|
+
* successful write — the events have been handed off to the outbox
|
|
967
|
+
* / event store and are no longer the aggregate's responsibility.
|
|
968
|
+
*/
|
|
1183
969
|
clearPendingEvents(): void;
|
|
970
|
+
protected setVersion(version: Version): void;
|
|
971
|
+
/**
|
|
972
|
+
* Manually bumps the aggregate version. Used by state-stored
|
|
973
|
+
* aggregates' `setState(_, true)` / `commit()` paths and by the
|
|
974
|
+
* event-sourced replay path after each applied event.
|
|
975
|
+
*/
|
|
976
|
+
protected bumpVersion(): void;
|
|
977
|
+
/**
|
|
978
|
+
* **Lifecycle marker — Post-Load.** Syncs both `_version` and
|
|
979
|
+
* `_persistedVersion` to the DB-stored version. Used by
|
|
980
|
+
* `reconstitute(...)` factories to assemble an in-memory aggregate
|
|
981
|
+
* from a persisted row.
|
|
982
|
+
*
|
|
983
|
+
* Does NOT fire {@link onPersisted} — that hook has post-save
|
|
984
|
+
* semantics (metrics, audit, cache eviction), not post-load. The
|
|
985
|
+
* Factory-vs-Reconstitution distinction (Vernon §11) is honoured
|
|
986
|
+
* structurally: two separate markers, one for each transition.
|
|
987
|
+
*
|
|
988
|
+
* @param version - The version the row currently holds in the DB
|
|
989
|
+
*
|
|
990
|
+
* @example
|
|
991
|
+
* ```ts
|
|
992
|
+
* static reconstitute(id: OrderId, state: OrderState, version: Version): Order {
|
|
993
|
+
* const order = new Order(id, state);
|
|
994
|
+
* order.markRestored(version);
|
|
995
|
+
* return order;
|
|
996
|
+
* }
|
|
997
|
+
* ```
|
|
998
|
+
*/
|
|
999
|
+
protected markRestored(version: Version): void;
|
|
1184
1000
|
/**
|
|
1185
1001
|
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
1186
1002
|
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
@@ -1233,7 +1049,240 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
|
|
|
1233
1049
|
* @param version - The version that was just persisted
|
|
1234
1050
|
*/
|
|
1235
1051
|
protected onPersisted(_version: Version): void;
|
|
1236
|
-
|
|
1052
|
+
/**
|
|
1053
|
+
* Appends a domain event to the pending list. Prefer the higher-level
|
|
1054
|
+
* `AggregateRoot.commit()` (state-stored) or `EventSourcedAggregate.apply()`
|
|
1055
|
+
* (event-sourced) call sites — both wrap `addDomainEvent` in the
|
|
1056
|
+
* canonical record-AFTER-mutation order (Vernon §8). Calling
|
|
1057
|
+
* `addDomainEvent` directly is appropriate only when state and event
|
|
1058
|
+
* recording have already been decoupled deliberately (e.g. a
|
|
1059
|
+
* deletion event before a hard-delete; see `docs/guide/repository.md`).
|
|
1060
|
+
*/
|
|
1061
|
+
protected addDomainEvent(event: TEvent): void;
|
|
1062
|
+
/**
|
|
1063
|
+
* Creates a snapshot of the current aggregate state — the state at
|
|
1064
|
+
* this moment plus the version. Useful for ES snapshot policies and
|
|
1065
|
+
* for state-stored backup / restore.
|
|
1066
|
+
*/
|
|
1067
|
+
createSnapshot(): AggregateSnapshot<TState>;
|
|
1068
|
+
/**
|
|
1069
|
+
* Sugar for `createDomainEvent` that auto-injects `aggregateId`
|
|
1070
|
+
* (from `this.id`) and `aggregateType` (from {@link aggregateType})
|
|
1071
|
+
* into the event's metadata fields. This is the canonical path for
|
|
1072
|
+
* recording events from inside aggregate domain methods.
|
|
1073
|
+
*
|
|
1074
|
+
* Downstream consumers — outbox dispatchers, projection handlers,
|
|
1075
|
+
* audit logs — route by these two fields. Calling
|
|
1076
|
+
* `createDomainEvent(...)` directly inside an aggregate method
|
|
1077
|
+
* leaves them unset and is caught at the `withCommit` harvest
|
|
1078
|
+
* boundary, but `this.recordEvent(...)` makes the right thing
|
|
1079
|
+
* impossible to forget.
|
|
1080
|
+
*
|
|
1081
|
+
* @example
|
|
1082
|
+
* ```ts
|
|
1083
|
+
* class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
|
|
1084
|
+
* protected readonly aggregateType = "Order";
|
|
1085
|
+
*
|
|
1086
|
+
* confirm(): void {
|
|
1087
|
+
* this.commit(
|
|
1088
|
+
* { ...this.state, status: "confirmed" },
|
|
1089
|
+
* this.recordEvent("OrderConfirmed", { orderId: this.id }),
|
|
1090
|
+
* );
|
|
1091
|
+
* }
|
|
1092
|
+
* }
|
|
1093
|
+
* ```
|
|
1094
|
+
*
|
|
1095
|
+
* @param type - event type discriminator (must be one of `TEvent`'s tags)
|
|
1096
|
+
* @param payload - payload for that event subtype
|
|
1097
|
+
* @param options - any remaining `createDomainEvent` options
|
|
1098
|
+
* (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
|
|
1099
|
+
* and `aggregateType` are deliberately omitted — the helper sets
|
|
1100
|
+
* them.
|
|
1101
|
+
*/
|
|
1102
|
+
protected recordEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateDomainEventOptions, "aggregateId" | "aggregateType">): E;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Configuration options for AggregateRoot behavior.
|
|
1107
|
+
*/
|
|
1108
|
+
interface AggregateConfig {
|
|
1109
|
+
/**
|
|
1110
|
+
* Whether `setState()` should bump the version automatically when the
|
|
1111
|
+
* caller omits the per-call `bumpVersion` argument.
|
|
1112
|
+
*
|
|
1113
|
+
* Defaults to **`false`** — `setState()` already takes an explicit
|
|
1114
|
+
* `bumpVersion` argument per call, so the config is just the default
|
|
1115
|
+
* the per-call argument falls back to. Set to `true` only if you have
|
|
1116
|
+
* a subclass that never passes `bumpVersion` and you want every state
|
|
1117
|
+
* change to advance the version anyway.
|
|
1118
|
+
*/
|
|
1119
|
+
autoVersionBump?: boolean;
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Base class for Aggregate Roots without Event Sourcing.
|
|
1123
|
+
*
|
|
1124
|
+
* In DDD (Evans), an Aggregate is a cluster of objects — root entity, child entities,
|
|
1125
|
+
* and value objects — treated as a unit for consistency. The **Aggregate Root** is the
|
|
1126
|
+
* root entity that represents the aggregate externally and is the only entry point
|
|
1127
|
+
* for external code. This class serves as both: it IS the root entity and it contains
|
|
1128
|
+
* the aggregate state (`TState`) which holds child entities and value objects.
|
|
1129
|
+
*
|
|
1130
|
+
* Provides:
|
|
1131
|
+
* - Identity (id) and state management (via `Entity`)
|
|
1132
|
+
* - Version + persistedVersion + pending-event tracking (via `BaseAggregate`)
|
|
1133
|
+
* - `setState`-based mutation with optional version bumping
|
|
1134
|
+
* - `commit()` record-after-mutation helper
|
|
1135
|
+
* - Snapshot support for performance optimization
|
|
1136
|
+
*
|
|
1137
|
+
* All changes to child entities within `TState` are versioned through this root.
|
|
1138
|
+
* Use `setState()` for state mutations to ensure invariant validation.
|
|
1139
|
+
*
|
|
1140
|
+
* For event sourcing, use `EventSourcedAggregate` instead.
|
|
1141
|
+
*
|
|
1142
|
+
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
1143
|
+
* @template TId - The type of the aggregate root identifier
|
|
1144
|
+
* @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.
|
|
1145
|
+
*
|
|
1146
|
+
* @example
|
|
1147
|
+
* ```typescript
|
|
1148
|
+
* // Order is an Aggregate Root (an Entity with version)
|
|
1149
|
+
* class Order extends AggregateRoot<OrderState, OrderId> {
|
|
1150
|
+
* protected readonly aggregateType = "Order";
|
|
1151
|
+
*
|
|
1152
|
+
* constructor(id: OrderId, initialState: OrderState) {
|
|
1153
|
+
* super(id, initialState);
|
|
1154
|
+
* }
|
|
1155
|
+
*
|
|
1156
|
+
* confirm(): void {
|
|
1157
|
+
* this.commit(
|
|
1158
|
+
* { ...this.state, status: "confirmed" },
|
|
1159
|
+
* this.recordEvent("OrderConfirmed", { orderId: this.id }),
|
|
1160
|
+
* );
|
|
1161
|
+
* }
|
|
1162
|
+
* }
|
|
1163
|
+
* ```
|
|
1164
|
+
*/
|
|
1165
|
+
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends BaseAggregate<TState, TId, TEvent> {
|
|
1166
|
+
private readonly _autoVersionBump;
|
|
1167
|
+
protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
|
|
1168
|
+
/**
|
|
1169
|
+
* Mutates state and records the resulting domain events in the
|
|
1170
|
+
* **canonical record-after-mutation order**. Use this instead of calling
|
|
1171
|
+
* `setState` + `addDomainEvent` separately and you cannot trip the
|
|
1172
|
+
* "event for a fact that never happened" footgun.
|
|
1173
|
+
*
|
|
1174
|
+
* Order of operations:
|
|
1175
|
+
* 1. `setState(newState, true)` — runs `validateState` first.
|
|
1176
|
+
* If it throws, the method propagates and **no event is recorded
|
|
1177
|
+
* and no version is bumped**.
|
|
1178
|
+
* 2. Each event in `events` is appended via `addDomainEvent`.
|
|
1179
|
+
*
|
|
1180
|
+
* `commit()` **always bumps the version**, regardless of the aggregate's
|
|
1181
|
+
* `autoVersionBump` config. Recording a domain event implies "something
|
|
1182
|
+
* happened that the outside world cares about", and optimistic-
|
|
1183
|
+
* concurrency callers must see a fresh version every time. The config
|
|
1184
|
+
* still governs the un-coupled `setState` path. If you need to mutate
|
|
1185
|
+
* state without bumping (e.g. cosmetic caches), call `setState(newState,
|
|
1186
|
+
* false)` and skip `commit` entirely.
|
|
1187
|
+
*
|
|
1188
|
+
* `events` accepts a single event or an array. Omit it (or pass `[]`)
|
|
1189
|
+
* for state-only mutations.
|
|
1190
|
+
*
|
|
1191
|
+
* @example
|
|
1192
|
+
* ```ts
|
|
1193
|
+
* confirm(): void {
|
|
1194
|
+
* if (this.state.status === "confirmed") {
|
|
1195
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
1196
|
+
* }
|
|
1197
|
+
* this.commit(
|
|
1198
|
+
* { ...this.state, status: "confirmed" },
|
|
1199
|
+
* this.recordEvent("OrderConfirmed", { orderId: this.id }),
|
|
1200
|
+
* );
|
|
1201
|
+
* }
|
|
1202
|
+
* ```
|
|
1203
|
+
*
|
|
1204
|
+
* `EventSourcedAggregate.apply()` enforces the same ordering
|
|
1205
|
+
* structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
|
|
1206
|
+
* where `setState` and `addDomainEvent` are otherwise decoupled and the
|
|
1207
|
+
* ordering is convention-only.
|
|
1208
|
+
*
|
|
1209
|
+
* @param newState - The new state (validated by `validateState`)
|
|
1210
|
+
* @param events - One event, an array of events, or none (default)
|
|
1211
|
+
*/
|
|
1212
|
+
protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
|
|
1213
|
+
/**
|
|
1214
|
+
* Sets the state and optionally bumps the version automatically.
|
|
1215
|
+
* Validates `newState` via `validateState()`.
|
|
1216
|
+
*
|
|
1217
|
+
* @param newState - The new state
|
|
1218
|
+
* @param bumpVersion - Whether to bump the version (defaults to autoVersionBump config)
|
|
1219
|
+
*/
|
|
1220
|
+
protected setState(newState: TState, bumpVersion?: boolean): void;
|
|
1221
|
+
/**
|
|
1222
|
+
* Restores the aggregate from a snapshot — loads state and aligns
|
|
1223
|
+
* `version` + `persistedVersion` to the snapshot version. Validates
|
|
1224
|
+
* the restored state.
|
|
1225
|
+
*
|
|
1226
|
+
* @param snapshot - The snapshot to restore from
|
|
1227
|
+
*/
|
|
1228
|
+
restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEvent) => TState;
|
|
1232
|
+
/**
|
|
1233
|
+
* Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
|
|
1234
|
+
*
|
|
1235
|
+
* Like `AggregateRoot`, this is both the root entity and the aggregate
|
|
1236
|
+
* boundary. The difference is persistence: state is derived from events,
|
|
1237
|
+
* not stored directly. Events are the single source of truth — all state
|
|
1238
|
+
* changes go through `apply()` → handler.
|
|
1239
|
+
*
|
|
1240
|
+
* Extends `BaseAggregate` (the shared lifecycle machinery) but does NOT
|
|
1241
|
+
* expose `setState()` or `commit()` from `AggregateRoot`. This enforces
|
|
1242
|
+
* the event sourcing pattern at the type level — there is no way to
|
|
1243
|
+
* mutate state without going through an event handler.
|
|
1244
|
+
*
|
|
1245
|
+
* `apply()` and `validateEvent()` throw `DomainError`-derived exceptions
|
|
1246
|
+
* on invariant violations. Subclasses override `validateEvent()` to
|
|
1247
|
+
* throw their own concrete subclasses (e.g. `OrderAlreadyConfirmedError`).
|
|
1248
|
+
* Only the infrastructure-boundary methods (`loadFromHistory`,
|
|
1249
|
+
* `restoreFromSnapshotWithEvents`) return `Result` — they catch
|
|
1250
|
+
* `DomainError` during replay so callers can react to corrupted event
|
|
1251
|
+
* streams without try/catch.
|
|
1252
|
+
*
|
|
1253
|
+
* @template TState - The aggregate state (contains child entities and value objects)
|
|
1254
|
+
* @template TEvent - The union type of all domain events
|
|
1255
|
+
* @template TId - The aggregate root identifier
|
|
1256
|
+
*
|
|
1257
|
+
* @example
|
|
1258
|
+
* ```typescript
|
|
1259
|
+
* class OrderAlreadyConfirmedError extends DomainError {
|
|
1260
|
+
* constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
|
|
1261
|
+
* }
|
|
1262
|
+
*
|
|
1263
|
+
* class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
1264
|
+
* protected readonly aggregateType = "Order";
|
|
1265
|
+
*
|
|
1266
|
+
* confirm(): void {
|
|
1267
|
+
* this.apply(this.recordEvent("OrderConfirmed", { orderId: this.id }));
|
|
1268
|
+
* }
|
|
1269
|
+
*
|
|
1270
|
+
* protected validateEvent(event: OrderEvent): void {
|
|
1271
|
+
* if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
|
|
1272
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
1273
|
+
* }
|
|
1274
|
+
* }
|
|
1275
|
+
*
|
|
1276
|
+
* protected readonly handlers = {
|
|
1277
|
+
* OrderConfirmed: (state: OrderState): OrderState => ({
|
|
1278
|
+
* ...state,
|
|
1279
|
+
* status: "confirmed",
|
|
1280
|
+
* }),
|
|
1281
|
+
* };
|
|
1282
|
+
* }
|
|
1283
|
+
* ```
|
|
1284
|
+
*/
|
|
1285
|
+
declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends BaseAggregate<TState, TId, TEvent> implements IEventSourcedAggregate<TId, TEvent> {
|
|
1237
1286
|
/**
|
|
1238
1287
|
* Validates an event before it is applied. Default is no-op.
|
|
1239
1288
|
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
@@ -1281,10 +1330,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
|
|
|
1281
1330
|
* 2 events ends at v=3, not v=2.
|
|
1282
1331
|
*/
|
|
1283
1332
|
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
1284
|
-
/**
|
|
1285
|
-
* Creates a snapshot of the current aggregate state.
|
|
1286
|
-
*/
|
|
1287
|
-
createSnapshot(): AggregateSnapshot<TState>;
|
|
1288
1333
|
/**
|
|
1289
1334
|
* Restores the aggregate from a snapshot and applies events that occurred
|
|
1290
1335
|
* after. Same infrastructure-boundary semantics as `loadFromHistory`:
|
|
@@ -1307,56 +1352,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
|
|
|
1307
1352
|
};
|
|
1308
1353
|
}
|
|
1309
1354
|
|
|
1310
|
-
type Version = number & {
|
|
1311
|
-
readonly __v: true;
|
|
1312
|
-
};
|
|
1313
|
-
/**
|
|
1314
|
-
* Snapshot of an aggregate state at a specific point in time.
|
|
1315
|
-
* Used for optimizing event replay by starting from a snapshot
|
|
1316
|
-
* instead of replaying all events from the beginning.
|
|
1317
|
-
*
|
|
1318
|
-
* @template TState - The type of the aggregate state
|
|
1319
|
-
*/
|
|
1320
|
-
interface AggregateSnapshot<TState> {
|
|
1321
|
-
/**
|
|
1322
|
-
* The state of the aggregate at the time of the snapshot.
|
|
1323
|
-
*/
|
|
1324
|
-
state: TState;
|
|
1325
|
-
/**
|
|
1326
|
-
* The version of the aggregate when the snapshot was taken.
|
|
1327
|
-
*/
|
|
1328
|
-
version: Version;
|
|
1329
|
-
/**
|
|
1330
|
-
* Timestamp when the snapshot was created.
|
|
1331
|
-
*/
|
|
1332
|
-
snapshotAt: Date;
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Checks if two aggregates are at the same version (same ID and version).
|
|
1336
|
-
* Useful for optimistic concurrency control checks.
|
|
1337
|
-
*
|
|
1338
|
-
* Note: Two aggregates with the same ID ARE the same aggregate (identity).
|
|
1339
|
-
* This function checks if they are at the same version — i.e., no concurrent modification.
|
|
1340
|
-
*
|
|
1341
|
-
* @example
|
|
1342
|
-
* ```typescript
|
|
1343
|
-
* const before = await repository.getById(id);
|
|
1344
|
-
* // ... some operations ...
|
|
1345
|
-
* const after = await repository.getById(id);
|
|
1346
|
-
*
|
|
1347
|
-
* if (!sameVersion(before, after)) {
|
|
1348
|
-
* throw new Error("Aggregate was modified by another process");
|
|
1349
|
-
* }
|
|
1350
|
-
* ```
|
|
1351
|
-
*/
|
|
1352
|
-
declare function sameVersion<TId extends Id<string>>(a: {
|
|
1353
|
-
id: TId;
|
|
1354
|
-
version: Version;
|
|
1355
|
-
}, b: {
|
|
1356
|
-
id: TId;
|
|
1357
|
-
version: Version;
|
|
1358
|
-
}): boolean;
|
|
1359
|
-
|
|
1360
1355
|
/**
|
|
1361
1356
|
* Marker interface for Commands.
|
|
1362
1357
|
* Commands represent write operations that change system state.
|
|
@@ -2213,26 +2208,25 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2213
2208
|
* stored (optimistic concurrency).
|
|
2214
2209
|
* 2. Write the aggregate to durable storage.
|
|
2215
2210
|
*
|
|
2216
|
-
* **Insert vs update —
|
|
2217
|
-
*
|
|
2218
|
-
*
|
|
2219
|
-
*
|
|
2220
|
-
*
|
|
2221
|
-
*
|
|
2211
|
+
* **Insert vs update — `persistedVersion` convention.** Every aggregate
|
|
2212
|
+
* exposes two version fields with distinct roles:
|
|
2213
|
+
*
|
|
2214
|
+
* - `aggregate.version` — in-memory post-mutation value, bumped by
|
|
2215
|
+
* `setState(_, true)` / `commit()` / `apply()`. NOT the right
|
|
2216
|
+
* routing key, because mutations can advance it past zero while
|
|
2217
|
+
* the DB row still does not exist.
|
|
2218
|
+
* - `aggregate.persistedVersion` — what the persistence layer holds.
|
|
2219
|
+
* `undefined` until the aggregate has been persisted or restored
|
|
2220
|
+
* at least once. This is the routing key.
|
|
2222
2221
|
*
|
|
2223
|
-
* - `aggregate.
|
|
2224
|
-
*
|
|
2222
|
+
* - `aggregate.persistedVersion === undefined` → **INSERT** (never
|
|
2223
|
+
* persisted; write succeeds unconditionally or fails the unique
|
|
2225
2224
|
* constraint on `id`).
|
|
2226
|
-
* -
|
|
2227
|
-
* `WHERE id = ? AND version =
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2230
|
-
*
|
|
2231
|
-
* version-bump semantics differ across the two aggregate flavours
|
|
2232
|
-
* (state-stored aggregates bump on the user's call to `setState(_,
|
|
2233
|
-
* true)`; event-sourced aggregates bump on every `apply()` by
|
|
2234
|
-
* definition). The `version === 0` invariant for "never persisted" is
|
|
2235
|
-
* the common contract.
|
|
2225
|
+
* - otherwise → **UPDATE** with the OCC predicate
|
|
2226
|
+
* `WHERE id = ? AND version = aggregate.persistedVersion` (the
|
|
2227
|
+
* load-time / last-save baseline, not the post-mutation in-memory
|
|
2228
|
+
* value). If the row count is `0`, another writer raced you —
|
|
2229
|
+
* throw `ConcurrencyConflictError`.
|
|
2236
2230
|
*
|
|
2237
2231
|
* Do **not** call `aggregate.markPersisted(...)` here. The library's
|
|
2238
2232
|
* `withCommit` orchestrator handles the post-save lifecycle (harvest
|