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

@@ -0,0 +1,220 @@
# Repository Pattern Examples
This directory contains examples demonstrating proper and improper implementations of the Repository Pattern.
## Overview
The Repository Pattern provides an abstraction layer between domain logic and data access. A well-implemented repository:
1. Uses domain types, not ORM-specific types
2. Depends on interfaces, not concrete implementations
3. Uses dependency injection, not direct instantiation
4. Uses domain language, not technical database terms
## Examples
### ❌ Bad Examples
#### 1. ORM Types in Interface
**File:** `bad-orm-types-in-interface.ts`
**Problem:** Repository interface exposes Prisma-specific types (`Prisma.UserWhereInput`, `Prisma.UserCreateInput`). This couples the domain layer to infrastructure concerns.
**Violations:**
- Domain depends on ORM library
- Cannot swap ORM without changing domain
- Breaks Clean Architecture principles
#### 2. Concrete Repository in Use Case
**File:** `bad-concrete-repository-in-use-case.ts`
**Problem:** Use case depends on `PrismaUserRepository` instead of `IUserRepository` interface.
**Violations:**
- Violates Dependency Inversion Principle
- Cannot easily mock for testing
- Tightly coupled to specific implementation
#### 3. Creating Repository with 'new'
**File:** `bad-new-repository.ts`
**Problem:** Use case instantiates repositories with `new UserRepository()` instead of receiving them through constructor.
**Violations:**
- Violates Dependency Injection principle
- Hard to test (cannot mock dependencies)
- Hidden dependencies
- Creates tight coupling
#### 4. Technical Method Names
**File:** `bad-technical-method-names.ts`
**Problem:** Repository methods use database/SQL terminology (`findOne`, `insert`, `query`, `execute`).
**Violations:**
- Uses technical terms instead of domain language
- Exposes implementation details
- Not aligned with ubiquitous language
### ✅ Good Examples
#### 1. Clean Interface
**File:** `good-clean-interface.ts`
**Benefits:**
- Uses only domain types (UserId, Email, User)
- ORM-agnostic interface
- Easy to understand and maintain
- Follows Clean Architecture
```typescript
interface IUserRepository {
findById(id: UserId): Promise<User | null>
findByEmail(email: Email): Promise<User | null>
save(user: User): Promise<void>
delete(id: UserId): Promise<void>
}
```
#### 2. Interface in Use Case
**File:** `good-interface-in-use-case.ts`
**Benefits:**
- Depends on interface, not concrete class
- Easy to test with mocks
- Can swap implementations
- Follows Dependency Inversion Principle
```typescript
class CreateUser {
constructor(private readonly userRepo: IUserRepository) {}
async execute(data: CreateUserRequest): Promise<UserResponseDto> {
// Uses interface, not concrete implementation
}
}
```
#### 3. Dependency Injection
**File:** `good-dependency-injection.ts`
**Benefits:**
- All dependencies injected through constructor
- Explicit dependencies (no hidden coupling)
- Easy to test with mocks
- Follows SOLID principles
```typescript
class CreateUser {
constructor(
private readonly userRepo: IUserRepository,
private readonly emailService: IEmailService
) {}
}
```
#### 4. Domain Language
**File:** `good-domain-language.ts`
**Benefits:**
- Methods use business-oriented names
- Self-documenting interface
- Aligns with ubiquitous language
- Hides implementation details
```typescript
interface IUserRepository {
findById(id: UserId): Promise<User | null>
findByEmail(email: Email): Promise<User | null>
findActiveUsers(): Promise<User[]>
save(user: User): Promise<void>
search(criteria: UserSearchCriteria): Promise<User[]>
}
```
## Key Principles
### 1. Persistence Ignorance
Domain entities and repositories should not know about how data is persisted.
```typescript
// ❌ Bad: Domain knows about Prisma
interface IUserRepository {
find(query: Prisma.UserWhereInput): Promise<User>
}
// ✅ Good: Domain uses own types
interface IUserRepository {
findById(id: UserId): Promise<User | null>
}
```
### 2. Dependency Inversion
High-level modules (use cases) should not depend on low-level modules (repositories). Both should depend on abstractions (interfaces).
```typescript
// ❌ Bad: Use case depends on concrete repository
class CreateUser {
constructor(private repo: PrismaUserRepository) {}
}
// ✅ Good: Use case depends on interface
class CreateUser {
constructor(private repo: IUserRepository) {}
}
```
### 3. Dependency Injection
Don't create dependencies inside classes. Inject them through constructor.
```typescript
// ❌ Bad: Creates dependency
class CreateUser {
execute() {
const repo = new UserRepository()
}
}
// ✅ Good: Injects dependency
class CreateUser {
constructor(private readonly repo: IUserRepository) {}
}
```
### 4. Ubiquitous Language
Use domain language everywhere, including repository methods.
```typescript
// ❌ Bad: Technical terminology
interface IUserRepository {
findOne(id: string): Promise<User>
insert(user: User): Promise<void>
}
// ✅ Good: Domain language
interface IUserRepository {
findById(id: UserId): Promise<User | null>
save(user: User): Promise<void>
}
```
## Testing with Guardian
Run Guardian to detect Repository Pattern violations:
```bash
guardian check --root ./examples/repository-pattern
```
Guardian will detect:
- ORM types in repository interfaces
- Concrete repository usage in use cases
- Repository instantiation with 'new'
- Technical method names in repositories
## Further Reading
- [Clean Architecture by Robert C. Martin](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [Domain-Driven Design by Eric Evans](https://www.domainlanguage.com/ddd/)
- [Repository Pattern - Martin Fowler](https://martinfowler.com/eaaCatalog/repository.html)
- [SOLID Principles](https://en.wikipedia.org/wiki/SOLID)

View File

@@ -0,0 +1,67 @@
/**
* ❌ BAD EXAMPLE: Concrete repository in use case
*
* Use case depends on concrete repository implementation instead of interface.
* This violates Dependency Inversion Principle.
*/
class CreateUser {
constructor(private userRepo: PrismaUserRepository) {}
async execute(data: CreateUserRequest): Promise<UserResponseDto> {
const user = User.create(data.email, data.name)
await this.userRepo.save(user)
return UserMapper.toDto(user)
}
}
class PrismaUserRepository {
constructor(private prisma: any) {}
async save(user: User): Promise<void> {
await this.prisma.user.create({
data: {
email: user.getEmail(),
name: user.getName(),
},
})
}
}
class User {
static create(email: string, name: string): User {
return new User(email, name)
}
constructor(
private email: string,
private name: string,
) {}
getEmail(): string {
return this.email
}
getName(): string {
return this.name
}
}
interface CreateUserRequest {
email: string
name: string
}
interface UserResponseDto {
email: string
name: string
}
class UserMapper {
static toDto(user: User): UserResponseDto {
return {
email: user.getEmail(),
name: user.getName(),
}
}
}

View File

@@ -0,0 +1,69 @@
/**
* ❌ BAD EXAMPLE: Creating repository with 'new' in use case
*
* Use case creates repository instances directly.
* This violates Dependency Injection principle and makes testing difficult.
*/
class CreateUser {
async execute(data: CreateUserRequest): Promise<UserResponseDto> {
const userRepo = new UserRepository()
const emailService = new EmailService()
const user = User.create(data.email, data.name)
await userRepo.save(user)
await emailService.sendWelcomeEmail(user.getEmail())
return UserMapper.toDto(user)
}
}
class UserRepository {
async save(user: User): Promise<void> {
console.warn("Saving user to database")
}
}
class EmailService {
async sendWelcomeEmail(email: string): Promise<void> {
console.warn(`Sending welcome email to ${email}`)
}
}
class User {
static create(email: string, name: string): User {
return new User(email, name)
}
constructor(
private email: string,
private name: string,
) {}
getEmail(): string {
return this.email
}
getName(): string {
return this.name
}
}
interface CreateUserRequest {
email: string
name: string
}
interface UserResponseDto {
email: string
name: string
}
class UserMapper {
static toDto(user: User): UserResponseDto {
return {
email: user.getEmail(),
name: user.getName(),
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* ❌ BAD EXAMPLE: ORM-specific types in repository interface
*
* This violates Repository Pattern by coupling domain layer to infrastructure (ORM).
* Domain should remain persistence-agnostic.
*/
import { Prisma, PrismaClient } from "@prisma/client"
interface IUserRepository {
findOne(query: Prisma.UserWhereInput): Promise<User | null>
findMany(query: Prisma.UserFindManyArgs): Promise<User[]>
create(data: Prisma.UserCreateInput): Promise<User>
update(id: string, data: Prisma.UserUpdateInput): Promise<User>
}
class User {
constructor(
public id: string,
public email: string,
public name: string,
) {}
}

View File

@@ -0,0 +1,32 @@
/**
* ❌ BAD EXAMPLE: Technical method names
*
* Repository interface uses database/ORM terminology instead of domain language.
* Methods should reflect business operations, not technical implementation.
*/
interface IUserRepository {
findOne(id: string): Promise<User | null>
findMany(filter: any): Promise<User[]>
insert(user: User): Promise<void>
updateOne(id: string, data: any): Promise<void>
deleteOne(id: string): Promise<void>
query(sql: string): Promise<any>
execute(command: string): Promise<void>
select(fields: string[]): Promise<User[]>
}
class User {
constructor(
public id: string,
public email: string,
public name: string,
) {}
}

View File

@@ -0,0 +1,61 @@
/**
* ✅ GOOD EXAMPLE: Clean repository interface
*
* Repository interface uses only domain types, keeping it persistence-agnostic.
* ORM implementation details stay in infrastructure layer.
*/
interface IUserRepository {
findById(id: UserId): Promise<User | null>
findByEmail(email: Email): Promise<User | null>
save(user: User): Promise<void>
delete(id: UserId): Promise<void>
findAll(criteria: UserSearchCriteria): Promise<User[]>
}
class UserId {
constructor(private readonly value: string) {}
getValue(): string {
return this.value
}
}
class Email {
constructor(private readonly value: string) {}
getValue(): string {
return this.value
}
}
class UserSearchCriteria {
constructor(
public readonly isActive?: boolean,
public readonly registeredAfter?: Date,
) {}
}
class User {
constructor(
private readonly id: UserId,
private email: Email,
private name: string,
) {}
getId(): UserId {
return this.id
}
getEmail(): Email {
return this.email
}
getName(): string {
return this.name
}
}

View File

@@ -0,0 +1,98 @@
/**
* ✅ GOOD EXAMPLE: Dependency Injection
*
* Use case receives dependencies through constructor.
* This makes code testable and follows SOLID principles.
*/
class CreateUser {
constructor(
private readonly userRepo: IUserRepository,
private readonly emailService: IEmailService,
) {}
async execute(data: CreateUserRequest): Promise<UserResponseDto> {
const user = User.create(Email.from(data.email), data.name)
await this.userRepo.save(user)
await this.emailService.sendWelcomeEmail(user.getEmail())
return UserMapper.toDto(user)
}
}
interface IUserRepository {
save(user: User): Promise<void>
findByEmail(email: Email): Promise<User | null>
}
interface IEmailService {
sendWelcomeEmail(email: Email): Promise<void>
}
class Email {
private constructor(private readonly value: string) {}
static from(value: string): Email {
if (!value.includes("@")) {
throw new Error("Invalid email")
}
return new Email(value)
}
getValue(): string {
return this.value
}
}
class User {
static create(email: Email, name: string): User {
return new User(email, name)
}
private constructor(
private readonly email: Email,
private readonly name: string,
) {}
getEmail(): Email {
return this.email
}
getName(): string {
return this.name
}
}
interface CreateUserRequest {
email: string
name: string
}
interface UserResponseDto {
email: string
name: string
}
class UserMapper {
static toDto(user: User): UserResponseDto {
return {
email: user.getEmail().getValue(),
name: user.getName(),
}
}
}
class UserRepository implements IUserRepository {
async save(_user: User): Promise<void> {
console.warn("Saving user to database")
}
async findByEmail(_email: Email): Promise<User | null> {
return null
}
}
class EmailService implements IEmailService {
async sendWelcomeEmail(email: Email): Promise<void> {
console.warn(`Sending welcome email to ${email.getValue()}`)
}
}

View File

@@ -0,0 +1,81 @@
/**
* ✅ GOOD EXAMPLE: Domain language in repository
*
* Repository interface uses domain-driven method names that reflect business operations.
* Method names are self-documenting and align with ubiquitous language.
*/
interface IUserRepository {
findById(id: UserId): Promise<User | null>
findByEmail(email: Email): Promise<User | null>
findActiveUsers(): Promise<User[]>
save(user: User): Promise<void>
delete(id: UserId): Promise<void>
search(criteria: UserSearchCriteria): Promise<User[]>
countActiveUsers(): Promise<number>
existsByEmail(email: Email): Promise<boolean>
}
class UserId {
constructor(private readonly value: string) {}
getValue(): string {
return this.value
}
}
class Email {
constructor(private readonly value: string) {}
getValue(): string {
return this.value
}
}
class UserSearchCriteria {
constructor(
public readonly isActive?: boolean,
public readonly registeredAfter?: Date,
public readonly department?: string,
) {}
}
class User {
constructor(
private readonly id: UserId,
private email: Email,
private name: string,
private isActive: boolean,
) {}
getId(): UserId {
return this.id
}
getEmail(): Email {
return this.email
}
getName(): string {
return this.name
}
isUserActive(): boolean {
return this.isActive
}
activate(): void {
this.isActive = true
}
deactivate(): void {
this.isActive = false
}
}

View File

@@ -0,0 +1,74 @@
/**
* ✅ GOOD EXAMPLE: Repository interface in use case
*
* Use case depends on repository interface, not concrete implementation.
* This follows Dependency Inversion Principle.
*/
class CreateUser {
constructor(private readonly userRepo: IUserRepository) {}
async execute(data: CreateUserRequest): Promise<UserResponseDto> {
const user = User.create(Email.from(data.email), data.name)
await this.userRepo.save(user)
return UserMapper.toDto(user)
}
}
interface IUserRepository {
save(user: User): Promise<void>
findByEmail(email: Email): Promise<User | null>
}
class Email {
private constructor(private readonly value: string) {}
static from(value: string): Email {
if (!value.includes("@")) {
throw new Error("Invalid email")
}
return new Email(value)
}
getValue(): string {
return this.value
}
}
class User {
static create(email: Email, name: string): User {
return new User(email, name)
}
private constructor(
private readonly email: Email,
private readonly name: string,
) {}
getEmail(): Email {
return this.email
}
getName(): string {
return this.name
}
}
interface CreateUserRequest {
email: string
name: string
}
interface UserResponseDto {
email: string
name: string
}
class UserMapper {
static toDto(user: User): UserResponseDto {
return {
email: user.getEmail().getValue(),
name: user.getName(),
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@samiyev/guardian",
"version": "0.4.0",
"version": "0.5.0",
"description": "Code quality guardian for vibe coders and enterprise teams - catch hardcodes, architecture violations, and circular deps. Enforce Clean Architecture at scale. Works with Claude, GPT, Copilot.",
"keywords": [
"puaros",

View File

@@ -10,6 +10,7 @@ import { INamingConventionDetector } from "./domain/services/INamingConventionDe
import { IFrameworkLeakDetector } from "./domain/services/IFrameworkLeakDetector"
import { IEntityExposureDetector } from "./domain/services/IEntityExposureDetector"
import { IDependencyDirectionDetector } from "./domain/services/IDependencyDirectionDetector"
import { IRepositoryPatternDetector } from "./domain/services/RepositoryPatternDetectorService"
import { FileScanner } from "./infrastructure/scanners/FileScanner"
import { CodeParser } from "./infrastructure/parsers/CodeParser"
import { HardcodeDetector } from "./infrastructure/analyzers/HardcodeDetector"
@@ -17,6 +18,7 @@ import { NamingConventionDetector } from "./infrastructure/analyzers/NamingConve
import { FrameworkLeakDetector } from "./infrastructure/analyzers/FrameworkLeakDetector"
import { EntityExposureDetector } from "./infrastructure/analyzers/EntityExposureDetector"
import { DependencyDirectionDetector } from "./infrastructure/analyzers/DependencyDirectionDetector"
import { RepositoryPatternDetector } from "./infrastructure/analyzers/RepositoryPatternDetector"
import { ERROR_MESSAGES } from "./shared/constants"
/**
@@ -73,6 +75,7 @@ export async function analyzeProject(
const entityExposureDetector: IEntityExposureDetector = new EntityExposureDetector()
const dependencyDirectionDetector: IDependencyDirectionDetector =
new DependencyDirectionDetector()
const repositoryPatternDetector: IRepositoryPatternDetector = new RepositoryPatternDetector()
const useCase = new AnalyzeProject(
fileScanner,
codeParser,
@@ -81,6 +84,7 @@ export async function analyzeProject(
frameworkLeakDetector,
entityExposureDetector,
dependencyDirectionDetector,
repositoryPatternDetector,
)
const result = await useCase.execute(options)
@@ -102,5 +106,6 @@ export type {
FrameworkLeakViolation,
EntityExposureViolation,
DependencyDirectionViolation,
RepositoryPatternViolation,
ProjectMetrics,
} from "./application/use-cases/AnalyzeProject"

View File

@@ -7,6 +7,7 @@ import { INamingConventionDetector } from "../../domain/services/INamingConventi
import { IFrameworkLeakDetector } from "../../domain/services/IFrameworkLeakDetector"
import { IEntityExposureDetector } from "../../domain/services/IEntityExposureDetector"
import { IDependencyDirectionDetector } from "../../domain/services/IDependencyDirectionDetector"
import { IRepositoryPatternDetector } from "../../domain/services/RepositoryPatternDetectorService"
import { SourceFile } from "../../domain/entities/SourceFile"
import { DependencyGraph } from "../../domain/entities/DependencyGraph"
import { ProjectPath } from "../../domain/value-objects/ProjectPath"
@@ -16,6 +17,7 @@ import {
LAYERS,
NAMING_VIOLATION_TYPES,
REGEX_PATTERNS,
REPOSITORY_VIOLATION_TYPES,
RULES,
SEVERITY_LEVELS,
} from "../../shared/constants"
@@ -36,6 +38,7 @@ export interface AnalyzeProjectResponse {
frameworkLeakViolations: FrameworkLeakViolation[]
entityExposureViolations: EntityExposureViolation[]
dependencyDirectionViolations: DependencyDirectionViolation[]
repositoryPatternViolations: RepositoryPatternViolation[]
metrics: ProjectMetrics
}
@@ -122,6 +125,21 @@ export interface DependencyDirectionViolation {
suggestion: string
}
export interface RepositoryPatternViolation {
rule: typeof RULES.REPOSITORY_PATTERN
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
file: string
layer: string
line?: number
details: string
message: string
suggestion: string
}
export interface ProjectMetrics {
totalFiles: number
totalFunctions: number
@@ -144,6 +162,7 @@ export class AnalyzeProject extends UseCase<
private readonly frameworkLeakDetector: IFrameworkLeakDetector,
private readonly entityExposureDetector: IEntityExposureDetector,
private readonly dependencyDirectionDetector: IDependencyDirectionDetector,
private readonly repositoryPatternDetector: IRepositoryPatternDetector,
) {
super()
}
@@ -195,6 +214,7 @@ export class AnalyzeProject extends UseCase<
const frameworkLeakViolations = this.detectFrameworkLeaks(sourceFiles)
const entityExposureViolations = this.detectEntityExposures(sourceFiles)
const dependencyDirectionViolations = this.detectDependencyDirections(sourceFiles)
const repositoryPatternViolations = this.detectRepositoryPatternViolations(sourceFiles)
const metrics = this.calculateMetrics(sourceFiles, totalFunctions, dependencyGraph)
return ResponseDto.ok({
@@ -207,6 +227,7 @@ export class AnalyzeProject extends UseCase<
frameworkLeakViolations,
entityExposureViolations,
dependencyDirectionViolations,
repositoryPatternViolations,
metrics,
})
} catch (error) {
@@ -452,6 +473,39 @@ export class AnalyzeProject extends UseCase<
return violations
}
private detectRepositoryPatternViolations(
sourceFiles: SourceFile[],
): RepositoryPatternViolation[] {
const violations: RepositoryPatternViolation[] = []
for (const file of sourceFiles) {
const patternViolations = this.repositoryPatternDetector.detectViolations(
file.content,
file.path.relative,
file.layer,
)
for (const violation of patternViolations) {
violations.push({
rule: RULES.REPOSITORY_PATTERN,
violationType: violation.violationType as
| 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,
file: file.path.relative,
layer: violation.layer,
line: violation.line,
details: violation.details,
message: violation.getMessage(),
suggestion: violation.getSuggestion(),
})
}
}
return violations
}
private calculateMetrics(
sourceFiles: SourceFile[],
totalFunctions: number,

View File

@@ -33,7 +33,20 @@ export const CLI_ARGUMENTS = {
PATH: "<path>",
} as const
export const DEFAULT_EXCLUDES = ["node_modules", "dist", "build", "coverage"] as const
export const DEFAULT_EXCLUDES = [
"node_modules",
"dist",
"build",
"coverage",
"tests",
"test",
"__tests__",
"examples",
"**/*.test.ts",
"**/*.test.js",
"**/*.spec.ts",
"**/*.spec.js",
] as const
export const CLI_MESSAGES = {
ANALYZING: "\n🛡 Guardian - Analyzing your code...\n",

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

View File

@@ -0,0 +1,387 @@
import { IRepositoryPatternDetector } from "../../domain/services/RepositoryPatternDetectorService"
import { RepositoryViolation } from "../../domain/value-objects/RepositoryViolation"
import { LAYERS, REPOSITORY_VIOLATION_TYPES } from "../../shared/constants/rules"
import { ORM_QUERY_METHODS } from "../constants/orm-methods"
import { REPOSITORY_PATTERN_MESSAGES } from "../../domain/constants/Messages"
/**
* Detects Repository Pattern violations in the codebase
*
* This detector identifies violations where the Repository Pattern is not properly implemented:
* 1. ORM-specific types in repository interfaces (domain should be ORM-agnostic)
* 2. Concrete repository usage in use cases (violates dependency inversion)
* 3. Repository instantiation with 'new' in use cases (should use DI)
* 4. Non-domain method names in repositories (should use ubiquitous language)
*
* @example
* ```typescript
* const detector = new RepositoryPatternDetector()
*
* // Detect violations in a repository interface
* const code = `
* interface IUserRepository {
* findOne(query: Prisma.UserWhereInput): Promise<User>
* }
* `
* const violations = detector.detectViolations(
* code,
* 'src/domain/repositories/IUserRepository.ts',
* 'domain'
* )
*
* // violations will contain ORM type violation
* console.log(violations.length) // 1
* console.log(violations[0].violationType) // 'orm-type-in-interface'
* ```
*/
export class RepositoryPatternDetector implements IRepositoryPatternDetector {
private readonly ormTypePatterns = [
/Prisma\./,
/PrismaClient/,
/TypeORM/,
/@Entity/,
/@Column/,
/@PrimaryColumn/,
/@PrimaryGeneratedColumn/,
/@ManyToOne/,
/@OneToMany/,
/@ManyToMany/,
/@JoinColumn/,
/@JoinTable/,
/Mongoose\./,
/Schema/,
/Model</,
/Document/,
/Sequelize\./,
/DataTypes\./,
/FindOptions/,
/WhereOptions/,
/IncludeOptions/,
/QueryInterface/,
/MikroORM/,
/EntityManager/,
/EntityRepository/,
/Collection</,
]
private readonly technicalMethodNames = ORM_QUERY_METHODS
private readonly domainMethodPatterns = [
/^findBy[A-Z]/,
/^findAll/,
/^save$/,
/^create$/,
/^update$/,
/^delete$/,
/^remove$/,
/^add$/,
/^get[A-Z]/,
/^search/,
/^list/,
]
private readonly concreteRepositoryPatterns = [
/PrismaUserRepository/,
/MongoUserRepository/,
/TypeOrmUserRepository/,
/SequelizeUserRepository/,
/InMemoryUserRepository/,
/PostgresUserRepository/,
/MySqlUserRepository/,
/Repository(?!Interface)/,
]
/**
* Detects all Repository Pattern violations in the given code
*/
public detectViolations(
code: string,
filePath: string,
layer: string | undefined,
): RepositoryViolation[] {
const violations: RepositoryViolation[] = []
if (this.isRepositoryInterface(filePath, layer)) {
violations.push(...this.detectOrmTypesInInterface(code, filePath, layer))
violations.push(...this.detectNonDomainMethodNames(code, filePath, layer))
}
if (this.isUseCase(filePath, layer)) {
violations.push(...this.detectConcreteRepositoryUsage(code, filePath, layer))
violations.push(...this.detectNewRepositoryInstantiation(code, filePath, layer))
}
return violations
}
/**
* Checks if a type is an ORM-specific type
*/
public isOrmType(typeName: string): boolean {
return this.ormTypePatterns.some((pattern) => pattern.test(typeName))
}
/**
* Checks if a method name follows domain language conventions
*/
public isDomainMethodName(methodName: string): boolean {
if ((this.technicalMethodNames as readonly string[]).includes(methodName)) {
return false
}
return this.domainMethodPatterns.some((pattern) => pattern.test(methodName))
}
/**
* Checks if a file is a repository interface
*/
public isRepositoryInterface(filePath: string, layer: string | undefined): boolean {
if (layer !== LAYERS.DOMAIN) {
return false
}
return /I[A-Z]\w*Repository\.ts$/.test(filePath) && /repositories?\//.test(filePath)
}
/**
* Checks if a file is a use case
*/
public isUseCase(filePath: string, layer: string | undefined): boolean {
if (layer !== LAYERS.APPLICATION) {
return false
}
return /use-cases?\//.test(filePath) && /[A-Z][a-z]+[A-Z]\w*\.ts$/.test(filePath)
}
/**
* Detects ORM-specific types in repository interfaces
*/
private detectOrmTypesInInterface(
code: string,
filePath: string,
layer: string | undefined,
): RepositoryViolation[] {
const violations: RepositoryViolation[] = []
const lines = code.split("\n")
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const lineNumber = i + 1
const methodMatch =
/(\w+)\s*\([^)]*:\s*([^)]+)\)\s*:\s*.*?(?:Promise<([^>]+)>|([A-Z]\w+))/.exec(line)
if (methodMatch) {
const params = methodMatch[2]
const returnType = methodMatch[3] || methodMatch[4]
if (this.isOrmType(params)) {
const ormType = this.extractOrmType(params)
violations.push(
RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
filePath,
layer || LAYERS.DOMAIN,
lineNumber,
`Method parameter uses ORM type: ${ormType}`,
ormType,
),
)
}
if (returnType && this.isOrmType(returnType)) {
const ormType = this.extractOrmType(returnType)
violations.push(
RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
filePath,
layer || LAYERS.DOMAIN,
lineNumber,
`Method return type uses ORM type: ${ormType}`,
ormType,
),
)
}
}
for (const pattern of this.ormTypePatterns) {
if (pattern.test(line) && !line.trim().startsWith("//")) {
const ormType = this.extractOrmType(line)
violations.push(
RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
filePath,
layer || LAYERS.DOMAIN,
lineNumber,
`Repository interface contains ORM-specific type: ${ormType}`,
ormType,
),
)
break
}
}
}
return violations
}
/**
* Detects non-domain method names in repository interfaces
*/
private detectNonDomainMethodNames(
code: string,
filePath: string,
layer: string | undefined,
): RepositoryViolation[] {
const violations: RepositoryViolation[] = []
const lines = code.split("\n")
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const lineNumber = i + 1
const methodMatch = /^\s*(\w+)\s*\(/.exec(line)
if (methodMatch) {
const methodName = methodMatch[1]
if (!this.isDomainMethodName(methodName) && !line.trim().startsWith("//")) {
violations.push(
RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
filePath,
layer || LAYERS.DOMAIN,
lineNumber,
`Method '${methodName}' uses technical name instead of domain language`,
undefined,
undefined,
methodName,
),
)
}
}
}
return violations
}
/**
* Detects concrete repository usage in use cases
*/
private detectConcreteRepositoryUsage(
code: string,
filePath: string,
layer: string | undefined,
): RepositoryViolation[] {
const violations: RepositoryViolation[] = []
const lines = code.split("\n")
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const lineNumber = i + 1
const constructorParamMatch =
/constructor\s*\([^)]*(?:private|public|protected)\s+(?:readonly\s+)?(\w+)\s*:\s*([A-Z]\w*Repository)/.exec(
line,
)
if (constructorParamMatch) {
const repositoryType = constructorParamMatch[2]
if (!repositoryType.startsWith("I")) {
violations.push(
RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
filePath,
layer || LAYERS.APPLICATION,
lineNumber,
`Use case depends on concrete repository '${repositoryType}'`,
undefined,
repositoryType,
),
)
}
}
const fieldMatch =
/(?:private|public|protected)\s+(?:readonly\s+)?(\w+)\s*:\s*([A-Z]\w*Repository)/.exec(
line,
)
if (fieldMatch) {
const repositoryType = fieldMatch[2]
if (
!repositoryType.startsWith("I") &&
!line.includes(REPOSITORY_PATTERN_MESSAGES.CONSTRUCTOR)
) {
violations.push(
RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
filePath,
layer || LAYERS.APPLICATION,
lineNumber,
`Use case field uses concrete repository '${repositoryType}'`,
undefined,
repositoryType,
),
)
}
}
}
return violations
}
/**
* Detects 'new Repository()' instantiation in use cases
*/
private detectNewRepositoryInstantiation(
code: string,
filePath: string,
layer: string | undefined,
): RepositoryViolation[] {
const violations: RepositoryViolation[] = []
const lines = code.split("\n")
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const lineNumber = i + 1
const newRepositoryMatch = /new\s+([A-Z]\w*Repository)\s*\(/.exec(line)
if (newRepositoryMatch && !line.trim().startsWith("//")) {
const repositoryName = newRepositoryMatch[1]
violations.push(
RepositoryViolation.create(
REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
filePath,
layer || LAYERS.APPLICATION,
lineNumber,
`Use case creates repository with 'new ${repositoryName}()'`,
undefined,
repositoryName,
),
)
}
}
return violations
}
/**
* Extracts ORM type name from a code line
*/
private extractOrmType(line: string): string {
for (const pattern of this.ormTypePatterns) {
const match = line.match(pattern)
if (match) {
const startIdx = match.index || 0
const typeMatch = /[\w.]+/.exec(line.slice(startIdx))
return typeMatch ? typeMatch[0] : REPOSITORY_PATTERN_MESSAGES.UNKNOWN_TYPE
}
}
return REPOSITORY_PATTERN_MESSAGES.UNKNOWN_TYPE
}
}

View File

@@ -0,0 +1,2 @@
export const NAMING_SUGGESTION_DEFAULT =
"Move to application or infrastructure layer, or rename to follow domain patterns"

View File

@@ -0,0 +1,24 @@
export const ORM_QUERY_METHODS = [
"findOne",
"findMany",
"findFirst",
"findAll",
"findAndCountAll",
"insert",
"insertMany",
"insertOne",
"updateOne",
"updateMany",
"deleteOne",
"deleteMany",
"select",
"query",
"execute",
"run",
"exec",
"aggregate",
"count",
"exists",
] as const
export type OrmQueryMethod = (typeof ORM_QUERY_METHODS)[number]

View File

@@ -0,0 +1,28 @@
export const DTO_SUFFIXES = [
"Dto",
"DTO",
"Request",
"Response",
"Command",
"Query",
"Result",
] as const
export const PRIMITIVE_TYPES = [
"string",
"number",
"boolean",
"void",
"any",
"unknown",
"null",
"undefined",
"object",
"never",
] as const
export const NULLABLE_TYPES = ["null", "undefined"] as const
export const TEST_FILE_EXTENSIONS = [".test.", ".spec."] as const
export const TEST_FILE_SUFFIXES = [".test.ts", ".test.js", ".spec.ts", ".spec.js"] as const

View File

@@ -1,3 +1,4 @@
export * from "./parsers/CodeParser"
export * from "./scanners/FileScanner"
export * from "./analyzers/HardcodeDetector"
export * from "./analyzers/RepositoryPatternDetector"

View File

@@ -9,6 +9,7 @@ export const RULES = {
FRAMEWORK_LEAK: "framework-leak",
ENTITY_EXPOSURE: "entity-exposure",
DEPENDENCY_DIRECTION: "dependency-direction",
REPOSITORY_PATTERN: "repository-pattern",
} as const
/**
@@ -397,4 +398,15 @@ export const FRAMEWORK_LEAK_MESSAGES = {
DOMAIN_IMPORT:
'Domain layer imports framework-specific package "{package}". Use interfaces and dependency injection instead.',
SUGGESTION: "Create an interface in domain layer and implement it in infrastructure layer.",
PACKAGE_PLACEHOLDER: "{package}",
} as const
/**
* Repository pattern violation types
*/
export const REPOSITORY_VIOLATION_TYPES = {
ORM_TYPE_IN_INTERFACE: "orm-type-in-interface",
CONCRETE_REPOSITORY_IN_USE_CASE: "concrete-repository-in-use-case",
NEW_REPOSITORY_IN_USE_CASE: "new-repository-in-use-case",
NON_DOMAIN_METHOD_NAME: "non-domain-method-name",
} as const

View File

@@ -0,0 +1,515 @@
import { describe, it, expect, beforeEach } from "vitest"
import { RepositoryPatternDetector } from "../src/infrastructure/analyzers/RepositoryPatternDetector"
import { REPOSITORY_VIOLATION_TYPES } from "../src/shared/constants/rules"
describe("RepositoryPatternDetector", () => {
let detector: RepositoryPatternDetector
beforeEach(() => {
detector = new RepositoryPatternDetector()
})
describe("detectViolations - ORM Types in Interface", () => {
it("should detect Prisma types in repository interface", () => {
const code = `
interface IUserRepository {
findOne(query: Prisma.UserWhereInput): Promise<User>
create(data: Prisma.UserCreateInput): Promise<User>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
expect(violations.length).toBeGreaterThan(0)
const ormViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
)
expect(ormViolations.length).toBeGreaterThan(0)
expect(ormViolations[0].getMessage()).toContain("ORM-specific type")
})
it("should detect TypeORM decorators in repository interface", () => {
const code = `
interface IUserRepository {
@Column()
findById(id: string): Promise<User>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
const ormViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
)
expect(ormViolations.length).toBeGreaterThan(0)
})
it("should detect Mongoose types in repository interface", () => {
const code = `
interface IUserRepository {
find(query: Model<User>): Promise<User[]>
findOne(query: Schema): Promise<User>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
const ormViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
)
expect(ormViolations.length).toBeGreaterThan(0)
})
it("should not detect ORM types in clean interface", () => {
const code = `
interface IUserRepository {
findById(id: UserId): Promise<User | null>
save(user: User): Promise<void>
delete(id: UserId): Promise<void>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
const ormViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
)
expect(ormViolations).toHaveLength(0)
})
})
describe("detectViolations - Concrete Repository in Use Case", () => {
it("should detect concrete repository in constructor", () => {
const code = `
class CreateUser {
constructor(private userRepo: PrismaUserRepository) {}
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/CreateUser.ts",
"application",
)
const concreteViolations = violations.filter(
(v) =>
v.violationType === REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
)
expect(concreteViolations).toHaveLength(1)
expect(concreteViolations[0].repositoryName).toBe("PrismaUserRepository")
})
it("should detect concrete repository as field", () => {
const code = `
class CreateUser {
private userRepo: MongoUserRepository
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/CreateUser.ts",
"application",
)
const concreteViolations = violations.filter(
(v) =>
v.violationType === REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
)
expect(concreteViolations).toHaveLength(1)
})
it("should not detect interface in constructor", () => {
const code = `
class CreateUser {
constructor(private userRepo: IUserRepository) {}
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/CreateUser.ts",
"application",
)
const concreteViolations = violations.filter(
(v) =>
v.violationType === REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
)
expect(concreteViolations).toHaveLength(0)
})
})
describe("detectViolations - new Repository() in Use Case", () => {
it("should detect repository instantiation with new", () => {
const code = `
class CreateUser {
async execute(data: CreateUserRequest) {
const repo = new UserRepository()
await repo.save(user)
}
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/CreateUser.ts",
"application",
)
const newRepoViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
)
expect(newRepoViolations).toHaveLength(1)
expect(newRepoViolations[0].repositoryName).toBe("UserRepository")
})
it("should detect multiple repository instantiations", () => {
const code = `
class ComplexUseCase {
async execute() {
const userRepo = new UserRepository()
const orderRepo = new OrderRepository()
await userRepo.save(user)
await orderRepo.save(order)
}
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/ComplexUseCase.ts",
"application",
)
const newRepoViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
)
expect(newRepoViolations).toHaveLength(2)
})
it("should not detect commented out new Repository()", () => {
const code = `
class CreateUser {
async execute(data: CreateUserRequest) {
// const repo = new UserRepository()
await this.userRepo.save(user)
}
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/CreateUser.ts",
"application",
)
const newRepoViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
)
expect(newRepoViolations).toHaveLength(0)
})
})
describe("detectViolations - Non-Domain Method Names", () => {
it("should detect technical method names", () => {
const code = `
interface IUserRepository {
findOne(id: string): Promise<User>
findMany(filter: any): Promise<User[]>
insert(user: User): Promise<void>
updateOne(id: string, data: any): Promise<void>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
const methodViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
)
expect(methodViolations.length).toBeGreaterThan(0)
expect(methodViolations.some((v) => v.methodName === "findOne")).toBe(true)
expect(methodViolations.some((v) => v.methodName === "insert")).toBe(true)
})
it("should not detect domain language method names", () => {
const code = `
interface IUserRepository {
findById(id: UserId): Promise<User | null>
findByEmail(email: Email): Promise<User | null>
save(user: User): Promise<void>
delete(id: UserId): Promise<void>
search(criteria: SearchCriteria): Promise<User[]>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
const methodViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
)
expect(methodViolations).toHaveLength(0)
})
it("should detect SQL terminology", () => {
const code = `
interface IUserRepository {
select(id: string): Promise<User>
query(filter: any): Promise<User[]>
execute(sql: string): Promise<any>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
const methodViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
)
expect(methodViolations.length).toBeGreaterThan(0)
})
})
describe("isOrmType", () => {
it("should identify Prisma types", () => {
expect(detector.isOrmType("Prisma.UserWhereInput")).toBe(true)
expect(detector.isOrmType("PrismaClient")).toBe(true)
})
it("should identify TypeORM decorators", () => {
expect(detector.isOrmType("@Entity")).toBe(true)
expect(detector.isOrmType("@Column")).toBe(true)
expect(detector.isOrmType("@ManyToOne")).toBe(true)
})
it("should identify Mongoose types", () => {
expect(detector.isOrmType("Schema")).toBe(true)
expect(detector.isOrmType("Model<User>")).toBe(true)
expect(detector.isOrmType("Document")).toBe(true)
})
it("should not identify domain types", () => {
expect(detector.isOrmType("User")).toBe(false)
expect(detector.isOrmType("UserId")).toBe(false)
expect(detector.isOrmType("Email")).toBe(false)
})
})
describe("isDomainMethodName", () => {
it("should identify domain method names", () => {
expect(detector.isDomainMethodName("findById")).toBe(true)
expect(detector.isDomainMethodName("findByEmail")).toBe(true)
expect(detector.isDomainMethodName("save")).toBe(true)
expect(detector.isDomainMethodName("delete")).toBe(true)
expect(detector.isDomainMethodName("create")).toBe(true)
expect(detector.isDomainMethodName("search")).toBe(true)
})
it("should reject technical method names", () => {
expect(detector.isDomainMethodName("findOne")).toBe(false)
expect(detector.isDomainMethodName("findMany")).toBe(false)
expect(detector.isDomainMethodName("insert")).toBe(false)
expect(detector.isDomainMethodName("updateOne")).toBe(false)
expect(detector.isDomainMethodName("query")).toBe(false)
expect(detector.isDomainMethodName("execute")).toBe(false)
})
})
describe("isRepositoryInterface", () => {
it("should identify repository interfaces in domain", () => {
expect(
detector.isRepositoryInterface(
"src/domain/repositories/IUserRepository.ts",
"domain",
),
).toBe(true)
expect(
detector.isRepositoryInterface(
"src/domain/repositories/IOrderRepository.ts",
"domain",
),
).toBe(true)
})
it("should not identify repository implementations in infrastructure", () => {
expect(
detector.isRepositoryInterface(
"src/infrastructure/repositories/UserRepository.ts",
"infrastructure",
),
).toBe(false)
})
it("should not identify non-repository files", () => {
expect(detector.isRepositoryInterface("src/domain/entities/User.ts", "domain")).toBe(
false,
)
})
})
describe("isUseCase", () => {
it("should identify use cases", () => {
expect(
detector.isUseCase("src/application/use-cases/CreateUser.ts", "application"),
).toBe(true)
expect(
detector.isUseCase("src/application/use-cases/UpdateProfile.ts", "application"),
).toBe(true)
expect(
detector.isUseCase("src/application/use-cases/DeleteOrder.ts", "application"),
).toBe(true)
})
it("should not identify DTOs as use cases", () => {
expect(
detector.isUseCase("src/application/dtos/UserResponseDto.ts", "application"),
).toBe(false)
})
it("should not identify use cases in wrong layer", () => {
expect(detector.isUseCase("src/domain/use-cases/CreateUser.ts", "domain")).toBe(false)
})
})
describe("getMessage and getSuggestion", () => {
it("should provide helpful message for ORM type violations", () => {
const code = `
interface IUserRepository {
findOne(query: Prisma.UserWhereInput): Promise<User>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
const ormViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE,
)
expect(ormViolations[0].getMessage()).toContain("ORM-specific type")
expect(ormViolations[0].getSuggestion()).toContain("domain types")
})
it("should provide helpful message for concrete repository violations", () => {
const code = `
class CreateUser {
constructor(private userRepo: PrismaUserRepository) {}
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/CreateUser.ts",
"application",
)
const concreteViolations = violations.filter(
(v) =>
v.violationType === REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE,
)
expect(concreteViolations[0].getMessage()).toContain("concrete repository")
expect(concreteViolations[0].getSuggestion()).toContain("interface")
})
it("should provide helpful message for new repository violations", () => {
const code = `
class CreateUser {
async execute() {
const repo = new UserRepository()
}
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/CreateUser.ts",
"application",
)
const newRepoViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE,
)
expect(newRepoViolations[0].getMessage()).toContain("new")
expect(newRepoViolations[0].getSuggestion()).toContain("dependency injection")
})
it("should provide helpful message for non-domain method violations", () => {
const code = `
interface IUserRepository {
findOne(id: string): Promise<User>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
const methodViolations = violations.filter(
(v) => v.violationType === REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME,
)
expect(methodViolations[0].getMessage()).toContain("technical name")
expect(methodViolations[0].getSuggestion()).toContain("domain language")
})
})
describe("Integration tests", () => {
it("should detect multiple violation types in same file", () => {
const code = `
interface IUserRepository {
findOne(query: Prisma.UserWhereInput): Promise<User>
insert(user: User): Promise<void>
findById(id: UserId): Promise<User | null>
}
`
const violations = detector.detectViolations(
code,
"src/domain/repositories/IUserRepository.ts",
"domain",
)
expect(violations.length).toBeGreaterThan(1)
const types = violations.map((v) => v.violationType)
expect(types).toContain(REPOSITORY_VIOLATION_TYPES.ORM_TYPE_IN_INTERFACE)
expect(types).toContain(REPOSITORY_VIOLATION_TYPES.NON_DOMAIN_METHOD_NAME)
})
it("should detect all violations in complex use case", () => {
const code = `
class CreateUser {
constructor(private userRepo: PrismaUserRepository) {}
async execute(data: CreateUserRequest) {
const repo = new OrderRepository()
await this.userRepo.save(user)
await repo.save(order)
}
}
`
const violations = detector.detectViolations(
code,
"src/application/use-cases/CreateUser.ts",
"application",
)
expect(violations.length).toBeGreaterThanOrEqual(2)
const types = violations.map((v) => v.violationType)
expect(types).toContain(REPOSITORY_VIOLATION_TYPES.CONCRETE_REPOSITORY_IN_USE_CASE)
expect(types).toContain(REPOSITORY_VIOLATION_TYPES.NEW_REPOSITORY_IN_USE_CASE)
})
})
})