@rineex/ddd 1.6.0 → 2.0.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 CHANGED
@@ -87,6 +87,7 @@ import {
87
87
  DomainEvent,
88
88
  ApplicationServicePort,
89
89
  AggregateId,
90
+ type EntityProps,
90
91
  } from '@rineex/ddd';
91
92
 
92
93
  // Define a Value Object
@@ -108,7 +109,7 @@ interface UserProps {
108
109
  isActive: boolean;
109
110
  }
110
111
 
111
- class User extends AggregateRoot<UserProps> {
112
+ class User extends AggregateRoot<AggregateId, UserProps> {
112
113
  get email(): Email {
113
114
  return this.props.email;
114
115
  }
@@ -122,10 +123,24 @@ class User extends AggregateRoot<UserProps> {
122
123
  throw new Error('Email is required');
123
124
  }
124
125
  }
126
+
127
+ public toObject(): Record<string, unknown> {
128
+ return {
129
+ id: this.id.toString(),
130
+ createdAt: this.createdAt.toISOString(),
131
+ email: this.email.value,
132
+ isActive: this.isActive,
133
+ };
134
+ }
135
+ }
136
+
137
+ // Define a Domain Event
138
+ class UserCreatedEvent extends DomainEvent<AggregateId> {
139
+ public readonly eventName = 'UserCreated';
125
140
  }
126
141
 
127
142
  // Create and use
