- 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.
335 lines
11 KiB
Markdown
335 lines
11 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
|
|
|
|
### Repository Architecture
|
|
|
|
The repository pattern creates an abstraction layer between the service layer and data access layer, providing a clean separation of concerns.
|
|
|
|
```mermaid
|
|
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 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
|
|
|
|
### Class Hierarchy
|
|
|
|
Repositories extend the `BaseRepository` abstract class, inheriting common CRUD operations while allowing custom query methods.
|
|
|
|
```mermaid
|
|
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
|
|
|
|
```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;
|
|
});
|
|
```
|
|
|
|
**Transaction Flow**:
|
|
|
|
```mermaid
|
|
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:
|
|
|
|
```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
|