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

@@ -48,3 +48,11 @@ export const REPOSITORY_PATTERN_MESSAGES = {
SUGGESTION_DELETE: "remove or delete",
SUGGESTION_QUERY: "find or search",
}
export const AGGREGATE_VIOLATION_MESSAGES = {
USE_ID_REFERENCE: "1. Reference other aggregates by ID (UserId, OrderId) instead of entity",
USE_VALUE_OBJECT:
"2. Use Value Objects to store needed data from other aggregates (CustomerInfo, ProductSummary)",
AVOID_DIRECT_REFERENCE: "3. Avoid direct entity references to maintain aggregate independence",
MAINTAIN_INDEPENDENCE: "4. Each aggregate should be independently modifiable and deployable",
}

View File

@@ -0,0 +1,45 @@
import { AggregateBoundaryViolation } from "../value-objects/AggregateBoundaryViolation"
/**
* Interface for detecting aggregate boundary violations in DDD
*
* Aggregate boundary violations occur when an entity from one aggregate
* directly references an entity from another aggregate. In DDD, aggregates
* should reference each other only by ID or Value Objects to maintain
* loose coupling and independence.
*/
export interface IAggregateBoundaryDetector {
/**
* Detects aggregate boundary violations in the given code
*
* Analyzes import statements to identify direct entity references
* across aggregate boundaries.
*
* @param code - Source code to analyze
* @param filePath - Path to the file being analyzed
* @param layer - The architectural layer of the file (should be 'domain')
* @returns Array of detected aggregate boundary violations
*/
detectViolations(
code: string,
filePath: string,
layer: string | undefined,
): AggregateBoundaryViolation[]
/**
* Checks if a file path belongs to an aggregate
*
* @param filePath - The file path to check
* @returns The aggregate name if found, undefined otherwise
*/
extractAggregateFromPath(filePath: string): string | undefined
/**
* Checks if an import path references an entity from another aggregate
*
* @param importPath - The import path to analyze
* @param currentAggregate - The aggregate of the current file
* @returns True if the import crosses aggregate boundaries inappropriately
*/
isAggregateBoundaryViolation(importPath: string, currentAggregate: string): boolean
}

View File

@@ -0,0 +1,137 @@
import { ValueObject } from "./ValueObject"
import { AGGREGATE_VIOLATION_MESSAGES } from "../constants/Messages"
interface AggregateBoundaryViolationProps {
readonly fromAggregate: string
readonly toAggregate: string
readonly entityName: string
readonly importPath: string
readonly filePath: string
readonly line?: number
}
/**
* Represents an aggregate boundary violation in the codebase
*
* Aggregate boundary violations occur when an entity from one aggregate
* directly references an entity from another aggregate, violating DDD principles:
* - Aggregates should reference each other only by ID or Value Objects
* - Direct entity references create tight coupling between aggregates
* - Changes to one aggregate should not require changes to another
*
* @example
* ```typescript
* // Bad: Direct entity reference across aggregates
* const violation = AggregateBoundaryViolation.create(
* 'order',
* 'user',
* 'User',
* '../user/User',
* 'src/domain/aggregates/order/Order.ts',
* 5
* )
*
* console.log(violation.getMessage())
* // "Order aggregate should not directly reference User entity from User aggregate"
* ```
*/
export class AggregateBoundaryViolation extends ValueObject<AggregateBoundaryViolationProps> {
private constructor(props: AggregateBoundaryViolationProps) {
super(props)
}
public static create(
fromAggregate: string,
toAggregate: string,
entityName: string,
importPath: string,
filePath: string,
line?: number,
): AggregateBoundaryViolation {
return new AggregateBoundaryViolation({
fromAggregate,
toAggregate,
entityName,
importPath,
filePath,
line,
})
}
public get fromAggregate(): string {
return this.props.fromAggregate
}
public get toAggregate(): string {
return this.props.toAggregate
}
public get entityName(): string {
return this.props.entityName
}
public get importPath(): string {
return this.props.importPath
}
public get filePath(): string {
return this.props.filePath
}
public get line(): number | undefined {
return this.props.line
}
public getMessage(): string {
return `${this.capitalizeFirst(this.props.fromAggregate)} aggregate should not directly reference ${this.props.entityName} entity from ${this.capitalizeFirst(this.props.toAggregate)} aggregate`
}
public getSuggestion(): string {
const suggestions: string[] = [
AGGREGATE_VIOLATION_MESSAGES.USE_ID_REFERENCE,
AGGREGATE_VIOLATION_MESSAGES.USE_VALUE_OBJECT,
AGGREGATE_VIOLATION_MESSAGES.AVOID_DIRECT_REFERENCE,
AGGREGATE_VIOLATION_MESSAGES.MAINTAIN_INDEPENDENCE,
]
return suggestions.join("\n")
}
public getExampleFix(): string {
return `
// ❌ Bad: Direct entity reference across aggregates
// domain/aggregates/order/Order.ts
import { User } from '../user/User'
class Order {
constructor(private user: User) {}
}
// ✅ Good: Reference by ID
// domain/aggregates/order/Order.ts
import { UserId } from '../user/value-objects/UserId'
class Order {
constructor(private userId: UserId) {}
}
// ✅ Good: Use Value Object for needed data
// domain/aggregates/order/value-objects/CustomerInfo.ts
class CustomerInfo {
constructor(
readonly customerId: string,
readonly customerName: string,
readonly customerEmail: string
) {}
}
// domain/aggregates/order/Order.ts
class Order {
constructor(private customerInfo: CustomerInfo) {}
}`
}
private capitalizeFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}