mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat: add entity exposure detection (v0.3.0)
Implement entity exposure detection to prevent domain entities from leaking to API responses. Detects when controllers/routes return domain entities instead of DTOs. Features: - EntityExposure value object with detailed suggestions - IEntityExposureDetector interface in domain layer - EntityExposureDetector implementation in infrastructure - Integration into AnalyzeProject use case - CLI display with helpful suggestions - 24 comprehensive unit tests (98% coverage) - Examples for bad and good patterns Detection scope: - Infrastructure layer only (controllers, routes, handlers, resolvers, gateways) - Identifies PascalCase entities without Dto/Request/Response suffixes - Parses async methods with Promise<T> return types - Provides step-by-step remediation suggestions Test coverage: - EntityExposureDetector: 98.07% - Overall project: 90.6% statements, 83.97% branches - 218 tests passing BREAKING CHANGE: Version bump to 0.3.0
This commit is contained in:
@@ -94,7 +94,7 @@ export class DependencyGraph extends BaseEntity {
|
||||
totalDependencies: number
|
||||
avgDependencies: number
|
||||
maxDependencies: number
|
||||
} {
|
||||
} {
|
||||
const nodes = Array.from(this.nodes.values())
|
||||
const totalFiles = nodes.length
|
||||
const totalDependencies = nodes.reduce((sum, node) => sum + node.dependencies.length, 0)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { EntityExposure } from "../value-objects/EntityExposure"
|
||||
|
||||
/**
|
||||
* Interface for detecting entity exposure violations in the codebase
|
||||
*
|
||||
* Entity exposure occurs when domain entities are directly returned from
|
||||
* controllers/routes instead of using DTOs (Data Transfer Objects).
|
||||
* This violates separation of concerns and can expose internal domain logic.
|
||||
*/
|
||||
export interface IEntityExposureDetector {
|
||||
/**
|
||||
* Detects entity exposure violations in the given code
|
||||
*
|
||||
* Analyzes method return types in controllers/routes to identify
|
||||
* domain entities being directly exposed to external clients.
|
||||
*
|
||||
* @param code - Source code to analyze
|
||||
* @param filePath - Path to the file being analyzed
|
||||
* @param layer - The architectural layer of the file (domain, application, infrastructure, shared)
|
||||
* @returns Array of detected entity exposure violations
|
||||
*/
|
||||
detectExposures(code: string, filePath: string, layer: string | undefined): EntityExposure[]
|
||||
|
||||
/**
|
||||
* Checks if a return type is a domain entity
|
||||
*
|
||||
* Domain entities are typically PascalCase nouns without Dto/Request/Response suffixes
|
||||
* and are defined in the domain layer.
|
||||
*
|
||||
* @param returnType - The return type to check
|
||||
* @returns True if the return type appears to be a domain entity
|
||||
*/
|
||||
isDomainEntity(returnType: string): boolean
|
||||
}
|
||||
109
packages/guardian/src/domain/value-objects/EntityExposure.ts
Normal file
109
packages/guardian/src/domain/value-objects/EntityExposure.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { ValueObject } from "./ValueObject"
|
||||
|
||||
interface EntityExposureProps {
|
||||
readonly entityName: string
|
||||
readonly returnType: string
|
||||
readonly filePath: string
|
||||
readonly layer: string
|
||||
readonly line?: number
|
||||
readonly methodName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an entity exposure violation in the codebase
|
||||
*
|
||||
* Entity exposure occurs when a domain entity is directly exposed in API responses
|
||||
* instead of using DTOs (Data Transfer Objects). This violates the separation of concerns
|
||||
* and can lead to exposing internal domain logic to external clients.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Bad: Controller returning domain entity
|
||||
* const exposure = EntityExposure.create(
|
||||
* 'User',
|
||||
* 'User',
|
||||
* 'src/infrastructure/controllers/UserController.ts',
|
||||
* 'infrastructure',
|
||||
* 25,
|
||||
* 'getUser'
|
||||
* )
|
||||
*
|
||||
* console.log(exposure.getMessage())
|
||||
* // "Method 'getUser' returns domain entity 'User' instead of DTO"
|
||||
* ```
|
||||
*/
|
||||
export class EntityExposure extends ValueObject<EntityExposureProps> {
|
||||
private constructor(props: EntityExposureProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(
|
||||
entityName: string,
|
||||
returnType: string,
|
||||
filePath: string,
|
||||
layer: string,
|
||||
line?: number,
|
||||
methodName?: string,
|
||||
): EntityExposure {
|
||||
return new EntityExposure({
|
||||
entityName,
|
||||
returnType,
|
||||
filePath,
|
||||
layer,
|
||||
line,
|
||||
methodName,
|
||||
})
|
||||
}
|
||||
|
||||
public get entityName(): string {
|
||||
return this.props.entityName
|
||||
}
|
||||
|
||||
public get returnType(): string {
|
||||
return this.props.returnType
|
||||
}
|
||||
|
||||
public get filePath(): string {
|
||||
return this.props.filePath
|
||||
}
|
||||
|
||||
public get layer(): string {
|
||||
return this.props.layer
|
||||
}
|
||||
|
||||
public get line(): number | undefined {
|
||||
return this.props.line
|
||||
}
|
||||
|
||||
public get methodName(): string | undefined {
|
||||
return this.props.methodName
|
||||
}
|
||||
|
||||
public getMessage(): string {
|
||||
const method = this.props.methodName ? `Method '${this.props.methodName}'` : "Method"
|
||||
return `${method} returns domain entity '${this.props.entityName}' instead of DTO`
|
||||
}
|
||||
|
||||
public getSuggestion(): string {
|
||||
const suggestions = [
|
||||
`Create a DTO class (e.g., ${this.props.entityName}ResponseDto) in the application layer`,
|
||||
`Create a mapper to convert ${this.props.entityName} to ${this.props.entityName}ResponseDto`,
|
||||
`Update the method to return ${this.props.entityName}ResponseDto instead of ${this.props.entityName}`,
|
||||
]
|
||||
return suggestions.join("\n")
|
||||
}
|
||||
|
||||
public getExampleFix(): string {
|
||||
return `
|
||||
// ❌ Bad: Exposing domain entity
|
||||
async ${this.props.methodName || "getEntity"}(): Promise<${this.props.entityName}> {
|
||||
return await this.service.find()
|
||||
}
|
||||
|
||||
// ✅ Good: Using DTO
|
||||
async ${this.props.methodName || "getEntity"}(): Promise<${this.props.entityName}ResponseDto> {
|
||||
const entity = await this.service.find()
|
||||
return ${this.props.entityName}Mapper.toDto(entity)
|
||||
}`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user