mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat(guardian): add guardian package - code quality analyzer
Add @puaros/guardian package v0.1.0 - code quality guardian for vibe coders and enterprise teams. Features: - Hardcode detection (magic numbers, magic strings) - Circular dependency detection - Naming convention enforcement (Clean Architecture) - Architecture violation detection - CLI tool with comprehensive reporting - 159 tests with 80%+ coverage - Smart suggestions for fixes - Built for AI-assisted development Built with Clean Architecture and DDD principles. Works with Claude, GPT, Copilot, Cursor, and any AI coding assistant.
This commit is contained in:
100
packages/guardian/examples/README.md
Normal file
100
packages/guardian/examples/README.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Guardian Examples
|
||||
|
||||
This directory contains examples of good and bad code patterns used for testing Guardian's detection capabilities.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
examples/
|
||||
├── good-architecture/ # ✅ Proper Clean Architecture + DDD patterns
|
||||
│ ├── domain/
|
||||
│ │ ├── aggregates/ # Aggregate Roots
|
||||
│ │ ├── entities/ # Domain Entities
|
||||
│ │ ├── value-objects/ # Value Objects
|
||||
│ │ ├── services/ # Domain Services
|
||||
│ │ ├── factories/ # Domain Factories
|
||||
│ │ ├── specifications/# Business Rules
|
||||
│ │ └── repositories/ # Repository Interfaces
|
||||
│ ├── application/
|
||||
│ │ ├── use-cases/ # Application Use Cases
|
||||
│ │ ├── dtos/ # Data Transfer Objects
|
||||
│ │ └── mappers/ # Domain <-> DTO mappers
|
||||
│ └── infrastructure/
|
||||
│ ├── repositories/ # Repository Implementations
|
||||
│ ├── controllers/ # HTTP Controllers
|
||||
│ └── services/ # External Services
|
||||
│
|
||||
└── bad-architecture/ # ❌ Anti-patterns for testing
|
||||
├── hardcoded/ # Hardcoded values
|
||||
├── circular/ # Circular dependencies
|
||||
├── framework-leaks/ # Framework in domain
|
||||
├── entity-exposure/ # Entities in controllers
|
||||
├── naming/ # Wrong naming conventions
|
||||
└── anemic-model/ # Anemic domain models
|
||||
```
|
||||
|
||||
## Patterns Demonstrated
|
||||
|
||||
### Domain-Driven Design (DDD)
|
||||
- **Aggregate Roots**: User, Order
|
||||
- **Entities**: OrderItem, Address
|
||||
- **Value Objects**: Email, Money, OrderStatus
|
||||
- **Domain Services**: UserRegistrationService, PricingService
|
||||
- **Domain Events**: UserCreatedEvent, OrderPlacedEvent
|
||||
- **Factories**: UserFactory, OrderFactory
|
||||
- **Specifications**: EmailSpecification, OrderCanBeCancelledSpecification
|
||||
- **Repository Interfaces**: IUserRepository, IOrderRepository
|
||||
|
||||
### SOLID Principles
|
||||
- **SRP**: Single Responsibility - each class has one reason to change
|
||||
- **OCP**: Open/Closed - extend with new classes, not modifications
|
||||
- **LSP**: Liskov Substitution - derived classes are substitutable
|
||||
- **ISP**: Interface Segregation - small, focused interfaces
|
||||
- **DIP**: Dependency Inversion - depend on abstractions
|
||||
|
||||
### Clean Architecture
|
||||
- **Dependency Rule**: Inner layers don't know about outer layers
|
||||
- **Domain**: Pure business logic, no frameworks
|
||||
- **Application**: Use cases orchestration
|
||||
- **Infrastructure**: External concerns (DB, HTTP, etc.)
|
||||
|
||||
### Clean Code Principles
|
||||
- **DRY**: Don't Repeat Yourself
|
||||
- **KISS**: Keep It Simple, Stupid
|
||||
- **YAGNI**: You Aren't Gonna Need It
|
||||
- **Meaningful Names**: Intention-revealing names
|
||||
- **Small Functions**: Do one thing well
|
||||
- **No Magic Values**: Named constants
|
||||
|
||||
## Testing Guardian
|
||||
|
||||
Run Guardian on examples:
|
||||
|
||||
```bash
|
||||
# Test good architecture (should have no violations)
|
||||
pnpm guardian check examples/good-architecture
|
||||
|
||||
# Test bad architecture (should detect violations)
|
||||
pnpm guardian check examples/bad-architecture
|
||||
|
||||
# Test specific anti-pattern
|
||||
pnpm guardian check examples/bad-architecture/hardcoded
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Development
|
||||
Use good examples as templates for new features
|
||||
|
||||
### 2. Testing
|
||||
Use bad examples to verify Guardian detects violations
|
||||
|
||||
### 3. Documentation
|
||||
Learn Clean Architecture + DDD patterns by example
|
||||
|
||||
### 4. CI/CD
|
||||
Run Guardian on examples in CI to prevent regressions
|
||||
|
||||
---
|
||||
|
||||
**Note:** These examples are intentionally simplified for educational purposes. Real-world applications would have more complexity.
|
||||
316
packages/guardian/examples/SUMMARY.md
Normal file
316
packages/guardian/examples/SUMMARY.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# Guardian Examples - Summary
|
||||
|
||||
This document summarizes the examples created for testing Guardian's detection capabilities.
|
||||
|
||||
## 📁 Structure Overview
|
||||
|
||||
```
|
||||
examples/
|
||||
├── README.md # Main documentation
|
||||
├── SUMMARY.md # This file
|
||||
├── good-architecture/ # ✅ Best practices (29 files)
|
||||
│ ├── domain/ # Domain layer (18 files)
|
||||
│ │ ├── aggregates/ # User, Order aggregate roots
|
||||
│ │ ├── entities/ # OrderItem entity
|
||||
│ │ ├── value-objects/ # Email, Money, UserId, OrderId, OrderStatus
|
||||
│ │ ├── events/ # UserCreatedEvent
|
||||
│ │ ├── services/ # UserRegistrationService, PricingService
|
||||
│ │ ├── factories/ # UserFactory, OrderFactory
|
||||
│ │ ├── specifications/ # Specification pattern, business rules
|
||||
│ │ └── repositories/ # IUserRepository, IOrderRepository interfaces
|
||||
│ ├── application/ # Application layer (7 files)
|
||||
│ │ ├── use-cases/ # CreateUser, PlaceOrder
|
||||
│ │ ├── dtos/ # UserResponseDto, OrderResponseDto, CreateUserRequest
|
||||
│ │ └── mappers/ # UserMapper, OrderMapper
|
||||
│ └── infrastructure/ # Infrastructure layer (4 files)
|
||||
│ ├── repositories/ # InMemoryUserRepository, InMemoryOrderRepository
|
||||
│ └── controllers/ # UserController, OrderController
|
||||
│
|
||||
└── bad-architecture/ # ❌ Anti-patterns (7 files)
|
||||
├── hardcoded/ # Magic numbers and strings
|
||||
├── circular/ # Circular dependencies
|
||||
├── framework-leaks/ # Framework in domain layer
|
||||
├── entity-exposure/ # Domain entities in controllers
|
||||
└── naming/ # Wrong naming conventions
|
||||
```
|
||||
|
||||
## ✅ Good Architecture Examples (29 files)
|
||||
|
||||
### Domain Layer - DDD Patterns
|
||||
|
||||
#### 1. **Aggregates** (2 files)
|
||||
- **User.ts** - User aggregate root with:
|
||||
- Business operations: activate, deactivate, block, unblock, recordLogin
|
||||
- Invariants validation
|
||||
- Domain events (UserCreatedEvent)
|
||||
- Factory methods: create(), reconstitute()
|
||||
|
||||
- **Order.ts** - Order aggregate with complex logic:
|
||||
- Manages OrderItem entities
|
||||
- Order lifecycle (confirm, pay, ship, deliver, cancel)
|
||||
- Status transitions with validation
|
||||
- Business rules enforcement
|
||||
- Total calculation
|
||||
|
||||
#### 2. **Value Objects** (5 files)
|
||||
- **Email.ts** - Self-validating email with regex, domain extraction
|
||||
- **Money.ts** - Money with currency, arithmetic operations, prevents currency mixing
|
||||
- **UserId.ts** - Strongly typed ID (UUID-based)
|
||||
- **OrderId.ts** - Strongly typed Order ID
|
||||
- **OrderStatus.ts** - Type-safe enum with valid transitions
|
||||
|
||||
#### 3. **Entities** (1 file)
|
||||
- **OrderItem.ts** - Entity with identity, part of Order aggregate
|
||||
|
||||
#### 4. **Domain Events** (1 file)
|
||||
- **UserCreatedEvent.ts** - Immutable domain event
|
||||
|
||||
#### 5. **Domain Services** (2 files)
|
||||
- **UserRegistrationService.ts** - Checks email uniqueness, coordinates user creation
|
||||
- **PricingService.ts** - Calculates discounts, shipping, tax
|
||||
|
||||
#### 6. **Factories** (2 files)
|
||||
- **UserFactory.ts** - Creates users from OAuth, legacy data, test users
|
||||
- **OrderFactory.ts** - Creates orders with various scenarios
|
||||
|
||||
#### 7. **Specifications** (3 files)
|
||||
- **Specification.ts** - Base class with AND, OR, NOT combinators
|
||||
- **EmailSpecification.ts** - Corporate email, blacklist rules
|
||||
- **OrderSpecification.ts** - Discount eligibility, cancellation rules
|
||||
|
||||
#### 8. **Repository Interfaces** (2 files)
|
||||
- **IUserRepository.ts** - User persistence abstraction
|
||||
- **IOrderRepository.ts** - Order persistence abstraction
|
||||
|
||||
### Application Layer
|
||||
|
||||
#### 9. **Use Cases** (2 files)
|
||||
- **CreateUser.ts** - Orchestrates user registration
|
||||
- **PlaceOrder.ts** - Orchestrates order placement
|
||||
|
||||
#### 10. **DTOs** (3 files)
|
||||
- **UserResponseDto.ts** - API response format
|
||||
- **CreateUserRequest.ts** - API request format
|
||||
- **OrderResponseDto.ts** - Order with items response
|
||||
|
||||
#### 11. **Mappers** (2 files)
|
||||
- **UserMapper.ts** - Domain ↔ DTO conversion
|
||||
- **OrderMapper.ts** - Domain ↔ DTO conversion
|
||||
|
||||
### Infrastructure Layer
|
||||
|
||||
#### 12. **Repositories** (2 files)
|
||||
- **InMemoryUserRepository.ts** - User repository implementation
|
||||
- **InMemoryOrderRepository.ts** - Order repository implementation
|
||||
|
||||
#### 13. **Controllers** (2 files)
|
||||
- **UserController.ts** - HTTP endpoints, returns DTOs
|
||||
- **OrderController.ts** - HTTP endpoints, delegates to use cases
|
||||
|
||||
## ❌ Bad Architecture Examples (7 files)
|
||||
|
||||
### 1. **Hardcoded Values** (1 file)
|
||||
- **ServerWithMagicNumbers.ts**
|
||||
- Magic numbers: 3000 (port), 5000 (timeout), 3 (retries), 100, 200, 60
|
||||
- Magic strings: "http://localhost:8080", "mongodb://localhost:27017/mydb"
|
||||
|
||||
### 2. **Circular Dependencies** (2 files)
|
||||
- **UserService.ts** → **OrderService.ts** → **UserService.ts**
|
||||
- Creates circular import cycle
|
||||
- Causes tight coupling
|
||||
- Makes testing difficult
|
||||
|
||||
### 3. **Framework Leaks** (1 file)
|
||||
- **UserEntity.ts**
|
||||
- Imports PrismaClient in domain layer
|
||||
- Violates Dependency Inversion
|
||||
- Couples domain to infrastructure
|
||||
|
||||
### 4. **Entity Exposure** (1 file)
|
||||
- **BadUserController.ts**
|
||||
- Returns domain entity directly (User)
|
||||
- Exposes internal structure (passwordHash, etc.)
|
||||
- No DTO layer
|
||||
|
||||
### 5. **Naming Conventions** (2 files)
|
||||
- **user.ts** - lowercase file name (should be User.ts)
|
||||
- **UserDto.ts** - DTO in domain layer (should be in application)
|
||||
|
||||
## 🧪 Guardian Test Results
|
||||
|
||||
### Test 1: Good Architecture
|
||||
```bash
|
||||
guardian check examples/good-architecture
|
||||
```
|
||||
|
||||
**Results:**
|
||||
- ✅ No critical violations
|
||||
- ⚠️ 60 hardcoded values (mostly error messages and enum values - acceptable for examples)
|
||||
- ⚠️ 1 false positive: "PlaceOrder" verb not recognized (FIXED: added "Place" to allowed verbs)
|
||||
|
||||
**Metrics:**
|
||||
- Files analyzed: 29
|
||||
- Total functions: 12
|
||||
- Total imports: 73
|
||||
- Layer distribution:
|
||||
- domain: 18 files
|
||||
- application: 7 files
|
||||
- infrastructure: 4 files
|
||||
|
||||
### Test 2: Bad Architecture
|
||||
```bash
|
||||
guardian check examples/bad-architecture
|
||||
```
|
||||
|
||||
**Results:**
|
||||
- ✅ Detected 9 hardcoded values in ServerWithMagicNumbers.ts
|
||||
- ⚠️ Circular dependencies not detected (needs investigation)
|
||||
|
||||
**Detected Issues:**
|
||||
1. Magic number: 3 (maxRetries)
|
||||
2. Magic number: 200 (burstLimit)
|
||||
3. Magic string: "mongodb://localhost:27017/mydb"
|
||||
4. Magic string: "http://localhost:8080"
|
||||
5. Magic string: "user@example.com"
|
||||
6. Magic string: "hashed_password_exposed!"
|
||||
|
||||
## 📊 Patterns Demonstrated
|
||||
|
||||
### DDD (Domain-Driven Design)
|
||||
- ✅ Aggregates: User, Order
|
||||
- ✅ Entities: OrderItem
|
||||
- ✅ Value Objects: Email, Money, UserId, OrderId, OrderStatus
|
||||
- ✅ Domain Services: UserRegistrationService, PricingService
|
||||
- ✅ Domain Events: UserCreatedEvent
|
||||
- ✅ Factories: UserFactory, OrderFactory
|
||||
- ✅ Specifications: Email rules, Order rules
|
||||
- ✅ Repository Interfaces: IUserRepository, IOrderRepository
|
||||
|
||||
### SOLID Principles
|
||||
- ✅ **SRP**: Each class has one responsibility
|
||||
- ✅ **OCP**: Extensible through inheritance, not modification
|
||||
- ✅ **LSP**: Specifications, repositories are substitutable
|
||||
- ✅ **ISP**: Small, focused interfaces
|
||||
- ✅ **DIP**: Domain depends on abstractions, infrastructure implements them
|
||||
|
||||
### Clean Architecture
|
||||
- ✅ **Dependency Rule**: Domain → Application → Infrastructure
|
||||
- ✅ **Boundaries**: Clear separation between layers
|
||||
- ✅ **DTOs**: Application layer isolates domain from external world
|
||||
- ✅ **Use Cases**: Application services orchestrate domain logic
|
||||
|
||||
### Clean Code Principles
|
||||
- ✅ **Meaningful Names**: Email, Money, Order (not E, M, O)
|
||||
- ✅ **Small Functions**: Each method does one thing
|
||||
- ✅ **No Magic Values**: Named constants (MAX_RETRIES, DEFAULT_PORT)
|
||||
- ✅ **DRY**: No repeated code
|
||||
- ✅ **KISS**: Simple, straightforward implementations
|
||||
- ✅ **YAGNI**: Only what's needed, no over-engineering
|
||||
|
||||
## 🎯 Key Learnings
|
||||
|
||||
### What Guardian Detects Well ✅
|
||||
1. **Hardcoded values** - Magic numbers and strings
|
||||
2. **Naming conventions** - Layer-specific patterns
|
||||
3. **Layer distribution** - Clean architecture structure
|
||||
4. **Project metrics** - Files, functions, imports
|
||||
|
||||
### What Needs Improvement ⚠️
|
||||
1. **Circular dependencies** - Detection needs investigation
|
||||
2. **Framework leaks** - Feature not yet implemented (v0.4.0)
|
||||
3. **Entity exposure** - Feature not yet implemented (v0.4.0)
|
||||
4. **False positives** - Some verbs missing from allowed list (fixed)
|
||||
|
||||
### What's Next (Roadmap) 🚀
|
||||
1. **v0.4.0**: Framework leaks detection, Entity exposure detection
|
||||
2. **v0.5.0**: Repository pattern enforcement, Dependency injection checks
|
||||
3. **v0.6.0**: Over-engineering detection, Primitive obsession
|
||||
4. **v0.7.0**: Configuration file support
|
||||
5. **v0.8.0**: Multiple output formats (JSON, HTML, SARIF)
|
||||
|
||||
## 💡 How to Use These Examples
|
||||
|
||||
### For Learning
|
||||
- Study `good-architecture/` to understand DDD and Clean Architecture
|
||||
- Compare with `bad-architecture/` to see anti-patterns
|
||||
- Read comments explaining WHY patterns are good or bad
|
||||
|
||||
### For Testing Guardian
|
||||
```bash
|
||||
# Test on good examples (should have minimal violations)
|
||||
pnpm guardian check examples/good-architecture
|
||||
|
||||
# Test on bad examples (should detect violations)
|
||||
pnpm guardian check examples/bad-architecture
|
||||
|
||||
# Test specific anti-pattern
|
||||
pnpm guardian check examples/bad-architecture/hardcoded
|
||||
```
|
||||
|
||||
### For Development
|
||||
- Use good examples as templates for new features
|
||||
- Add new anti-patterns to bad examples
|
||||
- Test Guardian improvements against these examples
|
||||
|
||||
### For CI/CD
|
||||
- Run Guardian on examples in CI to prevent regressions
|
||||
- Ensure new Guardian versions still detect known violations
|
||||
|
||||
## 📝 Statistics
|
||||
|
||||
### Good Architecture
|
||||
- **Total files**: 29
|
||||
- **Domain layer**: 18 files (62%)
|
||||
- **Application layer**: 7 files (24%)
|
||||
- **Infrastructure layer**: 4 files (14%)
|
||||
|
||||
**Pattern distribution:**
|
||||
- Aggregates: 2
|
||||
- Value Objects: 5
|
||||
- Entities: 1
|
||||
- Domain Events: 1
|
||||
- Domain Services: 2
|
||||
- Factories: 2
|
||||
- Specifications: 3
|
||||
- Repositories: 2
|
||||
- Use Cases: 2
|
||||
- DTOs: 3
|
||||
- Mappers: 2
|
||||
- Controllers: 2
|
||||
|
||||
### Bad Architecture
|
||||
- **Total files**: 7
|
||||
- **Anti-patterns**: 5 categories
|
||||
- **Violations detected**: 9 hardcoded values
|
||||
|
||||
## 🎓 Educational Value
|
||||
|
||||
These examples serve as:
|
||||
1. **Learning material** - For understanding Clean Architecture + DDD
|
||||
2. **Testing framework** - For Guardian development
|
||||
3. **Documentation** - Living examples of best practices
|
||||
4. **Templates** - Starting point for new projects
|
||||
5. **Reference** - Quick lookup for patterns
|
||||
|
||||
## 🔧 Maintenance
|
||||
|
||||
### Adding New Examples
|
||||
1. Add to appropriate directory (`good-architecture` or `bad-architecture`)
|
||||
2. Follow naming conventions
|
||||
3. Add detailed comments explaining patterns
|
||||
4. Test with Guardian
|
||||
5. Update this summary
|
||||
|
||||
### Testing Changes
|
||||
1. Run `pnpm build` in guardian package
|
||||
2. Test on both good and bad examples
|
||||
3. Verify detection accuracy
|
||||
4. Update SUMMARY.md with findings
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-24
|
||||
|
||||
**Guardian Version**: 0.2.0 (preparing 0.3.0)
|
||||
|
||||
**Examples Count**: 36 files (29 good + 7 bad)
|
||||
@@ -0,0 +1,34 @@
|
||||
import { UserService } from "./UserService"
|
||||
|
||||
/**
|
||||
* BAD EXAMPLE: Circular Dependency (part 2)
|
||||
*
|
||||
* OrderService -> UserService (creates cycle)
|
||||
*/
|
||||
export class OrderService {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
public getOrdersByUser(userId: string): void {
|
||||
console.warn(`Getting orders for user ${userId}`)
|
||||
}
|
||||
|
||||
public calculateUserDiscount(userId: string): number {
|
||||
const totalSpent = this.userService.getUserTotalSpent(userId)
|
||||
return totalSpent > 1000 ? 0.1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ GOOD VERSION:
|
||||
*
|
||||
* // interfaces/IOrderService.ts
|
||||
* export interface IOrderService {
|
||||
* getOrdersByUser(userId: string): Promise<Order[]>
|
||||
* }
|
||||
*
|
||||
* // UserService.ts
|
||||
* constructor(private readonly orderService: IOrderService) {}
|
||||
*
|
||||
* // OrderService.ts - no dependency on UserService
|
||||
* // Use domain events or separate service for discount logic
|
||||
*/
|
||||
@@ -0,0 +1,34 @@
|
||||
import { OrderService } from "./OrderService"
|
||||
|
||||
/**
|
||||
* BAD EXAMPLE: Circular Dependency
|
||||
*
|
||||
* UserService -> OrderService -> UserService
|
||||
*
|
||||
* Guardian should detect:
|
||||
* ❌ Circular dependency cycle
|
||||
*
|
||||
* Why bad:
|
||||
* - Tight coupling
|
||||
* - Hard to test
|
||||
* - Difficult to understand
|
||||
* - Can cause initialization issues
|
||||
* - Maintenance nightmare
|
||||
*
|
||||
* Fix:
|
||||
* - Use interfaces
|
||||
* - Use domain events
|
||||
* - Extract shared logic to third service
|
||||
*/
|
||||
export class UserService {
|
||||
constructor(private readonly orderService: OrderService) {}
|
||||
|
||||
public getUserOrders(userId: string): void {
|
||||
console.warn(`Getting orders for user ${userId}`)
|
||||
this.orderService.getOrdersByUser(userId)
|
||||
}
|
||||
|
||||
public getUserTotalSpent(userId: string): number {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* BAD EXAMPLE: Entity Exposure
|
||||
*
|
||||
* Guardian should detect:
|
||||
* ❌ Domain entity returned from controller
|
||||
* ❌ No DTO layer
|
||||
*
|
||||
* Why bad:
|
||||
* - Exposes internal structure
|
||||
* - Breaking changes propagate to API
|
||||
* - Can't version API independently
|
||||
* - Security risk (password fields, etc.)
|
||||
* - Violates Clean Architecture
|
||||
*/
|
||||
|
||||
class User {
|
||||
constructor(
|
||||
public id: string,
|
||||
public email: string,
|
||||
public passwordHash: string,
|
||||
public isAdmin: boolean,
|
||||
) {}
|
||||
}
|
||||
|
||||
export class BadUserController {
|
||||
/**
|
||||
* ❌ BAD: Returning domain entity directly!
|
||||
*/
|
||||
public async getUser(id: string): Promise<User> {
|
||||
return new User(id, "user@example.com", "hashed_password_exposed!", true)
|
||||
}
|
||||
|
||||
/**
|
||||
* ❌ BAD: Accepting domain entity as input!
|
||||
*/
|
||||
public async updateUser(user: User): Promise<User> {
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ GOOD VERSION:
|
||||
*
|
||||
* // application/dtos/UserResponseDto.ts
|
||||
* export interface UserResponseDto {
|
||||
* readonly id: string
|
||||
* readonly email: string
|
||||
* // NO password, NO internal fields
|
||||
* }
|
||||
*
|
||||
* // infrastructure/controllers/UserController.ts
|
||||
* export class UserController {
|
||||
* async getUser(id: string): Promise<UserResponseDto> {
|
||||
* const user = await this.getUserUseCase.execute(id)
|
||||
* return UserMapper.toDto(user) // Convert to DTO!
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* BAD EXAMPLE: Framework Leak in Domain Layer
|
||||
*
|
||||
* Guardian should detect:
|
||||
* ❌ Prisma import in domain layer
|
||||
* ❌ Framework dependency in domain
|
||||
*
|
||||
* Why bad:
|
||||
* - Domain coupled to infrastructure
|
||||
* - Hard to test
|
||||
* - Can't change DB without changing domain
|
||||
* - Violates Dependency Inversion Principle
|
||||
* - Violates Clean Architecture
|
||||
*/
|
||||
|
||||
// ❌ BAD: Framework in domain!
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
export class UserEntity {
|
||||
constructor(
|
||||
public id: string,
|
||||
public email: string,
|
||||
private readonly prisma: PrismaClient,
|
||||
) {}
|
||||
|
||||
public async save(): Promise<void> {
|
||||
await this.prisma.user.create({
|
||||
data: {
|
||||
id: this.id,
|
||||
email: this.email,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ GOOD VERSION:
|
||||
*
|
||||
* // domain/entities/User.ts - NO framework imports!
|
||||
* export class User {
|
||||
* constructor(
|
||||
* private readonly id: UserId,
|
||||
* private readonly email: Email,
|
||||
* ) {}
|
||||
*
|
||||
* // No persistence logic here
|
||||
* }
|
||||
*
|
||||
* // domain/repositories/IUserRepository.ts
|
||||
* export interface IUserRepository {
|
||||
* save(user: User): Promise<void>
|
||||
* }
|
||||
*
|
||||
* // infrastructure/repositories/PrismaUserRepository.ts
|
||||
* export class PrismaUserRepository implements IUserRepository {
|
||||
* constructor(private readonly prisma: PrismaClient) {}
|
||||
*
|
||||
* async save(user: User): Promise<void> {
|
||||
* // Prisma code here
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* BAD EXAMPLE: Hardcoded values
|
||||
*
|
||||
* Guardian should detect:
|
||||
* ❌ Magic number: 3000 (port)
|
||||
* ❌ Magic number: 5000 (timeout)
|
||||
* ❌ Magic number: 3 (max retries)
|
||||
* ❌ Magic string: "http://localhost:8080" (API URL)
|
||||
* ❌ Magic string: "mongodb://localhost:27017/mydb" (DB connection)
|
||||
*
|
||||
* Why bad:
|
||||
* - Hard to maintain
|
||||
* - Can't configure per environment
|
||||
* - Scattered across codebase
|
||||
* - No single source of truth
|
||||
*/
|
||||
|
||||
export class ServerWithMagicNumbers {
|
||||
public startServer(): void {
|
||||
console.warn("Starting server on port 3000")
|
||||
|
||||
setTimeout(() => {
|
||||
console.warn("Server timeout after 5000ms")
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
public connectToDatabase(): void {
|
||||
const connectionString = "mongodb://localhost:27017/mydb"
|
||||
console.warn(`Connecting to: ${connectionString}`)
|
||||
}
|
||||
|
||||
public async fetchDataWithRetry(): Promise<void> {
|
||||
const apiUrl = "http://localhost:8080"
|
||||
let attempts = 0
|
||||
const maxRetries = 3
|
||||
|
||||
while (attempts < maxRetries) {
|
||||
try {
|
||||
console.warn(`Fetching from ${apiUrl}`)
|
||||
break
|
||||
} catch (error) {
|
||||
attempts++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public configureRateLimits(): void {
|
||||
const requestsPerMinute = 100
|
||||
const burstLimit = 200
|
||||
const windowSizeSeconds = 60
|
||||
|
||||
console.warn(
|
||||
`Rate limits: ${requestsPerMinute} per ${windowSizeSeconds}s, burst: ${burstLimit}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ GOOD VERSION (for comparison):
|
||||
*
|
||||
* const DEFAULT_PORT = 3000
|
||||
* const TIMEOUT_MS = 5000
|
||||
* const MAX_RETRIES = 3
|
||||
* const API_BASE_URL = "http://localhost:8080"
|
||||
* const DB_CONNECTION_STRING = "mongodb://localhost:27017/mydb"
|
||||
* const REQUESTS_PER_MINUTE = 100
|
||||
*/
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* BAD EXAMPLE: DTO in Domain Layer
|
||||
*
|
||||
* Guardian should detect:
|
||||
* ❌ DTO in domain layer
|
||||
* ❌ DTOs belong in application or infrastructure
|
||||
*
|
||||
* Why bad:
|
||||
* - Domain should have entities and value objects
|
||||
* - DTOs are for external communication
|
||||
* - Violates layer responsibilities
|
||||
*/
|
||||
|
||||
export class UserDto {
|
||||
constructor(
|
||||
public id: string,
|
||||
public email: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ GOOD VERSION:
|
||||
*
|
||||
* // domain/entities/User.ts - Entity in domain
|
||||
* export class User { ... }
|
||||
*
|
||||
* // application/dtos/UserDto.ts - DTO in application
|
||||
* export interface UserDto { ... }
|
||||
*/
|
||||
29
packages/guardian/examples/bad-architecture/naming/user.ts
Normal file
29
packages/guardian/examples/bad-architecture/naming/user.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* BAD EXAMPLE: Naming Convention Violations
|
||||
*
|
||||
* Guardian should detect:
|
||||
* ❌ File name: user.ts (should be PascalCase: User.ts)
|
||||
* ❌ Location: application layer use case should start with verb
|
||||
*
|
||||
* Why bad:
|
||||
* - Inconsistent naming
|
||||
* - Hard to find files
|
||||
* - Not following Clean Architecture conventions
|
||||
*/
|
||||
|
||||
export class user {
|
||||
constructor(
|
||||
public id: string,
|
||||
public email: string,
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ GOOD VERSION:
|
||||
*
|
||||
* // domain/entities/User.ts - PascalCase entity
|
||||
* export class User { ... }
|
||||
*
|
||||
* // application/use-cases/CreateUser.ts - Verb+Noun
|
||||
* export class CreateUser { ... }
|
||||
*/
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Create User Request DTO
|
||||
*
|
||||
* Application Layer: Input DTO
|
||||
* - Validation at system boundary
|
||||
* - No domain logic
|
||||
* - API contract
|
||||
*/
|
||||
export interface CreateUserRequest {
|
||||
readonly email: string
|
||||
readonly firstName: string
|
||||
readonly lastName: string
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Order Response DTO
|
||||
*/
|
||||
export interface OrderItemDto {
|
||||
readonly id: string
|
||||
readonly productId: string
|
||||
readonly productName: string
|
||||
readonly price: number
|
||||
readonly currency: string
|
||||
readonly quantity: number
|
||||
readonly total: number
|
||||
}
|
||||
|
||||
export interface OrderResponseDto {
|
||||
readonly id: string
|
||||
readonly userId: string
|
||||
readonly items: OrderItemDto[]
|
||||
readonly status: string
|
||||
readonly subtotal: number
|
||||
readonly currency: string
|
||||
readonly createdAt: string
|
||||
readonly confirmedAt?: string
|
||||
readonly deliveredAt?: string
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* User Response DTO
|
||||
*
|
||||
* DDD Pattern: Data Transfer Object
|
||||
* - No business logic
|
||||
* - Presentation layer data structure
|
||||
* - Protects domain from external changes
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: only data transfer
|
||||
* - ISP: client-specific interface
|
||||
*
|
||||
* Clean Architecture:
|
||||
* - Application layer DTO
|
||||
* - Maps to/from domain
|
||||
* - API contracts
|
||||
*
|
||||
* Benefits:
|
||||
* - Domain entity isolation
|
||||
* - API versioning
|
||||
* - Client-specific data
|
||||
*/
|
||||
export interface UserResponseDto {
|
||||
readonly id: string
|
||||
readonly email: string
|
||||
readonly firstName: string
|
||||
readonly lastName: string
|
||||
readonly fullName: string
|
||||
readonly isActive: boolean
|
||||
readonly isBlocked: boolean
|
||||
readonly registeredAt: string
|
||||
readonly lastLoginAt?: string
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Order } from "../../domain/aggregates/Order"
|
||||
import { OrderItemDto, OrderResponseDto } from "../dtos/OrderResponseDto"
|
||||
|
||||
/**
|
||||
* Order Mapper
|
||||
*/
|
||||
export class OrderMapper {
|
||||
public static toDto(order: Order): OrderResponseDto {
|
||||
const total = order.calculateTotal()
|
||||
|
||||
return {
|
||||
id: order.orderId.value,
|
||||
userId: order.userId.value,
|
||||
items: order.items.map((item) => OrderMapper.toItemDto(item)),
|
||||
status: order.status.value,
|
||||
subtotal: total.amount,
|
||||
currency: total.currency,
|
||||
createdAt: order.createdAt.toISOString(),
|
||||
confirmedAt: order.confirmedAt?.toISOString(),
|
||||
deliveredAt: order.deliveredAt?.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
private static toItemDto(item: any): OrderItemDto {
|
||||
const total = item.calculateTotal()
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
price: item.price.amount,
|
||||
currency: item.price.currency,
|
||||
quantity: item.quantity,
|
||||
total: total.amount,
|
||||
}
|
||||
}
|
||||
|
||||
public static toDtoList(orders: Order[]): OrderResponseDto[] {
|
||||
return orders.map((order) => OrderMapper.toDto(order))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { User } from "../../domain/aggregates/User"
|
||||
import { UserResponseDto } from "../dtos/UserResponseDto"
|
||||
|
||||
/**
|
||||
* User Mapper
|
||||
*
|
||||
* DDD Pattern: Mapper
|
||||
* - Converts between domain and DTOs
|
||||
* - Isolates domain from presentation
|
||||
* - No business logic
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: only mapping
|
||||
* - OCP: extend for new DTOs
|
||||
*
|
||||
* Clean Architecture:
|
||||
* - Application layer
|
||||
* - Protects domain integrity
|
||||
*/
|
||||
export class UserMapper {
|
||||
/**
|
||||
* Map domain entity to response DTO
|
||||
*/
|
||||
public static toDto(user: User): UserResponseDto {
|
||||
return {
|
||||
id: user.userId.value,
|
||||
email: user.email.value,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
fullName: user.fullName,
|
||||
isActive: user.isActive,
|
||||
isBlocked: user.isBlocked,
|
||||
registeredAt: user.registeredAt.toISOString(),
|
||||
lastLoginAt: user.lastLoginAt?.toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map array of entities to DTOs
|
||||
*/
|
||||
public static toDtoList(users: User[]): UserResponseDto[] {
|
||||
return users.map((user) => UserMapper.toDto(user))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Email } from "../../domain/value-objects/Email"
|
||||
import { UserRegistrationService } from "../../domain/services/UserRegistrationService"
|
||||
import { UserMapper } from "../mappers/UserMapper"
|
||||
import { CreateUserRequest } from "../dtos/CreateUserRequest"
|
||||
import { UserResponseDto } from "../dtos/UserResponseDto"
|
||||
|
||||
/**
|
||||
* Use Case: CreateUser
|
||||
*
|
||||
* DDD Pattern: Application Service / Use Case
|
||||
* - Orchestrates domain operations
|
||||
* - Transaction boundary
|
||||
* - Converts DTOs to domain
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: handles user creation workflow
|
||||
* - DIP: depends on abstractions (UserRegistrationService)
|
||||
* - OCP: can extend without modifying
|
||||
*
|
||||
* Clean Architecture:
|
||||
* - Application layer
|
||||
* - Uses domain services
|
||||
* - Returns DTOs (not domain entities)
|
||||
*
|
||||
* Clean Code:
|
||||
* - Verb+Noun naming: CreateUser
|
||||
* - Single purpose
|
||||
* - No business logic (delegated to domain)
|
||||
*/
|
||||
export class CreateUser {
|
||||
constructor(private readonly userRegistrationService: UserRegistrationService) {}
|
||||
|
||||
public async execute(request: CreateUserRequest): Promise<UserResponseDto> {
|
||||
this.validateRequest(request)
|
||||
|
||||
const email = Email.create(request.email)
|
||||
|
||||
const user = await this.userRegistrationService.registerUser(
|
||||
email,
|
||||
request.firstName,
|
||||
request.lastName,
|
||||
)
|
||||
|
||||
return UserMapper.toDto(user)
|
||||
}
|
||||
|
||||
private validateRequest(request: CreateUserRequest): void {
|
||||
if (!request.email?.trim()) {
|
||||
throw new Error("Email is required")
|
||||
}
|
||||
|
||||
if (!request.firstName?.trim()) {
|
||||
throw new Error("First name is required")
|
||||
}
|
||||
|
||||
if (!request.lastName?.trim()) {
|
||||
throw new Error("Last name is required")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { OrderFactory } from "../../domain/factories/OrderFactory"
|
||||
import { IOrderRepository } from "../../domain/repositories/IOrderRepository"
|
||||
import { UserId } from "../../domain/value-objects/UserId"
|
||||
import { Money } from "../../domain/value-objects/Money"
|
||||
import { OrderMapper } from "../mappers/OrderMapper"
|
||||
import { OrderResponseDto } from "../dtos/OrderResponseDto"
|
||||
|
||||
/**
|
||||
* Place Order Request
|
||||
*/
|
||||
export interface PlaceOrderRequest {
|
||||
readonly userId: string
|
||||
readonly items: Array<{
|
||||
readonly productId: string
|
||||
readonly productName: string
|
||||
readonly price: number
|
||||
readonly currency: string
|
||||
readonly quantity: number
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Case: PlaceOrder
|
||||
*
|
||||
* Application Service:
|
||||
* - Orchestrates order placement
|
||||
* - Transaction boundary
|
||||
* - Validation at system boundary
|
||||
*
|
||||
* Business Flow:
|
||||
* 1. Validate request
|
||||
* 2. Create order with items
|
||||
* 3. Confirm order
|
||||
* 4. Persist order
|
||||
* 5. Return DTO
|
||||
*/
|
||||
export class PlaceOrder {
|
||||
constructor(private readonly orderRepository: IOrderRepository) {}
|
||||
|
||||
public async execute(request: PlaceOrderRequest): Promise<OrderResponseDto> {
|
||||
this.validateRequest(request)
|
||||
|
||||
const userId = UserId.create(request.userId)
|
||||
|
||||
const items = request.items.map((item) => ({
|
||||
productId: item.productId,
|
||||
productName: item.productName,
|
||||
price: Money.create(item.price, item.currency),
|
||||
quantity: item.quantity,
|
||||
}))
|
||||
|
||||
const order = OrderFactory.createWithItems(userId, items)
|
||||
|
||||
order.confirm()
|
||||
|
||||
await this.orderRepository.save(order)
|
||||
|
||||
return OrderMapper.toDto(order)
|
||||
}
|
||||
|
||||
private validateRequest(request: PlaceOrderRequest): void {
|
||||
if (!request.userId?.trim()) {
|
||||
throw new Error("User ID is required")
|
||||
}
|
||||
|
||||
if (!request.items || request.items.length === 0) {
|
||||
throw new Error("Order must have at least one item")
|
||||
}
|
||||
|
||||
for (const item of request.items) {
|
||||
if (!item.productId?.trim()) {
|
||||
throw new Error("Product ID is required")
|
||||
}
|
||||
|
||||
if (!item.productName?.trim()) {
|
||||
throw new Error("Product name is required")
|
||||
}
|
||||
|
||||
if (item.price <= 0) {
|
||||
throw new Error("Price must be positive")
|
||||
}
|
||||
|
||||
if (item.quantity <= 0) {
|
||||
throw new Error("Quantity must be positive")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
import { BaseEntity } from "../../../../src/domain/entities/BaseEntity"
|
||||
import { OrderId } from "../value-objects/OrderId"
|
||||
import { UserId } from "../value-objects/UserId"
|
||||
import { OrderStatus } from "../value-objects/OrderStatus"
|
||||
import { Money } from "../value-objects/Money"
|
||||
import { OrderItem } from "../entities/OrderItem"
|
||||
|
||||
/**
|
||||
* Order Aggregate Root
|
||||
*
|
||||
* DDD Patterns:
|
||||
* - Aggregate Root: controls access to OrderItems
|
||||
* - Consistency Boundary: all changes through Order
|
||||
* - Rich Domain Model: contains business logic
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: manages order lifecycle
|
||||
* - OCP: extensible through status transitions
|
||||
* - ISP: focused interface for order operations
|
||||
*
|
||||
* Business Rules (Invariants):
|
||||
* - Order must have at least one item
|
||||
* - Cannot modify confirmed/paid/shipped orders
|
||||
* - Status transitions must be valid
|
||||
* - Total = sum of all items
|
||||
* - Cannot cancel delivered orders
|
||||
*
|
||||
* Clean Code:
|
||||
* - No magic numbers: MIN_ITEMS constant
|
||||
* - Meaningful names: addItem, removeItem, confirm
|
||||
* - Small methods: each does one thing
|
||||
* - No hardcoded strings: OrderStatus enum
|
||||
*/
|
||||
export class Order extends BaseEntity {
|
||||
private static readonly MIN_ITEMS = 1
|
||||
|
||||
private readonly _orderId: OrderId
|
||||
private readonly _userId: UserId
|
||||
private readonly _items: Map<string, OrderItem>
|
||||
private _status: OrderStatus
|
||||
private readonly _createdAt: Date
|
||||
private _confirmedAt?: Date
|
||||
private _deliveredAt?: Date
|
||||
|
||||
private constructor(
|
||||
orderId: OrderId,
|
||||
userId: UserId,
|
||||
items: OrderItem[],
|
||||
status: OrderStatus,
|
||||
createdAt: Date,
|
||||
confirmedAt?: Date,
|
||||
deliveredAt?: Date,
|
||||
) {
|
||||
super(orderId.value)
|
||||
this._orderId = orderId
|
||||
this._userId = userId
|
||||
this._items = new Map(items.map((item) => [item.id, item]))
|
||||
this._status = status
|
||||
this._createdAt = createdAt
|
||||
this._confirmedAt = confirmedAt
|
||||
this._deliveredAt = deliveredAt
|
||||
|
||||
this.validateInvariants()
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: Create new order
|
||||
*/
|
||||
public static create(userId: UserId): Order {
|
||||
const orderId = OrderId.create()
|
||||
const now = new Date()
|
||||
|
||||
return new Order(orderId, userId, [], OrderStatus.PENDING, now)
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory: Reconstitute from persistence
|
||||
*/
|
||||
public static reconstitute(
|
||||
orderId: OrderId,
|
||||
userId: UserId,
|
||||
items: OrderItem[],
|
||||
status: OrderStatus,
|
||||
createdAt: Date,
|
||||
confirmedAt?: Date,
|
||||
deliveredAt?: Date,
|
||||
): Order {
|
||||
return new Order(orderId, userId, items, status, createdAt, confirmedAt, deliveredAt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Add item to order
|
||||
*
|
||||
* DDD: Only Aggregate Root can modify its entities
|
||||
*/
|
||||
public addItem(productId: string, productName: string, price: Money, quantity: number): void {
|
||||
this.ensureCanModify()
|
||||
|
||||
const existingItem = Array.from(this._items.values()).find(
|
||||
(item) => item.productId === productId,
|
||||
)
|
||||
|
||||
if (existingItem) {
|
||||
existingItem.updateQuantity(existingItem.quantity + quantity)
|
||||
} else {
|
||||
const newItem = OrderItem.create(productId, productName, price, quantity)
|
||||
this._items.set(newItem.id, newItem)
|
||||
}
|
||||
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Remove item from order
|
||||
*/
|
||||
public removeItem(itemId: string): void {
|
||||
this.ensureCanModify()
|
||||
|
||||
if (!this._items.has(itemId)) {
|
||||
throw new Error(`Item not found: ${itemId}`)
|
||||
}
|
||||
|
||||
this._items.delete(itemId)
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Update item quantity
|
||||
*/
|
||||
public updateItemQuantity(itemId: string, newQuantity: number): void {
|
||||
this.ensureCanModify()
|
||||
|
||||
const item = this._items.get(itemId)
|
||||
|
||||
if (!item) {
|
||||
throw new Error(`Item not found: ${itemId}`)
|
||||
}
|
||||
|
||||
item.updateQuantity(newQuantity)
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Confirm order
|
||||
*/
|
||||
public confirm(): void {
|
||||
this.transitionTo(OrderStatus.CONFIRMED)
|
||||
this._confirmedAt = new Date()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Mark as paid
|
||||
*/
|
||||
public markAsPaid(): void {
|
||||
this.transitionTo(OrderStatus.PAID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Ship order
|
||||
*/
|
||||
public ship(): void {
|
||||
this.transitionTo(OrderStatus.SHIPPED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Deliver order
|
||||
*/
|
||||
public deliver(): void {
|
||||
this.transitionTo(OrderStatus.DELIVERED)
|
||||
this._deliveredAt = new Date()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Cancel order
|
||||
*/
|
||||
public cancel(): void {
|
||||
if (this._status.isDelivered()) {
|
||||
throw new Error("Cannot cancel delivered order")
|
||||
}
|
||||
|
||||
this.transitionTo(OrderStatus.CANCELLED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Query: Calculate total
|
||||
*/
|
||||
public calculateTotal(): Money {
|
||||
const items = Array.from(this._items.values())
|
||||
|
||||
if (items.length === 0) {
|
||||
return Money.zero("USD")
|
||||
}
|
||||
|
||||
return items.reduce((total, item) => total.add(item.calculateTotal()), Money.zero("USD"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Query: Check if order can be modified
|
||||
*/
|
||||
public canModify(): boolean {
|
||||
return this._status.isPending()
|
||||
}
|
||||
|
||||
/**
|
||||
* Getters
|
||||
*/
|
||||
public get orderId(): OrderId {
|
||||
return this._orderId
|
||||
}
|
||||
|
||||
public get userId(): UserId {
|
||||
return this._userId
|
||||
}
|
||||
|
||||
public get items(): readonly OrderItem[] {
|
||||
return Array.from(this._items.values())
|
||||
}
|
||||
|
||||
public get status(): OrderStatus {
|
||||
return this._status
|
||||
}
|
||||
|
||||
public get createdAt(): Date {
|
||||
return this._createdAt
|
||||
}
|
||||
|
||||
public get confirmedAt(): Date | undefined {
|
||||
return this._confirmedAt
|
||||
}
|
||||
|
||||
public get deliveredAt(): Date | undefined {
|
||||
return this._deliveredAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Private helpers
|
||||
*/
|
||||
private ensureCanModify(): void {
|
||||
if (!this.canModify()) {
|
||||
throw new Error(`Cannot modify order in ${this._status.value} status`)
|
||||
}
|
||||
}
|
||||
|
||||
private transitionTo(newStatus: OrderStatus): void {
|
||||
if (!this._status.canTransitionTo(newStatus)) {
|
||||
throw new Error(
|
||||
`Invalid status transition: ${this._status.value} -> ${newStatus.value}`,
|
||||
)
|
||||
}
|
||||
|
||||
this._status = newStatus
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Invariant validation
|
||||
*/
|
||||
private validateInvariants(): void {
|
||||
if (!this._status.isPending() && this._items.size < Order.MIN_ITEMS) {
|
||||
throw new Error(`Order must have at least ${Order.MIN_ITEMS} item(s)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
import { BaseEntity } from "../../../../src/domain/entities/BaseEntity"
|
||||
import { Email } from "../value-objects/Email"
|
||||
import { UserId } from "../value-objects/UserId"
|
||||
import { UserCreatedEvent } from "../events/UserCreatedEvent"
|
||||
|
||||
/**
|
||||
* User Aggregate Root
|
||||
*
|
||||
* DDD Patterns:
|
||||
* - Aggregate Root: consistency boundary
|
||||
* - Rich Domain Model: contains business logic
|
||||
* - Domain Events: publishes UserCreatedEvent
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: manages user identity and state
|
||||
* - OCP: extensible through events
|
||||
* - DIP: depends on abstractions (Email, UserId)
|
||||
*
|
||||
* Business Rules (Invariants):
|
||||
* - Email must be unique (enforced by repository)
|
||||
* - User must have valid email
|
||||
* - Blocked users cannot be activated directly
|
||||
* - Only active users can be blocked
|
||||
*/
|
||||
export class User extends BaseEntity {
|
||||
private readonly _userId: UserId
|
||||
private readonly _email: Email
|
||||
private readonly _firstName: string
|
||||
private readonly _lastName: string
|
||||
private _isActive: boolean
|
||||
private _isBlocked: boolean
|
||||
private readonly _registeredAt: Date
|
||||
private _lastLoginAt?: Date
|
||||
|
||||
private constructor(
|
||||
userId: UserId,
|
||||
email: Email,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
isActive: boolean,
|
||||
isBlocked: boolean,
|
||||
registeredAt: Date,
|
||||
lastLoginAt?: Date,
|
||||
) {
|
||||
super(userId.value)
|
||||
this._userId = userId
|
||||
this._email = email
|
||||
this._firstName = firstName
|
||||
this._lastName = lastName
|
||||
this._isActive = isActive
|
||||
this._isBlocked = isBlocked
|
||||
this._registeredAt = registeredAt
|
||||
this._lastLoginAt = lastLoginAt
|
||||
|
||||
this.validateInvariants()
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method: Create new user (business operation)
|
||||
*
|
||||
* DDD: Named constructor that represents business intent
|
||||
* Clean Code: Intention-revealing method name
|
||||
*/
|
||||
public static create(email: Email, firstName: string, lastName: string): User {
|
||||
const userId = UserId.create()
|
||||
const now = new Date()
|
||||
|
||||
const user = new User(userId, email, firstName, lastName, true, false, now)
|
||||
|
||||
user.addDomainEvent(
|
||||
new UserCreatedEvent({
|
||||
userId: userId.value,
|
||||
email: email.value,
|
||||
registeredAt: now,
|
||||
}),
|
||||
)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method: Reconstitute from persistence
|
||||
*
|
||||
* DDD: Separate creation from reconstitution
|
||||
* No events raised - already happened
|
||||
*/
|
||||
public static reconstitute(
|
||||
userId: UserId,
|
||||
email: Email,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
isActive: boolean,
|
||||
isBlocked: boolean,
|
||||
registeredAt: Date,
|
||||
lastLoginAt?: Date,
|
||||
): User {
|
||||
return new User(
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
isActive,
|
||||
isBlocked,
|
||||
registeredAt,
|
||||
lastLoginAt,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Activate user
|
||||
*
|
||||
* DDD: Business logic in domain
|
||||
* SOLID SRP: User manages its own state
|
||||
*/
|
||||
public activate(): void {
|
||||
if (this._isBlocked) {
|
||||
throw new Error("Cannot activate blocked user. Unblock first.")
|
||||
}
|
||||
|
||||
if (this._isActive) {
|
||||
return
|
||||
}
|
||||
|
||||
this._isActive = true
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Deactivate user
|
||||
*/
|
||||
public deactivate(): void {
|
||||
if (!this._isActive) {
|
||||
return
|
||||
}
|
||||
|
||||
this._isActive = false
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Block user
|
||||
*
|
||||
* Business Rule: Only active users can be blocked
|
||||
*/
|
||||
public block(reason: string): void {
|
||||
if (!this._isActive) {
|
||||
throw new Error("Cannot block inactive user")
|
||||
}
|
||||
|
||||
if (this._isBlocked) {
|
||||
return
|
||||
}
|
||||
|
||||
this._isBlocked = true
|
||||
this._isActive = false
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Unblock user
|
||||
*/
|
||||
public unblock(): void {
|
||||
if (!this._isBlocked) {
|
||||
return
|
||||
}
|
||||
|
||||
this._isBlocked = false
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Operation: Record login
|
||||
*/
|
||||
public recordLogin(): void {
|
||||
if (!this._isActive) {
|
||||
throw new Error("Inactive user cannot login")
|
||||
}
|
||||
|
||||
if (this._isBlocked) {
|
||||
throw new Error("Blocked user cannot login")
|
||||
}
|
||||
|
||||
this._lastLoginAt = new Date()
|
||||
this.touch()
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Query: Check if user can login
|
||||
*/
|
||||
public canLogin(): boolean {
|
||||
return this._isActive && !this._isBlocked
|
||||
}
|
||||
|
||||
/**
|
||||
* Getters: Read-only access to state
|
||||
*/
|
||||
public get userId(): UserId {
|
||||
return this._userId
|
||||
}
|
||||
|
||||
public get email(): Email {
|
||||
return this._email
|
||||
}
|
||||
|
||||
public get firstName(): string {
|
||||
return this._firstName
|
||||
}
|
||||
|
||||
public get lastName(): string {
|
||||
return this._lastName
|
||||
}
|
||||
|
||||
public get fullName(): string {
|
||||
return `${this._firstName} ${this._lastName}`
|
||||
}
|
||||
|
||||
public get isActive(): boolean {
|
||||
return this._isActive
|
||||
}
|
||||
|
||||
public get isBlocked(): boolean {
|
||||
return this._isBlocked
|
||||
}
|
||||
|
||||
public get registeredAt(): Date {
|
||||
return this._registeredAt
|
||||
}
|
||||
|
||||
public get lastLoginAt(): Date | undefined {
|
||||
return this._lastLoginAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Invariant validation
|
||||
*
|
||||
* DDD: Enforce business rules
|
||||
*/
|
||||
private validateInvariants(): void {
|
||||
if (!this._firstName?.trim()) {
|
||||
throw new Error("First name is required")
|
||||
}
|
||||
|
||||
if (!this._lastName?.trim()) {
|
||||
throw new Error("Last name is required")
|
||||
}
|
||||
|
||||
if (this._isBlocked && this._isActive) {
|
||||
throw new Error("Blocked user cannot be active")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { BaseEntity } from "../../../../src/domain/entities/BaseEntity"
|
||||
import { Money } from "../value-objects/Money"
|
||||
|
||||
/**
|
||||
* OrderItem Entity
|
||||
*
|
||||
* DDD Pattern: Entity (not Aggregate Root)
|
||||
* - Has identity
|
||||
* - Part of Order aggregate
|
||||
* - Cannot exist without Order
|
||||
* - Accessed only through Order
|
||||
*
|
||||
* Business Rules:
|
||||
* - Quantity must be positive
|
||||
* - Price must be positive
|
||||
* - Total = price * quantity
|
||||
*/
|
||||
export class OrderItem extends BaseEntity {
|
||||
private readonly _productId: string
|
||||
private readonly _productName: string
|
||||
private readonly _price: Money
|
||||
private _quantity: number
|
||||
|
||||
private constructor(
|
||||
productId: string,
|
||||
productName: string,
|
||||
price: Money,
|
||||
quantity: number,
|
||||
id?: string,
|
||||
) {
|
||||
super(id)
|
||||
this._productId = productId
|
||||
this._productName = productName
|
||||
this._price = price
|
||||
this._quantity = quantity
|
||||
|
||||
this.validateInvariants()
|
||||
}
|
||||
|
||||
public static create(
|
||||
productId: string,
|
||||
productName: string,
|
||||
price: Money,
|
||||
quantity: number,
|
||||
): OrderItem {
|
||||
return new OrderItem(productId, productName, price, quantity)
|
||||
}
|
||||
|
||||
public static reconstitute(
|
||||
productId: string,
|
||||
productName: string,
|
||||
price: Money,
|
||||
quantity: number,
|
||||
id: string,
|
||||
): OrderItem {
|
||||
return new OrderItem(productId, productName, price, quantity, id)
|
||||
}
|
||||
|
||||
public updateQuantity(newQuantity: number): void {
|
||||
if (newQuantity <= 0) {
|
||||
throw new Error("Quantity must be positive")
|
||||
}
|
||||
|
||||
this._quantity = newQuantity
|
||||
this.touch()
|
||||
}
|
||||
|
||||
public calculateTotal(): Money {
|
||||
return this._price.multiply(this._quantity)
|
||||
}
|
||||
|
||||
public get productId(): string {
|
||||
return this._productId
|
||||
}
|
||||
|
||||
public get productName(): string {
|
||||
return this._productName
|
||||
}
|
||||
|
||||
public get price(): Money {
|
||||
return this._price
|
||||
}
|
||||
|
||||
public get quantity(): number {
|
||||
return this._quantity
|
||||
}
|
||||
|
||||
private validateInvariants(): void {
|
||||
if (!this._productId?.trim()) {
|
||||
throw new Error("Product ID is required")
|
||||
}
|
||||
|
||||
if (!this._productName?.trim()) {
|
||||
throw new Error("Product name is required")
|
||||
}
|
||||
|
||||
if (this._quantity <= 0) {
|
||||
throw new Error("Quantity must be positive")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { DomainEvent } from "../../../../src/domain/events/DomainEvent"
|
||||
|
||||
/**
|
||||
* Domain Event: UserCreatedEvent
|
||||
*
|
||||
* DDD Pattern: Domain Events
|
||||
* - Represents something that happened in the domain
|
||||
* - Immutable
|
||||
* - Past tense naming
|
||||
*
|
||||
* Use cases:
|
||||
* - Send welcome email (async)
|
||||
* - Create user profile
|
||||
* - Log user registration
|
||||
* - Analytics tracking
|
||||
*/
|
||||
export interface UserCreatedEventPayload {
|
||||
readonly userId: string
|
||||
readonly email: string
|
||||
readonly registeredAt: Date
|
||||
}
|
||||
|
||||
export class UserCreatedEvent extends DomainEvent<UserCreatedEventPayload> {
|
||||
constructor(payload: UserCreatedEventPayload) {
|
||||
super("user.created", payload)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Order } from "../aggregates/Order"
|
||||
import { OrderId } from "../value-objects/OrderId"
|
||||
import { UserId } from "../value-objects/UserId"
|
||||
import { OrderStatus } from "../value-objects/OrderStatus"
|
||||
import { OrderItem } from "../entities/OrderItem"
|
||||
import { Money } from "../value-objects/Money"
|
||||
|
||||
/**
|
||||
* Factory: OrderFactory
|
||||
*
|
||||
* DDD Pattern: Factory
|
||||
* - Handles complex Order creation
|
||||
* - Different creation scenarios
|
||||
* - Validation and defaults
|
||||
*
|
||||
* Clean Code:
|
||||
* - Each method has clear purpose
|
||||
* - No magic values
|
||||
* - Meaningful names
|
||||
*/
|
||||
export class OrderFactory {
|
||||
/**
|
||||
* Create empty order for user
|
||||
*/
|
||||
public static createEmptyOrder(userId: UserId): Order {
|
||||
return Order.create(userId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create order with initial items
|
||||
*/
|
||||
public static createWithItems(
|
||||
userId: UserId,
|
||||
items: Array<{ productId: string; productName: string; price: Money; quantity: number }>,
|
||||
): Order {
|
||||
const order = Order.create(userId)
|
||||
|
||||
for (const item of items) {
|
||||
order.addItem(item.productId, item.productName, item.price, item.quantity)
|
||||
}
|
||||
|
||||
return order
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstitute order from persistence
|
||||
*/
|
||||
public static reconstitute(data: {
|
||||
orderId: string
|
||||
userId: string
|
||||
items: Array<{
|
||||
id: string
|
||||
productId: string
|
||||
productName: string
|
||||
price: number
|
||||
currency: string
|
||||
quantity: number
|
||||
}>
|
||||
status: string
|
||||
createdAt: Date
|
||||
confirmedAt?: Date
|
||||
deliveredAt?: Date
|
||||
}): Order {
|
||||
const orderId = OrderId.create(data.orderId)
|
||||
const userId = UserId.create(data.userId)
|
||||
const status = OrderStatus.create(data.status)
|
||||
|
||||
const items = data.items.map((item) =>
|
||||
OrderItem.reconstitute(
|
||||
item.productId,
|
||||
item.productName,
|
||||
Money.create(item.price, item.currency),
|
||||
item.quantity,
|
||||
item.id,
|
||||
),
|
||||
)
|
||||
|
||||
return Order.reconstitute(
|
||||
orderId,
|
||||
userId,
|
||||
items,
|
||||
status,
|
||||
data.createdAt,
|
||||
data.confirmedAt,
|
||||
data.deliveredAt,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test order
|
||||
*/
|
||||
public static createTestOrder(userId?: UserId): Order {
|
||||
const testUserId = userId ?? UserId.create()
|
||||
const order = Order.create(testUserId)
|
||||
|
||||
order.addItem("test-product-1", "Test Product 1", Money.create(10, "USD"), 2)
|
||||
|
||||
order.addItem("test-product-2", "Test Product 2", Money.create(20, "USD"), 1)
|
||||
|
||||
return order
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { User } from "../aggregates/User"
|
||||
import { Email } from "../value-objects/Email"
|
||||
import { UserId } from "../value-objects/UserId"
|
||||
|
||||
/**
|
||||
* Factory: UserFactory
|
||||
*
|
||||
* DDD Pattern: Factory
|
||||
* - Encapsulates complex object creation
|
||||
* - Hides construction details
|
||||
* - Can create from different sources
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: responsible only for creating Users
|
||||
* - OCP: can add new creation methods
|
||||
* - DIP: returns domain object, not DTO
|
||||
*
|
||||
* Use cases:
|
||||
* - Create from external auth provider (OAuth, SAML)
|
||||
* - Create from legacy data
|
||||
* - Create with default values
|
||||
* - Create test users
|
||||
*/
|
||||
export class UserFactory {
|
||||
/**
|
||||
* Create user from OAuth provider data
|
||||
*/
|
||||
public static createFromOAuth(
|
||||
oauthEmail: string,
|
||||
oauthFirstName: string,
|
||||
oauthLastName: string,
|
||||
): User {
|
||||
const email = Email.create(oauthEmail)
|
||||
|
||||
const firstName = oauthFirstName.trim() || "Unknown"
|
||||
const lastName = oauthLastName.trim() || "User"
|
||||
|
||||
return User.create(email, firstName, lastName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user from legacy database format
|
||||
*/
|
||||
public static createFromLegacy(legacyData: {
|
||||
id: string
|
||||
email: string
|
||||
full_name: string
|
||||
active: number
|
||||
created_timestamp: number
|
||||
}): User {
|
||||
const [firstName = "Unknown", lastName = "User"] = legacyData.full_name.split(" ")
|
||||
|
||||
const userId = UserId.create(legacyData.id)
|
||||
const email = Email.create(legacyData.email)
|
||||
const isActive = legacyData.active === 1
|
||||
const registeredAt = new Date(legacyData.created_timestamp * 1000)
|
||||
|
||||
return User.reconstitute(userId, email, firstName, lastName, isActive, false, registeredAt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create test user with defaults
|
||||
*/
|
||||
public static createTestUser(emailSuffix: string = "test"): User {
|
||||
const email = Email.create(`test-${Date.now()}@${emailSuffix}.com`)
|
||||
return User.create(email, "Test", "User")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create admin user
|
||||
*/
|
||||
public static createAdmin(email: Email, firstName: string, lastName: string): User {
|
||||
const user = User.create(email, firstName, lastName)
|
||||
user.activate()
|
||||
return user
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Order } from "../aggregates/Order"
|
||||
import { OrderId } from "../value-objects/OrderId"
|
||||
import { UserId } from "../value-objects/UserId"
|
||||
|
||||
/**
|
||||
* Order Repository Interface
|
||||
*
|
||||
* DDD Pattern: Repository
|
||||
* - Aggregate-oriented persistence
|
||||
* - Collection metaphor
|
||||
* - No business logic (that's in Order aggregate)
|
||||
*/
|
||||
export interface IOrderRepository {
|
||||
/**
|
||||
* Save order (create or update)
|
||||
*/
|
||||
save(order: Order): Promise<void>
|
||||
|
||||
/**
|
||||
* Find order by ID
|
||||
*/
|
||||
findById(id: OrderId): Promise<Order | null>
|
||||
|
||||
/**
|
||||
* Find orders by user
|
||||
*/
|
||||
findByUserId(userId: UserId): Promise<Order[]>
|
||||
|
||||
/**
|
||||
* Find orders by status
|
||||
*/
|
||||
findByStatus(status: string): Promise<Order[]>
|
||||
|
||||
/**
|
||||
* Find all orders
|
||||
*/
|
||||
findAll(): Promise<Order[]>
|
||||
|
||||
/**
|
||||
* Delete order
|
||||
*/
|
||||
delete(id: OrderId): Promise<void>
|
||||
|
||||
/**
|
||||
* Check if order exists
|
||||
*/
|
||||
exists(id: OrderId): Promise<boolean>
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { User } from "../aggregates/User"
|
||||
import { UserId } from "../value-objects/UserId"
|
||||
import { Email } from "../value-objects/Email"
|
||||
|
||||
/**
|
||||
* User Repository Interface
|
||||
*
|
||||
* DDD Pattern: Repository
|
||||
* - Interface in domain layer
|
||||
* - Implementation in infrastructure layer
|
||||
* - Collection-like API for aggregates
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - DIP: domain depends on abstraction
|
||||
* - ISP: focused interface
|
||||
* - SRP: manages User persistence
|
||||
*
|
||||
* Clean Architecture:
|
||||
* - Domain doesn't know about DB
|
||||
* - Infrastructure implements this
|
||||
*/
|
||||
export interface IUserRepository {
|
||||
/**
|
||||
* Save user (create or update)
|
||||
*/
|
||||
save(user: User): Promise<void>
|
||||
|
||||
/**
|
||||
* Find user by ID
|
||||
*/
|
||||
findById(id: UserId): Promise<User | null>
|
||||
|
||||
/**
|
||||
* Find user by email
|
||||
*/
|
||||
findByEmail(email: Email): Promise<User | null>
|
||||
|
||||
/**
|
||||
* Find all users
|
||||
*/
|
||||
findAll(): Promise<User[]>
|
||||
|
||||
/**
|
||||
* Find active users
|
||||
*/
|
||||
findActive(): Promise<User[]>
|
||||
|
||||
/**
|
||||
* Delete user
|
||||
*/
|
||||
delete(id: UserId): Promise<void>
|
||||
|
||||
/**
|
||||
* Check if user exists
|
||||
*/
|
||||
exists(id: UserId): Promise<boolean>
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Order } from "../aggregates/Order"
|
||||
import { Money } from "../value-objects/Money"
|
||||
|
||||
/**
|
||||
* Domain Service: PricingService
|
||||
*
|
||||
* DDD Pattern: Domain Service
|
||||
* - Encapsulates pricing business logic
|
||||
* - Pure business logic (no infrastructure)
|
||||
* - Can be used by multiple aggregates
|
||||
*
|
||||
* Business Rules:
|
||||
* - Discounts based on order total
|
||||
* - Free shipping threshold
|
||||
* - Tax calculation
|
||||
*
|
||||
* Clean Code:
|
||||
* - No magic numbers: constants for thresholds
|
||||
* - Clear method names
|
||||
* - Single Responsibility
|
||||
*/
|
||||
export class PricingService {
|
||||
private static readonly DISCOUNT_THRESHOLD = Money.create(100, "USD")
|
||||
private static readonly DISCOUNT_PERCENTAGE = 0.1
|
||||
private static readonly FREE_SHIPPING_THRESHOLD = Money.create(50, "USD")
|
||||
private static readonly SHIPPING_COST = Money.create(10, "USD")
|
||||
private static readonly TAX_RATE = 0.2
|
||||
|
||||
/**
|
||||
* Calculate discount for order
|
||||
*
|
||||
* Business Rule: 10% discount for orders over $100
|
||||
*/
|
||||
public calculateDiscount(order: Order): Money {
|
||||
const total = order.calculateTotal()
|
||||
|
||||
if (total.isGreaterThan(PricingService.DISCOUNT_THRESHOLD)) {
|
||||
return total.multiply(PricingService.DISCOUNT_PERCENTAGE)
|
||||
}
|
||||
|
||||
return Money.zero(total.currency)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate shipping cost
|
||||
*
|
||||
* Business Rule: Free shipping for orders over $50
|
||||
*/
|
||||
public calculateShippingCost(order: Order): Money {
|
||||
const total = order.calculateTotal()
|
||||
|
||||
if (total.isGreaterThan(PricingService.FREE_SHIPPING_THRESHOLD)) {
|
||||
return Money.zero(total.currency)
|
||||
}
|
||||
|
||||
return PricingService.SHIPPING_COST
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate tax
|
||||
*
|
||||
* Business Rule: 20% tax on order total
|
||||
*/
|
||||
public calculateTax(order: Order): Money {
|
||||
const total = order.calculateTotal()
|
||||
return total.multiply(PricingService.TAX_RATE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate final total with all costs
|
||||
*/
|
||||
public calculateFinalTotal(order: Order): Money {
|
||||
const subtotal = order.calculateTotal()
|
||||
const discount = this.calculateDiscount(order)
|
||||
const shipping = this.calculateShippingCost(order)
|
||||
const tax = this.calculateTax(order)
|
||||
|
||||
return subtotal.subtract(discount).add(shipping).add(tax)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { User } from "../aggregates/User"
|
||||
import { Email } from "../value-objects/Email"
|
||||
import { IUserRepository } from "../repositories/IUserRepository"
|
||||
|
||||
/**
|
||||
* Domain Service: UserRegistrationService
|
||||
*
|
||||
* DDD Pattern: Domain Service
|
||||
* - Encapsulates business logic that doesn't belong to a single entity
|
||||
* - Coordinates multiple aggregates
|
||||
* - Stateless
|
||||
*
|
||||
* When to use Domain Service:
|
||||
* - Business logic spans multiple aggregates
|
||||
* - Operation doesn't naturally fit in any entity
|
||||
* - Need to check uniqueness (requires repository)
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: handles user registration logic
|
||||
* - DIP: depends on IUserRepository abstraction
|
||||
* - ISP: focused interface
|
||||
*
|
||||
* Clean Code:
|
||||
* - Meaningful name: clearly registration logic
|
||||
* - Small method: does one thing
|
||||
* - No magic strings: clear error messages
|
||||
*/
|
||||
export class UserRegistrationService {
|
||||
constructor(private readonly userRepository: IUserRepository) {}
|
||||
|
||||
/**
|
||||
* Business Operation: Register new user
|
||||
*
|
||||
* Business Rules:
|
||||
* - Email must be unique
|
||||
* - User must have valid data
|
||||
* - Registration creates active user
|
||||
*
|
||||
* @throws Error if email already exists
|
||||
* @throws Error if user data is invalid
|
||||
*/
|
||||
public async registerUser(email: Email, firstName: string, lastName: string): Promise<User> {
|
||||
const existingUser = await this.userRepository.findByEmail(email)
|
||||
|
||||
if (existingUser) {
|
||||
throw new Error(`User with email ${email.value} already exists`)
|
||||
}
|
||||
|
||||
const user = User.create(email, firstName, lastName)
|
||||
|
||||
await this.userRepository.save(user)
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Query: Check if email is available
|
||||
*/
|
||||
public async isEmailAvailable(email: Email): boolean {
|
||||
const existingUser = await this.userRepository.findByEmail(email)
|
||||
return !existingUser
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Specification } from "./Specification"
|
||||
import { Email } from "../value-objects/Email"
|
||||
|
||||
/**
|
||||
* Email Domain Specification
|
||||
*
|
||||
* Business Rule: Check if email is from corporate domain
|
||||
*/
|
||||
export class CorporateEmailSpecification extends Specification<Email> {
|
||||
private static readonly CORPORATE_DOMAINS = ["company.com", "corp.company.com"]
|
||||
|
||||
public isSatisfiedBy(email: Email): boolean {
|
||||
const domain = email.getDomain()
|
||||
return CorporateEmailSpecification.CORPORATE_DOMAINS.includes(domain)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Email Blacklist Specification
|
||||
*
|
||||
* Business Rule: Check if email domain is blacklisted
|
||||
*/
|
||||
export class BlacklistedEmailSpecification extends Specification<Email> {
|
||||
private static readonly BLACKLISTED_DOMAINS = [
|
||||
"tempmail.com",
|
||||
"throwaway.email",
|
||||
"guerrillamail.com",
|
||||
]
|
||||
|
||||
public isSatisfiedBy(email: Email): boolean {
|
||||
const domain = email.getDomain()
|
||||
return BlacklistedEmailSpecification.BLACKLISTED_DOMAINS.includes(domain)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid Email for Registration
|
||||
*
|
||||
* Composed specification: not blacklisted
|
||||
*/
|
||||
export class ValidEmailForRegistrationSpecification extends Specification<Email> {
|
||||
private readonly notBlacklisted: Specification<Email>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.notBlacklisted = new BlacklistedEmailSpecification().not()
|
||||
}
|
||||
|
||||
public isSatisfiedBy(email: Email): boolean {
|
||||
return this.notBlacklisted.isSatisfiedBy(email)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Specification } from "./Specification"
|
||||
import { Order } from "../aggregates/Order"
|
||||
import { Money } from "../value-objects/Money"
|
||||
|
||||
/**
|
||||
* Order Can Be Cancelled Specification
|
||||
*
|
||||
* Business Rule: Order can be cancelled if not delivered
|
||||
*/
|
||||
export class OrderCanBeCancelledSpecification extends Specification<Order> {
|
||||
public isSatisfiedBy(order: Order): boolean {
|
||||
return !order.status.isDelivered()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Order Eligible For Discount Specification
|
||||
*
|
||||
* Business Rule: Orders over $100 get discount
|
||||
*/
|
||||
export class OrderEligibleForDiscountSpecification extends Specification<Order> {
|
||||
private static readonly DISCOUNT_THRESHOLD = Money.create(100, "USD")
|
||||
|
||||
public isSatisfiedBy(order: Order): boolean {
|
||||
const total = order.calculateTotal()
|
||||
return total.isGreaterThan(OrderEligibleForDiscountSpecification.DISCOUNT_THRESHOLD)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Order Eligible For Free Shipping Specification
|
||||
*
|
||||
* Business Rule: Orders over $50 get free shipping
|
||||
*/
|
||||
export class OrderEligibleForFreeShippingSpecification extends Specification<Order> {
|
||||
private static readonly FREE_SHIPPING_THRESHOLD = Money.create(50, "USD")
|
||||
|
||||
public isSatisfiedBy(order: Order): boolean {
|
||||
const total = order.calculateTotal()
|
||||
return total.isGreaterThan(
|
||||
OrderEligibleForFreeShippingSpecification.FREE_SHIPPING_THRESHOLD,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* High Value Order Specification
|
||||
*
|
||||
* Business Rule: Orders over $500 are high value
|
||||
* (might need special handling, insurance, etc.)
|
||||
*/
|
||||
export class HighValueOrderSpecification extends Specification<Order> {
|
||||
private static readonly HIGH_VALUE_THRESHOLD = Money.create(500, "USD")
|
||||
|
||||
public isSatisfiedBy(order: Order): boolean {
|
||||
const total = order.calculateTotal()
|
||||
return total.isGreaterThan(HighValueOrderSpecification.HIGH_VALUE_THRESHOLD)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Composed Specification: Premium Order
|
||||
*
|
||||
* Premium = High Value AND Eligible for Discount
|
||||
*/
|
||||
export class PremiumOrderSpecification extends Specification<Order> {
|
||||
private readonly spec: Specification<Order>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.spec = new HighValueOrderSpecification().and(
|
||||
new OrderEligibleForDiscountSpecification(),
|
||||
)
|
||||
}
|
||||
|
||||
public isSatisfiedBy(order: Order): boolean {
|
||||
return this.spec.isSatisfiedBy(order)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Specification Pattern (base class)
|
||||
*
|
||||
* DDD Pattern: Specification
|
||||
* - Encapsulates business rules
|
||||
* - Reusable predicates
|
||||
* - Combinable (AND, OR, NOT)
|
||||
* - Testable in isolation
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: each specification has one rule
|
||||
* - OCP: extend by creating new specifications
|
||||
* - LSP: all specifications are substitutable
|
||||
*
|
||||
* Benefits:
|
||||
* - Business rules as first-class citizens
|
||||
* - Reusable across use cases
|
||||
* - Easy to test
|
||||
* - Can be combined
|
||||
*/
|
||||
export abstract class Specification<T> {
|
||||
/**
|
||||
* Check if entity satisfies specification
|
||||
*/
|
||||
public abstract isSatisfiedBy(entity: T): boolean
|
||||
|
||||
/**
|
||||
* Combine specifications with AND
|
||||
*/
|
||||
public and(other: Specification<T>): Specification<T> {
|
||||
return new AndSpecification(this, other)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine specifications with OR
|
||||
*/
|
||||
public or(other: Specification<T>): Specification<T> {
|
||||
return new OrSpecification(this, other)
|
||||
}
|
||||
|
||||
/**
|
||||
* Negate specification
|
||||
*/
|
||||
public not(): Specification<T> {
|
||||
return new NotSpecification(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AND Specification
|
||||
*/
|
||||
class AndSpecification<T> extends Specification<T> {
|
||||
constructor(
|
||||
private readonly left: Specification<T>,
|
||||
private readonly right: Specification<T>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public isSatisfiedBy(entity: T): boolean {
|
||||
return this.left.isSatisfiedBy(entity) && this.right.isSatisfiedBy(entity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OR Specification
|
||||
*/
|
||||
class OrSpecification<T> extends Specification<T> {
|
||||
constructor(
|
||||
private readonly left: Specification<T>,
|
||||
private readonly right: Specification<T>,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
public isSatisfiedBy(entity: T): boolean {
|
||||
return this.left.isSatisfiedBy(entity) || this.right.isSatisfiedBy(entity)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NOT Specification
|
||||
*/
|
||||
class NotSpecification<T> extends Specification<T> {
|
||||
constructor(private readonly spec: Specification<T>) {
|
||||
super()
|
||||
}
|
||||
|
||||
public isSatisfiedBy(entity: T): boolean {
|
||||
return !this.spec.isSatisfiedBy(entity)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
|
||||
|
||||
interface EmailProps {
|
||||
readonly value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Email Value Object
|
||||
*
|
||||
* DDD Pattern: Value Object
|
||||
* - Immutable
|
||||
* - Self-validating
|
||||
* - No identity
|
||||
* - Equality by value
|
||||
*
|
||||
* Clean Code:
|
||||
* - Single Responsibility: represents email
|
||||
* - Meaningful name: clearly email
|
||||
* - No magic values: validation rules as constants
|
||||
*/
|
||||
export class Email extends ValueObject<EmailProps> {
|
||||
private static readonly EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
private static readonly MAX_LENGTH = 255
|
||||
|
||||
private constructor(props: EmailProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(email: string): Email {
|
||||
const trimmed = email.trim().toLowerCase()
|
||||
|
||||
if (!trimmed) {
|
||||
throw new Error("Email cannot be empty")
|
||||
}
|
||||
|
||||
if (trimmed.length > Email.MAX_LENGTH) {
|
||||
throw new Error(`Email must be less than ${Email.MAX_LENGTH} characters`)
|
||||
}
|
||||
|
||||
if (!Email.EMAIL_REGEX.test(trimmed)) {
|
||||
throw new Error(`Invalid email format: ${email}`)
|
||||
}
|
||||
|
||||
return new Email({ value: trimmed })
|
||||
}
|
||||
|
||||
public get value(): string {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
public getDomain(): string {
|
||||
return this.props.value.split("@")[1]
|
||||
}
|
||||
|
||||
public isFromDomain(domain: string): boolean {
|
||||
return this.getDomain() === domain.toLowerCase()
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.props.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
|
||||
|
||||
interface MoneyProps {
|
||||
readonly amount: number
|
||||
readonly currency: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Money Value Object
|
||||
*
|
||||
* DDD Pattern: Value Object
|
||||
* - Encapsulates amount + currency
|
||||
* - Immutable
|
||||
* - Rich behavior (add, subtract, compare)
|
||||
*
|
||||
* Prevents common bugs:
|
||||
* - Adding different currencies
|
||||
* - Negative amounts (when not allowed)
|
||||
* - Floating point precision issues
|
||||
*/
|
||||
export class Money extends ValueObject<MoneyProps> {
|
||||
private static readonly SUPPORTED_CURRENCIES = ["USD", "EUR", "GBP", "RUB"]
|
||||
private static readonly DECIMAL_PLACES = 2
|
||||
|
||||
private constructor(props: MoneyProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(amount: number, currency: string): Money {
|
||||
const upperCurrency = currency.toUpperCase()
|
||||
|
||||
if (!Money.SUPPORTED_CURRENCIES.includes(upperCurrency)) {
|
||||
throw new Error(
|
||||
`Unsupported currency: ${currency}. Supported: ${Money.SUPPORTED_CURRENCIES.join(", ")}`,
|
||||
)
|
||||
}
|
||||
|
||||
if (amount < 0) {
|
||||
throw new Error("Money amount cannot be negative")
|
||||
}
|
||||
|
||||
const rounded = Math.round(amount * 100) / 100
|
||||
|
||||
return new Money({ amount: rounded, currency: upperCurrency })
|
||||
}
|
||||
|
||||
public static zero(currency: string): Money {
|
||||
return Money.create(0, currency)
|
||||
}
|
||||
|
||||
public get amount(): number {
|
||||
return this.props.amount
|
||||
}
|
||||
|
||||
public get currency(): string {
|
||||
return this.props.currency
|
||||
}
|
||||
|
||||
public add(other: Money): Money {
|
||||
this.ensureSameCurrency(other)
|
||||
return Money.create(this.amount + other.amount, this.currency)
|
||||
}
|
||||
|
||||
public subtract(other: Money): Money {
|
||||
this.ensureSameCurrency(other)
|
||||
const result = this.amount - other.amount
|
||||
|
||||
if (result < 0) {
|
||||
throw new Error("Cannot subtract: result would be negative")
|
||||
}
|
||||
|
||||
return Money.create(result, this.currency)
|
||||
}
|
||||
|
||||
public multiply(multiplier: number): Money {
|
||||
if (multiplier < 0) {
|
||||
throw new Error("Multiplier cannot be negative")
|
||||
}
|
||||
return Money.create(this.amount * multiplier, this.currency)
|
||||
}
|
||||
|
||||
public isGreaterThan(other: Money): boolean {
|
||||
this.ensureSameCurrency(other)
|
||||
return this.amount > other.amount
|
||||
}
|
||||
|
||||
public isLessThan(other: Money): boolean {
|
||||
this.ensureSameCurrency(other)
|
||||
return this.amount < other.amount
|
||||
}
|
||||
|
||||
public isZero(): boolean {
|
||||
return this.amount === 0
|
||||
}
|
||||
|
||||
private ensureSameCurrency(other: Money): void {
|
||||
if (this.currency !== other.currency) {
|
||||
throw new Error(
|
||||
`Cannot operate on different currencies: ${this.currency} vs ${other.currency}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `${this.amount.toFixed(Money.DECIMAL_PLACES)} ${this.currency}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
|
||||
import { v4 as uuidv4, validate as uuidValidate } from "uuid"
|
||||
|
||||
interface OrderIdProps {
|
||||
readonly value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* OrderId Value Object
|
||||
*
|
||||
* Type safety: cannot mix with UserId
|
||||
*/
|
||||
export class OrderId extends ValueObject<OrderIdProps> {
|
||||
private constructor(props: OrderIdProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(id?: string): OrderId {
|
||||
const value = id ?? uuidv4()
|
||||
|
||||
if (!uuidValidate(value)) {
|
||||
throw new Error(`Invalid OrderId format: ${value}`)
|
||||
}
|
||||
|
||||
return new OrderId({ value })
|
||||
}
|
||||
|
||||
public get value(): string {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.props.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
|
||||
|
||||
interface OrderStatusProps {
|
||||
readonly value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* OrderStatus Value Object
|
||||
*
|
||||
* DDD Pattern: Enum as Value Object
|
||||
* - Type-safe status
|
||||
* - Business logic: valid transitions
|
||||
* - Self-validating
|
||||
*/
|
||||
export class OrderStatus extends ValueObject<OrderStatusProps> {
|
||||
public static readonly PENDING = new OrderStatus({ value: "pending" })
|
||||
public static readonly CONFIRMED = new OrderStatus({ value: "confirmed" })
|
||||
public static readonly PAID = new OrderStatus({ value: "paid" })
|
||||
public static readonly SHIPPED = new OrderStatus({ value: "shipped" })
|
||||
public static readonly DELIVERED = new OrderStatus({ value: "delivered" })
|
||||
public static readonly CANCELLED = new OrderStatus({ value: "cancelled" })
|
||||
|
||||
private static readonly VALID_STATUSES = [
|
||||
"pending",
|
||||
"confirmed",
|
||||
"paid",
|
||||
"shipped",
|
||||
"delivered",
|
||||
"cancelled",
|
||||
]
|
||||
|
||||
private constructor(props: OrderStatusProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(status: string): OrderStatus {
|
||||
const lower = status.toLowerCase()
|
||||
|
||||
if (!OrderStatus.VALID_STATUSES.includes(lower)) {
|
||||
throw new Error(
|
||||
`Invalid order status: ${status}. Valid: ${OrderStatus.VALID_STATUSES.join(", ")}`,
|
||||
)
|
||||
}
|
||||
|
||||
return new OrderStatus({ value: lower })
|
||||
}
|
||||
|
||||
public get value(): string {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Business Rule: Valid status transitions
|
||||
*/
|
||||
public canTransitionTo(newStatus: OrderStatus): boolean {
|
||||
const transitions: Record<string, string[]> = {
|
||||
pending: ["confirmed", "cancelled"],
|
||||
confirmed: ["paid", "cancelled"],
|
||||
paid: ["shipped", "cancelled"],
|
||||
shipped: ["delivered"],
|
||||
delivered: [],
|
||||
cancelled: [],
|
||||
}
|
||||
|
||||
const allowedTransitions = transitions[this.value] ?? []
|
||||
return allowedTransitions.includes(newStatus.value)
|
||||
}
|
||||
|
||||
public isPending(): boolean {
|
||||
return this.value === "pending"
|
||||
}
|
||||
|
||||
public isConfirmed(): boolean {
|
||||
return this.value === "confirmed"
|
||||
}
|
||||
|
||||
public isCancelled(): boolean {
|
||||
return this.value === "cancelled"
|
||||
}
|
||||
|
||||
public isDelivered(): boolean {
|
||||
return this.value === "delivered"
|
||||
}
|
||||
|
||||
public isFinal(): boolean {
|
||||
return this.isDelivered() || this.isCancelled()
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.props.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ValueObject } from "../../../../src/domain/value-objects/ValueObject"
|
||||
import { v4 as uuidv4, validate as uuidValidate } from "uuid"
|
||||
|
||||
interface UserIdProps {
|
||||
readonly value: string
|
||||
}
|
||||
|
||||
/**
|
||||
* UserId Value Object
|
||||
*
|
||||
* DDD Pattern: Identity Value Object
|
||||
* - Strongly typed ID (not just string)
|
||||
* - Self-validating
|
||||
* - Type safety: can't mix with OrderId
|
||||
*
|
||||
* Benefits:
|
||||
* - No accidental ID mixing: `findUser(orderId)` won't compile
|
||||
* - Clear intent in code
|
||||
* - Encapsulated validation
|
||||
*/
|
||||
export class UserId extends ValueObject<UserIdProps> {
|
||||
private constructor(props: UserIdProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(id?: string): UserId {
|
||||
const value = id ?? uuidv4()
|
||||
|
||||
if (!uuidValidate(value)) {
|
||||
throw new Error(`Invalid UserId format: ${value}`)
|
||||
}
|
||||
|
||||
return new UserId({ value })
|
||||
}
|
||||
|
||||
public get value(): string {
|
||||
return this.props.value
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return this.props.value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { PlaceOrder, PlaceOrderRequest } from "../../application/use-cases/PlaceOrder"
|
||||
import { OrderResponseDto } from "../../application/dtos/OrderResponseDto"
|
||||
|
||||
/**
|
||||
* Order Controller
|
||||
*
|
||||
* Infrastructure Layer: HTTP Controller
|
||||
* - No business logic
|
||||
* - Returns DTOs (not domain entities!)
|
||||
* - Delegates to use cases
|
||||
*/
|
||||
export class OrderController {
|
||||
constructor(private readonly placeOrder: PlaceOrder) {}
|
||||
|
||||
/**
|
||||
* POST /orders
|
||||
*
|
||||
* ✅ Good: Returns DTO
|
||||
* ✅ Good: Delegates to use case
|
||||
* ✅ Good: No business logic
|
||||
*/
|
||||
public async placeOrder(request: PlaceOrderRequest): Promise<OrderResponseDto> {
|
||||
try {
|
||||
return await this.placeOrder.execute(request)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to place order: ${error.message}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { CreateUser } from "../../application/use-cases/CreateUser"
|
||||
import { CreateUserRequest } from "../../application/dtos/CreateUserRequest"
|
||||
import { UserResponseDto } from "../../application/dtos/UserResponseDto"
|
||||
|
||||
/**
|
||||
* User Controller
|
||||
*
|
||||
* Clean Architecture: Infrastructure / Presentation Layer
|
||||
* - HTTP concerns (not in use case)
|
||||
* - Request/Response handling
|
||||
* - Error handling
|
||||
* - Delegates to use cases
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - SRP: HTTP handling only
|
||||
* - DIP: depends on use case abstraction
|
||||
* - OCP: can add new endpoints
|
||||
*
|
||||
* Important:
|
||||
* - NO business logic here
|
||||
* - NO domain entities exposed
|
||||
* - Returns DTOs only
|
||||
* - Use cases do the work
|
||||
*/
|
||||
export class UserController {
|
||||
constructor(private readonly createUser: CreateUser) {}
|
||||
|
||||
/**
|
||||
* POST /users
|
||||
*
|
||||
* Clean Code:
|
||||
* - Returns DTO, not domain entity
|
||||
* - Delegates to use case
|
||||
* - Focused method
|
||||
*/
|
||||
public async createUser(request: CreateUserRequest): Promise<UserResponseDto> {
|
||||
try {
|
||||
return await this.createUser.execute(request)
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Failed to create user: ${error.message}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IOrderRepository } from "../../domain/repositories/IOrderRepository"
|
||||
import { Order } from "../../domain/aggregates/Order"
|
||||
import { OrderId } from "../../domain/value-objects/OrderId"
|
||||
import { UserId } from "../../domain/value-objects/UserId"
|
||||
|
||||
/**
|
||||
* In-Memory Order Repository
|
||||
*/
|
||||
export class InMemoryOrderRepository implements IOrderRepository {
|
||||
private readonly orders: Map<string, Order> = new Map()
|
||||
|
||||
public async save(order: Order): Promise<void> {
|
||||
this.orders.set(order.orderId.value, order)
|
||||
}
|
||||
|
||||
public async findById(id: OrderId): Promise<Order | null> {
|
||||
return this.orders.get(id.value) ?? null
|
||||
}
|
||||
|
||||
public async findByUserId(userId: UserId): Promise<Order[]> {
|
||||
return Array.from(this.orders.values()).filter(
|
||||
(order) => order.userId.value === userId.value,
|
||||
)
|
||||
}
|
||||
|
||||
public async findByStatus(status: string): Promise<Order[]> {
|
||||
return Array.from(this.orders.values()).filter((order) => order.status.value === status)
|
||||
}
|
||||
|
||||
public async findAll(): Promise<Order[]> {
|
||||
return Array.from(this.orders.values())
|
||||
}
|
||||
|
||||
public async delete(id: OrderId): Promise<void> {
|
||||
this.orders.delete(id.value)
|
||||
}
|
||||
|
||||
public async exists(id: OrderId): Promise<boolean> {
|
||||
return this.orders.has(id.value)
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.orders.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { IUserRepository } from "../../domain/repositories/IUserRepository"
|
||||
import { User } from "../../domain/aggregates/User"
|
||||
import { UserId } from "../../domain/value-objects/UserId"
|
||||
import { Email } from "../../domain/value-objects/Email"
|
||||
|
||||
/**
|
||||
* In-Memory User Repository
|
||||
*
|
||||
* DDD Pattern: Repository Implementation
|
||||
* - Implements domain interface
|
||||
* - Infrastructure concern
|
||||
* - Can be replaced with real DB
|
||||
*
|
||||
* SOLID Principles:
|
||||
* - DIP: implements abstraction from domain
|
||||
* - SRP: manages User persistence
|
||||
* - LSP: substitutable with other implementations
|
||||
*
|
||||
* Clean Architecture:
|
||||
* - Infrastructure layer
|
||||
* - Depends on domain
|
||||
* - Can be swapped (in-memory, Postgres, MongoDB)
|
||||
*
|
||||
* Use cases:
|
||||
* - Testing
|
||||
* - Development
|
||||
* - Prototyping
|
||||
*/
|
||||
export class InMemoryUserRepository implements IUserRepository {
|
||||
private readonly users: Map<string, User> = new Map()
|
||||
|
||||
public async save(user: User): Promise<void> {
|
||||
this.users.set(user.userId.value, user)
|
||||
}
|
||||
|
||||
public async findById(id: UserId): Promise<User | null> {
|
||||
return this.users.get(id.value) ?? null
|
||||
}
|
||||
|
||||
public async findByEmail(email: Email): Promise<User | null> {
|
||||
return Array.from(this.users.values()).find((user) => user.email.equals(email)) ?? null
|
||||
}
|
||||
|
||||
public async findAll(): Promise<User[]> {
|
||||
return Array.from(this.users.values())
|
||||
}
|
||||
|
||||
public async findActive(): Promise<User[]> {
|
||||
return Array.from(this.users.values()).filter((user) => user.isActive)
|
||||
}
|
||||
|
||||
public async delete(id: UserId): Promise<void> {
|
||||
this.users.delete(id.value)
|
||||
}
|
||||
|
||||
public async exists(id: UserId): Promise<boolean> {
|
||||
return this.users.has(id.value)
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.users.clear()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user