@shirudo/ddd-kit 2.0.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-CePTINEt.d.ts → aggregate-DFi6HlEh.d.ts} +113 -64
- package/dist/index.d.ts +876 -196
- package/dist/index.js +2027 -396
- package/dist/index.js.map +1 -1
- package/dist/presentation.js +1 -1
- package/dist/presentation.js.map +1 -1
- 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 +3 -3
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) |
|
|
@@ -106,6 +106,29 @@ declare class MissingHandlerError extends BaseError<"MissingHandlerError"> {
|
|
|
106
106
|
readonly eventType: string;
|
|
107
107
|
constructor(eventType: string, cause?: unknown);
|
|
108
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
|
+
}
|
|
109
132
|
/**
|
|
110
133
|
* Thrown by `withCommit` when an event harvested from an aggregate cannot
|
|
111
134
|
* be safely committed: it is missing `aggregateId` / `aggregateType`
|
|
@@ -131,6 +154,34 @@ declare class EventHarvestError extends BaseError<"EventHarvestError"> {
|
|
|
131
154
|
/** The `type` of the offending event, for programmatic routing. */
|
|
132
155
|
eventType?: string | undefined);
|
|
133
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
|
+
}
|
|
134
185
|
/**
|
|
135
186
|
* Thrown at the end of a `UnitOfWork.run` when an aggregate that was
|
|
136
187
|
* loaded into the identity map during the operation carries unflushed
|
|
@@ -231,6 +282,36 @@ declare class DuplicateAggregateError extends InfrastructureError<"DuplicateAggr
|
|
|
231
282
|
readonly aggregateId: string;
|
|
232
283
|
constructor(options: DuplicateAggregateErrorOptions);
|
|
233
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);
|
|
314
|
+
}
|
|
234
315
|
/**
|
|
235
316
|
* Thrown by `IRepository.save()` when the aggregate's expected version
|
|
236
317
|
* does not match the version currently persisted: i.e. another writer
|
|
@@ -354,61 +435,6 @@ declare function withEventIdFactory<T>(factory: EventIdFactory, fn: () => T): T;
|
|
|
354
435
|
* Intended for use in test `afterEach` hooks.
|
|
355
436
|
*/
|
|
356
437
|
declare function resetEventIdFactory(): void;
|
|
357
|
-
/**
|
|
358
|
-
* Clock function producing a fresh `Date` for each call. The library
|
|
359
|
-
* defaults to `() => new Date()`; override globally via `setClockFactory`
|
|
360
|
-
* for deterministic event-sourcing tests, time-travel debugging, or any
|
|
361
|
-
* scenario where `occurredAt` must be reproducible.
|
|
362
|
-
*/
|
|
363
|
-
type ClockFactory = () => Date;
|
|
364
|
-
/**
|
|
365
|
-
* Replaces the global clock factory used by `createDomainEvent`. Call once
|
|
366
|
-
* during application bootstrap (or per-test in deterministic test suites):
|
|
367
|
-
*
|
|
368
|
-
* ```ts
|
|
369
|
-
* import { setClockFactory } from "@shirudo/ddd-kit";
|
|
370
|
-
*
|
|
371
|
-
* setClockFactory(() => new Date("2026-01-01T00:00:00Z"));
|
|
372
|
-
* ```
|
|
373
|
-
*
|
|
374
|
-
* The per-call `options.occurredAt` override always wins over this
|
|
375
|
-
* factory. Symmetric to `setEventIdFactory`.
|
|
376
|
-
*
|
|
377
|
-
* Module-scoped: see {@link setEventIdFactory} for the global-state
|
|
378
|
-
* caveats. For test isolation prefer {@link withClockFactory}; for
|
|
379
|
-
* multi-tenant request isolation prefer the per-call
|
|
380
|
-
* `options.occurredAt`.
|
|
381
|
-
*/
|
|
382
|
-
declare function setClockFactory(factory: ClockFactory): void;
|
|
383
|
-
/**
|
|
384
|
-
* Scoped variant of {@link setClockFactory}: installs `factory`, runs
|
|
385
|
-
* `fn`, then restores the previous factory in a `finally` block.
|
|
386
|
-
* Synchronous-only, with the same constraints (and same runtime thenable
|
|
387
|
-
* guard) as {@link withEventIdFactory}.
|
|
388
|
-
*
|
|
389
|
-
* **When to prefer the per-call `options.occurredAt` instead.** Same
|
|
390
|
-
* trade-off as {@link withEventIdFactory}: passing `{ occurredAt }`
|
|
391
|
-
* to `createDomainEvent` is the strongest isolation for single-event
|
|
392
|
-
* cases. The scoped helper is for events constructed deep inside
|
|
393
|
-
* domain methods where threading an explicit timestamp is awkward.
|
|
394
|
-
*
|
|
395
|
-
* @example
|
|
396
|
-
* ```ts
|
|
397
|
-
* it("stamps events with a fixed clock", () => {
|
|
398
|
-
* const fixed = new Date("2026-01-01T00:00:00Z");
|
|
399
|
-
* withClockFactory(() => fixed, () => {
|
|
400
|
-
* const e = createDomainEvent("X", { v: 1 });
|
|
401
|
-
* expect(e.occurredAt).toEqual(fixed);
|
|
402
|
-
* });
|
|
403
|
-
* });
|
|
404
|
-
* ```
|
|
405
|
-
*/
|
|
406
|
-
declare function withClockFactory<T>(factory: ClockFactory, fn: () => T): T;
|
|
407
|
-
/**
|
|
408
|
-
* Restores the default clock factory (`() => new Date()`).
|
|
409
|
-
* Intended for use in test `afterEach` hooks.
|
|
410
|
-
*/
|
|
411
|
-
declare function resetClockFactory(): void;
|
|
412
438
|
/**
|
|
413
439
|
* Metadata associated with a domain event for traceability and correlation.
|
|
414
440
|
* Used in event-driven architectures to track event flow across services.
|
|
@@ -512,11 +538,16 @@ interface DomainEvent<T extends string, P = void> {
|
|
|
512
538
|
* `CreateDomainEventOptions.aggregateVersion`; a pre-set value is
|
|
513
539
|
* never overwritten.
|
|
514
540
|
*
|
|
515
|
-
* Consumers use it for ordering
|
|
516
|
-
*
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
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.
|
|
520
551
|
*/
|
|
521
552
|
aggregateVersion?: number;
|
|
522
553
|
/**
|
|
@@ -575,6 +606,14 @@ interface CreateDomainEventOptions {
|
|
|
575
606
|
* Creates a domain event with default values.
|
|
576
607
|
* Sets occurredAt to current date and version to 1 if not provided.
|
|
577
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
|
+
*
|
|
578
617
|
* **For aggregate-internal events, prefer `this.recordEvent(...)` on
|
|
579
618
|
* `AggregateRoot` / `EventSourcedAggregate`.** That helper auto-injects
|
|
580
619
|
* `aggregateId` (from `this.id`) and `aggregateType` (from the
|
|
@@ -646,15 +685,25 @@ interface AggregateSnapshot<TState> {
|
|
|
646
685
|
/**
|
|
647
686
|
* The state of the aggregate at the time of the snapshot.
|
|
648
687
|
*/
|
|
649
|
-
state: TState;
|
|
688
|
+
readonly state: TState;
|
|
650
689
|
/**
|
|
651
690
|
* The version of the aggregate when the snapshot was taken.
|
|
652
691
|
*/
|
|
653
|
-
version: Version;
|
|
692
|
+
readonly version: Version;
|
|
654
693
|
/**
|
|
655
694
|
* Timestamp when the snapshot was created.
|
|
656
695
|
*/
|
|
657
|
-
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;
|
|
658
707
|
}
|
|
659
708
|
/**
|
|
660
709
|
* Public contract every Aggregate Root satisfies. Implemented by
|
|
@@ -719,4 +768,4 @@ declare function sameVersion<TId extends Id<string>>(a: {
|
|
|
719
768
|
version: Version;
|
|
720
769
|
}): boolean;
|
|
721
770
|
|
|
722
|
-
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 };
|