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:
imfozilbek
2025-11-24 20:11:33 +05:00
parent 3fecc98676
commit 0534fdf1bd
23 changed files with 2149 additions and 2 deletions

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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>
}`
}
}