@shirudo/ddd-kit 0.16.0 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -218
- package/dist/index.d.ts +1141 -513
- package/dist/index.js +1144 -1
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +92 -1
- package/dist/utils.js +282 -0
- package/dist/utils.js.map +1 -1
- package/package.json +69 -65
- package/dist/deep-equal-except-C8yoSk4L.d.ts +0 -57
- package/dist/result-jCwPSjFa.d.ts +0 -352
- package/dist/result.d.ts +0 -204
- package/dist/result.js +0 -2
- package/dist/result.js.map +0 -1
- package/dist/utils-array.d.ts +0 -47
- package/dist/utils-array.js +0 -2
- package/dist/utils-array.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,18 +1,350 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Result } from '@shirudo/result';
|
|
2
|
+
import { DeepEqualExceptOptions } from './utils.js';
|
|
3
|
+
export { DeepOmitOptions, Key, PathSegment, deepEqual, deepEqualExcept, deepOmit } from './utils.js';
|
|
3
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Branded string ID. `Tag` carries the aggregate / entity name so two ids
|
|
7
|
+
* with different tags are not assignable to each other even though both
|
|
8
|
+
* are strings at runtime.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* type UserId = Id<"UserId">;
|
|
13
|
+
* type OrderId = Id<"OrderId">;
|
|
14
|
+
*
|
|
15
|
+
* const u = "user-1" as UserId;
|
|
16
|
+
* const o: OrderId = u; // ❌ compile error
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
4
19
|
type Id<Tag extends string> = string & {
|
|
5
20
|
readonly __brand: Tag;
|
|
6
21
|
};
|
|
7
|
-
|
|
8
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Produces fresh ids of a single, fixed tag. The tag is bound at the
|
|
24
|
+
* generator type — `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
|
|
25
|
+
* with no caller-side generic to abuse.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { ulid } from "ulid";
|
|
30
|
+
*
|
|
31
|
+
* const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
|
|
32
|
+
* const id = userIds.next(); // Id<"UserId">
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
|
|
36
|
+
* let callers pick `T` themselves — `gen.next<"AnyTag">()` typechecked
|
|
37
|
+
* even when the generator produced different-tag ids, silently defeating
|
|
38
|
+
* the brand.
|
|
39
|
+
*/
|
|
40
|
+
interface IdGenerator<Tag extends string> {
|
|
41
|
+
next: () => Id<Tag>;
|
|
9
42
|
}
|
|
10
43
|
|
|
11
44
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
45
|
+
* Factory function producing a fresh, unique event identifier for each call.
|
|
46
|
+
*
|
|
47
|
+
* The library ships a default that uses Web Crypto `crypto.randomUUID()`
|
|
48
|
+
* (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
|
|
49
|
+
* Cloudflare Workers, Vercel Edge, and any runtime that implements Web
|
|
50
|
+
* Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
|
|
51
|
+
* random) — for production event stores prefer a **time-ordered** id
|
|
52
|
+
* format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
|
|
53
|
+
* column stay clustered and `ORDER BY eventId` matches creation order.
|
|
54
|
+
* Swap one in via `setEventIdFactory(() => uuidv7())` or `() => ulid()`.
|
|
55
|
+
*/
|
|
56
|
+
type EventIdFactory = () => string;
|
|
57
|
+
/**
|
|
58
|
+
* Replaces the global event-id factory used by `createDomainEvent` and
|
|
59
|
+
* `createDomainEventWithMetadata`. Call once during application bootstrap,
|
|
60
|
+
* for example:
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { ulid } from "ulid";
|
|
64
|
+
* import { setEventIdFactory } from "@shirudo/ddd-kit";
|
|
65
|
+
*
|
|
66
|
+
* setEventIdFactory(() => ulid());
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* The per-call `options.eventId` override always wins over this factory.
|
|
70
|
+
*
|
|
71
|
+
* **Module-scoped — last setter wins.** The factory lives as a single
|
|
72
|
+
* module variable; importing two libraries that both call this races on
|
|
73
|
+
* load order. For multi-tenant request isolation (e.g. one factory per
|
|
74
|
+
* tenant in a single Worker invocation) **prefer the per-call
|
|
75
|
+
* `options.eventId`** instead of mutating the global. Same caveat applies
|
|
76
|
+
* to `setClockFactory`.
|
|
77
|
+
*/
|
|
78
|
+
declare function setEventIdFactory(factory: EventIdFactory): void;
|
|
79
|
+
/**
|
|
80
|
+
* Restores the default event-id factory (`crypto.randomUUID()`).
|
|
81
|
+
* Intended for use in test `afterEach` hooks.
|
|
82
|
+
*/
|
|
83
|
+
declare function resetEventIdFactory(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Clock function producing a fresh `Date` for each call. The library
|
|
86
|
+
* defaults to `() => new Date()`; override globally via `setClockFactory`
|
|
87
|
+
* for deterministic event-sourcing tests, time-travel debugging, or any
|
|
88
|
+
* scenario where `occurredAt` must be reproducible.
|
|
14
89
|
*/
|
|
15
|
-
type
|
|
90
|
+
type ClockFactory = () => Date;
|
|
91
|
+
/**
|
|
92
|
+
* Replaces the global clock factory used by `createDomainEvent` and
|
|
93
|
+
* `createDomainEventWithMetadata`. Call once during application bootstrap
|
|
94
|
+
* (or per-test in deterministic test suites):
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* import { setClockFactory } from "@shirudo/ddd-kit";
|
|
98
|
+
*
|
|
99
|
+
* setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* The per-call `options.occurredAt` override always wins over this
|
|
103
|
+
* factory. Symmetric to `setEventIdFactory`.
|
|
104
|
+
*/
|
|
105
|
+
declare function setClockFactory(factory: ClockFactory): void;
|
|
106
|
+
/**
|
|
107
|
+
* Restores the default clock factory (`() => new Date()`).
|
|
108
|
+
* Intended for use in test `afterEach` hooks.
|
|
109
|
+
*/
|
|
110
|
+
declare function resetClockFactory(): void;
|
|
111
|
+
/**
|
|
112
|
+
* Metadata associated with a domain event for traceability and correlation.
|
|
113
|
+
* Used in event-driven architectures to track event flow across services.
|
|
114
|
+
*/
|
|
115
|
+
interface EventMetadata {
|
|
116
|
+
/**
|
|
117
|
+
* Correlation ID for tracing events across multiple services/components.
|
|
118
|
+
* Typically used to group related events in a distributed system.
|
|
119
|
+
*/
|
|
120
|
+
correlationId?: string;
|
|
121
|
+
/**
|
|
122
|
+
* Causation ID referencing the event or command that caused this event.
|
|
123
|
+
* Used to build event chains and understand causality.
|
|
124
|
+
*/
|
|
125
|
+
causationId?: string;
|
|
126
|
+
/**
|
|
127
|
+
* User ID of the person or system that triggered the event.
|
|
128
|
+
*/
|
|
129
|
+
userId?: string;
|
|
130
|
+
/**
|
|
131
|
+
* Source service or component that produced the event.
|
|
132
|
+
*/
|
|
133
|
+
source?: string;
|
|
134
|
+
/**
|
|
135
|
+
* Additional custom metadata fields.
|
|
136
|
+
* Allows extensibility for domain-specific metadata.
|
|
137
|
+
*/
|
|
138
|
+
[key: string]: unknown;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Domain Event represents something meaningful that happened in the domain.
|
|
142
|
+
* Events are immutable and carry information about what occurred.
|
|
143
|
+
*
|
|
144
|
+
* @template T - The event type name (e.g., "OrderCreated")
|
|
145
|
+
* @template P - The event payload type
|
|
146
|
+
*/
|
|
147
|
+
interface DomainEvent<T extends string, P = void> {
|
|
148
|
+
/**
|
|
149
|
+
* Unique identifier for this specific event instance. Used by idempotent
|
|
150
|
+
* consumers, outbox dispatch tracking, and as the target of
|
|
151
|
+
* `metadata.causationId`. Defaults to `crypto.randomUUID()` if not
|
|
152
|
+
* supplied.
|
|
153
|
+
*/
|
|
154
|
+
eventId: string;
|
|
155
|
+
/**
|
|
156
|
+
* The type of the event, used for routing and handling.
|
|
157
|
+
*/
|
|
158
|
+
type: T;
|
|
159
|
+
/**
|
|
160
|
+
* Identifier of the aggregate that produced the event. Optional at the
|
|
161
|
+
* library level — set it whenever the producing aggregate is known so
|
|
162
|
+
* downstream subscribers, outboxes, and projections can scope by entity.
|
|
163
|
+
*/
|
|
164
|
+
aggregateId?: string;
|
|
165
|
+
/**
|
|
166
|
+
* Name of the aggregate type that produced the event (e.g. "Order").
|
|
167
|
+
* Pairs with `aggregateId` to fully qualify the source aggregate.
|
|
168
|
+
*/
|
|
169
|
+
aggregateType?: string;
|
|
170
|
+
/**
|
|
171
|
+
* The event payload containing the domain data. The field is always
|
|
172
|
+
* present; its value is `undefined` when `P` is `void`.
|
|
173
|
+
*/
|
|
174
|
+
payload: P;
|
|
175
|
+
/**
|
|
176
|
+
* Timestamp when the event occurred.
|
|
177
|
+
*/
|
|
178
|
+
occurredAt: Date;
|
|
179
|
+
/**
|
|
180
|
+
* Event schema version for handling schema evolution.
|
|
181
|
+
* Required for safe schema migration in event-sourced systems.
|
|
182
|
+
* Use 1 for the initial schema version.
|
|
183
|
+
*/
|
|
184
|
+
version: number;
|
|
185
|
+
/**
|
|
186
|
+
* Optional metadata for traceability, correlation, and auditing.
|
|
187
|
+
* Includes correlationId, causationId, userId, source, and custom fields.
|
|
188
|
+
*/
|
|
189
|
+
metadata?: EventMetadata;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Shared option bag for the `createDomainEvent*` factories.
|
|
193
|
+
*/
|
|
194
|
+
interface CreateDomainEventOptions {
|
|
195
|
+
/**
|
|
196
|
+
* Override for the auto-generated `eventId`. Pass an existing id (for
|
|
197
|
+
* replay, tests, or deterministic event sourcing) instead of letting the
|
|
198
|
+
* factory call `crypto.randomUUID()`.
|
|
199
|
+
*/
|
|
200
|
+
eventId?: string;
|
|
201
|
+
/**
|
|
202
|
+
* Identifier of the aggregate that produced the event.
|
|
203
|
+
*/
|
|
204
|
+
aggregateId?: string;
|
|
205
|
+
/**
|
|
206
|
+
* Name of the aggregate type that produced the event.
|
|
207
|
+
*/
|
|
208
|
+
aggregateType?: string;
|
|
209
|
+
/**
|
|
210
|
+
* Override for the auto-generated `occurredAt` timestamp.
|
|
211
|
+
*/
|
|
212
|
+
occurredAt?: Date;
|
|
213
|
+
/**
|
|
214
|
+
* Override for the default schema version (1).
|
|
215
|
+
*/
|
|
216
|
+
version?: number;
|
|
217
|
+
/**
|
|
218
|
+
* Event metadata — correlation, causation, user, source, custom fields.
|
|
219
|
+
*/
|
|
220
|
+
metadata?: EventMetadata;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Creates a domain event with default values.
|
|
224
|
+
* Sets occurredAt to current date and version to 1 if not provided.
|
|
225
|
+
*
|
|
226
|
+
* @param type - The event type
|
|
227
|
+
* @param payload - The event payload
|
|
228
|
+
* @param options - Optional event configuration
|
|
229
|
+
* @returns A domain event
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
|
|
237
|
+
declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
|
|
238
|
+
/**
|
|
239
|
+
* Creates a domain event with metadata for traceability.
|
|
240
|
+
* Convenience function for creating events with correlation and causation IDs.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```typescript
|
|
244
|
+
* const event = createDomainEventWithMetadata(
|
|
245
|
+
* "OrderCreated",
|
|
246
|
+
* { orderId: "123" },
|
|
247
|
+
* { correlationId: "corr-123", causationId: "cmd-456", userId: "user-789" }
|
|
248
|
+
* );
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
|
|
252
|
+
/**
|
|
253
|
+
* Copies metadata from a source event to a new event.
|
|
254
|
+
* Useful for maintaining correlation chains in event-driven architectures.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```typescript
|
|
258
|
+
* const newEvent = createDomainEvent(
|
|
259
|
+
* "OrderShipped",
|
|
260
|
+
* { orderId: "123" },
|
|
261
|
+
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.type }) }
|
|
262
|
+
* );
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
declare function copyMetadata(sourceEvent: DomainEvent<string, unknown>, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
|
|
266
|
+
/**
|
|
267
|
+
* Merges multiple metadata objects into one.
|
|
268
|
+
* Later metadata objects override earlier ones for the same keys.
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```typescript
|
|
272
|
+
* const metadata = mergeMetadata(
|
|
273
|
+
* { correlationId: "corr-123" },
|
|
274
|
+
* { userId: "user-456" },
|
|
275
|
+
* { source: "order-service" }
|
|
276
|
+
* );
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Entity utilities and interfaces for Domain-Driven Design.
|
|
283
|
+
*
|
|
284
|
+
* In Domain-Driven Design, there are two types of entities:
|
|
285
|
+
*
|
|
286
|
+
* 1. **Aggregate Root Entity**: The parent Entity of an aggregate.
|
|
287
|
+
* - Has identity (id), state, and version
|
|
288
|
+
* - Implemented by classes extending `AggregateRoot` or `EventSourcedAggregate`
|
|
289
|
+
* - Represents the aggregate externally
|
|
290
|
+
* - Loaded/saved through repositories
|
|
291
|
+
*
|
|
292
|
+
* 2. **Child Entities**: Entities within an aggregate.
|
|
293
|
+
* - Have identity (id) and state, but no own version
|
|
294
|
+
* - Can extend `EntityBase<TState, TId>` for class-based entities
|
|
295
|
+
* - Or use functional style with `Identifiable<TId> & TProps`
|
|
296
|
+
* - Exist only within the aggregate boundary
|
|
297
|
+
* - Versioned through the Aggregate Root
|
|
298
|
+
* - Cannot be referenced directly from outside the aggregate
|
|
299
|
+
*
|
|
300
|
+
* This module provides:
|
|
301
|
+
* - `EntityBase<TState, TId>` - Base class for entities with state
|
|
302
|
+
* - `Entity<TId>` - Simple class for entities without state management
|
|
303
|
+
* - `Identifiable<TId>` - Minimal interface for objects with id
|
|
304
|
+
* - Helper functions for working with collections of entities
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* ```typescript
|
|
308
|
+
* // Class-based child entity with logic
|
|
309
|
+
* class OrderItem extends EntityBase<OrderItemState, ItemId> {
|
|
310
|
+
* constructor(id: ItemId, initialState: OrderItemState) {
|
|
311
|
+
* super(id, initialState);
|
|
312
|
+
* }
|
|
313
|
+
*
|
|
314
|
+
* updateQuantity(quantity: number): void {
|
|
315
|
+
* this._state = { ...this._state, quantity };
|
|
316
|
+
* }
|
|
317
|
+
*
|
|
318
|
+
* calculateSubtotal(): number {
|
|
319
|
+
* return this._state.price * this._state.quantity;
|
|
320
|
+
* }
|
|
321
|
+
* }
|
|
322
|
+
*
|
|
323
|
+
* // Functional-style child entity (simpler, no logic)
|
|
324
|
+
* type OrderItem = Identifiable<ItemId> & {
|
|
325
|
+
* productId: string;
|
|
326
|
+
* quantity: number;
|
|
327
|
+
* price: number;
|
|
328
|
+
* };
|
|
329
|
+
*
|
|
330
|
+
* // Aggregate Root (Entity with version)
|
|
331
|
+
* class Order extends AggregateRoot<OrderState, OrderId> {
|
|
332
|
+
* // Order is an Aggregate Root Entity
|
|
333
|
+
* // OrderState contains OrderItem child entities
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
*/
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Functional definition of an Entity via its capability — an object is
|
|
340
|
+
* identifiable if it has an `id`.
|
|
341
|
+
*
|
|
342
|
+
* `TId` is constrained to `Id<string>` so the brand discipline that
|
|
343
|
+
* `Id<Tag>` enforces is preserved end-to-end: an `Identifiable<UserId>`
|
|
344
|
+
* cannot accidentally be paired with an `Identifiable<OrderId>` or with
|
|
345
|
+
* a plain `string`.
|
|
346
|
+
*/
|
|
347
|
+
type Identifiable<TId extends Id<string>> = {
|
|
16
348
|
readonly id: TId;
|
|
17
349
|
};
|
|
18
350
|
/**
|
|
@@ -72,7 +404,15 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
72
404
|
readonly id: TId;
|
|
73
405
|
/**
|
|
74
406
|
* Returns the current state of the entity.
|
|
75
|
-
*
|
|
407
|
+
*
|
|
408
|
+
* The state object is **shallowly frozen** — direct property writes
|
|
409
|
+
* (`entity.state.foo = …`) throw in strict mode, but writes to nested
|
|
410
|
+
* objects (`entity.state.address.zip = …`) bypass the freeze. For deep
|
|
411
|
+
* immutability either model nested data with `vo()` (which freezes
|
|
412
|
+
* deeply) or reach for a structural-sharing library like Immer at the
|
|
413
|
+
* App layer. The shallow contract is intentional: deep freezing on
|
|
414
|
+
* every state write is too expensive for hot paths, and DDD aggregates
|
|
415
|
+
* normally treat their own state as private (`Tell, Don't Ask`).
|
|
76
416
|
*/
|
|
77
417
|
get state(): TState;
|
|
78
418
|
/**
|
|
@@ -82,12 +422,25 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
82
422
|
protected _state: TState;
|
|
83
423
|
protected constructor(id: TId, initialState: TState);
|
|
84
424
|
/**
|
|
85
|
-
* Optional validation hook to ensure state invariants.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
425
|
+
* Optional validation hook to ensure state invariants. Called during
|
|
426
|
+
* construction (from `Entity`'s constructor) and again on every
|
|
427
|
+
* `setState()` call. Throw to reject invalid state.
|
|
428
|
+
*
|
|
429
|
+
* **⚠️ Must not read subclass instance fields via `this`.** The
|
|
430
|
+
* constructor calls `validateState(initialState)` BEFORE the subclass's
|
|
431
|
+
* field initializers run, so `this.someField` is `undefined` at that
|
|
432
|
+
* point — a classic TypeScript/JavaScript constructor-ordering footgun.
|
|
433
|
+
* The `state` argument is the single source of truth; treat the method
|
|
434
|
+
* as pure with respect to `this`.
|
|
435
|
+
*
|
|
436
|
+
* If your invariants genuinely depend on per-instance configuration
|
|
437
|
+
* that isn't part of the state, pass that configuration into the state
|
|
438
|
+
* itself (DDD-canonical: the aggregate's state contains everything it
|
|
439
|
+
* needs) or perform the additional check after construction in a
|
|
440
|
+
* dedicated factory method.
|
|
88
441
|
*
|
|
89
442
|
* @param state - The state to validate
|
|
90
|
-
* @throws Error if validation fails
|
|
443
|
+
* @throws Error (or `DomainError` subclass) if validation fails
|
|
91
444
|
*/
|
|
92
445
|
protected validateState(_state: TState): void;
|
|
93
446
|
/**
|
|
@@ -99,6 +452,18 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
99
452
|
*/
|
|
100
453
|
protected setState(newState: TState): void;
|
|
101
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* Shallow-freezes `value` when it's a non-null object or array, so that
|
|
457
|
+
* direct property writes throw in strict mode. Returns the value as-is for
|
|
458
|
+
* primitives. Used internally by `Entity` and its subclasses to prevent
|
|
459
|
+
* outside mutation of state read through the `state` getter without paying
|
|
460
|
+
* the cost of a deep clone on every read.
|
|
461
|
+
*
|
|
462
|
+
* Exported so that sibling classes (`EventSourcedAggregate`, `AggregateRoot`)
|
|
463
|
+
* can apply the same freeze when they bypass `setState` and assign
|
|
464
|
+
* `this._state` directly.
|
|
465
|
+
*/
|
|
466
|
+
declare function freezeShallow<T>(value: T): T;
|
|
102
467
|
/**
|
|
103
468
|
* Checks if two entities have the same ID.
|
|
104
469
|
* Works with any object that has an 'id' property.
|
|
@@ -116,7 +481,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
116
481
|
* sameEntity(item1, item1); // true
|
|
117
482
|
* ```
|
|
118
483
|
*/
|
|
119
|
-
declare function sameEntity<TId
|
|
484
|
+
declare function sameEntity<TId extends Id<string>>(a: Identifiable<TId>, b: Identifiable<TId>): boolean;
|
|
120
485
|
/**
|
|
121
486
|
* Finds an entity by ID in a collection.
|
|
122
487
|
* Returns undefined if not found.
|
|
@@ -136,7 +501,7 @@ declare function sameEntity<TId>(a: Identifiable<TId>, b: Identifiable<TId>): bo
|
|
|
136
501
|
* // item is { id: itemId1, productId: "prod-1", quantity: 2 }
|
|
137
502
|
* ```
|
|
138
503
|
*/
|
|
139
|
-
declare function findEntityById<TId
|
|
504
|
+
declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T | undefined;
|
|
140
505
|
/**
|
|
141
506
|
* Checks if an entity with the given ID exists in the collection.
|
|
142
507
|
*
|
|
@@ -154,7 +519,7 @@ declare function findEntityById<TId, T extends Identifiable<TId>>(entities: T[],
|
|
|
154
519
|
* hasEntityId(items, itemId2); // false
|
|
155
520
|
* ```
|
|
156
521
|
*/
|
|
157
|
-
declare function hasEntityId<TId
|
|
522
|
+
declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): boolean;
|
|
158
523
|
/**
|
|
159
524
|
* Removes an entity with the given ID from the collection.
|
|
160
525
|
* Returns a new array without the entity.
|
|
@@ -174,7 +539,7 @@ declare function hasEntityId<TId, T extends Identifiable<TId>>(entities: T[], id
|
|
|
174
539
|
* // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
|
|
175
540
|
* ```
|
|
176
541
|
*/
|
|
177
|
-
declare function removeEntityById<TId
|
|
542
|
+
declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T[];
|
|
178
543
|
/**
|
|
179
544
|
* Updates an entity with the given ID in the collection.
|
|
180
545
|
* Returns a new array with the updated entity.
|
|
@@ -198,7 +563,7 @@ declare function removeEntityById<TId, T extends Identifiable<TId>>(entities: T[
|
|
|
198
563
|
* // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
|
|
199
564
|
* ```
|
|
200
565
|
*/
|
|
201
|
-
declare function updateEntityById<TId
|
|
566
|
+
declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, updater: (entity: T) => T): T[];
|
|
202
567
|
/**
|
|
203
568
|
* Replaces an entity with the given ID in the collection.
|
|
204
569
|
* Returns a new array with the replaced entity.
|
|
@@ -222,7 +587,7 @@ declare function updateEntityById<TId, T extends Identifiable<TId>>(entities: T[
|
|
|
222
587
|
* });
|
|
223
588
|
* ```
|
|
224
589
|
*/
|
|
225
|
-
declare function replaceEntityById<TId
|
|
590
|
+
declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, replacement: T): T[];
|
|
226
591
|
/**
|
|
227
592
|
* Extracts all IDs from a collection of entities.
|
|
228
593
|
*
|
|
@@ -240,7 +605,7 @@ declare function replaceEntityById<TId, T extends Identifiable<TId>>(entities: T
|
|
|
240
605
|
* // ids is [itemId1, itemId2]
|
|
241
606
|
* ```
|
|
242
607
|
*/
|
|
243
|
-
declare function entityIds<TId
|
|
608
|
+
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[]): TId[];
|
|
244
609
|
|
|
245
610
|
/**
|
|
246
611
|
* Marker interface for Aggregate Roots.
|
|
@@ -280,45 +645,58 @@ interface IAggregateRoot<TId extends Id<string>> {
|
|
|
280
645
|
* This version applies to the entire aggregate, including all child entities.
|
|
281
646
|
*/
|
|
282
647
|
readonly version: Version;
|
|
648
|
+
/**
|
|
649
|
+
* Post-save hook: a `Repository.save()` implementation calls this with
|
|
650
|
+
* the persisted version after a successful write to push the new
|
|
651
|
+
* version back into the aggregate and clear any recorded domain events
|
|
652
|
+
* (they are now safely on the write side / in the outbox).
|
|
653
|
+
*
|
|
654
|
+
* Required by the interface so a Repository implementation can call it
|
|
655
|
+
* via the published `IAggregateRoot` contract without taking the
|
|
656
|
+
* abstract class as a compile-time dependency.
|
|
657
|
+
*
|
|
658
|
+
* @param version - The version assigned by the persistence layer
|
|
659
|
+
*/
|
|
660
|
+
markPersisted(version: Version): void;
|
|
283
661
|
}
|
|
284
662
|
/**
|
|
285
663
|
* Configuration options for AggregateRoot behavior.
|
|
286
664
|
*/
|
|
287
665
|
interface AggregateConfig {
|
|
288
666
|
/**
|
|
289
|
-
* Whether
|
|
290
|
-
*
|
|
667
|
+
* Whether `setState()` should bump the version automatically.
|
|
668
|
+
*
|
|
669
|
+
* Defaults to **`false`** for `AggregateRoot` — because `setState()`
|
|
670
|
+
* already takes an explicit `bumpVersion` argument per call, so adding
|
|
671
|
+
* an "always bump" config on top would be redundant. Keep it `false`
|
|
672
|
+
* unless you have a subclass that never passes `bumpVersion` and you
|
|
673
|
+
* want every state change to advance the version anyway.
|
|
674
|
+
*
|
|
675
|
+
* (Contrast with `EventSourcedAggregate`, which defaults this to
|
|
676
|
+
* `true` because every event-sourced state change is per definition a
|
|
677
|
+
* versioned commit.)
|
|
291
678
|
*/
|
|
292
679
|
autoVersionBump?: boolean;
|
|
293
680
|
}
|
|
294
681
|
/**
|
|
295
|
-
* Base class for
|
|
682
|
+
* Base class for Aggregate Roots without Event Sourcing.
|
|
296
683
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
684
|
+
* In DDD (Evans), an Aggregate is a cluster of objects — root entity, child entities,
|
|
685
|
+
* and value objects — treated as a unit for consistency. The **Aggregate Root** is the
|
|
686
|
+
* root entity that represents the aggregate externally and is the only entry point
|
|
687
|
+
* for external code. This class serves as both: it IS the root entity and it contains
|
|
688
|
+
* the aggregate state (`TState`) which holds child entities and value objects.
|
|
300
689
|
*
|
|
301
|
-
*
|
|
302
|
-
* - Identity (id)
|
|
303
|
-
* -
|
|
304
|
-
* -
|
|
305
|
-
*
|
|
306
|
-
* Adds Aggregate Root specific functionality:
|
|
307
|
-
* - Version management (for Optimistic Concurrency Control)
|
|
308
|
-
* - Domain events tracking
|
|
690
|
+
* Provides:
|
|
691
|
+
* - Identity (id) and state management (via `Entity`)
|
|
692
|
+
* - Version management for optimistic concurrency control
|
|
693
|
+
* - Domain event tracking for side-effects
|
|
309
694
|
* - Snapshot support for performance optimization
|
|
310
695
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* - Value objects (immutable objects)
|
|
314
|
-
*
|
|
315
|
-
* All changes to child entities are versioned through the Aggregate Root. The version
|
|
316
|
-
* applies to the entire aggregate, including all child entities.
|
|
696
|
+
* All changes to child entities within `TState` are versioned through this root.
|
|
697
|
+
* Use `setState()` for state mutations to ensure invariant validation.
|
|
317
698
|
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* Use this class when you don't need Event Sourcing but still want
|
|
321
|
-
* aggregate patterns with versioning and state management.
|
|
699
|
+
* For event sourcing, use `EventSourcedAggregate` instead.
|
|
322
700
|
*
|
|
323
701
|
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
324
702
|
* @template TId - The type of the aggregate root identifier
|
|
@@ -333,14 +711,15 @@ interface AggregateConfig {
|
|
|
333
711
|
* }
|
|
334
712
|
*
|
|
335
713
|
* confirm(): void {
|
|
336
|
-
* this.
|
|
337
|
-
* this.bumpVersion(); // Versions the entire aggregate
|
|
714
|
+
* this.setState({ ...this.state, status: "confirmed" }, true);
|
|
338
715
|
* }
|
|
339
716
|
* }
|
|
340
717
|
* ```
|
|
341
718
|
*/
|
|
342
|
-
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent =
|
|
343
|
-
|
|
719
|
+
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId> {
|
|
720
|
+
private _version;
|
|
721
|
+
get version(): Version;
|
|
722
|
+
protected setVersion(version: Version): void;
|
|
344
723
|
private readonly _config;
|
|
345
724
|
private readonly _autoVersionBump;
|
|
346
725
|
private _domainEvents;
|
|
@@ -354,12 +733,92 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
|
|
|
354
733
|
* Call this after dispatching the events.
|
|
355
734
|
*/
|
|
356
735
|
clearDomainEvents(): void;
|
|
736
|
+
/**
|
|
737
|
+
* Post-save hook called by a `Repository.save()` implementation to push
|
|
738
|
+
* the persisted version back into the in-memory aggregate and clear the
|
|
739
|
+
* recorded domain events (they are now safely on the write side / in
|
|
740
|
+
* the outbox).
|
|
741
|
+
*
|
|
742
|
+
* Use this so `save()` can keep its `Promise<void>` return type: the
|
|
743
|
+
* caller holds the aggregate reference, which is up to date after this
|
|
744
|
+
* call.
|
|
745
|
+
*/
|
|
746
|
+
markPersisted(version: Version): void;
|
|
747
|
+
/**
|
|
748
|
+
* Mutates state and records the resulting domain events in the
|
|
749
|
+
* **canonical record-after-mutation order**. Use this instead of calling
|
|
750
|
+
* `setState` + `addDomainEvent` separately and you cannot trip the
|
|
751
|
+
* "event for a fact that never happened" footgun.
|
|
752
|
+
*
|
|
753
|
+
* Order of operations:
|
|
754
|
+
* 1. `setState(newState, true)` — runs `validateState` first.
|
|
755
|
+
* If it throws, the method propagates and **no event is recorded
|
|
756
|
+
* and no version is bumped**.
|
|
757
|
+
* 2. Each event in `events` is appended via `addDomainEvent`.
|
|
758
|
+
*
|
|
759
|
+
* `commit()` **always bumps the version**, regardless of the aggregate's
|
|
760
|
+
* `autoVersionBump` config. Recording a domain event implies "something
|
|
761
|
+
* happened that the outside world cares about", and optimistic-
|
|
762
|
+
* concurrency callers must see a fresh version every time. The config
|
|
763
|
+
* still governs the un-coupled `setState` path. If you need to mutate
|
|
764
|
+
* state without bumping (e.g. cosmetic caches), call `setState(newState,
|
|
765
|
+
* false)` and skip `commit` entirely.
|
|
766
|
+
*
|
|
767
|
+
* `events` accepts a single event or an array. Omit it (or pass `[]`)
|
|
768
|
+
* for state-only mutations.
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* ```ts
|
|
772
|
+
* confirm(): void {
|
|
773
|
+
* if (this.state.status === "confirmed") {
|
|
774
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
775
|
+
* }
|
|
776
|
+
* this.commit(
|
|
777
|
+
* { ...this.state, status: "confirmed" },
|
|
778
|
+
* { type: "OrderConfirmed", orderId: this.id },
|
|
779
|
+
* );
|
|
780
|
+
* }
|
|
781
|
+
* ```
|
|
782
|
+
*
|
|
783
|
+
* `EventSourcedAggregate.apply()` enforces the same ordering
|
|
784
|
+
* structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
|
|
785
|
+
* where `setState` and `addDomainEvent` are otherwise decoupled and the
|
|
786
|
+
* ordering is convention-only.
|
|
787
|
+
*
|
|
788
|
+
* @param newState - The new state (validated by `validateState`)
|
|
789
|
+
* @param events - One event, an array of events, or none (default)
|
|
790
|
+
*/
|
|
791
|
+
protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
|
|
357
792
|
protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
|
|
358
793
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
794
|
+
* Records a domain event for later publication.
|
|
795
|
+
*
|
|
796
|
+
* **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
|
|
797
|
+
* explicit: a domain event describes something that has just happened
|
|
798
|
+
* to the aggregate — its existence implies the state change already
|
|
799
|
+
* occurred. Concretely:
|
|
800
|
+
*
|
|
801
|
+
* ```ts
|
|
802
|
+
* confirm(): void {
|
|
803
|
+
* if (this.state.status === "confirmed") {
|
|
804
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
805
|
+
* }
|
|
806
|
+
* this.setState({ ...this.state, status: "confirmed" }, true);
|
|
807
|
+
* this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
|
|
808
|
+
* // ↑ post-mutation. The event represents the committed fact.
|
|
809
|
+
* }
|
|
810
|
+
* ```
|
|
811
|
+
*
|
|
812
|
+
* Recording before mutation is a footgun: if a subsequent invariant
|
|
813
|
+
* check throws, the event has already been queued but the state never
|
|
814
|
+
* actually changed — consumers see an event for a fact that did not
|
|
815
|
+
* happen.
|
|
361
816
|
*
|
|
362
|
-
*
|
|
817
|
+
* `EventSourcedAggregate.apply()` enforces this ordering structurally;
|
|
818
|
+
* `AggregateRoot` leaves it as a convention because the state-mutation
|
|
819
|
+
* path (`setState`) is decoupled from event recording.
|
|
820
|
+
*
|
|
821
|
+
* @param event - The domain event to record
|
|
363
822
|
*/
|
|
364
823
|
protected addDomainEvent(event: TEvent): void;
|
|
365
824
|
/**
|
|
@@ -410,6 +869,54 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
|
|
|
410
869
|
restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
|
|
411
870
|
}
|
|
412
871
|
|
|
872
|
+
/**
|
|
873
|
+
* Abstract base for all domain-layer exceptions in ddd-kit.
|
|
874
|
+
*
|
|
875
|
+
* Domain methods throw `DomainError`-derived exceptions when invariants are
|
|
876
|
+
* violated. Consumers derive their own concrete errors from this base
|
|
877
|
+
* (e.g. `OrderAlreadyShippedError extends DomainError`) so that domain
|
|
878
|
+
* failures have sprechende names and can be caught via `instanceof`.
|
|
879
|
+
*
|
|
880
|
+
* The library itself uses this base only for its own internal errors
|
|
881
|
+
* (`MissingHandlerError`, `AggregateNotFoundError`) — everything else
|
|
882
|
+
* is consumer territory.
|
|
883
|
+
*/
|
|
884
|
+
declare abstract class DomainError extends Error {
|
|
885
|
+
constructor(message: string, options?: ErrorOptions);
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Thrown by `EventSourcedAggregate.apply()` when no handler is registered
|
|
889
|
+
* for the given event type. Indicates a missing entry in the aggregate's
|
|
890
|
+
* `handlers` map — a programming error in the aggregate definition.
|
|
891
|
+
*/
|
|
892
|
+
declare class MissingHandlerError extends DomainError {
|
|
893
|
+
readonly eventType: string;
|
|
894
|
+
constructor(eventType: string);
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Thrown by `IRepository.getByIdOrFail()` when an aggregate with the given
|
|
898
|
+
* id does not exist. Use the nullable variant `getById()` if "not found"
|
|
899
|
+
* is an expected outcome.
|
|
900
|
+
*/
|
|
901
|
+
declare class AggregateNotFoundError extends DomainError {
|
|
902
|
+
readonly aggregateType: string;
|
|
903
|
+
readonly id: string;
|
|
904
|
+
constructor(aggregateType: string, id: string);
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Thrown by `IRepository.save()` when the aggregate's expected version does
|
|
908
|
+
* not match the version currently persisted — i.e. another writer updated
|
|
909
|
+
* the aggregate concurrently. The canonical DDD optimistic-concurrency
|
|
910
|
+
* signal; callers typically reload, re-apply the use case, and retry.
|
|
911
|
+
*/
|
|
912
|
+
declare class ConcurrencyConflictError extends DomainError {
|
|
913
|
+
readonly aggregateType: string;
|
|
914
|
+
readonly aggregateId: string;
|
|
915
|
+
readonly expectedVersion: number;
|
|
916
|
+
readonly actualVersion: number;
|
|
917
|
+
constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number);
|
|
918
|
+
}
|
|
919
|
+
|
|
413
920
|
/**
|
|
414
921
|
* Interface for Event-Sourced Aggregate Roots.
|
|
415
922
|
* Defines the contract for aggregates that manage state changes via event sourcing.
|
|
@@ -417,17 +924,19 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
|
|
|
417
924
|
* @template TId - The type of the aggregate root identifier
|
|
418
925
|
* @template TEvent - The union type of all domain events
|
|
419
926
|
*/
|
|
420
|
-
interface
|
|
927
|
+
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEvent<string, unknown>> extends IAggregateRoot<TId> {
|
|
421
928
|
/**
|
|
422
929
|
* Returns a read-only list of new, not-yet-persisted events.
|
|
423
930
|
*/
|
|
424
931
|
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
425
932
|
/**
|
|
426
|
-
* Reconstitutes the aggregate from an event history.
|
|
933
|
+
* Reconstitutes the aggregate from an event history. Returns `Result`
|
|
934
|
+
* because event-stream corruption is an expected recoverable failure
|
|
935
|
+
* at the infrastructure boundary.
|
|
427
936
|
*
|
|
428
937
|
* @param history - An ordered list of past events
|
|
429
938
|
*/
|
|
430
|
-
loadFromHistory(history: TEvent[]): Result<void,
|
|
939
|
+
loadFromHistory(history: TEvent[]): Result<void, DomainError>;
|
|
431
940
|
/**
|
|
432
941
|
* Clears the list of pending events.
|
|
433
942
|
*/
|
|
@@ -447,336 +956,174 @@ interface IAggregateEventSourced<TId extends Id<string>, TEvent extends DomainEv
|
|
|
447
956
|
}
|
|
448
957
|
type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
|
|
449
958
|
/**
|
|
450
|
-
*
|
|
959
|
+
* Configuration options for EventSourcedAggregate behavior.
|
|
451
960
|
*/
|
|
452
|
-
interface
|
|
961
|
+
interface EventSourcedAggregateConfig {
|
|
453
962
|
/**
|
|
454
|
-
* Whether
|
|
455
|
-
*
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
* All changes to child entities are versioned through the Aggregate Root. The version
|
|
470
|
-
* applies to the entire aggregate, including all child entities.
|
|
471
|
-
*
|
|
472
|
-
* Extends `AggregateRoot` with Event Sourcing capabilities:
|
|
473
|
-
* - Event tracking (pendingEvents)
|
|
474
|
-
* - Event handlers for state transitions
|
|
475
|
-
* - Event validation
|
|
476
|
-
* - History replay
|
|
477
|
-
*
|
|
478
|
-
* Use this class when you want Event Sourcing with full event tracking
|
|
479
|
-
* and replay capabilities.
|
|
480
|
-
*
|
|
481
|
-
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
482
|
-
* @template TEvent - The union type of all domain events
|
|
483
|
-
* @template TId - The type of the aggregate root identifier
|
|
484
|
-
*
|
|
485
|
-
* @example
|
|
486
|
-
* ```typescript
|
|
487
|
-
* // Order is an Aggregate Root (an Entity) with Event Sourcing
|
|
488
|
-
* class Order extends AggregateEventSourced<OrderState, OrderEvent, OrderId> {
|
|
489
|
-
* confirm(): void {
|
|
490
|
-
* this.apply(createDomainEvent("OrderConfirmed", {}));
|
|
491
|
-
* }
|
|
492
|
-
*
|
|
493
|
-
* protected readonly handlers = {
|
|
494
|
-
* OrderConfirmed: (state: OrderState): OrderState => ({
|
|
495
|
-
* ...state,
|
|
496
|
-
* status: "confirmed",
|
|
497
|
-
* }),
|
|
498
|
-
* };
|
|
499
|
-
* }
|
|
500
|
-
* ```
|
|
501
|
-
*/
|
|
502
|
-
declare abstract class AggregateEventSourced<TState, TEvent extends DomainEvent<string, unknown>, TId extends Id<string>> extends AggregateRoot<TState, TId, TEvent> implements IAggregateEventSourced<TId, TEvent> {
|
|
503
|
-
private readonly _eventConfig;
|
|
504
|
-
private readonly _eventAutoVersionBump;
|
|
505
|
-
protected constructor(id: TId, initialState: TState, config?: AggregateEventSourcedConfig);
|
|
506
|
-
/**
|
|
507
|
-
* Returns a read-only list of new, not-yet-persisted events.
|
|
508
|
-
*/
|
|
509
|
-
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
510
|
-
/**
|
|
511
|
-
* Clears the list of pending events.
|
|
512
|
-
* Typically called after the events have been persisted.
|
|
513
|
-
*/
|
|
514
|
-
clearPendingEvents(): void;
|
|
515
|
-
/**
|
|
516
|
-
* Validates an event before it is applied.
|
|
517
|
-
* Override this method to add custom validation logic.
|
|
518
|
-
* Return `ok(true)` if the event is valid, `err(message)` otherwise.
|
|
519
|
-
*
|
|
520
|
-
* @param event - The event to validate
|
|
521
|
-
* @returns Result indicating if the event is valid
|
|
522
|
-
*
|
|
523
|
-
* @example
|
|
524
|
-
* ```typescript
|
|
525
|
-
* protected validateEvent(event: OrderEvent): Result<true, string> {
|
|
526
|
-
* if (event.type === "OrderShipped" && this.state.status !== "confirmed") {
|
|
527
|
-
* return err("Order must be confirmed before shipping");
|
|
528
|
-
* }
|
|
529
|
-
* return ok(true);
|
|
530
|
-
* }
|
|
531
|
-
* ```
|
|
532
|
-
*/
|
|
533
|
-
protected validateEvent(_event: TEvent): Result<true, string>;
|
|
534
|
-
/**
|
|
535
|
-
* Applies an event to change the state and adds it
|
|
536
|
-
* to the list of pending events.
|
|
537
|
-
* Returns a Result type instead of throwing an error.
|
|
538
|
-
*
|
|
539
|
-
* @param event - The domain event to apply
|
|
540
|
-
* @param isNew - Indicates whether the event is new (and needs to be persisted)
|
|
541
|
-
* or if it is being loaded from history
|
|
542
|
-
* @returns Result<void, string> - ok if successful, err with error message if validation fails or handler is missing
|
|
543
|
-
*/
|
|
544
|
-
protected apply(event: TEvent, isNew?: boolean): Result<void, string>;
|
|
545
|
-
/**
|
|
546
|
-
* Applies an event to change the state and adds it
|
|
547
|
-
* to the list of pending events.
|
|
548
|
-
* Throws an error if validation fails or handler is missing.
|
|
549
|
-
*
|
|
550
|
-
* @param event - The domain event to apply
|
|
551
|
-
* @param isNew - Indicates whether the event is new (and needs to be persisted)
|
|
552
|
-
* or if it is being loaded from history
|
|
553
|
-
* @throws Error if event validation fails or handler is missing
|
|
554
|
-
*/
|
|
555
|
-
protected applyUnsafe(event: TEvent, isNew?: boolean): void;
|
|
556
|
-
/**
|
|
557
|
-
* Manually bumps the aggregate version.
|
|
558
|
-
* Only needed if `autoVersionBump` is disabled.
|
|
559
|
-
*/
|
|
560
|
-
protected bumpVersion(): void;
|
|
561
|
-
/**
|
|
562
|
-
* Reconstitutes the aggregate from an event history.
|
|
563
|
-
* Sets the version to the number of events in the history.
|
|
564
|
-
*
|
|
565
|
-
* @param history - An ordered list of past events
|
|
566
|
-
*/
|
|
567
|
-
loadFromHistory(history: TEvent[]): Result<void, string>;
|
|
568
|
-
/**
|
|
569
|
-
* Checks if the aggregate has any pending events.
|
|
570
|
-
*
|
|
571
|
-
* @returns true if there are pending events, false otherwise
|
|
572
|
-
*/
|
|
573
|
-
hasPendingEvents(): boolean;
|
|
574
|
-
/**
|
|
575
|
-
* Returns the number of pending events.
|
|
576
|
-
*
|
|
577
|
-
* @returns The count of pending events
|
|
578
|
-
*/
|
|
579
|
-
getEventCount(): number;
|
|
580
|
-
/**
|
|
581
|
-
* Returns the latest pending event, if any.
|
|
582
|
-
*
|
|
583
|
-
* @returns The most recent event or undefined if no events exist
|
|
584
|
-
*/
|
|
585
|
-
getLatestEvent(): TEvent | undefined;
|
|
586
|
-
/**
|
|
587
|
-
* Restores the aggregate from a snapshot and applies events that occurred after the snapshot.
|
|
588
|
-
* This is more efficient than replaying all events from the beginning.
|
|
589
|
-
*
|
|
590
|
-
* @param snapshot - The snapshot to restore from
|
|
591
|
-
* @param eventsAfterSnapshot - Events that occurred after the snapshot was taken
|
|
592
|
-
*
|
|
593
|
-
* @example
|
|
594
|
-
* ```typescript
|
|
595
|
-
* const snapshot = await snapshotRepository.getLatest(aggregateId);
|
|
596
|
-
* const eventsAfter = await eventStore.getEventsAfter(aggregateId, snapshot.version);
|
|
597
|
-
* aggregate.restoreFromSnapshotWithEvents(snapshot, eventsAfter);
|
|
598
|
-
* ```
|
|
599
|
-
*/
|
|
600
|
-
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, string>;
|
|
601
|
-
/**
|
|
602
|
-
* A map of event types to their corresponding handlers.
|
|
603
|
-
* Subclasses MUST implement this property.
|
|
604
|
-
*/
|
|
605
|
-
protected abstract readonly handlers: {
|
|
606
|
-
[K in TEvent["type"]]: Handler<TState, Extract<TEvent, {
|
|
607
|
-
type: K;
|
|
608
|
-
}>>;
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
type Version = number & {
|
|
613
|
-
readonly __v: true;
|
|
614
|
-
};
|
|
615
|
-
/**
|
|
616
|
-
* Metadata associated with a domain event for traceability and correlation.
|
|
617
|
-
* Used in event-driven architectures to track event flow across services.
|
|
618
|
-
*/
|
|
619
|
-
interface EventMetadata {
|
|
620
|
-
/**
|
|
621
|
-
* Correlation ID for tracing events across multiple services/components.
|
|
622
|
-
* Typically used to group related events in a distributed system.
|
|
623
|
-
*/
|
|
624
|
-
correlationId?: string;
|
|
625
|
-
/**
|
|
626
|
-
* Causation ID referencing the event or command that caused this event.
|
|
627
|
-
* Used to build event chains and understand causality.
|
|
628
|
-
*/
|
|
629
|
-
causationId?: string;
|
|
630
|
-
/**
|
|
631
|
-
* User ID of the person or system that triggered the event.
|
|
632
|
-
*/
|
|
633
|
-
userId?: string;
|
|
634
|
-
/**
|
|
635
|
-
* Source service or component that produced the event.
|
|
636
|
-
*/
|
|
637
|
-
source?: string;
|
|
638
|
-
/**
|
|
639
|
-
* Additional custom metadata fields.
|
|
640
|
-
* Allows extensibility for domain-specific metadata.
|
|
641
|
-
*/
|
|
642
|
-
[key: string]: unknown;
|
|
643
|
-
}
|
|
644
|
-
/**
|
|
645
|
-
* Domain Event represents something meaningful that happened in the domain.
|
|
646
|
-
* Events are immutable and carry information about what occurred.
|
|
647
|
-
*
|
|
648
|
-
* @template T - The event type name (e.g., "OrderCreated")
|
|
649
|
-
* @template P - The event payload type
|
|
650
|
-
*/
|
|
651
|
-
interface DomainEvent<T extends string, P = void> {
|
|
652
|
-
/**
|
|
653
|
-
* The type of the event, used for routing and handling.
|
|
654
|
-
*/
|
|
655
|
-
type: T;
|
|
656
|
-
/**
|
|
657
|
-
* The event payload containing the domain data.
|
|
658
|
-
* Omitted when P is void (events without payload).
|
|
659
|
-
*/
|
|
660
|
-
payload: P;
|
|
661
|
-
/**
|
|
662
|
-
* Timestamp when the event occurred.
|
|
663
|
-
*/
|
|
664
|
-
occurredAt: Date;
|
|
665
|
-
/**
|
|
666
|
-
* Event schema version for handling schema evolution.
|
|
667
|
-
* Defaults to 1 if not specified. Higher versions indicate schema changes.
|
|
668
|
-
*/
|
|
669
|
-
version?: number;
|
|
670
|
-
/**
|
|
671
|
-
* Optional metadata for traceability, correlation, and auditing.
|
|
672
|
-
* Includes correlationId, causationId, userId, source, and custom fields.
|
|
673
|
-
*/
|
|
674
|
-
metadata?: EventMetadata;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Structural interface representing an aggregate with state and events.
|
|
679
|
-
* Used for type constraints in repositories and other infrastructure code.
|
|
680
|
-
*
|
|
681
|
-
* @template State - The type of the aggregate state
|
|
682
|
-
* @template Evt - The union type of all domain events
|
|
683
|
-
*/
|
|
684
|
-
interface Aggregate<State, Evt extends DomainEvent<string, unknown>> {
|
|
685
|
-
state: Readonly<State>;
|
|
686
|
-
version: Version;
|
|
687
|
-
pendingEvents: ReadonlyArray<Evt>;
|
|
688
|
-
}
|
|
689
|
-
declare function aggregate<State, Evt extends DomainEvent<string, unknown>>(state: State, version?: Version): Aggregate<State, Evt>;
|
|
690
|
-
declare function withEvent<S, E extends DomainEvent<string, unknown>>(agg: Aggregate<S, E>, evt: E): Aggregate<S, E>;
|
|
691
|
-
declare function bump<S, E extends DomainEvent<string, unknown>>(agg: Aggregate<S, E>): Aggregate<S, E>;
|
|
692
|
-
/**
|
|
693
|
-
* Creates a domain event with default values.
|
|
694
|
-
* Sets occurredAt to current date and version to 1 if not provided.
|
|
695
|
-
*
|
|
696
|
-
* @param type - The event type
|
|
697
|
-
* @param payload - The event payload
|
|
698
|
-
* @param options - Optional event configuration
|
|
699
|
-
* @returns A domain event
|
|
700
|
-
*
|
|
701
|
-
* @example
|
|
702
|
-
* ```typescript
|
|
703
|
-
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
704
|
-
* ```
|
|
705
|
-
*/
|
|
706
|
-
declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: {
|
|
707
|
-
occurredAt?: Date;
|
|
708
|
-
version?: number;
|
|
709
|
-
metadata?: EventMetadata;
|
|
710
|
-
}): DomainEvent<T, void>;
|
|
711
|
-
declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: {
|
|
712
|
-
occurredAt?: Date;
|
|
713
|
-
version?: number;
|
|
714
|
-
metadata?: EventMetadata;
|
|
715
|
-
}): DomainEvent<T, P>;
|
|
963
|
+
* Whether `apply()` should bump the version per event.
|
|
964
|
+
*
|
|
965
|
+
* Defaults to **`true`** for `EventSourcedAggregate` — each applied
|
|
966
|
+
* event is by definition a versioned state change, so the canonical
|
|
967
|
+
* event-sourcing pattern is "one event = one version bump". Set to
|
|
968
|
+
* `false` only if your event store assigns version numbers itself
|
|
969
|
+
* and you want the aggregate to track them via `bumpVersion()` /
|
|
970
|
+
* `setVersion()` calls instead.
|
|
971
|
+
*
|
|
972
|
+
* (Contrast with `AggregateRoot`, which defaults this to `false`
|
|
973
|
+
* because its `setState()` already takes a per-call `bumpVersion`
|
|
974
|
+
* argument.)
|
|
975
|
+
*/
|
|
976
|
+
autoVersionBump?: boolean;
|
|
977
|
+
}
|
|
716
978
|
/**
|
|
717
|
-
*
|
|
718
|
-
* Convenience function for creating events with correlation and causation IDs.
|
|
979
|
+
* Base class for Event-Sourced Aggregate Roots (Vernon, IDDD Chapter 8).
|
|
719
980
|
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
* @param options - Optional event configuration
|
|
724
|
-
* @returns A domain event with metadata
|
|
981
|
+
* Like `AggregateRoot`, this is both the root entity and the aggregate boundary.
|
|
982
|
+
* The difference is persistence: state is derived from events, not stored directly.
|
|
983
|
+
* Events are the single source of truth — all state changes go through `apply()` → handler.
|
|
725
984
|
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* "OrderCreated",
|
|
730
|
-
* { orderId: "123" },
|
|
731
|
-
* {
|
|
732
|
-
* correlationId: "corr-123",
|
|
733
|
-
* causationId: "cmd-456",
|
|
734
|
-
* userId: "user-789"
|
|
735
|
-
* }
|
|
736
|
-
* );
|
|
737
|
-
* ```
|
|
738
|
-
*/
|
|
739
|
-
declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: {
|
|
740
|
-
occurredAt?: Date;
|
|
741
|
-
version?: number;
|
|
742
|
-
}): DomainEvent<T, P>;
|
|
743
|
-
/**
|
|
744
|
-
* Copies metadata from a source event to a new event.
|
|
745
|
-
* Useful for maintaining correlation chains in event-driven architectures.
|
|
985
|
+
* Extends `Entity` directly (not `AggregateRoot`) so that `setState()` and
|
|
986
|
+
* `addDomainEvent()` are not available. This enforces the event sourcing pattern
|
|
987
|
+
* at the type level — there is no way to mutate state without going through an event handler.
|
|
746
988
|
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
989
|
+
* `apply()` and `validateEvent()` throw `DomainError`-derived exceptions on
|
|
990
|
+
* invariant violations. Subclasses override `validateEvent()` to throw their
|
|
991
|
+
* own concrete subclasses (e.g. `OrderAlreadyConfirmedError`). Only the
|
|
992
|
+
* infrastructure-boundary methods (`loadFromHistory`,
|
|
993
|
+
* `restoreFromSnapshotWithEvents`) return `Result` — they catch `DomainError`
|
|
994
|
+
* during replay so callers can react to corrupted event streams without
|
|
995
|
+
* try/catch.
|
|
996
|
+
*
|
|
997
|
+
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
998
|
+
* @template TEvent - The union type of all domain events
|
|
999
|
+
* @template TId - The type of the aggregate root identifier
|
|
750
1000
|
*
|
|
751
1001
|
* @example
|
|
752
1002
|
* ```typescript
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
1003
|
+
* class OrderAlreadyConfirmedError extends DomainError {
|
|
1004
|
+
* constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
|
|
1005
|
+
* }
|
|
1006
|
+
*
|
|
1007
|
+
* class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
1008
|
+
* confirm(): void {
|
|
1009
|
+
* this.apply(createDomainEvent("OrderConfirmed", {}));
|
|
758
1010
|
* }
|
|
759
|
-
* );
|
|
760
|
-
* ```
|
|
761
|
-
*/
|
|
762
|
-
declare function copyMetadata(sourceEvent: DomainEvent<string, unknown>, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
|
|
763
|
-
/**
|
|
764
|
-
* Merges multiple metadata objects into one.
|
|
765
|
-
* Later metadata objects override earlier ones for the same keys.
|
|
766
1011
|
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
1012
|
+
* protected validateEvent(event: OrderEvent): void {
|
|
1013
|
+
* if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
|
|
1014
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
1015
|
+
* }
|
|
1016
|
+
* }
|
|
769
1017
|
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
1018
|
+
* protected readonly handlers = {
|
|
1019
|
+
* OrderConfirmed: (state: OrderState): OrderState => ({
|
|
1020
|
+
* ...state,
|
|
1021
|
+
* status: "confirmed",
|
|
1022
|
+
* }),
|
|
1023
|
+
* };
|
|
1024
|
+
* }
|
|
777
1025
|
* ```
|
|
778
1026
|
*/
|
|
779
|
-
declare
|
|
1027
|
+
declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<string, unknown>, TId extends Id<string>> extends Entity<TState, TId> implements IEventSourcedAggregate<TId, TEvent> {
|
|
1028
|
+
private _version;
|
|
1029
|
+
get version(): Version;
|
|
1030
|
+
private setVersion;
|
|
1031
|
+
private _pendingEvents;
|
|
1032
|
+
private readonly _autoVersionBump;
|
|
1033
|
+
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
1034
|
+
clearPendingEvents(): void;
|
|
1035
|
+
/**
|
|
1036
|
+
* Post-save hook called by a `Repository.save()` implementation to push
|
|
1037
|
+
* the persisted version back into the in-memory aggregate and clear the
|
|
1038
|
+
* pending events (they are now in the event store / outbox). Lets
|
|
1039
|
+
* `save()` keep its `Promise<void>` return type.
|
|
1040
|
+
*/
|
|
1041
|
+
markPersisted(version: Version): void;
|
|
1042
|
+
protected constructor(id: TId, initialState: TState, config?: EventSourcedAggregateConfig);
|
|
1043
|
+
/**
|
|
1044
|
+
* Validates an event before it is applied. Default is no-op.
|
|
1045
|
+
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
1046
|
+
* the event violates an invariant in the current state.
|
|
1047
|
+
*/
|
|
1048
|
+
protected validateEvent(_event: TEvent): void;
|
|
1049
|
+
/**
|
|
1050
|
+
* Applies an event: validates, locates the handler, computes the next
|
|
1051
|
+
* state, then commits state + pending event + version bump atomically.
|
|
1052
|
+
*
|
|
1053
|
+
* Throws `DomainError` (or a subclass) on validation failure.
|
|
1054
|
+
* Throws `MissingHandlerError` if no handler is registered for `event.type`.
|
|
1055
|
+
*
|
|
1056
|
+
* State is not mutated if any step throws — the handler is invoked into
|
|
1057
|
+
* a local and only assigned to `_state` once all checks pass.
|
|
1058
|
+
*
|
|
1059
|
+
* The method is generic in the event tag `K`, so concrete callers
|
|
1060
|
+
* (`this.apply(orderCreated)`) narrow to the literal tag and the
|
|
1061
|
+
* dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }>>`
|
|
1062
|
+
* — no `as` cast required at the call site.
|
|
1063
|
+
*
|
|
1064
|
+
* @param event - The domain event to apply
|
|
1065
|
+
* @param isNew - Whether the event is new (needs persisting) or replayed from history
|
|
1066
|
+
*/
|
|
1067
|
+
protected apply<K extends TEvent["type"]>(event: Extract<TEvent, {
|
|
1068
|
+
type: K;
|
|
1069
|
+
}>, isNew?: boolean): void;
|
|
1070
|
+
/**
|
|
1071
|
+
* Internal dispatch path used by `apply()` and the replay methods
|
|
1072
|
+
* (`loadFromHistory`, `restoreFromSnapshotWithEvents`). The replay loop
|
|
1073
|
+
* iterates over `TEvent[]` and therefore cannot supply a narrowed `K`
|
|
1074
|
+
* generic, so this helper accepts `TEvent` and the discriminator is
|
|
1075
|
+
* resolved via the (statically-sound) `handlers` map.
|
|
1076
|
+
*/
|
|
1077
|
+
private dispatchAndCommit;
|
|
1078
|
+
/**
|
|
1079
|
+
* Manually bumps the aggregate version.
|
|
1080
|
+
* Only needed if `autoVersionBump` is disabled.
|
|
1081
|
+
*/
|
|
1082
|
+
protected bumpVersion(): void;
|
|
1083
|
+
/**
|
|
1084
|
+
* Reconstitutes the aggregate from an event history. Catches `DomainError`
|
|
1085
|
+
* thrown during replay and returns it as an `Err` — this is the
|
|
1086
|
+
* infrastructure boundary, where event-stream corruption is an expected
|
|
1087
|
+
* recoverable failure. Unexpected (non-DomainError) throws propagate.
|
|
1088
|
+
*
|
|
1089
|
+
* Version advances additively: the aggregate's pre-existing version plus
|
|
1090
|
+
* `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
|
|
1091
|
+
* an aggregate already at v=1 (e.g. after a creation event) loading
|
|
1092
|
+
* 2 events ends at v=3, not v=2.
|
|
1093
|
+
*/
|
|
1094
|
+
loadFromHistory(history: TEvent[]): Result<void, DomainError>;
|
|
1095
|
+
hasPendingEvents(): boolean;
|
|
1096
|
+
getEventCount(): number;
|
|
1097
|
+
getLatestEvent(): TEvent | undefined;
|
|
1098
|
+
/**
|
|
1099
|
+
* Creates a snapshot of the current aggregate state.
|
|
1100
|
+
*/
|
|
1101
|
+
createSnapshot(): AggregateSnapshot<TState>;
|
|
1102
|
+
/**
|
|
1103
|
+
* Restores the aggregate from a snapshot and applies events that occurred
|
|
1104
|
+
* after. Same infrastructure-boundary semantics as `loadFromHistory`:
|
|
1105
|
+
* catches `DomainError` and returns it as an `Err`; non-domain throws
|
|
1106
|
+
* propagate.
|
|
1107
|
+
*
|
|
1108
|
+
* All-or-nothing: if any event mid-stream throws a `DomainError`, the
|
|
1109
|
+
* aggregate is rolled back to its pre-call state + version. Partial
|
|
1110
|
+
* restoration is never observable to the caller.
|
|
1111
|
+
*/
|
|
1112
|
+
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, DomainError>;
|
|
1113
|
+
/**
|
|
1114
|
+
* A map of event types to their corresponding handlers.
|
|
1115
|
+
* Subclasses MUST implement this property.
|
|
1116
|
+
*/
|
|
1117
|
+
protected abstract readonly handlers: {
|
|
1118
|
+
[K in TEvent["type"]]: Handler<TState, Extract<TEvent, {
|
|
1119
|
+
type: K;
|
|
1120
|
+
}>>;
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
type Version = number & {
|
|
1125
|
+
readonly __v: true;
|
|
1126
|
+
};
|
|
780
1127
|
/**
|
|
781
1128
|
* Snapshot of an aggregate state at a specific point in time.
|
|
782
1129
|
* Used for optimizing event replay by starting from a snapshot
|
|
@@ -799,25 +1146,24 @@ interface AggregateSnapshot<TState> {
|
|
|
799
1146
|
snapshotAt: Date;
|
|
800
1147
|
}
|
|
801
1148
|
/**
|
|
802
|
-
* Checks if two aggregates are the same (same ID and version).
|
|
1149
|
+
* Checks if two aggregates are at the same version (same ID and version).
|
|
803
1150
|
* Useful for optimistic concurrency control checks.
|
|
804
1151
|
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
* @returns true if both aggregates have the same ID and version
|
|
1152
|
+
* Note: Two aggregates with the same ID ARE the same aggregate (identity).
|
|
1153
|
+
* This function checks if they are at the same version — i.e., no concurrent modification.
|
|
808
1154
|
*
|
|
809
1155
|
* @example
|
|
810
1156
|
* ```typescript
|
|
811
|
-
* const
|
|
1157
|
+
* const before = await repository.getById(id);
|
|
812
1158
|
* // ... some operations ...
|
|
813
|
-
* const
|
|
1159
|
+
* const after = await repository.getById(id);
|
|
814
1160
|
*
|
|
815
|
-
* if (!
|
|
1161
|
+
* if (!sameVersion(before, after)) {
|
|
816
1162
|
* throw new Error("Aggregate was modified by another process");
|
|
817
1163
|
* }
|
|
818
1164
|
* ```
|
|
819
1165
|
*/
|
|
820
|
-
declare function
|
|
1166
|
+
declare function sameVersion<TId extends Id<string>>(a: {
|
|
821
1167
|
id: TId;
|
|
822
1168
|
version: Version;
|
|
823
1169
|
}, b: {
|
|
@@ -913,42 +1259,85 @@ interface Command {
|
|
|
913
1259
|
*/
|
|
914
1260
|
type CommandHandler<C extends Command, R> = (cmd: C) => Promise<Result<R, string>>;
|
|
915
1261
|
|
|
1262
|
+
/**
|
|
1263
|
+
* Type map for command types to their return types.
|
|
1264
|
+
* Used to improve type inference in CommandBus.
|
|
1265
|
+
*
|
|
1266
|
+
* @example
|
|
1267
|
+
* ```typescript
|
|
1268
|
+
* type MyCommandMap = {
|
|
1269
|
+
* CreateOrder: OrderId;
|
|
1270
|
+
* CancelOrder: void;
|
|
1271
|
+
* };
|
|
1272
|
+
*
|
|
1273
|
+
* const bus = new CommandBus<MyCommandMap>();
|
|
1274
|
+
* const result = await bus.execute({ type: "CreateOrder", ... });
|
|
1275
|
+
* // result: Result<OrderId, string> ← automatically inferred
|
|
1276
|
+
* ```
|
|
1277
|
+
*/
|
|
1278
|
+
type CommandTypeMap = Record<string, unknown>;
|
|
916
1279
|
/**
|
|
917
1280
|
* Command Bus interface for dispatching commands to their handlers.
|
|
918
1281
|
* Provides a centralized way to execute commands with handler registration.
|
|
919
1282
|
*
|
|
1283
|
+
* Supports an optional type map (`TMap`) for automatic return type inference.
|
|
1284
|
+
* Without a type map, the return type must be specified manually or defaults to `unknown`.
|
|
1285
|
+
*
|
|
1286
|
+
* @template TMap - Optional mapping from command type strings to return types
|
|
1287
|
+
*
|
|
920
1288
|
* @example
|
|
921
1289
|
* ```typescript
|
|
1290
|
+
* // With type map (recommended) – return type is inferred
|
|
1291
|
+
* type MyCommands = { CreateOrder: OrderId; CancelOrder: void };
|
|
1292
|
+
* const bus = new CommandBus<MyCommands>();
|
|
1293
|
+
* const result = await bus.execute({ type: "CreateOrder", ... });
|
|
1294
|
+
* // result: Result<OrderId, string>
|
|
1295
|
+
*
|
|
1296
|
+
* // Without type map – works like before
|
|
922
1297
|
* const bus = new CommandBus();
|
|
923
1298
|
* bus.register("CreateOrder", createOrderHandler);
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
* type: "CreateOrder",
|
|
927
|
-
* customerId: "123",
|
|
928
|
-
* items: [...]
|
|
929
|
-
* });
|
|
1299
|
+
* const result = await bus.execute({ type: "CreateOrder", ... });
|
|
1300
|
+
* // result: Result<unknown, string>
|
|
930
1301
|
* ```
|
|
931
1302
|
*/
|
|
932
|
-
interface ICommandBus {
|
|
1303
|
+
interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
|
|
933
1304
|
/**
|
|
934
1305
|
* Executes a command by dispatching it to the registered handler.
|
|
1306
|
+
* When a type map is provided, the return type is inferred from the command type.
|
|
935
1307
|
*
|
|
936
1308
|
* @param command - The command to execute
|
|
937
1309
|
* @returns Result containing the success value or error message
|
|
938
1310
|
*/
|
|
1311
|
+
execute<C extends Command & {
|
|
1312
|
+
type: keyof TMap & string;
|
|
1313
|
+
}>(command: C): Promise<Result<TMap[C["type"]], string>>;
|
|
939
1314
|
execute<C extends Command, R>(command: C): Promise<Result<R, string>>;
|
|
940
1315
|
/**
|
|
941
1316
|
* Registers a handler for a specific command type.
|
|
942
1317
|
*
|
|
1318
|
+
* When `TMap` is supplied, the `commandType` argument is restricted to
|
|
1319
|
+
* its keys and the handler signature is forced to match `TMap[K]` for the
|
|
1320
|
+
* return value — typos and wrong-typed handlers are compile errors.
|
|
1321
|
+
* Without `TMap` the registration is loose (any string key, any return
|
|
1322
|
+
* type) so the no-config path keeps working.
|
|
1323
|
+
*
|
|
943
1324
|
* @param commandType - The command type to register the handler for
|
|
944
1325
|
* @param handler - The handler function for this command type
|
|
945
1326
|
*/
|
|
946
|
-
register<
|
|
1327
|
+
register<K extends keyof TMap & string, C extends Command & {
|
|
1328
|
+
type: K;
|
|
1329
|
+
} = Command & {
|
|
1330
|
+
type: K;
|
|
1331
|
+
}>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
|
|
947
1332
|
}
|
|
948
1333
|
/**
|
|
949
1334
|
* Simple in-memory command bus implementation.
|
|
950
1335
|
* Handlers are stored in a Map and dispatched based on command type.
|
|
951
1336
|
*
|
|
1337
|
+
* Supports an optional type map (`TMap`) for automatic return type inference.
|
|
1338
|
+
* When `TMap` is provided, `execute()` infers the result type from the command type.
|
|
1339
|
+
* Without `TMap`, it works like before (return type defaults to `unknown` or can be specified manually).
|
|
1340
|
+
*
|
|
952
1341
|
* **Note:** This is a basic implementation suitable for development and simple use cases.
|
|
953
1342
|
* For production environments, consider implementing or using a more feature-rich bus that includes:
|
|
954
1343
|
* - Middleware/Pipeline support (logging, validation, authorization)
|
|
@@ -961,20 +1350,32 @@ interface ICommandBus {
|
|
|
961
1350
|
* The `CommandHandler` type can still be used with external production-grade buses
|
|
962
1351
|
* (e.g., RabbitMQ, AWS SQS) while maintaining type safety.
|
|
963
1352
|
*
|
|
1353
|
+
* @template TMap - Optional mapping from command type strings to return types
|
|
1354
|
+
*
|
|
964
1355
|
* @example
|
|
965
1356
|
* ```typescript
|
|
966
|
-
*
|
|
967
|
-
*
|
|
968
|
-
*
|
|
969
|
-
*
|
|
970
|
-
*
|
|
1357
|
+
* // With type map – full inference
|
|
1358
|
+
* type Commands = { CreateOrder: OrderId; CancelOrder: void };
|
|
1359
|
+
* const bus = new CommandBus<Commands>();
|
|
1360
|
+
* const result = await bus.execute({ type: "CreateOrder", ... });
|
|
1361
|
+
* // result: Result<OrderId, string>
|
|
971
1362
|
*
|
|
1363
|
+
* // Without type map – same as before
|
|
1364
|
+
* const bus = new CommandBus();
|
|
1365
|
+
* bus.register("CreateOrder", async (cmd) => ok(orderId));
|
|
972
1366
|
* const result = await bus.execute({ type: "CreateOrder", ... });
|
|
973
1367
|
* ```
|
|
974
1368
|
*/
|
|
975
|
-
declare class CommandBus implements ICommandBus {
|
|
1369
|
+
declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap> implements ICommandBus<TMap> {
|
|
976
1370
|
private readonly handlers;
|
|
977
|
-
register<
|
|
1371
|
+
register<K extends keyof TMap & string, C extends Command & {
|
|
1372
|
+
type: K;
|
|
1373
|
+
} = Command & {
|
|
1374
|
+
type: K;
|
|
1375
|
+
}>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
|
|
1376
|
+
execute<C extends Command & {
|
|
1377
|
+
type: keyof TMap & string;
|
|
1378
|
+
}>(command: C): Promise<Result<TMap[C["type"]], string>>;
|
|
978
1379
|
execute<C extends Command, R>(command: C): Promise<Result<R, string>>;
|
|
979
1380
|
}
|
|
980
1381
|
|
|
@@ -1007,10 +1408,33 @@ type EventHandler<Evt> = (event: Evt) => Promise<void> | void;
|
|
|
1007
1408
|
* await bus.publish([orderCreatedEvent, orderShippedEvent]);
|
|
1008
1409
|
* ```
|
|
1009
1410
|
*/
|
|
1010
|
-
interface EventBus<Evt
|
|
1411
|
+
interface EventBus<Evt extends {
|
|
1412
|
+
type: string;
|
|
1413
|
+
}> {
|
|
1011
1414
|
/**
|
|
1012
1415
|
* Publishes events to all subscribed handlers.
|
|
1013
|
-
*
|
|
1416
|
+
*
|
|
1417
|
+
* **Ordering & parallelism contract:**
|
|
1418
|
+
*
|
|
1419
|
+
* 1. **Events run in input order.** `publish([a, b, c])` dispatches `a`,
|
|
1420
|
+
* awaits all of its handlers, then dispatches `b`, and so on. The
|
|
1421
|
+
* library never reorders or parallelises across events.
|
|
1422
|
+
* 2. **Handlers within a single event run in parallel.** All handlers
|
|
1423
|
+
* subscribed to `event.type` are awaited via `Promise.allSettled` —
|
|
1424
|
+
* none of them sees the others' errors and none is skipped if a
|
|
1425
|
+
* peer fails.
|
|
1426
|
+
* 3. **Errors are collected and thrown AFTER everything dispatches.**
|
|
1427
|
+
* If one handler throws, remaining handlers for that event still
|
|
1428
|
+
* run, and remaining events in the batch still publish. Once
|
|
1429
|
+
* `publish` reaches the end of the batch it throws — the single
|
|
1430
|
+
* error directly if there was one, or an `AggregateError`
|
|
1431
|
+
* ("Multiple event handlers failed") containing every captured
|
|
1432
|
+
* error otherwise. Callers that need fail-fast semantics should
|
|
1433
|
+
* publish events one at a time and not rely on batch atomicity.
|
|
1434
|
+
*
|
|
1435
|
+
* The contract is intentionally simple and in-process. For
|
|
1436
|
+
* cross-process delivery (RabbitMQ, Kafka, etc.), use the `Outbox`
|
|
1437
|
+
* port and a dedicated dispatcher.
|
|
1014
1438
|
*
|
|
1015
1439
|
* @param events - Array of events to publish
|
|
1016
1440
|
*/
|
|
@@ -1033,7 +1457,9 @@ interface EventBus<Evt> {
|
|
|
1033
1457
|
* unsubscribe();
|
|
1034
1458
|
* ```
|
|
1035
1459
|
*/
|
|
1036
|
-
subscribe: <
|
|
1460
|
+
subscribe: <K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1461
|
+
type: K;
|
|
1462
|
+
}>>) => () => void;
|
|
1037
1463
|
/**
|
|
1038
1464
|
* Subscribes to the next occurrence of an event type.
|
|
1039
1465
|
* Returns a Promise that resolves with the event data.
|
|
@@ -1048,27 +1474,125 @@ interface EventBus<Evt> {
|
|
|
1048
1474
|
* console.log("Order created:", event.payload.orderId);
|
|
1049
1475
|
* ```
|
|
1050
1476
|
*/
|
|
1051
|
-
once: <
|
|
1477
|
+
once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
|
|
1478
|
+
type: K;
|
|
1479
|
+
}>>;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Options for `EventBus.once()`. Both fields are optional; without them
|
|
1483
|
+
* `once()` waits forever (the historical behaviour).
|
|
1484
|
+
*/
|
|
1485
|
+
interface OnceOptions {
|
|
1486
|
+
/**
|
|
1487
|
+
* Aborts the wait. When `signal` fires, `once()` rejects with
|
|
1488
|
+
* `signal.reason` (or a generic abort error if none was supplied) and
|
|
1489
|
+
* the internal subscription is removed.
|
|
1490
|
+
*/
|
|
1491
|
+
signal?: AbortSignal;
|
|
1492
|
+
/**
|
|
1493
|
+
* Rejects with a timeout error after this many milliseconds if no event
|
|
1494
|
+
* has arrived. The internal subscription and timer are cleaned up
|
|
1495
|
+
* regardless of which path settles the promise.
|
|
1496
|
+
*/
|
|
1497
|
+
timeoutMs?: number;
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* One pending event in the outbox plus the opaque id the implementation
|
|
1501
|
+
* needs to ack it via `markDispatched`. The library does not prescribe
|
|
1502
|
+
* what `dispatchId` looks like — an implementation can reuse the event's
|
|
1503
|
+
* own `eventId`, generate its own UUID, use the row's auto-increment
|
|
1504
|
+
* primary key, or whatever the storage layer prefers.
|
|
1505
|
+
*/
|
|
1506
|
+
interface OutboxRecord<Evt> {
|
|
1507
|
+
dispatchId: string;
|
|
1508
|
+
event: Evt;
|
|
1052
1509
|
}
|
|
1510
|
+
/**
|
|
1511
|
+
* Transactional outbox port — the bridge between the write-side
|
|
1512
|
+
* transaction and the (out-of-band) event dispatcher.
|
|
1513
|
+
*
|
|
1514
|
+
* Lifecycle:
|
|
1515
|
+
* 1. `add()` inside the write transaction (`withCommit` calls this) so
|
|
1516
|
+
* events persist atomically with the aggregate state.
|
|
1517
|
+
* 2. A separate outbox dispatcher polls `getPending()` and forwards the
|
|
1518
|
+
* events to subscribers / external brokers.
|
|
1519
|
+
* 3. After successful dispatch, the dispatcher calls `markDispatched()`
|
|
1520
|
+
* with the records' `dispatchId`s so they don't come back next poll.
|
|
1521
|
+
*
|
|
1522
|
+
* `markDispatched` is required to be idempotent — calling it with an id
|
|
1523
|
+
* that's already marked is a no-op, not an error. This lets the
|
|
1524
|
+
* dispatcher safely retry on partial-failure.
|
|
1525
|
+
*/
|
|
1053
1526
|
interface Outbox<Evt> {
|
|
1527
|
+
/**
|
|
1528
|
+
* Persists events. Called from inside `withCommit`'s transactional
|
|
1529
|
+
* callback, atomically with the aggregate write.
|
|
1530
|
+
*
|
|
1531
|
+
* **Idempotency:** implementations should dedupe on the event's
|
|
1532
|
+
* `eventId`. `withCommit` itself does not retry, but the surrounding
|
|
1533
|
+
* use case (a queue consumer, an HTTP retry, a transactional
|
|
1534
|
+
* outbox-dispatcher loop) may legitimately invoke the same write more
|
|
1535
|
+
* than once. A unique-key constraint on `(eventId)` in the outbox
|
|
1536
|
+
* table is the standard implementation.
|
|
1537
|
+
*/
|
|
1054
1538
|
add: (events: ReadonlyArray<Evt>) => Promise<void>;
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1539
|
+
/**
|
|
1540
|
+
* Returns up to `limit` outbox records that have not yet been
|
|
1541
|
+
* dispatched. The dispatcher polls this on a schedule. When `limit`
|
|
1542
|
+
* is omitted, the implementation decides on a default page size.
|
|
1543
|
+
*/
|
|
1544
|
+
getPending: (limit?: number) => Promise<ReadonlyArray<OutboxRecord<Evt>>>;
|
|
1545
|
+
/**
|
|
1546
|
+
* Marks the given dispatch records as delivered so subsequent
|
|
1547
|
+
* `getPending` calls don't return them. Must be idempotent on
|
|
1548
|
+
* already-marked ids.
|
|
1549
|
+
*/
|
|
1550
|
+
markDispatched: (dispatchIds: ReadonlyArray<string>) => Promise<void>;
|
|
1058
1551
|
}
|
|
1059
1552
|
|
|
1060
|
-
|
|
1553
|
+
/**
|
|
1554
|
+
* Transaction-scope abstraction.
|
|
1555
|
+
*
|
|
1556
|
+
* Wraps a block of work so it runs inside the persistence layer's native
|
|
1557
|
+
* transaction (Postgres `BEGIN`/`COMMIT`, Mongo session, etc.). The block
|
|
1558
|
+
* commits when the callback resolves and rolls back if it throws.
|
|
1559
|
+
*
|
|
1560
|
+
* This is **not** Fowler's full Unit of Work (no change tracking, no
|
|
1561
|
+
* registerDirty/registerNew/registerDeleted, no commit-time flush). It is
|
|
1562
|
+
* intentionally minimal — change tracking is the ORM's job; the library
|
|
1563
|
+
* stays out of it. The name `TransactionScope` is therefore more honest
|
|
1564
|
+
* than `UnitOfWork`.
|
|
1565
|
+
*
|
|
1566
|
+
* @example
|
|
1567
|
+
* ```typescript
|
|
1568
|
+
* await scope.transactional(async () => {
|
|
1569
|
+
* const order = await repo.getByIdOrFail(orderId);
|
|
1570
|
+
* order.confirm();
|
|
1571
|
+
* await repo.save(order);
|
|
1572
|
+
* });
|
|
1573
|
+
* ```
|
|
1574
|
+
*/
|
|
1575
|
+
interface TransactionScope {
|
|
1061
1576
|
transactional<T>(fn: () => Promise<T>): Promise<T>;
|
|
1062
1577
|
}
|
|
1063
|
-
type RepoProvider<R> = (uow: UnitOfWork) => R;
|
|
1064
1578
|
|
|
1065
1579
|
/**
|
|
1066
|
-
* Helper
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1580
|
+
* Helper for executing a write Use Case inside a Unit of Work.
|
|
1581
|
+
*
|
|
1582
|
+
* Order of operations:
|
|
1583
|
+
* 1. `fn()` runs inside `uow.transactional(...)` — domain mutations + repo
|
|
1584
|
+
* writes happen here.
|
|
1585
|
+
* 2. `outbox.add(events)` is also inside the transaction, so events
|
|
1586
|
+
* persist atomically with the state change (outbox pattern).
|
|
1587
|
+
* 3. The transaction commits.
|
|
1588
|
+
* 4. **After** the commit, `bus.publish(events)` fires for the in-process
|
|
1589
|
+
* fast path.
|
|
1590
|
+
*
|
|
1591
|
+
* Publishing AFTER commit prevents the classic "publish before commit"
|
|
1592
|
+
* footgun: in-process subscribers can never react to events from a
|
|
1593
|
+
* transaction that later rolled back. If `bus.publish` itself fails, the
|
|
1594
|
+
* outbox still holds the events and an outbox-dispatcher will deliver them
|
|
1595
|
+
* (eventual consistency).
|
|
1072
1596
|
*
|
|
1073
1597
|
* @example
|
|
1074
1598
|
* ```typescript
|
|
@@ -1077,18 +1601,17 @@ type RepoProvider<R> = (uow: UnitOfWork) => R;
|
|
|
1077
1601
|
* async () => {
|
|
1078
1602
|
* const order = Order.create(customerId, items);
|
|
1079
1603
|
* await repository.save(order);
|
|
1080
|
-
* return {
|
|
1081
|
-
* result: order.id,
|
|
1082
|
-
* events: order.pendingEvents
|
|
1083
|
-
* };
|
|
1604
|
+
* return { result: order.id, events: order.domainEvents };
|
|
1084
1605
|
* }
|
|
1085
1606
|
* );
|
|
1086
1607
|
* ```
|
|
1087
1608
|
*/
|
|
1088
|
-
declare function withCommit<Evt
|
|
1609
|
+
declare function withCommit<Evt extends {
|
|
1610
|
+
type: string;
|
|
1611
|
+
}, R>(deps: {
|
|
1089
1612
|
outbox: Outbox<Evt>;
|
|
1090
1613
|
bus?: EventBus<Evt>;
|
|
1091
|
-
|
|
1614
|
+
scope: TransactionScope;
|
|
1092
1615
|
}, fn: () => Promise<{
|
|
1093
1616
|
result: R;
|
|
1094
1617
|
events: ReadonlyArray<Evt>;
|
|
@@ -1169,29 +1692,57 @@ interface Query {
|
|
|
1169
1692
|
*/
|
|
1170
1693
|
type QueryHandler<Q extends Query, R> = (query: Q) => Promise<R>;
|
|
1171
1694
|
|
|
1695
|
+
/**
|
|
1696
|
+
* Type map for query types to their return types.
|
|
1697
|
+
* Used to improve type inference in QueryBus.
|
|
1698
|
+
*
|
|
1699
|
+
* @example
|
|
1700
|
+
* ```typescript
|
|
1701
|
+
* type MyQueryMap = {
|
|
1702
|
+
* GetOrder: Order | null;
|
|
1703
|
+
* ListOrders: Order[];
|
|
1704
|
+
* };
|
|
1705
|
+
*
|
|
1706
|
+
* const bus = new QueryBus<MyQueryMap>();
|
|
1707
|
+
* const result = await bus.execute({ type: "GetOrder", orderId: "123" });
|
|
1708
|
+
* // result: Result<Order | null, string> ← automatically inferred
|
|
1709
|
+
* ```
|
|
1710
|
+
*/
|
|
1711
|
+
type QueryTypeMap = Record<string, unknown>;
|
|
1172
1712
|
/**
|
|
1173
1713
|
* Query Bus interface for dispatching queries to their handlers.
|
|
1174
1714
|
* Provides a centralized way to execute queries with handler registration.
|
|
1175
1715
|
*
|
|
1716
|
+
* Supports an optional type map (`TMap`) for automatic return type inference.
|
|
1717
|
+
* Without a type map, the return type must be specified manually or defaults to `unknown`.
|
|
1718
|
+
*
|
|
1719
|
+
* @template TMap - Optional mapping from query type strings to return types
|
|
1720
|
+
*
|
|
1176
1721
|
* @example
|
|
1177
1722
|
* ```typescript
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1723
|
+
* // With type map (recommended) – return type is inferred
|
|
1724
|
+
* type MyQueries = { GetOrder: Order | null; ListOrders: Order[] };
|
|
1725
|
+
* const bus = new QueryBus<MyQueries>();
|
|
1726
|
+
* const result = await bus.execute({ type: "GetOrder", orderId: "123" });
|
|
1727
|
+
* // result: Result<Order | null, string>
|
|
1180
1728
|
*
|
|
1181
|
-
*
|
|
1182
|
-
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1729
|
+
* // Without type map – works like before
|
|
1730
|
+
* const bus = new QueryBus();
|
|
1731
|
+
* const result = await bus.execute({ type: "GetOrder", orderId: "123" });
|
|
1732
|
+
* // result: Result<unknown, string>
|
|
1185
1733
|
* ```
|
|
1186
1734
|
*/
|
|
1187
|
-
interface IQueryBus {
|
|
1735
|
+
interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
|
|
1188
1736
|
/**
|
|
1189
1737
|
* Executes a query by dispatching it to the registered handler.
|
|
1190
|
-
*
|
|
1738
|
+
* When a type map is provided, the return type is inferred from the query type.
|
|
1191
1739
|
*
|
|
1192
1740
|
* @param query - The query to execute
|
|
1193
|
-
* @returns Result containing the query result if successful, or an error message
|
|
1741
|
+
* @returns Result containing the query result if successful, or an error message
|
|
1194
1742
|
*/
|
|
1743
|
+
execute<Q extends Query & {
|
|
1744
|
+
type: keyof TMap & string;
|
|
1745
|
+
}>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
|
|
1195
1746
|
execute<Q extends Query, R>(query: Q): Promise<Result<R, string>>;
|
|
1196
1747
|
/**
|
|
1197
1748
|
* Executes a query by dispatching it to the registered handler.
|
|
@@ -1201,24 +1752,36 @@ interface IQueryBus {
|
|
|
1201
1752
|
* @returns The query result
|
|
1202
1753
|
* @throws Error if no handler is registered for the query type
|
|
1203
1754
|
*/
|
|
1755
|
+
executeUnsafe<Q extends Query & {
|
|
1756
|
+
type: keyof TMap & string;
|
|
1757
|
+
}>(query: Q): Promise<TMap[Q["type"]]>;
|
|
1204
1758
|
executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
|
|
1205
1759
|
/**
|
|
1206
1760
|
* Registers a handler for a specific query type.
|
|
1207
1761
|
*
|
|
1762
|
+
* When `TMap` is supplied, the `queryType` argument is restricted to its
|
|
1763
|
+
* keys and the handler signature is forced to match `TMap[K]` for the
|
|
1764
|
+
* return value — typos and wrong-typed handlers are compile errors.
|
|
1765
|
+
* Without `TMap` the registration is loose (any string key, any return
|
|
1766
|
+
* type) so the no-config path keeps working.
|
|
1767
|
+
*
|
|
1208
1768
|
* @param queryType - The query type to register the handler for
|
|
1209
1769
|
* @param handler - The handler function for this query type
|
|
1210
1770
|
*/
|
|
1211
|
-
register<
|
|
1771
|
+
register<K extends keyof TMap & string, Q extends Query & {
|
|
1772
|
+
type: K;
|
|
1773
|
+
} = Query & {
|
|
1774
|
+
type: K;
|
|
1775
|
+
}>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
|
|
1212
1776
|
}
|
|
1213
|
-
/**
|
|
1214
|
-
* Type map for query types to their return types.
|
|
1215
|
-
* Used to improve type inference in QueryBus.
|
|
1216
|
-
*/
|
|
1217
|
-
type QueryTypeMap = Record<string, unknown>;
|
|
1218
1777
|
/**
|
|
1219
1778
|
* Simple in-memory query bus implementation.
|
|
1220
1779
|
* Handlers are stored in a Map and dispatched based on query type.
|
|
1221
1780
|
*
|
|
1781
|
+
* Supports an optional type map (`TMap`) for automatic return type inference.
|
|
1782
|
+
* When `TMap` is provided, `execute()` and `executeUnsafe()` infer the result type from the query type.
|
|
1783
|
+
* Without `TMap`, it works like before (return type defaults to `unknown` or can be specified manually).
|
|
1784
|
+
*
|
|
1222
1785
|
* **Note:** This is a basic implementation suitable for development and simple use cases.
|
|
1223
1786
|
* For production environments, consider implementing or using a more feature-rich bus that includes:
|
|
1224
1787
|
* - Middleware/Pipeline support (logging, caching, rate limiting)
|
|
@@ -1231,44 +1794,39 @@ type QueryTypeMap = Record<string, unknown>;
|
|
|
1231
1794
|
* The `QueryHandler` type can still be used with external production-grade buses
|
|
1232
1795
|
* (e.g., RabbitMQ, AWS SQS) while maintaining type safety.
|
|
1233
1796
|
*
|
|
1797
|
+
* @template TMap - Optional mapping from query type strings to return types
|
|
1798
|
+
*
|
|
1234
1799
|
* @example
|
|
1235
1800
|
* ```typescript
|
|
1236
|
-
*
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
* });
|
|
1801
|
+
* // With type map – full inference
|
|
1802
|
+
* type Queries = { GetOrder: Order | null; ListOrders: Order[] };
|
|
1803
|
+
* const bus = new QueryBus<Queries>();
|
|
1804
|
+
* const result = await bus.execute({ type: "GetOrder", orderId: "123" });
|
|
1805
|
+
* // result: Result<Order | null, string>
|
|
1240
1806
|
*
|
|
1241
|
-
*
|
|
1807
|
+
* // Without type map – same as before
|
|
1808
|
+
* const bus = new QueryBus();
|
|
1809
|
+
* bus.register("GetOrder", async (query) => repository.getById(query.orderId));
|
|
1810
|
+
* const result = await bus.execute({ type: "GetOrder", orderId: "123" });
|
|
1242
1811
|
* ```
|
|
1243
1812
|
*/
|
|
1244
|
-
declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus {
|
|
1813
|
+
declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus<TMap> {
|
|
1245
1814
|
private readonly handlers;
|
|
1246
|
-
register<
|
|
1815
|
+
register<K extends keyof TMap & string, Q extends Query & {
|
|
1816
|
+
type: K;
|
|
1817
|
+
} = Query & {
|
|
1818
|
+
type: K;
|
|
1819
|
+
}>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
|
|
1247
1820
|
execute<Q extends Query & {
|
|
1248
|
-
type: keyof TMap;
|
|
1821
|
+
type: keyof TMap & string;
|
|
1249
1822
|
}>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
|
|
1250
1823
|
execute<Q extends Query, R>(query: Q): Promise<Result<R, string>>;
|
|
1824
|
+
executeUnsafe<Q extends Query & {
|
|
1825
|
+
type: keyof TMap & string;
|
|
1826
|
+
}>(query: Q): Promise<TMap[Q["type"]]>;
|
|
1251
1827
|
executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
|
|
1252
1828
|
}
|
|
1253
1829
|
|
|
1254
|
-
/**
|
|
1255
|
-
* Guard function that validates a condition and returns a Result.
|
|
1256
|
-
* Returns `ok(true)` if the condition is met, otherwise `err(error)`.
|
|
1257
|
-
*
|
|
1258
|
-
* @param cond - The condition to check
|
|
1259
|
-
* @param error - Error message if condition fails
|
|
1260
|
-
* @returns Result<true, string>
|
|
1261
|
-
*
|
|
1262
|
-
* @example
|
|
1263
|
-
* ```typescript
|
|
1264
|
-
* const result = guard(id.length > 0, "ID cannot be empty");
|
|
1265
|
-
* if (!result.ok) {
|
|
1266
|
-
* return err(result.error);
|
|
1267
|
-
* }
|
|
1268
|
-
* ```
|
|
1269
|
-
*/
|
|
1270
|
-
declare function guard(cond: boolean, error: string): Result<true, string>;
|
|
1271
|
-
|
|
1272
1830
|
/**
|
|
1273
1831
|
* Simple in-memory event bus implementation.
|
|
1274
1832
|
* Supports multiple subscribers per event type (pub/sub pattern).
|
|
@@ -1293,68 +1851,158 @@ declare function guard(cond: boolean, error: string): Result<true, string>;
|
|
|
1293
1851
|
*/
|
|
1294
1852
|
declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements EventBus<Evt> {
|
|
1295
1853
|
private readonly handlers;
|
|
1296
|
-
subscribe<
|
|
1297
|
-
|
|
1854
|
+
subscribe<K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1855
|
+
type: K;
|
|
1856
|
+
}>>): () => void;
|
|
1857
|
+
once<K extends Evt["type"]>(eventType: K, options?: OnceOptions): Promise<Extract<Evt, {
|
|
1858
|
+
type: K;
|
|
1859
|
+
}>>;
|
|
1860
|
+
/**
|
|
1861
|
+
* See {@link EventBus.publish} for the full ordering / parallelism /
|
|
1862
|
+
* error-aggregation contract this implementation realises:
|
|
1863
|
+
* - events in input order, sequentially;
|
|
1864
|
+
* - handlers within one event in parallel via `Promise.allSettled`;
|
|
1865
|
+
* - errors collected and thrown after the batch (single Error, or
|
|
1866
|
+
* `AggregateError` for multiple failures).
|
|
1867
|
+
*/
|
|
1298
1868
|
publish(events: ReadonlyArray<Evt>): Promise<void>;
|
|
1299
1869
|
}
|
|
1300
1870
|
|
|
1301
1871
|
/**
|
|
1302
|
-
*
|
|
1303
|
-
*
|
|
1872
|
+
* Core repository contract for Aggregate Roots.
|
|
1873
|
+
*
|
|
1874
|
+
* In DDD a Repository is a "collection illusion" for aggregates: load by
|
|
1875
|
+
* identity, save the whole aggregate, delete by identity. Querying by
|
|
1876
|
+
* arbitrary criteria is a separate concern (CQRS read-side, ad-hoc bulk
|
|
1877
|
+
* operations) and lives on the `IQueryableRepository` extension below — so
|
|
1878
|
+
* write-side repositories don't have to implement query plumbing they
|
|
1879
|
+
* don't need.
|
|
1880
|
+
*
|
|
1881
|
+
* Repositories work exclusively with Aggregate Root Entities. The Aggregate
|
|
1882
|
+
* Root represents the aggregate externally and is the only object that can
|
|
1883
|
+
* be loaded or saved through repositories. When loading, all child entities
|
|
1884
|
+
* and value objects inside the aggregate are loaded too; when saving, the
|
|
1885
|
+
* whole aggregate is persisted as a unit.
|
|
1886
|
+
*
|
|
1887
|
+
* @template TAgg - The aggregate root type (must implement IAggregateRoot)
|
|
1888
|
+
* @template TId - The type of the aggregate root identifier
|
|
1304
1889
|
*/
|
|
1305
|
-
interface
|
|
1306
|
-
|
|
1890
|
+
interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>> {
|
|
1891
|
+
/**
|
|
1892
|
+
* Loads an aggregate by id. Returns `null` when not found.
|
|
1893
|
+
*/
|
|
1894
|
+
getById(id: TId): Promise<TAgg | null>;
|
|
1895
|
+
/**
|
|
1896
|
+
* Loads an aggregate by id and throws `AggregateNotFoundError` when not
|
|
1897
|
+
* found. Use this when "not found" is a programming/contract error in
|
|
1898
|
+
* the calling Use Case; use `getById` when null is a valid outcome.
|
|
1899
|
+
*/
|
|
1900
|
+
getByIdOrFail(id: TId): Promise<TAgg>;
|
|
1901
|
+
/**
|
|
1902
|
+
* Returns whether an aggregate with the given id exists. Cheaper than
|
|
1903
|
+
* `getById !== null` if your storage supports `EXISTS`-style queries.
|
|
1904
|
+
*/
|
|
1905
|
+
exists(id: TId): Promise<boolean>;
|
|
1906
|
+
/**
|
|
1907
|
+
* Persists the aggregate (insert or update). Implementations should:
|
|
1908
|
+
*
|
|
1909
|
+
* 1. Throw `ConcurrencyConflictError` from `@shirudo/ddd-kit` when the
|
|
1910
|
+
* aggregate's expected version does not match the version currently
|
|
1911
|
+
* stored (optimistic concurrency).
|
|
1912
|
+
* 2. After a successful write, call `aggregate.markPersisted(newVersion)`
|
|
1913
|
+
* so the in-memory aggregate reflects the new version and clears its
|
|
1914
|
+
* pending/domain events.
|
|
1915
|
+
*
|
|
1916
|
+
* Return type stays `void` — the caller already holds the aggregate
|
|
1917
|
+
* reference, which is now up to date.
|
|
1918
|
+
*/
|
|
1919
|
+
save(aggregate: TAgg): Promise<void>;
|
|
1920
|
+
/**
|
|
1921
|
+
* Removes the aggregate by id.
|
|
1922
|
+
*/
|
|
1923
|
+
delete(id: TId): Promise<void>;
|
|
1307
1924
|
}
|
|
1308
|
-
|
|
1309
1925
|
/**
|
|
1310
|
-
* Repository
|
|
1926
|
+
* Repository extension that adds filter-based querying. `TFilter` is the
|
|
1927
|
+
* filter shape your persistence layer speaks: a Drizzle `SQL` expression, a
|
|
1928
|
+
* Prisma `WhereInput`, a MongoDB filter document, a plain
|
|
1929
|
+
* `(t: TAgg) => boolean` predicate for in-memory repos, or anything else.
|
|
1311
1930
|
*
|
|
1312
|
-
*
|
|
1313
|
-
*
|
|
1314
|
-
*
|
|
1931
|
+
* The library does not prescribe a Specification or query DSL — the
|
|
1932
|
+
* Repository implementation owns its query language. This avoids the
|
|
1933
|
+
* phantom-interface trap of a library-level `ISpecification<T>` with no
|
|
1934
|
+
* methods and lets each Repository expose the strongest possible types for
|
|
1935
|
+
* its storage backend.
|
|
1315
1936
|
*
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
* (including all child entities) is persisted as a unit.
|
|
1937
|
+
* Aggregates that are only ever accessed by id should implement
|
|
1938
|
+
* `IRepository` directly and skip this extension.
|
|
1319
1939
|
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1940
|
+
* @template TAgg - The aggregate root type
|
|
1941
|
+
* @template TId - The aggregate root identifier type
|
|
1942
|
+
* @template TFilter - The filter shape understood by this repository
|
|
1322
1943
|
*
|
|
1323
|
-
* @
|
|
1324
|
-
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1944
|
+
* @example
|
|
1945
|
+
* ```typescript
|
|
1946
|
+
* // In-memory repo with a predicate filter
|
|
1947
|
+
* type Predicate<T> = (t: T) => boolean;
|
|
1948
|
+
* class InMemoryOrders implements IQueryableRepository<Order, OrderId, Predicate<Order>> {
|
|
1949
|
+
* // ...
|
|
1950
|
+
* async find(filter: Predicate<Order>): Promise<Order[]> { ... }
|
|
1951
|
+
* async findOne(filter: Predicate<Order>): Promise<Order | null> { ... }
|
|
1952
|
+
* }
|
|
1953
|
+
*
|
|
1954
|
+
* // Drizzle repo with a SQL expression filter
|
|
1955
|
+
* import type { SQL } from "drizzle-orm";
|
|
1956
|
+
* class DrizzleOrders implements IQueryableRepository<Order, OrderId, SQL> {
|
|
1957
|
+
* // ...
|
|
1958
|
+
* }
|
|
1959
|
+
* ```
|
|
1327
1960
|
*/
|
|
1328
|
-
interface
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1961
|
+
interface IQueryableRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>, TFilter> extends IRepository<TAgg, TId> {
|
|
1962
|
+
/**
|
|
1963
|
+
* Returns the first aggregate matching the filter, or `null` if none.
|
|
1964
|
+
*/
|
|
1965
|
+
findOne(filter: TFilter): Promise<TAgg | null>;
|
|
1966
|
+
/**
|
|
1967
|
+
* Returns **every** aggregate matching the filter — no pagination,
|
|
1968
|
+
* no cursor. For unbounded result sets, prefer a read-side projection
|
|
1969
|
+
* (CQRS read model) over loading aggregates in bulk; aggregates are
|
|
1970
|
+
* write-side objects and rehydrating thousands of them by id is rarely
|
|
1971
|
+
* what you want. If you need pagination on the write side, declare a
|
|
1972
|
+
* domain-specific paged method on your concrete repository (e.g.
|
|
1973
|
+
* `findPage(filter, cursor)`) — the library does not prescribe a
|
|
1974
|
+
* pagination contract because cursor/offset/keyset semantics vary too
|
|
1975
|
+
* much across storage backends.
|
|
1976
|
+
*/
|
|
1977
|
+
find(filter: TFilter): Promise<TAgg[]>;
|
|
1334
1978
|
}
|
|
1335
1979
|
|
|
1336
1980
|
type VO<T> = Readonly<T>;
|
|
1337
1981
|
/**
|
|
1338
|
-
* Deep freezes an object and all its nested properties recursively
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1982
|
+
* Deep freezes an object and all its nested properties recursively, then
|
|
1983
|
+
* returns it. Iterates both string-keyed and symbol-keyed own properties
|
|
1984
|
+
* so the freeze symmetry matches `deepEqual` (which also considers symbol
|
|
1985
|
+
* keys). Handles circular references by tracking visited objects.
|
|
1986
|
+
*
|
|
1987
|
+
* Note: `deepFreeze` mutates its argument in place — it sets `[[Frozen]]`
|
|
1988
|
+
* on the object you pass in. Callers that need to avoid touching the
|
|
1989
|
+
* input (e.g. `vo()`) should deep-clone first.
|
|
1341
1990
|
*/
|
|
1342
1991
|
declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
|
|
1343
1992
|
/**
|
|
1344
1993
|
* Creates a deeply immutable value object from the given data.
|
|
1345
|
-
* All nested objects and arrays are frozen recursively.
|
|
1346
1994
|
*
|
|
1347
|
-
*
|
|
1348
|
-
*
|
|
1995
|
+
* The input is first deep-cloned with `structuredClone`, then the clone
|
|
1996
|
+
* is frozen — so calling `vo(input)` never freezes the caller's own
|
|
1997
|
+
* object graph as a side-effect. Mutating the input afterwards does not
|
|
1998
|
+
* bleed into the VO.
|
|
1349
1999
|
*
|
|
1350
2000
|
* @example
|
|
1351
2001
|
* ```typescript
|
|
1352
|
-
* const
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1356
|
-
* });
|
|
1357
|
-
* // address.coordinates.lat = 99; // ❌ Error: Cannot assign to read-only property
|
|
2002
|
+
* const nested = { lat: 52.5, lng: 13.4 };
|
|
2003
|
+
* const address = vo({ street: "Main St", coordinates: nested });
|
|
2004
|
+
* address.coordinates.lat = 99; // ❌ Cannot assign to read-only property
|
|
2005
|
+
* nested.lat = 0; // ✅ caller's input still mutable
|
|
1358
2006
|
* ```
|
|
1359
2007
|
*/
|
|
1360
2008
|
declare function vo<T>(t: T): VO<T>;
|
|
@@ -1453,26 +2101,6 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
|
|
|
1453
2101
|
* ```
|
|
1454
2102
|
*/
|
|
1455
2103
|
declare function voWithValidation<T>(t: T, validate: (value: T) => boolean, errorMessage?: string): Result<VO<T>, string>;
|
|
1456
|
-
/**
|
|
1457
|
-
* Creates a value object with optional validation.
|
|
1458
|
-
* Throws an error if validation fails.
|
|
1459
|
-
*
|
|
1460
|
-
* @param t - The data to convert into a value object
|
|
1461
|
-
* @param validate - Validation function that returns true if valid
|
|
1462
|
-
* @param errorMessage - Optional custom error message if validation fails
|
|
1463
|
-
* @returns A deeply frozen, immutable value object
|
|
1464
|
-
* @throws Error if validation fails
|
|
1465
|
-
*
|
|
1466
|
-
* @example
|
|
1467
|
-
* ```typescript
|
|
1468
|
-
* const money = voWithValidationUnsafe(
|
|
1469
|
-
* { amount: 100, currency: "USD" },
|
|
1470
|
-
* (m) => m.amount >= 0 && m.currency.length === 3,
|
|
1471
|
-
* "Invalid money: amount must be non-negative and currency must be 3 characters"
|
|
1472
|
-
* );
|
|
1473
|
-
* ```
|
|
1474
|
-
*/
|
|
1475
|
-
declare function voWithValidationUnsafe<T>(t: T, validate: (value: T) => boolean, errorMessage?: string): VO<T>;
|
|
1476
2104
|
/**
|
|
1477
2105
|
* Interface for Value Objects.
|
|
1478
2106
|
* Value Objects are immutable and defined by their properties.
|
|
@@ -1564,4 +2192,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
1564
2192
|
toJSON(): Readonly<T>;
|
|
1565
2193
|
}
|
|
1566
2194
|
|
|
1567
|
-
export { type
|
|
2195
|
+
export { type AggregateConfig, AggregateNotFoundError, AggregateRoot, type AggregateSnapshot, type ClockFactory, type Command, CommandBus, type CommandHandler, ConcurrencyConflictError, type CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventIdFactory, type EventMetadata, EventSourcedAggregate, type EventSourcedAggregateConfig, type IAggregateRoot, type ICommandBus, type IEntity, type IEventSourcedAggregate, type IQueryBus, type IQueryableRepository, type IRepository, type IValueObject, type Id, type IdGenerator, type Identifiable, 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 };
|