@shirudo/ddd-kit 1.3.0 → 2.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 +6 -3
- package/dist/{aggregate-BGdgvqKh.d.ts → aggregate-DFi6HlEh.d.ts} +143 -88
- package/dist/http.d.ts +36 -13
- package/dist/http.js +15 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +916 -182
- package/dist/index.js +2017 -376
- package/dist/index.js.map +1 -1
- package/dist/presentation.d.ts +54 -0
- package/dist/presentation.js +45 -0
- package/dist/presentation.js.map +1 -0
- package/dist/testing.d.ts +124 -2
- package/dist/testing.js +535 -116
- package/dist/testing.js.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +160 -18
- package/dist/utils.js.map +1 -1
- package/package.json +20 -13
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @shirudo/ddd-kit
|
|
2
2
|
|
|
3
|
-
Composable TypeScript toolkit for tactical Domain-Driven Design. Ships the canonical building blocks (Value Objects, Entities, Aggregate Roots, Domain Events, Repositories, and CQRS handlers) without a framework or runtime lock-in. ESM-only; runs on Node
|
|
3
|
+
Composable TypeScript toolkit for tactical Domain-Driven Design. Ships the canonical building blocks (Value Objects, Entities, Aggregate Roots, Domain Events, Repositories, and CQRS handlers) without a framework or runtime lock-in. ESM-only; runs on Node 20+, Cloudflare Workers, Vercel Edge, Deno, and Bun.
|
|
4
4
|
|
|
5
|
-
> **Stable: 1
|
|
5
|
+
> **Stable: 2.1**
|
|
6
6
|
>
|
|
7
7
|
> The public API is stable and follows [Semantic Versioning](https://semver.org/). Breaking changes bump the major and ship with a migration path in the [CHANGELOG](https://github.com/shi-rudo/ddd-kit-ts/blob/main/CHANGELOG.md).
|
|
8
8
|
|
|
@@ -15,11 +15,13 @@ Composable TypeScript toolkit for tactical Domain-Driven Design. Ships the canon
|
|
|
15
15
|
- **Entities:** identity + lifecycle, with collection helpers branded by `Id<Tag>`.
|
|
16
16
|
- **Aggregate Roots:** state-stored (`AggregateRoot`) and event-sourced (`EventSourcedAggregate`), with optimistic-concurrency versioning.
|
|
17
17
|
- **Domain Events:** typed, deeply frozen, carry metadata for traceability and schema evolution.
|
|
18
|
+
- **Domain State Machine:** finite, named domain states with typed context, guards, reducers, terminal states, and value outputs for aggregate lifecycles and process managers.
|
|
18
19
|
- **Repositories:** technology-agnostic persistence ports with an Identity-Map contract and OCC.
|
|
19
20
|
- **CQRS:** zero-config in-memory `CommandBus` / `QueryBus`, plus `CommandHandler` / `QueryHandler` types for external brokers.
|
|
20
21
|
- **Unit of Work:** opt-in `UnitOfWork` facade with tx-bound repositories, repository-side enrollment, a per-operation Identity Map, and aggregate-level dirty tracking (`changedKeys` / `hasChanges`) for partial writes. Honestly speaking: a transaction coordinator with registration and Identity Map; writes stay explicit by design (no auto-flush).
|
|
21
22
|
- **Outbox:** `withCommit` harvests pending events inside the transaction, stamps them with the aggregate's commit version, and publishes them atomically.
|
|
22
|
-
- **
|
|
23
|
+
- **Event Store:** `EventStore` port with expectedVersion-guarded appends and snapshot catch-up reads, plus `InMemoryEventStore` as the reference implementation.
|
|
24
|
+
- **Repository contract tests:** `@shirudo/ddd-kit/testing` ships the suites (state-stored and event-sourced) every adapter must pass: OCC is a testable contract, not a documented pattern.
|
|
23
25
|
- **Result-first boundary:** a typed error hierarchy on [`@shirudo/base-error`](https://www.npmjs.com/package/@shirudo/base-error) and `Result` from [`@shirudo/result`](https://www.npmjs.com/package/@shirudo/result); `voValidated` collects field violations and renders RFC 9457 via the opt-in `@shirudo/ddd-kit/http` entry.
|
|
24
26
|
|
|
25
27
|
## Installation
|
|
@@ -58,6 +60,7 @@ Each building block has a dedicated guide. Start with [Design Decisions](https:/
|
|
|
58
60
|
| Aggregate Roots, factories, reconstitution | [Aggregate Roots](https://github.com/shi-rudo/ddd-kit-ts/blob/main/docs/guide/aggregates.md) |
|
|
59
61
|
| Event sourcing (`apply`, replay, snapshots) | [Event Sourcing](https://github.com/shi-rudo/ddd-kit-ts/blob/main/docs/guide/event-sourcing.md) |
|
|
60
62
|
| Domain Events (`createDomainEvent`, metadata) | [Domain Events](https://github.com/shi-rudo/ddd-kit-ts/blob/main/docs/guide/domain-events.md) |
|
|
63
|
+
| Domain State Machine (`DomainStateMachine`, `transitionDomainState`) | [Domain State Machine](https://github.com/shi-rudo/ddd-kit-ts/blob/main/docs/guide/domain-state-machine.md) |
|
|
61
64
|
| Errors: throw vs Result, `ValidationError`, RFC 9457 | [Result vs Throw](https://github.com/shi-rudo/ddd-kit-ts/blob/main/docs/guide/result-vs-throw.md) |
|
|
62
65
|
| Commands, queries, buses | [CQRS & Buses](https://github.com/shi-rudo/ddd-kit-ts/blob/main/docs/guide/cqrs-and-buses.md) |
|
|
63
66
|
| Repositories, Identity Map, OCC | [Repository](https://github.com/shi-rudo/ddd-kit-ts/blob/main/docs/guide/repository.md) |
|
|
@@ -63,8 +63,10 @@ interface IdGenerator<Tag extends string> {
|
|
|
63
63
|
* subclass: the kit can't know your invariants.
|
|
64
64
|
*
|
|
65
65
|
* Extends `BaseError<Name>`; see `@shirudo/base-error` for the inherited
|
|
66
|
-
* surface (timestamps, cause chains, `toJSON()`, `
|
|
67
|
-
*
|
|
66
|
+
* surface (timestamps, cause chains, `toJSON()`, `isRetryable`, …). For
|
|
67
|
+
* client-safe / localized messages, project errors through the opt-in
|
|
68
|
+
* `@shirudo/base-error/presentation` subpath at the boundary; the technical
|
|
69
|
+
* core deliberately carries no user-facing message.
|
|
68
70
|
*/
|
|
69
71
|
declare abstract class DomainError<Name extends string = string> extends BaseError<Name> {
|
|
70
72
|
}
|
|
@@ -104,6 +106,29 @@ declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
|
|
|
104
106
|
readonly eventType: string;
|
|
105
107
|
constructor(eventType: string, cause?: unknown);
|
|
106
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Thrown by `EventSourcedAggregate.loadFromHistory` and
|
|
111
|
+
* `restoreFromSnapshotWithEvents` when the replay target is not fresh:
|
|
112
|
+
* the aggregate carries unflushed `pendingEvents`, or (for
|
|
113
|
+
* `loadFromHistory`) an in-memory version that was never persisted.
|
|
114
|
+
* Replaying onto such an instance would `markRestored` a
|
|
115
|
+
* `persistedVersion` that counts unpersisted history: repository routing
|
|
116
|
+
* flips from INSERT to UPDATE (or appends with a wrong expected version)
|
|
117
|
+
* and harvested events would claim a version baseline the stream does
|
|
118
|
+
* not carry.
|
|
119
|
+
*
|
|
120
|
+
* Deliberately **not** a `DomainError` or `InfrastructureError` (same
|
|
121
|
+
* posture as {@link MissingHandlerError}): a deterministic programming
|
|
122
|
+
* bug in how the aggregate was constructed before replay. It propagates
|
|
123
|
+
* as a throw instead of riding the replay methods' `Result` channel, so
|
|
124
|
+
* a generic corrupted-stream handler cannot absorb it. Reconstitution
|
|
125
|
+
* belongs on a bare instance: construct the aggregate without
|
|
126
|
+
* factory-recorded events or prior mutations, then replay.
|
|
127
|
+
*/
|
|
128
|
+
declare class UnreplayableAggregateError extends BaseError<"UnreplayableAggregateError"> {
|
|
129
|
+
readonly aggregateId: string;
|
|
130
|
+
constructor(aggregateId: string, reason: string);
|
|
131
|
+
}
|
|
107
132
|
/**
|
|
108
133
|
* Thrown by `withCommit` when an event harvested from an aggregate cannot
|
|
109
134
|
* be safely committed: it is missing `aggregateId` / `aggregateType`
|
|
@@ -129,6 +154,34 @@ declare class EventHarvestError extends BaseError<"EventHarvestError"> {
|
|
|
129
154
|
/** The `type` of the offending event, for programmatic routing. */
|
|
130
155
|
eventType?: string | undefined);
|
|
131
156
|
}
|
|
157
|
+
/** Constructor options for {@link UnregisteredHandlerError}. */
|
|
158
|
+
interface UnregisteredHandlerErrorOptions {
|
|
159
|
+
/** Which bus rejected the dispatch. */
|
|
160
|
+
readonly busKind: "command" | "query";
|
|
161
|
+
/** The message type no handler was registered for. */
|
|
162
|
+
readonly messageType: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Produced by the in-memory `CommandBus` / `QueryBus` when a message is
|
|
166
|
+
* dispatched for a type no handler was registered under: a wiring bug
|
|
167
|
+
* (typo in the type string, missing `register` call at bootstrap), not
|
|
168
|
+
* a domain or infrastructure failure.
|
|
169
|
+
*
|
|
170
|
+
* Extends `BaseError` directly (same crash-loud family as
|
|
171
|
+
* {@link MissingHandlerError}): a generic `catch (e instanceof
|
|
172
|
+
* DomainError)` handler must not absorb it. For 2.x compatibility the
|
|
173
|
+
* buses still deliver it through their error CHANNEL (`err(...)` via the
|
|
174
|
+
* `errorMapper`, so the default string channel keeps its exact message)
|
|
175
|
+
* rather than throwing; the named type exists so a typed error channel
|
|
176
|
+
* can route it explicitly instead of pattern-matching message strings.
|
|
177
|
+
* `QueryBus.executeUnsafe` throws it directly. v3 makes `execute` throw
|
|
178
|
+
* it too.
|
|
179
|
+
*/
|
|
180
|
+
declare class UnregisteredHandlerError extends BaseError<"UnregisteredHandlerError"> {
|
|
181
|
+
readonly busKind: "command" | "query";
|
|
182
|
+
readonly messageType: string;
|
|
183
|
+
constructor(options: UnregisteredHandlerErrorOptions);
|
|
184
|
+
}
|
|
132
185
|
/**
|
|
133
186
|
* Thrown at the end of a `UnitOfWork.run` when an aggregate that was
|
|
134
187
|
* loaded into the identity map during the operation carries unflushed
|
|
@@ -188,10 +241,16 @@ declare class AggregateDeletedError extends BaseError<"AggregateDeletedError"> {
|
|
|
188
241
|
*
|
|
189
242
|
* Not retryable: retrying won't make the row appear.
|
|
190
243
|
*/
|
|
244
|
+
interface AggregateNotFoundErrorOptions {
|
|
245
|
+
readonly aggregateType: string;
|
|
246
|
+
readonly id: string;
|
|
247
|
+
/** Optional lower-level error to preserve in the cause chain. */
|
|
248
|
+
readonly cause?: unknown;
|
|
249
|
+
}
|
|
191
250
|
declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFoundError"> {
|
|
192
251
|
readonly aggregateType: string;
|
|
193
252
|
readonly id: string;
|
|
194
|
-
constructor(
|
|
253
|
+
constructor(options: AggregateNotFoundErrorOptions);
|
|
195
254
|
}
|
|
196
255
|
/**
|
|
197
256
|
* Thrown by a repository's `save()` INSERT path when a row with the
|
|
@@ -212,10 +271,46 @@ declare class AggregateNotFoundError extends InfrastructureError<"AggregateNotFo
|
|
|
212
271
|
* idempotency-key flows load the existing aggregate and treat the
|
|
213
272
|
* request as already-applied.
|
|
214
273
|
*/
|
|
274
|
+
interface DuplicateAggregateErrorOptions {
|
|
275
|
+
readonly aggregateType: string;
|
|
276
|
+
readonly aggregateId: string;
|
|
277
|
+
/** Optional driver-level error to preserve in the cause chain. */
|
|
278
|
+
readonly cause?: unknown;
|
|
279
|
+
}
|
|
215
280
|
declare class DuplicateAggregateError extends InfrastructureError<"DuplicateAggregateError"> {
|
|
216
281
|
readonly aggregateType: string;
|
|
217
282
|
readonly aggregateId: string;
|
|
218
|
-
constructor(
|
|
283
|
+
constructor(options: DuplicateAggregateErrorOptions);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Thrown on snapshot restore (`restoreFromSnapshot`,
|
|
287
|
+
* `restoreFromSnapshotWithEvents`) when the stored snapshot carries a
|
|
288
|
+
* different schema version than the aggregate's declared
|
|
289
|
+
* `snapshotSchemaVersion` and no `migrateSnapshotState` override handles
|
|
290
|
+
* the upgrade. Without the check, a snapshot written against an older
|
|
291
|
+
* `TSnapshotState` shape would surface as an undefined-field crash on
|
|
292
|
+
* the first method call after a much later restore.
|
|
293
|
+
*
|
|
294
|
+
* `InfrastructureError` because the storage boundary served outdated
|
|
295
|
+
* data; the schema evolving past stored snapshots is an expected
|
|
296
|
+
* lifecycle event, not a programming bug. NOT retryable: the recovery
|
|
297
|
+
* is a code path, not a repeat. Either override `migrateSnapshotState`
|
|
298
|
+
* on the aggregate (upgrade old shapes in place), or catch this error
|
|
299
|
+
* in the repository, discard the snapshot, and refold from the full
|
|
300
|
+
* event stream / reload from the source of truth.
|
|
301
|
+
*/
|
|
302
|
+
interface SnapshotSchemaMismatchErrorOptions {
|
|
303
|
+
readonly aggregateType: string;
|
|
304
|
+
readonly aggregateId: string;
|
|
305
|
+
readonly expectedSchemaVersion: number;
|
|
306
|
+
readonly actualSchemaVersion: number;
|
|
307
|
+
}
|
|
308
|
+
declare class SnapshotSchemaMismatchError extends InfrastructureError<"SnapshotSchemaMismatchError"> {
|
|
309
|
+
readonly aggregateType: string;
|
|
310
|
+
readonly aggregateId: string;
|
|
311
|
+
readonly expectedSchemaVersion: number;
|
|
312
|
+
readonly actualSchemaVersion: number;
|
|
313
|
+
constructor(options: SnapshotSchemaMismatchErrorOptions);
|
|
219
314
|
}
|
|
220
315
|
/**
|
|
221
316
|
* Thrown by `IRepository.save()` when the aggregate's expected version
|
|
@@ -235,18 +330,26 @@ declare class DuplicateAggregateError extends InfrastructureError<"DuplicateAggr
|
|
|
235
330
|
* rule) detects the race. Marks itself as `retryable: true` so the
|
|
236
331
|
* `isRetryable` predicate from `@shirudo/base-error` picks it up.
|
|
237
332
|
*/
|
|
238
|
-
|
|
333
|
+
interface ConcurrencyConflictErrorOptions {
|
|
239
334
|
readonly aggregateType: string;
|
|
240
335
|
readonly aggregateId: string;
|
|
241
336
|
readonly expectedVersion: number;
|
|
242
337
|
readonly actualVersion: number;
|
|
338
|
+
/** Optional driver-level error to preserve in the cause chain. */
|
|
339
|
+
readonly cause?: unknown;
|
|
340
|
+
}
|
|
341
|
+
declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyConflictError"> {
|
|
243
342
|
/**
|
|
244
343
|
* Marks this error as retryable so `isRetryable(err)` returns
|
|
245
344
|
* true. The canonical OCC pattern is to reload the aggregate, re-apply
|
|
246
345
|
* the use case, and retry on this exception.
|
|
247
346
|
*/
|
|
248
347
|
readonly retryable: true;
|
|
249
|
-
|
|
348
|
+
readonly aggregateType: string;
|
|
349
|
+
readonly aggregateId: string;
|
|
350
|
+
readonly expectedVersion: number;
|
|
351
|
+
readonly actualVersion: number;
|
|
352
|
+
constructor(options: ConcurrencyConflictErrorOptions);
|
|
250
353
|
}
|
|
251
354
|
|
|
252
355
|
/**
|
|
@@ -263,9 +366,8 @@ declare class ConcurrencyConflictError extends InfrastructureError<"ConcurrencyC
|
|
|
263
366
|
*/
|
|
264
367
|
type EventIdFactory = () => string;
|
|
265
368
|
/**
|
|
266
|
-
* Replaces the global event-id factory used by `createDomainEvent
|
|
267
|
-
*
|
|
268
|
-
* for example:
|
|
369
|
+
* Replaces the global event-id factory used by `createDomainEvent`. Call
|
|
370
|
+
* once during application bootstrap, for example:
|
|
269
371
|
*
|
|
270
372
|
* ```ts
|
|
271
373
|
* import { ulid } from "ulid";
|
|
@@ -333,62 +435,6 @@ declare function withEventIdFactory<T>(factory: EventIdFactory, fn: () => T): T;
|
|
|
333
435
|
* Intended for use in test `afterEach` hooks.
|
|
334
436
|
*/
|
|
335
437
|
declare function resetEventIdFactory(): void;
|
|
336
|
-
/**
|
|
337
|
-
* Clock function producing a fresh `Date` for each call. The library
|
|
338
|
-
* defaults to `() => new Date()`; override globally via `setClockFactory`
|
|
339
|
-
* for deterministic event-sourcing tests, time-travel debugging, or any
|
|
340
|
-
* scenario where `occurredAt` must be reproducible.
|
|
341
|
-
*/
|
|
342
|
-
type ClockFactory = () => Date;
|
|
343
|
-
/**
|
|
344
|
-
* Replaces the global clock factory used by `createDomainEvent` and
|
|
345
|
-
* `createDomainEventWithMetadata`. Call once during application bootstrap
|
|
346
|
-
* (or per-test in deterministic test suites):
|
|
347
|
-
*
|
|
348
|
-
* ```ts
|
|
349
|
-
* import { setClockFactory } from "@shirudo/ddd-kit";
|
|
350
|
-
*
|
|
351
|
-
* setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
|
|
352
|
-
* ```
|
|
353
|
-
*
|
|
354
|
-
* The per-call `options.occurredAt` override always wins over this
|
|
355
|
-
* factory. Symmetric to `setEventIdFactory`.
|
|
356
|
-
*
|
|
357
|
-
* Module-scoped: see {@link setEventIdFactory} for the global-state
|
|
358
|
-
* caveats. For test isolation prefer {@link withClockFactory}; for
|
|
359
|
-
* multi-tenant request isolation prefer the per-call
|
|
360
|
-
* `options.occurredAt`.
|
|
361
|
-
*/
|
|
362
|
-
declare function setClockFactory(factory: ClockFactory): void;
|
|
363
|
-
/**
|
|
364
|
-
* Scoped variant of {@link setClockFactory}: installs `factory`, runs
|
|
365
|
-
* `fn`, then restores the previous factory in a `finally` block.
|
|
366
|
-
* Synchronous-only, with the same constraints (and same runtime thenable
|
|
367
|
-
* guard) as {@link withEventIdFactory}.
|
|
368
|
-
*
|
|
369
|
-
* **When to prefer the per-call `options.occurredAt` instead.** Same
|
|
370
|
-
* trade-off as {@link withEventIdFactory}: passing `{ occurredAt }`
|
|
371
|
-
* to `createDomainEvent` is the strongest isolation for single-event
|
|
372
|
-
* cases. The scoped helper is for events constructed deep inside
|
|
373
|
-
* domain methods where threading an explicit timestamp is awkward.
|
|
374
|
-
*
|
|
375
|
-
* @example
|
|
376
|
-
* ```ts
|
|
377
|
-
* it("stamps events with a fixed clock", () => {
|
|
378
|
-
* const fixed = new Date("2026-01-01T00:00:00Z");
|
|
379
|
-
* withClockFactory(() => fixed, () => {
|
|
380
|
-
* const e = createDomainEvent("X", { v: 1 });
|
|
381
|
-
* expect(e.occurredAt).toEqual(fixed);
|
|
382
|
-
* });
|
|
383
|
-
* });
|
|
384
|
-
* ```
|
|
385
|
-
*/
|
|
386
|
-
declare function withClockFactory<T>(factory: ClockFactory, fn: () => T): T;
|
|
387
|
-
/**
|
|
388
|
-
* Restores the default clock factory (`() => new Date()`).
|
|
389
|
-
* Intended for use in test `afterEach` hooks.
|
|
390
|
-
*/
|
|
391
|
-
declare function resetClockFactory(): void;
|
|
392
438
|
/**
|
|
393
439
|
* Metadata associated with a domain event for traceability and correlation.
|
|
394
440
|
* Used in event-driven architectures to track event flow across services.
|
|
@@ -492,11 +538,16 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
492
538
|
* `CreateDomainEventOptions.aggregateVersion`; a pre-set value is
|
|
493
539
|
* never overwritten.
|
|
494
540
|
*
|
|
495
|
-
* Consumers use it for ordering
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
541
|
+
* Consumers use it for cross-commit ordering and debugging. It is NOT
|
|
542
|
+
* a per-event idempotency key: all events of one commit share the
|
|
543
|
+
* stamp, so a position cursor keyed on it alone would silently skip
|
|
544
|
+
* every event after the first within a commit (e.g. after a crash
|
|
545
|
+
* between two same-version events). Dedup keys belong on `eventId`;
|
|
546
|
+
* as a watermark this value is per-commit only (advance it after
|
|
547
|
+
* processing ALL events of that version; see the outbox guide).
|
|
548
|
+
* Optional at the type level: events created outside an aggregate
|
|
549
|
+
* (system/integration events) and events from older kit versions
|
|
550
|
+
* don't carry it.
|
|
500
551
|
*/
|
|
501
552
|
aggregateVersion?: number;
|
|
502
553
|
/**
|
|
@@ -555,6 +606,14 @@ interface CreateDomainEventOptions {
|
|
|
555
606
|
* Creates a domain event with default values.
|
|
556
607
|
* Sets occurredAt to current date and version to 1 if not provided.
|
|
557
608
|
*
|
|
609
|
+
* **Input ownership.** The event is deeply frozen, and `payload` and
|
|
610
|
+
* `metadata` are deep-cloned first, so the caller's own objects are never
|
|
611
|
+
* frozen in place and later mutation of them does not bleed into the
|
|
612
|
+
* event (same contract as `vo()`). The clone follows the plain-data event
|
|
613
|
+
* contract via `structuredClone`: functions, Promise, and WeakMap/WeakSet
|
|
614
|
+
* values throw a `TypeError`; symbol-keyed properties are not carried
|
|
615
|
+
* over.
|
|
616
|
+
*
|
|
558
617
|
* **For aggregate-internal events, prefer `this.recordEvent(...)` on
|
|
559
618
|
* `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
|
|
560
619
|
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
@@ -583,20 +642,6 @@ interface CreateDomainEventOptions {
|
|
|
583
642
|
*/
|
|
584
643
|
declare function createDomainEvent<T extends string>(type: T, payload?: undefined, options?: CreateDomainEventOptions): DomainEvent<T, void>;
|
|
585
644
|
declare function createDomainEvent<T extends string, P>(type: T, payload: P, options?: CreateDomainEventOptions): DomainEvent<T, P>;
|
|
586
|
-
/**
|
|
587
|
-
* Creates a domain event with metadata for traceability.
|
|
588
|
-
* Convenience function for creating events with correlation and causation IDs.
|
|
589
|
-
*
|
|
590
|
-
* @example
|
|
591
|
-
* ```typescript
|
|
592
|
-
* const event = createDomainEventWithMetadata(
|
|
593
|
-
* "OrderCreated",
|
|
594
|
-
* { orderId: "123" },
|
|
595
|
-
* { correlationId: "corr-123", causationId: "cmd-456", userId: "user-789" }
|
|
596
|
-
* );
|
|
597
|
-
* ```
|
|
598
|
-
*/
|
|
599
|
-
declare function createDomainEventWithMetadata<T extends string, P>(type: T, payload: P, metadata: EventMetadata, options?: Omit<CreateDomainEventOptions, "metadata">): DomainEvent<T, P>;
|
|
600
645
|
/**
|
|
601
646
|
* Copies metadata from a source event to a new event.
|
|
602
647
|
* Useful for maintaining correlation chains in event-driven architectures.
|
|
@@ -640,15 +685,25 @@ interface AggregateSnapshot<TState> {
|
|
|
640
685
|
/**
|
|
641
686
|
* The state of the aggregate at the time of the snapshot.
|
|
642
687
|
*/
|
|
643
|
-
state: TState;
|
|
688
|
+
readonly state: TState;
|
|
644
689
|
/**
|
|
645
690
|
* The version of the aggregate when the snapshot was taken.
|
|
646
691
|
*/
|
|
647
|
-
version: Version;
|
|
692
|
+
readonly version: Version;
|
|
648
693
|
/**
|
|
649
694
|
* Timestamp when the snapshot was created.
|
|
650
695
|
*/
|
|
651
|
-
snapshotAt: Date;
|
|
696
|
+
readonly snapshotAt: Date;
|
|
697
|
+
/**
|
|
698
|
+
* Schema version of the SHAPE of `state` (the aggregate's declared
|
|
699
|
+
* `snapshotSchemaVersion`), stamped by `createSnapshot`. Distinct from
|
|
700
|
+
* {@link version}, which counts mutations: this field says "which
|
|
701
|
+
* shape does the stored state have", so a restore can detect a
|
|
702
|
+
* snapshot written against an older `TSnapshotState` and migrate or
|
|
703
|
+
* discard it instead of crashing later. Optional: absent on snapshots
|
|
704
|
+
* written by older kit versions, which restore treats as schema `1`.
|
|
705
|
+
*/
|
|
706
|
+
readonly schemaVersion?: number;
|
|
652
707
|
}
|
|
653
708
|
/**
|
|
654
709
|
* Public contract every Aggregate Root satisfies. Implemented by
|
|
@@ -713,4 +768,4 @@ declare function sameVersion<TId extends Id<string>>(a: {
|
|
|
713
768
|
version: Version;
|
|
714
769
|
}): boolean;
|
|
715
770
|
|
|
716
|
-
export { type AnyDomainEvent as A, type CreateDomainEventOptions as C, DomainError as D, type EventIdFactory as E, type IAggregateRoot as I, MissingHandlerError as M, UnenrolledChangesError as U, type Version as V, type Id as a, type AggregateSnapshot as b, type IEventSourcedAggregate as c, InfrastructureError as d,
|
|
771
|
+
export { type AnyDomainEvent as A, type CreateDomainEventOptions as C, DomainError as D, type EventIdFactory as E, type IAggregateRoot as I, MissingHandlerError as M, SnapshotSchemaMismatchError as S, UnenrolledChangesError as U, type Version as V, type Id as a, type AggregateSnapshot as b, type IEventSourcedAggregate as c, InfrastructureError as d, copyMetadata as e, createDomainEvent as f, type DomainEvent as g, type EventMetadata as h, setEventIdFactory as i, AggregateDeletedError as j, AggregateNotFoundError as k, type AggregateNotFoundErrorOptions as l, mergeMetadata as m, ConcurrencyConflictError as n, type ConcurrencyConflictErrorOptions as o, DuplicateAggregateError as p, type DuplicateAggregateErrorOptions as q, resetEventIdFactory as r, sameVersion as s, EventHarvestError as t, type SnapshotSchemaMismatchErrorOptions as u, UnregisteredHandlerError as v, withEventIdFactory as w, type UnregisteredHandlerErrorOptions as x, UnreplayableAggregateError as y, type IdGenerator as z };
|
package/dist/http.d.ts
CHANGED
|
@@ -1,31 +1,54 @@
|
|
|
1
|
-
import { ValidationError
|
|
1
|
+
import { ValidationError } from '@shirudo/base-error';
|
|
2
|
+
import { ProblemDetailsExtensions, ProblemDetails } from '@shirudo/base-error/problem-details';
|
|
2
3
|
|
|
3
4
|
/** Extension member that carries the collected field issues. */
|
|
4
5
|
type ValidationProblemMember = "errors" | "invalid-params";
|
|
5
6
|
/**
|
|
6
|
-
* Options for {@link toProblemDetails}
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Options for {@link toProblemDetails}: the standard RFC 9457 members the
|
|
8
|
+
* boundary may set, plus {@link member} to choose the wire key for the issues
|
|
9
|
+
* and {@link extensions} for extra public members merged alongside them.
|
|
9
10
|
*/
|
|
10
|
-
interface ValidationProblemOptions
|
|
11
|
+
interface ValidationProblemOptions {
|
|
12
|
+
/** URI reference identifying the problem type. Defaults to `"about:blank"`. */
|
|
13
|
+
type?: string;
|
|
14
|
+
/** Short, human-readable summary. Default `"Validation Failed"`. */
|
|
15
|
+
title?: string;
|
|
16
|
+
/** HTTP status code. Default `422`. */
|
|
17
|
+
status?: number;
|
|
18
|
+
/** Human-readable explanation specific to this occurrence. */
|
|
19
|
+
detail?: string;
|
|
20
|
+
/** URI reference identifying this specific occurrence. */
|
|
21
|
+
instance?: string;
|
|
11
22
|
/**
|
|
12
23
|
* Extension member that carries the field issues. Default `"errors"`
|
|
13
24
|
* (`{ message, path, code?, pointer? }` entries). RFC 9457 does not
|
|
14
25
|
* standardize a multi-error member; `errors` is the common convention.
|
|
15
26
|
*/
|
|
16
27
|
member?: ValidationProblemMember;
|
|
17
|
-
/**
|
|
18
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Extra public extension members merged alongside the issues. JSON-safe
|
|
30
|
+
* by contract (RFC 9457 bodies must serialize); a trace id, for example,
|
|
31
|
+
* is passed here, not as a recognized top-level field.
|
|
32
|
+
*/
|
|
33
|
+
extensions?: ProblemDetailsExtensions;
|
|
19
34
|
}
|
|
20
35
|
/**
|
|
21
36
|
* Projects a base-error {@link ValidationError} to an RFC 9457 Problem Details
|
|
22
37
|
* object with the collected field issues attached under an extension member.
|
|
38
|
+
* The return type is base-error's own
|
|
39
|
+
* {@link ProblemDetails} (from `@shirudo/base-error/problem-details`), so the
|
|
40
|
+
* RFC 9457 shape stays a single source of truth across the ecosystem.
|
|
23
41
|
*
|
|
24
|
-
* base-error is **safe by default**:
|
|
25
|
-
*
|
|
26
|
-
*
|
|
42
|
+
* base-error is **safe by default**: the issues only cross to a client through
|
|
43
|
+
* the `publicIssues()` whitelist (`{ message, path, code?, pointer? }`, never
|
|
44
|
+
* raw validator extras). This helper performs that explicit projection and
|
|
27
45
|
* applies sensible validation defaults (`422`, `"Validation Failed"`), so the
|
|
28
|
-
* common boundary case is a one-liner instead of a footgun.
|
|
46
|
+
* common boundary case is a one-liner instead of a footgun. The full-fidelity
|
|
47
|
+
* issues remain available for observability via `error.toLogObject()`.
|
|
48
|
+
*
|
|
49
|
+
* For the general error-to-Problem-Details mapping (a public-code catalog with
|
|
50
|
+
* per-code `type` / `status`), use base-error's `defineProblemDetailsAdapter`
|
|
51
|
+
* over a `PublicErrorView`. This helper is the narrow validation shortcut.
|
|
29
52
|
*
|
|
30
53
|
* This is a presentation/transport concern and ships from the opt-in
|
|
31
54
|
* `@shirudo/ddd-kit/http` entry point: the core kit stays transport-free.
|
|
@@ -37,10 +60,10 @@ interface ValidationProblemOptions extends Omit<ProblemDetailsOptions, "extensio
|
|
|
37
60
|
* if (result.isErr()) {
|
|
38
61
|
* return Response.json(toProblemDetails(result.error), { status: 422 });
|
|
39
62
|
* }
|
|
40
|
-
* // → { type, title: "Validation Failed", status: 422,
|
|
63
|
+
* // → { type: "about:blank", title: "Validation Failed", status: 422,
|
|
41
64
|
* // errors: [{ message: "must be a valid email", path: ["email"], pointer: "email" }] }
|
|
42
65
|
* ```
|
|
43
66
|
*/
|
|
44
|
-
declare function toProblemDetails(error: ValidationError, options?: ValidationProblemOptions): ProblemDetails
|
|
67
|
+
declare function toProblemDetails(error: ValidationError, options?: ValidationProblemOptions): ProblemDetails<never, ProblemDetailsExtensions>;
|
|
45
68
|
|
|
46
69
|
export { type ValidationProblemMember, type ValidationProblemOptions, toProblemDetails };
|
package/dist/http.js
CHANGED
|
@@ -3,13 +3,21 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
3
3
|
|
|
4
4
|
// src/http/problem-details.ts
|
|
5
5
|
function toProblemDetails(error, options = {}) {
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
const {
|
|
7
|
+
member = "errors",
|
|
8
|
+
extensions,
|
|
9
|
+
type = "about:blank",
|
|
10
|
+
title = "Validation Failed",
|
|
11
|
+
status = 422,
|
|
12
|
+
detail,
|
|
13
|
+
instance
|
|
14
|
+
} = options;
|
|
15
|
+
const problem = { type, title, status };
|
|
16
|
+
if (detail !== void 0) problem.detail = detail;
|
|
17
|
+
if (instance !== void 0) problem.instance = instance;
|
|
18
|
+
if (extensions) Object.assign(problem, extensions);
|
|
19
|
+
problem[member] = error.publicIssues();
|
|
20
|
+
return problem;
|
|
13
21
|
}
|
|
14
22
|
__name(toProblemDetails, "toProblemDetails");
|
|
15
23
|
|
package/dist/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/http/problem-details.ts"],"names":[],"mappings":";;;;
|
|
1
|
+
{"version":3,"sources":["../src/http/problem-details.ts"],"names":[],"mappings":";;;;AAuEO,SAAS,gBAAA,CACf,KAAA,EACA,OAAA,GAAoC,EAAC,EACa;AAClD,EAAA,MAAM;AAAA,IACL,MAAA,GAAS,QAAA;AAAA,IACT,UAAA;AAAA,IACA,IAAA,GAAO,aAAA;AAAA,IACP,KAAA,GAAQ,mBAAA;AAAA,IACR,MAAA,GAAS,GAAA;AAAA,IACT,MAAA;AAAA,IACA;AAAA,GACD,GAAI,OAAA;AAEJ,EAAA,MAAM,OAAA,GAAmC,EAAE,IAAA,EAAM,KAAA,EAAO,MAAA,EAAO;AAC/D,EAAA,IAAI,MAAA,KAAW,MAAA,EAAW,OAAA,CAAQ,MAAA,GAAS,MAAA;AAC3C,EAAA,IAAI,QAAA,KAAa,MAAA,EAAW,OAAA,CAAQ,QAAA,GAAW,QAAA;AAC/C,EAAA,IAAI,UAAA,EAAY,MAAA,CAAO,MAAA,CAAO,OAAA,EAAS,UAAU,CAAA;AAIjD,EAAA,OAAA,CAAQ,MAAM,CAAA,GAAI,KAAA,CAAM,YAAA,EAAa;AAErC,EAAA,OAAO,OAAA;AACR;AAxBgB,MAAA,CAAA,gBAAA,EAAA,kBAAA,CAAA","file":"http.js","sourcesContent":["import type { ValidationError } from \"@shirudo/base-error\";\nimport type {\n\tProblemDetails,\n\tProblemDetailsExtensions,\n} from \"@shirudo/base-error/problem-details\";\n\n/** Extension member that carries the collected field issues. */\nexport type ValidationProblemMember = \"errors\" | \"invalid-params\";\n\n/**\n * Options for {@link toProblemDetails}: the standard RFC 9457 members the\n * boundary may set, plus {@link member} to choose the wire key for the issues\n * and {@link extensions} for extra public members merged alongside them.\n */\nexport interface ValidationProblemOptions {\n\t/** URI reference identifying the problem type. Defaults to `\"about:blank\"`. */\n\ttype?: string;\n\t/** Short, human-readable summary. Default `\"Validation Failed\"`. */\n\ttitle?: string;\n\t/** HTTP status code. Default `422`. */\n\tstatus?: number;\n\t/** Human-readable explanation specific to this occurrence. */\n\tdetail?: string;\n\t/** URI reference identifying this specific occurrence. */\n\tinstance?: string;\n\t/**\n\t * Extension member that carries the field issues. Default `\"errors\"`\n\t * (`{ message, path, code?, pointer? }` entries). RFC 9457 does not\n\t * standardize a multi-error member; `errors` is the common convention.\n\t */\n\tmember?: ValidationProblemMember;\n\t/**\n\t * Extra public extension members merged alongside the issues. JSON-safe\n\t * by contract (RFC 9457 bodies must serialize); a trace id, for example,\n\t * is passed here, not as a recognized top-level field.\n\t */\n\textensions?: ProblemDetailsExtensions;\n}\n\n/**\n * Projects a base-error {@link ValidationError} to an RFC 9457 Problem Details\n * object with the collected field issues attached under an extension member.\n * The return type is base-error's own\n * {@link ProblemDetails} (from `@shirudo/base-error/problem-details`), so the\n * RFC 9457 shape stays a single source of truth across the ecosystem.\n *\n * base-error is **safe by default**: the issues only cross to a client through\n * the `publicIssues()` whitelist (`{ message, path, code?, pointer? }`, never\n * raw validator extras). This helper performs that explicit projection and\n * applies sensible validation defaults (`422`, `\"Validation Failed\"`), so the\n * common boundary case is a one-liner instead of a footgun. The full-fidelity\n * issues remain available for observability via `error.toLogObject()`.\n *\n * For the general error-to-Problem-Details mapping (a public-code catalog with\n * per-code `type` / `status`), use base-error's `defineProblemDetailsAdapter`\n * over a `PublicErrorView`. This helper is the narrow validation shortcut.\n *\n * This is a presentation/transport concern and ships from the opt-in\n * `@shirudo/ddd-kit/http` entry point: the core kit stays transport-free.\n *\n * @example\n * ```ts\n * import { toProblemDetails } from \"@shirudo/ddd-kit/http\";\n *\n * if (result.isErr()) {\n * return Response.json(toProblemDetails(result.error), { status: 422 });\n * }\n * // → { type: \"about:blank\", title: \"Validation Failed\", status: 422,\n * // errors: [{ message: \"must be a valid email\", path: [\"email\"], pointer: \"email\" }] }\n * ```\n */\nexport function toProblemDetails(\n\terror: ValidationError,\n\toptions: ValidationProblemOptions = {},\n): ProblemDetails<never, ProblemDetailsExtensions> {\n\tconst {\n\t\tmember = \"errors\",\n\t\textensions,\n\t\ttype = \"about:blank\",\n\t\ttitle = \"Validation Failed\",\n\t\tstatus = 422,\n\t\tdetail,\n\t\tinstance,\n\t} = options;\n\n\tconst problem: Record<string, unknown> = { type, title, status };\n\tif (detail !== undefined) problem.detail = detail;\n\tif (instance !== undefined) problem.instance = instance;\n\tif (extensions) Object.assign(problem, extensions);\n\t// `PublicIssue.path` is `ReadonlyArray<PropertyKey>`, so the issue array is\n\t// not statically a `ProblemDetailsJsonValue`; the wire form only ever\n\t// carries string/number path segments, so this is JSON-safe in practice.\n\tproblem[member] = error.publicIssues();\n\n\treturn problem as ProblemDetails<never, ProblemDetailsExtensions>;\n}\n"]}
|