@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 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
- Entities are objects with unique identity that are defined by their ID rather than their attributes. The optional `Entity<TId>` interface can be used for nested entities within aggregates or entities that are not aggregate roots. Helper functions like `sameEntity()`, `findEntityById()`, and `hasEntityId()` provide utilities for working with entity collections.
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. The library provides:
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
- - **`AggregateRoot<TId>`** - Marker interface for Aggregate Roots. Aggregate Roots are the entry points for modifying aggregates in DDD. They have identity (id) and version for optimistic concurrency control. All aggregate base classes implement this interface.
93
+ The library provides:
72
94
 
73
- - **`AggregateBase<TState, TId>`** - Base class for aggregates without Event Sourcing. Implements `AggregateRoot<TId>`. 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 optimistic concurrency control.
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
- - **`AggregateEventSourced<TState, TEvent, TId>`** - Base class for Event-Sourced aggregates. 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.
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
- Both classes support automatic versioning (configurable), snapshot creation/restoration, and optimistic concurrency control.
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 Nested Entities
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
- // Define nested entity
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
- type ItemAdded = DomainEvent<"ItemAdded", { item: OrderItem }>;
748
- type ItemRemoved = DomainEvent<"ItemRemoved", { itemId: ItemId }>;
749
- type OrderEvent = ItemAdded | ItemRemoved;
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
- const order = new Order(id, initialState);
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
- addItem(item: OrderItem): void {
768
- if (hasEntityId(this.state.items, item.id)) {
769
- throw new Error("Item already exists");
770
- }
771
- const result = this.apply(createDomainEvent("ItemAdded", { item }) as ItemAdded);
772
- if (!result.ok) {
773
- throw new Error(result.error);
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
- if (!hasEntityId(this.state.items, itemId)) {
825
+ const item = findEntityById(this._state.items, itemId);
826
+ if (!item) {
779
827
  throw new Error("Item not found");
780
828
  }
781
- const result = this.apply(createDomainEvent("ItemRemoved", { itemId }) as ItemRemoved);
782
- if (!result.ok) {
783
- throw new Error(result.error);
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.state.items, itemId);
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 orderId = "order-123" as OrderId;
811
- const order = Order.create(orderId, "customer-456");
812
-
813
- const item: OrderItem = {
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.addItem(item);
821
- const foundItem = order.getItem(item.id);
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 { ok, err, isOk, isErr, type Result, guard } from "@shirudo/ddd-kit";
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
- const validation = guard(id.length > 0, "User ID cannot be empty");
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 createUser(id: string): Result<{ id: UserId; name: string }, string> {
841
- const userIdResult = validateUserId(id);
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
- // Usage with type guards (recommended)
853
- const result = createUser("user-123");
854
- if (isOk(result)) {
855
- console.log("User created:", result.value); // TypeScript knows result is Ok
856
- } else {
857
- console.error("Error:", result.error); // TypeScript knows result is Err
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
- // Usage with ok property (also works)
861
- const result2 = createUser("user-123");
862
- if (result2.ok) {
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 Roots
876
- - `AggregateBase<TState, TId>` - Base class for aggregates without Event Sourcing (implements `AggregateRoot<TId>`)
877
- - `AggregateEventSourced<TState, TEvent, TId>` - Base class for Event-Sourced aggregates (extends `AggregateBase`, implements `AggregateRoot<TId>`)
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 helpers
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