@shirudo/ddd-kit 0.11.0 → 0.13.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 +156 -24
- 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,
|
|
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 `
|
|
76
|
-
- Implements `
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
803
|
-
|
|
813
|
+
AggregateRoot,
|
|
814
|
+
Identifiable,
|
|
804
815
|
findEntityById,
|
|
805
|
-
hasEntityId,
|
|
806
|
-
removeEntityById,
|
|
807
816
|
updateEntityById,
|
|
808
|
-
|
|
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
|
-
//
|
|
817
|
-
type OrderItem =
|
|
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[];
|
|
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
|
|
833
|
-
implements
|
|
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
|
-
- `
|
|
1040
|
-
- `
|
|
1041
|
-
- `AggregateEventSourced<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Root Entities (extends `
|
|
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>` -
|
|
1046
|
-
- `
|
|
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
|