- Updated skill documentation files to include structured metadata for better organization. - Enhanced bilingual descriptions and guidelines for clarity in both English and Vietnamese. - Refined sections on usage, best practices, and related skills to ensure consistency across all documentation. - Improved formatting and removed outdated references to streamline the documentation experience. - Added best practices checklists to relevant skills for better usability and adherence to standards.
221 lines
6.8 KiB
Markdown
221 lines
6.8 KiB
Markdown
---
|
|
name: repository-pattern
|
|
description: 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
|
|
|
|
1. **Abstraction**: Separates business logic from data access
|
|
2. **Testability**: Easy to mock repositories for testing
|
|
3. **Maintainability**: Centralized database operations
|
|
4. **Consistency**: Standardized CRUD operations
|
|
5. **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 ID
|
|
- `findByUnique(field, value)` - Find by unique field
|
|
- `findAll(options)` - Find all with filtering, pagination, sorting
|
|
- `create(data)` - Create new entity
|
|
- `update(id, data)` - Update entity
|
|
- `delete(id)` - Delete entity
|
|
- `count(where)` - Count entities
|
|
- `exists(id)` - Check if entity exists
|
|
- `transaction(callback)` - Execute transaction
|
|
|
|
## Patterns
|
|
|
|
### Extending BaseRepository
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
1. **Extend BaseRepository**: Always extend BaseRepository instead of implementing from scratch
|
|
2. **Custom Methods**: Add domain-specific query methods in repository subclasses
|
|
3. **Type Safety**: Use TypeScript generics for type safety
|
|
4. **Error Handling**: Let BaseRepository handle common errors, handle domain-specific errors in custom methods
|
|
5. **Logging**: BaseRepository handles logging automatically
|
|
6. **Transactions**: Use repository transaction method for multi-step operations
|
|
7. **Query Optimization**: Use Prisma query options (select, include) to optimize queries
|
|
8. **Single Responsibility**: Each repository handles one entity type
|
|
9. **Dependency Injection**: Inject PrismaClient in constructor for testability
|
|
|
|
## Common Mistakes
|
|
|
|
1. **Not Extending BaseRepository**: Implementing CRUD from scratch instead of extending
|
|
2. **Business Logic in Repository**: Putting business logic in repository instead of service layer
|
|
3. **Exposing Prisma Client**: Exposing raw Prisma client instead of using repository methods
|
|
4. **Missing Error Handling**: Not handling errors in custom query methods
|
|
5. **Over-fetching Data**: Using `include` unnecessarily, fetching too much data
|
|
6. **No Type Safety**: Not using TypeScript generics properly
|
|
7. **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](../../services/iam-service/src/modules/common/repository.ts) - Base repository implementation
|
|
- [User Repository](../../services/iam-service/src/repositories/user.repository.ts) - Example repository
|
|
- [Database Prisma](../database-prisma/SKILL.md) - Prisma ORM patterns
|
|
- [Error Handling](../error-handling-patterns/SKILL.md) - Error handling in repositories
|