Files
pos-system/docs/en/skills/repository-pattern.md
Ho Ngoc Hai 9b6c585f57 Enhance documentation structure and improve bilingual support across skills
- 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.
2026-01-01 07:35:44 +07:00

6.8 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

  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

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

  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