@shirudo/ddd-kit 0.9.1 → 0.9.3
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 +157 -94
- package/dist/index.d.ts +441 -31
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,19 +62,43 @@ Value Objects are immutable objects that are defined by their attributes rather
|
|
|
62
62
|
|
|
63
63
|
### Entities
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
In Domain-Driven Design, there are two types of entities:
|
|
66
|
+
|
|
67
|
+
1. **Aggregate Root Entity**: The parent Entity of an aggregate.
|
|
68
|
+
- Has identity (id) and version for optimistic concurrency control
|
|
69
|
+
- Represents the aggregate externally
|
|
70
|
+
- Loaded/saved through repositories
|
|
71
|
+
- Created by extending `AggregateBase` or `AggregateEventSourced`
|
|
72
|
+
- Implements `AggregateRoot<TId>`
|
|
73
|
+
|
|
74
|
+
2. **Child Entities**: Entities within an aggregate.
|
|
75
|
+
- Have identity (id), but no own version
|
|
76
|
+
- Exist only within the aggregate boundary
|
|
77
|
+
- Versioned through the Aggregate Root
|
|
78
|
+
- Cannot be referenced directly from outside the aggregate
|
|
79
|
+
- Use the `Entity<TId>` interface for type safety
|
|
80
|
+
|
|
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.
|
|
66
82
|
|
|
67
83
|
### Aggregates
|
|
68
84
|
|
|
69
|
-
Aggregates are clusters of entities and value objects that form a consistency boundary.
|
|
85
|
+
Aggregates are clusters of entities and value objects that form a consistency boundary. An aggregate consists of:
|
|
86
|
+
|
|
87
|
+
- **One Aggregate Root** (Entity with id + version)
|
|
88
|
+
- **Optional child entities** (Entities with id, but no own version)
|
|
89
|
+
- **Optional value objects** (immutable objects)
|
|
90
|
+
|
|
91
|
+
The Aggregate Root is an Entity (the parent Entity of the aggregate) that represents the aggregate externally. All changes to child entities are versioned through the Aggregate Root. The version applies to the entire aggregate, including all child entities.
|
|
70
92
|
|
|
71
|
-
|
|
93
|
+
The library provides:
|
|
72
94
|
|
|
73
|
-
- **`
|
|
95
|
+
- **`AggregateRoot<TId>`** - Marker interface for Aggregate Root Entities. The Aggregate Root is an Entity with identity (id) and version for optimistic concurrency control. It represents the aggregate externally and is the only object that can be loaded/saved through repositories.
|
|
74
96
|
|
|
75
|
-
- **`
|
|
97
|
+
- **`AggregateBase<TState, TId>`** - Base class for creating Aggregate Root Entities without Event Sourcing. Implements `AggregateRoot<TId>`. The aggregate state (`TState`) contains child entities and value objects. Provides ID and version management, state management, and snapshot support. Use this when you don't need Event Sourcing but still want aggregate patterns with versioning and state management.
|
|
76
98
|
|
|
77
|
-
|
|
99
|
+
- **`AggregateEventSourced<TState, TEvent, TId>`** - Base class for Event-Sourced Aggregate Root Entities. Extends `AggregateBase` (and thus implements `AggregateRoot<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.
|
|
100
|
+
|
|
101
|
+
Both classes support automatic versioning (configurable), snapshot creation/restoration, and optimistic concurrency control. The version applies to the entire aggregate, including all child entities.
|
|
78
102
|
|
|
79
103
|
### CQRS (Command Query Responsibility Segregation)
|
|
80
104
|
|
|
@@ -712,169 +736,206 @@ const eventV2 = createDomainEvent(
|
|
|
712
736
|
);
|
|
713
737
|
```
|
|
714
738
|
|
|
715
|
-
### Working with
|
|
739
|
+
### Working with Child Entities
|
|
740
|
+
|
|
741
|
+
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.
|
|
716
742
|
|
|
717
743
|
```typescript
|
|
718
744
|
import {
|
|
719
745
|
AggregateBase,
|
|
720
|
-
createDomainEvent,
|
|
721
746
|
Entity,
|
|
722
747
|
findEntityById,
|
|
723
748
|
hasEntityId,
|
|
724
749
|
removeEntityById,
|
|
750
|
+
updateEntityById,
|
|
725
751
|
sameEntity,
|
|
752
|
+
type AggregateRoot,
|
|
726
753
|
type Id,
|
|
727
|
-
type DomainEvent,
|
|
728
754
|
} from "@shirudo/ddd-kit";
|
|
729
755
|
|
|
730
756
|
type OrderId = Id<"OrderId">;
|
|
731
757
|
type ItemId = Id<"ItemId">;
|
|
732
758
|
|
|
733
|
-
//
|
|
759
|
+
// Child Entity within the aggregate (has id, but no own version)
|
|
734
760
|
type OrderItem = Entity<ItemId> & {
|
|
735
761
|
productId: string;
|
|
736
762
|
quantity: number;
|
|
737
763
|
price: number;
|
|
738
764
|
};
|
|
739
765
|
|
|
766
|
+
// Aggregate state contains child entities
|
|
740
767
|
type OrderState = {
|
|
741
768
|
id: OrderId;
|
|
742
769
|
customerId: string;
|
|
743
|
-
items: OrderItem[];
|
|
770
|
+
items: OrderItem[]; // Child entities
|
|
744
771
|
total: number;
|
|
745
772
|
};
|
|
746
773
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
class Order extends AggregateBase<OrderState, OrderEvent, OrderId> {
|
|
774
|
+
// Order is the Aggregate Root (an Entity with id + version)
|
|
775
|
+
class Order extends AggregateBase<OrderState, OrderId>
|
|
776
|
+
implements AggregateRoot<OrderId> {
|
|
752
777
|
static create(id: OrderId, customerId: string): Order {
|
|
753
778
|
const initialState: OrderState = {
|
|
754
779
|
id,
|
|
755
780
|
customerId,
|
|
756
|
-
items: [],
|
|
781
|
+
items: [], // Child entities
|
|
757
782
|
total: 0,
|
|
758
783
|
};
|
|
759
|
-
|
|
760
|
-
const result = order.apply(createDomainEvent("OrderCreated", { customerId }) as any);
|
|
761
|
-
if (!result.ok) {
|
|
762
|
-
throw new Error(result.error);
|
|
763
|
-
}
|
|
764
|
-
return order;
|
|
784
|
+
return new Order(id, initialState);
|
|
765
785
|
}
|
|
766
786
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
787
|
+
// Operations on child entities are versioned through the Aggregate Root
|
|
788
|
+
addItem(productId: string, quantity: number, price: number): ItemId {
|
|
789
|
+
const itemId = `item-${Date.now()}` as ItemId;
|
|
790
|
+
const item: OrderItem = {
|
|
791
|
+
id: itemId,
|
|
792
|
+
productId,
|
|
793
|
+
quantity,
|
|
794
|
+
price,
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
this._state = {
|
|
798
|
+
...this._state,
|
|
799
|
+
items: [...this._state.items, item],
|
|
800
|
+
total: this._state.total + price * quantity,
|
|
801
|
+
};
|
|
802
|
+
this.bumpVersion(); // Versions the entire aggregate (including child entities)
|
|
803
|
+
return itemId;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
updateItemQuantity(itemId: ItemId, newQuantity: number): void {
|
|
807
|
+
const item = findEntityById(this._state.items, itemId);
|
|
808
|
+
if (!item) {
|
|
809
|
+
throw new Error("Item not found");
|
|
774
810
|
}
|
|
811
|
+
|
|
812
|
+
this._state = {
|
|
813
|
+
...this._state,
|
|
814
|
+
items: updateEntityById(
|
|
815
|
+
this._state.items,
|
|
816
|
+
itemId,
|
|
817
|
+
(i) => ({ ...i, quantity: newQuantity })
|
|
818
|
+
),
|
|
819
|
+
total: this._state.total - item.price * item.quantity + item.price * newQuantity,
|
|
820
|
+
};
|
|
821
|
+
this.bumpVersion(); // Versions the entire aggregate
|
|
775
822
|
}
|
|
776
823
|
|
|
777
824
|
removeItem(itemId: ItemId): void {
|
|
778
|
-
|
|
825
|
+
const item = findEntityById(this._state.items, itemId);
|
|
826
|
+
if (!item) {
|
|
779
827
|
throw new Error("Item not found");
|
|
780
828
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
829
|
+
|
|
830
|
+
this._state = {
|
|
831
|
+
...this._state,
|
|
832
|
+
items: removeEntityById(this._state.items, itemId),
|
|
833
|
+
total: this._state.total - item.price * item.quantity,
|
|
834
|
+
};
|
|
835
|
+
this.bumpVersion(); // Versions the entire aggregate
|
|
785
836
|
}
|
|
786
837
|
|
|
787
838
|
getItem(itemId: ItemId): OrderItem | undefined {
|
|
788
|
-
return findEntityById(this.
|
|
839
|
+
return findEntityById(this._state.items, itemId);
|
|
789
840
|
}
|
|
790
|
-
|
|
791
|
-
protected readonly handlers = {
|
|
792
|
-
ItemAdded: (state: OrderState, event: ItemAdded): OrderState => ({
|
|
793
|
-
...state,
|
|
794
|
-
items: [...state.items, event.payload.item],
|
|
795
|
-
total: state.total + event.payload.item.price * event.payload.item.quantity,
|
|
796
|
-
}),
|
|
797
|
-
ItemRemoved: (state: OrderState, event: ItemRemoved): OrderState => {
|
|
798
|
-
const item = findEntityById(state.items, event.payload.itemId);
|
|
799
|
-
if (!item) return state;
|
|
800
|
-
return {
|
|
801
|
-
...state,
|
|
802
|
-
items: removeEntityById(state.items, event.payload.itemId),
|
|
803
|
-
total: state.total - item.price * item.quantity,
|
|
804
|
-
};
|
|
805
|
-
},
|
|
806
|
-
};
|
|
807
841
|
}
|
|
808
842
|
|
|
809
843
|
// Usage
|
|
810
|
-
const
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
id: "item-1" as ItemId,
|
|
815
|
-
productId: "prod-123",
|
|
816
|
-
quantity: 2,
|
|
817
|
-
price: 10.99,
|
|
818
|
-
};
|
|
844
|
+
const order = Order.create("order-123" as OrderId, "customer-456");
|
|
845
|
+
const itemId = order.addItem("product-1", 2, 10.0); // Adds child entity
|
|
846
|
+
order.updateItemQuantity(itemId, 3); // Updates child entity
|
|
847
|
+
order.removeItem(itemId); // Removes child entity
|
|
819
848
|
|
|
820
|
-
order.
|
|
821
|
-
|
|
822
|
-
console.log(sameEntity(item, foundItem!)); // true
|
|
849
|
+
// All changes version the Aggregate Root (order.version increments)
|
|
850
|
+
console.log(order.version); // 3 (one for each operation)
|
|
823
851
|
```
|
|
824
852
|
|
|
825
853
|
### Using Result Type for Error Handling
|
|
826
854
|
|
|
855
|
+
The `Result<T, E>` type provides composition utilities to avoid repetitive `if (isErr)` checks:
|
|
856
|
+
|
|
827
857
|
```typescript
|
|
828
|
-
import {
|
|
858
|
+
import {
|
|
859
|
+
ok,
|
|
860
|
+
err,
|
|
861
|
+
isOk,
|
|
862
|
+
isErr,
|
|
863
|
+
andThen,
|
|
864
|
+
map,
|
|
865
|
+
mapErr,
|
|
866
|
+
unwrapOr,
|
|
867
|
+
unwrapOrElse,
|
|
868
|
+
match,
|
|
869
|
+
type Result,
|
|
870
|
+
guard
|
|
871
|
+
} from "@shirudo/ddd-kit";
|
|
829
872
|
|
|
830
873
|
type UserId = string;
|
|
831
874
|
|
|
832
875
|
function validateUserId(id: string): Result<UserId, string> {
|
|
833
|
-
|
|
834
|
-
if (isErr(validation)) {
|
|
835
|
-
return err(validation.error);
|
|
836
|
-
}
|
|
837
|
-
return ok(id as UserId);
|
|
876
|
+
return id.length > 0 ? ok(id as UserId) : err("User ID cannot be empty");
|
|
838
877
|
}
|
|
839
878
|
|
|
840
|
-
function
|
|
841
|
-
|
|
842
|
-
if (isErr(userIdResult)) {
|
|
843
|
-
return err(userIdResult.error);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
return ok({
|
|
847
|
-
id: userIdResult.value,
|
|
848
|
-
name: "John Doe",
|
|
849
|
-
});
|
|
879
|
+
function validateEmail(email: string): Result<string, string> {
|
|
880
|
+
return email.includes("@") ? ok(email) : err("Invalid email");
|
|
850
881
|
}
|
|
851
882
|
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
883
|
+
// Chaining operations with andThen (avoids if-checks)
|
|
884
|
+
function createUser(id: string, email: string): Result<{ id: UserId; email: string }, string> {
|
|
885
|
+
return andThen(validateUserId(id), (userId) =>
|
|
886
|
+
map(validateEmail(email), (email) => ({
|
|
887
|
+
id: userId,
|
|
888
|
+
email,
|
|
889
|
+
}))
|
|
890
|
+
);
|
|
858
891
|
}
|
|
859
892
|
|
|
860
|
-
//
|
|
861
|
-
const
|
|
862
|
-
|
|
893
|
+
// Using map for transformations
|
|
894
|
+
const result = ok(5);
|
|
895
|
+
const doubled = map(result, x => x * 2); // Ok<10>
|
|
896
|
+
|
|
897
|
+
// Using mapErr to transform errors
|
|
898
|
+
const errorResult = err("not found");
|
|
899
|
+
const mappedError = mapErr(errorResult, e => `Error: ${e}`); // Err<"Error: not found">
|
|
900
|
+
|
|
901
|
+
// Using unwrapOr for defaults
|
|
902
|
+
const userId = unwrapOr(validateUserId(""), "default-id");
|
|
903
|
+
|
|
904
|
+
// Using unwrapOrElse for computed defaults
|
|
905
|
+
const userId2 = unwrapOrElse(validateUserId(""), err => `fallback-${Date.now()}`);
|
|
906
|
+
|
|
907
|
+
// Using match for pattern matching
|
|
908
|
+
const message = match(createUser("user-123", "test@example.com"),
|
|
909
|
+
user => `User created: ${user.id}`,
|
|
910
|
+
error => `Error: ${error}`
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
// Usage with type guards (still works)
|
|
914
|
+
const result2 = createUser("user-123", "test@example.com");
|
|
915
|
+
if (isOk(result2)) {
|
|
863
916
|
console.log("User created:", result2.value);
|
|
864
917
|
} else {
|
|
865
918
|
console.error("Error:", result2.error);
|
|
866
919
|
}
|
|
867
920
|
```
|
|
868
921
|
|
|
922
|
+
**Available Composition Utilities:**
|
|
923
|
+
- `andThen<T, E, U>(result, fn)` - Chains Result operations (flatMap/bind). If Ok, applies function; if Err, returns error unchanged.
|
|
924
|
+
- `map<T, E, U>(result, fn)` - Transforms Ok value. If Err, returns error unchanged.
|
|
925
|
+
- `mapErr<T, E, F>(result, fn)` - Transforms Err value. If Ok, returns value unchanged.
|
|
926
|
+
- `unwrapOr<T, E>(result, defaultValue)` - Returns value if Ok, otherwise returns default.
|
|
927
|
+
- `unwrapOrElse<T, E>(result, fn)` - Returns value if Ok, otherwise computes default from error.
|
|
928
|
+
- `match<T, E, R>(result, onOk, onErr)` - Pattern matching. Applies one function if Ok, another if Err.
|
|
929
|
+
|
|
869
930
|
## API Documentation
|
|
870
931
|
|
|
871
932
|
This package is written in TypeScript and provides full type definitions. All types and functions are exported from the main entry point. You can explore the available APIs through your IDE's autocomplete or by examining the type definitions in `node_modules/@shirudo/ddd-kit/dist/index.d.ts`.
|
|
872
933
|
|
|
873
934
|
Key exports include:
|
|
874
935
|
- `vo()`, `voEquals()`, `voWithValidation()`, `voWithValidationUnsafe()` - Value Object utilities
|
|
875
|
-
- `AggregateRoot<TId>` - Marker interface for Aggregate
|
|
876
|
-
- `AggregateBase<TState, TId>` - Base class for
|
|
877
|
-
- `AggregateEventSourced<TState, TEvent, TId>` - Base class for Event-Sourced
|
|
936
|
+
- `AggregateRoot<TId>` - Marker interface for Aggregate Root Entities
|
|
937
|
+
- `AggregateBase<TState, TId>` - Base class for creating Aggregate Root Entities without Event Sourcing (implements `AggregateRoot<TId>`)
|
|
938
|
+
- `AggregateEventSourced<TState, TEvent, TId>` - Base class for Event-Sourced Aggregate Root Entities (extends `AggregateBase`, implements `AggregateRoot<TId>`)
|
|
878
939
|
- `AggregateConfig`, `AggregateEventSourcedConfig` - Configuration interfaces
|
|
879
940
|
- `AggregateSnapshot<TState>` - Snapshot interface for performance optimization
|
|
880
941
|
- `sameAggregate()` - Aggregate equality helper
|
|
@@ -892,7 +953,9 @@ Key exports include:
|
|
|
892
953
|
- `EventHandler<Evt>` - Event handler function type
|
|
893
954
|
- `EventBus.subscribe()` - Subscribe handlers to event types
|
|
894
955
|
- `EventBus.publish()` - Publish events to all subscribers
|
|
895
|
-
- `Result<T, E>`, `ok()`, `err()`, `isOk()`, `isErr()` - Result type and
|
|
956
|
+
- `Result<T, E>`, `ok()`, `err()`, `isOk()`, `isErr()` - Result type and type guards
|
|
957
|
+
- `andThen()`, `map()`, `mapErr()` - Result composition utilities
|
|
958
|
+
- `unwrapOr()`, `unwrapOrElse()`, `match()` - Result unwrapping and pattern matching
|
|
896
959
|
- `Id<Tag>` - Branded ID type
|
|
897
960
|
- `IRepository<TState, TEvent, TAgg, TId>` - Repository interface
|
|
898
961
|
- `ISpecification<T>` - Specification interface
|