mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 15:26:53 +05:00
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:
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user