mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
feat: add repository pattern validation (v0.5.0)
Add comprehensive Repository Pattern validation to detect violations and ensure proper domain-infrastructure separation. Features: - ORM type detection in repository interfaces (25+ patterns) - Concrete repository usage detection in use cases - Repository instantiation detection (new Repository()) - Domain language validation for repository methods - Smart violation reporting with fix suggestions Tests: - 31 new tests for repository pattern detection - 292 total tests passing (100% pass rate) - 96.77% statement coverage, 83.82% branch coverage Examples: - 8 example files (4 bad patterns, 4 good patterns) - Demonstrates Clean Architecture and SOLID principles
This commit is contained in:
@@ -5,9 +5,11 @@ export * from "./value-objects/ValueObject"
|
||||
export * from "./value-objects/ProjectPath"
|
||||
export * from "./value-objects/HardcodedValue"
|
||||
export * from "./value-objects/NamingViolation"
|
||||
export * from "./value-objects/RepositoryViolation"
|
||||
export * from "./repositories/IBaseRepository"
|
||||
export * from "./services/IFileScanner"
|
||||
export * from "./services/ICodeParser"
|
||||
export * from "./services/IHardcodeDetector"
|
||||
export * from "./services/INamingConventionDetector"
|
||||
export * from "./services/RepositoryPatternDetectorService"
|
||||
export * from "./events/DomainEvent"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { RepositoryViolation } from "../value-objects/RepositoryViolation"
|
||||
|
||||
/**
|
||||
* Interface for detecting Repository Pattern violations in the codebase
|
||||
*
|
||||
* Repository Pattern violations include:
|
||||
* - ORM-specific types in repository interfaces (domain layer)
|
||||
* - Concrete repository usage in use cases instead of interfaces
|
||||
* - Repository instantiation with 'new' in use cases (should use DI)
|
||||
* - Non-domain method names in repository interfaces
|
||||
*
|
||||
* The Repository Pattern ensures that domain logic remains decoupled from
|
||||
* infrastructure concerns like databases and ORMs.
|
||||
*/
|
||||
export interface IRepositoryPatternDetector {
|
||||
/**
|
||||
* Detects all Repository Pattern violations in the given code
|
||||
*
|
||||
* Analyzes code for proper implementation of the Repository Pattern,
|
||||
* including interface purity, dependency inversion, and domain language usage.
|
||||
*
|
||||
* @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 Repository Pattern violations
|
||||
*/
|
||||
detectViolations(
|
||||
code: string,
|
||||
filePath: string,
|
||||
layer: string | undefined,
|
||||
): RepositoryViolation[]
|
||||
|
||||
/**
|
||||
* Checks if a type is an ORM-specific type
|
||||
*
|
||||
* ORM-specific types include Prisma types, TypeORM decorators, Mongoose schemas, etc.
|
||||
* These types should not appear in domain repository interfaces.
|
||||
*
|
||||
* @param typeName - The type name to check
|
||||
* @returns True if the type is ORM-specific
|
||||
*/
|
||||
isOrmType(typeName: string): boolean
|
||||
|
||||
/**
|
||||
* Checks if a method name follows domain language conventions
|
||||
*
|
||||
* Domain repository methods should use business-oriented names like:
|
||||
* - findById, findByEmail, findByStatus
|
||||
* - save, create, update
|
||||
* - delete, remove
|
||||
*
|
||||
* Avoid technical database terms like:
|
||||
* - findOne, findMany, query
|
||||
* - insert, select, update (SQL terms)
|
||||
*
|
||||
* @param methodName - The method name to check
|
||||
* @returns True if the method name uses domain language
|
||||
*/
|
||||
isDomainMethodName(methodName: string): boolean
|
||||
|
||||
/**
|
||||
* Checks if a file is a repository interface
|
||||
*
|
||||
* Repository interfaces typically:
|
||||
* - Are in the domain layer
|
||||
* - Have names matching I*Repository pattern
|
||||
* - Contain interface definitions
|
||||
*
|
||||
* @param filePath - The file path to check
|
||||
* @param layer - The architectural layer
|
||||
* @returns True if the file is a repository interface
|
||||
*/
|
||||
isRepositoryInterface(filePath: string, layer: string | undefined): boolean
|
||||
|
||||
/**
|
||||
* Checks if a file is a use case
|
||||
*
|
||||
* Use cases typically:
|
||||
* - Are in the application layer
|
||||
* - Follow verb-noun naming pattern (CreateUser, UpdateProfile)
|
||||
* - Contain class definitions for business operations
|
||||
*
|
||||
* @param filePath - The file path to check
|
||||
* @param layer - The architectural layer
|
||||
* @returns True if the file is a use case
|
||||
*/
|
||||
isUseCase(filePath: string, layer: string | undefined): boolean
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { ValueObject } from "./ValueObject"
|
||||
import { REPOSITORY_VIOLATION_TYPES } from "../../shared/constants/rules"
|
||||
import { REPOSITORY_PATTERN_MESSAGES } from "../constants/Messages"
|
||||
|
||||
interface RepositoryViolationProps {
|
||||
readonly violationType:
|
||||
| typeof REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE
|
||||
| typeof REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE
|
||||
| typeof REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE
|
||||
| typeof REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME
|
||||
readonly filePath: string
|
||||
readonly layer: string
|
||||
readonly line?: number
|
||||
readonly details: string
|
||||
readonly ormType?: string
|
||||
readonly repositoryName?: string
|
||||
readonly methodName?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a Repository Pattern violation in the codebase
|
||||
*
|
||||
* Repository Pattern violations occur when:
|
||||
* 1. Repository interfaces contain ORM-specific types
|
||||
* 2. Use cases depend on concrete repository implementations instead of interfaces
|
||||
* 3. Repositories are instantiated with 'new' in use cases
|
||||
* 4. Repository methods use technical names instead of domain language
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Violation: ORM type in interface
|
||||
* const violation = RepositoryViolation.create(
|
||||
* 'orm-type-in-interface',
|
||||
* 'src/domain/repositories/IUserRepository.ts',
|
||||
* 'domain',
|
||||
* 15,
|
||||
* 'Repository interface uses Prisma-specific type',
|
||||
* 'Prisma.UserWhereInput'
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export class RepositoryViolation extends ValueObject<RepositoryViolationProps> {
|
||||
private constructor(props: RepositoryViolationProps) {
|
||||
super(props)
|
||||
}
|
||||
|
||||
public static create(
|
||||
violationType:
|
||||
| typeof REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE
|
||||
| typeof REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE
|
||||
| typeof REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE
|
||||
| typeof REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
|
||||
filePath: string,
|
||||
layer: string,
|
||||
line: number | undefined,
|
||||
details: string,
|
||||
ormType?: string,
|
||||
repositoryName?: string,
|
||||
methodName?: string,
|
||||
): RepositoryViolation {
|
||||
return new RepositoryViolation({
|
||||
violationType,
|
||||
filePath,
|
||||
layer,
|
||||
line,
|
||||
details,
|
||||
ormType,
|
||||
repositoryName,
|
||||
methodName,
|
||||
})
|
||||
}
|
||||
|
||||
public get violationType(): string {
|
||||
return this.props.violationType
|
||||
}
|
||||
|
||||
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 details(): string {
|
||||
return this.props.details
|
||||
}
|
||||
|
||||
public get ormType(): string | undefined {
|
||||
return this.props.ormType
|
||||
}
|
||||
|
||||
public get repositoryName(): string | undefined {
|
||||
return this.props.repositoryName
|
||||
}
|
||||
|
||||
public get methodName(): string | undefined {
|
||||
return this.props.methodName
|
||||
}
|
||||
|
||||
public getMessage(): string {
|
||||
switch (this.props.violationType) {
|
||||
case REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE:
|
||||
return `Repository interface uses ORM-specific type '${this.props.ormType || "unknown"}'. Domain should not depend on infrastructure concerns.`
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE:
|
||||
return `Use case depends on concrete repository '${this.props.repositoryName || "unknown"}' instead of interface. Use dependency inversion.`
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE:
|
||||
return `Use case creates repository with 'new ${this.props.repositoryName || "Repository"}()'. Use dependency injection instead.`
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME:
|
||||
return `Repository method '${this.props.methodName || "unknown"}' uses technical name. Use domain language instead.`
|
||||
|
||||
default:
|
||||
return `Repository pattern violation: ${this.props.details}`
|
||||
}
|
||||
}
|
||||
|
||||
public getSuggestion(): string {
|
||||
switch (this.props.violationType) {
|
||||
case REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE:
|
||||
return this.getOrmTypeSuggestion()
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE:
|
||||
return this.getConcreteRepositorySuggestion()
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE:
|
||||
return this.getNewRepositorySuggestion()
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME:
|
||||
return this.getNonDomainMethodSuggestion()
|
||||
|
||||
default:
|
||||
return REPOSITORY_PATTERN_MESSAGES.DEFAULT_SUGGESTION
|
||||
}
|
||||
}
|
||||
|
||||
private getOrmTypeSuggestion(): string {
|
||||
return [
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_REMOVE_ORM_TYPES,
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_USE_DOMAIN_TYPES,
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_KEEP_CLEAN,
|
||||
"",
|
||||
REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX,
|
||||
REPOSITORY_PATTERN_MESSAGES.BAD_ORM_EXAMPLE,
|
||||
REPOSITORY_PATTERN_MESSAGES.GOOD_DOMAIN_EXAMPLE,
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
private getConcreteRepositorySuggestion(): string {
|
||||
return [
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_DEPEND_ON_INTERFACE,
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_MOVE_TO_INFRASTRUCTURE,
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_USE_DI,
|
||||
"",
|
||||
REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX,
|
||||
`❌ Bad: constructor(private repo: ${this.props.repositoryName || "UserRepository"})`,
|
||||
`✅ Good: constructor(private repo: I${this.props.repositoryName?.replace(/^.*?([A-Z]\w+)$/, "$1") || "UserRepository"})`,
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
private getNewRepositorySuggestion(): string {
|
||||
return [
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_REMOVE_NEW,
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_INJECT_CONSTRUCTOR,
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_CONFIGURE_DI,
|
||||
"",
|
||||
REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX,
|
||||
REPOSITORY_PATTERN_MESSAGES.BAD_NEW_REPO,
|
||||
REPOSITORY_PATTERN_MESSAGES.GOOD_INJECT_REPO,
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
private getNonDomainMethodSuggestion(): string {
|
||||
const technicalToDomain = {
|
||||
findOne: REPOSITORY_PATTERN_MESSAGES.SUGGESTION_FINDONE,
|
||||
findMany: REPOSITORY_PATTERN_MESSAGES.SUGGESTION_FINDMANY,
|
||||
insert: REPOSITORY_PATTERN_MESSAGES.SUGGESTION_INSERT,
|
||||
update: REPOSITORY_PATTERN_MESSAGES.SUGGESTION_UPDATE,
|
||||
delete: REPOSITORY_PATTERN_MESSAGES.SUGGESTION_DELETE,
|
||||
query: REPOSITORY_PATTERN_MESSAGES.SUGGESTION_QUERY,
|
||||
}
|
||||
|
||||
const suggestion =
|
||||
technicalToDomain[this.props.methodName as keyof typeof technicalToDomain]
|
||||
|
||||
return [
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_RENAME_METHOD,
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_REFLECT_BUSINESS,
|
||||
REPOSITORY_PATTERN_MESSAGES.STEP_AVOID_TECHNICAL,
|
||||
"",
|
||||
REPOSITORY_PATTERN_MESSAGES.EXAMPLE_PREFIX,
|
||||
`❌ Bad: ${this.props.methodName || "findOne"}()`,
|
||||
`✅ Good: ${suggestion || "findById() or findByEmail()"}`,
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
public getExampleFix(): string {
|
||||
switch (this.props.violationType) {
|
||||
case REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE:
|
||||
return this.getOrmTypeExample()
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE:
|
||||
return this.getConcreteRepositoryExample()
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE:
|
||||
return this.getNewRepositoryExample()
|
||||
|
||||
case REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME:
|
||||
return this.getNonDomainMethodExample()
|
||||
|
||||
default:
|
||||
return REPOSITORY_PATTERN_MESSAGES.NO_EXAMPLE
|
||||
}
|
||||
}
|
||||
|
||||
private getOrmTypeExample(): string {
|
||||
return `
|
||||
// ❌ BAD: ORM-specific interface
|
||||
// domain/repositories/IUserRepository.ts
|
||||
interface IUserRepository {
|
||||
findOne(query: { where: { id: string } }) // Prisma-specific
|
||||
create(data: UserCreateInput) // ORM types in domain
|
||||
}
|
||||
|
||||
// ✅ GOOD: Clean domain interface
|
||||
interface IUserRepository {
|
||||
findById(id: UserId): Promise<User | null>
|
||||
save(user: User): Promise<void>
|
||||
delete(id: UserId): Promise<void>
|
||||
}`
|
||||
}
|
||||
|
||||
private getConcreteRepositoryExample(): string {
|
||||
return `
|
||||
// ❌ BAD: Use Case with concrete implementation
|
||||
class CreateUser {
|
||||
constructor(private prisma: PrismaClient) {} // VIOLATION!
|
||||
}
|
||||
|
||||
// ✅ GOOD: Use Case with interface
|
||||
class CreateUser {
|
||||
constructor(private userRepo: IUserRepository) {} // OK
|
||||
}`
|
||||
}
|
||||
|
||||
private getNewRepositoryExample(): string {
|
||||
return `
|
||||
// ❌ BAD: Creating repository in use case
|
||||
class CreateUser {
|
||||
async execute(data: CreateUserRequest) {
|
||||
const repo = new UserRepository() // VIOLATION!
|
||||
await repo.save(user)
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ GOOD: Inject repository via constructor
|
||||
class CreateUser {
|
||||
constructor(private readonly userRepo: IUserRepository) {}
|
||||
|
||||
async execute(data: CreateUserRequest) {
|
||||
await this.userRepo.save(user) // OK
|
||||
}
|
||||
}`
|
||||
}
|
||||
|
||||
private getNonDomainMethodExample(): string {
|
||||
return `
|
||||
// ❌ BAD: Technical method names
|
||||
interface IUserRepository {
|
||||
findOne(id: string) // Database terminology
|
||||
insert(user: User) // SQL terminology
|
||||
query(filter: any) // Technical term
|
||||
}
|
||||
|
||||
// ✅ GOOD: Domain language
|
||||
interface IUserRepository {
|
||||
findById(id: UserId): Promise<User | null>
|
||||
save(user: User): Promise<void>
|
||||
findByEmail(email: Email): Promise<User | null>
|
||||
}`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user