- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes. - Introduced configuration loading flow in configuration-management skill to clarify the configuration process. - Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers. - Enhanced various skills with additional diagrams to improve understanding of complex concepts. These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
11 KiB
name, description
| name | description |
|---|---|
| repository-pattern | Repository pattern implementation and best practices for GoodGo microservices. Use when implementing data access layers, extending BaseRepository, writing database queries, handling transactions, or optimizing database operations. |
Repository Pattern
When to Use This Skill
Use this skill when:
- Implementing data access layers for new modules
- Extending BaseRepository for specific entity types
- Writing custom database queries
- Handling database transactions
- Optimizing database queries and operations
- Testing repository implementations
- Organizing data access code
Core Concepts
Repository Pattern Benefits
- Abstraction: Separates business logic from data access
- Testability: Easy to mock repositories for testing
- Maintainability: Centralized database operations
- Consistency: Standardized CRUD operations
- Type Safety: TypeScript generics provide type safety
Repository Architecture
The repository pattern creates an abstraction layer between the service layer and data access layer, providing a clean separation of concerns.
graph TB
Controller["Controller<br/>(HTTP Handler)"] --> Service["Service Layer<br/>(Business Logic)"]
Service --> Repository["Repository<br/>(Data Access)"]
Repository --> Prisma["Prisma Client<br/>(ORM)"]
Prisma --> Database[("Database<br/>(PostgreSQL)")]
style Controller fill:#e1f5ff
style Service fill:#fff4e1
style Repository fill:#e8f5e9
style Prisma fill:#f3e5f5
style Database fill:#ffebee
BaseRepository Class
The BaseRepository abstract class provides common database operations that can be extended:
findById(id)- Find entity by IDfindByUnique(field, value)- Find by unique fieldfindAll(options)- Find all with filtering, pagination, sortingcreate(data)- Create new entityupdate(id, data)- Update entitydelete(id)- Delete entitycount(where)- Count entitiesexists(id)- Check if entity existstransaction(callback)- Execute transaction
Class Hierarchy
Repositories extend the BaseRepository abstract class, inheriting common CRUD operations while allowing custom query methods.
classDiagram
class BaseRepository {
<<abstract>>
#prisma: PrismaClient
#modelName: string
+findById(id: string) Promise~T~null~
+findByUnique(field: string, value: any) Promise~T~null~
+findAll(options?: any) Promise~T[]~
+create(data: CreateInput) Promise~T~
+update(id: string, data: UpdateInput) Promise~T~
+delete(id: string) Promise~boolean~
+count(where?: any) Promise~number~
+exists(id: string) Promise~boolean~
+transaction(callback: Function) Promise~R~
}
class IRepository {
<<interface>>
+findById(id: string) Promise~T~null~
+findByUnique(field: string, value: any) Promise~T~null~
+findAll(options?: any) Promise~T[]~
+create(data: CreateInput) Promise~T~
+update(id: string, data: UpdateInput) Promise~T~
+delete(id: string) Promise~boolean~
+count(where?: any) Promise~number~
+exists(id: string) Promise~boolean~
}
class UserRepository {
+findByEmail(email: string) Promise~User~null~
+findByUsername(username: string) Promise~User~null~
+findWithPermissions(userId: string) Promise~User~null~
+findActiveUsers(organizationId?: string) Promise~User[]~
}
class ProductRepository {
+findByCategory(categoryId: string) Promise~Product[]~
+findActiveProducts() Promise~Product[]~
}
BaseRepository <|-- UserRepository : extends
BaseRepository <|-- ProductRepository : extends
IRepository <|.. UserRepository : implements
Note: Specific repositories like UserRepository and ProductRepository extend BaseRepository and can implement the IRepository interface for additional type safety. They inherit all base CRUD methods and add domain-specific query methods.
Patterns
Extending BaseRepository
import { PrismaClient, User } from '@prisma/client';
import { BaseRepository } from '../modules/common/repository';
export class UserRepository extends BaseRepository<User, CreateUserInput, UpdateUserInput> {
constructor(prisma: PrismaClient) {
super(prisma, 'user');
}
// Add custom methods
async findByEmail(email: string): Promise<User | null> {
return this.prisma.user.findUnique({
where: { email },
});
}
async findByUsername(username: string): Promise<User | null> {
return this.prisma.user.findUnique({
where: { username },
});
}
}
Custom Query Methods
Add domain-specific query methods:
export class UserRepository extends BaseRepository<User, CreateUserInput, UpdateUserInput> {
// Find user with related data
async findWithPermissions(userId: string): Promise<User | null> {
return this.prisma.user.findUnique({
where: { id: userId },
include: {
userRoles: {
include: { role: true },
},
userPermissions: {
include: { permission: true },
},
},
});
}
// Complex query with filtering
async findActiveUsers(organizationId?: string): Promise<User[]> {
return this.prisma.user.findMany({
where: {
isActive: true,
...(organizationId && { organizationId }),
},
orderBy: { createdAt: 'desc' },
});
}
}
Using Repository Interface
Implement the IRepository interface for type safety:
import { IRepository } from '../modules/common/repository';
export class UserRepository
extends BaseRepository<User, CreateUserInput, UpdateUserInput>
implements IRepository<User, CreateUserInput, UpdateUserInput> {
// Implementation...
}
Error Handling
BaseRepository handles errors automatically:
async findById(id: string): Promise<T | null> {
try {
// Database operation
const entity = await this.prisma.user.findUnique({ where: { id } });
return entity;
} catch (error: any) {
logger.error(`Failed to find ${this.modelName} by ID`, { error, id });
throw new DatabaseError(`Failed to find ${this.modelName}`, { id, originalError: error });
}
}
Transactions
Use repository transaction method for multiple operations:
await repository.transaction(async (tx) => {
const user = await tx.user.create({ data: userData });
await tx.userProfile.create({ data: { userId: user.id, ...profileData } });
return user;
});
Transaction Flow:
sequenceDiagram
participant Service
participant Repository
participant Prisma as Prisma Client
participant DB as Database
Service->>Repository: transaction(callback)
Repository->>Prisma: $transaction(callback)
Prisma->>DB: BEGIN TRANSACTION
Note over Service,DB: All operations use transaction client (tx)
Service->>Repository: tx.user.create(data)
Repository->>Prisma: tx.user.create(data)
Prisma->>DB: INSERT INTO users ...
DB-->>Prisma: User created
Prisma-->>Repository: User entity
Repository-->>Service: User entity
Service->>Repository: tx.userProfile.create(data)
Repository->>Prisma: tx.userProfile.create(data)
Prisma->>DB: INSERT INTO user_profiles ...
DB-->>Prisma: Profile created
Prisma-->>Repository: Profile entity
Repository-->>Service: Profile entity
alt All operations succeed
Prisma->>DB: COMMIT
DB-->>Prisma: Transaction committed
Prisma-->>Repository: Success result
Repository-->>Service: Success result
else Error occurs
Prisma->>DB: ROLLBACK
DB-->>Prisma: Transaction rolled back
Prisma-->>Repository: Error thrown
Repository-->>Service: DatabaseError thrown
end
Important: All operations within the transaction callback must use the transaction client (tx) parameter, not the main Prisma client, to ensure atomicity.
Query Options
Use Prisma query options in findAll:
// Pagination
const users = await userRepository.findAll({
skip: (page - 1) * limit,
take: limit,
});
// Filtering
const activeUsers = await userRepository.findAll({
where: { isActive: true },
});
// Sorting
const recentUsers = await userRepository.findAll({
orderBy: { createdAt: 'desc' },
});
// Including relations
const usersWithRoles = await userRepository.findAll({
include: { userRoles: true },
});
Best Practices
- Extend BaseRepository: Always extend BaseRepository instead of implementing from scratch
- Custom Methods: Add domain-specific query methods in repository subclasses
- Type Safety: Use TypeScript generics for type safety
- Error Handling: Let BaseRepository handle common errors, handle domain-specific errors in custom methods
- Logging: BaseRepository handles logging automatically
- Transactions: Use repository transaction method for multi-step operations
- Query Optimization: Use Prisma query options (select, include) to optimize queries
- Single Responsibility: Each repository handles one entity type
- Dependency Injection: Inject PrismaClient in constructor for testability
Common Mistakes
- Not Extending BaseRepository: Implementing CRUD from scratch instead of extending
- Business Logic in Repository: Putting business logic in repository instead of service layer
- Exposing Prisma Client: Exposing raw Prisma client instead of using repository methods
- Missing Error Handling: Not handling errors in custom query methods
- Over-fetching Data: Using
includeunnecessarily, fetching too much data - No Type Safety: Not using TypeScript generics properly
- Transaction Mistakes: Not using repository transaction method for related operations
Troubleshooting
Type Errors with Prisma
Problem: TypeScript errors when using Prisma client methods
Solution: Ensure Prisma client is generated: pnpm prisma generate. Use proper type assertions if needed.
Transaction Rollback Issues
Problem: Transaction not rolling back on error
Solution: Ensure all operations in transaction callback use the transaction client (tx) parameter, not the main Prisma client.
Performance Issues
Problem: Slow queries or N+1 query problems
Solution: Use include to fetch related data in single query. Use select to limit fields. Add database indexes.
Resources
- BaseRepository - Base repository implementation
- User Repository - Example repository
- Database Prisma - Prisma ORM patterns
- Error Handling - Error handling in repositories