@shirudo/ddd-kit 0.10.0 → 0.12.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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Composable TypeScript toolkit for tactical Domain-Driven Design.
4
4
 
5
+ > **⚠️ BETA WARNING**
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.
8
+
5
9
  ## Badges
6
10
 
7
11
  ![npm version](https://img.shields.io/npm/v/@shirudo/ddd-kit)
@@ -62,23 +66,30 @@ Value Objects are immutable objects that are defined by their attributes rather
62
66
 
63
67
  ### Entities
64
68
 
65
- In Domain-Driven Design, there are two types of entities:
69
+ In Domain-Driven Design, Entities are objects with identity and state. Unlike Value Objects (compared by value), Entities are compared by identity (id). There are two types of entities:
66
70
 
67
71
  1. **Aggregate Root Entity**: The parent Entity of an aggregate.
68
- - Has identity (id) and version for optimistic concurrency control
72
+ - Has identity (id), state, and version for optimistic concurrency control
69
73
  - Represents the aggregate externally
70
74
  - Loaded/saved through repositories
71
- - Created by extending `AggregateBase` or `AggregateEventSourced`
72
- - Implements `AggregateRoot<TId>`
75
+ - Created by extending `AggregateRoot` or `AggregateEventSourced`
76
+ - Implements `IAggregateRoot<TId>`
73
77
 
74
78
  2. **Child Entities**: Entities within an aggregate.
75
- - Have identity (id), but no own version
79
+ - Have identity (id) and state, but no own version
80
+ - Can have business logic (methods) specific to the entity
76
81
  - Exist only within the aggregate boundary
77
82
  - Versioned through the Aggregate Root
78
83
  - Cannot be referenced directly from outside the aggregate
79
- - Use the `Entity<TId>` interface for type safety
84
+ - **Two approaches**:
85
+ - **Class-based** (recommended for entities with logic): Extend `Entity<TState, TId>`
86
+ - **Functional-style** (for simple data): Use `Identifiable<TId> & TProps`
80
87
 
81
- The `Entity<TId>` interface is used for child entities within aggregates. Helper functions like `sameEntity()`, `findEntityById()`, `hasEntityId()`, `updateEntityById()`, and `removeEntityById()` provide utilities for working with child entity collections.
88
+ The library provides:
89
+ - **`Entity<TState, TId>`** - Base class for entities with state and business logic
90
+ - **`Entity<TId>`** - Simple class for entities without state management
91
+ - **`Identifiable<TId>`** - Minimal interface for objects with id
92
+ - Helper functions like `sameEntity()`, `findEntityById()`, `hasEntityId()`, `updateEntityById()`, and `removeEntityById()` for working with entity collections
82
93
 
83
94
  ### Aggregates
84
95
 
@@ -474,7 +485,7 @@ import {
474
485
  type CreateOrderCommand = Command & {
475
486
  type: "CreateOrder";
476
487
  customerId: string;
477
- items: Array<{ productId: string; quantity: number }>;
488
+ items: Array<{ productId: string; quantity: number; price: number }>;
478
489
  };
479
490
 
480
491
  // Create a command handler
@@ -487,7 +498,14 @@ const createOrderHandler: CommandHandler<CreateOrderCommand, string> = async (
487
498
  }
488
499
 
489
500
  // Perform business logic
490
- const order = Order.create(cmd.customerId, cmd.items);
501
+ const orderId = `order-${Date.now()}` as OrderId;
502
+ const order = Order.create(orderId, cmd.customerId);
503
+
504
+ // Add items to the order
505
+ for (const item of cmd.items) {
506
+ order.addItem(item.productId, item.quantity, item.price);
507
+ }
508
+
491
509
  await repository.save(order);
492
510
 
493
511
  return ok(order.id);
@@ -497,7 +515,7 @@ const createOrderHandler: CommandHandler<CreateOrderCommand, string> = async (
497
515
  const result = await createOrderHandler({
498
516
  type: "CreateOrder",
499
517
  customerId: "customer-123",
500
- items: [{ productId: "product-1", quantity: 2 }],
518
+ items: [{ productId: "product-1", quantity: 2, price: 10.0 }],
501
519
  });
502
520
 
503
521
  if (result.ok) {
@@ -514,7 +532,7 @@ commandBus.register("CreateOrder", createOrderHandler);
514
532
  const busResult = await commandBus.execute({
515
533
  type: "CreateOrder",
516
534
  customerId: "customer-123",
517
- items: [{ productId: "product-1", quantity: 2 }],
535
+ items: [{ productId: "product-1", quantity: 2, price: 10.0 }],
518
536
  });
519
537
  ```
520
538
 
@@ -584,7 +602,14 @@ const createOrderHandler: CommandHandler<CreateOrderCommand, string> = async (
584
602
  return await withCommit(
585
603
  { outbox, bus, uow },
586
604
  async () => {
587
- const order = Order.create(cmd.customerId, cmd.items);
605
+ const orderId = `order-${Date.now()}` as OrderId;
606
+ const order = Order.create(orderId, cmd.customerId);
607
+
608
+ // Add items to the order
609
+ for (const item of cmd.items) {
610
+ order.addItem(item.productId, item.quantity, item.price);
611
+ }
612
+
588
613
  await repository.save(order);
589
614
 
590
615
  return {
@@ -622,7 +647,7 @@ import {
622
647
  type CreateOrderCommand = Command & {
623
648
  type: "CreateOrder";
624
649
  customerId: string;
625
- items: OrderItem[];
650
+ items: Array<{ productId: string; quantity: number; price: number }>;
626
651
  };
627
652
 
628
653
  type GetOrderQuery = Query & {
@@ -634,7 +659,14 @@ type GetOrderQuery = Query & {
634
659
  const createOrderHandler: CommandHandler<CreateOrderCommand, OrderId> = async (
635
660
  cmd
636
661
  ) => {
637
- const order = Order.create(cmd.customerId, cmd.items);
662
+ const orderId = `order-${Date.now()}` as OrderId;
663
+ const order = Order.create(orderId, cmd.customerId);
664
+
665
+ // Add items to the order
666
+ for (const item of cmd.items) {
667
+ order.addItem(item.productId, item.quantity, item.price);
668
+ }
669
+
638
670
  await repository.save(order);
639
671
  return ok(order.id);
640
672
  };
@@ -770,26 +802,27 @@ const eventV2 = createDomainEvent(
770
802
 
771
803
  ### Working with Child Entities
772
804
 
773
- An Aggregate Root Entity can contain multiple child entities. Child entities have identity (id) but no own version - they are versioned through the Aggregate Root.
805
+ An Aggregate Root Entity can contain multiple child entities. Child entities have identity (id) and state, but no own version - they are versioned through the Aggregate Root.
806
+
807
+ #### Approach 1: Functional-Style Child Entities (Simple Data)
808
+
809
+ For simple child entities without business logic, use the functional approach with intersection types:
774
810
 
775
811
  ```typescript
776
812
  import {
777
- AggregateBase,
778
- Entity,
813
+ AggregateRoot,
814
+ Identifiable,
779
815
  findEntityById,
780
- hasEntityId,
781
- removeEntityById,
782
816
  updateEntityById,
783
- sameEntity,
784
- type AggregateRoot,
817
+ type IAggregateRoot,
785
818
  type Id,
786
819
  } from "@shirudo/ddd-kit";
787
820
 
788
821
  type OrderId = Id<"OrderId">;
789
822
  type ItemId = Id<"ItemId">;
790
823
 
791
- // Child Entity within the aggregate (has id, but no own version)
792
- type OrderItem = Entity<ItemId> & {
824
+ // Functional-style child entity (simple data, no logic)
825
+ type OrderItem = Identifiable<ItemId> & {
793
826
  productId: string;
794
827
  quantity: number;
795
828
  price: number;
@@ -799,13 +832,13 @@ type OrderItem = Entity<ItemId> & {
799
832
  type OrderState = {
800
833
  id: OrderId;
801
834
  customerId: string;
802
- items: OrderItem[]; // Child entities
835
+ items: OrderItem[];
803
836
  total: number;
804
837
  };
805
838
 
806
839
  // Order is the Aggregate Root (an Entity with id + version)
807
- class Order extends AggregateBase<OrderState, OrderId>
808
- implements AggregateRoot<OrderId> {
840
+ class Order extends AggregateRoot<OrderState, OrderId>
841
+ implements IAggregateRoot<OrderId> {
809
842
  static create(id: OrderId, customerId: string): Order {
810
843
  const initialState: OrderState = {
811
844
  id,
@@ -882,6 +915,128 @@ order.removeItem(itemId); // Removes child entity
882
915
  console.log(order.version); // 3 (one for each operation)
883
916
  ```
884
917
 
918
+ #### Approach 2: Class-Based Child Entities (With Business Logic)
919
+
920
+ For child entities that need business logic, extend `Entity<TState, TId>`:
921
+
922
+ ```typescript
923
+ import {
924
+ AggregateRoot,
925
+ Entity,
926
+ findEntityById,
927
+ type IAggregateRoot,
928
+ type Id,
929
+ } from "@shirudo/ddd-kit";
930
+
931
+ type OrderId = Id<"OrderId">;
932
+ type ItemId = Id<"ItemId">;
933
+
934
+ // State of OrderItem
935
+ type OrderItemState = {
936
+ productId: string;
937
+ quantity: number;
938
+ price: number;
939
+ };
940
+
941
+ // Class-based child entity with business logic
942
+ class OrderItem extends Entity<OrderItemState, ItemId> {
943
+ constructor(id: ItemId, productId: string, quantity: number, price: number) {
944
+ const initialState: OrderItemState = { productId, quantity, price };
945
+ super(id, initialState);
946
+ }
947
+
948
+ // Entity-specific business logic
949
+ updateQuantity(newQuantity: number): void {
950
+ if (newQuantity <= 0) {
951
+ throw new Error("Quantity must be greater than 0");
952
+ }
953
+ this._state = { ...this._state, quantity: newQuantity };
954
+ }
955
+
956
+ calculateSubtotal(): number {
957
+ return this._state.price * this._state.quantity;
958
+ }
959
+
960
+ isForProduct(productId: string): boolean {
961
+ return this._state.productId === productId;
962
+ }
963
+
964
+ protected validateState(state: OrderItemState): void {
965
+ if (state.quantity <= 0) throw new Error("Quantity must be greater than 0");
966
+ if (state.price < 0) throw new Error("Price cannot be negative");
967
+ if (!state.productId) throw new Error("Product ID is required");
968
+ }
969
+ }
970
+
971
+ // Aggregate state contains child entity instances
972
+ type OrderState = {
973
+ id: OrderId;
974
+ customerId: string;
975
+ items: OrderItem[]; // Child entities with logic
976
+ status: "pending" | "confirmed";
977
+ };
978
+
979
+ // Aggregate Root
980
+ class Order extends AggregateRoot<OrderState, OrderId>
981
+ implements IAggregateRoot<OrderId> {
982
+ private itemCounter = 0;
983
+
984
+ static create(id: OrderId, customerId: string): Order {
985
+ const initialState: OrderState = {
986
+ id,
987
+ customerId,
988
+ items: [],
989
+ status: "pending",
990
+ };
991
+ return new Order(id, initialState);
992
+ }
993
+
994
+ addItem(productId: string, quantity: number, price: number): ItemId {
995
+ const itemId = `item-${++this.itemCounter}` as ItemId;
996
+ const item = new OrderItem(itemId, productId, quantity, price);
997
+
998
+ this._state = {
999
+ ...this._state,
1000
+ items: [...this._state.items, item],
1001
+ };
1002
+ this.bumpVersion();
1003
+ return itemId;
1004
+ }
1005
+
1006
+ // Delegate to entity's business logic
1007
+ updateItemQuantity(itemId: ItemId, newQuantity: number): void {
1008
+ const item = findEntityById(this._state.items, itemId);
1009
+ if (!item) throw new Error("Item not found");
1010
+
1011
+ item.updateQuantity(newQuantity); // Uses entity's logic
1012
+ this.bumpVersion();
1013
+ }
1014
+
1015
+ // Use entity's business logic
1016
+ calculateTotal(): number {
1017
+ return this._state.items.reduce(
1018
+ (total, item) => total + item.calculateSubtotal(),
1019
+ 0
1020
+ );
1021
+ }
1022
+
1023
+ confirm(): void {
1024
+ if (this._state.items.length === 0) {
1025
+ throw new Error("Cannot confirm an order without items");
1026
+ }
1027
+ this._state = { ...this._state, status: "confirmed" };
1028
+ this.bumpVersion();
1029
+ }
1030
+ }
1031
+
1032
+ // Usage
1033
+ const order = Order.create("order-1" as OrderId, "customer-1");
1034
+ const itemId = order.addItem("product-1", 2, 10.0);
1035
+ order.updateItemQuantity(itemId, 3); // Uses entity's validation
1036
+ const total = order.calculateTotal(); // Uses entity's calculateSubtotal()
1037
+ console.log(total); // 30.0
1038
+ ```
1039
+
885
1040
  ### Using Result Type for Error Handling
886
1041
 
887
1042
  The `Result<T, E>` type provides composition utilities to avoid repetitive `if (isErr)` checks:
@@ -1011,14 +1166,16 @@ This package is written in TypeScript and provides full type definitions. All ty
1011
1166
 
1012
1167
  Key exports include:
1013
1168
  - `vo()`, `voEquals()`, `voEqualsExcept()`, `voWithValidation()`, `voWithValidationUnsafe()` - Value Object utilities
1014
- - `AggregateRoot<TId>` - Marker interface for Aggregate Root Entities
1015
- - `AggregateBase<TState, TId>` - Base class for creating Aggregate Root Entities without Event Sourcing (implements `AggregateRoot<TId>`)
1016
- - `AggregateEventSourced<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Root Entities (extends `AggregateBase`, implements `AggregateRoot<TId>`)
1169
+ - `IAggregateRoot<TId>` - Marker interface for Aggregate Root Entities
1170
+ - `AggregateRoot<TState, TId>` - Base class for creating Aggregate Root Entities without Event Sourcing (extends `Entity`, implements `IAggregateRoot<TId>`)
1171
+ - `AggregateEventSourced<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Root Entities (extends `AggregateRoot`, implements `IAggregateEventSourced<TId, TEvent>`)
1017
1172
  - `AggregateConfig`, `AggregateEventSourcedConfig` - Configuration interfaces
1018
1173
  - `AggregateSnapshot<TState>` - Snapshot interface for performance optimization
1019
1174
  - `sameAggregate()` - Aggregate equality helper
1020
- - `Entity<TId>` - Optional interface for entities with identity
1021
- - `sameEntity()`, `findEntityById()`, `hasEntityId()`, `removeEntityById()` - Entity helpers
1175
+ - `Entity<TState, TId>` - Base class for entities with state and business logic
1176
+ - `IEntity<TId, TState>` - Entity interface
1177
+ - `Identifiable<TId>` - Minimal interface for objects with id
1178
+ - `sameEntity()`, `findEntityById()`, `hasEntityId()`, `removeEntityById()`, `updateEntityById()`, `replaceEntityById()`, `entityIds()` - Entity helper functions
1022
1179
  - `Command`, `CommandHandler<C, R>` - Command interface and handler type for CQRS
1023
1180
  - `Query`, `QueryHandler<Q, R>` - Query interface and handler type for CQRS
1024
1181
  - `CommandBus`, `ICommandBus` - Command bus for centralized command execution