@shirudo/ddd-kit 0.11.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.
Files changed (2) hide show
  1. package/README.md +156 -24
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -66,23 +66,30 @@ Value Objects are immutable objects that are defined by their attributes rather
66
66
 
67
67
  ### Entities
68
68
 
69
- 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:
70
70
 
71
71
  1. **Aggregate Root Entity**: The parent Entity of an aggregate.
72
- - Has identity (id) and version for optimistic concurrency control
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 `AggregateBase` or `AggregateEventSourced`
76
- - Implements `AggregateRoot<TId>`
75
+ - Created by extending `AggregateRoot` or `AggregateEventSourced`
76
+ - Implements `IAggregateRoot<TId>`
77
77
 
78
78
  2. **Child Entities**: Entities within an aggregate.
79
- - 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
80
81
  - Exist only within the aggregate boundary
81
82
  - Versioned through the Aggregate Root
82
83
  - Cannot be referenced directly from outside the aggregate
83
- - 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`
84
87
 
85
- 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
86
93
 
87
94
  ### Aggregates
88
95
 
@@ -795,26 +802,27 @@ const eventV2 = createDomainEvent(
795
802
 
796
803
  ### Working with Child Entities
797
804
 
798
- 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:
799
810
 
800
811
  ```typescript
801
812
  import {
802
- AggregateBase,
803
- Entity,
813
+ AggregateRoot,
814
+ Identifiable,
804
815
  findEntityById,
805
- hasEntityId,
806
- removeEntityById,
807
816
  updateEntityById,
808
- sameEntity,
809
- type AggregateRoot,
817
+ type IAggregateRoot,
810
818
  type Id,
811
819
  } from "@shirudo/ddd-kit";
812
820
 
813
821
  type OrderId = Id<"OrderId">;
814
822
  type ItemId = Id<"ItemId">;
815
823
 
816
- // Child Entity within the aggregate (has id, but no own version)
817
- type OrderItem = Entity<ItemId> & {
824
+ // Functional-style child entity (simple data, no logic)
825
+ type OrderItem = Identifiable<ItemId> & {
818
826
  productId: string;
819
827
  quantity: number;
820
828
  price: number;
@@ -824,13 +832,13 @@ type OrderItem = Entity<ItemId> & {
824
832
  type OrderState = {
825
833
  id: OrderId;
826
834
  customerId: string;
827
- items: OrderItem[]; // Child entities
835
+ items: OrderItem[];
828
836
  total: number;
829
837
  };
830
838
 
831
839
  // Order is the Aggregate Root (an Entity with id + version)
832
- class Order extends AggregateBase<OrderState, OrderId>
833
- implements AggregateRoot<OrderId> {
840
+ class Order extends AggregateRoot<OrderState, OrderId>
841
+ implements IAggregateRoot<OrderId> {
834
842
  static create(id: OrderId, customerId: string): Order {
835
843
  const initialState: OrderState = {
836
844
  id,
@@ -907,6 +915,128 @@ order.removeItem(itemId); // Removes child entity
907
915
  console.log(order.version); // 3 (one for each operation)
908
916
  ```
909
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
+
910
1040
  ### Using Result Type for Error Handling
911
1041
 
912
1042
  The `Result<T, E>` type provides composition utilities to avoid repetitive `if (isErr)` checks:
@@ -1036,14 +1166,16 @@ This package is written in TypeScript and provides full type definitions. All ty
1036
1166
 
1037
1167
  Key exports include:
1038
1168
  - `vo()`, `voEquals()`, `voEqualsExcept()`, `voWithValidation()`, `voWithValidationUnsafe()` - Value Object utilities
1039
- - `AggregateRoot<TId>` - Marker interface for Aggregate Root Entities
1040
- - `AggregateBase<TState, TId>` - Base class for creating Aggregate Root Entities without Event Sourcing (implements `AggregateRoot<TId>`)
1041
- - `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>`)
1042
1172
  - `AggregateConfig`, `AggregateEventSourcedConfig` - Configuration interfaces
1043
1173
  - `AggregateSnapshot<TState>` - Snapshot interface for performance optimization
1044
1174
  - `sameAggregate()` - Aggregate equality helper
1045
- - `Entity<TId>` - Optional interface for entities with identity
1046
- - `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
1047
1179
  - `Command`, `CommandHandler<C, R>` - Command interface and handler type for CQRS
1048
1180
  - `Query`, `QueryHandler<Q, R>` - Query interface and handler type for CQRS
1049
1181
  - `CommandBus`, `ICommandBus` - Command bus for centralized command execution
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shirudo/ddd-kit",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Composable TypeScript toolkit for tactical DDD",
5
5
  "type": "module",
6
6
  "repository": {