@shirudo/ddd-kit 1.0.0-rc.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -143
- package/dist/index.d.ts +808 -186
- package/dist/index.js +643 -380
- package/dist/index.js.map +1 -1
- package/dist/utils.d.ts +92 -1
- package/dist/utils.js +281 -0
- package/dist/utils.js.map +1 -1
- package/package.json +69 -65
- package/dist/deep-equal-except-C8yoSk4L.d.ts +0 -57
- package/dist/result-jCwPSjFa.d.ts +0 -352
- package/dist/result.d.ts +0 -204
- package/dist/result.js +0 -298
- package/dist/result.js.map +0 -1
- package/dist/utils-array.d.ts +0 -47
- package/dist/utils-array.js +0 -242
- package/dist/utils-array.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Composable TypeScript toolkit for tactical Domain-Driven Design.
|
|
4
4
|
|
|
5
|
+
📚 **[Full documentation site](https://shi-rudo.github.io/ddd-kit-ts/)** — guides, API reference, design decisions.
|
|
6
|
+
|
|
5
7
|
> **Release Candidate**
|
|
6
8
|
>
|
|
7
9
|
> This library is in Release Candidate phase. The API is considered stable and ready for production evaluation. Please report any issues before the final 1.0.0 release.
|
|
@@ -62,7 +64,7 @@ const email = createEmail("user@example.com");
|
|
|
62
64
|
|
|
63
65
|
### Value Objects
|
|
64
66
|
|
|
65
|
-
Value Objects are immutable objects that are defined by their attributes rather than identity. They ensure data integrity by preventing modification after creation. Use the `vo()` helper function to create deeply frozen value objects that cannot be mutated, even nested objects and arrays. The library provides `voEquals()` for value-based equality comparison, `voEqualsExcept()` for comparing while ignoring specified keys (useful for metadata), `voWithValidation()` for creating validated value objects (returns Result),
|
|
67
|
+
Value Objects are immutable objects that are defined by their attributes rather than identity. They ensure data integrity by preventing modification after creation. Use the `vo()` helper function to create deeply frozen value objects that cannot be mutated, even nested objects and arrays. The library provides `voEquals()` for value-based equality comparison, `voEqualsExcept()` for comparing while ignoring specified keys (useful for metadata), and `voWithValidation()` for creating validated value objects at the App-Service boundary (returns Result). For Domain construction, prefer the `ValueObject` base class — its constructor throws on invariant violation via the `validate()` hook.
|
|
66
68
|
|
|
67
69
|
### Entities
|
|
68
70
|
|
|
@@ -170,19 +172,16 @@ const result = voWithValidation(
|
|
|
170
172
|
"Amount must be non-negative and currency must be 3 characters"
|
|
171
173
|
);
|
|
172
174
|
|
|
173
|
-
if (result.
|
|
175
|
+
if (result.isOk()) {
|
|
174
176
|
const validMoney = result.value;
|
|
175
177
|
// Use validMoney...
|
|
176
178
|
} else {
|
|
177
179
|
console.error(result.error);
|
|
178
180
|
}
|
|
179
181
|
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
(m) => m.amount >= 0 && m.currency.length === 3,
|
|
184
|
-
"Amount must be non-negative and currency must be 3 characters"
|
|
185
|
-
);
|
|
182
|
+
// For Domain construction, use the `ValueObject` base class — its constructor
|
|
183
|
+
// throws via the `validate()` hook, so Domain code keeps a throw-based contract.
|
|
184
|
+
// Reserve `voWithValidation` for parsing untrusted input at the App boundary.
|
|
186
185
|
|
|
187
186
|
// Value object with nested structures (deep freeze)
|
|
188
187
|
const address = vo({
|
|
@@ -308,6 +307,48 @@ class Order extends AggregateRoot<OrderState, OrderId, OrderDomainEvent> {
|
|
|
308
307
|
// order.addDomainEvent({ type: "WrongEvent" }) → compile error
|
|
309
308
|
```
|
|
310
309
|
|
|
310
|
+
> **Domain-event ordering: record AFTER mutation.** A domain event represents something that has *just happened* to the aggregate. Always mutate state first (`setState`, invariant checks), then `addDomainEvent`. Recording before mutation is a footgun: if a subsequent invariant throws, the event has been queued for a fact that never actually happened.
|
|
311
|
+
>
|
|
312
|
+
> Two paths give you that ordering for free, so you don't have to remember the rule:
|
|
313
|
+
>
|
|
314
|
+
> - **`EventSourcedAggregate.apply(event)`** — `validateEvent` runs, then the handler computes the next state, then state + event + version commit atomically. State is never mutated without the event, and the event is never recorded without the state.
|
|
315
|
+
> - **`AggregateRoot.commit(newState, event)`** — opt-in helper that runs `setState(newState)` first (which throws on `validateState` failure) and only then appends the event(s). Use this instead of calling `setState` + `addDomainEvent` separately:
|
|
316
|
+
>
|
|
317
|
+
> ```ts
|
|
318
|
+
> confirm(): void {
|
|
319
|
+
> if (this.state.status === "confirmed") throw new OrderAlreadyConfirmedError(this.id);
|
|
320
|
+
> this.commit(
|
|
321
|
+
> { ...this.state, status: "confirmed" },
|
|
322
|
+
> { type: "OrderConfirmed", orderId: this.id },
|
|
323
|
+
> );
|
|
324
|
+
> }
|
|
325
|
+
> ```
|
|
326
|
+
>
|
|
327
|
+
> `commit()` accepts a single event, an array of events, or none. Direct `setState`/`addDomainEvent` calls remain available for cases that don't fit the helper (state-only mutations, audit-only events, multi-step transactions).
|
|
328
|
+
|
|
329
|
+
> **Aggregate methods own the behaviour, not the consumer.** Subclasses of `AggregateRoot` and `EventSourcedAggregate` expose state via the `state` getter (DDD requires invariant checks to read it), but the canonical Pattern is *Tell, Don't Ask*: write business methods on the aggregate that mutate via `commit()` / `setState()` / `apply()` and emit events; do not write `if (order.state.status === "draft") order.state.status = "confirmed"` from outside the aggregate. The state getter is for the aggregate's own methods and for read-only projections — not as a public mutation handle.
|
|
330
|
+
|
|
331
|
+
### Event-Sourcing Schema Evolution (Upcasting)
|
|
332
|
+
|
|
333
|
+
`DomainEvent.version` is intentionally a plain integer rather than a library-managed migration chain. Schema evolution is **the consumer's responsibility** — every event store handles it differently (sync upcasters in the load path, async upcasters in a projection rebuild, schema-registry coupling, etc.). The recommended pattern is to wrap your event-store read path:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
// At the infrastructure boundary, before passing events to loadFromHistory:
|
|
337
|
+
function upcast(event: PersistedEvent): DomainEvent {
|
|
338
|
+
if (event.type === "OrderCreated" && event.version === 1) {
|
|
339
|
+
// v1 → v2 migration; produce a new DomainEvent
|
|
340
|
+
return { ...event, version: 2, payload: { ...event.payload, currency: "EUR" } };
|
|
341
|
+
}
|
|
342
|
+
return event;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const history = await eventStore.read(aggregateId);
|
|
346
|
+
const upcasted = history.map(upcast);
|
|
347
|
+
aggregate.loadFromHistory(upcasted);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The library deliberately ships no `EventUpcaster` port. Real upcasting strategies vary too much (chained vs schema-registry vs lazy) to commit to one shape pre-1.0 without concrete usage data.
|
|
351
|
+
|
|
311
352
|
### Creating an Aggregate WITH Event Sourcing
|
|
312
353
|
|
|
313
354
|
```typescript
|
|
@@ -350,28 +391,22 @@ class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
|
350
391
|
}
|
|
351
392
|
|
|
352
393
|
confirm(): void {
|
|
353
|
-
|
|
354
|
-
createDomainEvent("OrderConfirmed") as OrderConfirmed
|
|
355
|
-
);
|
|
356
|
-
if (!result.ok) {
|
|
357
|
-
throw new Error(result.error);
|
|
358
|
-
}
|
|
394
|
+
this.apply(createDomainEvent("OrderConfirmed") as OrderConfirmed);
|
|
359
395
|
}
|
|
360
396
|
|
|
361
397
|
ship(trackingNumber: string): void {
|
|
362
|
-
|
|
398
|
+
this.apply(
|
|
363
399
|
createDomainEvent("OrderShipped", { trackingNumber }) as OrderShipped
|
|
364
400
|
);
|
|
365
|
-
if (!result.ok) {
|
|
366
|
-
throw new Error(result.error);
|
|
367
|
-
}
|
|
368
401
|
}
|
|
369
402
|
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
)
|
|
403
|
+
// Override `validateEvent` to throw a DomainError subclass when an invariant
|
|
404
|
+
// is violated (e.g. confirming an already-confirmed order). `apply()` itself
|
|
405
|
+
// throws `MissingHandlerError` when no handler is registered for the event.
|
|
406
|
+
protected validateEvent(event: OrderEvent): void {
|
|
407
|
+
if (event.type === "OrderConfirmed" && this.state.status === "confirmed") {
|
|
408
|
+
throw new OrderAlreadyConfirmedError(this.id);
|
|
409
|
+
}
|
|
375
410
|
}
|
|
376
411
|
|
|
377
412
|
protected readonly handlers = {
|
|
@@ -490,19 +525,24 @@ class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
|
|
|
490
525
|
|
|
491
526
|
### Using CQRS: Commands and Queries
|
|
492
527
|
|
|
528
|
+
The library ships an in-memory `CommandBus` and `QueryBus`. These are zero-config in-process dispatchers — they fit:
|
|
529
|
+
|
|
530
|
+
- **Edge runtimes** (Cloudflare Workers, Vercel Edge, Deno Deploy, Bun): each worker invocation handles one command in-process; external brokers would defeat edge latency.
|
|
531
|
+
- **Modular monoliths**: a single Node process with several bounded contexts; the bus routes commands between modules. Domain events still leave the process via outbox/external bus when other services need them.
|
|
532
|
+
- **Tests and local development**: stand-in for production buses without infrastructure.
|
|
533
|
+
- **Small CLIs and scripts**: CQRS structure without infrastructure.
|
|
534
|
+
|
|
535
|
+
For **cross-process messaging** (RabbitMQ, NATS, Kafka, AWS SQS, etc.), don't use the in-memory bus — keep the `CommandHandler<C, R>` / `QueryHandler<Q, R>` types as the contract and wire them to your transport of choice. The handlers stay portable; only the dispatcher changes.
|
|
536
|
+
|
|
537
|
+
The included buses intentionally have no middleware/pipeline machinery — wrap handlers with decorator functions when you need logging, auth, metrics. Anything more elaborate is "in-house framework" territory and lives outside the kit.
|
|
538
|
+
|
|
493
539
|
#### Commands (Write Operations)
|
|
494
540
|
|
|
495
541
|
Commands represent write operations that change system state. They return `Result` for explicit error handling.
|
|
496
542
|
|
|
497
543
|
```typescript
|
|
498
|
-
import {
|
|
499
|
-
|
|
500
|
-
CommandHandler,
|
|
501
|
-
CommandBus,
|
|
502
|
-
ok,
|
|
503
|
-
err,
|
|
504
|
-
type Result,
|
|
505
|
-
} from "@shirudo/ddd-kit";
|
|
544
|
+
import { Command, CommandHandler, CommandBus } from "@shirudo/ddd-kit";
|
|
545
|
+
import { ok, err, type Result } from "@shirudo/result";
|
|
506
546
|
|
|
507
547
|
// Define a command
|
|
508
548
|
type CreateOrderCommand = Command & {
|
|
@@ -541,7 +581,7 @@ const result = await createOrderHandler({
|
|
|
541
581
|
items: [{ productId: "product-1", quantity: 2, price: 10.0 }],
|
|
542
582
|
});
|
|
543
583
|
|
|
544
|
-
if (result.
|
|
584
|
+
if (result.isOk()) {
|
|
545
585
|
console.log("Order created:", result.value);
|
|
546
586
|
} else {
|
|
547
587
|
console.error("Error:", result.error);
|
|
@@ -600,7 +640,7 @@ const result = await queryBus.execute({
|
|
|
600
640
|
orderId: "order-123",
|
|
601
641
|
});
|
|
602
642
|
|
|
603
|
-
if (result.
|
|
643
|
+
if (result.isOk()) {
|
|
604
644
|
const orderFromBus = result.value;
|
|
605
645
|
// Use orderFromBus...
|
|
606
646
|
} else {
|
|
@@ -714,7 +754,7 @@ channel.consume("order.commands", async (message) => {
|
|
|
714
754
|
const command = JSON.parse(message.content.toString()) as CreateOrderCommand;
|
|
715
755
|
const result = await createOrderHandler(command);
|
|
716
756
|
|
|
717
|
-
if (result.
|
|
757
|
+
if (result.isOk()) {
|
|
718
758
|
channel.ack(message);
|
|
719
759
|
} else {
|
|
720
760
|
channel.nack(message, false, true); // Requeue on error
|
|
@@ -1063,26 +1103,7 @@ The `Result<T, E>` type provides composition utilities to avoid repetitive `if (
|
|
|
1063
1103
|
**Import Result utilities from the dedicated export path:**
|
|
1064
1104
|
|
|
1065
1105
|
```typescript
|
|
1066
|
-
import {
|
|
1067
|
-
ok,
|
|
1068
|
-
err,
|
|
1069
|
-
isOk,
|
|
1070
|
-
isErr,
|
|
1071
|
-
andThen,
|
|
1072
|
-
map,
|
|
1073
|
-
mapErr,
|
|
1074
|
-
unwrapOr,
|
|
1075
|
-
unwrapOrElse,
|
|
1076
|
-
match,
|
|
1077
|
-
matchAsync,
|
|
1078
|
-
pipe,
|
|
1079
|
-
tryCatch,
|
|
1080
|
-
tryCatchAsync,
|
|
1081
|
-
type Result,
|
|
1082
|
-
Outcome,
|
|
1083
|
-
Success,
|
|
1084
|
-
Erroneous
|
|
1085
|
-
} from "@shirudo/ddd-kit/result";
|
|
1106
|
+
import { ok, err, type Result } from "@shirudo/result";
|
|
1086
1107
|
|
|
1087
1108
|
type UserId = string;
|
|
1088
1109
|
|
|
@@ -1090,101 +1111,35 @@ function validateUserId(id: string): Result<UserId, string> {
|
|
|
1090
1111
|
return id.length > 0 ? ok(id as UserId) : err("User ID cannot be empty");
|
|
1091
1112
|
}
|
|
1092
1113
|
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
// Chaining operations with andThen (avoids if-checks)
|
|
1098
|
-
function createUser(id: string, email: string): Result<{ id: UserId; email: string }, string> {
|
|
1099
|
-
return andThen(validateUserId(id), (userId) =>
|
|
1100
|
-
map(validateEmail(email), (email) => ({
|
|
1101
|
-
id: userId,
|
|
1102
|
-
email,
|
|
1103
|
-
}))
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
// Using map for transformations
|
|
1108
|
-
const result = ok(5);
|
|
1109
|
-
const doubled = map(result, x => x * 2); // Ok<10>
|
|
1110
|
-
|
|
1111
|
-
// Using mapErr to transform errors
|
|
1112
|
-
const errorResult = err("not found");
|
|
1113
|
-
const mappedError = mapErr(errorResult, e => `Error: ${e}`); // Err<"Error: not found">
|
|
1114
|
-
|
|
1115
|
-
// Using unwrapOr for defaults
|
|
1116
|
-
const userId = unwrapOr(validateUserId(""), "default-id");
|
|
1117
|
-
|
|
1118
|
-
// Using unwrapOrElse for computed defaults
|
|
1119
|
-
const userId2 = unwrapOrElse(validateUserId(""), err => `fallback-${Date.now()}`);
|
|
1120
|
-
|
|
1121
|
-
// Using match for pattern matching
|
|
1122
|
-
const message = match(createUser("user-123", "test@example.com"),
|
|
1123
|
-
user => `User created: ${user.id}`,
|
|
1124
|
-
error => `Error: ${error}`
|
|
1125
|
-
);
|
|
1126
|
-
|
|
1127
|
-
// Usage with type guards (still works)
|
|
1128
|
-
const result2 = createUser("user-123", "test@example.com");
|
|
1129
|
-
if (isOk(result2)) {
|
|
1130
|
-
console.log("User created:", result2.value);
|
|
1114
|
+
const result = validateUserId("user-123");
|
|
1115
|
+
if (result.isOk()) {
|
|
1116
|
+
console.log("Valid:", result.value);
|
|
1131
1117
|
} else {
|
|
1132
|
-
console.error("
|
|
1118
|
+
console.error("Invalid:", result.error);
|
|
1133
1119
|
}
|
|
1120
|
+
```
|
|
1134
1121
|
|
|
1135
|
-
|
|
1136
|
-
function riskyOperation(): string {
|
|
1137
|
-
if (Math.random() > 0.5) {
|
|
1138
|
-
throw new Error("Something went wrong");
|
|
1139
|
-
}
|
|
1140
|
-
return "success";
|
|
1141
|
-
}
|
|
1122
|
+
`ddd-kit` declares `@shirudo/result` as a `peerDependency`, so install it once in your app:
|
|
1142
1123
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
} else {
|
|
1147
|
-
console.error(result3.error.message); // "Something went wrong"
|
|
1148
|
-
}
|
|
1124
|
+
```bash
|
|
1125
|
+
pnpm add @shirudo/result
|
|
1126
|
+
```
|
|
1149
1127
|
|
|
1150
|
-
|
|
1151
|
-
async function riskyAsyncOperation(): Promise<string> {
|
|
1152
|
-
if (Math.random() > 0.5) {
|
|
1153
|
-
throw new Error("Async error");
|
|
1154
|
-
}
|
|
1155
|
-
return "async success";
|
|
1156
|
-
}
|
|
1128
|
+
For composition utilities (`map`, `flatMap`, `mapErr`, `match`, `unwrapOr`, `pipe`, `tryCatch`, async variants, etc.), refer to the [`@shirudo/result` documentation](https://www.npmjs.com/package/@shirudo/result).
|
|
1157
1129
|
|
|
1158
|
-
|
|
1159
|
-
match(result4,
|
|
1160
|
-
(value) => console.log("Success:", value),
|
|
1161
|
-
(error) => console.error("Error:", error.message)
|
|
1162
|
-
);
|
|
1163
|
-
```
|
|
1130
|
+
**Where `ddd-kit` uses `Result` vs `throw`:**
|
|
1164
1131
|
|
|
1165
|
-
**
|
|
1166
|
-
-
|
|
1167
|
-
- `
|
|
1168
|
-
-
|
|
1169
|
-
- `unwrapOr<T, E>(result, defaultValue)` - Returns value if Ok, otherwise returns default.
|
|
1170
|
-
- `unwrapOrElse<T, E>(result, fn)` - Returns value if Ok, otherwise computes default from error.
|
|
1171
|
-
- `match<T, E, R>(result, onOk, onErr)` - Pattern matching. Applies one function if Ok, another if Err. Supports both function and object syntax.
|
|
1172
|
-
- `matchAsync<T, E, R>(result, onOk, onErr)` - Asynchronous pattern matching. Applies async functions for Ok/Err cases. Supports both function and object syntax.
|
|
1173
|
-
- `pipe<T, E>(initial, ...fns)` - Pipes a Result through multiple operations. Stops on first error. Cleaner alternative to nested `andThen` calls.
|
|
1174
|
-
- `tryCatch<T, E>(fn, errorMapper?)` - Wraps a function that may throw exceptions into a Result type. Catches exceptions and converts them to Err results.
|
|
1175
|
-
- `tryCatchAsync<T, E>(fn, errorMapper?)` - Wraps an async function that may throw exceptions into a Promise<Result>. Catches exceptions and Promise rejections.
|
|
1176
|
-
|
|
1177
|
-
**Class-based API (for method chaining):**
|
|
1178
|
-
- `Outcome<T, E>` - Wrapper class for Result with method chaining support
|
|
1179
|
-
- `Success<T>` - Class representing successful results (created via `Ok()` factory)
|
|
1180
|
-
- `Erroneous<E>` - Class representing error results (created via `Err()` factory)
|
|
1132
|
+
- **Domain layer throws** `DomainError`-derived exceptions. `EventSourcedAggregate.apply()`, the `validateEvent()` hook, and the `ValueObject` constructor all throw. Subclass `DomainError` for your aggregate-specific errors (e.g. `OrderAlreadyShippedError`) and catch via `instanceof`.
|
|
1133
|
+
- **Infrastructure boundary returns `Result`** where corruption is an expected recoverable failure: `EventSourcedAggregate.loadFromHistory()`, `restoreFromSnapshotWithEvents()`.
|
|
1134
|
+
- **App-Service boundary returns `Result`**: `CommandBus.execute()`, `QueryBus.execute()`, `CommandHandler<C,R>`, `QueryHandler<Q,R>`, `withCommit()`. This is where you map errors to HTTP statuses, logs, etc.
|
|
1135
|
+
- **`voWithValidation`** is the explicit Result variant for parsing untrusted input at the App boundary. For Domain construction, use the `ValueObject` base class (constructor throws via `validate()`).
|
|
1181
1136
|
|
|
1182
1137
|
## API Documentation
|
|
1183
1138
|
|
|
1184
1139
|
This package is written in TypeScript and provides full type definitions. All types and functions are exported from the main entry point. You can explore the available APIs through your IDE's autocomplete or by examining the type definitions in `node_modules/@shirudo/ddd-kit/dist/index.d.ts`.
|
|
1185
1140
|
|
|
1186
1141
|
Key exports include:
|
|
1187
|
-
- `vo()`, `voEquals()`, `voEqualsExcept()`, `voWithValidation()
|
|
1142
|
+
- `vo()`, `voEquals()`, `voEqualsExcept()`, `voWithValidation()` - Value Object utilities (`voWithValidation` is for the App-Service boundary; Domain construction goes through the `ValueObject` base class which throws via `validate()`)
|
|
1188
1143
|
- `IAggregateRoot<TId>` - Marker interface for Aggregate Root Entities
|
|
1189
1144
|
- `AggregateRoot<TState, TId, TEvent?>` - Base class for creating Aggregate Root Entities without Event Sourcing (extends `Entity`, implements `IAggregateRoot<TId>`). Optional `TEvent` parameter enables type-safe domain events
|
|
1190
1145
|
- `EventSourcedAggregate<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Roots (extends `Entity`, implements `IEventSourcedAggregate<TId, TEvent>`)
|
|
@@ -1210,14 +1165,12 @@ Key exports include:
|
|
|
1210
1165
|
- `EventBus.subscribe()` - Subscribe handlers to event types
|
|
1211
1166
|
- `EventBus.publish()` - Publish events to all subscribers (uses `Promise.allSettled` — all handlers run even if one fails)
|
|
1212
1167
|
- `EventBus.once()` - Wait for the next event of a given type (returns Promise, auto-unsubscribes)
|
|
1213
|
-
- `
|
|
1214
|
-
- `andThen()`, `map()`, `mapErr()` - Result composition utilities
|
|
1215
|
-
- `unwrapOr()`, `unwrapOrElse()`, `match()` - Result unwrapping and pattern matching
|
|
1216
|
-
- `Id<Tag>` - Branded ID type
|
|
1168
|
+
- `Id<Tag>` - Branded ID type (Result type and operators come from the `@shirudo/result` peer dep)
|
|
1217
1169
|
- `IRepository<TAgg, TId>` - Repository interface
|
|
1218
|
-
- `
|
|
1219
|
-
- `
|
|
1220
|
-
- `
|
|
1170
|
+
- `IQueryableRepository<TAgg, TId, TFilter>` - Repository extension that adds filter-based querying. `TFilter` is the persistence layer's native filter shape (Drizzle SQL, Prisma WhereInput, Mongo filter doc, in-memory predicate, …). Repositories that are only accessed by id should implement `IRepository` directly and skip this extension.
|
|
1171
|
+
- `TransactionScope` - Transaction-scope abstraction (wraps a block of work in the persistence layer's native transaction). Intentionally minimal — not Fowler's full UoW with change tracking.
|
|
1172
|
+
- `DomainError` - Abstract base for domain exceptions (Consumer subclasses for their aggregate-specific errors)
|
|
1173
|
+
- `MissingHandlerError`, `AggregateNotFoundError` - Concrete library-internal `DomainError` subclasses
|
|
1221
1174
|
|
|
1222
1175
|
## Concurrency & Thread Safety
|
|
1223
1176
|
|