7.9 KiB
trigger
| trigger |
|---|
| always_on |
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
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
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;
});
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.
Quick Reference
BaseRepository Methods:
| Method | Returns | Description |
|---|---|---|
findById(id) |
T | null |
Find by primary key |
findByUnique(field, value) |
T | null |
Find by unique field |
findAll(options) |
T[] |
Find with filters |
create(data) |
T |
Create entity |
update(id, data) |
T |
Update entity |
delete(id) |
void |
Delete entity |
count(where) |
number |
Count entities |
exists(id) |
boolean |
Check existence |
transaction(cb) |
R |
Execute transaction |
Query Options:
// Pagination
{ skip: 0, take: 10 }
// Filtering
{ where: { isActive: true } }
// Sorting
{ orderBy: { createdAt: 'desc' } }
// Relations
{ include: { roles: true } }
// Field selection
{ select: { id: true, email: true } }
Repository Template:
export class EntityRepository extends BaseRepository<Entity, CreateDto, UpdateDto> {
constructor(prisma: PrismaClient) {
super(prisma, 'entity');
}
// Custom query methods
async findByField(value: string): Promise<Entity | null> {
return this.prisma.entity.findUnique({ where: { field: value } });
}
}
Essential Imports:
import { PrismaClient } from '@prisma/client';
import { BaseRepository, IRepository } from '../modules/common/repository';
import { DatabaseError } from '../errors/http-error';
Resources
- BaseRepository - Base repository implementation
- User Repository - Example repository
- Database Prisma - Prisma ORM patterns
- Error Handling - Error handling in repositories