@shirudo/ddd-kit 1.0.0-rc.1 → 1.0.0-rc.4
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 +94 -143
- package/dist/index.d.ts +858 -186
- package/dist/index.js +655 -381
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +92 -1
- package/dist/utils.js +281 -0
- package/dist/utils.js.map +1 -1
- package/package.json +71 -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 -298
- package/dist/result.js.map +0 -1
- package/dist/utils-array.d.ts +0 -47
- package/dist/utils-array.js +0 -242
- package/dist/utils-array.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,114 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { Result } from '@shirudo/result';
|
|
2
|
+
import { BaseError } from '@shirudo/base-error';
|
|
3
|
+
import { DeepEqualExceptOptions } from './utils.js';
|
|
4
|
+
export { DeepOmitOptions, Key, PathSegment, deepEqual, deepEqualExcept, deepOmit } from './utils.js';
|
|
3
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Branded string ID. `Tag` carries the aggregate / entity name so two ids
|
|
8
|
+
* with different tags are not assignable to each other even though both
|
|
9
|
+
* are strings at runtime.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* type UserId = Id<"UserId">;
|
|
14
|
+
* type OrderId = Id<"OrderId">;
|
|
15
|
+
*
|
|
16
|
+
* const u = "user-1" as UserId;
|
|
17
|
+
* const o: OrderId = u; // ❌ compile error
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
4
20
|
type Id<Tag extends string> = string & {
|
|
5
21
|
readonly __brand: Tag;
|
|
6
22
|
};
|
|
7
|
-
|
|
8
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Produces fresh ids of a single, fixed tag. The tag is bound at the
|
|
25
|
+
* generator type — `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
|
|
26
|
+
* with no caller-side generic to abuse.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { ulid } from "ulid";
|
|
31
|
+
*
|
|
32
|
+
* const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
|
|
33
|
+
* const id = userIds.next(); // Id<"UserId">
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
|
|
37
|
+
* let callers pick `T` themselves — `gen.next<"AnyTag">()` typechecked
|
|
38
|
+
* even when the generator produced different-tag ids, silently defeating
|
|
39
|
+
* the brand.
|
|
40
|
+
*/
|
|
41
|
+
interface IdGenerator<Tag extends string> {
|
|
42
|
+
next: () => Id<Tag>;
|
|
9
43
|
}
|
|
10
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Factory function producing a fresh, unique event identifier for each call.
|
|
47
|
+
*
|
|
48
|
+
* The library ships a default that uses Web Crypto `crypto.randomUUID()`
|
|
49
|
+
* (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
|
|
50
|
+
* Cloudflare Workers, Vercel Edge, and any runtime that implements Web
|
|
51
|
+
* Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
|
|
52
|
+
* random) — for production event stores prefer a **time-ordered** id
|
|
53
|
+
* format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
|
|
54
|
+
* column stay clustered and `ORDER BY eventId` matches creation order.
|
|
55
|
+
* Swap one in via `setEventIdFactory(() => uuidv7())` or `() => ulid()`.
|
|
56
|
+
*/
|
|
57
|
+
type EventIdFactory = () => string;
|
|
58
|
+
/**
|
|
59
|
+
* Replaces the global event-id factory used by `createDomainEvent` and
|
|
60
|
+
* `createDomainEventWithMetadata`. Call once during application bootstrap,
|
|
61
|
+
* for example:
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* import { ulid } from "ulid";
|
|
65
|
+
* import { setEventIdFactory } from "@shirudo/ddd-kit";
|
|
66
|
+
*
|
|
67
|
+
* setEventIdFactory(() => ulid());
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* The per-call `options.eventId` override always wins over this factory.
|
|
71
|
+
*
|
|
72
|
+
* **Module-scoped — last setter wins.** The factory lives as a single
|
|
73
|
+
* module variable; importing two libraries that both call this races on
|
|
74
|
+
* load order. For multi-tenant request isolation (e.g. one factory per
|
|
75
|
+
* tenant in a single Worker invocation) **prefer the per-call
|
|
76
|
+
* `options.eventId`** instead of mutating the global. Same caveat applies
|
|
77
|
+
* to `setClockFactory`.
|
|
78
|
+
*/
|
|
79
|
+
declare function setEventIdFactory(factory: EventIdFactory): void;
|
|
80
|
+
/**
|
|
81
|
+
* Restores the default event-id factory (`crypto.randomUUID()`).
|
|
82
|
+
* Intended for use in test `afterEach` hooks.
|
|
83
|
+
*/
|
|
84
|
+
declare function resetEventIdFactory(): void;
|
|
85
|
+
/**
|
|
86
|
+
* Clock function producing a fresh `Date` for each call. The library
|
|
87
|
+
* defaults to `() => new Date()`; override globally via `setClockFactory`
|
|
88
|
+
* for deterministic event-sourcing tests, time-travel debugging, or any
|
|
89
|
+
* scenario where `occurredAt` must be reproducible.
|
|
90
|
+
*/
|
|
91
|
+
type ClockFactory = () => Date;
|
|
92
|
+
/**
|
|
93
|
+
* Replaces the global clock factory used by `createDomainEvent` and
|
|
94
|
+
* `createDomainEventWithMetadata`. Call once during application bootstrap
|
|
95
|
+
* (or per-test in deterministic test suites):
|
|
96
|
+
*
|
|
97
|
+
* ```ts
|
|
98
|
+
* import { setClockFactory } from "@shirudo/ddd-kit";
|
|
99
|
+
*
|
|
100
|
+
* setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* The per-call `options.occurredAt` override always wins over this
|
|
104
|
+
* factory. Symmetric to `setEventIdFactory`.
|
|
105
|
+
*/
|
|
106
|
+
declare function setClockFactory(factory: ClockFactory): void;
|
|
107
|
+
/**
|
|
108
|
+
* Restores the default clock factory (`() => new Date()`).
|
|
109
|
+
* Intended for use in test `afterEach` hooks.
|
|
110
|
+
*/
|
|
111
|
+
declare function resetClockFactory(): void;
|
|
11
112
|
/**
|
|
12
113
|
* Metadata associated with a domain event for traceability and correlation.
|
|
13
114
|
* Used in event-driven architectures to track event flow across services.
|
|
@@ -45,13 +146,31 @@ interface EventMetadata {
|
|
|
45
146
|
* @template P - The event payload type
|
|
46
147
|
*/
|
|
47
148
|
interface DomainEvent<T extends string, P = void> {
|
|
149
|
+
/**
|
|
150
|
+
* Unique identifier for this specific event instance. Used by idempotent
|
|
151
|
+
* consumers, outbox dispatch tracking, and as the target of
|
|
152
|
+
* `metadata.causationId`. Defaults to `crypto.randomUUID()` if not
|
|
153
|
+
* supplied.
|
|
154
|
+
*/
|
|
155
|
+
eventId: string;
|
|
48
156
|
/**
|
|
49
157
|
* The type of the event, used for routing and handling.
|
|
50
158
|
*/
|
|
51
159
|
type: T;
|
|
52
160
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
161
|
+
* Identifier of the aggregate that produced the event. Optional at the
|
|
162
|
+
* library level — set it whenever the producing aggregate is known so
|
|
163
|
+
* downstream subscribers, outboxes, and projections can scope by entity.
|
|
164
|
+
*/
|
|
165
|
+
aggregateId?: string;
|
|
166
|
+
/**
|
|
167
|
+
* Name of the aggregate type that produced the event (e.g. "Order").
|
|
168
|
+
* Pairs with `aggregateId` to fully qualify the source aggregate.
|
|
169
|
+
*/
|
|
170
|
+
aggregateType?: string;
|
|
171
|
+
/**
|
|
172
|
+
* The event payload containing the domain data. The field is always
|
|
173
|
+
* present; its value is `undefined` when `P` is `void`.
|
|
55
174
|
*/
|
|
56
175
|
payload: P;
|
|
57
176
|
/**
|
|
@@ -70,6 +189,37 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
70
189
|
*/
|
|
71
190
|
metadata?: EventMetadata;
|
|
72
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Shared option bag for the `createDomainEvent*` factories.
|
|
194
|
+
*/
|
|
195
|
+
interface CreateDomainEventOptions {
|
|
196
|
+
/**
|
|
197
|
+
* Override for the auto-generated `eventId`. Pass an existing id (for
|
|
198
|
+
* replay, tests, or deterministic event sourcing) instead of letting the
|
|
199
|
+
* factory call `crypto.randomUUID()`.
|
|
200
|
+
*/
|
|
201
|
+
eventId?: string;
|
|
202
|
+
/**
|
|
203
|
+
* Identifier of the aggregate that produced the event.
|
|
204
|
+
*/
|
|
205
|
+
aggregateId?: string;
|
|
206
|
+
/**
|
|
207
|
+
* Name of the aggregate type that produced the event.
|
|
208
|
+
*/
|
|
209
|
+
aggregateType?: string;
|
|
210
|
+
/**
|
|
211
|
+
* Override for the auto-generated `occurredAt` timestamp.
|
|
212
|
+
*/
|
|
213
|
+
occurredAt?: Date;
|
|
214
|
+
/**
|
|
215
|
+
* Override for the default schema version (1).
|
|
216
|
+
*/
|
|
217
|
+
version?: number;
|
|
218
|
+
/**
|
|
219
|
+
* Event metadata — correlation, causation, user, source, custom fields.
|
|
220
|
+
*/
|
|
221
|
+
metadata?: EventMetadata;
|
|
222
|
+
}
|
|
73
223
|
/**
|
|
74
224
|
* Creates a domain event with default values.
|
|
75
225
|
* Sets occurredAt to current date and version to 1 if not provided.
|
|
@@ -84,16 +234,8 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
84
234
|
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
85
235
|
* ```
|
|
86
236
|
*/
|
|
87
|
-
declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?:
|
|
88
|
-
|
|
89
|
-
version?: number;
|
|
90
|
-
metadata?: EventMetadata;
|
|
91
|
-
}): DomainEvent<T, void>;
|
|
92
|
-
declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: {
|
|
93
|
-
occurredAt?: Date;
|
|
94
|
-
version?: number;
|
|
95
|
-
metadata?: EventMetadata;
|
|
96
|
-
}): DomainEvent<T, P>;
|
|
237
|
+
declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
|
|
238
|
+
declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
|
|
97
239
|
/**
|
|
98
240
|
* Creates a domain event with metadata for traceability.
|
|
99
241
|
* Convenience function for creating events with correlation and causation IDs.
|
|
@@ -107,10 +249,7 @@ declare function createDomainEvent<T extends string, P>(type: T, payload: P, opt
|
|
|
107
249
|
* );
|
|
108
250
|
* ```
|
|
109
251
|
*/
|
|
110
|
-
declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?:
|
|
111
|
-
occurredAt?: Date;
|
|
112
|
-
version?: number;
|
|
113
|
-
}): DomainEvent<T, P>;
|
|
252
|
+
declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
|
|
114
253
|
/**
|
|
115
254
|
* Copies metadata from a source event to a new event.
|
|
116
255
|
* Useful for maintaining correlation chains in event-driven architectures.
|
|
@@ -141,10 +280,72 @@ declare function copyMetadata(sourceEvent: DomainEvent<string, unknown>, additio
|
|
|
141
280
|
declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
|
|
142
281
|
|
|
143
282
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
283
|
+
* Entity utilities and interfaces for Domain-Driven Design.
|
|
284
|
+
*
|
|
285
|
+
* In Domain-Driven Design, there are two types of entities:
|
|
286
|
+
*
|
|
287
|
+
* 1. **Aggregate Root Entity**: The parent Entity of an aggregate.
|
|
288
|
+
* - Has identity (id), state, and version
|
|
289
|
+
* - Implemented by classes extending `AggregateRoot` or `EventSourcedAggregate`
|
|
290
|
+
* - Represents the aggregate externally
|
|
291
|
+
* - Loaded/saved through repositories
|
|
292
|
+
*
|
|
293
|
+
* 2. **Child Entities**: Entities within an aggregate.
|
|
294
|
+
* - Have identity (id) and state, but no own version
|
|
295
|
+
* - Can extend `EntityBase<TState, TId>` for class-based entities
|
|
296
|
+
* - Or use functional style with `Identifiable<TId> & TProps`
|
|
297
|
+
* - Exist only within the aggregate boundary
|
|
298
|
+
* - Versioned through the Aggregate Root
|
|
299
|
+
* - Cannot be referenced directly from outside the aggregate
|
|
300
|
+
*
|
|
301
|
+
* This module provides:
|
|
302
|
+
* - `EntityBase<TState, TId>` - Base class for entities with state
|
|
303
|
+
* - `Entity<TId>` - Simple class for entities without state management
|
|
304
|
+
* - `Identifiable<TId>` - Minimal interface for objects with id
|
|
305
|
+
* - Helper functions for working with collections of entities
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```typescript
|
|
309
|
+
* // Class-based child entity with logic
|
|
310
|
+
* class OrderItem extends EntityBase<OrderItemState, ItemId> {
|
|
311
|
+
* constructor(id: ItemId, initialState: OrderItemState) {
|
|
312
|
+
* super(id, initialState);
|
|
313
|
+
* }
|
|
314
|
+
*
|
|
315
|
+
* updateQuantity(quantity: number): void {
|
|
316
|
+
* this._state = { ...this._state, quantity };
|
|
317
|
+
* }
|
|
318
|
+
*
|
|
319
|
+
* calculateSubtotal(): number {
|
|
320
|
+
* return this._state.price * this._state.quantity;
|
|
321
|
+
* }
|
|
322
|
+
* }
|
|
323
|
+
*
|
|
324
|
+
* // Functional-style child entity (simpler, no logic)
|
|
325
|
+
* type OrderItem = Identifiable<ItemId> & {
|
|
326
|
+
* productId: string;
|
|
327
|
+
* quantity: number;
|
|
328
|
+
* price: number;
|
|
329
|
+
* };
|
|
330
|
+
*
|
|
331
|
+
* // Aggregate Root (Entity with version)
|
|
332
|
+
* class Order extends AggregateRoot<OrderState, OrderId> {
|
|
333
|
+
* // Order is an Aggregate Root Entity
|
|
334
|
+
* // OrderState contains OrderItem child entities
|
|
335
|
+
* }
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Functional definition of an Entity via its capability — an object is
|
|
341
|
+
* identifiable if it has an `id`.
|
|
342
|
+
*
|
|
343
|
+
* `TId` is constrained to `Id<string>` so the brand discipline that
|
|
344
|
+
* `Id<Tag>` enforces is preserved end-to-end: an `Identifiable<UserId>`
|
|
345
|
+
* cannot accidentally be paired with an `Identifiable<OrderId>` or with
|
|
346
|
+
* a plain `string`.
|
|
146
347
|
*/
|
|
147
|
-
type Identifiable<TId
|
|
348
|
+
type Identifiable<TId extends Id<string>> = {
|
|
148
349
|
readonly id: TId;
|
|
149
350
|
};
|
|
150
351
|
/**
|
|
@@ -204,7 +405,15 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
204
405
|
readonly id: TId;
|
|
205
406
|
/**
|
|
206
407
|
* Returns the current state of the entity.
|
|
207
|
-
*
|
|
408
|
+
*
|
|
409
|
+
* The state object is **shallowly frozen** — direct property writes
|
|
410
|
+
* (`entity.state.foo = …`) throw in strict mode, but writes to nested
|
|
411
|
+
* objects (`entity.state.address.zip = …`) bypass the freeze. For deep
|
|
412
|
+
* immutability either model nested data with `vo()` (which freezes
|
|
413
|
+
* deeply) or reach for a structural-sharing library like Immer at the
|
|
414
|
+
* App layer. The shallow contract is intentional: deep freezing on
|
|
415
|
+
* every state write is too expensive for hot paths, and DDD aggregates
|
|
416
|
+
* normally treat their own state as private (`Tell, Don't Ask`).
|
|
208
417
|
*/
|
|
209
418
|
get state(): TState;
|
|
210
419
|
/**
|
|
@@ -214,12 +423,25 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
214
423
|
protected _state: TState;
|
|
215
424
|
protected constructor(id: TId, initialState: TState);
|
|
216
425
|
/**
|
|
217
|
-
* Optional validation hook to ensure state invariants.
|
|
218
|
-
*
|
|
219
|
-
*
|
|
426
|
+
* Optional validation hook to ensure state invariants. Called during
|
|
427
|
+
* construction (from `Entity`'s constructor) and again on every
|
|
428
|
+
* `setState()` call. Throw to reject invalid state.
|
|
429
|
+
*
|
|
430
|
+
* **⚠️ Must not read subclass instance fields via `this`.** The
|
|
431
|
+
* constructor calls `validateState(initialState)` BEFORE the subclass's
|
|
432
|
+
* field initializers run, so `this.someField` is `undefined` at that
|
|
433
|
+
* point — a classic TypeScript/JavaScript constructor-ordering footgun.
|
|
434
|
+
* The `state` argument is the single source of truth; treat the method
|
|
435
|
+
* as pure with respect to `this`.
|
|
436
|
+
*
|
|
437
|
+
* If your invariants genuinely depend on per-instance configuration
|
|
438
|
+
* that isn't part of the state, pass that configuration into the state
|
|
439
|
+
* itself (DDD-canonical: the aggregate's state contains everything it
|
|
440
|
+
* needs) or perform the additional check after construction in a
|
|
441
|
+
* dedicated factory method.
|
|
220
442
|
*
|
|
221
443
|
* @param state - The state to validate
|
|
222
|
-
* @throws Error if validation fails
|
|
444
|
+
* @throws Error (or `DomainError` subclass) if validation fails
|
|
223
445
|
*/
|
|
224
446
|
protected validateState(_state: TState): void;
|
|
225
447
|
/**
|
|
@@ -231,6 +453,18 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
231
453
|
*/
|
|
232
454
|
protected setState(newState: TState): void;
|
|
233
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* Shallow-freezes `value` when it's a non-null object or array, so that
|
|
458
|
+
* direct property writes throw in strict mode. Returns the value as-is for
|
|
459
|
+
* primitives. Used internally by `Entity` and its subclasses to prevent
|
|
460
|
+
* outside mutation of state read through the `state` getter without paying
|
|
461
|
+
* the cost of a deep clone on every read.
|
|
462
|
+
*
|
|
463
|
+
* Exported so that sibling classes (`EventSourcedAggregate`, `AggregateRoot`)
|
|
464
|
+
* can apply the same freeze when they bypass `setState` and assign
|
|
465
|
+
* `this._state` directly.
|
|
466
|
+
*/
|
|
467
|
+
declare function freezeShallow<T>(value: T): T;
|
|
234
468
|
/**
|
|
235
469
|
* Checks if two entities have the same ID.
|
|
236
470
|
* Works with any object that has an 'id' property.
|
|
@@ -248,7 +482,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
248
482
|
* sameEntity(item1, item1); // true
|
|
249
483
|
* ```
|
|
250
484
|
*/
|
|
251
|
-
declare function sameEntity<TId
|
|
485
|
+
declare function sameEntity<TId extends Id<string>>(a: Identifiable<TId>, b: Identifiable<TId>): boolean;
|
|
252
486
|
/**
|
|
253
487
|
* Finds an entity by ID in a collection.
|
|
254
488
|
* Returns undefined if not found.
|
|
@@ -268,7 +502,7 @@ declare function sameEntity<TId>(a: Identifiable<TId>, b: Identifiable<TId>): bo
|
|
|
268
502
|
* // item is { id: itemId1, productId: "prod-1", quantity: 2 }
|
|
269
503
|
* ```
|
|
270
504
|
*/
|
|
271
|
-
declare function findEntityById<TId
|
|
505
|
+
declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T | undefined;
|
|
272
506
|
/**
|
|
273
507
|
* Checks if an entity with the given ID exists in the collection.
|
|
274
508
|
*
|
|
@@ -286,7 +520,7 @@ declare function findEntityById<TId, T extends Identifiable<TId>>(entities: T[],
|
|
|
286
520
|
* hasEntityId(items, itemId2); // false
|
|
287
521
|
* ```
|
|
288
522
|
*/
|
|
289
|
-
declare function hasEntityId<TId
|
|
523
|
+
declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): boolean;
|
|
290
524
|
/**
|
|
291
525
|
* Removes an entity with the given ID from the collection.
|
|
292
526
|
* Returns a new array without the entity.
|
|
@@ -306,7 +540,7 @@ declare function hasEntityId<TId, T extends Identifiable<TId>>(entities: T[], id
|
|
|
306
540
|
* // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
|
|
307
541
|
* ```
|
|
308
542
|
*/
|
|
309
|
-
declare function removeEntityById<TId
|
|
543
|
+
declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T[];
|
|
310
544
|
/**
|
|
311
545
|
* Updates an entity with the given ID in the collection.
|
|
312
546
|
* Returns a new array with the updated entity.
|
|
@@ -330,7 +564,7 @@ declare function removeEntityById<TId, T extends Identifiable<TId>>(entities: T[
|
|
|
330
564
|
* // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
|
|
331
565
|
* ```
|
|
332
566
|
*/
|
|
333
|
-
declare function updateEntityById<TId
|
|
567
|
+
declare function updateEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, updater: (entity: T) => T): T[];
|
|
334
568
|
/**
|
|
335
569
|
* Replaces an entity with the given ID in the collection.
|
|
336
570
|
* Returns a new array with the replaced entity.
|
|
@@ -354,7 +588,7 @@ declare function updateEntityById<TId, T extends Identifiable<TId>>(entities: T[
|
|
|
354
588
|
* });
|
|
355
589
|
* ```
|
|
356
590
|
*/
|
|
357
|
-
declare function replaceEntityById<TId
|
|
591
|
+
declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, replacement: T): T[];
|
|
358
592
|
/**
|
|
359
593
|
* Extracts all IDs from a collection of entities.
|
|
360
594
|
*
|
|
@@ -372,7 +606,7 @@ declare function replaceEntityById<TId, T extends Identifiable<TId>>(entities: T
|
|
|
372
606
|
* // ids is [itemId1, itemId2]
|
|
373
607
|
* ```
|
|
374
608
|
*/
|
|
375
|
-
declare function entityIds<TId
|
|
609
|
+
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[]): TId[];
|
|
376
610
|
|
|
377
611
|
/**
|
|
378
612
|
* Marker interface for Aggregate Roots.
|
|
@@ -412,14 +646,36 @@ interface IAggregateRoot<TId extends Id<string>> {
|
|
|
412
646
|
* This version applies to the entire aggregate, including all child entities.
|
|
413
647
|
*/
|
|
414
648
|
readonly version: Version;
|
|
649
|
+
/**
|
|
650
|
+
* Post-save hook: a `Repository.save()` implementation calls this with
|
|
651
|
+
* the persisted version after a successful write to push the new
|
|
652
|
+
* version back into the aggregate and clear any recorded domain events
|
|
653
|
+
* (they are now safely on the write side / in the outbox).
|
|
654
|
+
*
|
|
655
|
+
* Required by the interface so a Repository implementation can call it
|
|
656
|
+
* via the published `IAggregateRoot` contract without taking the
|
|
657
|
+
* abstract class as a compile-time dependency.
|
|
658
|
+
*
|
|
659
|
+
* @param version - The version assigned by the persistence layer
|
|
660
|
+
*/
|
|
661
|
+
markPersisted(version: Version): void;
|
|
415
662
|
}
|
|
416
663
|
/**
|
|
417
664
|
* Configuration options for AggregateRoot behavior.
|
|
418
665
|
*/
|
|
419
666
|
interface AggregateConfig {
|
|
420
667
|
/**
|
|
421
|
-
* Whether
|
|
422
|
-
*
|
|
668
|
+
* Whether `setState()` should bump the version automatically.
|
|
669
|
+
*
|
|
670
|
+
* Defaults to **`false`** for `AggregateRoot` — because `setState()`
|
|
671
|
+
* already takes an explicit `bumpVersion` argument per call, so adding
|
|
672
|
+
* an "always bump" config on top would be redundant. Keep it `false`
|
|
673
|
+
* unless you have a subclass that never passes `bumpVersion` and you
|
|
674
|
+
* want every state change to advance the version anyway.
|
|
675
|
+
*
|
|
676
|
+
* (Contrast with `EventSourcedAggregate`, which defaults this to
|
|
677
|
+
* `true` because every event-sourced state change is per definition a
|
|
678
|
+
* versioned commit.)
|
|
423
679
|
*/
|
|
424
680
|
autoVersionBump?: boolean;
|
|
425
681
|
}
|
|
@@ -461,7 +717,7 @@ interface AggregateConfig {
|
|
|
461
717
|
* }
|
|
462
718
|
* ```
|
|
463
719
|
*/
|
|
464
|
-
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent =
|
|
720
|
+
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId> {
|
|
465
721
|
private _version;
|
|
466
722
|
get version(): Version;
|
|
467
723
|
protected setVersion(version: Version): void;
|
|
@@ -478,12 +734,92 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
|
|
|
478
734
|
* Call this after dispatching the events.
|
|
479
735
|
*/
|
|
480
736
|
clearDomainEvents(): void;
|
|
737
|
+
/**
|
|
738
|
+
* Post-save hook called by a `Repository.save()` implementation to push
|
|
739
|
+
* the persisted version back into the in-memory aggregate and clear the
|
|
740
|
+
* recorded domain events (they are now safely on the write side / in
|
|
741
|
+
* the outbox).
|
|
742
|
+
*
|
|
743
|
+
* Use this so `save()` can keep its `Promise<void>` return type: the
|
|
744
|
+
* caller holds the aggregate reference, which is up to date after this
|
|
745
|
+
* call.
|
|
746
|
+
*/
|
|
747
|
+
markPersisted(version: Version): void;
|
|
748
|
+
/**
|
|
749
|
+
* Mutates state and records the resulting domain events in the
|
|
750
|
+
* **canonical record-after-mutation order**. Use this instead of calling
|
|
751
|
+
* `setState` + `addDomainEvent` separately and you cannot trip the
|
|
752
|
+
* "event for a fact that never happened" footgun.
|
|
753
|
+
*
|
|
754
|
+
* Order of operations:
|
|
755
|
+
* 1. `setState(newState, true)` — runs `validateState` first.
|
|
756
|
+
* If it throws, the method propagates and **no event is recorded
|
|
757
|
+
* and no version is bumped**.
|
|
758
|
+
* 2. Each event in `events` is appended via `addDomainEvent`.
|
|
759
|
+
*
|
|
760
|
+
* `commit()` **always bumps the version**, regardless of the aggregate's
|
|
761
|
+
* `autoVersionBump` config. Recording a domain event implies "something
|
|
762
|
+
* happened that the outside world cares about", and optimistic-
|
|
763
|
+
* concurrency callers must see a fresh version every time. The config
|
|
764
|
+
* still governs the un-coupled `setState` path. If you need to mutate
|
|
765
|
+
* state without bumping (e.g. cosmetic caches), call `setState(newState,
|
|
766
|
+
* false)` and skip `commit` entirely.
|
|
767
|
+
*
|
|
768
|
+
* `events` accepts a single event or an array. Omit it (or pass `[]`)
|
|
769
|
+
* for state-only mutations.
|
|
770
|
+
*
|
|
771
|
+
* @example
|
|
772
|
+
* ```ts
|
|
773
|
+
* confirm(): void {
|
|
774
|
+
* if (this.state.status === "confirmed") {
|
|
775
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
776
|
+
* }
|
|
777
|
+
* this.commit(
|
|
778
|
+
* { ...this.state, status: "confirmed" },
|
|
779
|
+
* { type: "OrderConfirmed", orderId: this.id },
|
|
780
|
+
* );
|
|
781
|
+
* }
|
|
782
|
+
* ```
|
|
783
|
+
*
|
|
784
|
+
* `EventSourcedAggregate.apply()` enforces the same ordering
|
|
785
|
+
* structurally; `commit()` is the opt-in equivalent on `AggregateRoot`,
|
|
786
|
+
* where `setState` and `addDomainEvent` are otherwise decoupled and the
|
|
787
|
+
* ordering is convention-only.
|
|
788
|
+
*
|
|
789
|
+
* @param newState - The new state (validated by `validateState`)
|
|
790
|
+
* @param events - One event, an array of events, or none (default)
|
|
791
|
+
*/
|
|
792
|
+
protected commit(newState: TState, events?: TEvent | readonly TEvent[]): void;
|
|
481
793
|
protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
|
|
482
794
|
/**
|
|
483
|
-
*
|
|
484
|
-
*
|
|
795
|
+
* Records a domain event for later publication.
|
|
796
|
+
*
|
|
797
|
+
* **Ordering: record AFTER state mutation.** Vernon (IDDD §8) is
|
|
798
|
+
* explicit: a domain event describes something that has just happened
|
|
799
|
+
* to the aggregate — its existence implies the state change already
|
|
800
|
+
* occurred. Concretely:
|
|
801
|
+
*
|
|
802
|
+
* ```ts
|
|
803
|
+
* confirm(): void {
|
|
804
|
+
* if (this.state.status === "confirmed") {
|
|
805
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
806
|
+
* }
|
|
807
|
+
* this.setState({ ...this.state, status: "confirmed" }, true);
|
|
808
|
+
* this.addDomainEvent({ type: "OrderConfirmed", orderId: this.id });
|
|
809
|
+
* // ↑ post-mutation. The event represents the committed fact.
|
|
810
|
+
* }
|
|
811
|
+
* ```
|
|
812
|
+
*
|
|
813
|
+
* Recording before mutation is a footgun: if a subsequent invariant
|
|
814
|
+
* check throws, the event has already been queued but the state never
|
|
815
|
+
* actually changed — consumers see an event for a fact that did not
|
|
816
|
+
* happen.
|
|
817
|
+
*
|
|
818
|
+
* `EventSourcedAggregate.apply()` enforces this ordering structurally;
|
|
819
|
+
* `AggregateRoot` leaves it as a convention because the state-mutation
|
|
820
|
+
* path (`setState`) is decoupled from event recording.
|
|
485
821
|
*
|
|
486
|
-
* @param event - The domain event to
|
|
822
|
+
* @param event - The domain event to record
|
|
487
823
|
*/
|
|
488
824
|
protected addDomainEvent(event: TEvent): void;
|
|
489
825
|
/**
|
|
@@ -534,6 +870,103 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
|
|
|
534
870
|
restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
|
|
535
871
|
}
|
|
536
872
|
|
|
873
|
+
/**
|
|
874
|
+
* Abstract base for **domain-invariant violations**. Domain methods
|
|
875
|
+
* (aggregates, entity validation hooks, value-object constructors)
|
|
876
|
+
* throw `DomainError`-derived exceptions when a business rule is
|
|
877
|
+
* violated. Consumers derive their own concrete errors — e.g.
|
|
878
|
+
* `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
|
|
879
|
+
* for `instanceof`-style catching at the App-Service boundary, where
|
|
880
|
+
* they typically map to HTTP 400 / business-rule responses.
|
|
881
|
+
*
|
|
882
|
+
* The library itself does **not** ship any concrete `DomainError`
|
|
883
|
+
* subclass — the kit can't know your invariants. {@link MissingHandlerError},
|
|
884
|
+
* {@link AggregateNotFoundError}, and {@link ConcurrencyConflictError}
|
|
885
|
+
* deliberately sit on other branches of the hierarchy (see below) because
|
|
886
|
+
* they are not invariant violations.
|
|
887
|
+
*
|
|
888
|
+
* Extends `BaseError<Name>` from `@shirudo/base-error`, so derived
|
|
889
|
+
* classes get timestamps, `error.cause` traversal, `toJSON()`, i18n-
|
|
890
|
+
* aware `getUserMessage()`, and the `isRetryable` predicate for free.
|
|
891
|
+
*/
|
|
892
|
+
declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Abstract base for **infrastructure / persistence failures** that the
|
|
896
|
+
* App-Service can recover from — typically by retrying, by returning
|
|
897
|
+
* HTTP 404 / 409, or by surfacing a "please try again" UX. These are
|
|
898
|
+
* not domain-invariant violations (the business rules were not
|
|
899
|
+
* broken); they describe race conditions and missing rows at the
|
|
900
|
+
* storage boundary.
|
|
901
|
+
*
|
|
902
|
+
* Library-internal concrete subclasses:
|
|
903
|
+
* - {@link AggregateNotFoundError}
|
|
904
|
+
* - {@link ConcurrencyConflictError}
|
|
905
|
+
*
|
|
906
|
+
* Extends `BaseError<Name>` from `@shirudo/base-error`.
|
|
907
|
+
*/
|
|
908
|
+
declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Thrown by `EventSourcedAggregate.apply()` when no handler is
|
|
912
|
+
* registered for the event's type. This means the aggregate's subclass
|
|
913
|
+
* forgot to add an entry to its `handlers` map — a programming /
|
|
914
|
+
* configuration bug, not a domain or infrastructure failure.
|
|
915
|
+
*
|
|
916
|
+
* Deliberately **not** on `DomainError` or `InfrastructureError` —
|
|
917
|
+
* a generic `catch (e instanceof DomainError)` handler at the App
|
|
918
|
+
* layer must not mask a forgotten handler; this should crash loud and
|
|
919
|
+
* fail the calling Use Case so the bug surfaces in development. The
|
|
920
|
+
* replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
|
|
921
|
+
* also let it propagate uncaught instead of wrapping it in `Result.Err`.
|
|
922
|
+
*
|
|
923
|
+
* Use `isBaseError(e)` from `@shirudo/base-error` to detect
|
|
924
|
+
* "any structured error from the kit or any other BaseError-using
|
|
925
|
+
* library" at the App boundary.
|
|
926
|
+
*/
|
|
927
|
+
declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
|
|
928
|
+
readonly eventType: string;
|
|
929
|
+
constructor(eventType: string);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
|
|
933
|
+
* given id does not exist. `InfrastructureError` because the storage
|
|
934
|
+
* boundary, not a business rule, decided the row is absent. Use the
|
|
935
|
+
* nullable variant `getById()` if "not found" is a valid outcome.
|
|
936
|
+
*
|
|
937
|
+
* Ships with a user-safe message via `withUserMessage`. Not retryable —
|
|
938
|
+
* retrying won't make the row appear.
|
|
939
|
+
*/
|
|
940
|
+
declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
|
|
941
|
+
readonly aggregateType: string;
|
|
942
|
+
readonly id: string;
|
|
943
|
+
constructor(aggregateType: string, id: string);
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Thrown by `IRepository.save()` when the aggregate's expected version
|
|
947
|
+
* does not match the version currently persisted — i.e. another writer
|
|
948
|
+
* updated the aggregate concurrently. The canonical optimistic-
|
|
949
|
+
* concurrency signal; the App-Service typically reloads, re-applies
|
|
950
|
+
* the use case, and retries, or surfaces HTTP 409 to the caller.
|
|
951
|
+
*
|
|
952
|
+
* `InfrastructureError` because the persistence layer (not a domain
|
|
953
|
+
* rule) detects the race. Marks itself as `retryable: true` so the
|
|
954
|
+
* `isRetryable` predicate from `@shirudo/base-error` picks it up.
|
|
955
|
+
*/
|
|
956
|
+
declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
|
|
957
|
+
readonly aggregateType: string;
|
|
958
|
+
readonly aggregateId: string;
|
|
959
|
+
readonly expectedVersion: number;
|
|
960
|
+
readonly actualVersion: number;
|
|
961
|
+
/**
|
|
962
|
+
* Marks this error as retryable so `isRetryable(err)` returns
|
|
963
|
+
* true. The canonical OCC pattern is to reload the aggregate, re-apply
|
|
964
|
+
* the use case, and retry on this exception.
|
|
965
|
+
*/
|
|
966
|
+
readonly retryable: true;
|
|
967
|
+
constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number);
|
|
968
|
+
}
|
|
969
|
+
|
|
537
970
|
/**
|
|
538
971
|
* Interface for Event-Sourced Aggregate Roots.
|
|
539
972
|
* Defines the contract for aggregates that manage state changes via event sourcing.
|
|
@@ -547,11 +980,13 @@ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEv
|
|
|
547
980
|
*/
|
|
548
981
|
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
549
982
|
/**
|
|
550
|
-
* Reconstitutes the aggregate from an event history.
|
|
983
|
+
* Reconstitutes the aggregate from an event history. Returns `Result`
|
|
984
|
+
* because event-stream corruption is an expected recoverable failure
|
|
985
|
+
* at the infrastructure boundary.
|
|
551
986
|
*
|
|
552
987
|
* @param history - An ordered list of past events
|
|
553
988
|
*/
|
|
554
|
-
loadFromHistory(history: TEvent[]): Result<void,
|
|
989
|
+
loadFromHistory(history: TEvent[]): Result<void, DomainError>;
|
|
555
990
|
/**
|
|
556
991
|
* Clears the list of pending events.
|
|
557
992
|
*/
|
|
@@ -575,8 +1010,18 @@ type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
|
|
|
575
1010
|
*/
|
|
576
1011
|
interface EventSourcedAggregateConfig {
|
|
577
1012
|
/**
|
|
578
|
-
* Whether
|
|
579
|
-
*
|
|
1013
|
+
* Whether `apply()` should bump the version per event.
|
|
1014
|
+
*
|
|
1015
|
+
* Defaults to **`true`** for `EventSourcedAggregate` — each applied
|
|
1016
|
+
* event is by definition a versioned state change, so the canonical
|
|
1017
|
+
* event-sourcing pattern is "one event = one version bump". Set to
|
|
1018
|
+
* `false` only if your event store assigns version numbers itself
|
|
1019
|
+
* and you want the aggregate to track them via `bumpVersion()` /
|
|
1020
|
+
* `setVersion()` calls instead.
|
|
1021
|
+
*
|
|
1022
|
+
* (Contrast with `AggregateRoot`, which defaults this to `false`
|
|
1023
|
+
* because its `setState()` already takes a per-call `bumpVersion`
|
|
1024
|
+
* argument.)
|
|
580
1025
|
*/
|
|
581
1026
|
autoVersionBump?: boolean;
|
|
582
1027
|
}
|
|
@@ -591,17 +1036,35 @@ interface EventSourcedAggregateConfig {
|
|
|
591
1036
|
* `addDomainEvent()` are not available. This enforces the event sourcing pattern
|
|
592
1037
|
* at the type level — there is no way to mutate state without going through an event handler.
|
|
593
1038
|
*
|
|
1039
|
+
* `apply()` and `validateEvent()` throw `DomainError`-derived exceptions on
|
|
1040
|
+
* invariant violations. Subclasses override `validateEvent()` to throw their
|
|
1041
|
+
* own concrete subclasses (e.g. `OrderAlreadyConfirmedError`). Only the
|
|
1042
|
+
* infrastructure-boundary methods (`loadFromHistory`,
|
|
1043
|
+
* `restoreFromSnapshotWithEvents`) return `Result` — they catch `DomainError`
|
|
1044
|
+
* during replay so callers can react to corrupted event streams without
|
|
1045
|
+
* try/catch.
|
|
1046
|
+
*
|
|
594
1047
|
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
595
1048
|
* @template TEvent - The union type of all domain events
|
|
596
1049
|
* @template TId - The type of the aggregate root identifier
|
|
597
1050
|
*
|
|
598
1051
|
* @example
|
|
599
1052
|
* ```typescript
|
|
1053
|
+
* class OrderAlreadyConfirmedError extends DomainError {
|
|
1054
|
+
* constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
|
|
1055
|
+
* }
|
|
1056
|
+
*
|
|
600
1057
|
* class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
601
1058
|
* confirm(): void {
|
|
602
1059
|
* this.apply(createDomainEvent("OrderConfirmed", {}));
|
|
603
1060
|
* }
|
|
604
1061
|
*
|
|
1062
|
+
* protected validateEvent(event: OrderEvent): void {
|
|
1063
|
+
* if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
|
|
1064
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
1065
|
+
* }
|
|
1066
|
+
* }
|
|
1067
|
+
*
|
|
605
1068
|
* protected readonly handlers = {
|
|
606
1069
|
* OrderConfirmed: (state: OrderState): OrderState => ({
|
|
607
1070
|
* ...state,
|
|
@@ -619,36 +1082,66 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
619
1082
|
private readonly _autoVersionBump;
|
|
620
1083
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
621
1084
|
clearPendingEvents(): void;
|
|
1085
|
+
/**
|
|
1086
|
+
* Post-save hook called by a `Repository.save()` implementation to push
|
|
1087
|
+
* the persisted version back into the in-memory aggregate and clear the
|
|
1088
|
+
* pending events (they are now in the event store / outbox). Lets
|
|
1089
|
+
* `save()` keep its `Promise<void>` return type.
|
|
1090
|
+
*/
|
|
1091
|
+
markPersisted(version: Version): void;
|
|
622
1092
|
protected constructor(id: TId, initialState: TState, config?: EventSourcedAggregateConfig);
|
|
623
1093
|
/**
|
|
624
|
-
* Validates an event before it is applied.
|
|
625
|
-
*
|
|
626
|
-
*
|
|
1094
|
+
* Validates an event before it is applied. Default is no-op.
|
|
1095
|
+
* Subclasses override to throw a concrete `DomainError` subclass when
|
|
1096
|
+
* the event violates an invariant in the current state.
|
|
627
1097
|
*/
|
|
628
|
-
protected validateEvent(_event: TEvent):
|
|
1098
|
+
protected validateEvent(_event: TEvent): void;
|
|
629
1099
|
/**
|
|
630
|
-
* Applies an event
|
|
631
|
-
*
|
|
1100
|
+
* Applies an event: validates, locates the handler, computes the next
|
|
1101
|
+
* state, then commits state + pending event + version bump atomically.
|
|
1102
|
+
*
|
|
1103
|
+
* Throws `DomainError` (or a subclass) on validation failure.
|
|
1104
|
+
* Throws `MissingHandlerError` if no handler is registered for `event.type`.
|
|
1105
|
+
*
|
|
1106
|
+
* State is not mutated if any step throws — the handler is invoked into
|
|
1107
|
+
* a local and only assigned to `_state` once all checks pass.
|
|
1108
|
+
*
|
|
1109
|
+
* The method is generic in the event tag `K`, so concrete callers
|
|
1110
|
+
* (`this.apply(orderCreated)`) narrow to the literal tag and the
|
|
1111
|
+
* dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }>>`
|
|
1112
|
+
* — no `as` cast required at the call site.
|
|
632
1113
|
*
|
|
633
1114
|
* @param event - The domain event to apply
|
|
634
|
-
* @param isNew - Whether the event is new (needs persisting) or from history
|
|
1115
|
+
* @param isNew - Whether the event is new (needs persisting) or replayed from history
|
|
635
1116
|
*/
|
|
636
|
-
protected apply(event: TEvent,
|
|
1117
|
+
protected apply<K extends TEvent["type"]>(event: Extract<TEvent, {
|
|
1118
|
+
type: K;
|
|
1119
|
+
}>, isNew?: boolean): void;
|
|
637
1120
|
/**
|
|
638
|
-
*
|
|
639
|
-
*
|
|
1121
|
+
* Internal dispatch path used by `apply()` and the replay methods
|
|
1122
|
+
* (`loadFromHistory`, `restoreFromSnapshotWithEvents`). The replay loop
|
|
1123
|
+
* iterates over `TEvent[]` and therefore cannot supply a narrowed `K`
|
|
1124
|
+
* generic, so this helper accepts `TEvent` and the discriminator is
|
|
1125
|
+
* resolved via the (statically-sound) `handlers` map.
|
|
640
1126
|
*/
|
|
641
|
-
|
|
1127
|
+
private dispatchAndCommit;
|
|
642
1128
|
/**
|
|
643
1129
|
* Manually bumps the aggregate version.
|
|
644
1130
|
* Only needed if `autoVersionBump` is disabled.
|
|
645
1131
|
*/
|
|
646
1132
|
protected bumpVersion(): void;
|
|
647
1133
|
/**
|
|
648
|
-
* Reconstitutes the aggregate from an event history.
|
|
649
|
-
*
|
|
1134
|
+
* Reconstitutes the aggregate from an event history. Catches `DomainError`
|
|
1135
|
+
* thrown during replay and returns it as an `Err` — this is the
|
|
1136
|
+
* infrastructure boundary, where event-stream corruption is an expected
|
|
1137
|
+
* recoverable failure. Unexpected (non-DomainError) throws propagate.
|
|
1138
|
+
*
|
|
1139
|
+
* Version advances additively: the aggregate's pre-existing version plus
|
|
1140
|
+
* `history.length`. A fresh aggregate (v=0) loading 3 events ends at v=3;
|
|
1141
|
+
* an aggregate already at v=1 (e.g. after a creation event) loading
|
|
1142
|
+
* 2 events ends at v=3, not v=2.
|
|
650
1143
|
*/
|
|
651
|
-
loadFromHistory(history: TEvent[]): Result<void,
|
|
1144
|
+
loadFromHistory(history: TEvent[]): Result<void, DomainError>;
|
|
652
1145
|
hasPendingEvents(): boolean;
|
|
653
1146
|
getEventCount(): number;
|
|
654
1147
|
getLatestEvent(): TEvent | undefined;
|
|
@@ -657,9 +1150,16 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
657
1150
|
*/
|
|
658
1151
|
createSnapshot(): AggregateSnapshot<TState>;
|
|
659
1152
|
/**
|
|
660
|
-
* Restores the aggregate from a snapshot and applies events that occurred
|
|
1153
|
+
* Restores the aggregate from a snapshot and applies events that occurred
|
|
1154
|
+
* after. Same infrastructure-boundary semantics as `loadFromHistory`:
|
|
1155
|
+
* catches `DomainError` and returns it as an `Err`; non-domain throws
|
|
1156
|
+
* propagate.
|
|
1157
|
+
*
|
|
1158
|
+
* All-or-nothing: if any event mid-stream throws a `DomainError`, the
|
|
1159
|
+
* aggregate is rolled back to its pre-call state + version. Partial
|
|
1160
|
+
* restoration is never observable to the caller.
|
|
661
1161
|
*/
|
|
662
|
-
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void,
|
|
1162
|
+
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, DomainError>;
|
|
663
1163
|
/**
|
|
664
1164
|
* A map of event types to their corresponding handlers.
|
|
665
1165
|
* Subclasses MUST implement this property.
|
|
@@ -674,33 +1174,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
674
1174
|
type Version = number & {
|
|
675
1175
|
readonly __v: true;
|
|
676
1176
|
};
|
|
677
|
-
/**
|
|
678
|
-
* Lightweight functional aggregate state — state + version, no event sourcing.
|
|
679
|
-
* This is a data projection, not a full Aggregate (which requires identity via IAggregateRoot).
|
|
680
|
-
*
|
|
681
|
-
* For event sourcing, use the class-based `EventSourcedAggregate` which enforces
|
|
682
|
-
* that state changes happen through event handlers.
|
|
683
|
-
*
|
|
684
|
-
* @template State - The type of the aggregate state
|
|
685
|
-
*/
|
|
686
|
-
interface AggregateState<State> {
|
|
687
|
-
state: Readonly<State>;
|
|
688
|
-
version: Version;
|
|
689
|
-
}
|
|
690
|
-
/**
|
|
691
|
-
* Creates a lightweight functional aggregate state.
|
|
692
|
-
*
|
|
693
|
-
* @example
|
|
694
|
-
* ```typescript
|
|
695
|
-
* const order = aggregate<OrderState>({ status: "draft", items: [] });
|
|
696
|
-
* ```
|
|
697
|
-
*/
|
|
698
|
-
declare function aggregate<State>(state: State, version?: Version): AggregateState<State>;
|
|
699
|
-
/**
|
|
700
|
-
* Bumps the version of a functional aggregate state.
|
|
701
|
-
* Returns a new aggregate state with incremented version.
|
|
702
|
-
*/
|
|
703
|
-
declare function bump<S>(agg: AggregateState<S>): AggregateState<S>;
|
|
704
1177
|
/**
|
|
705
1178
|
* Snapshot of an aggregate state at a specific point in time.
|
|
706
1179
|
* Used for optimizing event replay by starting from a snapshot
|
|
@@ -892,10 +1365,20 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
|
|
|
892
1365
|
/**
|
|
893
1366
|
* Registers a handler for a specific command type.
|
|
894
1367
|
*
|
|
1368
|
+
* When `TMap` is supplied, the `commandType` argument is restricted to
|
|
1369
|
+
* its keys and the handler signature is forced to match `TMap[K]` for the
|
|
1370
|
+
* return value — typos and wrong-typed handlers are compile errors.
|
|
1371
|
+
* Without `TMap` the registration is loose (any string key, any return
|
|
1372
|
+
* type) so the no-config path keeps working.
|
|
1373
|
+
*
|
|
895
1374
|
* @param commandType - The command type to register the handler for
|
|
896
1375
|
* @param handler - The handler function for this command type
|
|
897
1376
|
*/
|
|
898
|
-
register<
|
|
1377
|
+
register<K extends keyof TMap & string, C extends Command & {
|
|
1378
|
+
type: K;
|
|
1379
|
+
} = Command & {
|
|
1380
|
+
type: K;
|
|
1381
|
+
}>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
|
|
899
1382
|
}
|
|
900
1383
|
/**
|
|
901
1384
|
* Simple in-memory command bus implementation.
|
|
@@ -935,7 +1418,11 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
|
|
|
935
1418
|
*/
|
|
936
1419
|
declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap> implements ICommandBus<TMap> {
|
|
937
1420
|
private readonly handlers;
|
|
938
|
-
register<
|
|
1421
|
+
register<K extends keyof TMap & string, C extends Command & {
|
|
1422
|
+
type: K;
|
|
1423
|
+
} = Command & {
|
|
1424
|
+
type: K;
|
|
1425
|
+
}>(commandType: K, handler: CommandHandler<C, TMap[K]>): void;
|
|
939
1426
|
execute<C extends Command & {
|
|
940
1427
|
type: keyof TMap & string;
|
|
941
1428
|
}>(command: C): Promise<Result<TMap[C["type"]], string>>;
|
|
@@ -976,7 +1463,28 @@ interface EventBus<Evt extends {
|
|
|
976
1463
|
}> {
|
|
977
1464
|
/**
|
|
978
1465
|
* Publishes events to all subscribed handlers.
|
|
979
|
-
*
|
|
1466
|
+
*
|
|
1467
|
+
* **Ordering & parallelism contract:**
|
|
1468
|
+
*
|
|
1469
|
+
* 1. **Events run in input order.** `publish([a, b, c])` dispatches `a`,
|
|
1470
|
+
* awaits all of its handlers, then dispatches `b`, and so on. The
|
|
1471
|
+
* library never reorders or parallelises across events.
|
|
1472
|
+
* 2. **Handlers within a single event run in parallel.** All handlers
|
|
1473
|
+
* subscribed to `event.type` are awaited via `Promise.allSettled` —
|
|
1474
|
+
* none of them sees the others' errors and none is skipped if a
|
|
1475
|
+
* peer fails.
|
|
1476
|
+
* 3. **Errors are collected and thrown AFTER everything dispatches.**
|
|
1477
|
+
* If one handler throws, remaining handlers for that event still
|
|
1478
|
+
* run, and remaining events in the batch still publish. Once
|
|
1479
|
+
* `publish` reaches the end of the batch it throws — the single
|
|
1480
|
+
* error directly if there was one, or an `AggregateError`
|
|
1481
|
+
* ("Multiple event handlers failed") containing every captured
|
|
1482
|
+
* error otherwise. Callers that need fail-fast semantics should
|
|
1483
|
+
* publish events one at a time and not rely on batch atomicity.
|
|
1484
|
+
*
|
|
1485
|
+
* The contract is intentionally simple and in-process. For
|
|
1486
|
+
* cross-process delivery (RabbitMQ, Kafka, etc.), use the `Outbox`
|
|
1487
|
+
* port and a dedicated dispatcher.
|
|
980
1488
|
*
|
|
981
1489
|
* @param events - Array of events to publish
|
|
982
1490
|
*/
|
|
@@ -999,7 +1507,9 @@ interface EventBus<Evt extends {
|
|
|
999
1507
|
* unsubscribe();
|
|
1000
1508
|
* ```
|
|
1001
1509
|
*/
|
|
1002
|
-
subscribe: <
|
|
1510
|
+
subscribe: <K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1511
|
+
type: K;
|
|
1512
|
+
}>>) => () => void;
|
|
1003
1513
|
/**
|
|
1004
1514
|
* Subscribes to the next occurrence of an event type.
|
|
1005
1515
|
* Returns a Promise that resolves with the event data.
|
|
@@ -1014,24 +1524,125 @@ interface EventBus<Evt extends {
|
|
|
1014
1524
|
* console.log("Order created:", event.payload.orderId);
|
|
1015
1525
|
* ```
|
|
1016
1526
|
*/
|
|
1017
|
-
once: <
|
|
1527
|
+
once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
|
|
1528
|
+
type: K;
|
|
1529
|
+
}>>;
|
|
1018
1530
|
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Options for `EventBus.once()`. Both fields are optional; without them
|
|
1533
|
+
* `once()` waits forever (the historical behaviour).
|
|
1534
|
+
*/
|
|
1535
|
+
interface OnceOptions {
|
|
1536
|
+
/**
|
|
1537
|
+
* Aborts the wait. When `signal` fires, `once()` rejects with
|
|
1538
|
+
* `signal.reason` (or a generic abort error if none was supplied) and
|
|
1539
|
+
* the internal subscription is removed.
|
|
1540
|
+
*/
|
|
1541
|
+
signal?: AbortSignal;
|
|
1542
|
+
/**
|
|
1543
|
+
* Rejects with a timeout error after this many milliseconds if no event
|
|
1544
|
+
* has arrived. The internal subscription and timer are cleaned up
|
|
1545
|
+
* regardless of which path settles the promise.
|
|
1546
|
+
*/
|
|
1547
|
+
timeoutMs?: number;
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* One pending event in the outbox plus the opaque id the implementation
|
|
1551
|
+
* needs to ack it via `markDispatched`. The library does not prescribe
|
|
1552
|
+
* what `dispatchId` looks like — an implementation can reuse the event's
|
|
1553
|
+
* own `eventId`, generate its own UUID, use the row's auto-increment
|
|
1554
|
+
* primary key, or whatever the storage layer prefers.
|
|
1555
|
+
*/
|
|
1556
|
+
interface OutboxRecord<Evt> {
|
|
1557
|
+
dispatchId: string;
|
|
1558
|
+
event: Evt;
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Transactional outbox port — the bridge between the write-side
|
|
1562
|
+
* transaction and the (out-of-band) event dispatcher.
|
|
1563
|
+
*
|
|
1564
|
+
* Lifecycle:
|
|
1565
|
+
* 1. `add()` inside the write transaction (`withCommit` calls this) so
|
|
1566
|
+
* events persist atomically with the aggregate state.
|
|
1567
|
+
* 2. A separate outbox dispatcher polls `getPending()` and forwards the
|
|
1568
|
+
* events to subscribers / external brokers.
|
|
1569
|
+
* 3. After successful dispatch, the dispatcher calls `markDispatched()`
|
|
1570
|
+
* with the records' `dispatchId`s so they don't come back next poll.
|
|
1571
|
+
*
|
|
1572
|
+
* `markDispatched` is required to be idempotent — calling it with an id
|
|
1573
|
+
* that's already marked is a no-op, not an error. This lets the
|
|
1574
|
+
* dispatcher safely retry on partial-failure.
|
|
1575
|
+
*/
|
|
1019
1576
|
interface Outbox<Evt> {
|
|
1577
|
+
/**
|
|
1578
|
+
* Persists events. Called from inside `withCommit`'s transactional
|
|
1579
|
+
* callback, atomically with the aggregate write.
|
|
1580
|
+
*
|
|
1581
|
+
* **Idempotency:** implementations should dedupe on the event's
|
|
1582
|
+
* `eventId`. `withCommit` itself does not retry, but the surrounding
|
|
1583
|
+
* use case (a queue consumer, an HTTP retry, a transactional
|
|
1584
|
+
* outbox-dispatcher loop) may legitimately invoke the same write more
|
|
1585
|
+
* than once. A unique-key constraint on `(eventId)` in the outbox
|
|
1586
|
+
* table is the standard implementation.
|
|
1587
|
+
*/
|
|
1020
1588
|
add: (events: ReadonlyArray<Evt>) => Promise<void>;
|
|
1589
|
+
/**
|
|
1590
|
+
* Returns up to `limit` outbox records that have not yet been
|
|
1591
|
+
* dispatched. The dispatcher polls this on a schedule. When `limit`
|
|
1592
|
+
* is omitted, the implementation decides on a default page size.
|
|
1593
|
+
*/
|
|
1594
|
+
getPending: (limit?: number) => Promise<ReadonlyArray<OutboxRecord<Evt>>>;
|
|
1595
|
+
/**
|
|
1596
|
+
* Marks the given dispatch records as delivered so subsequent
|
|
1597
|
+
* `getPending` calls don't return them. Must be idempotent on
|
|
1598
|
+
* already-marked ids.
|
|
1599
|
+
*/
|
|
1600
|
+
markDispatched: (dispatchIds: ReadonlyArray<string>) => Promise<void>;
|
|
1021
1601
|
}
|
|
1022
1602
|
|
|
1023
|
-
|
|
1603
|
+
/**
|
|
1604
|
+
* Transaction-scope abstraction.
|
|
1605
|
+
*
|
|
1606
|
+
* Wraps a block of work so it runs inside the persistence layer's native
|
|
1607
|
+
* transaction (Postgres `BEGIN`/`COMMIT`, Mongo session, etc.). The block
|
|
1608
|
+
* commits when the callback resolves and rolls back if it throws.
|
|
1609
|
+
*
|
|
1610
|
+
* This is **not** Fowler's full Unit of Work (no change tracking, no
|
|
1611
|
+
* registerDirty/registerNew/registerDeleted, no commit-time flush). It is
|
|
1612
|
+
* intentionally minimal — change tracking is the ORM's job; the library
|
|
1613
|
+
* stays out of it. The name `TransactionScope` is therefore more honest
|
|
1614
|
+
* than `UnitOfWork`.
|
|
1615
|
+
*
|
|
1616
|
+
* @example
|
|
1617
|
+
* ```typescript
|
|
1618
|
+
* await scope.transactional(async () => {
|
|
1619
|
+
* const order = await repo.getByIdOrFail(orderId);
|
|
1620
|
+
* order.confirm();
|
|
1621
|
+
* await repo.save(order);
|
|
1622
|
+
* });
|
|
1623
|
+
* ```
|
|
1624
|
+
*/
|
|
1625
|
+
interface TransactionScope {
|
|
1024
1626
|
transactional<T>(fn: () => Promise<T>): Promise<T>;
|
|
1025
1627
|
}
|
|
1026
|
-
type RepoProvider<R> = (uow: UnitOfWork) => R;
|
|
1027
1628
|
|
|
1028
1629
|
/**
|
|
1029
|
-
* Helper
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1630
|
+
* Helper for executing a write Use Case inside a Unit of Work.
|
|
1631
|
+
*
|
|
1632
|
+
* Order of operations:
|
|
1633
|
+
* 1. `fn()` runs inside `uow.transactional(...)` — domain mutations + repo
|
|
1634
|
+
* writes happen here.
|
|
1635
|
+
* 2. `outbox.add(events)` is also inside the transaction, so events
|
|
1636
|
+
* persist atomically with the state change (outbox pattern).
|
|
1637
|
+
* 3. The transaction commits.
|
|
1638
|
+
* 4. **After** the commit, `bus.publish(events)` fires for the in-process
|
|
1639
|
+
* fast path.
|
|
1640
|
+
*
|
|
1641
|
+
* Publishing AFTER commit prevents the classic "publish before commit"
|
|
1642
|
+
* footgun: in-process subscribers can never react to events from a
|
|
1643
|
+
* transaction that later rolled back. If `bus.publish` itself fails, the
|
|
1644
|
+
* outbox still holds the events and an outbox-dispatcher will deliver them
|
|
1645
|
+
* (eventual consistency).
|
|
1035
1646
|
*
|
|
1036
1647
|
* @example
|
|
1037
1648
|
* ```typescript
|
|
@@ -1040,10 +1651,7 @@ type RepoProvider<R> = (uow: UnitOfWork) => R;
|
|
|
1040
1651
|
* async () => {
|
|
1041
1652
|
* const order = Order.create(customerId, items);
|
|
1042
1653
|
* await repository.save(order);
|
|
1043
|
-
* return {
|
|
1044
|
-
* result: order.id,
|
|
1045
|
-
* events: order.pendingEvents
|
|
1046
|
-
* };
|
|
1654
|
+
* return { result: order.id, events: order.domainEvents };
|
|
1047
1655
|
* }
|
|
1048
1656
|
* );
|
|
1049
1657
|
* ```
|
|
@@ -1053,7 +1661,7 @@ declare function withCommit<Evt extends {
|
|
|
1053
1661
|
}, R>(deps: {
|
|
1054
1662
|
outbox: Outbox<Evt>;
|
|
1055
1663
|
bus?: EventBus<Evt>;
|
|
1056
|
-
|
|
1664
|
+
scope: TransactionScope;
|
|
1057
1665
|
}, fn: () => Promise<{
|
|
1058
1666
|
result: R;
|
|
1059
1667
|
events: ReadonlyArray<Evt>;
|
|
@@ -1201,10 +1809,20 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
|
|
|
1201
1809
|
/**
|
|
1202
1810
|
* Registers a handler for a specific query type.
|
|
1203
1811
|
*
|
|
1812
|
+
* When `TMap` is supplied, the `queryType` argument is restricted to its
|
|
1813
|
+
* keys and the handler signature is forced to match `TMap[K]` for the
|
|
1814
|
+
* return value — typos and wrong-typed handlers are compile errors.
|
|
1815
|
+
* Without `TMap` the registration is loose (any string key, any return
|
|
1816
|
+
* type) so the no-config path keeps working.
|
|
1817
|
+
*
|
|
1204
1818
|
* @param queryType - The query type to register the handler for
|
|
1205
1819
|
* @param handler - The handler function for this query type
|
|
1206
1820
|
*/
|
|
1207
|
-
register<
|
|
1821
|
+
register<K extends keyof TMap & string, Q extends Query & {
|
|
1822
|
+
type: K;
|
|
1823
|
+
} = Query & {
|
|
1824
|
+
type: K;
|
|
1825
|
+
}>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
|
|
1208
1826
|
}
|
|
1209
1827
|
/**
|
|
1210
1828
|
* Simple in-memory query bus implementation.
|
|
@@ -1244,7 +1862,11 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
|
|
|
1244
1862
|
*/
|
|
1245
1863
|
declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus<TMap> {
|
|
1246
1864
|
private readonly handlers;
|
|
1247
|
-
register<
|
|
1865
|
+
register<K extends keyof TMap & string, Q extends Query & {
|
|
1866
|
+
type: K;
|
|
1867
|
+
} = Query & {
|
|
1868
|
+
type: K;
|
|
1869
|
+
}>(queryType: K, handler: QueryHandler<Q, TMap[K]>): void;
|
|
1248
1870
|
execute<Q extends Query & {
|
|
1249
1871
|
type: keyof TMap & string;
|
|
1250
1872
|
}>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
|
|
@@ -1255,24 +1877,6 @@ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQue
|
|
|
1255
1877
|
executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
|
|
1256
1878
|
}
|
|
1257
1879
|
|
|
1258
|
-
/**
|
|
1259
|
-
* Guard function that validates a condition and returns a Result.
|
|
1260
|
-
* Returns `ok(true)` if the condition is met, otherwise `err(error)`.
|
|
1261
|
-
*
|
|
1262
|
-
* @param cond - The condition to check
|
|
1263
|
-
* @param error - Error message if condition fails
|
|
1264
|
-
* @returns Result<true, string>
|
|
1265
|
-
*
|
|
1266
|
-
* @example
|
|
1267
|
-
* ```typescript
|
|
1268
|
-
* const result = guard(id.length > 0, "ID cannot be empty");
|
|
1269
|
-
* if (!result.ok) {
|
|
1270
|
-
* return err(result.error);
|
|
1271
|
-
* }
|
|
1272
|
-
* ```
|
|
1273
|
-
*/
|
|
1274
|
-
declare function guard(cond: boolean, error: string): Result<true, string>;
|
|
1275
|
-
|
|
1276
1880
|
/**
|
|
1277
1881
|
* Simple in-memory event bus implementation.
|
|
1278
1882
|
* Supports multiple subscribers per event type (pub/sub pattern).
|
|
@@ -1297,70 +1901,158 @@ declare function guard(cond: boolean, error: string): Result<true, string>;
|
|
|
1297
1901
|
*/
|
|
1298
1902
|
declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements EventBus<Evt> {
|
|
1299
1903
|
private readonly handlers;
|
|
1300
|
-
subscribe<
|
|
1301
|
-
|
|
1904
|
+
subscribe<K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1905
|
+
type: K;
|
|
1906
|
+
}>>): () => void;
|
|
1907
|
+
once<K extends Evt["type"]>(eventType: K, options?: OnceOptions): Promise<Extract<Evt, {
|
|
1908
|
+
type: K;
|
|
1909
|
+
}>>;
|
|
1910
|
+
/**
|
|
1911
|
+
* See {@link EventBus.publish} for the full ordering / parallelism /
|
|
1912
|
+
* error-aggregation contract this implementation realises:
|
|
1913
|
+
* - events in input order, sequentially;
|
|
1914
|
+
* - handlers within one event in parallel via `Promise.allSettled`;
|
|
1915
|
+
* - errors collected and thrown after the batch (single Error, or
|
|
1916
|
+
* `AggregateError` for multiple failures).
|
|
1917
|
+
*/
|
|
1302
1918
|
publish(events: ReadonlyArray<Evt>): Promise<void>;
|
|
1303
1919
|
}
|
|
1304
1920
|
|
|
1305
|
-
declare const __specBrand: unique symbol;
|
|
1306
|
-
/**
|
|
1307
|
-
* A Specification is a named, standalone object that represents a business rule for a query.
|
|
1308
|
-
* It is "translatable" into a concrete database query.
|
|
1309
|
-
*
|
|
1310
|
-
* Uses a branded type to carry the generic parameter without requiring
|
|
1311
|
-
* implementors to add a runtime field.
|
|
1312
|
-
*/
|
|
1313
|
-
interface ISpecification<T> {
|
|
1314
|
-
readonly [__specBrand]?: T;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
1921
|
/**
|
|
1318
|
-
*
|
|
1922
|
+
* Core repository contract for Aggregate Roots.
|
|
1319
1923
|
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1924
|
+
* In DDD a Repository is a "collection illusion" for aggregates: load by
|
|
1925
|
+
* identity, save the whole aggregate, delete by identity. Querying by
|
|
1926
|
+
* arbitrary criteria is a separate concern (CQRS read-side, ad-hoc bulk
|
|
1927
|
+
* operations) and lives on the `IQueryableRepository` extension below — so
|
|
1928
|
+
* write-side repositories don't have to implement query plumbing they
|
|
1929
|
+
* don't need.
|
|
1323
1930
|
*
|
|
1324
|
-
*
|
|
1325
|
-
* the aggregate
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1328
|
-
*
|
|
1329
|
-
* within the aggregate boundary and are managed through the Aggregate Root.
|
|
1931
|
+
* Repositories work exclusively with Aggregate Root Entities. The Aggregate
|
|
1932
|
+
* Root represents the aggregate externally and is the only object that can
|
|
1933
|
+
* be loaded or saved through repositories. When loading, all child entities
|
|
1934
|
+
* and value objects inside the aggregate are loaded too; when saving, the
|
|
1935
|
+
* whole aggregate is persisted as a unit.
|
|
1330
1936
|
*
|
|
1331
1937
|
* @template TAgg - The aggregate root type (must implement IAggregateRoot)
|
|
1332
|
-
* @template TId
|
|
1938
|
+
* @template TId - The type of the aggregate root identifier
|
|
1333
1939
|
*/
|
|
1334
1940
|
interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>> {
|
|
1941
|
+
/**
|
|
1942
|
+
* Loads an aggregate by id. Returns `null` when not found.
|
|
1943
|
+
*/
|
|
1335
1944
|
getById(id: TId): Promise<TAgg | null>;
|
|
1336
|
-
|
|
1337
|
-
|
|
1945
|
+
/**
|
|
1946
|
+
* Loads an aggregate by id and throws `AggregateNotFoundError` when not
|
|
1947
|
+
* found. Use this when "not found" is a programming/contract error in
|
|
1948
|
+
* the calling Use Case; use `getById` when null is a valid outcome.
|
|
1949
|
+
*/
|
|
1950
|
+
getByIdOrFail(id: TId): Promise<TAgg>;
|
|
1951
|
+
/**
|
|
1952
|
+
* Returns whether an aggregate with the given id exists. Cheaper than
|
|
1953
|
+
* `getById !== null` if your storage supports `EXISTS`-style queries.
|
|
1954
|
+
*/
|
|
1955
|
+
exists(id: TId): Promise<boolean>;
|
|
1956
|
+
/**
|
|
1957
|
+
* Persists the aggregate (insert or update). Implementations should:
|
|
1958
|
+
*
|
|
1959
|
+
* 1. Throw `ConcurrencyConflictError` from `@shirudo/ddd-kit` when the
|
|
1960
|
+
* aggregate's expected version does not match the version currently
|
|
1961
|
+
* stored (optimistic concurrency).
|
|
1962
|
+
* 2. After a successful write, call `aggregate.markPersisted(newVersion)`
|
|
1963
|
+
* so the in-memory aggregate reflects the new version and clears its
|
|
1964
|
+
* pending/domain events.
|
|
1965
|
+
*
|
|
1966
|
+
* Return type stays `void` — the caller already holds the aggregate
|
|
1967
|
+
* reference, which is now up to date.
|
|
1968
|
+
*/
|
|
1338
1969
|
save(aggregate: TAgg): Promise<void>;
|
|
1970
|
+
/**
|
|
1971
|
+
* Removes the aggregate by id.
|
|
1972
|
+
*/
|
|
1339
1973
|
delete(id: TId): Promise<void>;
|
|
1340
1974
|
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Repository extension that adds filter-based querying. `TFilter` is the
|
|
1977
|
+
* filter shape your persistence layer speaks: a Drizzle `SQL` expression, a
|
|
1978
|
+
* Prisma `WhereInput`, a MongoDB filter document, a plain
|
|
1979
|
+
* `(t: TAgg) => boolean` predicate for in-memory repos, or anything else.
|
|
1980
|
+
*
|
|
1981
|
+
* The library does not prescribe a Specification or query DSL — the
|
|
1982
|
+
* Repository implementation owns its query language. This avoids the
|
|
1983
|
+
* phantom-interface trap of a library-level `ISpecification<T>` with no
|
|
1984
|
+
* methods and lets each Repository expose the strongest possible types for
|
|
1985
|
+
* its storage backend.
|
|
1986
|
+
*
|
|
1987
|
+
* Aggregates that are only ever accessed by id should implement
|
|
1988
|
+
* `IRepository` directly and skip this extension.
|
|
1989
|
+
*
|
|
1990
|
+
* @template TAgg - The aggregate root type
|
|
1991
|
+
* @template TId - The aggregate root identifier type
|
|
1992
|
+
* @template TFilter - The filter shape understood by this repository
|
|
1993
|
+
*
|
|
1994
|
+
* @example
|
|
1995
|
+
* ```typescript
|
|
1996
|
+
* // In-memory repo with a predicate filter
|
|
1997
|
+
* type Predicate<T> = (t: T) => boolean;
|
|
1998
|
+
* class InMemoryOrders implements IQueryableRepository<Order, OrderId, Predicate<Order>> {
|
|
1999
|
+
* // ...
|
|
2000
|
+
* async find(filter: Predicate<Order>): Promise<Order[]> { ... }
|
|
2001
|
+
* async findOne(filter: Predicate<Order>): Promise<Order | null> { ... }
|
|
2002
|
+
* }
|
|
2003
|
+
*
|
|
2004
|
+
* // Drizzle repo with a SQL expression filter
|
|
2005
|
+
* import type { SQL } from "drizzle-orm";
|
|
2006
|
+
* class DrizzleOrders implements IQueryableRepository<Order, OrderId, SQL> {
|
|
2007
|
+
* // ...
|
|
2008
|
+
* }
|
|
2009
|
+
* ```
|
|
2010
|
+
*/
|
|
2011
|
+
interface IQueryableRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>, TFilter> extends IRepository<TAgg, TId> {
|
|
2012
|
+
/**
|
|
2013
|
+
* Returns the first aggregate matching the filter, or `null` if none.
|
|
2014
|
+
*/
|
|
2015
|
+
findOne(filter: TFilter): Promise<TAgg | null>;
|
|
2016
|
+
/**
|
|
2017
|
+
* Returns **every** aggregate matching the filter — no pagination,
|
|
2018
|
+
* no cursor. For unbounded result sets, prefer a read-side projection
|
|
2019
|
+
* (CQRS read model) over loading aggregates in bulk; aggregates are
|
|
2020
|
+
* write-side objects and rehydrating thousands of them by id is rarely
|
|
2021
|
+
* what you want. If you need pagination on the write side, declare a
|
|
2022
|
+
* domain-specific paged method on your concrete repository (e.g.
|
|
2023
|
+
* `findPage(filter, cursor)`) — the library does not prescribe a
|
|
2024
|
+
* pagination contract because cursor/offset/keyset semantics vary too
|
|
2025
|
+
* much across storage backends.
|
|
2026
|
+
*/
|
|
2027
|
+
find(filter: TFilter): Promise<TAgg[]>;
|
|
2028
|
+
}
|
|
1341
2029
|
|
|
1342
2030
|
type VO<T> = Readonly<T>;
|
|
1343
2031
|
/**
|
|
1344
|
-
* Deep freezes an object and all its nested properties recursively
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
2032
|
+
* Deep freezes an object and all its nested properties recursively, then
|
|
2033
|
+
* returns it. Iterates both string-keyed and symbol-keyed own properties
|
|
2034
|
+
* so the freeze symmetry matches `deepEqual` (which also considers symbol
|
|
2035
|
+
* keys). Handles circular references by tracking visited objects.
|
|
2036
|
+
*
|
|
2037
|
+
* Note: `deepFreeze` mutates its argument in place — it sets `[[Frozen]]`
|
|
2038
|
+
* on the object you pass in. Callers that need to avoid touching the
|
|
2039
|
+
* input (e.g. `vo()`) should deep-clone first.
|
|
1347
2040
|
*/
|
|
1348
2041
|
declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
|
|
1349
2042
|
/**
|
|
1350
2043
|
* Creates a deeply immutable value object from the given data.
|
|
1351
|
-
* All nested objects and arrays are frozen recursively.
|
|
1352
2044
|
*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
2045
|
+
* The input is first deep-cloned with `structuredClone`, then the clone
|
|
2046
|
+
* is frozen — so calling `vo(input)` never freezes the caller's own
|
|
2047
|
+
* object graph as a side-effect. Mutating the input afterwards does not
|
|
2048
|
+
* bleed into the VO.
|
|
1355
2049
|
*
|
|
1356
2050
|
* @example
|
|
1357
2051
|
* ```typescript
|
|
1358
|
-
* const
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
* });
|
|
1363
|
-
* // address.coordinates.lat = 99; // ❌ Error: Cannot assign to read-only property
|
|
2052
|
+
* const nested = { lat: 52.5, lng: 13.4 };
|
|
2053
|
+
* const address = vo({ street: "Main St", coordinates: nested });
|
|
2054
|
+
* address.coordinates.lat = 99; // ❌ Cannot assign to read-only property
|
|
2055
|
+
* nested.lat = 0; // ✅ caller's input still mutable
|
|
1364
2056
|
* ```
|
|
1365
2057
|
*/
|
|
1366
2058
|
declare function vo<T>(t: T): VO<T>;
|
|
@@ -1459,26 +2151,6 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
|
|
|
1459
2151
|
* ```
|
|
1460
2152
|
*/
|
|
1461
2153
|
declare function voWithValidation<T>(t: T, validate: (value: T) => boolean, errorMessage?: string): Result<VO<T>, string>;
|
|
1462
|
-
/**
|
|
1463
|
-
* Creates a value object with optional validation.
|
|
1464
|
-
* Throws an error if validation fails.
|
|
1465
|
-
*
|
|
1466
|
-
* @param t - The data to convert into a value object
|
|
1467
|
-
* @param validate - Validation function that returns true if valid
|
|
1468
|
-
* @param errorMessage - Optional custom error message if validation fails
|
|
1469
|
-
* @returns A deeply frozen, immutable value object
|
|
1470
|
-
* @throws Error if validation fails
|
|
1471
|
-
*
|
|
1472
|
-
* @example
|
|
1473
|
-
* ```typescript
|
|
1474
|
-
* const money = voWithValidationUnsafe(
|
|
1475
|
-
* { amount: 100, currency: "USD" },
|
|
1476
|
-
* (m) => m.amount >= 0 && m.currency.length === 3,
|
|
1477
|
-
* "Invalid money: amount must be non-negative and currency must be 3 characters"
|
|
1478
|
-
* );
|
|
1479
|
-
* ```
|
|
1480
|
-
*/
|
|
1481
|
-
declare function voWithValidationUnsafe<T>(t: T, validate: (value: T) => boolean, errorMessage?: string): VO<T>;
|
|
1482
2154
|
/**
|
|
1483
2155
|
* Interface for Value Objects.
|
|
1484
2156
|
* Value Objects are immutable and defined by their properties.
|
|
@@ -1570,4 +2242,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
1570
2242
|
toJSON(): Readonly<T>;
|
|
1571
2243
|
}
|
|
1572
2244
|
|
|
1573
|
-
export { type AggregateConfig, AggregateRoot, type AggregateSnapshot, type
|
|
2245
|
+
export { type AggregateConfig, AggregateNotFoundError, AggregateRoot, type AggregateSnapshot, type ClockFactory, type Command, CommandBus, type CommandHandler, ConcurrencyConflictError, type CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, type DomainEvent, Entity, type EventBus, EventBusImpl, type EventHandler, type EventIdFactory, type EventMetadata, EventSourcedAggregate, type EventSourcedAggregateConfig, type IAggregateRoot, type ICommandBus, type IEntity, type IEventSourcedAggregate, type IQueryBus, type IQueryableRepository, type IRepository, type IValueObject, type Id, type IdGenerator, type Identifiable, InfrastructureError, MissingHandlerError, type OnceOptions, type Outbox, type OutboxRecord, type Query, QueryBus, type QueryHandler, type TransactionScope, type VO, ValueObject, type Version, copyMetadata, createDomainEvent, createDomainEventWithMetadata, deepFreeze, entityIds, findEntityById, freezeShallow, hasEntityId, mergeMetadata, removeEntityById, replaceEntityById, resetClockFactory, resetEventIdFactory, sameEntity, sameVersion, setClockFactory, setEventIdFactory, updateEntityById, vo, voEquals, voEqualsExcept, voWithValidation, withCommit };
|