mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
import { ValueObject } from "./ValueObject"
|
|
import { REPOSITORY_VIOLATION_TYPES } from "../../shared/constants/rules"
|
|
import {
|
|
REPOSITORY_FALLBACK_SUGGESTIONS,
|
|
REPOSITORY_PATTERN_MESSAGES,
|
|
VIOLATION_EXAMPLE_VALUES,
|
|
} 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 || VIOLATION_EXAMPLE_VALUES.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 || VIOLATION_EXAMPLE_VALUES.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 || VIOLATION_EXAMPLE_VALUES.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 || VIOLATION_EXAMPLE_VALUES.USER_REPOSITORY})`,
|
|
`✅ Good: constructor(private repo: I${this.props.repositoryName?.replace(/^.*?([A-Z]\w+)$/, "$1") || VIOLATION_EXAMPLE_VALUES.USER_REPOSITORY})`,
|
|
].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 detailsMatch = /Consider: (.+)$/.exec(this.props.details)
|
|
const smartSuggestion = detailsMatch ? detailsMatch[1] : null
|
|
|
|
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 fallbackSuggestion =
|
|
technicalToDomain[this.props.methodName as keyof typeof technicalToDomain]
|
|
const finalSuggestion =
|
|
smartSuggestion || fallbackSuggestion || REPOSITORY_FALLBACK_SUGGESTIONS.DEFAULT
|
|
|
|
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 || VIOLATION_EXAMPLE_VALUES.FIND_ONE}()`,
|
|
`✅ Good: ${finalSuggestion}`,
|
|
].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>
|
|
}`
|
|
}
|
|
}
|