feat: add aggregate boundary validation (v0.7.0)

Implement DDD aggregate boundary validation to detect and prevent direct
entity references across aggregate boundaries.

Features:
- Detect direct entity imports between aggregates
- Allow only ID or Value Object references
- Support multiple folder structures (domain/aggregates/*, domain/*, domain/entities/*)
- Filter allowed imports (value-objects, events, repositories, services)
- Critical severity level for violations
- 41 comprehensive tests with 92.55% coverage
- CLI output with detailed suggestions
- Examples of good and bad patterns

Breaking changes: None
Backwards compatible: Yes
This commit is contained in:
imfozilbek
2025-11-24 23:54:16 +05:00
parent 83b5dccee4
commit c75738ba51
16 changed files with 1297 additions and 12 deletions

View File

@@ -0,0 +1,40 @@
/**
* ❌ BAD EXAMPLE: Direct Entity Reference Across Aggregates
*
* Violation: Order aggregate directly imports and uses User entity from User aggregate
*
* Problems:
* 1. Creates tight coupling between aggregates
* 2. Changes to User entity affect Order aggregate
* 3. Violates aggregate boundary principles in DDD
* 4. Makes aggregates not independently modifiable
*/
import { User } from "../user/User"
import { Product } from "../product/Product"
export class Order {
private id: string
private user: User
private product: Product
private quantity: number
constructor(id: string, user: User, product: Product, quantity: number) {
this.id = id
this.user = user
this.product = product
this.quantity = quantity
}
getUserEmail(): string {
return this.user.email
}
getProductPrice(): number {
return this.product.price
}
calculateTotal(): number {
return this.product.price * this.quantity
}
}

View File

@@ -0,0 +1,40 @@
/**
* ✅ GOOD EXAMPLE: Reference by ID
*
* Best Practice: Order aggregate references other aggregates only by their IDs
*
* Benefits:
* 1. Loose coupling between aggregates
* 2. Each aggregate can be modified independently
* 3. Follows DDD aggregate boundary principles
* 4. Clear separation of concerns
*/
import { UserId } from "../user/value-objects/UserId"
import { ProductId } from "../product/value-objects/ProductId"
export class Order {
private id: string
private userId: UserId
private productId: ProductId
private quantity: number
constructor(id: string, userId: UserId, productId: ProductId, quantity: number) {
this.id = id
this.userId = userId
this.productId = productId
this.quantity = quantity
}
getUserId(): UserId {
return this.userId
}
getProductId(): ProductId {
return this.productId
}
getQuantity(): number {
return this.quantity
}
}

View File

@@ -0,0 +1,61 @@
/**
* ✅ GOOD EXAMPLE: Using Value Objects for Needed Data
*
* Best Practice: When Order needs specific data from other aggregates,
* use Value Objects to store that data (denormalization)
*
* Benefits:
* 1. Order aggregate has all data it needs
* 2. No runtime dependency on other aggregates
* 3. Better performance (no joins needed)
* 4. Clear contract through Value Objects
*/
import { UserId } from "../user/value-objects/UserId"
import { ProductId } from "../product/value-objects/ProductId"
export class CustomerInfo {
constructor(
readonly customerId: UserId,
readonly customerName: string,
readonly customerEmail: string,
) {}
}
export class ProductInfo {
constructor(
readonly productId: ProductId,
readonly productName: string,
readonly productPrice: number,
) {}
}
export class Order {
private id: string
private customer: CustomerInfo
private product: ProductInfo
private quantity: number
constructor(id: string, customer: CustomerInfo, product: ProductInfo, quantity: number) {
this.id = id
this.customer = customer
this.product = product
this.quantity = quantity
}
getCustomerEmail(): string {
return this.customer.customerEmail
}
calculateTotal(): number {
return this.product.productPrice * this.quantity
}
getCustomerInfo(): CustomerInfo {
return this.customer
}
getProductInfo(): ProductInfo {
return this.product
}
}