@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
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { Result } from '@shirudo/result';
|
|
2
|
+
import { BaseError } from '@shirudo/base-error';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Branded string ID. `Tag` carries the aggregate / entity name so two ids
|
|
6
|
+
* with different tags are not assignable to each other even though both
|
|
7
|
+
* are strings at runtime.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* type UserId = Id<"UserId">;
|
|
12
|
+
* type OrderId = Id<"OrderId">;
|
|
13
|
+
*
|
|
14
|
+
* const u = "user-1" as UserId;
|
|
15
|
+
* const o: OrderId = u; // ❌ compile error
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
type Id<Tag extends string> = string & {
|
|
19
|
+
readonly __brand: Tag;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Produces fresh ids of a single, fixed tag. The tag is bound at the
|
|
23
|
+
* generator type: `IdGenerator<"UserId">.next()` returns `Id<"UserId">`
|
|
24
|
+
* with no caller-side generic to abuse.
|
|
25
|
+
*
|
|
26
|
+
* **Your factory must produce unique ids under concurrent calls.**
|
|
27
|
+
* The kit makes no attempt to dedupe or detect collisions: a collision
|
|
28
|
+
* silently overwrites earlier rows (under unique-key constraints) or
|
|
29
|
+
* silently aliases two different entities (without them). Safe choices:
|
|
30
|
+
* `crypto.randomUUID()` (UUIDv4, the default for events), ULID, UUIDv7,
|
|
31
|
+
* KSUID: all collision-resistant by design. Unsafe choices: `Date.now()`
|
|
32
|
+
* alone (duplicates within the same millisecond), a process-local
|
|
33
|
+
* counter without persistence (resets to 1 on restart, collides with
|
|
34
|
+
* prior runs), a sequential id derived from non-atomic state.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* import { ulid } from "ulid";
|
|
39
|
+
*
|
|
40
|
+
* const userIds: IdGenerator<"UserId"> = { next: () => ulid() as Id<"UserId"> };
|
|
41
|
+
* const id = userIds.next(); // Id<"UserId">
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* The previous shape (`IdGenerator { next<T extends string>(): Id<T> }`)
|
|
45
|
+
* let callers pick `T` themselves: `gen.next<"AnyTag">()` typechecked
|
|
46
|
+
* even when the generator produced different-tag ids, silently defeating
|
|
47
|
+
* the brand.
|
|
48
|
+
*/
|
|
49
|
+
interface IdGenerator<Tag extends string> {
|
|
50
|
+
next: () => Id<Tag>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Abstract base for **domain-invariant violations**. Domain methods
|
|
55
|
+
* (aggregates, entity validation hooks, value-object constructors)
|
|
56
|
+
* throw `DomainError`-derived exceptions when a business rule is
|
|
57
|
+
* violated. Consumers derive their own concrete errors (e.g.
|
|
58
|
+
* `class OrderAlreadyShippedError extends DomainError<"OrderAlreadyShippedError"> {}`)
|
|
59
|
+
* for `instanceof`-style catching at the App-Service boundary, where
|
|
60
|
+
* they typically map to HTTP 400 / business-rule responses.
|
|
61
|
+
*
|
|
62
|
+
* The library itself does **not** ship any concrete `DomainError`
|
|
63
|
+
* subclass: the kit can't know your invariants.
|
|
64
|
+
*
|
|
65
|
+
* Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
|
|
66
|
+
* surface (timestamps, cause chains, `toJSON()`, `getUserMessage()`,
|
|
67
|
+
* `isRetryable`, …).
|
|
68
|
+
*/
|
|
69
|
+
declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Abstract base for **infrastructure / persistence failures** that the
|
|
73
|
+
* App-Service can recover from: typically by retrying, by returning
|
|
74
|
+
* HTTP 404 / 409, or by surfacing a "please try again" UX. These are
|
|
75
|
+
* not domain-invariant violations (the business rules were not
|
|
76
|
+
* broken); they describe race conditions and missing rows at the
|
|
77
|
+
* storage boundary.
|
|
78
|
+
*
|
|
79
|
+
* Library-internal concrete subclasses: {@link AggregateNotFoundError},
|
|
80
|
+
* {@link ConcurrencyConflictError}, {@link DuplicateAggregateError},
|
|
81
|
+
* plus the unit-of-work lifecycle wrappers `CommitError` and
|
|
82
|
+
* `RollbackError` (in `src/app/unit-of-work.ts`).
|
|
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 when an aggregate that was deleted within the current unit of
|
|
109
|
+
* work is saved or re-registered again in the same operation: by
|
|
110
|
+
* `UnitOfWorkSession.enrollSaved` after `enrollDeleted` of the same
|
|
111
|
+
* instance, and by `IdentityMap.set` for a type+id that was deleted.
|
|
112
|
+
* Deletion is final within an operation; saving afterwards would write
|
|
113
|
+
* a row the delete just removed (or resurrect it), which is always a
|
|
114
|
+
* use-case bug.
|
|
115
|
+
*
|
|
116
|
+
* Extends `BaseError` directly (same reasoning as
|
|
117
|
+
* {@link MissingHandlerError}): a programming bug that should crash
|
|
118
|
+
* loud, not be absorbed by a generic infrastructure-error handler.
|
|
119
|
+
*/
|
|
120
|
+
declare class AggregateDeletedError extends BaseError<"AggregateDeletedError"> {
|
|
121
|
+
readonly aggregateId: string;
|
|
122
|
+
constructor(aggregateId: string);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Thrown by `IRepository.getByIdOrFail()` when an aggregate with the
|
|
126
|
+
* given id does not exist. `InfrastructureError` because the storage
|
|
127
|
+
* boundary, not a business rule, decided the row is absent. Use the
|
|
128
|
+
* nullable variant `getById()` if "not found" is a valid outcome.
|
|
129
|
+
*
|
|
130
|
+
* Accepts an optional `cause` so a `Repository.save()` implementation
|
|
131
|
+
* can wrap a lower-level "row not found" / driver-level error without
|
|
132
|
+
* losing context. Cause-chain helpers (`getRootCause`,
|
|
133
|
+
* `findInCauseChain`) from `@shirudo/base-error` traverse the chain.
|
|
134
|
+
*
|
|
135
|
+
* Not retryable: retrying won't make the row appear.
|
|
136
|
+
*/
|
|
137
|
+
declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
|
|
138
|
+
readonly aggregateType: string;
|
|
139
|
+
readonly id: string;
|
|
140
|
+
constructor(aggregateType: string, id: string, cause?: unknown);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Thrown by a repository's `save()` INSERT path when a row with the
|
|
144
|
+
* aggregate's id already exists (unique-constraint violation): two
|
|
145
|
+
* concurrent creators raced on the same business-derived id, or the
|
|
146
|
+
* id generator collided. Same delegation model as
|
|
147
|
+
* {@link ConcurrencyConflictError}: the kit ships the class, the
|
|
148
|
+
* consumer repository maps its driver's unique-violation signal to it
|
|
149
|
+
* instead of letting a raw driver error escape -
|
|
150
|
+
*
|
|
151
|
+
* - Postgres: SQLSTATE `23505` (`unique_violation`)
|
|
152
|
+
* - MySQL/MariaDB: errno `1062` (`ER_DUP_ENTRY`)
|
|
153
|
+
* - SQLite: `SQLITE_CONSTRAINT_UNIQUE` (extended code 2067)
|
|
154
|
+
*
|
|
155
|
+
* `InfrastructureError` because the storage boundary detects the
|
|
156
|
+
* collision. NOT retryable: re-running the same INSERT cannot succeed.
|
|
157
|
+
* The right reactions are domain decisions - map to HTTP 409, or for
|
|
158
|
+
* idempotency-key flows load the existing aggregate and treat the
|
|
159
|
+
* request as already-applied.
|
|
160
|
+
*/
|
|
161
|
+
declare class DuplicateAggregateError extends InfrastructureError<"DuplicateAggregateError"> {
|
|
162
|
+
readonly aggregateType: string;
|
|
163
|
+
readonly aggregateId: string;
|
|
164
|
+
constructor(aggregateType: string, aggregateId: string, cause?: unknown);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Thrown by `IRepository.save()` when the aggregate's expected version
|
|
168
|
+
* does not match the version currently persisted: i.e. another writer
|
|
169
|
+
* updated the aggregate concurrently. The canonical optimistic-
|
|
170
|
+
* concurrency signal; the App-Service typically reloads, re-applies
|
|
171
|
+
* the use case, and retries, or surfaces HTTP 409 to the caller.
|
|
172
|
+
*
|
|
173
|
+
* **Retry means a FRESH unit of work** (a new `UnitOfWork.run()` /
|
|
174
|
+
* `withCommit` invocation): reload, re-apply, save. Do NOT catch this
|
|
175
|
+
* inside the same `run()` callback and continue — the failed aggregate
|
|
176
|
+
* is already enrolled (its events would be committed for a write that
|
|
177
|
+
* never happened) and the identity map still serves the same stale
|
|
178
|
+
* instance to any in-place "reload".
|
|
179
|
+
*
|
|
180
|
+
* `InfrastructureError` because the persistence layer (not a domain
|
|
181
|
+
* rule) detects the race. Marks itself as `retryable: true` so the
|
|
182
|
+
* `isRetryable` predicate from `@shirudo/base-error` picks it up.
|
|
183
|
+
*/
|
|
184
|
+
declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
|
|
185
|
+
readonly aggregateType: string;
|
|
186
|
+
readonly aggregateId: string;
|
|
187
|
+
readonly expectedVersion: number;
|
|
188
|
+
readonly actualVersion: number;
|
|
189
|
+
/**
|
|
190
|
+
* Marks this error as retryable so `isRetryable(err)` returns
|
|
191
|
+
* true. The canonical OCC pattern is to reload the aggregate, re-apply
|
|
192
|
+
* the use case, and retry on this exception.
|
|
193
|
+
*/
|
|
194
|
+
readonly retryable: true;
|
|
195
|
+
constructor(aggregateType: string, aggregateId: string, expectedVersion: number, actualVersion: number, cause?: unknown);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Factory function producing a fresh, unique event identifier for each call.
|
|
200
|
+
*
|
|
201
|
+
* The library ships a default that uses Web Crypto `crypto.randomUUID()`
|
|
202
|
+
* (works on Node 19+, modern browsers in secure contexts, Deno, Bun,
|
|
203
|
+
* Cloudflare Workers, Vercel Edge, and any runtime that implements Web
|
|
204
|
+
* Crypto). Note that `crypto.randomUUID()` returns **UUID v4** (purely
|
|
205
|
+
* random); for production event stores prefer a **time-ordered** id
|
|
206
|
+
* format (UUID v7 / ULID / KSUID) so B-tree indexes on the eventId
|
|
207
|
+
* column stay clustered and `ORDER BY eventId` matches creation order.
|
|
208
|
+
* Swap one in via `setEventIdFactory(() => uuidv7())` or `() => ulid()`.
|
|
209
|
+
*/
|
|
210
|
+
type EventIdFactory = () => string;
|
|
211
|
+
/**
|
|
212
|
+
* Replaces the global event-id factory used by `createDomainEvent` and
|
|
213
|
+
* `createDomainEventWithMetadata`. Call once during application bootstrap,
|
|
214
|
+
* for example:
|
|
215
|
+
*
|
|
216
|
+
* ```ts
|
|
217
|
+
* import { ulid } from "ulid";
|
|
218
|
+
* import { setEventIdFactory } from "@shirudo/ddd-kit";
|
|
219
|
+
*
|
|
220
|
+
* setEventIdFactory(() => ulid());
|
|
221
|
+
* ```
|
|
222
|
+
*
|
|
223
|
+
* The per-call `options.eventId` override always wins over this factory.
|
|
224
|
+
*
|
|
225
|
+
* **Module-scoped: last setter wins.** The factory lives as a single
|
|
226
|
+
* module variable; importing two libraries that both call this races on
|
|
227
|
+
* load order, and parallel test workers will see each other's factory.
|
|
228
|
+
* For test isolation and short-lived contexts prefer
|
|
229
|
+
* {@link withEventIdFactory}; for multi-tenant request isolation
|
|
230
|
+
* (e.g. one factory per tenant in a single Worker invocation) **prefer
|
|
231
|
+
* the per-call `options.eventId`** instead of mutating the global. Same
|
|
232
|
+
* caveat applies to `setClockFactory`.
|
|
233
|
+
*/
|
|
234
|
+
declare function setEventIdFactory(factory: EventIdFactory): void;
|
|
235
|
+
/**
|
|
236
|
+
* Scoped variant of {@link setEventIdFactory}: installs `factory`,
|
|
237
|
+
* runs `fn`, then restores the previous factory in a `finally` block,
|
|
238
|
+
* so the restoration happens even if `fn` throws. Safe for parallel
|
|
239
|
+
* tests and for synchronous request handlers that need a tenant-
|
|
240
|
+
* specific factory without polluting the global.
|
|
241
|
+
*
|
|
242
|
+
* **Synchronous-only, enforced at runtime.** If `fn` returns a
|
|
243
|
+
* thenable (a `Promise` or any object with a `then` method), the
|
|
244
|
+
* helper throws *before* returning the value to the caller. This
|
|
245
|
+
* catches the async-misuse footgun where the factory would be
|
|
246
|
+
* restored before the awaited body of `fn` runs, leaving the awaited
|
|
247
|
+
* code reading the previous factory. For async scoping across `await`
|
|
248
|
+
* boundaries, use `AsyncLocalStorage`, which is out of scope for this
|
|
249
|
+
* helper; build it on top if you need it.
|
|
250
|
+
*
|
|
251
|
+
* Composes by nesting: an inner `withEventIdFactory` restores back to
|
|
252
|
+
* the outer's factory; the outer restores to the original.
|
|
253
|
+
*
|
|
254
|
+
* **When to prefer the per-call `options.eventId` instead.** If you're
|
|
255
|
+
* constructing a single event and want full control over its id,
|
|
256
|
+
* passing `{ eventId: "..." }` to `createDomainEvent` is the strongest
|
|
257
|
+
* isolation: it bypasses the factory mechanism entirely, no global
|
|
258
|
+
* mutation, no scope to manage. Reach for `withEventIdFactory` when
|
|
259
|
+
* the events are constructed deep inside domain methods you can't
|
|
260
|
+
* thread an explicit id through (typical test scenario), or when many
|
|
261
|
+
* events in a scope should share the same factory.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```ts
|
|
265
|
+
* // In a vitest test:
|
|
266
|
+
* it("emits deterministic ids", () => {
|
|
267
|
+
* withEventIdFactory(() => "evt-fixed", () => {
|
|
268
|
+
* const e = createDomainEvent("X", { v: 1 });
|
|
269
|
+
* expect(e.eventId).toBe("evt-fixed");
|
|
270
|
+
* });
|
|
271
|
+
* // Outside the callback the default crypto.randomUUID is restored,
|
|
272
|
+
* // even if the body had thrown.
|
|
273
|
+
* });
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
declare function withEventIdFactory<T>(factory: EventIdFactory, fn: () => T): T;
|
|
277
|
+
/**
|
|
278
|
+
* Restores the default event-id factory (`crypto.randomUUID()`).
|
|
279
|
+
* Intended for use in test `afterEach` hooks.
|
|
280
|
+
*/
|
|
281
|
+
declare function resetEventIdFactory(): void;
|
|
282
|
+
/**
|
|
283
|
+
* Clock function producing a fresh `Date` for each call. The library
|
|
284
|
+
* defaults to `() => new Date()`; override globally via `setClockFactory`
|
|
285
|
+
* for deterministic event-sourcing tests, time-travel debugging, or any
|
|
286
|
+
* scenario where `occurredAt` must be reproducible.
|
|
287
|
+
*/
|
|
288
|
+
type ClockFactory = () => Date;
|
|
289
|
+
/**
|
|
290
|
+
* Replaces the global clock factory used by `createDomainEvent` and
|
|
291
|
+
* `createDomainEventWithMetadata`. Call once during application bootstrap
|
|
292
|
+
* (or per-test in deterministic test suites):
|
|
293
|
+
*
|
|
294
|
+
* ```ts
|
|
295
|
+
* import { setClockFactory } from "@shirudo/ddd-kit";
|
|
296
|
+
*
|
|
297
|
+
* setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
|
|
298
|
+
* ```
|
|
299
|
+
*
|
|
300
|
+
* The per-call `options.occurredAt` override always wins over this
|
|
301
|
+
* factory. Symmetric to `setEventIdFactory`.
|
|
302
|
+
*
|
|
303
|
+
* Module-scoped: see {@link setEventIdFactory} for the global-state
|
|
304
|
+
* caveats. For test isolation prefer {@link withClockFactory}; for
|
|
305
|
+
* multi-tenant request isolation prefer the per-call
|
|
306
|
+
* `options.occurredAt`.
|
|
307
|
+
*/
|
|
308
|
+
declare function setClockFactory(factory: ClockFactory): void;
|
|
309
|
+
/**
|
|
310
|
+
* Scoped variant of {@link setClockFactory}: installs `factory`, runs
|
|
311
|
+
* `fn`, then restores the previous factory in a `finally` block.
|
|
312
|
+
* Synchronous-only, with the same constraints (and same runtime thenable
|
|
313
|
+
* guard) as {@link withEventIdFactory}.
|
|
314
|
+
*
|
|
315
|
+
* **When to prefer the per-call `options.occurredAt` instead.** Same
|
|
316
|
+
* trade-off as {@link withEventIdFactory}: passing `{ occurredAt }`
|
|
317
|
+
* to `createDomainEvent` is the strongest isolation for single-event
|
|
318
|
+
* cases. The scoped helper is for events constructed deep inside
|
|
319
|
+
* domain methods where threading an explicit timestamp is awkward.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```ts
|
|
323
|
+
* it("stamps events with a fixed clock", () => {
|
|
324
|
+
* const fixed = new Date("2026-01-01T00:00:00Z");
|
|
325
|
+
* withClockFactory(() => fixed, () => {
|
|
326
|
+
* const e = createDomainEvent("X", { v: 1 });
|
|
327
|
+
* expect(e.occurredAt).toEqual(fixed);
|
|
328
|
+
* });
|
|
329
|
+
* });
|
|
330
|
+
* ```
|
|
331
|
+
*/
|
|
332
|
+
declare function withClockFactory<T>(factory: ClockFactory, fn: () => T): T;
|
|
333
|
+
/**
|
|
334
|
+
* Restores the default clock factory (`() => new Date()`).
|
|
335
|
+
* Intended for use in test `afterEach` hooks.
|
|
336
|
+
*/
|
|
337
|
+
declare function resetClockFactory(): void;
|
|
338
|
+
/**
|
|
339
|
+
* Metadata associated with a domain event for traceability and correlation.
|
|
340
|
+
* Used in event-driven architectures to track event flow across services.
|
|
341
|
+
*/
|
|
342
|
+
interface EventMetadata {
|
|
343
|
+
/**
|
|
344
|
+
* Correlation ID for tracing events across multiple services/components.
|
|
345
|
+
* Typically used to group related events in a distributed system.
|
|
346
|
+
*/
|
|
347
|
+
correlationId?: string;
|
|
348
|
+
/**
|
|
349
|
+
* Causation ID referencing the event or command that caused this event.
|
|
350
|
+
* Used to build event chains and understand causality.
|
|
351
|
+
*/
|
|
352
|
+
causationId?: string;
|
|
353
|
+
/**
|
|
354
|
+
* User ID of the person or system that triggered the event.
|
|
355
|
+
*/
|
|
356
|
+
userId?: string;
|
|
357
|
+
/**
|
|
358
|
+
* Source service or component that produced the event.
|
|
359
|
+
*/
|
|
360
|
+
source?: string;
|
|
361
|
+
/**
|
|
362
|
+
* Additional custom metadata fields.
|
|
363
|
+
* Allows extensibility for domain-specific metadata.
|
|
364
|
+
*/
|
|
365
|
+
[key: string]: unknown;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Domain Event represents something meaningful that happened in the domain.
|
|
369
|
+
* Events are immutable and carry information about what occurred.
|
|
370
|
+
*
|
|
371
|
+
* **Events are PLAIN DATA objects**, constructed via `createDomainEvent`
|
|
372
|
+
* (or the aggregate's `recordEvent` helper) and deeply frozen. Class-based
|
|
373
|
+
* event objects that satisfy this shape structurally via prototype
|
|
374
|
+
* members are unsupported: the `withCommit` harvest copies events with a
|
|
375
|
+
* shallow spread (to stamp `aggregateVersion`), which only carries own
|
|
376
|
+
* enumerable properties.
|
|
377
|
+
*
|
|
378
|
+
* **Field-accretion boundary.** This type already carries the write-side
|
|
379
|
+
* transport concerns the outbox needs (`aggregateId`, `aggregateType`,
|
|
380
|
+
* `aggregateVersion`, `metadata`). That is the line: further transport
|
|
381
|
+
* fields (partition keys, tenancy, schema URNs, …) belong in an outbox
|
|
382
|
+
* envelope / `metadata`, not on the domain event — the next first-class
|
|
383
|
+
* transport field forces an `OutboxMessage` envelope port instead.
|
|
384
|
+
*
|
|
385
|
+
* @template T - The event type name (e.g., "OrderCreated")
|
|
386
|
+
* @template P - The event payload type
|
|
387
|
+
*/
|
|
388
|
+
interface DomainEvent<T extends string, P = void> {
|
|
389
|
+
/**
|
|
390
|
+
* Unique identifier for this specific event instance. Used by idempotent
|
|
391
|
+
* consumers, outbox dispatch tracking, and as the target of
|
|
392
|
+
* `metadata.causationId`. Defaults to `crypto.randomUUID()` if not
|
|
393
|
+
* supplied.
|
|
394
|
+
*/
|
|
395
|
+
eventId: string;
|
|
396
|
+
/**
|
|
397
|
+
* The type of the event, used for routing and handling.
|
|
398
|
+
*/
|
|
399
|
+
type: T;
|
|
400
|
+
/**
|
|
401
|
+
* Identifier of the aggregate that produced the event. Optional at the
|
|
402
|
+
* library level; set it whenever the producing aggregate is known so
|
|
403
|
+
* downstream subscribers, outboxes, and projections can scope by entity.
|
|
404
|
+
*/
|
|
405
|
+
aggregateId?: string;
|
|
406
|
+
/**
|
|
407
|
+
* Name of the aggregate type that produced the event (e.g. "Order").
|
|
408
|
+
* Pairs with `aggregateId` to fully qualify the source aggregate.
|
|
409
|
+
*/
|
|
410
|
+
aggregateType?: string;
|
|
411
|
+
/**
|
|
412
|
+
* The event payload containing the domain data. The field is always
|
|
413
|
+
* present; its value is `undefined` when `P` is `void`.
|
|
414
|
+
*/
|
|
415
|
+
payload: P;
|
|
416
|
+
/**
|
|
417
|
+
* Timestamp when the event occurred.
|
|
418
|
+
*/
|
|
419
|
+
occurredAt: Date;
|
|
420
|
+
/**
|
|
421
|
+
* Event schema version for handling schema evolution.
|
|
422
|
+
* Required for safe schema migration in event-sourced systems.
|
|
423
|
+
* Use 1 for the initial schema version.
|
|
424
|
+
*
|
|
425
|
+
* **NOT the aggregate's version** — that is
|
|
426
|
+
* {@link aggregateVersion}. The two are deliberately distinct
|
|
427
|
+
* fields: this one says "which shape does the payload have"
|
|
428
|
+
* (upcasting), the other says "which state revision of the
|
|
429
|
+
* aggregate emitted this".
|
|
430
|
+
*/
|
|
431
|
+
version: number;
|
|
432
|
+
/**
|
|
433
|
+
* The version of the producing aggregate at COMMIT time: the same
|
|
434
|
+
* value the OCC row write carries. Stamped automatically by
|
|
435
|
+
* `withCommit` at the harvest boundary (all events of one aggregate
|
|
436
|
+
* in one commit share it; their relative order within the commit is
|
|
437
|
+
* the harvest order), or set manually via
|
|
438
|
+
* `CreateDomainEventOptions.aggregateVersion` — a pre-set value is
|
|
439
|
+
* never overwritten.
|
|
440
|
+
*
|
|
441
|
+
* Consumers use it for ordering ("apply projections up to aggregate
|
|
442
|
+
* version N"), idempotency watermarks, debugging, and integration
|
|
443
|
+
* logs. Optional at the type level: events created outside an
|
|
444
|
+
* aggregate (system/integration events) and events from older kit
|
|
445
|
+
* versions don't carry it.
|
|
446
|
+
*/
|
|
447
|
+
aggregateVersion?: number;
|
|
448
|
+
/**
|
|
449
|
+
* Optional metadata for traceability, correlation, and auditing.
|
|
450
|
+
* Includes correlationId, causationId, userId, source, and custom fields.
|
|
451
|
+
*/
|
|
452
|
+
metadata?: EventMetadata;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Upper-bound alias for "any `DomainEvent` shape". Use as a generic
|
|
456
|
+
* constraint when a type parameter should accept any concrete event
|
|
457
|
+
* union. The `unknown` payload is the upper bound; concrete unions
|
|
458
|
+
* still narrow via `Extract<Evt, { type: K }>` at the use-site.
|
|
459
|
+
*/
|
|
460
|
+
type AnyDomainEvent = DomainEvent<string, unknown>;
|
|
461
|
+
/**
|
|
462
|
+
* Shared option bag for the `createDomainEvent*` factories.
|
|
463
|
+
*/
|
|
464
|
+
interface CreateDomainEventOptions {
|
|
465
|
+
/**
|
|
466
|
+
* Override for the auto-generated `eventId`. Pass an existing id (for
|
|
467
|
+
* replay, tests, or deterministic event sourcing) instead of letting the
|
|
468
|
+
* factory call `crypto.randomUUID()`.
|
|
469
|
+
*/
|
|
470
|
+
eventId?: string;
|
|
471
|
+
/**
|
|
472
|
+
* Identifier of the aggregate that produced the event.
|
|
473
|
+
*/
|
|
474
|
+
aggregateId?: string;
|
|
475
|
+
/**
|
|
476
|
+
* Name of the aggregate type that produced the event.
|
|
477
|
+
*/
|
|
478
|
+
aggregateType?: string;
|
|
479
|
+
/**
|
|
480
|
+
* Override for the auto-generated `occurredAt` timestamp.
|
|
481
|
+
*/
|
|
482
|
+
occurredAt?: Date;
|
|
483
|
+
/**
|
|
484
|
+
* Override for the default schema version (1).
|
|
485
|
+
*/
|
|
486
|
+
version?: number;
|
|
487
|
+
/**
|
|
488
|
+
* Pre-set the producing aggregate's version (see
|
|
489
|
+
* `DomainEvent.aggregateVersion`). Normally left unset — `withCommit`
|
|
490
|
+
* stamps it at the harvest boundary with the commit version — but
|
|
491
|
+
* useful for replay fixtures and events constructed outside an
|
|
492
|
+
* aggregate. A pre-set value is never overwritten by the harvest.
|
|
493
|
+
*/
|
|
494
|
+
aggregateVersion?: number;
|
|
495
|
+
/**
|
|
496
|
+
* Event metadata: correlation, causation, user, source, custom fields.
|
|
497
|
+
*/
|
|
498
|
+
metadata?: EventMetadata;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Creates a domain event with default values.
|
|
502
|
+
* Sets occurredAt to current date and version to 1 if not provided.
|
|
503
|
+
*
|
|
504
|
+
* **For aggregate-internal events, prefer `this.recordEvent(...)` on
|
|
505
|
+
* `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
|
|
506
|
+
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
507
|
+
* aggregate's declared `aggregateType` property), which downstream
|
|
508
|
+
* consumers (outbox dispatchers, projection handlers, audit logs)
|
|
509
|
+
* route by. The `withCommit` harvest boundary now validates both fields
|
|
510
|
+
* are present and throws if they're missing, so a direct
|
|
511
|
+
* `createDomainEvent(...)` call inside an aggregate that forgets the
|
|
512
|
+
* options is caught at runtime.
|
|
513
|
+
*
|
|
514
|
+
* Use `createDomainEvent(...)` directly for events that don't belong to
|
|
515
|
+
* an aggregate: system events, integration events, configuration events,
|
|
516
|
+
* test fixtures. For those, set `aggregateId` / `aggregateType` in
|
|
517
|
+
* `options` if downstream consumers expect routing metadata.
|
|
518
|
+
*
|
|
519
|
+
* @param type - The event type
|
|
520
|
+
* @param payload - The event payload
|
|
521
|
+
* @param options - Optional event configuration (including `aggregateId`
|
|
522
|
+
* and `aggregateType` for routing)
|
|
523
|
+
* @returns A domain event
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```typescript
|
|
527
|
+
* const event = createDomainEvent("OrderCreated", { orderId: "123" });
|
|
528
|
+
* ```
|
|
529
|
+
*/
|
|
530
|
+
declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
|
|
531
|
+
declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
|
|
532
|
+
/**
|
|
533
|
+
* Creates a domain event with metadata for traceability.
|
|
534
|
+
* Convenience function for creating events with correlation and causation IDs.
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* ```typescript
|
|
538
|
+
* const event = createDomainEventWithMetadata(
|
|
539
|
+
* "OrderCreated",
|
|
540
|
+
* { orderId: "123" },
|
|
541
|
+
* { correlationId: "corr-123", causationId: "cmd-456", userId: "user-789" }
|
|
542
|
+
* );
|
|
543
|
+
* ```
|
|
544
|
+
*/
|
|
545
|
+
declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
|
|
546
|
+
/**
|
|
547
|
+
* Copies metadata from a source event to a new event.
|
|
548
|
+
* Useful for maintaining correlation chains in event-driven architectures.
|
|
549
|
+
*
|
|
550
|
+
* @example
|
|
551
|
+
* ```typescript
|
|
552
|
+
* const newEvent = createDomainEvent(
|
|
553
|
+
* "OrderShipped",
|
|
554
|
+
* { orderId: "123" },
|
|
555
|
+
* { metadata: copyMetadata(previousEvent, { causationId: previousEvent.type }) }
|
|
556
|
+
* );
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
declare function copyMetadata(sourceEvent: AnyDomainEvent, additionalMetadata?: Partial<EventMetadata>): EventMetadata;
|
|
560
|
+
/**
|
|
561
|
+
* Merges multiple metadata objects into one.
|
|
562
|
+
* Later metadata objects override earlier ones for the same keys.
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* ```typescript
|
|
566
|
+
* const metadata = mergeMetadata(
|
|
567
|
+
* { correlationId: "corr-123" },
|
|
568
|
+
* { userId: "user-456" },
|
|
569
|
+
* { source: "order-service" }
|
|
570
|
+
* );
|
|
571
|
+
* ```
|
|
572
|
+
*/
|
|
573
|
+
declare function mergeMetadata(...metadataObjects: Array<EventMetadata | undefined>): EventMetadata;
|
|
574
|
+
|
|
575
|
+
type Version = number & {
|
|
576
|
+
readonly __v: true;
|
|
577
|
+
};
|
|
578
|
+
/**
|
|
579
|
+
* Snapshot of an aggregate state at a specific point in time.
|
|
580
|
+
* Used for optimizing event replay by starting from a snapshot
|
|
581
|
+
* instead of replaying all events from the beginning.
|
|
582
|
+
*
|
|
583
|
+
* @template TState - The type of the aggregate state
|
|
584
|
+
*/
|
|
585
|
+
interface AggregateSnapshot<TState> {
|
|
586
|
+
/**
|
|
587
|
+
* The state of the aggregate at the time of the snapshot.
|
|
588
|
+
*/
|
|
589
|
+
state: TState;
|
|
590
|
+
/**
|
|
591
|
+
* The version of the aggregate when the snapshot was taken.
|
|
592
|
+
*/
|
|
593
|
+
version: Version;
|
|
594
|
+
/**
|
|
595
|
+
* Timestamp when the snapshot was created.
|
|
596
|
+
*/
|
|
597
|
+
snapshotAt: Date;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Public contract every Aggregate Root satisfies. Implemented by
|
|
601
|
+
* `BaseAggregate` and inherited by both `AggregateRoot` and
|
|
602
|
+
* `EventSourcedAggregate`. Repository implementations type their
|
|
603
|
+
* `save(aggregate)` parameter against this interface rather than the
|
|
604
|
+
* concrete classes, so the repo layer does not take a compile-time
|
|
605
|
+
* dependency on the aggregate hierarchy.
|
|
606
|
+
*
|
|
607
|
+
* Full per-member documentation lives on the concrete `BaseAggregate`
|
|
608
|
+
* class; the interface is intentionally terse to avoid drift.
|
|
609
|
+
*
|
|
610
|
+
* @template TId - The aggregate root identifier (branded via `Id<Tag>`)
|
|
611
|
+
* @template TEvent - The domain-event union, defaults to `never`
|
|
612
|
+
*/
|
|
613
|
+
interface IAggregateRoot<TId extends Id<string>, TEvent = never> {
|
|
614
|
+
readonly id: TId;
|
|
615
|
+
readonly version: Version;
|
|
616
|
+
readonly persistedVersion: Version | undefined;
|
|
617
|
+
readonly pendingEvents: ReadonlyArray<TEvent>;
|
|
618
|
+
clearPendingEvents(): void;
|
|
619
|
+
markPersisted(version: Version): void;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Public contract for Event-Sourced Aggregate Roots. Extends
|
|
623
|
+
* `IAggregateRoot` with the replay-from-history boundary.
|
|
624
|
+
*
|
|
625
|
+
* @template TId - The aggregate root identifier
|
|
626
|
+
* @template TEvent - The union type of all domain events
|
|
627
|
+
*/
|
|
628
|
+
interface IEventSourcedAggregate<TId extends Id<string>, TEvent extends AnyDomainEvent> extends IAggregateRoot<TId, TEvent> {
|
|
629
|
+
/**
|
|
630
|
+
* Reconstitutes the aggregate from an event history. Returns
|
|
631
|
+
* `Result` because event-stream corruption is an expected
|
|
632
|
+
* recoverable failure at the infrastructure boundary.
|
|
633
|
+
*/
|
|
634
|
+
loadFromHistory(history: ReadonlyArray<TEvent>): Result<void, DomainError>;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Checks if two aggregates are at the same version (same ID and version).
|
|
638
|
+
* Useful for optimistic concurrency control checks.
|
|
639
|
+
*
|
|
640
|
+
* Note: Two aggregates with the same ID ARE the same aggregate (identity).
|
|
641
|
+
* This function checks if they are at the same version: i.e., no concurrent modification.
|
|
642
|
+
*
|
|
643
|
+
* @example
|
|
644
|
+
* ```typescript
|
|
645
|
+
* const before = await repository.getById(id);
|
|
646
|
+
* // ... some operations ...
|
|
647
|
+
* const after = await repository.getById(id);
|
|
648
|
+
*
|
|
649
|
+
* if (!sameVersion(before, after)) {
|
|
650
|
+
* throw new Error("Aggregate was modified by another process");
|
|
651
|
+
* }
|
|
652
|
+
* ```
|
|
653
|
+
*/
|
|
654
|
+
declare function sameVersion<TId extends Id<string>>(a: {
|
|
655
|
+
id: TId;
|
|
656
|
+
version: Version;
|
|
657
|
+
}, b: {
|
|
658
|
+
id: TId;
|
|
659
|
+
version: Version;
|
|
660
|
+
}): boolean;
|
|
661
|
+
|
|
662
|
+
export { type AnyDomainEvent as A, type CreateDomainEventOptions as C, DomainError as D, type EventIdFactory as E, type IAggregateRoot as I, MissingHandlerError as M, type Version as V, type Id as a, type AggregateSnapshot as b, type IEventSourcedAggregate as c, InfrastructureError as d, setEventIdFactory as e, type ClockFactory as f, setClockFactory as g, withClockFactory as h, resetClockFactory as i, type EventMetadata as j, type DomainEvent as k, createDomainEvent as l, createDomainEventWithMetadata as m, copyMetadata as n, mergeMetadata as o, AggregateDeletedError as p, AggregateNotFoundError as q, resetEventIdFactory as r, sameVersion as s, DuplicateAggregateError as t, ConcurrencyConflictError as u, type IdGenerator as v, withEventIdFactory as w };
|
package/dist/http.d.ts
CHANGED
|
@@ -22,13 +22,13 @@ interface ValidationProblemOptions extends Omit<ProblemDetailsOptions, "extensio
|
|
|
22
22
|
* object with the collected field issues attached under an extension member.
|
|
23
23
|
*
|
|
24
24
|
* base-error is **safe by default**: `ValidationError.toProblemDetails()` does
|
|
25
|
-
* not expose the issues on its own
|
|
25
|
+
* not expose the issues on its own: they only cross to a client through the
|
|
26
26
|
* `publicIssues()` whitelist. This helper performs that explicit projection and
|
|
27
27
|
* applies sensible validation defaults (`422`, `"Validation Failed"`), so the
|
|
28
28
|
* common boundary case is a one-liner instead of a footgun.
|
|
29
29
|
*
|
|
30
30
|
* This is a presentation/transport concern and ships from the opt-in
|
|
31
|
-
* `@shirudo/ddd-kit/http` entry point
|
|
31
|
+
* `@shirudo/ddd-kit/http` entry point: the core kit stays transport-free.
|
|
32
32
|
*
|
|
33
33
|
* @example
|
|
34
34
|
* ```ts
|