mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +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:
220
packages/guardian/examples/repository-pattern/README.md
Normal file
220
packages/guardian/examples/repository-pattern/README.md
Normal 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)
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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,
|
||||
) {}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()}`)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user