@shirudo/ddd-kit 1.1.0 → 1.2.0
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 +21 -18
- package/dist/aggregate-DclYgG_D.d.ts +662 -0
- package/dist/http.d.ts +2 -2
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +623 -655
- package/dist/index.js +554 -48
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +251 -0
- package/dist/testing.js +793 -0
- package/dist/testing.js.map +1 -0
- package/dist/utils.d.ts +3 -3
- package/dist/utils.js.map +1 -1
- package/package.json +6 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,572 +1,10 @@
|
|
|
1
|
+
import { a as Id, A as AnyDomainEvent, I as IAggregateRoot, V as Version, b as AggregateSnapshot, C as CreateDomainEventOptions, c as IEventSourcedAggregate, D as DomainError, d as InfrastructureError } from './aggregate-DclYgG_D.js';
|
|
2
|
+
export { p as AggregateDeletedError, q as AggregateNotFoundError, f as ClockFactory, u as ConcurrencyConflictError, k as DomainEvent, t as DuplicateAggregateError, E as EventIdFactory, j as EventMetadata, v as IdGenerator, M as MissingHandlerError, n as copyMetadata, l as createDomainEvent, m as createDomainEventWithMetadata, o as mergeMetadata, i as resetClockFactory, r as resetEventIdFactory, s as sameVersion, g as setClockFactory, e as setEventIdFactory, h as withClockFactory, w as withEventIdFactory } from './aggregate-DclYgG_D.js';
|
|
1
3
|
import { Result } from '@shirudo/result';
|
|
2
4
|
import { BaseError, ValidationError } from '@shirudo/base-error';
|
|
3
5
|
import { DeepEqualExceptOptions } from './utils.js';
|
|
4
6
|
export { DeepOmitOptions, Key, PathSegment, deepEqual, deepEqualExcept, deepOmit } from './utils.js';
|
|
5
7
|
|
|
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
|
-
*/
|
|
20
|
-
type Id<Tag extends string> = string & {
|
|
21
|
-
readonly __brand: Tag;
|
|
22
|
-
};
|
|
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
|
-
* **Your factory must produce unique ids under concurrent calls.**
|
|
29
|
-
* The kit makes no attempt to dedupe or detect collisions — a collision
|
|
30
|
-
* silently overwrites earlier rows (under unique-key constraints) or
|
|
31
|
-
* silently aliases two different entities (without them). Safe choices:
|
|
32
|
-
* `crypto.randomUUID()` (UUIDv4, the default for events), ULID, UUIDv7,
|
|
33
|
-
* KSUID — all collision-resistant by design. Unsafe choices: `Date.now()`
|
|
34
|
-
* alone (duplicates within the same millisecond), a process-local
|
|
35
|
-
* counter without persistence (resets to 1 on restart, collides with
|
|
36
|
-
* prior runs), a sequential id derived from non-atomic state.
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```ts
|
|
40
|
-
* import { ulid } from "ulid";
|
|
41
|
-
*
|
|
42
|
-
* const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
|
|
43
|
-
* const id = userIds.next(); // Id<"UserId">
|
|
44
|
-
* ```
|
|
45
|
-
*
|
|
46
|
-
* The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
|
|
47
|
-
* let callers pick `T` themselves — `gen.next<"AnyTag">()` typechecked
|
|
48
|
-
* even when the generator produced different-tag ids, silently defeating
|
|
49
|
-
* the brand.
|
|
50
|
-
*/
|
|
51
|
-
interface IdGenerator<Tag extends string> {
|
|
52
|
-
next: () => Id<Tag>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Abstract base for **domain-invariant violations**. Domain methods
|
|
57
|
-
* (aggregates, entity validation hooks, value-object constructors)
|
|
58
|
-
* throw `DomainError`-derived exceptions when a business rule is
|
|
59
|
-
* violated. Consumers derive their own concrete errors — e.g.
|
|
60
|
-
* `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}` —
|
|
61
|
-
* for `instanceof`-style catching at the App-Service boundary, where
|
|
62
|
-
* they typically map to HTTP 400 / business-rule responses.
|
|
63
|
-
*
|
|
64
|
-
* The library itself does **not** ship any concrete `DomainError`
|
|
65
|
-
* subclass — the kit can't know your invariants.
|
|
66
|
-
*
|
|
67
|
-
* Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
|
|
68
|
-
* surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
|
|
69
|
-
* `isRetryable`, …).
|
|
70
|
-
*/
|
|
71
|
-
declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Abstract base for **infrastructure / persistence failures** that the
|
|
75
|
-
* App-Service can recover from — typically by retrying, by returning
|
|
76
|
-
* HTTP 404 / 409, or by surfacing a "please try again" UX. These are
|
|
77
|
-
* not domain-invariant violations (the business rules were not
|
|
78
|
-
* broken); they describe race conditions and missing rows at the
|
|
79
|
-
* storage boundary.
|
|
80
|
-
*
|
|
81
|
-
* Library-internal concrete subclasses: {@link AggregateNotFoundError},
|
|
82
|
-
* {@link ConcurrencyConflictError}.
|
|
83
|
-
*/
|
|
84
|
-
declare abstract class InfrastructureError<Name extends string = string> extends BaseError<Name> {
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Thrown by `EventSourcedAggregate.apply()` when no handler is
|
|
88
|
-
* registered for the event's type. This means the aggregate's subclass
|
|
89
|
-
* forgot to add an entry to its `handlers` map — a programming /
|
|
90
|
-
* configuration bug, not a domain or infrastructure failure.
|
|
91
|
-
*
|
|
92
|
-
* Deliberately **not** on `DomainError` or `InfrastructureError` —
|
|
93
|
-
* a generic `catch (e instanceof DomainError)` handler at the App
|
|
94
|
-
* layer must not mask a forgotten handler; this should crash loud and
|
|
95
|
-
* fail the calling Use Case so the bug surfaces in development. The
|
|
96
|
-
* replay methods (`loadFromHistory`, `restoreFromSnapshotWithEvents`)
|
|
97
|
-
* also let it propagate uncaught instead of wrapping it in `Result.Err`.
|
|
98
|
-
*
|
|
99
|
-
* Use `isBaseError(e)` from `@shirudo/base-error` to detect
|
|
100
|
-
* "any structured error from the kit or any other BaseError-using
|
|
101
|
-
* library" at the App boundary.
|
|
102
|
-
*/
|
|
103
|
-
declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
|
|
104
|
-
readonly eventType: string;
|
|
105
|
-
constructor(eventType: string, cause?: unknown);
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
|
|
109
|
-
* given id does not exist. `InfrastructureError` because the storage
|
|
110
|
-
* boundary, not a business rule, decided the row is absent. Use the
|
|
111
|
-
* nullable variant `getById()` if "not found" is a valid outcome.
|
|
112
|
-
*
|
|
113
|
-
* Accepts an optional `cause` so a `Repository.save()` implementation
|
|
114
|
-
* can wrap a lower-level "row not found" / driver-level error without
|
|
115
|
-
* losing context. Cause-chain helpers (`getRootCause`,
|
|
116
|
-
* `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
|
|
117
|
-
*
|
|
118
|
-
* Not retryable — retrying won't make the row appear.
|
|
119
|
-
*/
|
|
120
|
-
declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
|
|
121
|
-
readonly aggregateType: string;
|
|
122
|
-
readonly id: string;
|
|
123
|
-
constructor(aggregateType: string, id: string, cause?: unknown);
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Thrown by `IRepository.save()` when the aggregate's expected version
|
|
127
|
-
* does not match the version currently persisted — i.e. another writer
|
|
128
|
-
* updated the aggregate concurrently. The canonical optimistic-
|
|
129
|
-
* concurrency signal; the App-Service typically reloads, re-applies
|
|
130
|
-
* the use case, and retries, or surfaces HTTP 409 to the caller.
|
|
131
|
-
*
|
|
132
|
-
* `InfrastructureError` because the persistence layer (not a domain
|
|
133
|
-
* rule) detects the race. Marks itself as `retryable: true` so the
|
|
134
|
-
* `isRetryable` predicate from `@shirudo/base-error` picks it up.
|
|
135
|
-
*/
|
|
136
|
-
declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
|
|
137
|
-
readonly aggregateType: string;
|
|
138
|
-
readonly aggregateId: string;
|
|
139
|
-
readonly expectedVersion: number;
|
|
140
|
-
readonly actualVersion: number;
|
|
141
|
-
/**
|
|
142
|
-
* Marks this error as retryable so `isRetryable(err)` returns
|
|
143
|
-
* true. The canonical OCC pattern is to reload the aggregate, re-apply
|
|
144
|
-
* the use case, and retry on this exception.
|
|
145
|
-
*/
|
|
146
|
-
readonly retryable: true;
|
|
147
|
-
constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Factory function producing a fresh, unique event identifier for each call.
|
|
152
|
-
*
|
|
153
|
-
* The library ships a default that uses Web Crypto `crypto.randomUUID()`
|
|
154
|
-
* (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
|
|
155
|
-
* Cloudflare Workers, Vercel Edge, and any runtime that implements Web
|
|
156
|
-
* Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
|
|
157
|
-
* random) — for production event stores prefer a **time-ordered** id
|
|
158
|
-
* format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
|
|
159
|
-
* column stay clustered and `ORDER BY eventId` matches creation order.
|
|
160
|
-
* Swap one in via `setEventIdFactory(() => uuidv7())` or `() => ulid()`.
|
|
161
|
-
*/
|
|
162
|
-
type EventIdFactory = () => string;
|
|
163
|
-
/**
|
|
164
|
-
* Replaces the global event-id factory used by `createDomainEvent` and
|
|
165
|
-
* `createDomainEventWithMetadata`. Call once during application bootstrap,
|
|
166
|
-
* for example:
|
|
167
|
-
*
|
|
168
|
-
* ```ts
|
|
169
|
-
* import { ulid } from "ulid";
|
|
170
|
-
* import { setEventIdFactory } from "@shirudo/ddd-kit";
|
|
171
|
-
*
|
|
172
|
-
* setEventIdFactory(() => ulid());
|
|
173
|
-
* ```
|
|
174
|
-
*
|
|
175
|
-
* The per-call `options.eventId` override always wins over this factory.
|
|
176
|
-
*
|
|
177
|
-
* **Module-scoped — last setter wins.** The factory lives as a single
|
|
178
|
-
* module variable; importing two libraries that both call this races on
|
|
179
|
-
* load order, and parallel test workers will see each other's factory.
|
|
180
|
-
* For test isolation and short-lived contexts prefer
|
|
181
|
-
* {@link withEventIdFactory}; for multi-tenant request isolation
|
|
182
|
-
* (e.g. one factory per tenant in a single Worker invocation) **prefer
|
|
183
|
-
* the per-call `options.eventId`** instead of mutating the global. Same
|
|
184
|
-
* caveat applies to `setClockFactory`.
|
|
185
|
-
*/
|
|
186
|
-
declare function setEventIdFactory(factory: EventIdFactory): void;
|
|
187
|
-
/**
|
|
188
|
-
* Scoped variant of {@link setEventIdFactory}: installs `factory`,
|
|
189
|
-
* runs `fn`, then restores the previous factory in a `finally` block —
|
|
190
|
-
* so the restoration happens even if `fn` throws. Safe for parallel
|
|
191
|
-
* tests and for synchronous request handlers that need a tenant-
|
|
192
|
-
* specific factory without polluting the global.
|
|
193
|
-
*
|
|
194
|
-
* **Synchronous-only — enforced at runtime.** If `fn` returns a
|
|
195
|
-
* thenable (a `Promise` or any object with a `then` method), the
|
|
196
|
-
* helper throws *before* returning the value to the caller. This
|
|
197
|
-
* catches the async-misuse footgun where the factory would be
|
|
198
|
-
* restored before the awaited body of `fn` runs, leaving the awaited
|
|
199
|
-
* code reading the previous factory. For async scoping across `await`
|
|
200
|
-
* boundaries, use `AsyncLocalStorage` — out of scope for this helper;
|
|
201
|
-
* build it on top if you need it.
|
|
202
|
-
*
|
|
203
|
-
* Composes by nesting: an inner `withEventIdFactory` restores back to
|
|
204
|
-
* the outer's factory; the outer restores to the original.
|
|
205
|
-
*
|
|
206
|
-
* **When to prefer the per-call `options.eventId` instead.** If you're
|
|
207
|
-
* constructing a single event and want full control over its id,
|
|
208
|
-
* passing `{ eventId: "..." }` to `createDomainEvent` is the strongest
|
|
209
|
-
* isolation — it bypasses the factory mechanism entirely, no global
|
|
210
|
-
* mutation, no scope to manage. Reach for `withEventIdFactory` when
|
|
211
|
-
* the events are constructed deep inside domain methods you can't
|
|
212
|
-
* thread an explicit id through (typical test scenario), or when many
|
|
213
|
-
* events in a scope should share the same factory.
|
|
214
|
-
*
|
|
215
|
-
* @example
|
|
216
|
-
* ```ts
|
|
217
|
-
* // In a vitest test:
|
|
218
|
-
* it("emits deterministic ids", () => {
|
|
219
|
-
* withEventIdFactory(() => "evt-fixed", () => {
|
|
220
|
-
* const e = createDomainEvent("X", { v: 1 });
|
|
221
|
-
* expect(e.eventId).toBe("evt-fixed");
|
|
222
|
-
* });
|
|
223
|
-
* // Outside the callback the default crypto.randomUUID is restored,
|
|
224
|
-
* // even if the body had thrown.
|
|
225
|
-
* });
|
|
226
|
-
* ```
|
|
227
|
-
*/
|
|
228
|
-
declare function withEventIdFactory<T>(factory: EventIdFactory, fn: () => T): T;
|
|
229
|
-
/**
|
|
230
|
-
* Restores the default event-id factory (`crypto.randomUUID()`).
|
|
231
|
-
* Intended for use in test `afterEach` hooks.
|
|
232
|
-
*/
|
|
233
|
-
declare function resetEventIdFactory(): void;
|
|
234
|
-
/**
|
|
235
|
-
* Clock function producing a fresh `Date` for each call. The library
|
|
236
|
-
* defaults to `() => new Date()`; override globally via `setClockFactory`
|
|
237
|
-
* for deterministic event-sourcing tests, time-travel debugging, or any
|
|
238
|
-
* scenario where `occurredAt` must be reproducible.
|
|
239
|
-
*/
|
|
240
|
-
type ClockFactory = () => Date;
|
|
241
|
-
/**
|
|
242
|
-
* Replaces the global clock factory used by `createDomainEvent` and
|
|
243
|
-
* `createDomainEventWithMetadata`. Call once during application bootstrap
|
|
244
|
-
* (or per-test in deterministic test suites):
|
|
245
|
-
*
|
|
246
|
-
* ```ts
|
|
247
|
-
* import { setClockFactory } from "@shirudo/ddd-kit";
|
|
248
|
-
*
|
|
249
|
-
* setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
|
|
250
|
-
* ```
|
|
251
|
-
*
|
|
252
|
-
* The per-call `options.occurredAt` override always wins over this
|
|
253
|
-
* factory. Symmetric to `setEventIdFactory`.
|
|
254
|
-
*
|
|
255
|
-
* Module-scoped — see {@link setEventIdFactory} for the global-state
|
|
256
|
-
* caveats. For test isolation prefer {@link withClockFactory}; for
|
|
257
|
-
* multi-tenant request isolation prefer the per-call
|
|
258
|
-
* `options.occurredAt`.
|
|
259
|
-
*/
|
|
260
|
-
declare function setClockFactory(factory: ClockFactory): void;
|
|
261
|
-
/**
|
|
262
|
-
* Scoped variant of {@link setClockFactory}: installs `factory`, runs
|
|
263
|
-
* `fn`, then restores the previous factory in a `finally` block.
|
|
264
|
-
* Synchronous-only — same constraints (and same runtime thenable
|
|
265
|
-
* guard) as {@link withEventIdFactory}.
|
|
266
|
-
*
|
|
267
|
-
* **When to prefer the per-call `options.occurredAt` instead.** Same
|
|
268
|
-
* trade-off as {@link withEventIdFactory}: passing `{ occurredAt }`
|
|
269
|
-
* to `createDomainEvent` is the strongest isolation for single-event
|
|
270
|
-
* cases. The scoped helper is for events constructed deep inside
|
|
271
|
-
* domain methods where threading an explicit timestamp is awkward.
|
|
272
|
-
*
|
|
273
|
-
* @example
|
|
274
|
-
* ```ts
|
|
275
|
-
* it("stamps events with a fixed clock", () => {
|
|
276
|
-
* const fixed = new Date("2026-01-01T00:00:00Z");
|
|
277
|
-
* withClockFactory(() => fixed, () => {
|
|
278
|
-
* const e = createDomainEvent("X", { v: 1 });
|
|
279
|
-
* expect(e.occurredAt).toEqual(fixed);
|
|
280
|
-
* });
|
|
281
|
-
* });
|
|
282
|
-
* ```
|
|
283
|
-
*/
|
|
284
|
-
declare function withClockFactory<T>(factory: ClockFactory, fn: () => T): T;
|
|
285
|
-
/**
|
|
286
|
-
* Restores the default clock factory (`() => new Date()`).
|
|
287
|
-
* Intended for use in test `afterEach` hooks.
|
|
288
|
-
*/
|
|
289
|
-
declare function resetClockFactory(): void;
|
|
290
|
-
/**
|
|
291
|
-
* Metadata associated with a domain event for traceability and correlation.
|
|
292
|
-
* Used in event-driven architectures to track event flow across services.
|
|
293
|
-
*/
|
|
294
|
-
interface EventMetadata {
|
|
295
|
-
/**
|
|
296
|
-
* Correlation ID for tracing events across multiple services/components.
|
|
297
|
-
* Typically used to group related events in a distributed system.
|
|
298
|
-
*/
|
|
299
|
-
correlationId?: string;
|
|
300
|
-
/**
|
|
301
|
-
* Causation ID referencing the event or command that caused this event.
|
|
302
|
-
* Used to build event chains and understand causality.
|
|
303
|
-
*/
|
|
304
|
-
causationId?: string;
|
|
305
|
-
/**
|
|
306
|
-
* User ID of the person or system that triggered the event.
|
|
307
|
-
*/
|
|
308
|
-
userId?: string;
|
|
309
|
-
/**
|
|
310
|
-
* Source service or component that produced the event.
|
|
311
|
-
*/
|
|
312
|
-
source?: string;
|
|
313
|
-
/**
|
|
314
|
-
* Additional custom metadata fields.
|
|
315
|
-
* Allows extensibility for domain-specific metadata.
|
|
316
|
-
*/
|
|
317
|
-
[key: string]: unknown;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Domain Event represents something meaningful that happened in the domain.
|
|
321
|
-
* Events are immutable and carry information about what occurred.
|
|
322
|
-
*
|
|
323
|
-
* @template T - The event type name (e.g., "OrderCreated")
|
|
324
|
-
* @template P - The event payload type
|
|
325
|
-
*/
|
|
326
|
-
interface DomainEvent<T extends string, P = void> {
|
|
327
|
-
/**
|
|
328
|
-
* Unique identifier for this specific event instance. Used by idempotent
|
|
329
|
-
* consumers, outbox dispatch tracking, and as the target of
|
|
330
|
-
* `metadata.causationId`. Defaults to `crypto.randomUUID()` if not
|
|
331
|
-
* supplied.
|
|
332
|
-
*/
|
|
333
|
-
eventId: string;
|
|
334
|
-
/**
|
|
335
|
-
* The type of the event, used for routing and handling.
|
|
336
|
-
*/
|
|
337
|
-
type: T;
|
|
338
|
-
/**
|
|
339
|
-
* Identifier of the aggregate that produced the event. Optional at the
|
|
340
|
-
* library level — set it whenever the producing aggregate is known so
|
|
341
|
-
* downstream subscribers, outboxes, and projections can scope by entity.
|
|
342
|
-
*/
|
|
343
|
-
aggregateId?: string;
|
|
344
|
-
/**
|
|
345
|
-
* Name of the aggregate type that produced the event (e.g. "Order").
|
|
346
|
-
* Pairs with `aggregateId` to fully qualify the source aggregate.
|
|
347
|
-
*/
|
|
348
|
-
aggregateType?: string;
|
|
349
|
-
/**
|
|
350
|
-
* The event payload containing the domain data. The field is always
|
|
351
|
-
* present; its value is `undefined` when `P` is `void`.
|
|
352
|
-
*/
|
|
353
|
-
payload: P;
|
|
354
|
-
/**
|
|
355
|
-
* Timestamp when the event occurred.
|
|
356
|
-
*/
|
|
357
|
-
occurredAt: Date;
|
|
358
|
-
/**
|
|
359
|
-
* Event schema version for handling schema evolution.
|
|
360
|
-
* Required for safe schema migration in event-sourced systems.
|
|
361
|
-
* Use 1 for the initial schema version.
|
|
362
|
-
*/
|
|
363
|
-
version: number;
|
|
364
|
-
/**
|
|
365
|
-
* Optional metadata for traceability, correlation, and auditing.
|
|
366
|
-
* Includes correlationId, causationId, userId, source, and custom fields.
|
|
367
|
-
*/
|
|
368
|
-
metadata?: EventMetadata;
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Upper-bound alias for "any `DomainEvent` shape". Use as a generic
|
|
372
|
-
* constraint when a type parameter should accept any concrete event
|
|
373
|
-
* union. The `unknown` payload is the upper bound — concrete unions
|
|
374
|
-
* still narrow via `Extract<Evt, { type: K }>` at the use-site.
|
|
375
|
-
*/
|
|
376
|
-
type AnyDomainEvent = DomainEvent<string, unknown>;
|
|
377
|
-
/**
|
|
378
|
-
* Shared option bag for the `createDomainEvent*` factories.
|
|
379
|
-
*/
|
|
380
|
-
interface CreateDomainEventOptions {
|
|
381
|
-
/**
|
|
382
|
-
* Override for the auto-generated `eventId`. Pass an existing id (for
|
|
383
|
-
* replay, tests, or deterministic event sourcing) instead of letting the
|
|
384
|
-
* factory call `crypto.randomUUID()`.
|
|
385
|
-
*/
|
|
386
|
-
eventId?: string;
|
|
387
|
-
/**
|
|
388
|
-
* Identifier of the aggregate that produced the event.
|
|
389
|
-
*/
|
|
390
|
-
aggregateId?: string;
|
|
391
|
-
/**
|
|
392
|
-
* Name of the aggregate type that produced the event.
|
|
393
|
-
*/
|
|
394
|
-
aggregateType?: string;
|
|
395
|
-
/**
|
|
396
|
-
* Override for the auto-generated `occurredAt` timestamp.
|
|
397
|
-
*/
|
|
398
|
-
occurredAt?: Date;
|
|
399
|
-
/**
|
|
400
|
-
* Override for the default schema version (1).
|
|
401
|
-
*/
|
|
402
|
-
version?: number;
|
|
403
|
-
/**
|
|
404
|
-
* Event metadata — correlation, causation, user, source, custom fields.
|
|
405
|
-
*/
|
|
406
|
-
metadata?: EventMetadata;
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Creates a domain event with default values.
|
|
410
|
-
* Sets occurredAt to current date and version to 1 if not provided.
|
|
411
|
-
*
|
|
412
|
-
* **For aggregate-internal events, prefer `this.recordEvent(...)` on
|
|
413
|
-
* `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
|
|
414
|
-
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
415
|
-
* aggregate's declared `aggregateType` property), which downstream
|
|
416
|
-
* consumers — outbox dispatchers, projection handlers, audit logs —
|
|
417
|
-
* route by. The `withCommit` harvest boundary now validates both fields
|
|
418
|
-
* are present and throws if they're missing, so a direct
|
|
419
|
-
* `createDomainEvent(...)` call inside an aggregate that forgets the
|
|
420
|
-
* options is caught at runtime.
|
|
421
|
-
*
|
|
422
|
-
* Use `createDomainEvent(...)` directly for events that don't belong to
|
|
423
|
-
* an aggregate: system events, integration events, configuration events,
|
|
424
|
-
* test fixtures. For those, set `aggregateId` / `aggregateType` in
|
|
425
|
-
* `options` if downstream consumers expect routing metadata.
|
|
426
|
-
*
|
|
427
|
-
* @param type - The event type
|
|
428
|
-
* @param payload - The event payload
|
|
429
|
-
* @param options - Optional event configuration (including `aggregateId`
|
|
430
|
-
* and `aggregateType` for routing)
|
|
431
|
-
* @returns A domain event
|
|
432
|
-
*
|
|
433
|
-
* @example
|
|
434
|
-
* ```typescript
|
|
435
|
-
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
436
|
-
* ```
|
|
437
|
-
*/
|
|
438
|
-
declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
|
|
439
|
-
declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
|
|
440
|
-
/**
|
|
441
|
-
* Creates a domain event with metadata for traceability.
|
|
442
|
-
* Convenience function for creating events with correlation and causation IDs.
|
|
443
|
-
*
|
|
444
|
-
* @example
|
|
445
|
-
* ```typescript
|
|
446
|
-
* const event = createDomainEventWithMetadata(
|
|
447
|
-
* "OrderCreated",
|
|
448
|
-
* { orderId: "123" },
|
|
449
|
-
* { correlationId: "corr-123", causationId: "cmd-456", userId: "user-789" }
|
|
450
|
-
* );
|
|
451
|
-
* ```
|
|
452
|
-
*/
|
|
453
|
-
declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
|
|
454
|
-
/**
|
|
455
|
-
* Copies metadata from a source event to a new event.
|
|
456
|
-
* Useful for maintaining correlation chains in event-driven architectures.
|
|
457
|
-
*
|
|
458
|
-
* @example
|
|
459
|
-
* ```typescript
|
|
460
|
-
* const newEvent = createDomainEvent(
|
|
461
|
-
* "OrderShipped",
|
|
462
|
-
* { orderId: "123" },
|
|
463
|
-
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.type }) }
|
|
464
|
-
* );
|
|
465
|
-
* ```
|
|
466
|
-
*/
|
|
467
|
-
declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
|
|
468
|
-
/**
|
|
469
|
-
* Merges multiple metadata objects into one.
|
|
470
|
-
* Later metadata objects override earlier ones for the same keys.
|
|
471
|
-
*
|
|
472
|
-
* @example
|
|
473
|
-
* ```typescript
|
|
474
|
-
* const metadata = mergeMetadata(
|
|
475
|
-
* { correlationId: "corr-123" },
|
|
476
|
-
* { userId: "user-456" },
|
|
477
|
-
* { source: "order-service" }
|
|
478
|
-
* );
|
|
479
|
-
* ```
|
|
480
|
-
*/
|
|
481
|
-
declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
|
|
482
|
-
|
|
483
|
-
type Version = number & {
|
|
484
|
-
readonly __v: true;
|
|
485
|
-
};
|
|
486
|
-
/**
|
|
487
|
-
* Snapshot of an aggregate state at a specific point in time.
|
|
488
|
-
* Used for optimizing event replay by starting from a snapshot
|
|
489
|
-
* instead of replaying all events from the beginning.
|
|
490
|
-
*
|
|
491
|
-
* @template TState - The type of the aggregate state
|
|
492
|
-
*/
|
|
493
|
-
interface AggregateSnapshot<TState> {
|
|
494
|
-
/**
|
|
495
|
-
* The state of the aggregate at the time of the snapshot.
|
|
496
|
-
*/
|
|
497
|
-
state: TState;
|
|
498
|
-
/**
|
|
499
|
-
* The version of the aggregate when the snapshot was taken.
|
|
500
|
-
*/
|
|
501
|
-
version: Version;
|
|
502
|
-
/**
|
|
503
|
-
* Timestamp when the snapshot was created.
|
|
504
|
-
*/
|
|
505
|
-
snapshotAt: Date;
|
|
506
|
-
}
|
|
507
|
-
/**
|
|
508
|
-
* Public contract every Aggregate Root satisfies. Implemented by
|
|
509
|
-
* `BaseAggregate` and inherited by both `AggregateRoot` and
|
|
510
|
-
* `EventSourcedAggregate`. Repository implementations type their
|
|
511
|
-
* `save(aggregate)` parameter against this interface rather than the
|
|
512
|
-
* concrete classes, so the repo layer does not take a compile-time
|
|
513
|
-
* dependency on the aggregate hierarchy.
|
|
514
|
-
*
|
|
515
|
-
* Full per-member documentation lives on the concrete `BaseAggregate`
|
|
516
|
-
* class; the interface is intentionally terse to avoid drift.
|
|
517
|
-
*
|
|
518
|
-
* @template TId - The aggregate root identifier (branded via `Id<Tag>`)
|
|
519
|
-
* @template TEvent - The domain-event union, defaults to `never`
|
|
520
|
-
*/
|
|
521
|
-
interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
|
|
522
|
-
readonly id: TId;
|
|
523
|
-
readonly version: Version;
|
|
524
|
-
readonly persistedVersion: Version | undefined;
|
|
525
|
-
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
526
|
-
clearPendingEvents(): void;
|
|
527
|
-
markPersisted(version: Version): void;
|
|
528
|
-
}
|
|
529
|
-
/**
|
|
530
|
-
* Public contract for Event-Sourced Aggregate Roots. Extends
|
|
531
|
-
* `IAggregateRoot` with the replay-from-history boundary.
|
|
532
|
-
*
|
|
533
|
-
* @template TId - The aggregate root identifier
|
|
534
|
-
* @template TEvent - The union type of all domain events
|
|
535
|
-
*/
|
|
536
|
-
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
|
|
537
|
-
/**
|
|
538
|
-
* Reconstitutes the aggregate from an event history. Returns
|
|
539
|
-
* `Result` because event-stream corruption is an expected
|
|
540
|
-
* recoverable failure at the infrastructure boundary.
|
|
541
|
-
*/
|
|
542
|
-
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* Checks if two aggregates are at the same version (same ID and version).
|
|
546
|
-
* Useful for optimistic concurrency control checks.
|
|
547
|
-
*
|
|
548
|
-
* Note: Two aggregates with the same ID ARE the same aggregate (identity).
|
|
549
|
-
* This function checks if they are at the same version — i.e., no concurrent modification.
|
|
550
|
-
*
|
|
551
|
-
* @example
|
|
552
|
-
* ```typescript
|
|
553
|
-
* const before = await repository.getById(id);
|
|
554
|
-
* // ... some operations ...
|
|
555
|
-
* const after = await repository.getById(id);
|
|
556
|
-
*
|
|
557
|
-
* if (!sameVersion(before, after)) {
|
|
558
|
-
* throw new Error("Aggregate was modified by another process");
|
|
559
|
-
* }
|
|
560
|
-
* ```
|
|
561
|
-
*/
|
|
562
|
-
declare function sameVersion<TId extends Id<string>>(a: {
|
|
563
|
-
id: TId;
|
|
564
|
-
version: Version;
|
|
565
|
-
}, b: {
|
|
566
|
-
id: TId;
|
|
567
|
-
version: Version;
|
|
568
|
-
}): boolean;
|
|
569
|
-
|
|
570
8
|
/**
|
|
571
9
|
* Entity utilities and interfaces for Domain-Driven Design.
|
|
572
10
|
*
|
|
@@ -625,7 +63,7 @@ declare function sameVersion<TId extends Id<string>>(a: {
|
|
|
625
63
|
*/
|
|
626
64
|
|
|
627
65
|
/**
|
|
628
|
-
* Functional definition of an Entity via its capability
|
|
66
|
+
* Functional definition of an Entity via its capability: an object is
|
|
629
67
|
* identifiable if it has an `id`.
|
|
630
68
|
*
|
|
631
69
|
* `TId` is constrained to `Id<string>` so the brand discipline that
|
|
@@ -694,7 +132,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
694
132
|
/**
|
|
695
133
|
* Returns the current state of the entity.
|
|
696
134
|
*
|
|
697
|
-
* The state object is **shallowly frozen
|
|
135
|
+
* The state object is **shallowly frozen**: direct property writes
|
|
698
136
|
* (`entity.state.foo = …`) throw in strict mode, but writes to nested
|
|
699
137
|
* objects (`entity.state.address.zip = …`) bypass the freeze. For deep
|
|
700
138
|
* immutability either model nested data with `vo()` (which freezes
|
|
@@ -713,7 +151,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
713
151
|
* **State ownership.** Plain-object and array states are shallow-copied
|
|
714
152
|
* before the freeze, so the caller's own object stays mutable. A CLASS
|
|
715
153
|
* INSTANCE passed as state is an ownership transfer: it is frozen
|
|
716
|
-
* in place (a copy would strip its prototype)
|
|
154
|
+
* in place (a copy would strip its prototype). Do not keep mutating
|
|
717
155
|
* the instance after handing it to the entity. The same contract
|
|
718
156
|
* applies to {@link setState}.
|
|
719
157
|
*/
|
|
@@ -726,7 +164,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
726
164
|
* **⚠️ Must not read subclass instance fields via `this`.** The
|
|
727
165
|
* constructor calls `validateState(initialState)` BEFORE the subclass's
|
|
728
166
|
* field initializers run, so `this.someField` is `undefined` at that
|
|
729
|
-
* point
|
|
167
|
+
* point, a classic TypeScript/JavaScript constructor-ordering footgun.
|
|
730
168
|
* The `state` argument is the single source of truth; treat the method
|
|
731
169
|
* as pure with respect to `this`.
|
|
732
170
|
*
|
|
@@ -747,7 +185,7 @@ declare abstract class Entity<TState, TId extends Id<string>> implements IEntity
|
|
|
747
185
|
*
|
|
748
186
|
* Plain-object and array states are shallow-copied before the freeze
|
|
749
187
|
* (the caller's object stays mutable); a class-instance state is an
|
|
750
|
-
* ownership transfer and is frozen in place
|
|
188
|
+
* ownership transfer and is frozen in place; see the constructor.
|
|
751
189
|
*
|
|
752
190
|
* @param newState - The new state
|
|
753
191
|
*/
|
|
@@ -917,7 +355,7 @@ declare function entityIds<TId extends Id<string>, T extends Identifiable<TId>>(
|
|
|
917
355
|
* `recordEvent` helper that auto-injects `aggregateId` +
|
|
918
356
|
* `aggregateType` on every event the aggregate emits.
|
|
919
357
|
*
|
|
920
|
-
* Consumers do NOT extend this class directly
|
|
358
|
+
* Consumers do NOT extend this class directly; extend
|
|
921
359
|
* `AggregateRoot` for state-stored aggregates or
|
|
922
360
|
* `EventSourcedAggregate` for event-sourced ones. The split between
|
|
923
361
|
* those two reflects the canonical Vernon §8 (state-stored) /
|
|
@@ -949,7 +387,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
949
387
|
*
|
|
950
388
|
* The string is *the* identifier downstream consumers (outbox
|
|
951
389
|
* dispatchers, projection handlers, audit logs) use to route by
|
|
952
|
-
* aggregate kind. Use the same canonical name across your system
|
|
390
|
+
* aggregate kind. Use the same canonical name across your system;
|
|
953
391
|
* matching the class name is the obvious choice, but the value
|
|
954
392
|
* comes from this explicit declaration, not `constructor.name`
|
|
955
393
|
* (which is fragile under minification, bundler transforms, and
|
|
@@ -965,7 +403,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
965
403
|
*
|
|
966
404
|
* Distinct from {@link version}, which is the in-memory
|
|
967
405
|
* post-mutation value. Mutations bump `_version` but never touch
|
|
968
|
-
* `_persistedVersion
|
|
406
|
+
* `_persistedVersion`; that field only moves on {@link markRestored}
|
|
969
407
|
* (Post-Load) and {@link markPersisted} (Post-Save).
|
|
970
408
|
*/
|
|
971
409
|
private _persistedVersion;
|
|
@@ -979,7 +417,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
979
417
|
get pendingEvents(): ReadonlyArray<TEvent>;
|
|
980
418
|
/**
|
|
981
419
|
* Clears the pending-event list. Called by `markPersisted` after a
|
|
982
|
-
* successful write
|
|
420
|
+
* successful write: the events have been handed off to the outbox
|
|
983
421
|
* / event store and are no longer the aggregate's responsibility.
|
|
984
422
|
*/
|
|
985
423
|
clearPendingEvents(): void;
|
|
@@ -991,16 +429,26 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
991
429
|
*/
|
|
992
430
|
protected bumpVersion(): void;
|
|
993
431
|
/**
|
|
994
|
-
* **Lifecycle marker
|
|
432
|
+
* **Lifecycle marker, Post-Load.** Syncs both `_version` and
|
|
995
433
|
* `_persistedVersion` to the DB-stored version. Used by
|
|
996
434
|
* `reconstitute(...)` factories to assemble an in-memory aggregate
|
|
997
435
|
* from a persisted row.
|
|
998
436
|
*
|
|
999
|
-
* Does NOT fire {@link onPersisted}
|
|
437
|
+
* Does NOT fire {@link onPersisted}; that hook has post-save
|
|
1000
438
|
* semantics (metrics, audit, cache eviction), not post-load. The
|
|
1001
439
|
* Factory-vs-Reconstitution distinction (Vernon §11) is honoured
|
|
1002
440
|
* structurally: two separate markers, one for each transition.
|
|
1003
441
|
*
|
|
442
|
+
* **If you override this, call `super.markRestored(version)` FIRST**,
|
|
443
|
+
* same discipline as {@link markPersisted}. The marker is load-bearing
|
|
444
|
+
* twice over: it syncs `version`/`persistedVersion`, and on
|
|
445
|
+
* `AggregateRoot` it also captures the dirty-tracking baseline for
|
|
446
|
+
* `changedKeys`/`hasChanges`. An override that skips `super` leaves
|
|
447
|
+
* that baseline uncaptured: `changedKeys` permanently reports ALL
|
|
448
|
+
* keys and `hasChanges` never returns `false`, so a partial-write
|
|
449
|
+
* repository silently degrades to full writes on every save — on top
|
|
450
|
+
* of the broken version sync.
|
|
451
|
+
*
|
|
1004
452
|
* @param version - The version the row currently holds in the DB
|
|
1005
453
|
*
|
|
1006
454
|
* @example
|
|
@@ -1014,14 +462,14 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1014
462
|
*/
|
|
1015
463
|
protected markRestored(version: Version): void;
|
|
1016
464
|
/**
|
|
1017
|
-
* **Framework lifecycle method
|
|
465
|
+
* **Framework lifecycle method (`@sealed`).** Called by `withCommit`
|
|
1018
466
|
* (or by your own orchestration code, after harvesting `pendingEvents`)
|
|
1019
467
|
* to push the persisted version back into the in-memory aggregate and
|
|
1020
468
|
* clear `pendingEvents`. TypeScript has no `final` keyword, but
|
|
1021
469
|
* subclasses **should not** override this method directly.
|
|
1022
470
|
*
|
|
1023
471
|
* Overriding without calling `super.markPersisted(version)` silently
|
|
1024
|
-
* leaks `pendingEvents
|
|
472
|
+
* leaks `pendingEvents`: the next `withCommit` will re-dispatch them
|
|
1025
473
|
* through the outbox, double-emitting events. This bug has been hit
|
|
1026
474
|
* in production by consumers; the {@link onPersisted} hook below is
|
|
1027
475
|
* the safer extension point.
|
|
@@ -1031,17 +479,17 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1031
479
|
* runs, then add your logic afterwards.
|
|
1032
480
|
*
|
|
1033
481
|
* @param version - The version assigned by the persistence layer
|
|
1034
|
-
* @see onPersisted
|
|
482
|
+
* @see onPersisted, the safe extension point for subclasses
|
|
1035
483
|
*/
|
|
1036
484
|
markPersisted(version: Version): void;
|
|
1037
485
|
/**
|
|
1038
|
-
* Subclass extension point
|
|
486
|
+
* Subclass extension point: fires AFTER {@link markPersisted} has
|
|
1039
487
|
* updated the version and cleared `pendingEvents`. Override this for
|
|
1040
488
|
* post-persist logging, metrics, or cache-eviction without risk of
|
|
1041
489
|
* breaking the framework's pendingEvents cleanup.
|
|
1042
490
|
*
|
|
1043
491
|
* The default implementation is a no-op. Subclasses do NOT need to
|
|
1044
|
-
* call `super.onPersisted(version)
|
|
492
|
+
* call `super.onPersisted(version)`: there is nothing in the parent
|
|
1045
493
|
* implementation to preserve.
|
|
1046
494
|
*
|
|
1047
495
|
* **Observer contract: errors are swallowed.** `withCommit` invokes
|
|
@@ -1053,7 +501,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1053
501
|
* **`onPersisted` deliberately receives only the version, not the
|
|
1054
502
|
* drained events.** Event-driven post-persist logic (aggregate-level
|
|
1055
503
|
* audit logging, per-event-type side effects) belongs in `EventBus`
|
|
1056
|
-
* subscribers or the outbox dispatcher
|
|
504
|
+
* subscribers or the outbox dispatcher; that is the proper
|
|
1057
505
|
* Aggregate-Boundary separation. Building event-aware logic into
|
|
1058
506
|
* `onPersisted` couples aggregate lifecycle to event processing and
|
|
1059
507
|
* recreates the boundary problems Vernon's aggregate discipline is
|
|
@@ -1062,7 +510,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1062
510
|
* **The hook must return synchronously.** `markPersisted` is `void`-
|
|
1063
511
|
* typed and calls `onPersisted` without `await`. TypeScript's
|
|
1064
512
|
* permissive `void` will accept an `async`-override returning
|
|
1065
|
-
* `Promise<void>`, but the returned promise is fire-and-forget
|
|
513
|
+
* `Promise<void>`, but the returned promise is fire-and-forget:
|
|
1066
514
|
* any rejection becomes an unhandled rejection and `withCommit`
|
|
1067
515
|
* proceeds without waiting. For asynchronous work, subscribe to the
|
|
1068
516
|
* relevant domain event on the `EventBus` instead; that is the
|
|
@@ -1074,7 +522,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1074
522
|
/**
|
|
1075
523
|
* Appends a domain event to the pending list. Prefer the higher-level
|
|
1076
524
|
* `AggregateRoot.commit()` (state-stored) or `EventSourcedAggregate.apply()`
|
|
1077
|
-
* (event-sourced) call sites
|
|
525
|
+
* (event-sourced) call sites, both of which wrap `addDomainEvent` in the
|
|
1078
526
|
* canonical record-AFTER-mutation order (Vernon §8). Calling
|
|
1079
527
|
* `addDomainEvent` directly is appropriate only when state and event
|
|
1080
528
|
* recording have already been decoupled deliberately (e.g. a
|
|
@@ -1082,7 +530,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1082
530
|
*/
|
|
1083
531
|
protected addDomainEvent(event: TEvent): void;
|
|
1084
532
|
/**
|
|
1085
|
-
* Creates a snapshot of the current aggregate state
|
|
533
|
+
* Creates a snapshot of the current aggregate state: the state at
|
|
1086
534
|
* this moment plus the version. Useful for ES snapshot policies and
|
|
1087
535
|
* for state-stored backup / restore.
|
|
1088
536
|
*
|
|
@@ -1094,7 +542,7 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1094
542
|
* Converts live aggregate state into the plain-data shape stored in a
|
|
1095
543
|
* snapshot. The default validates that the state graph is plain,
|
|
1096
544
|
* serialisable data (no class instances, functions, Promise/WeakMap/
|
|
1097
|
-
* WeakSet) and then `structuredClone`s it
|
|
545
|
+
* WeakSet) and then `structuredClone`s it: class instances would
|
|
1098
546
|
* silently lose their prototype here AND on every snapshot-store
|
|
1099
547
|
* round-trip, so the default fails fast with the offending path
|
|
1100
548
|
* instead of producing a snapshot that breaks on first method call
|
|
@@ -1120,8 +568,8 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1120
568
|
* into the event's metadata fields. This is the canonical path for
|
|
1121
569
|
* recording events from inside aggregate domain methods.
|
|
1122
570
|
*
|
|
1123
|
-
* Downstream consumers
|
|
1124
|
-
* audit logs
|
|
571
|
+
* Downstream consumers (outbox dispatchers, projection handlers,
|
|
572
|
+
* audit logs) route by these two fields. Calling
|
|
1125
573
|
* `createDomainEvent(...)` directly inside an aggregate method
|
|
1126
574
|
* leaves them unset and is caught at the `withCommit` harvest
|
|
1127
575
|
* boundary, but `this.recordEvent(...)` makes the right thing
|
|
@@ -1145,8 +593,8 @@ declare abstract class BaseAggregate<TState, TId extends Id<string>, TEvent exte
|
|
|
1145
593
|
* @param payload - payload for that event subtype
|
|
1146
594
|
* @param options - any remaining `createDomainEvent` options
|
|
1147
595
|
* (`eventId`, `occurredAt`, `metadata`, `version`); `aggregateId`
|
|
1148
|
-
* and `aggregateType` are deliberately omitted
|
|
1149
|
-
* them.
|
|
596
|
+
* and `aggregateType` are deliberately omitted, because the helper
|
|
597
|
+
* sets them.
|
|
1150
598
|
*/
|
|
1151
599
|
protected recordEvent<E extends TEvent>(type: E["type"], payload: E["payload"], options?: Omit<CreateDomainEventOptions, "aggregateId" | "aggregateType">): E;
|
|
1152
600
|
}
|
|
@@ -1159,7 +607,7 @@ interface AggregateConfig {
|
|
|
1159
607
|
* Whether `setState()` should bump the version automatically when the
|
|
1160
608
|
* caller omits the per-call `bumpVersion` argument.
|
|
1161
609
|
*
|
|
1162
|
-
* Defaults to **`false
|
|
610
|
+
* Defaults to **`false`**: `setState()` already takes an explicit
|
|
1163
611
|
* `bumpVersion` argument per call, so the config is just the default
|
|
1164
612
|
* the per-call argument falls back to. Set to `true` only if you have
|
|
1165
613
|
* a subclass that never passes `bumpVersion` and you want every state
|
|
@@ -1170,8 +618,8 @@ interface AggregateConfig {
|
|
|
1170
618
|
/**
|
|
1171
619
|
* Base class for Aggregate Roots without Event Sourcing.
|
|
1172
620
|
*
|
|
1173
|
-
* In DDD (Evans), an Aggregate is a cluster of objects
|
|
1174
|
-
* and value objects
|
|
621
|
+
* In DDD (Evans), an Aggregate is a cluster of objects (root entity, child entities,
|
|
622
|
+
* and value objects) treated as a unit for consistency. The **Aggregate Root** is the
|
|
1175
623
|
* root entity that represents the aggregate externally and is the only entry point
|
|
1176
624
|
* for external code. This class serves as both: it IS the root entity and it contains
|
|
1177
625
|
* the aggregate state (`TState`) which holds child entities and value objects.
|
|
@@ -1190,7 +638,7 @@ interface AggregateConfig {
|
|
|
1190
638
|
*
|
|
1191
639
|
* @template TState - The type of the aggregate state (contains child entities and value objects)
|
|
1192
640
|
* @template TId - The type of the aggregate root identifier
|
|
1193
|
-
* @template TEvent - The type of domain events recorded by this aggregate. Defaults to `never
|
|
641
|
+
* @template TEvent - The type of domain events recorded by this aggregate. Defaults to `never`: aggregates without a declared event type cannot emit events (emitting any event becomes a compile error). Supply a concrete event union to opt in.
|
|
1194
642
|
*
|
|
1195
643
|
* @example
|
|
1196
644
|
* ```typescript
|
|
@@ -1213,7 +661,106 @@ interface AggregateConfig {
|
|
|
1213
661
|
*/
|
|
1214
662
|
declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent extends AnyDomainEvent = never, TSnapshotState = TState> extends BaseAggregate<TState, TId, TEvent, TSnapshotState> {
|
|
1215
663
|
private readonly _autoVersionBump;
|
|
664
|
+
/**
|
|
665
|
+
* The state reference as of the last {@link markRestored} /
|
|
666
|
+
* `markPersisted` (the persistence-lifecycle markers). Only
|
|
667
|
+
* meaningful while {@link _hasBaseline} is `true`; tracked by a
|
|
668
|
+
* separate flag rather than an `undefined` sentinel so a `TState`
|
|
669
|
+
* that itself admits `undefined` cannot be confused with the
|
|
670
|
+
* never-persisted insert path.
|
|
671
|
+
*
|
|
672
|
+
* Held by reference, never copied: `_state` is shallow-frozen and only
|
|
673
|
+
* ever *replaced* (via `setState` / restore), so the captured reference
|
|
674
|
+
* stays an exact image of the state at baseline time.
|
|
675
|
+
*/
|
|
676
|
+
private _baselineState;
|
|
677
|
+
/**
|
|
678
|
+
* `false` until the aggregate has been persisted or restored at least
|
|
679
|
+
* once: the insert path, where every key counts as changed.
|
|
680
|
+
*/
|
|
681
|
+
private _hasBaseline;
|
|
1216
682
|
protected constructor(id: TId, initialState: TState, config?: AggregateConfig);
|
|
683
|
+
/**
|
|
684
|
+
* **Lifecycle marker, Post-Load (see `BaseAggregate.markRestored`).**
|
|
685
|
+
* Additionally captures the current state reference as the dirty-
|
|
686
|
+
* tracking baseline for {@link changedKeys} / {@link hasChanges}.
|
|
687
|
+
*
|
|
688
|
+
* Covers all three baseline-capture paths through a single override:
|
|
689
|
+
* `reconstitute(...)` factories, {@link restoreFromSnapshot} (which
|
|
690
|
+
* assigns the restored state *before* calling this), and
|
|
691
|
+
* `markPersisted` (which delegates here, so a successful save
|
|
692
|
+
* re-baselines the diff).
|
|
693
|
+
*
|
|
694
|
+
* If you override this, call `super.markRestored(version)` FIRST:
|
|
695
|
+
* skipping it leaves the baseline uncaptured, so `changedKeys`
|
|
696
|
+
* permanently reports ALL keys and `hasChanges` never returns `false`
|
|
697
|
+
* — partial-write repositories silently degrade to full writes — on
|
|
698
|
+
* top of breaking version sync.
|
|
699
|
+
*/
|
|
700
|
+
protected markRestored(version: Version): void;
|
|
701
|
+
/**
|
|
702
|
+
* Top-level state keys whose value (or presence) changed since the
|
|
703
|
+
* last {@link markRestored} / `markPersisted`. Never-persisted
|
|
704
|
+
* aggregates report ALL current keys (the insert path).
|
|
705
|
+
*
|
|
706
|
+
* This is the write-scoping signal for **partial writes in multi-table
|
|
707
|
+
* repositories**: a `save()` for an aggregate whose state spans a root
|
|
708
|
+
* row plus N child-collection tables can write only the collections
|
|
709
|
+
* whose key is dirty, while the root-row OCC version write rides every
|
|
710
|
+
* save. See `docs/guide/repository.md` → "Partial writes for
|
|
711
|
+
* multi-table aggregates".
|
|
712
|
+
*
|
|
713
|
+
* **How it works.** `setState()` replaces state immutably and the
|
|
714
|
+
* state object is shallow-frozen, so unchanged top-level sub-objects
|
|
715
|
+
* keep reference identity across mutations. The diff is therefore a
|
|
716
|
+
* shallow per-key `!==` against the baseline reference — O(top-level
|
|
717
|
+
* keys), no proxies, no deep diff. A key also counts as dirty when its
|
|
718
|
+
* *presence* differs (added or removed, even with an `undefined`
|
|
719
|
+
* value). Computed fresh on every access (a new `Set` each time), so
|
|
720
|
+
* callers cannot poison later reads.
|
|
721
|
+
*
|
|
722
|
+
* **Soundness contract (same one `freezeShallow` already makes):**
|
|
723
|
+
* the per-key diff is exact only for plain-record `TState` mutated via
|
|
724
|
+
* `setState` / `commit` (whole-state replacement). In-place mutation
|
|
725
|
+
* of NESTED objects bypasses the shallow freeze AND this diff; a
|
|
726
|
+
* class-instance `TState` mutated through its own methods defeats
|
|
727
|
+
* tracking entirely (the reference never changes). A keyless `TState`
|
|
728
|
+
* (primitive, bare `Date`) has no keys to report, so `changedKeys`
|
|
729
|
+
* stays empty for it — use {@link hasChanges}, whose reference
|
|
730
|
+
* fallback covers keyless states. A deep-equal but newly-referenced
|
|
731
|
+
* value reports a false POSITIVE (harmless extra write); under the
|
|
732
|
+
* contract above there are no false negatives.
|
|
733
|
+
*
|
|
734
|
+
* Granularity is per top-level key — table-granular, not row-granular:
|
|
735
|
+
* a dirty collection key means "this child table changed", not which
|
|
736
|
+
* rows. `EventSourcedAggregate` deliberately has no `changedKeys`;
|
|
737
|
+
* its `pendingEvents` are the change record.
|
|
738
|
+
*/
|
|
739
|
+
get changedKeys(): ReadonlySet<Extract<keyof TState, string>>;
|
|
740
|
+
/**
|
|
741
|
+
* Safe skip signal: `false` only when there is genuinely nothing to
|
|
742
|
+
* persist or flush. `true` when the aggregate has never been
|
|
743
|
+
* persisted, the version moved past `persistedVersion`, there are
|
|
744
|
+
* unflushed {@link pendingEvents}, any state key is dirty, or — for
|
|
745
|
+
* keyless states the per-key diff cannot see (primitive `TState`,
|
|
746
|
+
* zero-own-key objects like a bare `Date`) — the state reference
|
|
747
|
+
* changed since the baseline.
|
|
748
|
+
*
|
|
749
|
+
* The version clause is deliberate: `setState({...state}, true)` with
|
|
750
|
+
* identical per-key values yields empty {@link changedKeys} but a
|
|
751
|
+
* bumped version. If a repository skipped `save()` on a state-only
|
|
752
|
+
* check, `withCommit` would still call `markPersisted(version)` after
|
|
753
|
+
* commit, desyncing `persistedVersion` from the DB row — and the next
|
|
754
|
+
* uncontended save would throw a false `ConcurrencyConflictError`.
|
|
755
|
+
*
|
|
756
|
+
* The pending-events clause covers the sanctioned decoupled
|
|
757
|
+
* `addDomainEvent` path (an event recorded without a state change,
|
|
758
|
+
* e.g. a deletion event before a hard delete): the aggregate still
|
|
759
|
+
* needs its trip through `withCommit` so the event reaches the
|
|
760
|
+
* outbox. With all clauses included, `hasChanges === false` genuinely
|
|
761
|
+
* means "skipping save is safe".
|
|
762
|
+
*/
|
|
763
|
+
get hasChanges(): boolean;
|
|
1217
764
|
/**
|
|
1218
765
|
* Mutates state and records the resulting domain events in the
|
|
1219
766
|
* **canonical record-after-mutation order**. Use this instead of calling
|
|
@@ -1221,7 +768,7 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
|
|
|
1221
768
|
* "event for a fact that never happened" footgun.
|
|
1222
769
|
*
|
|
1223
770
|
* Order of operations:
|
|
1224
|
-
* 1. `setState(newState, true)
|
|
771
|
+
* 1. `setState(newState, true)`: runs `validateState` first.
|
|
1225
772
|
* If it throws, the method propagates and **no event is recorded
|
|
1226
773
|
* and no version is bumped**.
|
|
1227
774
|
* 2. Each event in `events` is appended via `addDomainEvent`.
|
|
@@ -1268,7 +815,7 @@ declare abstract class AggregateRoot<TState, TId extends Id<string>, TEvent exte
|
|
|
1268
815
|
*/
|
|
1269
816
|
protected setState(newState: TState, bumpVersion?: boolean): void;
|
|
1270
817
|
/**
|
|
1271
|
-
* Restores the aggregate from a snapshot
|
|
818
|
+
* Restores the aggregate from a snapshot: loads state and aligns
|
|
1272
819
|
* `version` + `persistedVersion` to the snapshot version. Validates
|
|
1273
820
|
* the restored state.
|
|
1274
821
|
*
|
|
@@ -1283,19 +830,19 @@ type Handler<TState, TEvent extends AnyDomainEvent> = (state: TState, event: TEv
|
|
|
1283
830
|
*
|
|
1284
831
|
* Like `AggregateRoot`, this is both the root entity and the aggregate
|
|
1285
832
|
* boundary. The difference is persistence: state is derived from events,
|
|
1286
|
-
* not stored directly. Events are the single source of truth
|
|
833
|
+
* not stored directly. Events are the single source of truth: all state
|
|
1287
834
|
* changes go through `apply()` → handler.
|
|
1288
835
|
*
|
|
1289
836
|
* Extends `BaseAggregate` (the shared lifecycle machinery) but does NOT
|
|
1290
837
|
* expose `setState()` or `commit()` from `AggregateRoot`. This enforces
|
|
1291
|
-
* the event sourcing pattern at the type level
|
|
838
|
+
* the event sourcing pattern at the type level: there is no way to
|
|
1292
839
|
* mutate state without going through an event handler.
|
|
1293
840
|
*
|
|
1294
841
|
* `apply()` and `validateEvent()` throw `DomainError`-derived exceptions
|
|
1295
842
|
* on invariant violations. Subclasses override `validateEvent()` to
|
|
1296
843
|
* throw their own concrete subclasses (e.g. `OrderAlreadyConfirmedError`).
|
|
1297
844
|
* Only the infrastructure-boundary methods (`loadFromHistory`,
|
|
1298
|
-
* `restoreFromSnapshotWithEvents`) return `Result
|
|
845
|
+
* `restoreFromSnapshotWithEvents`) return `Result`: they catch
|
|
1299
846
|
* `DomainError` during replay so callers can react to corrupted event
|
|
1300
847
|
* streams without try/catch.
|
|
1301
848
|
*
|
|
@@ -1345,13 +892,13 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
|
|
|
1345
892
|
* Throws `DomainError` (or a subclass) on validation failure.
|
|
1346
893
|
* Throws `MissingHandlerError` if no handler is registered for `event.type`.
|
|
1347
894
|
*
|
|
1348
|
-
* State is not mutated if any step throws
|
|
895
|
+
* State is not mutated if any step throws: the handler is invoked into
|
|
1349
896
|
* a local and only assigned to `_state` once all checks pass.
|
|
1350
897
|
*
|
|
1351
898
|
* The method is generic in the event tag `K`, so concrete callers
|
|
1352
899
|
* (`this.apply(orderCreated)`) narrow to the literal tag and the
|
|
1353
|
-
* dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }
|
|
1354
|
-
*
|
|
900
|
+
* dispatched handler is typed as `Handler<TState, Extract<TEvent, { type: K }>>`,
|
|
901
|
+
* with no `as` cast required at the call site.
|
|
1355
902
|
*
|
|
1356
903
|
* @param event - The domain event to apply
|
|
1357
904
|
* @param isNew - Whether the event is new (needs persisting) or replayed from history
|
|
@@ -1369,12 +916,12 @@ declare abstract class EventSourcedAggregate<TState, TEvent extends AnyDomainEve
|
|
|
1369
916
|
private dispatchAndCommit;
|
|
1370
917
|
/**
|
|
1371
918
|
* Reconstitutes the aggregate from an event history. Catches `DomainError`
|
|
1372
|
-
* thrown during replay and returns it as an `Err
|
|
919
|
+
* thrown during replay and returns it as an `Err`: this is the
|
|
1373
920
|
* infrastructure boundary, where event-stream corruption is an expected
|
|
1374
921
|
* recoverable failure. Unexpected (non-DomainError) throws propagate.
|
|
1375
922
|
*
|
|
1376
923
|
* All-or-nothing: if any event mid-stream throws, the aggregate's state
|
|
1377
|
-
* is rolled back to its pre-call value
|
|
924
|
+
* is rolled back to its pre-call value, the same contract as
|
|
1378
925
|
* `restoreFromSnapshotWithEvents`. Partial replay is never observable.
|
|
1379
926
|
* (Version needs no rollback: replay dispatches with `isNew = false`,
|
|
1380
927
|
* which never bumps it; only the final `markRestored` advances it.)
|
|
@@ -1553,7 +1100,7 @@ interface ICommandBus<TMap extends CommandTypeMap = CommandTypeMap> {
|
|
|
1553
1100
|
*
|
|
1554
1101
|
* When `TMap` is supplied, the `commandType` argument is restricted to
|
|
1555
1102
|
* its keys and the handler signature is forced to match `TMap[K]` for the
|
|
1556
|
-
* return value
|
|
1103
|
+
* return value: typos and wrong-typed handlers are compile errors.
|
|
1557
1104
|
* Without `TMap` the registration is loose (any string key, any return
|
|
1558
1105
|
* type) so the no-config path keeps working.
|
|
1559
1106
|
*
|
|
@@ -1654,13 +1201,13 @@ interface EventBus<Evt extends AnyDomainEvent> {
|
|
|
1654
1201
|
* awaits all of its handlers, then dispatches `b`, and so on. The
|
|
1655
1202
|
* library never reorders or parallelises across events.
|
|
1656
1203
|
* 2. **Handlers within a single event run in parallel.** All handlers
|
|
1657
|
-
* subscribed to `event.type` are awaited via `Promise.allSettled
|
|
1204
|
+
* subscribed to `event.type` are awaited via `Promise.allSettled`:
|
|
1658
1205
|
* none of them sees the others' errors and none is skipped if a
|
|
1659
1206
|
* peer fails.
|
|
1660
1207
|
* 3. **Errors are collected and thrown AFTER everything dispatches.**
|
|
1661
1208
|
* If one handler throws, remaining handlers for that event still
|
|
1662
1209
|
* run, and remaining events in the batch still publish. Once
|
|
1663
|
-
* `publish` reaches the end of the batch it throws
|
|
1210
|
+
* `publish` reaches the end of the batch it throws: the single
|
|
1664
1211
|
* error directly if there was one, or an `AggregateError`
|
|
1665
1212
|
* ("Multiple event handlers failed") containing every captured
|
|
1666
1213
|
* error otherwise. Callers that need fail-fast semantics should
|
|
@@ -1733,7 +1280,7 @@ interface OnceOptions {
|
|
|
1733
1280
|
/**
|
|
1734
1281
|
* One pending event in the outbox plus the opaque id the implementation
|
|
1735
1282
|
* needs to ack it via `markDispatched`. The library does not prescribe
|
|
1736
|
-
* what `dispatchId` looks like
|
|
1283
|
+
* what `dispatchId` looks like: an implementation can reuse the event's
|
|
1737
1284
|
* own `eventId`, generate its own UUID, use the row's auto-increment
|
|
1738
1285
|
* primary key, or whatever the storage layer prefers.
|
|
1739
1286
|
*/
|
|
@@ -1742,7 +1289,7 @@ interface OutboxRecord<Evt extends AnyDomainEvent> {
|
|
|
1742
1289
|
event: Evt;
|
|
1743
1290
|
}
|
|
1744
1291
|
/**
|
|
1745
|
-
* Transactional outbox port
|
|
1292
|
+
* Transactional outbox port: the bridge between the write-side
|
|
1746
1293
|
* transaction and the (out-of-band) event dispatcher.
|
|
1747
1294
|
*
|
|
1748
1295
|
* Lifecycle:
|
|
@@ -1753,7 +1300,7 @@ interface OutboxRecord<Evt extends AnyDomainEvent> {
|
|
|
1753
1300
|
* 3. After successful dispatch, the dispatcher calls `markDispatched()`
|
|
1754
1301
|
* with the records' `dispatchId`s so they don't come back next poll.
|
|
1755
1302
|
*
|
|
1756
|
-
* `markDispatched` is required to be idempotent
|
|
1303
|
+
* `markDispatched` is required to be idempotent: calling it with an id
|
|
1757
1304
|
* that's already marked is a no-op, not an error. This lets the
|
|
1758
1305
|
* dispatcher safely retry on partial-failure.
|
|
1759
1306
|
*/
|
|
@@ -1792,20 +1339,24 @@ interface Outbox<Evt extends AnyDomainEvent> {
|
|
|
1792
1339
|
* `$transaction`, etc.). The block commits when the callback resolves
|
|
1793
1340
|
* and rolls back if it throws.
|
|
1794
1341
|
*
|
|
1795
|
-
* `TCtx` is the persistence layer's transaction handle
|
|
1342
|
+
* `TCtx` is the persistence layer's transaction handle: Drizzle's `tx`,
|
|
1796
1343
|
* Prisma's `tx`, Mongo's session, etc. The scope opens the transaction
|
|
1797
1344
|
* and passes the handle to `fn`; the use case binds its repositories to
|
|
1798
1345
|
* that handle (typically by constructing a tx-scoped repo from the ctx).
|
|
1799
1346
|
*
|
|
1800
1347
|
* No default for `TCtx`: every implementor names their context type
|
|
1801
1348
|
* explicitly. For genuinely context-free scopes (in-memory tests, naive
|
|
1802
|
-
* no-tx scopes) use `TransactionScope<undefined
|
|
1349
|
+
* no-tx scopes) use `TransactionScope<undefined>`: that's a conscious
|
|
1803
1350
|
* "there is nothing meaningful here" statement, not an accidental
|
|
1804
1351
|
* `unknown` fallback.
|
|
1805
1352
|
*
|
|
1806
|
-
* Intentionally
|
|
1807
|
-
* no
|
|
1808
|
-
*
|
|
1353
|
+
* Intentionally minimal: the scope itself does no change tracking and
|
|
1354
|
+
* no commit-time flush. Those concerns live in the layers above - the
|
|
1355
|
+
* aggregate detects its own changes (`changedKeys` / `hasChanges`),
|
|
1356
|
+
* `withCommit` orchestrates the commit lifecycle, and the opt-in
|
|
1357
|
+
* `UnitOfWork` facade adds tx-bound repositories and enrollment. See
|
|
1358
|
+
* "TransactionScope stays minimal; the Unit of Work lives above it" in
|
|
1359
|
+
* docs/guide/design-decisions.md.
|
|
1809
1360
|
*
|
|
1810
1361
|
* @example Drizzle implementation
|
|
1811
1362
|
* ```typescript
|
|
@@ -1817,7 +1368,7 @@ interface Outbox<Evt extends AnyDomainEvent> {
|
|
|
1817
1368
|
* }
|
|
1818
1369
|
* ```
|
|
1819
1370
|
*
|
|
1820
|
-
* @example Use site
|
|
1371
|
+
* @example Use site: bind repos to the live transaction
|
|
1821
1372
|
* ```typescript
|
|
1822
1373
|
* await scope.transactional(async (tx) => {
|
|
1823
1374
|
* // Construct tx-bound repos from ctx (your factory / DI of choice)
|
|
@@ -1829,7 +1380,7 @@ interface Outbox<Evt extends AnyDomainEvent> {
|
|
|
1829
1380
|
* });
|
|
1830
1381
|
* ```
|
|
1831
1382
|
*
|
|
1832
|
-
* `IRepository`'s contract takes the id / aggregate only
|
|
1383
|
+
* `IRepository`'s contract takes the id / aggregate only: the tx handle
|
|
1833
1384
|
* is wired into a concrete repository at construction time, not threaded
|
|
1834
1385
|
* through every call. Different ORMs have different idioms for that
|
|
1835
1386
|
* (constructor injection, factory functions, `withTx` chains); pick one
|
|
@@ -1849,14 +1400,17 @@ interface TransactionScope<TCtx> {
|
|
|
1849
1400
|
* committed" is the orchestrator's call to make, not the repo's.
|
|
1850
1401
|
*
|
|
1851
1402
|
* Order of operations:
|
|
1852
|
-
* 1. `fn(ctx)` runs inside `scope.transactional(...)
|
|
1403
|
+
* 1. `fn(ctx)` runs inside `scope.transactional(...)`; domain mutations
|
|
1853
1404
|
* + repo writes happen here. `ctx` is whatever transaction handle the
|
|
1854
1405
|
* `scope` exposes (Drizzle `tx`, Prisma `tx`, Mongo session, or
|
|
1855
1406
|
* `undefined` for context-free scopes).
|
|
1856
1407
|
* 2. **Still inside the transaction**, `withCommit` harvests every
|
|
1857
1408
|
* aggregate's `pendingEvents` and writes them via `outbox.add` (so
|
|
1858
1409
|
* events persist atomically with the state change). Skipped when no
|
|
1859
|
-
* events were recorded.
|
|
1410
|
+
* events were recorded. Each harvested event is stamped with
|
|
1411
|
+
* `aggregateVersion` = the aggregate's commit version (onto a frozen
|
|
1412
|
+
* copy; a pre-set value wins) - consumers get the OCC version the
|
|
1413
|
+
* row was written with, for ordering and idempotency watermarks.
|
|
1860
1414
|
*
|
|
1861
1415
|
* **Harvest order.** Events are concatenated in the order
|
|
1862
1416
|
* aggregates appear in the returned `aggregates` array, then in
|
|
@@ -1867,14 +1421,14 @@ interface TransactionScope<TCtx> {
|
|
|
1867
1421
|
* that exact order.
|
|
1868
1422
|
*
|
|
1869
1423
|
* **Two ordering guarantees, not one.** Within a single aggregate
|
|
1870
|
-
* the order is *causal
|
|
1424
|
+
* the order is *causal*: events are recorded in the order the
|
|
1871
1425
|
* domain methods ran, and subscribers (handlers, projections,
|
|
1872
1426
|
* replay) MUST process them in that order. Across aggregates the
|
|
1873
1427
|
* order in this batch is deterministic but *not* a domain
|
|
1874
1428
|
* guarantee. Greg Young / Vernon IDDD §10: aggregates are
|
|
1875
1429
|
* independent consistency boundaries; events across them are
|
|
1876
1430
|
* eventually consistent. Subscribers should NOT engineer
|
|
1877
|
-
* dependencies on cross-aggregate ordering
|
|
1431
|
+
* dependencies on cross-aggregate ordering; use
|
|
1878
1432
|
* `EventMetadata.causationId` to express true causation, or a
|
|
1879
1433
|
* process manager to coordinate. The in-process EventBus delivers
|
|
1880
1434
|
* this batch in order, sequential outbox-dispatchers preserve it
|
|
@@ -1882,8 +1436,11 @@ interface TransactionScope<TCtx> {
|
|
|
1882
1436
|
* across aggregates at delivery time.
|
|
1883
1437
|
* 3. The transaction commits.
|
|
1884
1438
|
* 4. **After** the commit, `aggregate.markPersisted(aggregate.version)`
|
|
1885
|
-
* fires on each returned aggregate
|
|
1886
|
-
* considered flushed.
|
|
1439
|
+
* fires on each returned aggregate; only now are pending events
|
|
1440
|
+
* considered flushed. Aggregates listed in the optional `deleted`
|
|
1441
|
+
* marker array are the exception: their pending events are cleared
|
|
1442
|
+
* directly WITHOUT `markPersisted`, so the post-save `onPersisted`
|
|
1443
|
+
* hook never fires for a row that was just deleted.
|
|
1887
1444
|
* 5. `bus.publish(events)` fires for the in-process fast path (skipped
|
|
1888
1445
|
* when no events or no `bus` is wired).
|
|
1889
1446
|
*
|
|
@@ -1894,22 +1451,36 @@ interface TransactionScope<TCtx> {
|
|
|
1894
1451
|
* them (eventual consistency).
|
|
1895
1452
|
*
|
|
1896
1453
|
* **A `bus.publish` failure never rejects `withCommit`.** Once the
|
|
1897
|
-
* transaction has committed, the write succeeded
|
|
1454
|
+
* transaction has committed, the write succeeded; surfacing a subscriber
|
|
1898
1455
|
* failure as a rejection would hand the caller a use-case failure for a
|
|
1899
1456
|
* committed write (a typical caller retries, double-executing it). The
|
|
1900
1457
|
* in-process fast path is best-effort by design; the error is reported to
|
|
1901
1458
|
* the optional `onPublishError(error, events)` hook (wire it to your
|
|
1902
|
-
* logger/metrics) and otherwise dropped
|
|
1459
|
+
* logger/metrics) and otherwise dropped; delivery is still guaranteed via
|
|
1903
1460
|
* the outbox. The hook is an observer: if it throws, its error is
|
|
1904
1461
|
* swallowed so the post-commit invariant holds.
|
|
1905
1462
|
*
|
|
1906
|
-
* If the transaction rolls back, `markPersisted` is **not** called
|
|
1463
|
+
* If the transaction rolls back, `markPersisted` is **not** called: the
|
|
1907
1464
|
* aggregate keeps its pending events, so the caller can retry or discard.
|
|
1908
1465
|
*
|
|
1466
|
+
* **Do not mutate an aggregate after `repository.save(...)` inside `fn`.**
|
|
1467
|
+
* `withCommit` cannot see what `save` wrote; the post-commit
|
|
1468
|
+
* `markPersisted` syncs `persistedVersion` to the CURRENT in-memory
|
|
1469
|
+
* version and (on `AggregateRoot`) re-baselines dirty tracking against
|
|
1470
|
+
* the CURRENT state. A mutation between `save` and the callback's return
|
|
1471
|
+
* therefore desyncs OCC (next save throws a false
|
|
1472
|
+
* `ConcurrencyConflictError`) — and under a partial-write repository
|
|
1473
|
+
* using `changedKeys`, an un-bumped mutation is silently marked clean
|
|
1474
|
+
* and never written. The `aggregateVersion` stamp widens the blast
|
|
1475
|
+
* radius further: harvested events would publicly claim a version the
|
|
1476
|
+
* committed row does not carry, poisoning every consumer's ordering
|
|
1477
|
+
* and idempotency watermarks — a cross-service inconsistency, not just
|
|
1478
|
+
* a local one. Mutate first, save last.
|
|
1479
|
+
*
|
|
1909
1480
|
* **Duplicate aggregates are deduped by reference.** If the returned
|
|
1910
|
-
* `aggregates` array contains the same instance twice
|
|
1481
|
+
* `aggregates` array contains the same instance twice (e.g. a use
|
|
1911
1482
|
* case touches an order via two repository references that happen to
|
|
1912
|
-
* resolve to the same identity-map entry
|
|
1483
|
+
* resolve to the same identity-map entry), `withCommit` dedupes by
|
|
1913
1484
|
* JavaScript object identity before harvesting. Each event lands in
|
|
1914
1485
|
* the outbox exactly once and `markPersisted` fires exactly once. Two
|
|
1915
1486
|
* *different* instances with the same logical id cannot be detected
|
|
@@ -1925,7 +1496,7 @@ interface TransactionScope<TCtx> {
|
|
|
1925
1496
|
* const orderRepository = makeOrderRepository(tx); // your factory binds tx to the repo
|
|
1926
1497
|
* const order = await orderRepository.getByIdOrFail(orderId);
|
|
1927
1498
|
* order.confirm();
|
|
1928
|
-
* await orderRepository.save(order); // pure persistence
|
|
1499
|
+
* await orderRepository.save(order); // pure persistence; does NOT call markPersisted
|
|
1929
1500
|
* return { result: order.id, aggregates: [order] };
|
|
1930
1501
|
* });
|
|
1931
1502
|
* ```
|
|
@@ -1937,12 +1508,23 @@ declare function withCommit<Evt extends AnyDomainEvent, R, TCtx>(deps: {
|
|
|
1937
1508
|
/**
|
|
1938
1509
|
* Observer for post-commit `bus.publish` failures. Called with the
|
|
1939
1510
|
* error and the events that were published. Must not be relied on
|
|
1940
|
-
* for delivery
|
|
1511
|
+
* for delivery: the outbox dispatcher is the reliable path.
|
|
1941
1512
|
*/
|
|
1942
1513
|
onPublishError?: (error: unknown, events: ReadonlyArray<Evt>) => void;
|
|
1943
1514
|
}, fn: (ctx: TCtx) => Promise<{
|
|
1944
1515
|
result: R;
|
|
1945
1516
|
aggregates: ReadonlyArray<IAggregateRoot<Id<string>, Evt>>;
|
|
1517
|
+
/**
|
|
1518
|
+
* Optional marker: which of `aggregates` were DELETED in this unit
|
|
1519
|
+
* of work. Their pending events are harvested like any other
|
|
1520
|
+
* (deletion events must reach the outbox), but the post-commit
|
|
1521
|
+
* lifecycle differs: `markPersisted` is NOT called on them — it
|
|
1522
|
+
* would fire the user-overridable `onPersisted` hook, whose
|
|
1523
|
+
* post-save semantics (cache fill, read-model warm-up) are a lie
|
|
1524
|
+
* for a row that was just deleted. Their pending events are
|
|
1525
|
+
* cleared directly instead, so a later commit cannot re-emit them.
|
|
1526
|
+
*/
|
|
1527
|
+
deleted?: ReadonlyArray<IAggregateRoot<Id<string>, Evt>>;
|
|
1946
1528
|
}>): Promise<R>;
|
|
1947
1529
|
|
|
1948
1530
|
/**
|
|
@@ -2020,6 +1602,366 @@ interface Query {
|
|
|
2020
1602
|
*/
|
|
2021
1603
|
type QueryHandler<Q extends Query, R> = (query: Q) => Promise<R>;
|
|
2022
1604
|
|
|
1605
|
+
/**
|
|
1606
|
+
* A class reference used as the type key of the identity map. Keying
|
|
1607
|
+
* on the CLASS (not a name string) makes collisions impossible by
|
|
1608
|
+
* construction: `Restaurant` and `Booking` are different keys even if
|
|
1609
|
+
* someone names two aggregates identically across modules, and there
|
|
1610
|
+
* is no string-discipline to maintain.
|
|
1611
|
+
*
|
|
1612
|
+
* The `Function & { prototype: TAgg }` branch is load-bearing: the
|
|
1613
|
+
* kit's aggregate convention is a **protected constructor** plus
|
|
1614
|
+
* static factories, and TypeScript rejects assigning a class with a
|
|
1615
|
+
* protected constructor to a construct-signature type. The prototype
|
|
1616
|
+
* witness accepts those classes while still inferring `TAgg`.
|
|
1617
|
+
*/
|
|
1618
|
+
type AggregateClass<TAgg> = (abstract new (...args: any[]) => TAgg) | (Function & {
|
|
1619
|
+
prototype: TAgg;
|
|
1620
|
+
});
|
|
1621
|
+
/**
|
|
1622
|
+
* Per-unit-of-work Identity Map (Fowler, PoEAA): within one operation,
|
|
1623
|
+
* one aggregate type+id maps to exactly ONE in-memory instance.
|
|
1624
|
+
*
|
|
1625
|
+
* This is the shipped implementation of the contract the
|
|
1626
|
+
* [Repository guide](../../docs/guide/repository.md) places on
|
|
1627
|
+
* `IRepository` implementations: two `getById(id)` calls in the same
|
|
1628
|
+
* unit of work MUST return the same instance, because `withCommit`'s
|
|
1629
|
+
* aggregate dedupe (and therefore exactly-once event harvest and
|
|
1630
|
+
* `markPersisted`) is keyed on JavaScript object identity.
|
|
1631
|
+
*
|
|
1632
|
+
* Storage is two-level (per-type stores created lazily), so
|
|
1633
|
+
* `Restaurant:123` and `Booking:123` can never collide — the type key
|
|
1634
|
+
* is the aggregate CLASS, not the id alone and not a name string.
|
|
1635
|
+
*
|
|
1636
|
+
* Repository read-path contract:
|
|
1637
|
+
*
|
|
1638
|
+
* ```ts
|
|
1639
|
+
* async getById(id: OrderId): Promise<Order | null> {
|
|
1640
|
+
* const cached = this.session.identityMap.get(Order, id);
|
|
1641
|
+
* if (cached) return cached;
|
|
1642
|
+
* // Deleted in this unit of work = gone, even if the physical
|
|
1643
|
+
* // delete is deferred and the row is still visible in the tx.
|
|
1644
|
+
* if (this.session.identityMap.isDeleted(Order, id)) return null;
|
|
1645
|
+
*
|
|
1646
|
+
* const row = await this.loadRow(id);
|
|
1647
|
+
* if (!row) return null;
|
|
1648
|
+
* const order = Order.reconstitute(row.id, row.state, row.version);
|
|
1649
|
+
* this.session.identityMap.set(Order, id, order);
|
|
1650
|
+
* return order;
|
|
1651
|
+
* }
|
|
1652
|
+
* ```
|
|
1653
|
+
*
|
|
1654
|
+
* Deletion is final within an operation: {@link delete} removes the
|
|
1655
|
+
* entry AND records a tombstone, so a later {@link set} of the same
|
|
1656
|
+
* type+id throws `AggregateDeletedError` — a second instance of a
|
|
1657
|
+
* deleted aggregate can never sneak back into the unit of work, even
|
|
1658
|
+
* through a repository whose row delete is deferred.
|
|
1659
|
+
*
|
|
1660
|
+
* Lifetime is ONE unit of work: the `UnitOfWork` creates a fresh map
|
|
1661
|
+
* per `run()` and clears it on close. Never cache across operations;
|
|
1662
|
+
* that would silently bypass optimistic concurrency control.
|
|
1663
|
+
*/
|
|
1664
|
+
declare class IdentityMap {
|
|
1665
|
+
private readonly _stores;
|
|
1666
|
+
private readonly _deleted;
|
|
1667
|
+
/** The cached instance for type+id, or `undefined` (also after {@link delete}). */
|
|
1668
|
+
get<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): TAgg | undefined;
|
|
1669
|
+
/** Whether an instance is registered for type+id (false after {@link delete}). */
|
|
1670
|
+
has<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): boolean;
|
|
1671
|
+
/**
|
|
1672
|
+
* Whether type+id was {@link delete}d in this unit of work. The
|
|
1673
|
+
* read path checks this BEFORE hydrating and returns `null`, so
|
|
1674
|
+
* "deleted in this operation" reads uniformly as not-found —
|
|
1675
|
+
* regardless of whether the repository's physical delete already
|
|
1676
|
+
* removed the row or is deferred within the transaction. Without
|
|
1677
|
+
* the check, a read-only probe of a deleted aggregate would crash
|
|
1678
|
+
* in {@link set} for deferred-write repositories and return `null`
|
|
1679
|
+
* for immediate-write ones.
|
|
1680
|
+
*/
|
|
1681
|
+
isDeleted<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): boolean;
|
|
1682
|
+
/**
|
|
1683
|
+
* Registers the hydrated instance for type+id.
|
|
1684
|
+
*
|
|
1685
|
+
* - Re-registering the SAME instance is a no-op (idempotent).
|
|
1686
|
+
* - Registering a DIFFERENT instance for an occupied type+id throws:
|
|
1687
|
+
* that is precisely the identity-map violation this class exists
|
|
1688
|
+
* to prevent (the repository hydrated twice instead of checking
|
|
1689
|
+
* {@link get} first), and letting it pass would double-harvest
|
|
1690
|
+
* events downstream.
|
|
1691
|
+
* - Registering a type+id that was {@link delete}d in this unit of
|
|
1692
|
+
* work throws `AggregateDeletedError`: deletion is final within
|
|
1693
|
+
* the operation.
|
|
1694
|
+
*/
|
|
1695
|
+
set<TAgg>(type: AggregateClass<TAgg>, id: Id<string>, aggregate: TAgg): void;
|
|
1696
|
+
/**
|
|
1697
|
+
* Removes the entry for type+id and records a tombstone: subsequent
|
|
1698
|
+
* {@link get} / {@link has} report absence, and a subsequent
|
|
1699
|
+
* {@link set} of the same type+id throws `AggregateDeletedError`.
|
|
1700
|
+
* Called by a repository's `delete(aggregate)` alongside
|
|
1701
|
+
* `session.enrollDeleted(aggregate)`.
|
|
1702
|
+
*/
|
|
1703
|
+
delete<TAgg>(type: AggregateClass<TAgg>, id: Id<string>): void;
|
|
1704
|
+
/** Empties all stores and tombstones. Called by the unit of work on close. */
|
|
1705
|
+
clear(): void;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Thrown when `UnitOfWork.run()` is called while the same instance is
|
|
1710
|
+
* already executing a unit of work: either a genuinely nested `run()`
|
|
1711
|
+
* inside the work callback, or two concurrent operations sharing one
|
|
1712
|
+
* instance.
|
|
1713
|
+
*
|
|
1714
|
+
* Both are contract violations, not recoverable infrastructure
|
|
1715
|
+
* failures, so this extends `BaseError` directly (same reasoning as
|
|
1716
|
+
* `MissingHandlerError`): a generic `catch (e instanceof
|
|
1717
|
+
* InfrastructureError)` handler must not mask it.
|
|
1718
|
+
*
|
|
1719
|
+
* A nested `run()` would NOT join the outer transaction; it would open
|
|
1720
|
+
* an independent one, silently breaking the all-or-nothing guarantee.
|
|
1721
|
+
* If two operations must commit together, they are ONE unit of work:
|
|
1722
|
+
* merge them into a single `run()` callback. For concurrent requests,
|
|
1723
|
+
* construct one `UnitOfWork` per operation (construction is trivially
|
|
1724
|
+
* cheap; the dependency object is the thing you share).
|
|
1725
|
+
*/
|
|
1726
|
+
declare class NestedUnitOfWorkError extends BaseError<"NestedUnitOfWorkError"> {
|
|
1727
|
+
constructor();
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Thrown when the unit-of-work context is used after `run()` has
|
|
1731
|
+
* settled: reading `context.repositories` / `context.transaction`, or
|
|
1732
|
+
* calling `session.enrollSaved` / `session.enrollDeleted`, once the
|
|
1733
|
+
* transaction has committed or rolled back.
|
|
1734
|
+
*
|
|
1735
|
+
* Use-after-close is a programming bug (typically a leaked context
|
|
1736
|
+
* reference or a fire-and-forget promise outliving the callback), so
|
|
1737
|
+
* this extends `BaseError` directly and should crash loud.
|
|
1738
|
+
*
|
|
1739
|
+
* **Honest scope of this guard:** the kit can only invalidate what it
|
|
1740
|
+
* controls - the context getters and the session. A repository or raw
|
|
1741
|
+
* transaction handle captured into a variable BEFORE close keeps
|
|
1742
|
+
* working as far as the kit can see; whether the underlying tx handle
|
|
1743
|
+
* rejects is ORM-specific. Do not let references escape the callback.
|
|
1744
|
+
*/
|
|
1745
|
+
declare class TransactionClosedError extends BaseError<"TransactionClosedError"> {
|
|
1746
|
+
readonly operation: string;
|
|
1747
|
+
constructor(operation: string);
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* The unit of work failed AFTER the work callback completed
|
|
1751
|
+
* successfully: during the event harvest, the outbox write, or the
|
|
1752
|
+
* transaction commit itself. The kit cannot see inside
|
|
1753
|
+
* `TransactionScope.transactional`, so these three are deliberately
|
|
1754
|
+
* one error class - the underlying failure is attached as `cause`.
|
|
1755
|
+
*
|
|
1756
|
+
* `InfrastructureError`: the business logic ran to completion; the
|
|
1757
|
+
* persistence boundary failed. The transaction rolled back (or never
|
|
1758
|
+
* committed), no aggregate was marked persisted, and pending events
|
|
1759
|
+
* survive on the aggregates — the operation left no partial state
|
|
1760
|
+
* behind. **Whether a retry helps depends on the cause**: a commit-
|
|
1761
|
+
* time serialization failure is transient, but `withCommit`'s harvest
|
|
1762
|
+
* guard (an event missing `aggregateId` / `aggregateType` — a
|
|
1763
|
+
* programming bug) also lands here and will fail deterministically on
|
|
1764
|
+
* every retry. Inspect the `cause` before routing into retry logic.
|
|
1765
|
+
*/
|
|
1766
|
+
declare class CommitError extends InfrastructureError<"CommitError"> {
|
|
1767
|
+
constructor(cause: unknown);
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* The work callback threw AND the transaction scope rejected with a
|
|
1771
|
+
* DIFFERENT error that does not wrap the callback's error in its cause
|
|
1772
|
+
* chain - the strongest available signal that the rollback itself
|
|
1773
|
+
* failed. The callback's (primary) error is preserved as `cause`, so
|
|
1774
|
+
* cause-chain helpers (`someChainRetryable`, `findInCauseChain`) still
|
|
1775
|
+
* see a wrapped `ConcurrencyConflictError` & co.; the scope's error is
|
|
1776
|
+
* carried in {@link rollbackCause}.
|
|
1777
|
+
*
|
|
1778
|
+
* Scopes that rethrow the original error (Drizzle, Prisma do) never
|
|
1779
|
+
* produce this; scopes that WRAP the original are detected via the
|
|
1780
|
+
* cause chain and passed through unchanged instead.
|
|
1781
|
+
*/
|
|
1782
|
+
declare class RollbackError extends InfrastructureError<"RollbackError"> {
|
|
1783
|
+
readonly rollbackCause: unknown;
|
|
1784
|
+
constructor(cause: unknown, rollbackCause: unknown);
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* The enrollment handle a unit of work hands to its repositories.
|
|
1788
|
+
*
|
|
1789
|
+
* Repositories enroll every aggregate they write so the unit of work
|
|
1790
|
+
* can harvest pending events into the outbox (inside the transaction)
|
|
1791
|
+
* and call `markPersisted` after the commit - the same lifecycle
|
|
1792
|
+
* `withCommit` runs for its returned `aggregates` array, minus the
|
|
1793
|
+
* footgun: with enrollment, "forgot to list the aggregate" cannot
|
|
1794
|
+
* happen per call site; each repository implements it once and its
|
|
1795
|
+
* tests pin it once.
|
|
1796
|
+
*
|
|
1797
|
+
* Contract for repository implementations:
|
|
1798
|
+
* - `getById(id)` checks `identityMap.get` BEFORE hydrating, treats
|
|
1799
|
+
* `identityMap.isDeleted` as not-found (`null`), and registers the
|
|
1800
|
+
* hydrated instance after - two loads of the same aggregate in one
|
|
1801
|
+
* unit of work must return the same instance.
|
|
1802
|
+
* - `save(aggregate)` calls {@link enrollSaved} BEFORE the row write:
|
|
1803
|
+
* the deleted-gate then throws `AggregateDeletedError` before any SQL
|
|
1804
|
+
* runs (instead of the write surfacing as a confusing
|
|
1805
|
+
* `ConcurrencyConflictError` against the deleted row). Enrollment is
|
|
1806
|
+
* idempotent per instance, mirroring `withCommit`'s reference dedupe,
|
|
1807
|
+
* and a failed write rolls the whole unit of work back anyway.
|
|
1808
|
+
* - `delete(aggregate)` calls {@link enrollDeleted} - ONE call does all
|
|
1809
|
+
* the deletion bookkeeping: the identity-map entry is removed and
|
|
1810
|
+
* tombstoned automatically (keyed on the instance's concrete class),
|
|
1811
|
+
* the recorded deletion events are still harvested into the outbox,
|
|
1812
|
+
* and saving or re-registering the aggregate (same instance OR a
|
|
1813
|
+
* re-created one with the same type+id) later in this unit of work
|
|
1814
|
+
* throws `AggregateDeletedError`.
|
|
1815
|
+
*
|
|
1816
|
+
* The use case can also enroll manually via `context.session` for the
|
|
1817
|
+
* rare write that bypasses a repository.
|
|
1818
|
+
*/
|
|
1819
|
+
interface UnitOfWorkSession<Evt extends AnyDomainEvent = AnyDomainEvent> {
|
|
1820
|
+
/**
|
|
1821
|
+
* The per-operation Identity Map (Fowler): one aggregate type+id,
|
|
1822
|
+
* one in-memory instance. Created fresh per `run()`, cleared on
|
|
1823
|
+
* close; accessing it after close throws
|
|
1824
|
+
* {@link TransactionClosedError}.
|
|
1825
|
+
*/
|
|
1826
|
+
readonly identityMap: IdentityMap;
|
|
1827
|
+
/** Enroll an aggregate that was (or will be) written in this unit of work. */
|
|
1828
|
+
enrollSaved(aggregate: IAggregateRoot<Id<string>, Evt>): void;
|
|
1829
|
+
/**
|
|
1830
|
+
* Enroll an aggregate whose row was (or will be) deleted in this
|
|
1831
|
+
* unit of work. Its pending events (e.g. a recorded deletion event)
|
|
1832
|
+
* are harvested like any other; re-saving the instance afterwards
|
|
1833
|
+
* throws `AggregateDeletedError`.
|
|
1834
|
+
*/
|
|
1835
|
+
enrollDeleted(aggregate: IAggregateRoot<Id<string>, Evt>): void;
|
|
1836
|
+
}
|
|
1837
|
+
/**
|
|
1838
|
+
* What the work callback receives: repositories already bound to the
|
|
1839
|
+
* live transaction, the enrollment session, and — deliberately named to
|
|
1840
|
+
* look like the escape hatch it is — the raw transaction handle.
|
|
1841
|
+
*
|
|
1842
|
+
* All members throw {@link TransactionClosedError} once `run()` has
|
|
1843
|
+
* settled; do not let the context escape the callback.
|
|
1844
|
+
*/
|
|
1845
|
+
interface UnitOfWorkContext<TCtx, TRepos, Evt extends AnyDomainEvent = AnyDomainEvent> {
|
|
1846
|
+
readonly repositories: TRepos;
|
|
1847
|
+
/**
|
|
1848
|
+
* **Escape hatch — you are leaving the unit of work's guarantees.**
|
|
1849
|
+
* A write issued on the raw handle bypasses the repository contract,
|
|
1850
|
+
* enrollment (its aggregate's events are NOT harvested unless you
|
|
1851
|
+
* also call `session.enrollSaved`), and the identity map (a later
|
|
1852
|
+
* `getById` of the same aggregate hydrates a SECOND instance —
|
|
1853
|
+
* double harvest, double `markPersisted`). Use it only for writes no
|
|
1854
|
+
* repository covers, pair it with manual enrollment, and prefer
|
|
1855
|
+
* adding a repository method whenever one could exist.
|
|
1856
|
+
*/
|
|
1857
|
+
readonly rawTransaction: TCtx;
|
|
1858
|
+
readonly session: UnitOfWorkSession<Evt>;
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Per-repository factory map: for each key of `TRepos`, a function
|
|
1862
|
+
* that constructs the repository bound to the live transaction handle
|
|
1863
|
+
* and the enrollment session. Called once per `run()`, so every
|
|
1864
|
+
* repository of one unit of work shares the same transaction.
|
|
1865
|
+
*
|
|
1866
|
+
* ```ts
|
|
1867
|
+
* const factories = {
|
|
1868
|
+
* orders: (tx, session) => new DrizzleOrderRepository(tx, session),
|
|
1869
|
+
* invoices: (tx, session) => new DrizzleInvoiceRepository(tx, session),
|
|
1870
|
+
* };
|
|
1871
|
+
* ```
|
|
1872
|
+
*/
|
|
1873
|
+
type RepositoryFactories<TCtx, TRepos, Evt extends AnyDomainEvent = AnyDomainEvent> = {
|
|
1874
|
+
[K in keyof TRepos]: (tx: TCtx, session: UnitOfWorkSession<Evt>) => TRepos[K];
|
|
1875
|
+
};
|
|
1876
|
+
/** Dependencies for {@link UnitOfWork}; the app-level singleton part. */
|
|
1877
|
+
interface UnitOfWorkDeps<Evt extends AnyDomainEvent, TCtx, TRepos> {
|
|
1878
|
+
scope: TransactionScope<TCtx>;
|
|
1879
|
+
outbox: Outbox<Evt>;
|
|
1880
|
+
bus?: EventBus<Evt>;
|
|
1881
|
+
/** See `withCommit`: observer for post-commit `bus.publish` failures. */
|
|
1882
|
+
onPublishError?: (error: unknown, events: ReadonlyArray<Evt>) => void;
|
|
1883
|
+
repositories: RepositoryFactories<TCtx, TRepos, Evt>;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Explicit-save Unit of Work: one `run()` call is one application-level
|
|
1887
|
+
* write operation. All repository writes inside the callback share one
|
|
1888
|
+
* transaction and either persist completely or not at all.
|
|
1889
|
+
*
|
|
1890
|
+
* Built ON TOP of `withCommit` - the commit orchestration (event
|
|
1891
|
+
* harvest into the outbox inside the transaction, `markPersisted`
|
|
1892
|
+
* after the commit, best-effort in-process publish last) is inherited,
|
|
1893
|
+
* not reimplemented. What this layer adds:
|
|
1894
|
+
*
|
|
1895
|
+
* - **Tx-bound repositories via a registry.** The callback receives
|
|
1896
|
+
* ready-made repositories instead of a raw transaction handle; the
|
|
1897
|
+
* factory map is wired once at construction.
|
|
1898
|
+
* - **Enrollment instead of a returned aggregates array.** Repositories
|
|
1899
|
+
* enroll what they write via {@link UnitOfWorkSession}; the use case
|
|
1900
|
+
* cannot forget to list an aggregate (the `withCommit` footgun).
|
|
1901
|
+
* - **Lifecycle errors.** {@link NestedUnitOfWorkError},
|
|
1902
|
+
* {@link TransactionClosedError}, {@link CommitError},
|
|
1903
|
+
* {@link RollbackError}, {@link AggregateDeletedError}.
|
|
1904
|
+
*
|
|
1905
|
+
* - **A per-operation Identity Map** on the session: repositories
|
|
1906
|
+
* check it before hydrating and register after, so one type+id maps
|
|
1907
|
+
* to one in-memory instance per unit of work (the contract
|
|
1908
|
+
* `withCommit`'s reference-dedupe relies on, now shipped instead of
|
|
1909
|
+
* merely documented).
|
|
1910
|
+
*
|
|
1911
|
+
* What it deliberately does NOT do (v1): no auto-flush (explicit
|
|
1912
|
+
* `save()` only - `hasChanges` makes a redundant save a cheap no-op),
|
|
1913
|
+
* no savepoints, no nested-transaction joining. `withCommit` with
|
|
1914
|
+
* hand-rolled repos remains fully supported; this facade is opt-in.
|
|
1915
|
+
*
|
|
1916
|
+
* **Instance discipline:** one instance owns one logical operation at
|
|
1917
|
+
* a time. `run()` while a run is active throws
|
|
1918
|
+
* {@link NestedUnitOfWorkError} - that covers genuine nesting AND two
|
|
1919
|
+
* concurrent requests sharing one instance, which is the same bug in
|
|
1920
|
+
* different clothes. Construct one `UnitOfWork` per operation
|
|
1921
|
+
* (construction stores one reference; the shareable singleton is the
|
|
1922
|
+
* deps object). Sequential reuse of an instance is fine.
|
|
1923
|
+
*
|
|
1924
|
+
* **Error pass-through:** an error thrown by the work callback (a
|
|
1925
|
+
* repository's `ConcurrencyConflictError`, a `DomainError`, anything)
|
|
1926
|
+
* is rethrown UNCHANGED - the unit of work never converts a concurrency
|
|
1927
|
+
* conflict into a generic error. Only the two failure modes the
|
|
1928
|
+
* callback cannot observe are wrapped: see {@link CommitError} and
|
|
1929
|
+
* {@link RollbackError}.
|
|
1930
|
+
*
|
|
1931
|
+
* @example
|
|
1932
|
+
* ```ts
|
|
1933
|
+
* const deps = {
|
|
1934
|
+
* scope: drizzleScope,
|
|
1935
|
+
* outbox: drizzleOutbox,
|
|
1936
|
+
* bus: eventBus,
|
|
1937
|
+
* repositories: {
|
|
1938
|
+
* restaurants: (tx, session) => new DrizzleRestaurantRepository(tx, session),
|
|
1939
|
+
* },
|
|
1940
|
+
* };
|
|
1941
|
+
*
|
|
1942
|
+
* const uow = new UnitOfWork(deps);
|
|
1943
|
+
* const result = await uow.run(async ({ repositories }) => {
|
|
1944
|
+
* const restaurant = await repositories.restaurants.getByIdOrFail(id);
|
|
1945
|
+
* restaurant.changeOpeningHours(openingHours);
|
|
1946
|
+
* await repositories.restaurants.save(restaurant); // save() enrolls
|
|
1947
|
+
* return restaurant.id;
|
|
1948
|
+
* });
|
|
1949
|
+
* ```
|
|
1950
|
+
*/
|
|
1951
|
+
declare class UnitOfWork<Evt extends AnyDomainEvent, TCtx, TRepos extends Record<string, unknown>> {
|
|
1952
|
+
private readonly deps;
|
|
1953
|
+
private _active;
|
|
1954
|
+
constructor(deps: UnitOfWorkDeps<Evt, TCtx, TRepos>);
|
|
1955
|
+
/**
|
|
1956
|
+
* Execute one unit of work: open the transaction, hand the callback
|
|
1957
|
+
* tx-bound repositories, commit on resolve, roll back on throw,
|
|
1958
|
+
* run the post-commit lifecycle (markPersisted, publish) for every
|
|
1959
|
+
* enrolled aggregate. Returns the callback's result.
|
|
1960
|
+
*/
|
|
1961
|
+
run<R>(work: (context: UnitOfWorkContext<TCtx, TRepos, Evt>) => Promise<R>): Promise<R>;
|
|
1962
|
+
private buildRepositories;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
2023
1965
|
/**
|
|
2024
1966
|
* Type map for query types to their return types.
|
|
2025
1967
|
* Used to improve type inference in QueryBus.
|
|
@@ -2089,7 +2031,7 @@ interface IQueryBus<TMap extends QueryTypeMap = QueryTypeMap> {
|
|
|
2089
2031
|
*
|
|
2090
2032
|
* When `TMap` is supplied, the `queryType` argument is restricted to its
|
|
2091
2033
|
* keys and the handler signature is forced to match `TMap[K]` for the
|
|
2092
|
-
* return value
|
|
2034
|
+
* return value: typos and wrong-typed handlers are compile errors.
|
|
2093
2035
|
* Without `TMap` the registration is loose (any string key, any return
|
|
2094
2036
|
* type) so the no-config path keeps working.
|
|
2095
2037
|
*
|
|
@@ -2200,7 +2142,7 @@ declare class EventBusImpl<Evt extends AnyDomainEvent> implements EventBus<Evt>
|
|
|
2200
2142
|
* In-memory reference implementation of `Outbox<Evt>`.
|
|
2201
2143
|
*
|
|
2202
2144
|
* Intended for tests, single-process workers, and quick-start demos.
|
|
2203
|
-
* Uses the event's own `eventId` as the dispatch id
|
|
2145
|
+
* Uses the event's own `eventId` as the dispatch id: the common, clean
|
|
2204
2146
|
* choice. Storage is a `Map<string, OutboxRecord<Evt>>` keyed by
|
|
2205
2147
|
* `eventId`, so re-adding the same event is naturally idempotent (the
|
|
2206
2148
|
* duplicate entry overwrites itself; `getPending` returns each event at
|
|
@@ -2209,7 +2151,13 @@ declare class EventBusImpl<Evt extends AnyDomainEvent> implements EventBus<Evt>
|
|
|
2209
2151
|
* For production, back the outbox with a transactional store so the
|
|
2210
2152
|
* outbox row participates in the same transaction as the aggregate
|
|
2211
2153
|
* write (see `TransactionScope` + `withCommit`). This class lives in
|
|
2212
|
-
* memory only
|
|
2154
|
+
* memory only: events are lost on process restart — and, sharper:
|
|
2155
|
+
* events `add()`ed inside a transaction that later rolls back are NOT
|
|
2156
|
+
* removed (the Map knows nothing about your scope's rollback). Tests
|
|
2157
|
+
* that assert rollback purity need an outbox that participates in the
|
|
2158
|
+
* test store's transactional semantics; see the reference adapter at
|
|
2159
|
+
* https://github.com/shi-rudo/ddd-kit-ts/blob/main/src/testing/repository-contract.test.ts
|
|
2160
|
+
* (repo-only, not shipped to npm).
|
|
2213
2161
|
*
|
|
2214
2162
|
* @example
|
|
2215
2163
|
* ```ts
|
|
@@ -2234,13 +2182,33 @@ declare class InMemoryOutbox<Evt extends AnyDomainEvent> implements Outbox<Evt>
|
|
|
2234
2182
|
markDispatched(dispatchIds: ReadonlyArray<string>): Promise<void>;
|
|
2235
2183
|
}
|
|
2236
2184
|
|
|
2185
|
+
/**
|
|
2186
|
+
* The canonical shape of a unit-of-work-facing repository. Unlike
|
|
2187
|
+
* `IRepository` below (id-canonical CRUD for `withCommit`-style
|
|
2188
|
+
* setups), `delete` takes the AGGREGATE: the unit of work needs the
|
|
2189
|
+
* instance for deletion-event harvest, the identity-map tombstone, and
|
|
2190
|
+
* the deleted-cannot-be-resaved gate. Ids stay branded (`TId extends
|
|
2191
|
+
* Id<string>`) end-to-end.
|
|
2192
|
+
*
|
|
2193
|
+
* Implementing this interface is optional — the `UnitOfWork` registry
|
|
2194
|
+
* is structurally typed — but it is the single source of truth the
|
|
2195
|
+
* guide's examples and the repository contract test suite
|
|
2196
|
+
* (`@shirudo/ddd-kit/testing`, whose `ContractRepository` is the
|
|
2197
|
+
* minimal structural subset of this shape) are written against.
|
|
2198
|
+
*/
|
|
2199
|
+
interface IUnitOfWorkRepository<TAgg extends IAggregateRoot<TId, Evt>, TId extends Id<string>, Evt extends AnyDomainEvent = AnyDomainEvent> {
|
|
2200
|
+
getById(id: TId): Promise<TAgg | null>;
|
|
2201
|
+
getByIdOrFail(id: TId): Promise<TAgg>;
|
|
2202
|
+
save(aggregate: TAgg): Promise<void>;
|
|
2203
|
+
delete(aggregate: TAgg): Promise<void>;
|
|
2204
|
+
}
|
|
2237
2205
|
/**
|
|
2238
2206
|
* Core repository contract for Aggregate Roots.
|
|
2239
2207
|
*
|
|
2240
2208
|
* In DDD a Repository is a "collection illusion" for aggregates: load by
|
|
2241
2209
|
* identity, save the whole aggregate, delete by identity. Querying by
|
|
2242
2210
|
* arbitrary criteria is a separate concern (CQRS read-side, ad-hoc bulk
|
|
2243
|
-
* operations) and lives on the `IQueryableRepository` extension below
|
|
2211
|
+
* operations) and lives on the `IQueryableRepository` extension below, so
|
|
2244
2212
|
* write-side repositories don't have to implement query plumbing they
|
|
2245
2213
|
* don't need.
|
|
2246
2214
|
*
|
|
@@ -2271,7 +2239,7 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2271
2239
|
exists(id: TId): Promise<boolean>;
|
|
2272
2240
|
/**
|
|
2273
2241
|
* Persists the aggregate (insert or update). Implementations are
|
|
2274
|
-
* responsible for **persistence only
|
|
2242
|
+
* responsible for **persistence only**; they must NOT touch the
|
|
2275
2243
|
* aggregate's in-memory state:
|
|
2276
2244
|
*
|
|
2277
2245
|
* 1. Throw `ConcurrencyConflictError` from `@shirudo/ddd-kit` when the
|
|
@@ -2279,14 +2247,14 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2279
2247
|
* stored (optimistic concurrency).
|
|
2280
2248
|
* 2. Write the aggregate to durable storage.
|
|
2281
2249
|
*
|
|
2282
|
-
* **Insert vs update
|
|
2250
|
+
* **Insert vs update: the `persistedVersion` convention.** Every aggregate
|
|
2283
2251
|
* exposes two version fields with distinct roles:
|
|
2284
2252
|
*
|
|
2285
|
-
* - `aggregate.version
|
|
2253
|
+
* - `aggregate.version`: in-memory post-mutation value, bumped by
|
|
2286
2254
|
* `setState(_, true)` / `commit()` / `apply()`. NOT the right
|
|
2287
2255
|
* routing key, because mutations can advance it past zero while
|
|
2288
2256
|
* the DB row still does not exist.
|
|
2289
|
-
* - `aggregate.persistedVersion
|
|
2257
|
+
* - `aggregate.persistedVersion`: what the persistence layer holds.
|
|
2290
2258
|
* `undefined` until the aggregate has been persisted or restored
|
|
2291
2259
|
* at least once. This is the routing key.
|
|
2292
2260
|
*
|
|
@@ -2296,14 +2264,14 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2296
2264
|
* - otherwise → **UPDATE** with the OCC predicate
|
|
2297
2265
|
* `WHERE id = ? AND version = aggregate.persistedVersion` (the
|
|
2298
2266
|
* load-time / last-save baseline, not the post-mutation in-memory
|
|
2299
|
-
* value). If the row count is `0`, another writer raced you
|
|
2267
|
+
* value). If the row count is `0`, another writer raced you:
|
|
2300
2268
|
* throw `ConcurrencyConflictError`.
|
|
2301
2269
|
*
|
|
2302
2270
|
* Do **not** call `aggregate.markPersisted(...)` here. The library's
|
|
2303
2271
|
* `withCommit` orchestrator handles the post-save lifecycle (harvest
|
|
2304
2272
|
* pending events into the outbox, then mark persisted after commit).
|
|
2305
2273
|
* Calling `markPersisted` inside `save` clears pending events too early
|
|
2306
|
-
* and breaks the harvest path
|
|
2274
|
+
* and breaks the harvest path, and is also why the Vernon/Axon/
|
|
2307
2275
|
* EventFlow pattern separates persistence from commit-events.
|
|
2308
2276
|
*
|
|
2309
2277
|
* If you are not using `withCommit` (custom orchestration), call
|
|
@@ -2312,7 +2280,7 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2312
2280
|
*/
|
|
2313
2281
|
save(aggregate: TAgg): Promise<void>;
|
|
2314
2282
|
/**
|
|
2315
|
-
* Removes the aggregate's row by id. Pure persistence
|
|
2283
|
+
* Removes the aggregate's row by id. Pure persistence: does NOT
|
|
2316
2284
|
* harvest pending events from the aggregate (the contract takes
|
|
2317
2285
|
* only the id, so there is no aggregate to harvest from).
|
|
2318
2286
|
*
|
|
@@ -2320,7 +2288,7 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2320
2288
|
* is the right domain verb. Most are actually state transitions
|
|
2321
2289
|
* (*cancel*, *archive*, *close*, *deactivate*, *terminate*) with
|
|
2322
2290
|
* proper domain names that should be modelled as state changes plus
|
|
2323
|
-
* a recorded event
|
|
2291
|
+
* a recorded event, not as row removal.
|
|
2324
2292
|
*
|
|
2325
2293
|
* `delete(id)` belongs in the toolkit for three distinct cases, in
|
|
2326
2294
|
* decreasing order of common occurrence (see
|
|
@@ -2346,7 +2314,7 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2346
2314
|
* subscriber cares. If the entity has identity in the ubiquitous
|
|
2347
2315
|
* language, you probably want path 1 or 2 instead.
|
|
2348
2316
|
*
|
|
2349
|
-
* In pure event-sourced systems `delete` is rarely meaningful
|
|
2317
|
+
* In pure event-sourced systems `delete` is rarely meaningful:
|
|
2350
2318
|
* end-of-lifecycle there is a `Closed` / `Terminated` event in the
|
|
2351
2319
|
* stream, and identity persists in the event log. `delete` applies
|
|
2352
2320
|
* primarily to state-stored aggregates and snapshot / projection
|
|
@@ -2360,7 +2328,7 @@ interface IRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<string>>
|
|
|
2360
2328
|
* Prisma `WhereInput`, a MongoDB filter document, a plain
|
|
2361
2329
|
* `(t: TAgg) => boolean` predicate for in-memory repos, or anything else.
|
|
2362
2330
|
*
|
|
2363
|
-
* The library does not prescribe a Specification or query DSL
|
|
2331
|
+
* The library does not prescribe a Specification or query DSL: the
|
|
2364
2332
|
* Repository implementation owns its query language. This avoids the
|
|
2365
2333
|
* phantom-interface trap of a library-level `ISpecification<T>` with no
|
|
2366
2334
|
* methods and lets each Repository expose the strongest possible types for
|
|
@@ -2396,13 +2364,13 @@ interface IQueryableRepository<TAgg extends IAggregateRoot<TId>, TId extends Id<
|
|
|
2396
2364
|
*/
|
|
2397
2365
|
findOne(filter: TFilter): Promise<TAgg | null>;
|
|
2398
2366
|
/**
|
|
2399
|
-
* Returns **every** aggregate matching the filter
|
|
2367
|
+
* Returns **every** aggregate matching the filter: no pagination,
|
|
2400
2368
|
* no cursor. For unbounded result sets, prefer a read-side projection
|
|
2401
2369
|
* (CQRS read model) over loading aggregates in bulk; aggregates are
|
|
2402
2370
|
* write-side objects and rehydrating thousands of them by id is rarely
|
|
2403
2371
|
* what you want. If you need pagination on the write side, declare a
|
|
2404
2372
|
* domain-specific paged method on your concrete repository (e.g.
|
|
2405
|
-
* `findPage(filter, cursor)`)
|
|
2373
|
+
* `findPage(filter, cursor)`): the library does not prescribe a
|
|
2406
2374
|
* pagination contract because cursor/offset/keyset semantics vary too
|
|
2407
2375
|
* much across storage backends.
|
|
2408
2376
|
*/
|
|
@@ -2416,31 +2384,31 @@ type VO<T> = Readonly<T>;
|
|
|
2416
2384
|
* so the freeze symmetry matches `deepEqual` (which also considers symbol
|
|
2417
2385
|
* keys). Handles circular references by tracking visited objects.
|
|
2418
2386
|
*
|
|
2419
|
-
* Note: `deepFreeze` mutates its argument in place
|
|
2387
|
+
* Note: `deepFreeze` mutates its argument in place; it sets `[[Frozen]]`
|
|
2420
2388
|
* on the object you pass in. Callers that need to avoid touching the
|
|
2421
2389
|
* input (e.g. `vo()`) should deep-clone first.
|
|
2422
2390
|
*
|
|
2423
2391
|
* Date/Map/Set keep internal-slot mutability under `Object.freeze`
|
|
2424
2392
|
* (`setTime`, `set`, `add`, … still work on frozen instances), so their
|
|
2425
2393
|
* mutator methods are shadowed with throwing own properties and Map/Set
|
|
2426
|
-
* contents are frozen recursively. The shadows are non-enumerable
|
|
2394
|
+
* contents are frozen recursively. The shadows are non-enumerable:
|
|
2427
2395
|
* invisible to `Object.keys`, spread, `deepEqual`, and `structuredClone`.
|
|
2428
2396
|
*
|
|
2429
2397
|
* The shadowing is deny-by-enumeration: only the mutators known at
|
|
2430
2398
|
* release time are blocked. If the runtime grows a NEW mutator (e.g. the
|
|
2431
2399
|
* stage-3 `Map.prototype.getOrInsert` upsert proposal), it is not blocked
|
|
2432
|
-
* until the list is updated
|
|
2400
|
+
* until the list is updated. Treat the mutator blocking as a guard rail,
|
|
2433
2401
|
* not a security boundary.
|
|
2434
2402
|
*
|
|
2435
2403
|
* Limitation: ArrayBuffer views (TypedArrays, DataView) are passed through
|
|
2436
|
-
* unfrozen
|
|
2437
|
-
* cannot protect the underlying buffer. Their contents remain mutable.
|
|
2404
|
+
* unfrozen, because the spec forbids freezing a view with elements, and
|
|
2405
|
+
* freezing cannot protect the underlying buffer. Their contents remain mutable.
|
|
2438
2406
|
*/
|
|
2439
2407
|
declare function deepFreeze<T>(obj: T, visited?: WeakSet<object>): Readonly<T>;
|
|
2440
2408
|
/**
|
|
2441
2409
|
* Creates a deeply immutable value object from the given data.
|
|
2442
2410
|
*
|
|
2443
|
-
* The input is first deep-cloned, then the clone is frozen
|
|
2411
|
+
* The input is first deep-cloned, then the clone is frozen, so calling
|
|
2444
2412
|
* `vo(input)` never freezes the caller's own object graph as a
|
|
2445
2413
|
* side-effect. Mutating the input afterwards does not bleed into the VO.
|
|
2446
2414
|
* Symbol-keyed properties are preserved (matching `voEquals`); function
|
|
@@ -2531,7 +2499,7 @@ declare function voEqualsExcept<T>(a: VO<T>, b: VO<T>, options: DeepEqualExceptO
|
|
|
2531
2499
|
*
|
|
2532
2500
|
* Note: the Result covers VALIDATION failures only. Non-data values in
|
|
2533
2501
|
* the input (functions, Promise/WeakMap/WeakSet) still throw a
|
|
2534
|
-
* `TypeError` from `vo()
|
|
2502
|
+
* `TypeError` from `vo()`; they cannot occur in parsed JSON and signal
|
|
2535
2503
|
* a programming error, not a validation failure.
|
|
2536
2504
|
*
|
|
2537
2505
|
* @param t - The data to convert into a value object
|
|
@@ -2599,7 +2567,7 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
2599
2567
|
/**
|
|
2600
2568
|
* Creates a new ValueObject.
|
|
2601
2569
|
* The properties are deep-cloned (prototype-preserving) and then deeply
|
|
2602
|
-
* frozen
|
|
2570
|
+
* frozen, so the caller's own object graph is never frozen or mutated,
|
|
2603
2571
|
* and later mutation of the input does not bleed into the value object.
|
|
2604
2572
|
*
|
|
2605
2573
|
* @param props - The properties of the value object
|
|
@@ -2659,7 +2627,7 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
2659
2627
|
* was recorded the input is frozen into a `VO<T>` and returned as `Ok`;
|
|
2660
2628
|
* otherwise the populated `ValidationError` is returned as `Err`.
|
|
2661
2629
|
*
|
|
2662
|
-
* `ValidationError` comes from `@shirudo/base-error
|
|
2630
|
+
* `ValidationError` comes from `@shirudo/base-error`; import it from there to
|
|
2663
2631
|
* narrow the `Err` branch, exactly as `Result` is imported from
|
|
2664
2632
|
* `@shirudo/result`. It serializes to RFC 9457 Problem Details; use
|
|
2665
2633
|
* {@link validationProblemDetails} at the HTTP boundary to surface the issues.
|
|
@@ -2681,4 +2649,4 @@ declare abstract class ValueObject<T extends object> implements IValueObject<T>
|
|
|
2681
2649
|
*/
|
|
2682
2650
|
declare function voValidated<T>(t: T, validate: (issues: ValidationError, value: T) => void, message?: string): Result<VO<T>, ValidationError>;
|
|
2683
2651
|
|
|
2684
|
-
export { type
|
|
2652
|
+
export { type AggregateClass, type AggregateConfig, AggregateRoot, AggregateSnapshot, AnyDomainEvent, type Command, CommandBus, type CommandHandler, CommitError, CreateDomainEventOptions, DeepEqualExceptOptions, DomainError, Entity, type EventBus, EventBusImpl, type EventHandler, EventSourcedAggregate, IAggregateRoot, type ICommandBus, type IEntity, IEventSourcedAggregate, type IQueryBus, type IQueryableRepository, type IRepository, type IUnitOfWorkRepository, type IValueObject, Id, type Identifiable, IdentityMap, InMemoryOutbox, InfrastructureError, NestedUnitOfWorkError, type OnceOptions, type Outbox, type OutboxRecord, type Query, QueryBus, type QueryHandler, type RepositoryFactories, RollbackError, TransactionClosedError, type TransactionScope, UnitOfWork, type UnitOfWorkContext, type UnitOfWorkDeps, type UnitOfWorkSession, type VO, ValueObject, Version, deepFreeze, entityIds, findEntityById, freezeShallow, hasEntityId, removeEntityById, replaceEntityById, sameEntity, updateEntityById, vo, voEquals, voEqualsExcept, voValidated, voWithValidation, withCommit };
|