Files
pos-system/.agent/rules/repository-pattern.md

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

  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.

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