@shirudo/ddd-kit 1.0.0-rc.6 → 1.0.0-rc.8
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 +399 -19
- package/dist/index.js +221 -14
- package/dist/index.js.map +1 -1
- package/package.json +72 -71
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ type Id<Tag extends string> = string & {
|
|
|
25
25
|
* generator type — `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
|
|
26
26
|
* with no caller-side generic to abuse.
|
|
27
27
|
*
|
|
28
|
+
* **Your factory must produce unique ids under concurrent calls.**
|
|
29
|
+
* The kit makes no attempt to dedupe or detect collisions — a collision
|
|
30
|
+
* silently overwrites earlier rows (under unique-key constraints) or
|
|
31
|
+
* silently aliases two different entities (without them). Safe choices:
|
|
32
|
+
* `crypto.randomUUID()` (UUIDv4, the default for events), ULID, UUIDv7,
|
|
33
|
+
* KSUID — all collision-resistant by design. Unsafe choices: `Date.now()`
|
|
34
|
+
* alone (duplicates within the same millisecond), a process-local
|
|
35
|
+
* counter without persistence (resets to 1 on restart, collides with
|
|
36
|
+
* prior runs), a sequential id derived from non-atomic state.
|
|
37
|
+
*
|
|
28
38
|
* @example
|
|
29
39
|
* ```ts
|
|
30
40
|
* import { ulid } from "ulid";
|
|
@@ -71,12 +81,56 @@ type EventIdFactory = () => string;
|
|
|
71
81
|
*
|
|
72
82
|
* **Module-scoped — last setter wins.** The factory lives as a single
|
|
73
83
|
* module variable; importing two libraries that both call this races on
|
|
74
|
-
* load order
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
84
|
+
* load order, and parallel test workers will see each other's factory.
|
|
85
|
+
* For test isolation and short-lived contexts prefer
|
|
86
|
+
* {@link withEventIdFactory}; for multi-tenant request isolation
|
|
87
|
+
* (e.g. one factory per tenant in a single Worker invocation) **prefer
|
|
88
|
+
* the per-call `options.eventId`** instead of mutating the global. Same
|
|
89
|
+
* caveat applies to `setClockFactory`.
|
|
78
90
|
*/
|
|
79
91
|
declare function setEventIdFactory(factory: EventIdFactory): void;
|
|
92
|
+
/**
|
|
93
|
+
* Scoped variant of {@link setEventIdFactory}: installs `factory`,
|
|
94
|
+
* runs `fn`, then restores the previous factory in a `finally` block —
|
|
95
|
+
* so the restoration happens even if `fn` throws. Safe for parallel
|
|
96
|
+
* tests and for synchronous request handlers that need a tenant-
|
|
97
|
+
* specific factory without polluting the global.
|
|
98
|
+
*
|
|
99
|
+
* **Synchronous-only — enforced at runtime.** If `fn` returns a
|
|
100
|
+
* thenable (a `Promise` or any object with a `then` method), the
|
|
101
|
+
* helper throws *before* returning the value to the caller. This
|
|
102
|
+
* catches the async-misuse footgun where the factory would be
|
|
103
|
+
* restored before the awaited body of `fn` runs, leaving the awaited
|
|
104
|
+
* code reading the previous factory. For async scoping across `await`
|
|
105
|
+
* boundaries, use `AsyncLocalStorage` — out of scope for this helper;
|
|
106
|
+
* build it on top if you need it.
|
|
107
|
+
*
|
|
108
|
+
* Composes by nesting: an inner `withEventIdFactory` restores back to
|
|
109
|
+
* the outer's factory; the outer restores to the original.
|
|
110
|
+
*
|
|
111
|
+
* **When to prefer the per-call `options.eventId` instead.** If you're
|
|
112
|
+
* constructing a single event and want full control over its id,
|
|
113
|
+
* passing `{ eventId: "..." }` to `createDomainEvent` is the strongest
|
|
114
|
+
* isolation — it bypasses the factory mechanism entirely, no global
|
|
115
|
+
* mutation, no scope to manage. Reach for `withEventIdFactory` when
|
|
116
|
+
* the events are constructed deep inside domain methods you can't
|
|
117
|
+
* thread an explicit id through (typical test scenario), or when many
|
|
118
|
+
* events in a scope should share the same factory.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* // In a vitest test:
|
|
123
|
+
* it("emits deterministic ids", () => {
|
|
124
|
+
* withEventIdFactory(() => "evt-fixed", () => {
|
|
125
|
+
* const e = createDomainEvent("X", { v: 1 });
|
|
126
|
+
* expect(e.eventId).toBe("evt-fixed");
|
|
127
|
+
* });
|
|
128
|
+
* // Outside the callback the default crypto.randomUUID is restored,
|
|
129
|
+
* // even if the body had thrown.
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
declare function withEventIdFactory<T>(factory: EventIdFactory, fn: () => T): T;
|
|
80
134
|
/**
|
|
81
135
|
* Restores the default event-id factory (`crypto.randomUUID()`).
|
|
82
136
|
* Intended for use in test `afterEach` hooks.
|
|
@@ -102,8 +156,37 @@ type ClockFactory = () => Date;
|
|
|
102
156
|
*
|
|
103
157
|
* The per-call `options.occurredAt` override always wins over this
|
|
104
158
|
* factory. Symmetric to `setEventIdFactory`.
|
|
159
|
+
*
|
|
160
|
+
* Module-scoped — see {@link setEventIdFactory} for the global-state
|
|
161
|
+
* caveats. For test isolation prefer {@link withClockFactory}; for
|
|
162
|
+
* multi-tenant request isolation prefer the per-call
|
|
163
|
+
* `options.occurredAt`.
|
|
105
164
|
*/
|
|
106
165
|
declare function setClockFactory(factory: ClockFactory): void;
|
|
166
|
+
/**
|
|
167
|
+
* Scoped variant of {@link setClockFactory}: installs `factory`, runs
|
|
168
|
+
* `fn`, then restores the previous factory in a `finally` block.
|
|
169
|
+
* Synchronous-only — same constraints (and same runtime thenable
|
|
170
|
+
* guard) as {@link withEventIdFactory}.
|
|
171
|
+
*
|
|
172
|
+
* **When to prefer the per-call `options.occurredAt` instead.** Same
|
|
173
|
+
* trade-off as {@link withEventIdFactory}: passing `{ occurredAt }`
|
|
174
|
+
* to `createDomainEvent` is the strongest isolation for single-event
|
|
175
|
+
* cases. The scoped helper is for events constructed deep inside
|
|
176
|
+
* domain methods where threading an explicit timestamp is awkward.
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* it("stamps events with a fixed clock", () => {
|
|
181
|
+
* const fixed = new Date("2026-01-01T00:00:00Z");
|
|
182
|
+
* withClockFactory(() => fixed, () => {
|
|
183
|
+
* const e = createDomainEvent("X", { v: 1 });
|
|
184
|
+
* expect(e.occurredAt).toEqual(fixed);
|
|
185
|
+
* });
|
|
186
|
+
* });
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
declare function withClockFactory<T>(factory: ClockFactory, fn: () => T): T;
|
|
107
190
|
/**
|
|
108
191
|
* Restores the default clock factory (`() => new Date()`).
|
|
109
192
|
* Intended for use in test `afterEach` hooks.
|
|
@@ -231,9 +314,25 @@ interface CreateDomainEventOptions {
|
|
|
231
314
|
* Creates a domain event with default values.
|
|
232
315
|
* Sets occurredAt to current date and version to 1 if not provided.
|
|
233
316
|
*
|
|
317
|
+
* **For aggregate-internal events, prefer `this.recordEvent(...)` on
|
|
318
|
+
* `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
|
|
319
|
+
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
320
|
+
* aggregate's declared `aggregateType` property), which downstream
|
|
321
|
+
* consumers — outbox dispatchers, projection handlers, audit logs —
|
|
322
|
+
* route by. The `withCommit` harvest boundary now validates both fields
|
|
323
|
+
* are present and throws if they're missing, so a direct
|
|
324
|
+
* `createDomainEvent(...)` call inside an aggregate that forgets the
|
|
325
|
+
* options is caught at runtime.
|
|
326
|
+
*
|
|
327
|
+
* Use `createDomainEvent(...)` directly for events that don't belong to
|
|
328
|
+
* an aggregate: system events, integration events, configuration events,
|
|
329
|
+
* test fixtures. For those, set `aggregateId` / `aggregateType` in
|
|
330
|
+
* `options` if downstream consumers expect routing metadata.
|
|
331
|
+
*
|
|
234
332
|
* @param type - The event type
|
|
235
333
|
* @param payload - The event payload
|
|
236
|
-
* @param options - Optional event configuration
|
|
334
|
+
* @param options - Optional event configuration (including `aggregateId`
|
|
335
|
+
* and `aggregateType` for routing)
|
|
237
336
|
* @returns A domain event
|
|
238
337
|
*
|
|
239
338
|
* @example
|
|
@@ -735,7 +834,28 @@ interface AggregateConfig {
|
|
|
735
834
|
* }
|
|
736
835
|
* ```
|
|
737
836
|
*/
|
|
738
|
-
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
|
|
837
|
+
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId, TEvent> {
|
|
838
|
+
/**
|
|
839
|
+
* The aggregate's domain type as a string, used to populate
|
|
840
|
+
* `aggregateType` on events recorded via {@link recordEvent}.
|
|
841
|
+
*
|
|
842
|
+
* Subclasses MUST declare this as a string literal:
|
|
843
|
+
*
|
|
844
|
+
* ```ts
|
|
845
|
+
* class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
|
|
846
|
+
* protected readonly aggregateType = "Order";
|
|
847
|
+
* }
|
|
848
|
+
* ```
|
|
849
|
+
*
|
|
850
|
+
* The string is *the* identifier downstream consumers (outbox
|
|
851
|
+
* dispatchers, projection handlers, audit logs) use to route by
|
|
852
|
+
* aggregate kind. Use the same canonical name across your system —
|
|
853
|
+
* matching the class name is the obvious choice, but the value
|
|
854
|
+
* comes from this explicit declaration, not `constructor.name`
|
|
855
|
+
* (which is fragile under minification, bundler transforms, and
|
|
856
|
+
* subclass renaming).
|
|
857
|
+
*/
|
|
858
|
+
protected abstract readonly aggregateType: string;
|
|
739
859
|
private _version;
|
|
740
860
|
get version(): Version;
|
|
741
861
|
protected setVersion(version: Version): void;
|
|
@@ -753,16 +873,57 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = ne
|
|
|
753
873
|
*/
|
|
754
874
|
clearPendingEvents(): void;
|
|
755
875
|
/**
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
876
|
+
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
877
|
+
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
878
|
+
* to push the persisted version back into the in-memory aggregate and
|
|
879
|
+
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
880
|
+
* subclasses **should not** override this method directly.
|
|
881
|
+
*
|
|
882
|
+
* Overriding without calling `super.markPersisted(version)` silently
|
|
883
|
+
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
884
|
+
* through the outbox, double-emitting events. This bug has been hit
|
|
885
|
+
* in production by consumers; the {@link onPersisted} hook below is
|
|
886
|
+
* the safer extension point.
|
|
887
|
+
*
|
|
888
|
+
* If you must override (legitimate cases are very rare), call
|
|
889
|
+
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
890
|
+
* runs, then add your logic afterwards.
|
|
760
891
|
*
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
* call.
|
|
892
|
+
* @param version - The version assigned by the persistence layer
|
|
893
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
764
894
|
*/
|
|
765
895
|
markPersisted(version: Version): void;
|
|
896
|
+
/**
|
|
897
|
+
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
898
|
+
* updated the version and cleared `pendingEvents`. Override this for
|
|
899
|
+
* post-persist logging, metrics, or cache-eviction without risk of
|
|
900
|
+
* breaking the framework's pendingEvents cleanup.
|
|
901
|
+
*
|
|
902
|
+
* The default implementation is a no-op. Subclasses do NOT need to
|
|
903
|
+
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
904
|
+
* implementation to preserve.
|
|
905
|
+
*
|
|
906
|
+
* **`onPersisted` deliberately receives only the version, not the
|
|
907
|
+
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
908
|
+
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
909
|
+
* subscribers or the outbox dispatcher — that is the proper
|
|
910
|
+
* Aggregate-Boundary separation. Building event-aware logic into
|
|
911
|
+
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
912
|
+
* recreates the boundary problems Vernon's aggregate discipline is
|
|
913
|
+
* meant to prevent.
|
|
914
|
+
*
|
|
915
|
+
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
916
|
+
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
917
|
+
* permissive `void` will accept an `async`-override returning
|
|
918
|
+
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
919
|
+
* any rejection becomes an unhandled rejection and `withCommit`
|
|
920
|
+
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
921
|
+
* relevant domain event on the `EventBus` instead; that is the
|
|
922
|
+
* properly awaited extension point.
|
|
923
|
+
*
|
|
924
|
+
* @param version - The version that was just persisted
|
|
925
|
+
*/
|
|
926
|
+
protected onPersisted(_version: Version): void;
|
|
766
927
|
/**
|
|
767
928
|
* Mutates state and records the resulting domain events in the
|
|
768
929
|
* **canonical record-after-mutation order**. Use this instead of calling
|
|
@@ -840,6 +1001,41 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = ne
|
|
|
840
1001
|
* @param event - The domain event to record
|
|
841
1002
|
*/
|
|
842
1003
|
protected addDomainEvent(event: TEvent): void;
|
|
1004
|
+
/**
|
|
1005
|
+
* Sugar for `createDomainEvent` that auto-injects `aggregateId`
|
|
1006
|
+
* (from `this.id`) and `aggregateType` (from {@link aggregateType})
|
|
1007
|
+
* into the event's metadata fields. This is the canonical path for
|
|
1008
|
+
* recording events from inside aggregate domain methods.
|
|
1009
|
+
*
|
|
1010
|
+
* Downstream consumers — outbox dispatchers, projection handlers,
|
|
1011
|
+
* audit logs — route by these two fields. Calling
|
|
1012
|
+
* `createDomainEvent(...)` directly inside an aggregate method
|
|
1013
|
+
* leaves them unset and is caught at the `withCommit` harvest
|
|
1014
|
+
* boundary, but `this.recordEvent(...)` makes the right thing
|
|
1015
|
+
* impossible to forget.
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* ```ts
|
|
1019
|
+
* class Order extends AggregateRoot<OrderState, OrderId, OrderEvent> {
|
|
1020
|
+
* protected readonly aggregateType = "Order";
|
|
1021
|
+
*
|
|
1022
|
+
* confirm(): void {
|
|
1023
|
+
* this.commit(
|
|
1024
|
+
* { ...this.state, status: "confirmed" },
|
|
1025
|
+
* this.recordEvent("OrderConfirmed", { orderId: this.id }),
|
|
1026
|
+
* );
|
|
1027
|
+
* }
|
|
1028
|
+
* }
|
|
1029
|
+
* ```
|
|
1030
|
+
*
|
|
1031
|
+
* @param type - event type discriminator (must be one of `TEvent`'s tags)
|
|
1032
|
+
* @param payload - payload for that event subtype
|
|
1033
|
+
* @param options - any remaining `createDomainEvent` options
|
|
1034
|
+
* (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
|
|
1035
|
+
* and `aggregateType` are deliberately omitted — the helper sets
|
|
1036
|
+
* them.
|
|
1037
|
+
*/
|
|
1038
|
+
protected recordEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateDomainEventOptions, "aggregateId" | "aggregateType">): E;
|
|
843
1039
|
/**
|
|
844
1040
|
* Manually bumps the aggregate version.
|
|
845
1041
|
* Call this after state changes for Optimistic Concurrency Control.
|
|
@@ -1051,6 +1247,25 @@ type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEv
|
|
|
1051
1247
|
* ```
|
|
1052
1248
|
*/
|
|
1053
1249
|
declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEvent, TId extends Id<string>> extends Entity<TState, TId> implements IEventSourcedAggregate<TId, TEvent> {
|
|
1250
|
+
/**
|
|
1251
|
+
* The aggregate's domain type as a string, used to populate
|
|
1252
|
+
* `aggregateType` on events recorded via {@link recordEvent}.
|
|
1253
|
+
*
|
|
1254
|
+
* Subclasses MUST declare this as a string literal:
|
|
1255
|
+
*
|
|
1256
|
+
* ```ts
|
|
1257
|
+
* class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
1258
|
+
* protected readonly aggregateType = "Order";
|
|
1259
|
+
* }
|
|
1260
|
+
* ```
|
|
1261
|
+
*
|
|
1262
|
+
* Downstream consumers (outbox dispatchers, projection handlers,
|
|
1263
|
+
* audit logs) route by this. Use the canonical aggregate name
|
|
1264
|
+
* consistently across your bounded context. The value comes from
|
|
1265
|
+
* this explicit declaration, not `constructor.name` (fragile under
|
|
1266
|
+
* minification + bundler transforms).
|
|
1267
|
+
*/
|
|
1268
|
+
protected abstract readonly aggregateType: string;
|
|
1054
1269
|
private _version;
|
|
1055
1270
|
get version(): Version;
|
|
1056
1271
|
private setVersion;
|
|
@@ -1058,13 +1273,83 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
|
|
|
1058
1273
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
1059
1274
|
clearPendingEvents(): void;
|
|
1060
1275
|
/**
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
* `
|
|
1276
|
+
* **Framework lifecycle method — `@sealed`.** Called by `withCommit`
|
|
1277
|
+
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
1278
|
+
* to push the persisted version back into the in-memory aggregate and
|
|
1279
|
+
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
1280
|
+
* subclasses **should not** override this method directly.
|
|
1281
|
+
*
|
|
1282
|
+
* Overriding without calling `super.markPersisted(version)` silently
|
|
1283
|
+
* leaks `pendingEvents` — the next `withCommit` will re-dispatch them
|
|
1284
|
+
* through the outbox, double-emitting events. This bug has been hit
|
|
1285
|
+
* in production by consumers; the {@link onPersisted} hook below is
|
|
1286
|
+
* the safer extension point.
|
|
1287
|
+
*
|
|
1288
|
+
* If you must override (legitimate cases are very rare), call
|
|
1289
|
+
* `super.markPersisted(version)` FIRST so the framework's cleanup
|
|
1290
|
+
* runs, then add your logic afterwards.
|
|
1291
|
+
*
|
|
1292
|
+
* @param version - The version assigned by the persistence layer
|
|
1293
|
+
* @see onPersisted — the safe extension point for subclasses
|
|
1065
1294
|
*/
|
|
1066
1295
|
markPersisted(version: Version): void;
|
|
1296
|
+
/**
|
|
1297
|
+
* Subclass extension point — fires AFTER {@link markPersisted} has
|
|
1298
|
+
* updated the version and cleared `pendingEvents`. Override this for
|
|
1299
|
+
* post-persist logging, metrics, or cache-eviction without risk of
|
|
1300
|
+
* breaking the framework's pendingEvents cleanup.
|
|
1301
|
+
*
|
|
1302
|
+
* The default implementation is a no-op. Subclasses do NOT need to
|
|
1303
|
+
* call `super.onPersisted(version)` — there is nothing in the parent
|
|
1304
|
+
* implementation to preserve.
|
|
1305
|
+
*
|
|
1306
|
+
* **`onPersisted` deliberately receives only the version, not the
|
|
1307
|
+
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
1308
|
+
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
1309
|
+
* subscribers or the outbox dispatcher — that is the proper
|
|
1310
|
+
* Aggregate-Boundary separation. Building event-aware logic into
|
|
1311
|
+
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
1312
|
+
* recreates the boundary problems Vernon's aggregate discipline is
|
|
1313
|
+
* meant to prevent.
|
|
1314
|
+
*
|
|
1315
|
+
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
1316
|
+
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
1317
|
+
* permissive `void` will accept an `async`-override returning
|
|
1318
|
+
* `Promise<void>`, but the returned promise is fire-and-forget —
|
|
1319
|
+
* any rejection becomes an unhandled rejection and `withCommit`
|
|
1320
|
+
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
1321
|
+
* relevant domain event on the `EventBus` instead; that is the
|
|
1322
|
+
* properly awaited extension point.
|
|
1323
|
+
*
|
|
1324
|
+
* @param version - The version that was just persisted
|
|
1325
|
+
*/
|
|
1326
|
+
protected onPersisted(_version: Version): void;
|
|
1067
1327
|
protected constructor(id: TId, initialState: TState);
|
|
1328
|
+
/**
|
|
1329
|
+
* Sugar for `createDomainEvent` that auto-injects `aggregateId`
|
|
1330
|
+
* (from `this.id`) and `aggregateType` (from {@link aggregateType})
|
|
1331
|
+
* into the event's metadata fields. The canonical path for
|
|
1332
|
+
* constructing events to feed into `apply()` from inside aggregate
|
|
1333
|
+
* domain methods.
|
|
1334
|
+
*
|
|
1335
|
+
* @example
|
|
1336
|
+
* ```ts
|
|
1337
|
+
* class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
1338
|
+
* protected readonly aggregateType = "Order";
|
|
1339
|
+
*
|
|
1340
|
+
* confirm(): void {
|
|
1341
|
+
* this.apply(this.recordEvent("OrderConfirmed", { orderId: this.id }));
|
|
1342
|
+
* }
|
|
1343
|
+
* }
|
|
1344
|
+
* ```
|
|
1345
|
+
*
|
|
1346
|
+
* Calling `createDomainEvent(...)` directly inside an aggregate
|
|
1347
|
+
* method leaves `aggregateId` and `aggregateType` unset; the
|
|
1348
|
+
* `withCommit` harvest boundary catches it at runtime, but
|
|
1349
|
+
* `this.recordEvent(...)` makes the right thing impossible to
|
|
1350
|
+
* forget.
|
|
1351
|
+
*/
|
|
1352
|
+
protected recordEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateDomainEventOptions, "aggregateId" | "aggregateType">): E;
|
|
1068
1353
|
/**
|
|
1069
1354
|
* Validates an event before it is applied. Default is no-op.
|
|
1070
1355
|
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
@@ -1638,6 +1923,29 @@ interface TransactionScope<TCtx> {
|
|
|
1638
1923
|
* aggregate's `pendingEvents` and writes them via `outbox.add` (so
|
|
1639
1924
|
* events persist atomically with the state change). Skipped when no
|
|
1640
1925
|
* events were recorded.
|
|
1926
|
+
*
|
|
1927
|
+
* **Harvest order.** Events are concatenated in the order
|
|
1928
|
+
* aggregates appear in the returned `aggregates` array, then in
|
|
1929
|
+
* each aggregate's `pendingEvents` order (insertion order via
|
|
1930
|
+
* `apply` / `commit` / `addDomainEvent`). So `aggregates: [a, b]`
|
|
1931
|
+
* with `a` emitting `[e1, e2]` and `b` emitting `[e3]` produces
|
|
1932
|
+
* `outbox.add([e1, e2, e3])` and `bus.publish([e1, e2, e3])` in
|
|
1933
|
+
* that exact order.
|
|
1934
|
+
*
|
|
1935
|
+
* **Two ordering guarantees, not one.** Within a single aggregate
|
|
1936
|
+
* the order is *causal* — events are recorded in the order the
|
|
1937
|
+
* domain methods ran, and subscribers (handlers, projections,
|
|
1938
|
+
* replay) MUST process them in that order. Across aggregates the
|
|
1939
|
+
* order in this batch is deterministic but *not* a domain
|
|
1940
|
+
* guarantee. Greg Young / Vernon IDDD §10: aggregates are
|
|
1941
|
+
* independent consistency boundaries; events across them are
|
|
1942
|
+
* eventually consistent. Subscribers should NOT engineer
|
|
1943
|
+
* dependencies on cross-aggregate ordering — use
|
|
1944
|
+
* `EventMetadata.causationId` to express true causation, or a
|
|
1945
|
+
* process manager to coordinate. The in-process EventBus delivers
|
|
1946
|
+
* this batch in order, sequential outbox-dispatchers preserve it
|
|
1947
|
+
* too, but parallel dispatchers or message brokers may reorder
|
|
1948
|
+
* across aggregates at delivery time.
|
|
1641
1949
|
* 3. The transaction commits.
|
|
1642
1950
|
* 4. **After** the commit, `aggregate.markPersisted(aggregate.version)`
|
|
1643
1951
|
* fires on each returned aggregate — only now are pending events
|
|
@@ -1654,6 +1962,19 @@ interface TransactionScope<TCtx> {
|
|
|
1654
1962
|
* If the transaction rolls back, `markPersisted` is **not** called — the
|
|
1655
1963
|
* aggregate keeps its pending events, so the caller can retry or discard.
|
|
1656
1964
|
*
|
|
1965
|
+
* **Duplicate aggregates are deduped by reference.** If the returned
|
|
1966
|
+
* `aggregates` array contains the same instance twice — e.g. a use
|
|
1967
|
+
* case touches an order via two repository references that happen to
|
|
1968
|
+
* resolve to the same identity-map entry — `withCommit` dedupes by
|
|
1969
|
+
* JavaScript object identity before harvesting. Each event lands in
|
|
1970
|
+
* the outbox exactly once and `markPersisted` fires exactly once. Two
|
|
1971
|
+
* *different* instances with the same logical id cannot be detected
|
|
1972
|
+
* at this layer; that is a Repository contract violation (failure to
|
|
1973
|
+
* maintain Fowler's Identity Map per Unit of Work). See
|
|
1974
|
+
* `docs/guide/repository.md` → "Identity Map: one instance per
|
|
1975
|
+
* aggregate per Unit of Work" for the requirement on `IRepository`
|
|
1976
|
+
* implementations that makes this dedupe sound.
|
|
1977
|
+
*
|
|
1657
1978
|
* @example Tx-bound repos (Drizzle, Prisma, Mongo, …)
|
|
1658
1979
|
* ```typescript
|
|
1659
1980
|
* const result = await withCommit({ outbox, bus, scope }, async (tx) => {
|
|
@@ -2008,6 +2329,27 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2008
2329
|
* stored (optimistic concurrency).
|
|
2009
2330
|
* 2. Write the aggregate to durable storage.
|
|
2010
2331
|
*
|
|
2332
|
+
* **Insert vs update — library convention.** A fresh aggregate begins
|
|
2333
|
+
* at `version === 0` (the `Version` brand defaults to `0` in both
|
|
2334
|
+
* `AggregateRoot` and `EventSourcedAggregate`). After the first
|
|
2335
|
+
* versioned mutation (`setState(_, true)`, `apply()`, `commit()`) the
|
|
2336
|
+
* version is `> 0`. Implementations distinguish the two paths by the
|
|
2337
|
+
* incoming `aggregate.version`:
|
|
2338
|
+
*
|
|
2339
|
+
* - `aggregate.version === 0` → **INSERT** (no existing row to lock
|
|
2340
|
+
* against; the write succeeds unconditionally or fails the unique
|
|
2341
|
+
* constraint on `id`).
|
|
2342
|
+
* - `aggregate.version > 0` → **UPDATE** with the OCC predicate
|
|
2343
|
+
* `WHERE id = ? AND version = expected`. If the row count is `0`,
|
|
2344
|
+
* another writer raced you — throw `ConcurrencyConflictError`.
|
|
2345
|
+
*
|
|
2346
|
+
* The library does not formalise this in the type system because
|
|
2347
|
+
* version-bump semantics differ across the two aggregate flavours
|
|
2348
|
+
* (state-stored aggregates bump on the user's call to `setState(_,
|
|
2349
|
+
* true)`; event-sourced aggregates bump on every `apply()` by
|
|
2350
|
+
* definition). The `version === 0` invariant for "never persisted" is
|
|
2351
|
+
* the common contract.
|
|
2352
|
+
*
|
|
2011
2353
|
* Do **not** call `aggregate.markPersisted(...)` here. The library's
|
|
2012
2354
|
* `withCommit` orchestrator handles the post-save lifecycle (harvest
|
|
2013
2355
|
* pending events into the outbox, then mark persisted after commit).
|
|
@@ -2021,7 +2363,45 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2021
2363
|
*/
|
|
2022
2364
|
save(aggregate: TAgg): Promise<void>;
|
|
2023
2365
|
/**
|
|
2024
|
-
* Removes the aggregate by id.
|
|
2366
|
+
* Removes the aggregate's row by id. Pure persistence — does NOT
|
|
2367
|
+
* harvest pending events from the aggregate (the contract takes
|
|
2368
|
+
* only the id, so there is no aggregate to harvest from).
|
|
2369
|
+
*
|
|
2370
|
+
* Before reaching for `delete`, ask whether the user-facing "delete"
|
|
2371
|
+
* is the right domain verb. Most are actually state transitions
|
|
2372
|
+
* (*cancel*, *archive*, *close*, *deactivate*, *terminate*) with
|
|
2373
|
+
* proper domain names that should be modelled as state changes plus
|
|
2374
|
+
* a recorded event — not as row removal.
|
|
2375
|
+
*
|
|
2376
|
+
* `delete(id)` belongs in the toolkit for three distinct cases, in
|
|
2377
|
+
* decreasing order of common occurrence (see
|
|
2378
|
+
* `docs/guide/repository.md` → "Deletion and Domain Events" for
|
|
2379
|
+
* worked examples):
|
|
2380
|
+
*
|
|
2381
|
+
* 1. **State transition that records an event.** The user-facing
|
|
2382
|
+
* "delete" maps to a real domain operation (e.g. `order.cancel()`,
|
|
2383
|
+
* `order.archive()`). Call `save(aggregate)`; the row stays with
|
|
2384
|
+
* a status column. `delete(id)` is never called by the use case.
|
|
2385
|
+
*
|
|
2386
|
+
* 2. **Hard-delete with event harvest.** The row genuinely must
|
|
2387
|
+
* vanish (regulatory purge, retention-window expiry, true
|
|
2388
|
+
* termination) *and* the disappearance is a domain fact
|
|
2389
|
+
* subscribers care about. Inside `withCommit`'s transactional
|
|
2390
|
+
* callback, record the deletion event on the aggregate, then
|
|
2391
|
+
* call `delete(id)`. Return the aggregate in the `aggregates`
|
|
2392
|
+
* array so `withCommit` harvests its pending events into the
|
|
2393
|
+
* outbox before the row is gone.
|
|
2394
|
+
*
|
|
2395
|
+
* 3. **Hard-delete without event.** Deletion is invisible to the
|
|
2396
|
+
* domain (abandoned-cart cleanup, expired session rows). No
|
|
2397
|
+
* subscriber cares. If the entity has identity in the ubiquitous
|
|
2398
|
+
* language, you probably want path 1 or 2 instead.
|
|
2399
|
+
*
|
|
2400
|
+
* In pure event-sourced systems `delete` is rarely meaningful —
|
|
2401
|
+
* end-of-lifecycle there is a `Closed` / `Terminated` event in the
|
|
2402
|
+
* stream, and identity persists in the event log. `delete` applies
|
|
2403
|
+
* primarily to state-stored aggregates and snapshot / projection
|
|
2404
|
+
* tables.
|
|
2025
2405
|
*/
|
|
2026
2406
|
delete(id: TId): Promise<void>;
|
|
2027
2407
|
}
|
|
@@ -2295,4 +2675,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
2295
2675
|
toJSON(): Readonly<T>;
|
|
2296
2676
|
}
|
|
2297
2677
|
|
|
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 };
|
|
2678
|
+
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, withClockFactory, withCommit, withEventIdFactory };
|