mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
feat: add dependency direction enforcement (v0.4.0)
Implement dependency direction detection to enforce Clean Architecture rules: - Domain layer can only import from Domain and Shared - Application layer can only import from Domain, Application, and Shared - Infrastructure layer can import from all layers - Shared layer can be imported by all layers Added: - IDependencyDirectionDetector interface in domain layer - DependencyViolation value object with detailed suggestions and examples - DependencyDirectionDetector implementation in infrastructure - Integration with AnalyzeProject use case - New DEPENDENCY_DIRECTION rule in constants - 43 comprehensive tests covering all scenarios (100% passing) - Good and bad examples in examples directory Improvements: - Optimized extractLayerFromImport method to reduce complexity - Fixed indentation in DependencyGraph.ts - Updated getExampleFix to avoid false positives in old detector Test Results: - All 261 tests passing - Build successful - Self-check: 0 architecture violations in src code
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* ❌ BAD: Application layer with incorrect dependencies
|
||||
*
|
||||
* Application importing from Infrastructure layer
|
||||
* This violates Clean Architecture dependency rules!
|
||||
*/
|
||||
|
||||
import { User } from "../../good-architecture/domain/entities/User"
|
||||
import { Email } from "../../good-architecture/domain/value-objects/Email"
|
||||
import { UserId } from "../../good-architecture/domain/value-objects/UserId"
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Application importing from Infrastructure layer
|
||||
*/
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
export class CreateUser {
|
||||
/**
|
||||
* ❌ VIOLATION: Application use case depending on concrete infrastructure (Prisma)
|
||||
*/
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async execute(email: string): Promise<User> {
|
||||
const userId = UserId.generate()
|
||||
const emailVO = Email.create(email).value
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Application logic directly accessing database
|
||||
*/
|
||||
await this.prisma.user.create({
|
||||
data: {
|
||||
id: userId.getValue(),
|
||||
email: emailVO.getValue(),
|
||||
},
|
||||
})
|
||||
|
||||
return new User(userId, emailVO)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Application importing concrete email service from infrastructure
|
||||
*/
|
||||
import { SmtpEmailService } from "../../good-architecture/infrastructure/adapters/SmtpEmailService"
|
||||
|
||||
export class SendWelcomeEmail {
|
||||
/**
|
||||
* ❌ VIOLATION: Application depending on concrete infrastructure implementation
|
||||
* Should depend on IEmailService interface instead
|
||||
*/
|
||||
constructor(
|
||||
private prisma: PrismaClient,
|
||||
private emailService: SmtpEmailService,
|
||||
) {}
|
||||
|
||||
async execute(userId: string): Promise<void> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
throw new Error("User not found")
|
||||
}
|
||||
|
||||
await this.emailService.sendWelcomeEmail(user.email)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Application importing from infrastructure controller
|
||||
*/
|
||||
import { UserController } from "../../good-architecture/infrastructure/controllers/UserController"
|
||||
|
||||
export class ValidateUser {
|
||||
/**
|
||||
* ❌ VIOLATION: Application use case depending on infrastructure controller
|
||||
* The dependency direction is completely wrong!
|
||||
*/
|
||||
constructor(private userController: UserController) {}
|
||||
|
||||
async execute(userId: string): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Application importing HTTP framework
|
||||
*/
|
||||
import express from "express"
|
||||
|
||||
export class ProcessUserRequest {
|
||||
/**
|
||||
* ❌ VIOLATION: Application layer knows about HTTP/Express
|
||||
* HTTP concerns should be in infrastructure layer
|
||||
*/
|
||||
async execute(req: express.Request): Promise<void> {
|
||||
const email = req.body.email
|
||||
console.log(`Processing user: ${email}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Application importing infrastructure repository implementation
|
||||
*/
|
||||
import { InMemoryUserRepository } from "../../good-architecture/infrastructure/repositories/InMemoryUserRepository"
|
||||
|
||||
export class GetUser {
|
||||
/**
|
||||
* ❌ VIOLATION: Application depending on concrete repository implementation
|
||||
* Should depend on IUserRepository interface from domain
|
||||
*/
|
||||
constructor(private userRepo: InMemoryUserRepository) {}
|
||||
|
||||
async execute(userId: string): Promise<User | null> {
|
||||
return await this.userRepo.findById(UserId.from(userId))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* ❌ BAD: Domain layer with incorrect dependencies
|
||||
*
|
||||
* Domain importing from Application and Infrastructure layers
|
||||
* This violates Clean Architecture dependency rules!
|
||||
*/
|
||||
|
||||
import { Email } from "../../good-architecture/domain/value-objects/Email"
|
||||
import { UserId } from "../../good-architecture/domain/value-objects/UserId"
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Domain importing from Application layer
|
||||
*/
|
||||
import { UserResponseDto } from "../../good-architecture/application/dtos/UserResponseDto"
|
||||
|
||||
export class User {
|
||||
private readonly id: UserId
|
||||
private email: Email
|
||||
|
||||
constructor(id: UserId, email: Email) {
|
||||
this.id = id
|
||||
this.email = email
|
||||
}
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Domain entity returning DTO from application layer
|
||||
*/
|
||||
toDto(): UserResponseDto {
|
||||
return {
|
||||
id: this.id.getValue(),
|
||||
email: this.email.getValue(),
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Domain importing from Infrastructure layer
|
||||
*/
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
export class UserService {
|
||||
/**
|
||||
* ❌ VIOLATION: Domain service depending on concrete infrastructure implementation
|
||||
*/
|
||||
constructor(private prisma: PrismaClient) {}
|
||||
|
||||
async createUser(email: string): Promise<User> {
|
||||
const userId = UserId.generate()
|
||||
const emailVO = Email.create(email).value
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Domain logic directly accessing database
|
||||
*/
|
||||
await this.prisma.user.create({
|
||||
data: {
|
||||
id: userId.getValue(),
|
||||
email: emailVO.getValue(),
|
||||
},
|
||||
})
|
||||
|
||||
return new User(userId, emailVO)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Domain importing email service from infrastructure
|
||||
*/
|
||||
import { SmtpEmailService } from "../../good-architecture/infrastructure/adapters/SmtpEmailService"
|
||||
|
||||
export class UserRegistration {
|
||||
/**
|
||||
* ❌ VIOLATION: Domain depending on infrastructure email service
|
||||
*/
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private emailService: SmtpEmailService,
|
||||
) {}
|
||||
|
||||
async register(email: string): Promise<User> {
|
||||
const user = await this.userService.createUser(email)
|
||||
|
||||
/**
|
||||
* ❌ VIOLATION: Domain calling infrastructure service directly
|
||||
*/
|
||||
await this.emailService.sendWelcomeEmail(email)
|
||||
|
||||
return user
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* ✅ GOOD: Application layer with correct dependencies
|
||||
*
|
||||
* Application should only import from:
|
||||
* - Domain layer
|
||||
* - Other application files
|
||||
* - Shared utilities
|
||||
*
|
||||
* Application should NOT import from:
|
||||
* - Infrastructure layer
|
||||
*/
|
||||
|
||||
import { User } from "../domain/entities/User"
|
||||
import { Email } from "../domain/value-objects/Email"
|
||||
import { UserId } from "../domain/value-objects/UserId"
|
||||
import { IUserRepository } from "../domain/repositories/IUserRepository"
|
||||
import { Result } from "../../../src/shared/types/Result"
|
||||
|
||||
/**
|
||||
* ✅ Use case depends on domain interfaces (IUserRepository)
|
||||
* NOT on infrastructure implementations
|
||||
*/
|
||||
export class CreateUser {
|
||||
constructor(private readonly userRepo: IUserRepository) {}
|
||||
|
||||
async execute(request: CreateUserRequest): Promise<Result<UserResponseDto>> {
|
||||
const emailResult = Email.create(request.email)
|
||||
if (emailResult.isFailure) {
|
||||
return Result.fail(emailResult.error)
|
||||
}
|
||||
|
||||
const userId = UserId.generate()
|
||||
const user = new User(userId, emailResult.value)
|
||||
|
||||
await this.userRepo.save(user)
|
||||
|
||||
return Result.ok(UserMapper.toDto(user))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ DTO in application layer
|
||||
*/
|
||||
export interface CreateUserRequest {
|
||||
email: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface UserResponseDto {
|
||||
id: string
|
||||
email: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Mapper in application layer converting domain to DTO
|
||||
*/
|
||||
export class UserMapper {
|
||||
static toDto(user: User): UserResponseDto {
|
||||
return {
|
||||
id: user.getId().getValue(),
|
||||
email: user.getEmail().getValue(),
|
||||
createdAt: user.getCreatedAt().toISOString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Application defines Port (interface) for email service
|
||||
* Infrastructure will provide the Adapter (implementation)
|
||||
*/
|
||||
export interface IEmailService {
|
||||
sendWelcomeEmail(email: string): Promise<void>
|
||||
}
|
||||
|
||||
export class SendWelcomeEmail {
|
||||
constructor(
|
||||
private readonly userRepo: IUserRepository,
|
||||
private readonly emailService: IEmailService,
|
||||
) {}
|
||||
|
||||
async execute(userId: string): Promise<Result<void>> {
|
||||
const user = await this.userRepo.findById(UserId.from(userId))
|
||||
if (!user) {
|
||||
return Result.fail("User not found")
|
||||
}
|
||||
|
||||
await this.emailService.sendWelcomeEmail(user.getEmail().getValue())
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* ✅ GOOD: Domain layer with correct dependencies
|
||||
*
|
||||
* Domain should only import from:
|
||||
* - Other domain files
|
||||
* - Shared utilities
|
||||
*
|
||||
* Domain should NOT import from:
|
||||
* - Application layer
|
||||
* - Infrastructure layer
|
||||
*/
|
||||
|
||||
import { Email } from "../domain/value-objects/Email"
|
||||
import { UserId } from "../domain/value-objects/UserId"
|
||||
import { Result } from "../../../src/shared/types/Result"
|
||||
|
||||
/**
|
||||
* ✅ Domain entity using only domain value objects and shared types
|
||||
*/
|
||||
export class User {
|
||||
private readonly id: UserId
|
||||
private email: Email
|
||||
private readonly createdAt: Date
|
||||
|
||||
constructor(id: UserId, email: Email, createdAt: Date = new Date()) {
|
||||
this.id = id
|
||||
this.email = email
|
||||
this.createdAt = createdAt
|
||||
}
|
||||
|
||||
public getId(): UserId {
|
||||
return this.id
|
||||
}
|
||||
|
||||
public getEmail(): Email {
|
||||
return this.email
|
||||
}
|
||||
|
||||
public changeEmail(newEmail: Email): Result<void> {
|
||||
if (this.email.equals(newEmail)) {
|
||||
return Result.fail("Email is the same")
|
||||
}
|
||||
|
||||
this.email = newEmail
|
||||
return Result.ok()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Domain repository interface (not importing from infrastructure)
|
||||
*/
|
||||
export interface IUserRepository {
|
||||
findById(id: UserId): Promise<User | null>
|
||||
save(user: User): Promise<void>
|
||||
delete(id: UserId): Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* ✅ GOOD: Infrastructure layer with correct dependencies
|
||||
*
|
||||
* Infrastructure CAN import from:
|
||||
* - Domain layer
|
||||
* - Application layer
|
||||
* - Other infrastructure files
|
||||
* - Shared utilities
|
||||
* - External libraries (ORM, frameworks, etc.)
|
||||
*/
|
||||
|
||||
import { User } from "../domain/entities/User"
|
||||
import { UserId } from "../domain/value-objects/UserId"
|
||||
import { Email } from "../domain/value-objects/Email"
|
||||
import { IUserRepository } from "../domain/repositories/IUserRepository"
|
||||
import { CreateUser } from "../application/use-cases/CreateUser"
|
||||
import { UserResponseDto } from "../application/dtos/UserResponseDto"
|
||||
import { IEmailService } from "../application/ports/IEmailService"
|
||||
|
||||
/**
|
||||
* ✅ Infrastructure implements domain interface
|
||||
*/
|
||||
export class InMemoryUserRepository implements IUserRepository {
|
||||
private users: Map<string, User> = new Map()
|
||||
|
||||
async findById(id: UserId): Promise<User | null> {
|
||||
return this.users.get(id.getValue()) ?? null
|
||||
}
|
||||
|
||||
async save(user: User): Promise<void> {
|
||||
this.users.set(user.getId().getValue(), user)
|
||||
}
|
||||
|
||||
async delete(id: UserId): Promise<void> {
|
||||
this.users.delete(id.getValue())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Infrastructure provides Adapter implementing application Port
|
||||
*/
|
||||
export class SmtpEmailService implements IEmailService {
|
||||
constructor(
|
||||
private readonly host: string,
|
||||
private readonly port: number,
|
||||
) {}
|
||||
|
||||
async sendWelcomeEmail(email: string): Promise<void> {
|
||||
console.log(`Sending welcome email to ${email} via SMTP`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Controller uses application use cases and DTOs
|
||||
*/
|
||||
export class UserController {
|
||||
constructor(private readonly createUser: CreateUser) {}
|
||||
|
||||
async create(request: { email: string; name: string }): Promise<UserResponseDto> {
|
||||
const result = await this.createUser.execute(request)
|
||||
|
||||
if (result.isFailure) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
|
||||
return result.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Infrastructure can use external frameworks
|
||||
*/
|
||||
import express from "express"
|
||||
|
||||
export class ExpressServer {
|
||||
private app = express()
|
||||
|
||||
constructor(private readonly userController: UserController) {
|
||||
this.setupRoutes()
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
this.app.post("/users", async (req, res) => {
|
||||
const user = await this.userController.create(req.body)
|
||||
res.json(user)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Infrastructure can use ORM
|
||||
*/
|
||||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
export class PrismaUserRepository implements IUserRepository {
|
||||
constructor(private readonly prisma: PrismaClient) {}
|
||||
|
||||
async findById(id: UserId): Promise<User | null> {
|
||||
const userData = await this.prisma.user.findUnique({
|
||||
where: { id: id.getValue() },
|
||||
})
|
||||
|
||||
if (!userData) {
|
||||
return null
|
||||
}
|
||||
|
||||
return new User(UserId.from(userData.id), Email.create(userData.email).value)
|
||||
}
|
||||
|
||||
async save(user: User): Promise<void> {
|
||||
await this.prisma.user.upsert({
|
||||
where: { id: user.getId().getValue() },
|
||||
create: {
|
||||
id: user.getId().getValue(),
|
||||
email: user.getEmail().getValue(),
|
||||
},
|
||||
update: {
|
||||
email: user.getEmail().getValue(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async delete(id: UserId): Promise<void> {
|
||||
await this.prisma.user.delete({
|
||||
where: { id: id.getValue() },
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user