128
- const userId = AggregateId.create();
143
+ const userId = AggregateId.generate();
129
144
  const user = new User({
130
145
  id: userId,
131
146
  createdAt: new Date(),
@@ -134,8 +149,7 @@ const user = new User({
134
149
 
135
150
  user.addEvent(
136
151
  new UserCreatedEvent({
137
- id: 'evt-1',
138
- aggregateId: userId.uuid,
152
+ aggregateId: userId,
139
153
  schemaVersion: 1,
140
154
  occurredAt: Date.now(),
141
155
  payload: { email: user.email.value },
@@ -219,13 +233,38 @@ const address = Address.create({
219
233
  // address.props.street = 'foo'; // Error: Cannot assign to read only property
220
234
  ```
221
235
 
236
+ #### Primitive Value Objects
237
+
238
+ For value objects that wrap a single primitive (string, number, or boolean), use
239
+ `PrimitiveValueObject` for better performance:
240
+
241
+ ```typescript
242
+ import { PrimitiveValueObject } from '@rineex/ddd';
243
+
244
+ class Email extends PrimitiveValueObject<string> {
245
+ public static create(value: string): Email {
246
+ return new Email(value);
247
+ }
248
+
249
+ protected validate(value: string): void {
250
+ if (!value.includes('@')) {
251
+ throw new Error('Invalid email');
252
+ }
253
+ }
254
+ }
255
+
256
+ // Usage
257
+ const email = Email.create('user@example.com');
258
+ console.log(email.getValue()); // 'user@example.com'
259
+ ```
260
+
222
261
  #### Type Safety with `unwrapValueObject`
223
262
 
224
263
  When working with collections of value objects, use the `unwrapValueObject`
225
264
  utility:
226
265
 
227
266
  ```typescript
228
- import { unwrapValueObject, UnwrapValueObject } from '@rineex/ddd';
267
+ import { unwrapValueObject, type UnwrapValueObject } from '@rineex/ddd';
229
268
 
230
269
  interface UserProps {
231
270
  tags: Tag[]; // where Tag extends ValueObject<string>
@@ -251,8 +290,7 @@ value objects, they can be mutable and have a lifecycle.
251
290
  #### Implementation
252
291
 
253
292
  ```typescript
254
- import { Entity, AggregateId } from '@rineex/ddd';
255
- import type { CreateEntityProps } from '@rineex/ddd';
293
+ import { Entity, AggregateId, type EntityProps } from '@rineex/ddd';
256
294
 
257
295
  interface OrderItemProps {
258
296
  productId: string;
@@ -260,7 +298,7 @@ interface OrderItemProps {
260
298
  unitPrice: number;
261
299
  }
262
300
 
263
- class OrderItem extends Entity<OrderItemProps> {
301
+ class OrderItem extends Entity<AggregateId, OrderItemProps> {
264
302
  get productId(): string {
265
303
  return this.props.productId;
266
304
  }
@@ -285,11 +323,22 @@ class OrderItem extends Entity<OrderItemProps> {
285
323
  throw new Error('Unit price cannot be negative');
286
324
  }
287
325
  }
326
+
327
+ public toObject(): Record<string, unknown> {
328
+ return {
329
+ id: this.id.toString(),
330
+ createdAt: this.createdAt.toISOString(),
331
+ productId: this.productId,
332
+ quantity: this.quantity,
333
+ unitPrice: this.unitPrice,
334
+ total: this.total,
335
+ };
336
+ }
288
337
  }
289
338
 
290
339
  // Creating an entity
291
340
  const item = new OrderItem({
292
- id: AggregateId.create(),
341
+ id: AggregateId.generate(),
293
342
  createdAt: new Date(),
294
343
  props: {
295
344
  productId: 'prod-123',
@@ -326,19 +375,23 @@ import {
326
375
  } from '@rineex/ddd';
327
376
 
328
377
  // Define domain events
329
- class UserCreatedEvent extends DomainEvent {
378
+ interface UserCreatedPayload extends DomainEventPayload {
379
+ email: string;
380
+ }
381
+
382
+ class UserCreatedEvent extends DomainEvent<AggregateId, UserCreatedPayload> {
330
383
  public readonly eventName = 'UserCreated';
384
+ }
331
385
 
332
- constructor(
333
- props: Parameters<DomainEvent['constructor']>[0] & {
334
- payload: { email: string };
335
- },
336
- ) {
337
- super(props);
338
- }
386
+ interface UserEmailChangedPayload extends DomainEventPayload {
387
+ oldEmail: string;
388
+ newEmail: string;
339
389
  }
340
390
 
341
- class UserEmailChangedEvent extends DomainEvent {
391
+ class UserEmailChangedEvent extends DomainEvent<
392
+ AggregateId,
393
+ UserEmailChangedPayload
394
+ > {
342
395
  public readonly eventName = 'UserEmailChanged';
343
396
  }
344
397
 
@@ -348,7 +401,7 @@ interface UserProps {
348
401
  isActive: boolean;
349
402
  }
350
403
 
351
- class User extends AggregateRoot<UserProps> {
404
+ class User extends AggregateRoot<AggregateId, UserProps> {
352
405
  get email(): string {
353
406
  return this.props.email;
354
407
  }
@@ -358,16 +411,16 @@ class User extends AggregateRoot<UserProps> {
358
411
  }
359
412
 
360
413
  public static create(email: string, id?: AggregateId): User {
414
+ const userId = id || AggregateId.generate();
361
415
  const user = new User({
362
- id: id || AggregateId.create(),
416
+ id: userId,
363
417
  createdAt: new Date(),
364
418
  props: { email, isActive: true },
365
419
  });
366
420
 
367
421
  user.addEvent(
368
422
  new UserCreatedEvent({
369
- id: crypto.randomUUID(),
370
- aggregateId: user.id.uuid,
423
+ aggregateId: userId,
371
424
  schemaVersion: 1,
372
425
  occurredAt: Date.now(),
373
426
  payload: { email },
@@ -383,15 +436,15 @@ class User extends AggregateRoot<UserProps> {
383
436
  throw new Error('Invalid email format');
384
437
  }
385
438
 
439
+ const oldEmail = this.props.email;
386
440
  this.props = { ...this.props, email: newEmail };
387
441
 
388
442
  this.addEvent(
389
443
  new UserEmailChangedEvent({
390
- id: crypto.randomUUID(),
391
- aggregateId: this.id.uuid,
444
+ aggregateId: this.id,
392
445
  schemaVersion: 1,
393
446
  occurredAt: Date.now(),
394
- payload: { oldEmail: this.props.email, newEmail },
447
+ payload: { oldEmail, newEmail },
395
448
  }),
396
449
  );
397
450
  }
@@ -401,6 +454,15 @@ class User extends AggregateRoot<UserProps> {
401
454
  throw new Error('User must have a valid email');
402
455
  }
403
456
  }
457
+
458
+ public toObject(): Record<string, unknown> {
459
+ return {
460
+ id: this.id.toString(),
461
+ createdAt: this.createdAt.toISOString(),
462
+ email: this.email,
463
+ isActive: this.isActive,
464
+ };
465
+ }
404
466
  }
405
467
 
406
468
  // Usage
@@ -435,7 +497,7 @@ immutable records of past events and enable event-driven architectures.
435
497
  #### Implementation
436
498
 
437
499
  ```typescript
438
- import { DomainEvent, type DomainEventPayload } from '@rineex/ddd';
500
+ import { DomainEvent, type DomainEventPayload, AggregateId } from '@rineex/ddd';
439
501
 
440
502
  // Define event payloads (only primitives allowed)
441
503
  interface OrderPlacedPayload extends DomainEventPayload {
@@ -446,30 +508,19 @@ interface OrderPlacedPayload extends DomainEventPayload {
446
508
  }
447
509
 
448
510
  // Create event class
449
- class OrderPlacedEvent extends DomainEvent<OrderPlacedPayload> {
511
+ class OrderPlacedEvent extends DomainEvent<AggregateId, OrderPlacedPayload> {
450
512
  public readonly eventName = 'OrderPlaced';
451
-
452
- constructor(
453
- props: Omit<
454
- Parameters<DomainEvent<OrderPlacedPayload>['constructor']>[0],
455
- 'payload'
456
- > & {
457
- payload: OrderPlacedPayload;
458
- },
459
- ) {
460
- super(props);
461
- }
462
513
  }
463
514
 
464
515
  // Using events
516
+ const orderId = AggregateId.generate();
465
517
  const event = new OrderPlacedEvent({
466
- id: crypto.randomUUID(),
467
- aggregateId: 'order-123',
518
+ aggregateId: orderId,
468
519
  schemaVersion: 1,
469
520
  occurredAt: Date.now(),
470
521
  payload: {
471
522
  customerId: 'cust-456',
472
- orderId: 'order-123',
523
+ orderId: orderId.toString(),
473
524
  totalAmount: 99.99,
474
525
  itemCount: 3,
475
526
  },
@@ -479,7 +530,7 @@ const event = new OrderPlacedEvent({
479
530
  const primitives = event.toPrimitives();
480
531
  // {
481
532
  // id: '...',
482
- // aggregateId: 'order-123',
533
+ // aggregateId: '...',
483
534
  // schemaVersion: 1,
484
535
  // occurredAt: 1234567890,
485
536
  // eventName: 'OrderPlaced',
@@ -590,17 +641,17 @@ export abstract class ValueObject<T> {
590
641
 
591
642
  ### Entities
592
643
 
593
- #### `Entity<EntityProps>`
644
+ #### `Entity<ID extends EntityId, Props>`
594
645
 
595
646
  Abstract base class for domain entities.
596
647
 
597
648
  ```typescript
598
- export abstract class Entity<EntityProps> {
599
- readonly id: AggregateId;
649
+ export abstract class Entity<ID extends EntityId, Props> {
650
+ readonly id: ID;
600
651
  readonly createdAt: Date;
601
- readonly metadata: { createdAt: string; id: string };
602
652
  abstract validate(): void;
603
- equals(entity: unknown): boolean;
653
+ abstract toObject(): Record<string, unknown>;
654
+ equals(other?: Entity<ID, Props>): boolean;
604
655
  }
605
656
  ```
606
657
 
@@ -608,22 +659,26 @@ export abstract class Entity<EntityProps> {
608
659
 
609
660
  ```typescript
610
661
  new Entity({
611
- id?: AggregateId; // Generated if not provided
612
- createdAt: Date; // Required
613
- props: EntityProps; // Domain-specific properties
662
+ id: ID; // Required - must be an EntityId type
663
+ createdAt?: Date; // Optional - defaults to new Date()
664
+ props: Props; // Domain-specific properties
614
665
  })
615
666
  ```
616
667
 
617
668
  ### Aggregate Roots
618
669
 
619
- #### `AggregateRoot<EntityProps>`
670
+ #### `AggregateRoot<ID extends EntityId, P>`
620
671
 
621
672
  Extends `Entity` with domain event support.
622
673
 
623
674
  ```typescript
624
- export abstract class AggregateRoot<EntityProps> extends Entity<EntityProps> {
675
+ export abstract class AggregateRoot<ID extends EntityId, P> extends Entity<
676
+ ID,
677
+ P
678
+ > {
625
679
  readonly domainEvents: readonly DomainEvent[];
626
680
  abstract validate(): void;
681
+ abstract toObject(): Record<string, unknown>;
627
682
  addEvent(event: DomainEvent): void;
628
683
  pullDomainEvents(): readonly DomainEvent[];
629
684
  }
@@ -637,15 +692,18 @@ export abstract class AggregateRoot<EntityProps> extends Entity<EntityProps> {
637
692
 
638
693
  ### Domain Events
639
694
 
640
- #### `DomainEvent<T extends DomainEventPayload>`
695
+ #### `DomainEvent<AggregateId extends EntityId, T extends DomainEventPayload>`
641
696
 
642
697
  Abstract base class for domain events.
643
698
 
644
699
  ```typescript
645
- export abstract class DomainEvent<T extends DomainEventPayload> {
700
+ export abstract class DomainEvent<
701
+ AggregateId extends EntityId = EntityId,
702
+ T extends DomainEventPayload = DomainEventPayload,
703
+ > {
646
704
  abstract readonly eventName: string;
647
705
  readonly id: string;
648
- readonly aggregateId: string;
706
+ readonly aggregateId: AggregateId;
649
707
  readonly schemaVersion: number;
650
708
  readonly occurredAt: number;
651
709
  readonly payload: Readonly<T>;
@@ -677,12 +735,28 @@ export interface ApplicationServicePort<I, O> {
677
735
 
678
736
  #### `AggregateId`
679
737
 
680
- Represents the unique identifier for an aggregate.
738
+ Represents the unique identifier for an aggregate. Extends `UUID` which extends
739
+ `PrimitiveValueObject<string>`.
681
740
 
682
741
  ```typescript
683
- class AggregateId extends ValueObject<{ uuid: string }> {
684
- get uuid(): string;
685
- public static create(id?: string): AggregateId;
742
+ class AggregateId extends UUID {
743
+ public static generate(): AggregateId;
744
+ public static fromString(value: string): AggregateId;
745
+ public getValue(): string;
746
+ public toString(): string;
747
+ }
748
+ ```
749
+
750
+ #### `UUID`
751
+
752
+ Base class for UUID-based value objects.
753
+
754
+ ```typescript
755
+ class UUID extends PrimitiveValueObject<string> {
756
+ public static generate<T extends typeof UUID>(this: T): InstanceType<T>;
757
+ public static fromString(value: string): UUID;
758
+ public getValue(): string;
759
+ public toString(): string;
686
760
  }
687
761
  ```
688
762
 
@@ -693,16 +767,19 @@ Validates IPv4 and IPv6 addresses.
693
767
  ```typescript
694
768
  class IPAddress extends ValueObject<string> {
695
769
  public static create(value: string): IPAddress;
770
+ public get value(): string;
696
771
  }
697
772
  ```
698
773
 
699
- #### `URL`
774
+ #### `Url`
700
775
 
701
776
  Validates web URLs.
702
777
 
703
778
  ```typescript
704
- class URL extends ValueObject<string> {
705
- public static create(value: string): URL;
779
+ class Url extends ValueObject<string> {
780
+ public static create(value: string): Url;
781
+ public get value(): string;
782
+ public get href(): string;
706
783
  }
707
784
  ```
708
785
 
@@ -713,6 +790,10 @@ Parses and validates user agent strings.
713
790
  ```typescript
714
791
  class UserAgent extends ValueObject<string> {
715
792
  public static create(value: string): UserAgent;
793
+ public get value(): string;
794
+ public get isMobile(): boolean;
795
+ public get isBot(): boolean;
796
+ public getProps(): string;
716
797
  }
717
798
  ```
718
799
 
@@ -746,10 +827,301 @@ export class InvalidValueObjectError extends DomainError {}
746
827
 
747
828
  #### `ApplicationError`
748
829
 
749
- Thrown for application-level errors.
830
+ Thrown for application-level errors with HTTP status codes.
831
+
832
+ ```typescript
833
+ export abstract class ApplicationError extends Error {
834
+ readonly code: HttpStatusMessage;
835
+ readonly status: HttpStatusCode;
836
+ readonly isOperational: boolean;
837
+ readonly metadata?: Record<string, unknown>;
838
+ readonly cause?: Error;
839
+ }
840
+ ```
841
+
842
+ ### Result Type
843
+
844
+ #### `Result<T, E>`
845
+
846
+ Represents the outcome of an operation that can either succeed or fail, without
847
+ relying on exceptions for control flow. This is a functional programming pattern
848
+ that makes error handling explicit in the type system and is commonly used in
849
+ Domain-Driven Design to represent domain operation outcomes.
850
+
851
+ **Key Features:**
852
+
853
+ - **Immutable**: Result instances are frozen to prevent accidental mutations
854
+ - **Type-Safe**: Full TypeScript support with generic types
855
+ - **Explicit Error Handling**: No hidden exceptions, all errors are explicit
856
+ - **DomainError Integration**: Works seamlessly with `DomainError` by default
857
+
858
+ ```typescript
859
+ export class Result<T, E = DomainError> {
860
+ readonly isSuccess: boolean;
861
+ readonly isFailure: boolean;
862
+
863
+ public static ok<T, E = never>(value: T): Result<T, E>;
864
+ public static fail<T = never, E = DomainError>(error: E): Result<T, E>;
865
+ public getValue(): T | undefined;
866
+ public getError(): E | undefined;
867
+ public isSuccessResult(): this is Result<T, never>;
868
+ public isFailureResult(): this is Result<never, E>;
869
+ }
870
+ ```
871
+
872
+ **Basic Example:**
873
+
874
+ ```typescript
875
+ import { Result, DomainError } from '@rineex/ddd';
876
+
877
+ class InvalidValueError extends DomainError {
878
+ public get code() {
879
+ return 'DOMAIN.INVALID_VALUE' as const;
880
+ }
881
+
882
+ constructor(message: string) {
883
+ super({ message });
884
+ }
885
+ }
886
+
887
+ function parseNumber(input: string): Result<number, DomainError> {
888
+ const value = Number(input);
889
+ if (Number.isNaN(value)) {
890
+ return Result.fail(new InvalidValueError('Invalid number'));
891
+ }
892
+ return Result.ok(value);
893
+ }
894
+
895
+ const result = parseNumber('42');
896
+ if (result.isSuccess) {
897
+ const value = result.getValue(); // number | undefined
898
+ console.log(value); // 42
899
+ } else {
900
+ const error = result.getError(); // DomainError | undefined
901
+ console.error(error?.message);
902
+ }
903
+ ```
904
+
905
+ **Validation Pattern:**
906
+
907
+ ```typescript
908
+ function validateAge(age: number): Result<number, DomainError> {
909
+ if (age < 0) {
910
+ return Result.fail(new InvalidValueError('Age cannot be negative'));
911
+ }
912
+ if (age > 150) {
913
+ return Result.fail(new InvalidValueError('Age seems unrealistic'));
914
+ }
915
+ return Result.ok(age);
916
+ }
917
+
918
+ const result = validateAge(25);
919
+ if (result.isSuccess) {
920
+ console.log('Valid age:', result.getValue());
921
+ }
922
+ ```
923
+
924
+ **Chaining Pattern:**
925
+
926
+ ```typescript
927
+ function validateEmail(email: string): Result<string, DomainError> {
928
+ if (!email.includes('@')) {
929
+ return Result.fail(new InvalidValueError('Invalid email format'));
930
+ }
931
+ return Result.ok(email);
932
+ }
933
+
934
+ function createAccount(email: string): Result<{ email: string }, DomainError> {
935
+ const emailResult = validateEmail(email);
936
+ if (emailResult.isFailureResult()) {
937
+ return emailResult; // Forward the error
938
+ }
939
+
940
+ const validatedEmail = emailResult.getValue()!;
941
+ return Result.ok({ email: validatedEmail });
942
+ }
943
+ ```
944
+
945
+ **Using Type Guards:**
946
+
947
+ The `isSuccessResult()` and `isFailureResult()` methods provide type-safe
948
+ narrowing:
949
+
950
+ ```typescript
951
+ function processResult<T>(result: Result<T, DomainError>): void {
952
+ if (result.isSuccessResult()) {
953
+ // TypeScript narrows result to Result<T, never>
954
+ const value = result.getValue();
955
+ if (value) {
956
+ // Process the value with full type safety
957
+ console.log('Success:', value);
958
+ }
959
+ } else if (result.isFailureResult()) {
960
+ // TypeScript narrows result to Result<never, DomainError>
961
+ const error = result.getError();
962
+ if (error) {
963
+ // Access error properties with full type safety
964
+ console.error('Error code:', error.code);
965
+ console.error('Error message:', error.message);
966
+ }
967
+ }
968
+ }
969
+ ```
970
+
971
+ **Working with Domain Errors:**
750
972
 
751
973
  ```typescript
752
- export class ApplicationError extends DomainError {}
974
+ class InvalidStateError extends DomainError {
975
+ public get code() {
976
+ return 'DOMAIN.INVALID_STATE' as const;
977
+ }
978
+
979
+ constructor(
980
+ message: string,
981
+ metadata?: Record<string, boolean | number | string>,
982
+ ) {
983
+ super({ message, metadata });
984
+ }
985
+ }
986
+
987
+ function processOrder(orderId: string): Result<Order, DomainError> {
988
+ const order = orderRepository.findById(orderId);
989
+ if (!order) {
990
+ return Result.fail(new InvalidValueError('Order not found', { orderId }));
991
+ }
992
+ if (order.status !== 'PENDING') {
993
+ return Result.fail(
994
+ new InvalidStateError('Order cannot be processed', {
995
+ orderId,
996
+ currentStatus: order.status,
997
+ }),
998
+ );
999
+ }
1000
+ return Result.ok(order);
1001
+ }
1002
+ ```
1003
+
1004
+ **Void Operations:**
1005
+
1006
+ ```typescript
1007
+ function deleteUser(id: number): Result<void, DomainError> {
1008
+ if (id <= 0) {
1009
+ return Result.fail(new InvalidValueError('Invalid user ID'));
1010
+ }
1011
+ // ... deletion logic ...
1012
+ return Result.ok(undefined);
1013
+ }
1014
+ ```
1015
+
1016
+ **Best Practices:**
1017
+
1018
+ 1. Always check `isSuccess` or `isFailure` before calling `getValue()` or
1019
+ `getError()`
1020
+ 2. Use `isSuccessResult()` and `isFailureResult()` type guards for better type
1021
+ narrowing when you need TypeScript to narrow the result type
1022
+ 3. Use `DomainError` for domain-specific errors to maintain consistency
1023
+ 4. Forward errors in chaining operations rather than creating new ones
1024
+ 5. Leverage TypeScript's type narrowing for safe value extraction
1025
+
1026
+ ### Domain Violations
1027
+
1028
+ #### `DomainViolation`
1029
+
1030
+ Base class for domain violations. Purposely does NOT extend native Error to
1031
+ avoid stack trace overhead in the domain.
1032
+
1033
+ ```typescript
1034
+ export abstract class DomainViolation {
1035
+ abstract readonly code: string;
1036
+ abstract readonly message: string;
1037
+ readonly metadata: Readonly<Record<string, unknown>>;
1038
+ }
1039
+ ```
1040
+
1041
+ ### HTTP Status Codes
1042
+
1043
+ #### `HttpStatus` and `HttpStatusMessage`
1044
+
1045
+ Typed HTTP status code constants and messages.
1046
+
1047
+ ```typescript
1048
+ export const HttpStatus = {
1049
+ OK: 200,
1050
+ CREATED: 201,
1051
+ BAD_REQUEST: 400,
1052
+ UNAUTHORIZED: 401,
1053
+ FORBIDDEN: 403,
1054
+ NOT_FOUND: 404,
1055
+ INTERNAL_SERVER_ERROR: 500,
1056
+ // ... and many more
1057
+ } as const;
1058
+
1059
+ export const HttpStatusMessage = {
1060
+ 200: 'OK',
1061
+ 201: 'Created',
1062
+ 400: 'Bad Request',
1063
+ // ... and many more
1064
+ } as const;
1065
+
1066
+ export type HttpStatusCode = keyof typeof HttpStatusMessage;
1067
+ export type HttpStatusMessage =
1068
+ (typeof HttpStatusMessage)[keyof typeof HttpStatusMessage];
1069
+ ```
1070
+
1071
+ ### Utilities
1072
+
1073
+ #### `unwrapValueObject<T>`
1074
+
1075
+ Recursively unwraps value objects from nested structures.
1076
+
1077
+ ```typescript
1078
+ export function unwrapValueObject<T>(
1079
+ input: T,
1080
+ seen?: WeakSet<object>
1081
+ ): UnwrapValueObject<T>;
1082
+
1083
+ export type UnwrapValueObject<T> = /* recursive type utility */;
1084
+ ```
1085
+
1086
+ #### `deepFreeze<T>`
1087
+
1088
+ Deeply freezes objects to ensure immutability.
1089
+
1090
+ ```typescript
1091
+ export function deepFreeze<T>(obj: T, seen?: WeakSet<object>): Readonly<T>;
1092
+ ```
1093
+
1094
+ ### Types
1095
+
1096
+ #### `EntityId<T>`
1097
+
1098
+ Interface for identity value objects.
1099
+
1100
+ ```typescript
1101
+ export interface EntityId<T = string> {
1102
+ equals(other?: EntityId<T> | null | undefined): boolean;
1103
+ toString(): string;
1104
+ }
1105
+ ```
1106
+
1107
+ #### `EntityProps<ID extends EntityId, Props>`
1108
+
1109
+ Configuration for the base Entity constructor.
1110
+
1111
+ ```typescript
1112
+ export interface EntityProps<ID extends EntityId, Props> {
1113
+ readonly id: ID;
1114
+ readonly createdAt?: Date;
1115
+ readonly props: Props;
1116
+ }
1117
+ ```
1118
+
1119
+ #### `DomainEventPayload`
1120
+
1121
+ Type for domain event payloads (only primitives and serializable structures).
1122
+
1123
+ ```typescript
1124
+ export type DomainEventPayload = Record<string, Serializable>;
753
1125
  ```
754
1126
 
755
1127
  ## Examples
@@ -767,6 +1139,7 @@ import {
767
1139
  DomainEvent,
768
1140
  Entity,
769
1141
  ApplicationServicePort,
1142
+ type DomainEventPayload,
770
1143
  } from '@rineex/ddd';
771
1144
 
772
1145
  // ============ Value Objects ============
@@ -807,7 +1180,7 @@ interface OrderLineProps {
807
1180
  price: Money;
808
1181
  }
809
1182
 
810
- class OrderLine extends Entity<OrderLineProps> {
1183
+ class OrderLine extends Entity<AggregateId, OrderLineProps> {
811
1184
  get productId(): string {
812
1185
  return this.props.productId;
813
1186
  }
@@ -829,19 +1202,48 @@ class OrderLine extends Entity<OrderLineProps> {
829
1202
  throw new Error('Quantity must be positive');
830
1203
  }
831
1204
  }
1205
+
1206
+ public toObject(): Record<string, unknown> {
1207
+ return {
1208
+ id: this.id.toString(),
1209
+ createdAt: this.createdAt.toISOString(),
1210
+ productId: this.productId,
1211
+ quantity: this.quantity,
1212
+ price: this.price.value,
1213
+ };
1214
+ }
832
1215
  }
833
1216
 
834
1217
  // ============ Domain Events ============
835
1218
 
836
- class OrderCreatedEvent extends DomainEvent {
1219
+ interface OrderCreatedPayload extends DomainEventPayload {
1220
+ customerId: string;
1221
+ }
1222
+
1223
+ class OrderCreatedEvent extends DomainEvent<AggregateId, OrderCreatedPayload> {
837
1224
  public readonly eventName = 'OrderCreated';
838
1225
  }
839
1226
 
840
- class OrderLineAddedEvent extends DomainEvent {
1227
+ interface OrderLineAddedPayload extends DomainEventPayload {
1228
+ productId: string;
1229
+ quantity: number;
1230
+ }
1231
+
1232
+ class OrderLineAddedEvent extends DomainEvent<
1233
+ AggregateId,
1234
+ OrderLineAddedPayload
1235
+ > {
841
1236
  public readonly eventName = 'OrderLineAdded';
842
1237
  }
843
1238
 
844
- class OrderCompletedEvent extends DomainEvent {
1239
+ interface OrderCompletedPayload extends DomainEventPayload {
1240
+ total: number;
1241
+ }
1242
+
1243
+ class OrderCompletedEvent extends DomainEvent<
1244
+ AggregateId,
1245
+ OrderCompletedPayload
1246
+ > {
845
1247
  public readonly eventName = 'OrderCompleted';
846
1248
  }
847
1249
 
@@ -854,7 +1256,7 @@ interface OrderProps {
854
1256
  total: Money;
855
1257
  }
856
1258
 
857
- class Order extends AggregateRoot<OrderProps> {
1259
+ class Order extends AggregateRoot<AggregateId, OrderProps> {
858
1260
  get customerId(): string {
859
1261
  return this.props.customerId;
860
1262
  }
@@ -872,8 +1274,9 @@ class Order extends AggregateRoot<OrderProps> {
872
1274
  }
873
1275
 
874
1276
  public static create(customerId: string, id?: AggregateId): Order {
1277
+ const orderId = id || AggregateId.generate();
875
1278
  const order = new Order({
876
- id: id || AggregateId.create(),
1279
+ id: orderId,
877
1280
  createdAt: new Date(),
878
1281
  props: {
879
1282
  customerId,
@@ -885,8 +1288,7 @@ class Order extends AggregateRoot<OrderProps> {
885
1288
 
886
1289
  order.addEvent(
887
1290
  new OrderCreatedEvent({
888
- id: crypto.randomUUID(),
889
- aggregateId: order.id.uuid,
1291
+ aggregateId: orderId,
890
1292
  schemaVersion: 1,
891
1293
  occurredAt: Date.now(),
892
1294
  payload: { customerId },
@@ -898,7 +1300,7 @@ class Order extends AggregateRoot<OrderProps> {
898
1300
 
899
1301
  public addLine(productId: string, quantity: number, price: Money): void {
900
1302
  const line = new OrderLine({
901
- id: AggregateId.create(),
1303
+ id: AggregateId.generate(),
902
1304
  createdAt: new Date(),
903
1305
  props: { productId, quantity, price },
904
1306
  });
@@ -908,8 +1310,7 @@ class Order extends AggregateRoot<OrderProps> {
908
1310
 
909
1311
  this.addEvent(
910
1312
  new OrderLineAddedEvent({
911
- id: crypto.randomUUID(),
912
- aggregateId: this.id.uuid,
1313
+ aggregateId: this.id,
913
1314
  schemaVersion: 1,
914
1315
  occurredAt: Date.now(),
915
1316
  payload: { productId, quantity },
@@ -926,8 +1327,7 @@ class Order extends AggregateRoot<OrderProps> {
926
1327
 
927
1328
  this.addEvent(
928
1329
  new OrderCompletedEvent({
929
- id: crypto.randomUUID(),
930
- aggregateId: this.id.uuid,
1330
+ aggregateId: this.id,
931
1331
  schemaVersion: 1,
932
1332
  occurredAt: Date.now(),
933
1333
  payload: { total: this.total.amount },
@@ -948,6 +1348,17 @@ class Order extends AggregateRoot<OrderProps> {
948
1348
  throw new Error('Order must have at least one line');
949
1349
  }
950
1350
  }
1351
+
1352
+ public toObject(): Record<string, unknown> {
1353
+ return {
1354
+ id: this.id.toString(),
1355
+ createdAt: this.createdAt.toISOString(),
1356
+ customerId: this.customerId,
1357
+ lines: this.lines.map(line => line.toObject()),
1358
+ status: this.status,
1359
+ total: this.total.value,
1360
+ };
1361
+ }
951
1362
  }
952
1363
 
953
1364
  // ============ Application Service ============
@@ -981,7 +1392,7 @@ class CreateOrderService implements ApplicationServicePort<
981
1392
  await this.orderRepository.save(order);
982
1393
 
983
1394
  return {
984
- id: order.id.uuid,
1395
+ id: order.id.toString(),
985
1396
  customerId: order.customerId,
986
1397
  total: order.total.amount,
987
1398
  lineCount: order.lines.length,
@@ -1144,8 +1555,10 @@ import {
1144
1555
  EntityValidationError,
1145
1556
  InvalidValueObjectError,
1146
1557
  ApplicationError,
1558
+ Result,
1147
1559
  } from '@rineex/ddd';
1148
1560
 
1561
+ // Using exceptions (traditional approach)
1149
1562
  try {
1150
1563
  const email = Email.create('invalid-email');
1151
1564
  } catch (error) {
@@ -1157,7 +1570,7 @@ try {
1157
1570
 
1158
1571
  try {
1159
1572
  const user = new User({
1160
- id: AggregateId.create(),
1573
+ id: AggregateId.generate(),
1161
1574
  createdAt: new Date(),
1162
1575
  props: { email: Email.create('user@example.com') },
1163
1576
  });
@@ -1175,9 +1588,66 @@ try {
1175
1588
  if (error instanceof ApplicationError) {
1176
1589
  // Handle application-level errors
1177
1590
  console.error('Service failed:', error.message);
1591
+ console.error('HTTP Status:', error.status);
1592
+ console.error('Error Code:', error.code);
1178
1593
  } else if (error instanceof DomainError) {
1179
1594
  // Catch-all for domain errors
1180
1595
  console.error('Domain error:', error.message);
1596
+ console.error('Error Code:', error.code);
1597
+ }
1598
+ }
1599
+
1600
+ // Using Result type (functional approach with DomainError)
1601
+ class InvalidEmailError extends DomainError {
1602
+ public get code() {
1603
+ return 'DOMAIN.INVALID_VALUE' as const;
1604
+ }
1605
+
1606
+ constructor(message: string) {
1607
+ super({ message });
1608
+ }
1609
+ }
1610
+
1611
+ function createUser(email: string): Result<User, DomainError> {
1612
+ // Validate email format
1613
+ if (!email.includes('@')) {
1614
+ return Result.fail(new InvalidEmailError('Invalid email format'));
1615
+ }
1616
+
1617
+ try {
1618
+ const emailVO = Email.create(email);
1619
+ const user = new User({
1620
+ id: AggregateId.generate(),
1621
+ createdAt: new Date(),
1622
+ props: { email: emailVO, isActive: true },
1623
+ });
1624
+ return Result.ok(user);
1625
+ } catch (error) {
1626
+ if (error instanceof InvalidValueObjectError) {
1627
+ return Result.fail(new InvalidEmailError(error.message));
1628
+ }
1629
+ return Result.fail(
1630
+ new InvalidEmailError(
1631
+ error instanceof Error ? error.message : 'Unknown error',
1632
+ ),
1633
+ );
1634
+ }
1635
+ }
1636
+
1637
+ const result = createUser('user@example.com');
1638
+ if (result.isSuccessResult()) {
1639
+ const user = result.getValue();
1640
+ if (user) {
1641
+ // Use user safely with full type safety
1642
+ console.log('User created:', user.id.toString());
1643
+ }
1644
+ } else if (result.isFailureResult()) {
1645
+ const error = result.getError();
1646
+ if (error) {
1647
+ // Handle error with full context and type safety
1648
+ console.error('Error code:', error.code);
1649
+ console.error('Error message:', error.message);
1650
+ console.error('Metadata:', error.metadata);
1181
1651
  }
1182
1652
  }
1183
1653
  ```