@rineex/ddd 1.5.1 → 1.6.1
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 +377 -79
- package/dist/index.d.mts +382 -298
- package/dist/index.d.ts +382 -298
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
391
|
-
aggregateId: this.id.uuid,
|
|
444
|
+
aggregateId: this.id,
|
|
392
445
|
schemaVersion: 1,
|
|
393
446
|
occurredAt: Date.now(),
|
|
394
|
-
payload: { oldEmail
|
|
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
|
-
|
|
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:
|
|
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: '
|
|
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<
|
|
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<
|
|
599
|
-
readonly id:
|
|
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
|
-
|
|
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
|
|
612
|
-
createdAt
|
|
613
|
-
props:
|
|
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<
|
|
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<
|
|
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<
|
|
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:
|
|
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
|
|
684
|
-
|
|
685
|
-
public static
|
|
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
|
-
#### `
|
|
774
|
+
#### `Url`
|
|
700
775
|
|
|
701
776
|
Validates web URLs.
|
|
702
777
|
|
|
703
778
|
```typescript
|
|
704
|
-
class
|
|
705
|
-
public static create(value: string):
|
|
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,157 @@ 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.
|
|
750
831
|
|
|
751
832
|
```typescript
|
|
752
|
-
export class ApplicationError extends
|
|
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.
|
|
848
|
+
|
|
849
|
+
```typescript
|
|
850
|
+
export class Result<T, E> {
|
|
851
|
+
readonly isSuccess: boolean;
|
|
852
|
+
readonly isFailure: boolean;
|
|
853
|
+
|
|
854
|
+
public static ok<T, E>(value: T): Result<T, E>;
|
|
855
|
+
public static fail<T, E>(error: E): Result<T, E>;
|
|
856
|
+
public getValue(): T;
|
|
857
|
+
public getError(): E;
|
|
858
|
+
}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
**Example:**
|
|
862
|
+
|
|
863
|
+
```typescript
|
|
864
|
+
import { Result } from '@rineex/ddd';
|
|
865
|
+
|
|
866
|
+
function parseNumber(input: string): Result<number, string> {
|
|
867
|
+
const value = Number(input);
|
|
868
|
+
if (Number.isNaN(value)) {
|
|
869
|
+
return Result.fail('Invalid number');
|
|
870
|
+
}
|
|
871
|
+
return Result.ok(value);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const result = parseNumber('42');
|
|
875
|
+
if (result.isSuccess) {
|
|
876
|
+
console.log(result.getValue()); // 42
|
|
877
|
+
} else {
|
|
878
|
+
console.error(result.getError());
|
|
879
|
+
}
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### Domain Violations
|
|
883
|
+
|
|
884
|
+
#### `DomainViolation`
|
|
885
|
+
|
|
886
|
+
Base class for domain violations. Purposely does NOT extend native Error to
|
|
887
|
+
avoid stack trace overhead in the domain.
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
export abstract class DomainViolation {
|
|
891
|
+
abstract readonly code: string;
|
|
892
|
+
abstract readonly message: string;
|
|
893
|
+
readonly metadata: Readonly<Record<string, unknown>>;
|
|
894
|
+
}
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### HTTP Status Codes
|
|
898
|
+
|
|
899
|
+
#### `HttpStatus` and `HttpStatusMessage`
|
|
900
|
+
|
|
901
|
+
Typed HTTP status code constants and messages.
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
export const HttpStatus = {
|
|
905
|
+
OK: 200,
|
|
906
|
+
CREATED: 201,
|
|
907
|
+
BAD_REQUEST: 400,
|
|
908
|
+
UNAUTHORIZED: 401,
|
|
909
|
+
FORBIDDEN: 403,
|
|
910
|
+
NOT_FOUND: 404,
|
|
911
|
+
INTERNAL_SERVER_ERROR: 500,
|
|
912
|
+
// ... and many more
|
|
913
|
+
} as const;
|
|
914
|
+
|
|
915
|
+
export const HttpStatusMessage = {
|
|
916
|
+
200: 'OK',
|
|
917
|
+
201: 'Created',
|
|
918
|
+
400: 'Bad Request',
|
|
919
|
+
// ... and many more
|
|
920
|
+
} as const;
|
|
921
|
+
|
|
922
|
+
export type HttpStatusCode = keyof typeof HttpStatusMessage;
|
|
923
|
+
export type HttpStatusMessage =
|
|
924
|
+
(typeof HttpStatusMessage)[keyof typeof HttpStatusMessage];
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
### Utilities
|
|
928
|
+
|
|
929
|
+
#### `unwrapValueObject<T>`
|
|
930
|
+
|
|
931
|
+
Recursively unwraps value objects from nested structures.
|
|
932
|
+
|
|
933
|
+
```typescript
|
|
934
|
+
export function unwrapValueObject<T>(
|
|
935
|
+
input: T,
|
|
936
|
+
seen?: WeakSet<object>
|
|
937
|
+
): UnwrapValueObject<T>;
|
|
938
|
+
|
|
939
|
+
export type UnwrapValueObject<T> = /* recursive type utility */;
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
#### `deepFreeze<T>`
|
|
943
|
+
|
|
944
|
+
Deeply freezes objects to ensure immutability.
|
|
945
|
+
|
|
946
|
+
```typescript
|
|
947
|
+
export function deepFreeze<T>(obj: T, seen?: WeakSet<object>): Readonly<T>;
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
### Types
|
|
951
|
+
|
|
952
|
+
#### `EntityId<T>`
|
|
953
|
+
|
|
954
|
+
Interface for identity value objects.
|
|
955
|
+
|
|
956
|
+
```typescript
|
|
957
|
+
export interface EntityId<T = string> {
|
|
958
|
+
equals(other?: EntityId<T> | null | undefined): boolean;
|
|
959
|
+
toString(): string;
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
#### `EntityProps<ID extends EntityId, Props>`
|
|
964
|
+
|
|
965
|
+
Configuration for the base Entity constructor.
|
|
966
|
+
|
|
967
|
+
```typescript
|
|
968
|
+
export interface EntityProps<ID extends EntityId, Props> {
|
|
969
|
+
readonly id: ID;
|
|
970
|
+
readonly createdAt?: Date;
|
|
971
|
+
readonly props: Props;
|
|
972
|
+
}
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
#### `DomainEventPayload`
|
|
976
|
+
|
|
977
|
+
Type for domain event payloads (only primitives and serializable structures).
|
|
978
|
+
|
|
979
|
+
```typescript
|
|
980
|
+
export type DomainEventPayload = Record<string, Serializable>;
|
|
753
981
|
```
|
|
754
982
|
|
|
755
983
|
## Examples
|
|
@@ -767,6 +995,7 @@ import {
|
|
|
767
995
|
DomainEvent,
|
|
768
996
|
Entity,
|
|
769
997
|
ApplicationServicePort,
|
|
998
|
+
type DomainEventPayload,
|
|
770
999
|
} from '@rineex/ddd';
|
|
771
1000
|
|
|
772
1001
|
// ============ Value Objects ============
|
|
@@ -807,7 +1036,7 @@ interface OrderLineProps {
|
|
|
807
1036
|
price: Money;
|
|
808
1037
|
}
|
|
809
1038
|
|
|
810
|
-
class OrderLine extends Entity<OrderLineProps> {
|
|
1039
|
+
class OrderLine extends Entity<AggregateId, OrderLineProps> {
|
|
811
1040
|
get productId(): string {
|
|
812
1041
|
return this.props.productId;
|
|
813
1042
|
}
|
|
@@ -829,19 +1058,48 @@ class OrderLine extends Entity<OrderLineProps> {
|
|
|
829
1058
|
throw new Error('Quantity must be positive');
|
|
830
1059
|
}
|
|
831
1060
|
}
|
|
1061
|
+
|
|
1062
|
+
public toObject(): Record<string, unknown> {
|
|
1063
|
+
return {
|
|
1064
|
+
id: this.id.toString(),
|
|
1065
|
+
createdAt: this.createdAt.toISOString(),
|
|
1066
|
+
productId: this.productId,
|
|
1067
|
+
quantity: this.quantity,
|
|
1068
|
+
price: this.price.value,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
832
1071
|
}
|
|
833
1072
|
|
|
834
1073
|
// ============ Domain Events ============
|
|
835
1074
|
|
|
836
|
-
|
|
1075
|
+
interface OrderCreatedPayload extends DomainEventPayload {
|
|
1076
|
+
customerId: string;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
class OrderCreatedEvent extends DomainEvent<AggregateId, OrderCreatedPayload> {
|
|
837
1080
|
public readonly eventName = 'OrderCreated';
|
|
838
1081
|
}
|
|
839
1082
|
|
|
840
|
-
|
|
1083
|
+
interface OrderLineAddedPayload extends DomainEventPayload {
|
|
1084
|
+
productId: string;
|
|
1085
|
+
quantity: number;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
class OrderLineAddedEvent extends DomainEvent<
|
|
1089
|
+
AggregateId,
|
|
1090
|
+
OrderLineAddedPayload
|
|
1091
|
+
> {
|
|
841
1092
|
public readonly eventName = 'OrderLineAdded';
|
|
842
1093
|
}
|
|
843
1094
|
|
|
844
|
-
|
|
1095
|
+
interface OrderCompletedPayload extends DomainEventPayload {
|
|
1096
|
+
total: number;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
class OrderCompletedEvent extends DomainEvent<
|
|
1100
|
+
AggregateId,
|
|
1101
|
+
OrderCompletedPayload
|
|
1102
|
+
> {
|
|
845
1103
|
public readonly eventName = 'OrderCompleted';
|
|
846
1104
|
}
|
|
847
1105
|
|
|
@@ -854,7 +1112,7 @@ interface OrderProps {
|
|
|
854
1112
|
total: Money;
|
|
855
1113
|
}
|
|
856
1114
|
|
|
857
|
-
class Order extends AggregateRoot<OrderProps> {
|
|
1115
|
+
class Order extends AggregateRoot<AggregateId, OrderProps> {
|
|
858
1116
|
get customerId(): string {
|
|
859
1117
|
return this.props.customerId;
|
|
860
1118
|
}
|
|
@@ -872,8 +1130,9 @@ class Order extends AggregateRoot<OrderProps> {
|
|
|
872
1130
|
}
|
|
873
1131
|
|
|
874
1132
|
public static create(customerId: string, id?: AggregateId): Order {
|
|
1133
|
+
const orderId = id || AggregateId.generate();
|
|
875
1134
|
const order = new Order({
|
|
876
|
-
id:
|
|
1135
|
+
id: orderId,
|
|
877
1136
|
createdAt: new Date(),
|
|
878
1137
|
props: {
|
|
879
1138
|
customerId,
|
|
@@ -885,8 +1144,7 @@ class Order extends AggregateRoot<OrderProps> {
|
|
|
885
1144
|
|
|
886
1145
|
order.addEvent(
|
|
887
1146
|
new OrderCreatedEvent({
|
|
888
|
-
|
|
889
|
-
aggregateId: order.id.uuid,
|
|
1147
|
+
aggregateId: orderId,
|
|
890
1148
|
schemaVersion: 1,
|
|
891
1149
|
occurredAt: Date.now(),
|
|
892
1150
|
payload: { customerId },
|
|
@@ -898,7 +1156,7 @@ class Order extends AggregateRoot<OrderProps> {
|
|
|
898
1156
|
|
|
899
1157
|
public addLine(productId: string, quantity: number, price: Money): void {
|
|
900
1158
|
const line = new OrderLine({
|
|
901
|
-
id: AggregateId.
|
|
1159
|
+
id: AggregateId.generate(),
|
|
902
1160
|
createdAt: new Date(),
|
|
903
1161
|
props: { productId, quantity, price },
|
|
904
1162
|
});
|
|
@@ -908,8 +1166,7 @@ class Order extends AggregateRoot<OrderProps> {
|
|
|
908
1166
|
|
|
909
1167
|
this.addEvent(
|
|
910
1168
|
new OrderLineAddedEvent({
|
|
911
|
-
|
|
912
|
-
aggregateId: this.id.uuid,
|
|
1169
|
+
aggregateId: this.id,
|
|
913
1170
|
schemaVersion: 1,
|
|
914
1171
|
occurredAt: Date.now(),
|
|
915
1172
|
payload: { productId, quantity },
|
|
@@ -926,8 +1183,7 @@ class Order extends AggregateRoot<OrderProps> {
|
|
|
926
1183
|
|
|
927
1184
|
this.addEvent(
|
|
928
1185
|
new OrderCompletedEvent({
|
|
929
|
-
|
|
930
|
-
aggregateId: this.id.uuid,
|
|
1186
|
+
aggregateId: this.id,
|
|
931
1187
|
schemaVersion: 1,
|
|
932
1188
|
occurredAt: Date.now(),
|
|
933
1189
|
payload: { total: this.total.amount },
|
|
@@ -948,6 +1204,17 @@ class Order extends AggregateRoot<OrderProps> {
|
|
|
948
1204
|
throw new Error('Order must have at least one line');
|
|
949
1205
|
}
|
|
950
1206
|
}
|
|
1207
|
+
|
|
1208
|
+
public toObject(): Record<string, unknown> {
|
|
1209
|
+
return {
|
|
1210
|
+
id: this.id.toString(),
|
|
1211
|
+
createdAt: this.createdAt.toISOString(),
|
|
1212
|
+
customerId: this.customerId,
|
|
1213
|
+
lines: this.lines.map(line => line.toObject()),
|
|
1214
|
+
status: this.status,
|
|
1215
|
+
total: this.total.value,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
951
1218
|
}
|
|
952
1219
|
|
|
953
1220
|
// ============ Application Service ============
|
|
@@ -981,7 +1248,7 @@ class CreateOrderService implements ApplicationServicePort<
|
|
|
981
1248
|
await this.orderRepository.save(order);
|
|
982
1249
|
|
|
983
1250
|
return {
|
|
984
|
-
id: order.id.
|
|
1251
|
+
id: order.id.toString(),
|
|
985
1252
|
customerId: order.customerId,
|
|
986
1253
|
total: order.total.amount,
|
|
987
1254
|
lineCount: order.lines.length,
|
|
@@ -1144,8 +1411,10 @@ import {
|
|
|
1144
1411
|
EntityValidationError,
|
|
1145
1412
|
InvalidValueObjectError,
|
|
1146
1413
|
ApplicationError,
|
|
1414
|
+
Result,
|
|
1147
1415
|
} from '@rineex/ddd';
|
|
1148
1416
|
|
|
1417
|
+
// Using exceptions (traditional approach)
|
|
1149
1418
|
try {
|
|
1150
1419
|
const email = Email.create('invalid-email');
|
|
1151
1420
|
} catch (error) {
|
|
@@ -1157,7 +1426,7 @@ try {
|
|
|
1157
1426
|
|
|
1158
1427
|
try {
|
|
1159
1428
|
const user = new User({
|
|
1160
|
-
id: AggregateId.
|
|
1429
|
+
id: AggregateId.generate(),
|
|
1161
1430
|
createdAt: new Date(),
|
|
1162
1431
|
props: { email: Email.create('user@example.com') },
|
|
1163
1432
|
});
|
|
@@ -1175,11 +1444,40 @@ try {
|
|
|
1175
1444
|
if (error instanceof ApplicationError) {
|
|
1176
1445
|
// Handle application-level errors
|
|
1177
1446
|
console.error('Service failed:', error.message);
|
|
1447
|
+
console.error('HTTP Status:', error.status);
|
|
1448
|
+
console.error('Error Code:', error.code);
|
|
1178
1449
|
} else if (error instanceof DomainError) {
|
|
1179
1450
|
// Catch-all for domain errors
|
|
1180
1451
|
console.error('Domain error:', error.message);
|
|
1452
|
+
console.error('Error Code:', error.code);
|
|
1181
1453
|
}
|
|
1182
1454
|
}
|
|
1455
|
+
|
|
1456
|
+
// Using Result type (functional approach)
|
|
1457
|
+
function createUser(email: string): Result<User, string> {
|
|
1458
|
+
try {
|
|
1459
|
+
const emailVO = Email.create(email);
|
|
1460
|
+
const user = new User({
|
|
1461
|
+
id: AggregateId.generate(),
|
|
1462
|
+
createdAt: new Date(),
|
|
1463
|
+
props: { email: emailVO, isActive: true },
|
|
1464
|
+
});
|
|
1465
|
+
return Result.ok(user);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
return Result.fail(
|
|
1468
|
+
error instanceof Error ? error.message : 'Unknown error',
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const result = createUser('user@example.com');
|
|
1474
|
+
if (result.isSuccess) {
|
|
1475
|
+
const user = result.getValue();
|
|
1476
|
+
// Use user
|
|
1477
|
+
} else {
|
|
1478
|
+
const error = result.getError();
|
|
1479
|
+
// Handle error
|
|
1480
|
+
}
|
|
1183
1481
|
```
|
|
1184
1482
|
|
|
1185
1483
|
## TypeScript Support
|