@shirudo/ddd-kit 1.0.0-rc.1 → 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 +96 -143
- package/dist/index.d.ts +808 -186
- package/dist/index.js +643 -380
- 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 +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 -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,113 @@
|
|
|
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
|
|
|
44
|
+
/**
|
|
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.
|
|
89
|
+
*/
|
|
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;
|
|
11
111
|
/**
|
|
12
112
|
* Metadata associated with a domain event for traceability and correlation.
|
|
13
113
|
* Used in event-driven architectures to track event flow across services.
|
|
@@ -45,13 +145,31 @@ interface EventMetadata {
|
|
|
45
145
|
* @template P - The event payload type
|
|
46
146
|
*/
|
|
47
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;
|
|
48
155
|
/**
|
|
49
156
|
* The type of the event, used for routing and handling.
|
|
50
157
|
*/
|
|
51
158
|
type: T;
|
|
52
159
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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`.
|
|
55
173
|
*/
|
|
56
174
|
payload: P;
|
|
57
175
|
/**
|
|
@@ -70,6 +188,37 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
70
188
|
*/
|
|
71
189
|
metadata?: EventMetadata;
|
|
72
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
|
+
}
|
|
73
222
|
/**
|
|
74
223
|
* Creates a domain event with default values.
|
|
75
224
|
* Sets occurredAt to current date and version to 1 if not provided.
|
|
@@ -84,16 +233,8 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
84
233
|
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
85
234
|
* ```
|
|
86
235
|
*/
|
|
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>;
|
|
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>;
|
|
97
238
|
/**
|
|
98
239
|
* Creates a domain event with metadata for traceability.
|
|
99
240
|
* Convenience function for creating events with correlation and causation IDs.
|
|
@@ -107,10 +248,7 @@ declare function createDomainEvent<T extends string, P>(type: T, payload: P, opt
|
|
|
107
248
|
* );
|
|
108
249
|
* ```
|
|
109
250
|
*/
|
|
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>;
|
|
251
|
+
declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
|
|
114
252
|
/**
|
|
115
253
|
* Copies metadata from a source event to a new event.
|
|
116
254
|
* Useful for maintaining correlation chains in event-driven architectures.
|
|
@@ -141,10 +279,72 @@ declare function copyMetadata(sourceEvent: DomainEvent<string, unknown>, additio
|
|
|
141
279
|
declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
|
|
142
280
|
|
|
143
281
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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
|
+
* ```
|
|
146
336
|
*/
|
|
147
|
-
|
|
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>> = {
|
|
148
348
|
readonly id: TId;
|
|
149
349
|
};
|
|
150
350
|
/**
|
|
@@ -204,7 +404,15 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
204
404
|
readonly id: TId;
|
|
205
405
|
/**
|
|
206
406
|
* Returns the current state of the entity.
|
|
207
|
-
*
|
|
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`).
|
|
208
416
|
*/
|
|
209
417
|
get state(): TState;
|
|
210
418
|
/**
|
|
@@ -214,12 +422,25 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
214
422
|
protected _state: TState;
|
|
215
423
|
protected constructor(id: TId, initialState: TState);
|
|
216
424
|
/**
|
|
217
|
-
* Optional validation hook to ensure state invariants.
|
|
218
|
-
*
|
|
219
|
-
*
|
|
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.
|
|
220
441
|
*
|
|
221
442
|
* @param state - The state to validate
|
|
222
|
-
* @throws Error if validation fails
|
|
443
|
+
* @throws Error (or `DomainError` subclass) if validation fails
|
|
223
444
|
*/
|
|
224
445
|
protected validateState(_state: TState): void;
|
|
225
446
|
/**
|
|
@@ -231,6 +452,18 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
231
452
|
*/
|
|
232
453
|
protected setState(newState: TState): void;
|
|
233
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;
|
|
234
467
|
/**
|
|
235
468
|
* Checks if two entities have the same ID.
|
|
236
469
|
* Works with any object that has an 'id' property.
|
|
@@ -248,7 +481,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
248
481
|
* sameEntity(item1, item1); // true
|
|
249
482
|
* ```
|
|
250
483
|
*/
|
|
251
|
-
declare function sameEntity<TId
|
|
484
|
+
declare function sameEntity<TId extends Id<string>>(a: Identifiable<TId>, b: Identifiable<TId>): boolean;
|
|
252
485
|
/**
|
|
253
486
|
* Finds an entity by ID in a collection.
|
|
254
487
|
* Returns undefined if not found.
|
|
@@ -268,7 +501,7 @@ declare function sameEntity<TId>(a: Identifiable<TId>, b: Identifiable<TId>): bo
|
|
|
268
501
|
* // item is { id: itemId1, productId: "prod-1", quantity: 2 }
|
|
269
502
|
* ```
|
|
270
503
|
*/
|
|
271
|
-
declare function findEntityById<TId
|
|
504
|
+
declare function findEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T | undefined;
|
|
272
505
|
/**
|
|
273
506
|
* Checks if an entity with the given ID exists in the collection.
|
|
274
507
|
*
|
|
@@ -286,7 +519,7 @@ declare function findEntityById<TId, T extends Identifiable<TId>>(entities: T[],
|
|
|
286
519
|
* hasEntityId(items, itemId2); // false
|
|
287
520
|
* ```
|
|
288
521
|
*/
|
|
289
|
-
declare function hasEntityId<TId
|
|
522
|
+
declare function hasEntityId<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): boolean;
|
|
290
523
|
/**
|
|
291
524
|
* Removes an entity with the given ID from the collection.
|
|
292
525
|
* Returns a new array without the entity.
|
|
@@ -306,7 +539,7 @@ declare function hasEntityId<TId, T extends Identifiable<TId>>(entities: T[], id
|
|
|
306
539
|
* // updated is [{ id: itemId2, productId: "prod-2", quantity: 1 }]
|
|
307
540
|
* ```
|
|
308
541
|
*/
|
|
309
|
-
declare function removeEntityById<TId
|
|
542
|
+
declare function removeEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId): T[];
|
|
310
543
|
/**
|
|
311
544
|
* Updates an entity with the given ID in the collection.
|
|
312
545
|
* Returns a new array with the updated entity.
|
|
@@ -330,7 +563,7 @@ declare function removeEntityById<TId, T extends Identifiable<TId>>(entities: T[
|
|
|
330
563
|
* // updated is [{ id: itemId1, productId: "prod-1", quantity: 3 }]
|
|
331
564
|
* ```
|
|
332
565
|
*/
|
|
333
|
-
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[];
|
|
334
567
|
/**
|
|
335
568
|
* Replaces an entity with the given ID in the collection.
|
|
336
569
|
* Returns a new array with the replaced entity.
|
|
@@ -354,7 +587,7 @@ declare function updateEntityById<TId, T extends Identifiable<TId>>(entities: T[
|
|
|
354
587
|
* });
|
|
355
588
|
* ```
|
|
356
589
|
*/
|
|
357
|
-
declare function replaceEntityById<TId
|
|
590
|
+
declare function replaceEntityById<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[], id: TId, replacement: T): T[];
|
|
358
591
|
/**
|
|
359
592
|
* Extracts all IDs from a collection of entities.
|
|
360
593
|
*
|
|
@@ -372,7 +605,7 @@ declare function replaceEntityById<TId, T extends Identifiable<TId>>(entities: T
|
|
|
372
605
|
* // ids is [itemId1, itemId2]
|
|
373
606
|
* ```
|
|
374
607
|
*/
|
|
375
|
-
declare function entityIds<TId
|
|
608
|
+
declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(entities: T[]): TId[];
|
|
376
609
|
|
|
377
610
|
/**
|
|
378
611
|
* Marker interface for Aggregate Roots.
|
|
@@ -412,14 +645,36 @@ interface IAggregateRoot<TId extends Id<string>> {
|
|
|
412
645
|
* This version applies to the entire aggregate, including all child entities.
|
|
413
646
|
*/
|
|
414
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;
|
|
415
661
|
}
|
|
416
662
|
/**
|
|
417
663
|
* Configuration options for AggregateRoot behavior.
|
|
418
664
|
*/
|
|
419
665
|
interface AggregateConfig {
|
|
420
666
|
/**
|
|
421
|
-
* Whether
|
|
422
|
-
*
|
|
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.)
|
|
423
678
|
*/
|
|
424
679
|
autoVersionBump?: boolean;
|
|
425
680
|
}
|
|
@@ -461,7 +716,7 @@ interface AggregateConfig {
|
|
|
461
716
|
* }
|
|
462
717
|
* ```
|
|
463
718
|
*/
|
|
464
|
-
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent =
|
|
719
|
+
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = never> extends Entity<TState, TId> implements IAggregateRoot<TId> {
|
|
465
720
|
private _version;
|
|
466
721
|
get version(): Version;
|
|
467
722
|
protected setVersion(version: Version): void;
|
|
@@ -478,12 +733,92 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
|
|
|
478
733
|
* Call this after dispatching the events.
|
|
479
734
|
*/
|
|
480
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;
|
|
481
792
|
protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
|
|
482
793
|
/**
|
|
483
|
-
*
|
|
484
|
-
* Use this to record side-effects that should be published.
|
|
794
|
+
* Records a domain event for later publication.
|
|
485
795
|
*
|
|
486
|
-
*
|
|
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.
|
|
816
|
+
*
|
|
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
|
|
487
822
|
*/
|
|
488
823
|
protected addDomainEvent(event: TEvent): void;
|
|
489
824
|
/**
|
|
@@ -534,6 +869,54 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent = un
|
|
|
534
869
|
restoreFromSnapshot(snapshot: AggregateSnapshot<TState>): void;
|
|
535
870
|
}
|
|
536
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
|
+
|
|
537
920
|
/**
|
|
538
921
|
* Interface for Event-Sourced Aggregate Roots.
|
|
539
922
|
* Defines the contract for aggregates that manage state changes via event sourcing.
|
|
@@ -547,11 +930,13 @@ interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends DomainEv
|
|
|
547
930
|
*/
|
|
548
931
|
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
549
932
|
/**
|
|
550
|
-
* 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.
|
|
551
936
|
*
|
|
552
937
|
* @param history - An ordered list of past events
|
|
553
938
|
*/
|
|
554
|
-
loadFromHistory(history: TEvent[]): Result<void,
|
|
939
|
+
loadFromHistory(history: TEvent[]): Result<void, DomainError>;
|
|
555
940
|
/**
|
|
556
941
|
* Clears the list of pending events.
|
|
557
942
|
*/
|
|
@@ -575,8 +960,18 @@ type Handler<TState, TEvent> = (state: TState, event: TEvent) => TState;
|
|
|
575
960
|
*/
|
|
576
961
|
interface EventSourcedAggregateConfig {
|
|
577
962
|
/**
|
|
578
|
-
* Whether
|
|
579
|
-
*
|
|
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.)
|
|
580
975
|
*/
|
|
581
976
|
autoVersionBump?: boolean;
|
|
582
977
|
}
|
|
@@ -591,17 +986,35 @@ interface EventSourcedAggregateConfig {
|
|
|
591
986
|
* `addDomainEvent()` are not available. This enforces the event sourcing pattern
|
|
592
987
|
* at the type level — there is no way to mutate state without going through an event handler.
|
|
593
988
|
*
|
|
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
|
+
*
|
|
594
997
|
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
595
998
|
* @template TEvent - The union type of all domain events
|
|
596
999
|
* @template TId - The type of the aggregate root identifier
|
|
597
1000
|
*
|
|
598
1001
|
* @example
|
|
599
1002
|
* ```typescript
|
|
1003
|
+
* class OrderAlreadyConfirmedError extends DomainError {
|
|
1004
|
+
* constructor(id: OrderId) { super(`Order ${id} is already confirmed`); }
|
|
1005
|
+
* }
|
|
1006
|
+
*
|
|
600
1007
|
* class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
601
1008
|
* confirm(): void {
|
|
602
1009
|
* this.apply(createDomainEvent("OrderConfirmed", {}));
|
|
603
1010
|
* }
|
|
604
1011
|
*
|
|
1012
|
+
* protected validateEvent(event: OrderEvent): void {
|
|
1013
|
+
* if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
|
|
1014
|
+
* throw new OrderAlreadyConfirmedError(this.id);
|
|
1015
|
+
* }
|
|
1016
|
+
* }
|
|
1017
|
+
*
|
|
605
1018
|
* protected readonly handlers = {
|
|
606
1019
|
* OrderConfirmed: (state: OrderState): OrderState => ({
|
|
607
1020
|
* ...state,
|
|
@@ -619,36 +1032,66 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
619
1032
|
private readonly _autoVersionBump;
|
|
620
1033
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
621
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;
|
|
622
1042
|
protected constructor(id: TId, initialState: TState, config?: EventSourcedAggregateConfig);
|
|
623
1043
|
/**
|
|
624
|
-
* Validates an event before it is applied.
|
|
625
|
-
*
|
|
626
|
-
*
|
|
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.
|
|
627
1047
|
*/
|
|
628
|
-
protected validateEvent(_event: TEvent):
|
|
1048
|
+
protected validateEvent(_event: TEvent): void;
|
|
629
1049
|
/**
|
|
630
|
-
* Applies an event
|
|
631
|
-
*
|
|
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.
|
|
632
1063
|
*
|
|
633
1064
|
* @param event - The domain event to apply
|
|
634
|
-
* @param isNew - Whether the event is new (needs persisting) or from history
|
|
1065
|
+
* @param isNew - Whether the event is new (needs persisting) or replayed from history
|
|
635
1066
|
*/
|
|
636
|
-
protected apply(event: TEvent,
|
|
1067
|
+
protected apply<K extends TEvent["type"]>(event: Extract<TEvent, {
|
|
1068
|
+
type: K;
|
|
1069
|
+
}>, isNew?: boolean): void;
|
|
637
1070
|
/**
|
|
638
|
-
*
|
|
639
|
-
*
|
|
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.
|
|
640
1076
|
*/
|
|
641
|
-
|
|
1077
|
+
private dispatchAndCommit;
|
|
642
1078
|
/**
|
|
643
1079
|
* Manually bumps the aggregate version.
|
|
644
1080
|
* Only needed if `autoVersionBump` is disabled.
|
|
645
1081
|
*/
|
|
646
1082
|
protected bumpVersion(): void;
|
|
647
1083
|
/**
|
|
648
|
-
* Reconstitutes the aggregate from an event history.
|
|
649
|
-
*
|
|
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.
|
|
650
1093
|
*/
|
|
651
|
-
loadFromHistory(history: TEvent[]): Result<void,
|
|
1094
|
+
loadFromHistory(history: TEvent[]): Result<void, DomainError>;
|
|
652
1095
|
hasPendingEvents(): boolean;
|
|
653
1096
|
getEventCount(): number;
|
|
654
1097
|
getLatestEvent(): TEvent | undefined;
|
|
@@ -657,9 +1100,16 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
657
1100
|
*/
|
|
658
1101
|
createSnapshot(): AggregateSnapshot<TState>;
|
|
659
1102
|
/**
|
|
660
|
-
* Restores the aggregate from a snapshot and applies events that occurred
|
|
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.
|
|
661
1111
|
*/
|
|
662
|
-
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void,
|
|
1112
|
+
restoreFromSnapshotWithEvents(snapshot: AggregateSnapshot<TState>, eventsAfterSnapshot: TEvent[]): Result<void, DomainError>;
|
|
663
1113
|
/**
|
|
664
1114
|
* A map of event types to their corresponding handlers.
|
|
665
1115
|
* Subclasses MUST implement this property.
|
|
@@ -674,33 +1124,6 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends DomainEvent<
|
|
|
674
1124
|
type Version = number & {
|
|
675
1125
|
readonly __v: true;
|
|
676
1126
|
};
|
|
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
1127
|
/**
|
|
705
1128
|
* Snapshot of an aggregate state at a specific point in time.
|
|
706
1129
|
* Used for optimizing event replay by starting from a snapshot
|
|
@@ -892,10 +1315,20 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
|
|
|
892
1315
|
/**
|
|
893
1316
|
* Registers a handler for a specific command type.
|
|
894
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
|
+
*
|
|
895
1324
|
* @param commandType - The command type to register the handler for
|
|
896
1325
|
* @param handler - The handler function for this command type
|
|
897
1326
|
*/
|
|
898
|
-
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;
|
|
899
1332
|
}
|
|
900
1333
|
/**
|
|
901
1334
|
* Simple in-memory command bus implementation.
|
|
@@ -935,7 +1368,11 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
|
|
|
935
1368
|
*/
|
|
936
1369
|
declare class CommandBus<TMap extends CommandTypeMap = CommandTypeMap> implements ICommandBus<TMap> {
|
|
937
1370
|
private readonly handlers;
|
|
938
|
-
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;
|
|
939
1376
|
execute<C extends Command & {
|
|
940
1377
|
type: keyof TMap & string;
|
|
941
1378
|
}>(command: C): Promise<Result<TMap[C["type"]], string>>;
|
|
@@ -976,7 +1413,28 @@ interface EventBus<Evt extends {
|
|
|
976
1413
|
}> {
|
|
977
1414
|
/**
|
|
978
1415
|
* Publishes events to all subscribed handlers.
|
|
979
|
-
*
|
|
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.
|
|
980
1438
|
*
|
|
981
1439
|
* @param events - Array of events to publish
|
|
982
1440
|
*/
|
|
@@ -999,7 +1457,9 @@ interface EventBus<Evt extends {
|
|
|
999
1457
|
* unsubscribe();
|
|
1000
1458
|
* ```
|
|
1001
1459
|
*/
|
|
1002
|
-
subscribe: <
|
|
1460
|
+
subscribe: <K extends Evt["type"]>(eventType: K, handler: EventHandler<Extract<Evt, {
|
|
1461
|
+
type: K;
|
|
1462
|
+
}>>) => () => void;
|
|
1003
1463
|
/**
|
|
1004
1464
|
* Subscribes to the next occurrence of an event type.
|
|
1005
1465
|
* Returns a Promise that resolves with the event data.
|
|
@@ -1014,24 +1474,125 @@ interface EventBus<Evt extends {
|
|
|
1014
1474
|
* console.log("Order created:", event.payload.orderId);
|
|
1015
1475
|
* ```
|
|
1016
1476
|
*/
|
|
1017
|
-
once: <
|
|
1477
|
+
once: <K extends Evt["type"]>(eventType: K, options?: OnceOptions) => Promise<Extract<Evt, {
|
|
1478
|
+
type: K;
|
|
1479
|
+
}>>;
|
|
1018
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;
|
|
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
|
+
*/
|
|
1019
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
|
+
*/
|
|
1020
1538
|
add: (events: ReadonlyArray<Evt>) => Promise<void>;
|
|
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>;
|
|
1021
1551
|
}
|
|
1022
1552
|
|
|
1023
|
-
|
|
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 {
|
|
1024
1576
|
transactional<T>(fn: () => Promise<T>): Promise<T>;
|
|
1025
1577
|
}
|
|
1026
|
-
type RepoProvider<R> = (uow: UnitOfWork) => R;
|
|
1027
1578
|
|
|
1028
1579
|
/**
|
|
1029
|
-
* Helper
|
|
1030
|
-
*
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
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).
|
|
1035
1596
|
*
|
|
1036
1597
|
* @example
|
|
1037
1598
|
* ```typescript
|
|
@@ -1040,10 +1601,7 @@ type RepoProvider<R> = (uow: UnitOfWork) => R;
|
|
|
1040
1601
|
* async () => {
|
|
1041
1602
|
* const order = Order.create(customerId, items);
|
|
1042
1603
|
* await repository.save(order);
|
|
1043
|
-
* return {
|
|
1044
|
-
* result: order.id,
|
|
1045
|
-
* events: order.pendingEvents
|
|
1046
|
-
* };
|
|
1604
|
+
* return { result: order.id, events: order.domainEvents };
|
|
1047
1605
|
* }
|
|
1048
1606
|
* );
|
|
1049
1607
|
* ```
|
|
@@ -1053,7 +1611,7 @@ declare function withCommit<Evt extends {
|
|
|
1053
1611
|
}, R>(deps: {
|
|
1054
1612
|
outbox: Outbox<Evt>;
|
|
1055
1613
|
bus?: EventBus<Evt>;
|
|
1056
|
-
|
|
1614
|
+
scope: TransactionScope;
|
|
1057
1615
|
}, fn: () => Promise<{
|
|
1058
1616
|
result: R;
|
|
1059
1617
|
events: ReadonlyArray<Evt>;
|
|
@@ -1201,10 +1759,20 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
|
|
|
1201
1759
|
/**
|
|
1202
1760
|
* Registers a handler for a specific query type.
|
|
1203
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
|
+
*
|
|
1204
1768
|
* @param queryType - The query type to register the handler for
|
|
1205
1769
|
* @param handler - The handler function for this query type
|
|
1206
1770
|
*/
|
|
1207
|
-
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;
|
|
1208
1776
|
}
|
|
1209
1777
|
/**
|
|
1210
1778
|
* Simple in-memory query bus implementation.
|
|
@@ -1244,7 +1812,11 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
|
|
|
1244
1812
|
*/
|
|
1245
1813
|
declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQueryBus<TMap> {
|
|
1246
1814
|
private readonly handlers;
|
|
1247
|
-
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;
|
|
1248
1820
|
execute<Q extends Query & {
|
|
1249
1821
|
type: keyof TMap & string;
|
|
1250
1822
|
}>(query: Q): Promise<Result<TMap[Q["type"]], string>>;
|
|
@@ -1255,24 +1827,6 @@ declare class QueryBus<TMap extends QueryTypeMap = QueryTypeMap> implements IQue
|
|
|
1255
1827
|
executeUnsafe<Q extends Query, R>(query: Q): Promise<R>;
|
|
1256
1828
|
}
|
|
1257
1829
|
|
|
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
1830
|
/**
|
|
1277
1831
|
* Simple in-memory event bus implementation.
|
|
1278
1832
|
* Supports multiple subscribers per event type (pub/sub pattern).
|
|
@@ -1297,70 +1851,158 @@ declare function guard(cond: boolean, error: string): Result<true, string>;
|
|
|
1297
1851
|
*/
|
|
1298
1852
|
declare class EventBusImpl<Evt extends DomainEvent<string, unknown>> implements EventBus<Evt> {
|
|
1299
1853
|
private readonly handlers;
|
|
1300
|
-
subscribe<
|
|
1301
|
-
|
|
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
|
+
*/
|
|
1302
1868
|
publish(events: ReadonlyArray<Evt>): Promise<void>;
|
|
1303
1869
|
}
|
|
1304
1870
|
|
|
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
1871
|
/**
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
* Repositories work exclusively with Aggregate Root Entities. The Aggregate Root
|
|
1321
|
-
* is the Entity that represents the aggregate externally and is the only object
|
|
1322
|
-
* that can be loaded/saved through repositories.
|
|
1872
|
+
* Core repository contract for Aggregate Roots.
|
|
1323
1873
|
*
|
|
1324
|
-
*
|
|
1325
|
-
* the aggregate
|
|
1326
|
-
*
|
|
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.
|
|
1327
1880
|
*
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
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.
|
|
1330
1886
|
*
|
|
1331
1887
|
* @template TAgg - The aggregate root type (must implement IAggregateRoot)
|
|
1332
|
-
* @template TId
|
|
1888
|
+
* @template TId - The type of the aggregate root identifier
|
|
1333
1889
|
*/
|
|
1334
1890
|
interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>> {
|
|
1891
|
+
/**
|
|
1892
|
+
* Loads an aggregate by id. Returns `null` when not found.
|
|
1893
|
+
*/
|
|
1335
1894
|
getById(id: TId): Promise<TAgg | null>;
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
+
*/
|
|
1338
1919
|
save(aggregate: TAgg): Promise<void>;
|
|
1920
|
+
/**
|
|
1921
|
+
* Removes the aggregate by id.
|
|
1922
|
+
*/
|
|
1339
1923
|
delete(id: TId): Promise<void>;
|
|
1340
1924
|
}
|
|
1925
|
+
/**
|
|
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.
|
|
1930
|
+
*
|
|
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.
|
|
1936
|
+
*
|
|
1937
|
+
* Aggregates that are only ever accessed by id should implement
|
|
1938
|
+
* `IRepository` directly and skip this extension.
|
|
1939
|
+
*
|
|
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
|
|
1943
|
+
*
|
|
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
|
+
* ```
|
|
1960
|
+
*/
|
|
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[]>;
|
|
1978
|
+
}
|
|
1341
1979
|
|
|
1342
1980
|
type VO<T> = Readonly<T>;
|
|
1343
1981
|
/**
|
|
1344
|
-
* Deep freezes an object and all its nested properties recursively
|
|
1345
|
-
*
|
|
1346
|
-
*
|
|
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.
|
|
1347
1990
|
*/
|
|
1348
1991
|
declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
|
|
1349
1992
|
/**
|
|
1350
1993
|
* Creates a deeply immutable value object from the given data.
|
|
1351
|
-
* All nested objects and arrays are frozen recursively.
|
|
1352
1994
|
*
|
|
1353
|
-
*
|
|
1354
|
-
*
|
|
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.
|
|
1355
1999
|
*
|
|
1356
2000
|
* @example
|
|
1357
2001
|
* ```typescript
|
|
1358
|
-
* const
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
*
|
|
1362
|
-
* });
|
|
1363
|
-
* // 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
|
|
1364
2006
|
* ```
|
|
1365
2007
|
*/
|
|
1366
2008
|
declare function vo<T>(t: T): VO<T>;
|
|
@@ -1459,26 +2101,6 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
|
|
|
1459
2101
|
* ```
|
|
1460
2102
|
*/
|
|
1461
2103
|
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
2104
|
/**
|
|
1483
2105
|
* Interface for Value Objects.
|
|
1484
2106
|
* Value Objects are immutable and defined by their properties.
|
|
@@ -1570,4 +2192,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
1570
2192
|
toJSON(): Readonly<T>;
|
|
1571
2193
|
}
|
|
1572
2194
|
|
|
1573
|
-
export { type AggregateConfig, AggregateRoot, type AggregateSnapshot, 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 };
|