@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 +188 -31
- package/dist/index.d.ts +539 -423
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|

|
|
@@ -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,
|
|
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 `
|
|
72
|
-
- Implements `
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
778
|
-
|
|
813
|
+
AggregateRoot,
|
|
814
|
+
Identifiable,
|
|
779
815
|
findEntityById,
|
|
780
|
-
hasEntityId,
|
|
781
|
-
removeEntityById,
|
|
782
816
|
updateEntityById,
|
|
783
|
-
|
|
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
|
-
//
|
|
792
|
-
type OrderItem =
|
|
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[];
|
|
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
|
|
808
|
-
implements
|
|
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
|
-
- `
|
|
1015
|
-
- `
|
|
1016
|
-
- `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>`)
|
|
1017
1172
|
- `AggregateConfig`, `AggregateEventSourcedConfig` - Configuration interfaces
|
|
1018
1173
|
- `AggregateSnapshot<TState>` - Snapshot interface for performance optimization
|
|
1019
1174
|
- `sameAggregate()` - Aggregate equality helper
|
|
1020
|
-
- `Entity<TId>` -
|
|
1021
|
-
- `
|
|
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
|