@shirudo/ddd-kit 0.16.0 → 1.0.0-rc.1

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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Composable TypeScript toolkit for tactical Domain-Driven Design.
4
4
 
5
- > **⚠️ BETA WARNING**
5
+ > **Release Candidate**
6
6
  >
7
- > This library is currently in beta. The API is subject to change until a stable 1.0.0 release. Breaking changes may occur in minor versions during the beta phase. Please pin your dependencies to specific versions.
7
+ > 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.
8
8
 
9
9
  ## Badges
10
10
 
@@ -20,7 +20,7 @@ Composable TypeScript toolkit for tactical Domain-Driven Design.
20
20
  - **Repositories** - Persistence abstraction layer for aggregates with specification pattern support
21
21
  - **Specifications** - Reusable query specifications for complex domain queries
22
22
  - **Unit of Work** - Transaction management for maintaining consistency across operations
23
- - **Result Type** - Functional error handling with `Result<T, E>` type for explicit success/failure states
23
+ - **Result Type** - Functional error handling with `Result<T, E>` type for explicit success/failure states. For advanced error handling with typed error hierarchies, see [`@shirudo/base-error`](https://www.npmjs.com/package/@shirudo/base-error)
24
24
 
25
25
  ## Installation
26
26
 
@@ -72,7 +72,7 @@ In Domain-Driven Design, Entities are objects with identity and state. Unlike Va
72
72
  - Has identity (id), state, and version for optimistic concurrency control
73
73
  - Represents the aggregate externally
74
74
  - Loaded/saved through repositories
75
- - Created by extending `AggregateRoot` or `AggregateEventSourced`
75
+ - Created by extending `AggregateRoot` (state-based) or `EventSourcedAggregate` (event-sourced)
76
76
  - Implements `IAggregateRoot<TId>`
77
77
 
78
78
  2. **Child Entities**: Entities within an aggregate.
@@ -107,7 +107,7 @@ The library provides:
107
107
 
108
108
  - **`AggregateRoot<TState, TId, TEvent?>`** - Base class for creating Aggregate Root Entities without Event Sourcing. Implements `IAggregateRoot<TId>`. The optional `TEvent` parameter (defaults to `unknown`) enables type-safe domain events — only aggregates that specify it get compile-time event validation. Provides ID and version management, state management, domain event tracking, and snapshot support. Use this when you don't need Event Sourcing but still want aggregate patterns with versioning and state management.
109
109
 
110
- - **`AggregateEventSourced<TState, TEvent, TId>`** - Base class for Event-Sourced Aggregate Root Entities. Extends `AggregateRoot` (and thus implements `IAggregateRoot<TId>`). Adds event tracking, event handlers, event validation, and history replay capabilities. Use this when you want full Event Sourcing with event tracking and replay.
110
+ - **`EventSourcedAggregate<TState, TEvent, TId>`** - Base class for Event-Sourced Aggregate Roots. Extends `Entity` directly (not `AggregateRoot`) so that state changes can only happen through event handlers via `apply()`. Provides event tracking, event validation, history replay, and snapshot support.
111
111
 
112
112
  Both classes support automatic versioning (configurable), snapshot creation/restoration, and optimistic concurrency control. The version applies to the entire aggregate, including all child entities.
113
113
 
@@ -117,7 +117,7 @@ CQRS separates read operations (Queries) from write operations (Commands), provi
117
117
 
118
118
  ### Domain Events
119
119
 
120
- Domain Events represent something meaningful that happened in your domain. They are immutable records with a type, payload, timestamp, optional version for schema evolution, and metadata for traceability. Events support versioning for handling schema changes over time and include metadata fields like `correlationId`, `causationId`, `userId`, and `source` for tracking event flow in distributed systems. Events are automatically tracked by aggregates and can be published to event buses or stored in outboxes for eventual consistency.
120
+ Domain Events represent something meaningful that happened in your domain. They are immutable records with a type, payload, timestamp, version for schema evolution, and optional metadata for traceability. Events support versioning for handling schema changes over time and include metadata fields like `correlationId`, `causationId`, `userId`, and `source` for tracking event flow in distributed systems. Events are automatically tracked by aggregates and can be published to event buses or stored in outboxes for eventual consistency.
121
121
 
122
122
  ### Repositories
123
123
 
@@ -247,32 +247,29 @@ class Order extends AggregateRoot<OrderState, OrderId> {
247
247
  }
248
248
 
249
249
  addItem(productId: string, quantity: number, price: number): void {
250
- if (this._state.status !== "pending") {
250
+ if (this.state.status !== "pending") {
251
251
  throw new Error("Cannot add items to a non-pending order");
252
252
  }
253
253
 
254
- this._state = {
255
- ...this._state,
256
- items: [...this._state.items, { productId, quantity, price }],
257
- total: this._state.total + quantity * price,
258
- };
259
- this.bumpVersion(); // Manual version bump for optimistic concurrency control
254
+ this.setState({
255
+ ...this.state,
256
+ items: [...this.state.items, { productId, quantity, price }],
257
+ total: this.state.total + quantity * price,
258
+ }, true); // true = bump version for optimistic concurrency control
260
259
  }
261
260
 
262
261
  confirm(): void {
263
- if (this._state.status !== "pending") {
262
+ if (this.state.status !== "pending") {
264
263
  throw new Error("Only pending orders can be confirmed");
265
264
  }
266
- this._state = { ...this._state, status: "confirmed" };
267
- this.bumpVersion();
265
+ this.setState({ ...this.state, status: "confirmed" }, true);
268
266
  }
269
267
 
270
268
  ship(): void {
271
- if (this._state.status !== "confirmed") {
269
+ if (this.state.status !== "confirmed") {
272
270
  throw new Error("Only confirmed orders can be shipped");
273
271
  }
274
- this._state = { ...this._state, status: "shipped" };
275
- this.bumpVersion();
272
+ this.setState({ ...this.state, status: "shipped" }, true);
276
273
  }
277
274
  }
278
275
 
@@ -297,15 +294,13 @@ type OrderDomainEvent =
297
294
 
298
295
  class Order extends AggregateRoot<OrderState, OrderId, OrderDomainEvent> {
299
296
  confirm(): void {
300
- this._state = { ...this._state, status: "confirmed" };
297
+ this.setState({ ...this.state, status: "confirmed" }, true);
301
298
  this.addDomainEvent({ type: "OrderConfirmed" }); // type-safe
302
- this.bumpVersion();
303
299
  }
304
300
 
305
301
  ship(trackingNumber: string): void {
306
- this._state = { ...this._state, status: "shipped" };
302
+ this.setState({ ...this.state, status: "shipped" }, true);
307
303
  this.addDomainEvent({ type: "OrderShipped", trackingNumber }); // type-safe
308
- this.bumpVersion();
309
304
  }
310
305
  }
311
306
 
@@ -317,7 +312,7 @@ class Order extends AggregateRoot<OrderState, OrderId, OrderDomainEvent> {
317
312
 
318
313
  ```typescript
319
314
  import {
320
- AggregateEventSourced,
315
+ EventSourcedAggregate,
321
316
  createDomainEvent,
322
317
  type AggregateRoot,
323
318
  type Id,
@@ -339,7 +334,7 @@ type OrderShipped = DomainEvent<"OrderShipped", { trackingNumber: string }>;
339
334
 
340
335
  type OrderEvent = OrderCreated | OrderConfirmed | OrderShipped;
341
336
 
342
- class Order extends AggregateEventSourced<OrderState, OrderEvent, OrderId> implements AggregateRoot<OrderId> {
337
+ class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
343
338
  static create(id: OrderId, customerId: string): Order {
344
339
  const initialState: OrderState = {
345
340
  id,
@@ -417,8 +412,8 @@ console.log(order.version); // 3 (automatically bumped)
417
412
  ```typescript
418
413
  import {
419
414
  AggregateRoot,
420
- AggregateEventSourced,
421
- sameAggregate,
415
+ EventSourcedAggregate,
416
+ sameVersion,
422
417
  type Id,
423
418
  } from "@shirudo/ddd-kit";
424
419
 
@@ -441,11 +436,11 @@ const eventSourcedOrder = EventSourcedOrder.create("order-123" as OrderId, "cust
441
436
  const eventsAfterSnapshot = [/* events that occurred after snapshot */];
442
437
  eventSourcedOrder.restoreFromSnapshotWithEvents(snapshot, eventsAfterSnapshot);
443
438
 
444
- // Aggregate equality check
439
+ // Optimistic concurrency check
445
440
  const order1 = await repository.getById(id);
446
441
  // ... some operations ...
447
442
  const order2 = await repository.getById(id);
448
- if (!sameAggregate(order1, order2)) {
443
+ if (!sameVersion(order1, order2)) {
449
444
  throw new Error("Aggregate was modified by another process");
450
445
  }
451
446
  ```
@@ -454,7 +449,7 @@ if (!sameAggregate(order1, order2)) {
454
449
 
455
450
  ```typescript
456
451
  import {
457
- AggregateEventSourced,
452
+ EventSourcedAggregate,
458
453
  createDomainEvent,
459
454
  err,
460
455
  ok,
@@ -469,7 +464,7 @@ type OrderState = { id: OrderId; status: "pending" | "confirmed" | "shipped" };
469
464
  type OrderShipped = DomainEvent<"OrderShipped", { trackingNumber: string }>;
470
465
  type OrderEvent = OrderShipped;
471
466
 
472
- class Order extends AggregateEventSourced<OrderState, OrderEvent, OrderId> implements AggregateRoot<OrderId> {
467
+ class Order extends EventSourcedAggregate<OrderState, OrderEvent, OrderId> {
473
468
  // Event validation
474
469
  protected validateEvent(event: OrderEvent): Result<true, string> {
475
470
  if (event.type === "OrderShipped" && this.state.status !== "confirmed") {
@@ -892,49 +887,42 @@ class Order extends AggregateRoot<OrderState, OrderId>
892
887
  price,
893
888
  };
894
889
 
895
- this._state = {
896
- ...this._state,
897
- items: [...this._state.items, item],
898
- total: this._state.total + price * quantity,
899
- };
900
- this.bumpVersion(); // Versions the entire aggregate (including child entities)
890
+ this.setState({
891
+ ...this.state,
892
+ items: [...this.state.items, item],
893
+ total: this.state.total + price * quantity,
894
+ }, true); // true = bump version (versions the entire aggregate including child entities)
901
895
  return itemId;
902
896
  }
903
897
 
904
898
  updateItemQuantity(itemId: ItemId, newQuantity: number): void {
905
- const item = findEntityById(this._state.items, itemId);
899
+ const item = findEntityById(this.state.items, itemId);
906
900
  if (!item) {
907
901
  throw new Error("Item not found");
908
902
  }
909
903
 
910
- this._state = {
911
- ...this._state,
912
- items: updateEntityById(
913
- this._state.items,
914
- itemId,
915
- (i) => ({ ...i, quantity: newQuantity })
916
- ),
917
- total: this._state.total - item.price * item.quantity + item.price * newQuantity,
918
- };
919
- this.bumpVersion(); // Versions the entire aggregate
904
+ this.setState({
905
+ ...this.state,
906
+ items: updateEntityById(this.state.items, itemId, (i) => ({ ...i, quantity: newQuantity })),
907
+ total: this.state.total - item.price * item.quantity + item.price * newQuantity,
908
+ }, true);
920
909
  }
921
910
 
922
911
  removeItem(itemId: ItemId): void {
923
- const item = findEntityById(this._state.items, itemId);
912
+ const item = findEntityById(this.state.items, itemId);
924
913
  if (!item) {
925
914
  throw new Error("Item not found");
926
915
  }
927
916
 
928
- this._state = {
929
- ...this._state,
930
- items: removeEntityById(this._state.items, itemId),
931
- total: this._state.total - item.price * item.quantity,
932
- };
933
- this.bumpVersion(); // Versions the entire aggregate
917
+ this.setState({
918
+ ...this.state,
919
+ items: removeEntityById(this.state.items, itemId),
920
+ total: this.state.total - item.price * item.quantity,
921
+ }, true);
934
922
  }
935
923
 
936
924
  getItem(itemId: ItemId): OrderItem | undefined {
937
- return findEntityById(this._state.items, itemId);
925
+ return findEntityById(this.state.items, itemId);
938
926
  }
939
927
  }
940
928
 
@@ -983,15 +971,15 @@ class OrderItem extends Entity<OrderItemState, ItemId> {
983
971
  if (newQuantity <= 0) {
984
972
  throw new Error("Quantity must be greater than 0");
985
973
  }
986
- this._state = { ...this._state, quantity: newQuantity };
974
+ this.setState({ ...this.state, quantity: newQuantity });
987
975
  }
988
976
 
989
977
  calculateSubtotal(): number {
990
- return this._state.price * this._state.quantity;
978
+ return this.state.price * this.state.quantity;
991
979
  }
992
980
 
993
981
  isForProduct(productId: string): boolean {
994
- return this._state.productId === productId;
982
+ return this.state.productId === productId;
995
983
  }
996
984
 
997
985
  protected validateState(state: OrderItemState): void {
@@ -1028,17 +1016,16 @@ class Order extends AggregateRoot<OrderState, OrderId>
1028
1016
  const itemId = `item-${++this.itemCounter}` as ItemId;
1029
1017
  const item = new OrderItem(itemId, productId, quantity, price);
1030
1018
 
1031
- this._state = {
1032
- ...this._state,
1033
- items: [...this._state.items, item],
1034
- };
1035
- this.bumpVersion();
1019
+ this.setState({
1020
+ ...this.state,
1021
+ items: [...this.state.items, item],
1022
+ }, true);
1036
1023
  return itemId;
1037
1024
  }
1038
1025
 
1039
1026
  // Delegate to entity's business logic
1040
1027
  updateItemQuantity(itemId: ItemId, newQuantity: number): void {
1041
- const item = findEntityById(this._state.items, itemId);
1028
+ const item = findEntityById(this.state.items, itemId);
1042
1029
  if (!item) throw new Error("Item not found");
1043
1030
 
1044
1031
  item.updateQuantity(newQuantity); // Uses entity's logic
@@ -1047,18 +1034,17 @@ class Order extends AggregateRoot<OrderState, OrderId>
1047
1034
 
1048
1035
  // Use entity's business logic
1049
1036
  calculateTotal(): number {
1050
- return this._state.items.reduce(
1037
+ return this.state.items.reduce(
1051
1038
  (total, item) => total + item.calculateSubtotal(),
1052
1039
  0
1053
1040
  );
1054
1041
  }
1055
1042
 
1056
1043
  confirm(): void {
1057
- if (this._state.items.length === 0) {
1044
+ if (this.state.items.length === 0) {
1058
1045
  throw new Error("Cannot confirm an order without items");
1059
1046
  }
1060
- this._state = { ...this._state, status: "confirmed" };
1061
- this.bumpVersion();
1047
+ this.setState({ ...this.state, status: "confirmed" }, true);
1062
1048
  }
1063
1049
  }
1064
1050
 
@@ -1201,10 +1187,10 @@ Key exports include:
1201
1187
  - `vo()`, `voEquals()`, `voEqualsExcept()`, `voWithValidation()`, `voWithValidationUnsafe()` - Value Object utilities
1202
1188
  - `IAggregateRoot<TId>` - Marker interface for Aggregate Root Entities
1203
1189
  - `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
1204
- - `AggregateEventSourced<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Root Entities (extends `AggregateRoot`, implements `IAggregateEventSourced<TId, TEvent>`)
1205
- - `AggregateConfig`, `AggregateEventSourcedConfig` - Configuration interfaces
1190
+ - `EventSourcedAggregate<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Roots (extends `Entity`, implements `IEventSourcedAggregate<TId, TEvent>`)
1191
+ - `AggregateConfig`, `EventSourcedAggregateConfig` - Configuration interfaces
1206
1192
  - `AggregateSnapshot<TState>` - Snapshot interface for performance optimization
1207
- - `sameAggregate()` - Aggregate equality helper
1193
+ - `sameVersion()` - Optimistic concurrency check (same ID and version)
1208
1194
  - `Entity<TState, TId>` - Base class for entities with state and business logic
1209
1195
  - `IEntity<TId, TState>` - Entity interface
1210
1196
  - `Identifiable<TId>` - Minimal interface for objects with id
@@ -1228,7 +1214,7 @@ Key exports include:
1228
1214
  - `andThen()`, `map()`, `mapErr()` - Result composition utilities
1229
1215
  - `unwrapOr()`, `unwrapOrElse()`, `match()` - Result unwrapping and pattern matching
1230
1216
  - `Id<Tag>` - Branded ID type
1231
- - `IRepository<TState, TEvent, TAgg, TId>` - Repository interface
1217
+ - `IRepository<TAgg, TId>` - Repository interface
1232
1218
  - `ISpecification<T>` - Specification interface
1233
1219
  - `UnitOfWork` - Unit of Work interface
1234
1220
  - `guard()` - Guard/validation helper
@@ -1349,7 +1335,7 @@ class CreateOrderHandler implements CommandHandler<CreateOrderCommand, OrderId>
1349
1335
 
1350
1336
  // 3. Save
1351
1337
  await this.repository.save(order);
1352
- await this.eventBus.publish(order.pendingEvents);
1338
+ await this.eventBus.publish(order.domainEvents);
1353
1339
 
1354
1340
  return ok(order.id);
1355
1341
  // 4. Aggregate is garbage collected when method returns
@@ -1556,7 +1542,7 @@ class OrderService {
1556
1542
  }
1557
1543
 
1558
1544
  await this.repository.save(order);
1559
- await this.eventBus.publish(order.pendingEvents);
1545
+ await this.eventBus.publish(order.domainEvents);
1560
1546
 
1561
1547
  return ok(order.id);
1562
1548
  // order is garbage collected here