Skip to content

Repository API Reference

Comprehensive API documentation for PGRestify's ORM-style Repository pattern with full ORM-inspired functionality.

Repository Types

PGRestify provides three repository types:

🏗️ SimpleRepository

Basic CRUD operations with type safety.

🔧 BaseRepository

Advanced repository with query builder support.

🚀 CustomRepositoryBase

Extensible repository for custom business logic.


SimpleRepository Interface

typescript
interface SimpleRepository<T extends Record<string, unknown> = Record<string, unknown>> {
  // Basic find methods
  find(): Promise<T[]>;
  findBy(where: Partial<T>): Promise<T[]>;
  findOne(where: Partial<T>): Promise<T | null>;
  findOneBy(where: Partial<T>): Promise<T | null>;

  // Create/Update methods
  save(entity: Partial<T>): Promise<T>;
  update(criteria: Partial<T>, updates: Partial<T>): Promise<T[]>;
  delete(criteria: Partial<T>): Promise<void>;

  // Advanced querying
  createQueryBuilder(): SelectQueryBuilder<T>;
  count(where?: Partial<T>): Promise<number>;
}

BaseRepository Interface

typescript
interface BaseRepository<T extends Record<string, unknown> = Record<string, unknown>> {
  // Find methods with options
  find(options?: FindManyOptions<T>): Promise<T[]>;
  findBy(where: FindOptionsWhere<T>): Promise<T[]>;
  findOne(options?: FindOneOptions<T>): Promise<T | null>;
  findOneBy(where: FindOptionsWhere<T>): Promise<T | null>;
  findOneOrFail(options?: FindOneOptions<T>): Promise<T>;

  // CRUD operations
  save(entity: Partial<T>): Promise<T>;
  create(entityData: Partial<T>): Promise<T>;
  update(criteria: Partial<T>, updates: Partial<T>): Promise<T[]>;
  delete(criteria: Partial<T>): Promise<void>;
  remove(entity: T): Promise<void>;

  // Utility methods
  count(where?: Partial<T>): Promise<number>;
  increment(criteria: Partial<T>, propertyPath: string, value: number): Promise<void>;
  decrement(criteria: Partial<T>, propertyPath: string, value: number): Promise<void>;

  // Query builder
  createQueryBuilder(alias?: string): SelectQueryBuilder<T>;
}

CustomRepositoryBase Interface

typescript
abstract class CustomRepositoryBase<T extends Record<string, unknown> = Record<string, unknown>> {
  protected tableName: string;
  protected httpClient: HttpClient;
  protected cache: QueryCache;
  protected auth: AuthManager;
  protected config: ClientConfig;

  // Inherited from BaseRepository
  find(options?: FindManyOptions<T>): Promise<T[]>;
  findBy(where: FindOptionsWhere<T>): Promise<T[]>;
  findOne(options?: FindOneOptions<T>): Promise<T | null>;
  findOneBy(where: FindOptionsWhere<T>): Promise<T | null>;
  findOneOrFail(options?: FindOneOptions<T>): Promise<T>;

  save(entity: Partial<T>): Promise<T>;
  create(entityData: Partial<T>): Promise<T>;
  update(criteria: Partial<T>, updates: Partial<T>): Promise<T[]>;
  delete(criteria: Partial<T>): Promise<void>;
  remove(entity: T): Promise<void>;

  count(where?: Partial<T>): Promise<number>;
  increment(criteria: Partial<T>, propertyPath: string, value: number): Promise<void>;
  decrement(criteria: Partial<T>, propertyPath: string, value: number): Promise<void>;

  // Query builder access
  createQueryBuilder(alias?: string): SelectQueryBuilder<T>;
  createAdvancedQuery(): SelectQueryBuilder<T>;
}

Creating Repositories

typescript
// Define your entity interface
interface User {
  id: number;
  name: string;
  email: string;
  active: boolean;
  created_at: string;
}

// Create a simple repository
const userRepo = client.getRepository<User>('users');

// Create a custom repository
import { CustomRepositoryBase } from '@webcoded/pgrestify';

class UserRepository extends CustomRepositoryBase<User> {
  async findActiveUsers(): Promise<User[]> {
    return this.createQueryBuilder()
      .where('active = :active', { active: true })
      .andWhere('verified = :verified', { verified: true })
      .getMany();
  }
}

const customUserRepo = client.getCustomRepository(UserRepository, 'users');

Find Methods

typescript
// Find all records
const allUsers = await userRepo.find();

// Find with criteria
const activeUsers = await userRepo.findBy({ active: true });

// Find single record
const user = await userRepo.findOne({ email: 'john@example.com' });
const userById = await userRepo.findOneBy({ id: 123 });

// Find or throw error
try {
  const requiredUser = await userRepo.findOneOrFail({ id: 999 });
} catch (error) {
  console.error('User not found');
}

// Find with options
const recentUsers = await userRepo.find({
  where: { active: true },
  order: { created_at: 'DESC' },
  take: 10,
  skip: 0
});

Create and Update Methods

typescript
// Create a new user
const newUser = await userRepo.save({
  name: 'John Doe',
  email: 'john@example.com',
  active: true
});

// Create with explicit method
const createdUser = await userRepo.create({
  name: 'Jane Doe',
  email: 'jane@example.com',
  active: true
});

// Update by criteria
const updatedUsers = await userRepo.update(
  { id: 123 }, 
  { name: 'Updated Name', active: false }
);

// Update multiple records
await userRepo.update(
  { active: false }, 
  { active: true }
);

// Increment/Decrement counters
await userRepo.increment({ id: 123 }, 'login_count', 1);
await userRepo.decrement({ id: 123 }, 'credit_balance', 50);

Delete Methods

typescript
// Remove a specific entity
await userRepo.remove(user);

// Delete by criteria
await userRepo.delete({ active: false });

// Delete inactive users older than 1 year
await userRepo.delete({ 
  active: false,
  // Note: Complex date conditions better handled with query builder
});

Advanced Querying with SelectQueryBuilder

typescript
// Basic query builder usage
const activeUsers = await userRepo
  .createQueryBuilder()
  .where('active = :active', { active: true })
  .orderBy('created_at', 'DESC')
  .getMany();

// Complex query with multiple conditions
const complexQuery = await userRepo
  .createQueryBuilder()
  .select(['id', 'name', 'email', 'created_at'])
  .where('active = :active', { active: true })
  .andWhere('age >= :minAge', { minAge: 18 })
  .andWhere('created_at >= :date', { date: '2024-01-01' })
  .orderBy('created_at', 'DESC')
  .addOrderBy('name', 'ASC')
  .limit(20)
  .offset(40)
  .getMany();

// Query with JOINs (PostgREST embedded resources)
const usersWithPosts = await userRepo
  .createQueryBuilder()
  .leftJoinAndSelect('posts', 'post')
  .leftJoinAndSelect('profile', 'profile')
  .where('active = :active', { active: true })
  .getMany();

// Using brackets for complex conditions
import { Brackets } from '@webcoded/pgrestify';

const complexConditions = await userRepo
  .createQueryBuilder()
  .where('active = :active', { active: true })
  .andWhere(new Brackets(qb => {
    qb.where('role = :admin', { admin: 'admin' })
      .orWhere('verified = :verified', { verified: true });
  }))
  .getMany();

// Subqueries with EXISTS
const usersWithPosts = await userRepo
  .createQueryBuilder()
  .whereExists(subQuery => {
    subQuery
      .select('1')
      .from('posts')
      .where('posts.user_id = users.id');
  })
  .getMany();

// Get single results
const singleUser = await userRepo
  .createQueryBuilder()
  .where('email = :email', { email: 'john@example.com' })
  .getOne(); // Returns User | null

const requiredUser = await userRepo
  .createQueryBuilder()
  .where('id = :id', { id: 123 })
  .getOneOrFail(); // Throws error if not found

// Get count
const activeUserCount = await userRepo
  .createQueryBuilder()
  .where('active = :active', { active: true })
  .getCount();

Custom Repository Examples

typescript
import { CustomRepositoryBase, Brackets } from '@webcoded/pgrestify';

class UserRepository extends CustomRepositoryBase<User> {
  // Find active verified users
  async findActiveUsers(): Promise<User[]> {
    return this.createQueryBuilder()
      .where('active = :active', { active: true })
      .andWhere('verified = :verified', { verified: true })
      .orderBy('created_at', 'DESC')
      .getMany();
  }

  // Find users by role with complex conditions
  async findUsersByRole(role: string, includeInactive = false): Promise<User[]> {
    const query = this.createQueryBuilder()
      .where('role = :role', { role });

    if (!includeInactive) {
      query.andWhere('active = :active', { active: true });
    }

    return query
      .leftJoinAndSelect('profile', 'profile')
      .orderBy('name', 'ASC')
      .getMany();
  }

  // Complex search with multiple criteria
  async searchUsers(searchTerm: string, filters: {
    role?: string;
    active?: boolean;
    ageRange?: { min?: number; max?: number };
  }): Promise<User[]> {
    const query = this.createQueryBuilder();

    // Text search
    if (searchTerm) {
      query.where(new Brackets(qb => {
        qb.where('name ILIKE :search', { search: `%${searchTerm}%` })
          .orWhere('email ILIKE :search', { search: `%${searchTerm}%` });
      }));
    }

    // Apply filters
    if (filters.role) {
      query.andWhere('role = :role', { role: filters.role });
    }

    if (filters.active !== undefined) {
      query.andWhere('active = :active', { active: filters.active });
    }

    if (filters.ageRange) {
      if (filters.ageRange.min) {
        query.andWhere('age >= :minAge', { minAge: filters.ageRange.min });
      }
      if (filters.ageRange.max) {
        query.andWhere('age <= :maxAge', { maxAge: filters.ageRange.max });
      }
    }

    return query
      .orderBy('name', 'ASC')
      .getMany();
  }

  // Find users with recent activity
  async findUsersWithRecentActivity(days: number): Promise<User[]> {
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - days);

    return this.createQueryBuilder()
      .where('last_login >= :cutoff', { cutoff: cutoffDate.toISOString() })
      .orWhere('updated_at >= :cutoff', { cutoff: cutoffDate.toISOString() })
      .orderBy('last_login', 'DESC')
      .getMany();
  }

  // Pagination example
  async findUsersPaginated(page: number, pageSize: number): Promise<{
    users: User[];
    total: number;
    hasMore: boolean;
  }> {
    const [users, total] = await Promise.all([
      this.createQueryBuilder()
        .where('active = :active', { active: true })
        .orderBy('created_at', 'DESC')
        .limit(pageSize)
        .offset((page - 1) * pageSize)
        .getMany(),
      this.createQueryBuilder()
        .where('active = :active', { active: true })
        .getCount()
    ]);

    return {
      users,
      total,
      hasMore: (page * pageSize) < total
    };
  }
}

// Use the custom repository
const customUserRepo = client.getCustomRepository(UserRepository, 'users');

// Use custom methods
const activeUsers = await customUserRepo.findActiveUsers();
const admins = await customUserRepo.findUsersByRole('admin');
const searchResults = await customUserRepo.searchUsers('john', {
  role: 'user',
  active: true,
  ageRange: { min: 18, max: 65 }
});

Repository Factory

typescript
// Using the RepositoryFactory for multiple repositories
const repositoryFactory = client.repositoryFactory;

// Create multiple repositories
const userRepo = repositoryFactory.getRepository<User>('users');
const postRepo = repositoryFactory.getRepository<Post>('posts');
const commentRepo = repositoryFactory.getRepository<Comment>('comments');

// Create custom repositories
const customUserRepo = repositoryFactory.getCustomRepository(UserRepository, 'users');

Data Manager

typescript
// Using the DataManager for coordinated operations
const dataManager = client.manager;

// Perform related operations
async function createUserWithProfile(userData: Partial<User>, profileData: any) {
  // Create user
  const user = await dataManager.getRepository<User>('users').save(userData);
  
  // Create related profile
  const profile = await dataManager.getRepository('profiles').save({
    ...profileData,
    user_id: user.id
  });

  return { user, profile };
}

Type Safety and Error Handling

typescript
// Complete type safety with TypeScript
const typeSafeUser = await userRepo.save({
  name: 'John Doe',              // ✅ Correct type
  email: 'john@example.com',     // ✅ Correct type
  active: true,                  // ✅ Correct type
  // age: '30'                   // ❌ TypeScript error - wrong type
});

// Proper error handling
try {
  const user = await userRepo.findOneOrFail({ id: 999 });
  console.log('Found user:', user.name);
} catch (error) {
  console.error('User not found:', error.message);
}

// Handle validation errors
try {
  await userRepo.save({
    name: '',  // Invalid empty name
    email: 'invalid-email'  // Invalid email format
  });
} catch (error) {
  if (error.name === 'ValidationError') {
    console.error('Validation failed:', error.details);
  }
}

Query Performance and Best Practices

typescript
// ✅ Good: Select only needed columns
const lightUsers = await userRepo
  .createQueryBuilder()
  .select(['id', 'name', 'email'])
  .where('active = :active', { active: true })
  .getMany();

// ✅ Good: Use parameter binding for security
const searchUsers = await userRepo
  .createQueryBuilder()
  .where('name ILIKE :search', { search: `%${searchTerm}%` })
  .getMany();

// ✅ Good: Use pagination for large datasets
const paginatedUsers = await userRepo
  .createQueryBuilder()
  .orderBy('created_at', 'DESC')
  .limit(20)
  .offset(page * 20)
  .getMany();

// ✅ Good: Use appropriate JOINs
const usersWithProfiles = await userRepo
  .createQueryBuilder()
  .leftJoinAndSelect('profile', 'profile')
  .where('active = :active', { active: true })
  .getMany();

// ❌ Avoid: Too many unfiltered queries
// const allUsers = await userRepo.find(); // Could return millions of records

Advanced Repository Patterns

typescript
// Repository with caching
class CachedUserRepository extends CustomRepositoryBase<User> {
  private cache = new Map<string, User[]>();

  async findActiveUsers(): Promise<User[]> {
    const cacheKey = 'active_users';
    
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey)!;
    }

    const users = await this.createQueryBuilder()
      .where('active = :active', { active: true })
      .getMany();

    this.cache.set(cacheKey, users);
    
    // Clear cache after 5 minutes
    setTimeout(() => this.cache.delete(cacheKey), 5 * 60 * 1000);
    
    return users;
  }
}

// Repository with validation
class ValidatedUserRepository extends CustomRepositoryBase<User> {
  async save(userData: Partial<User>): Promise<User> {
    // Custom validation
    if (userData.email && !this.isValidEmail(userData.email)) {
      throw new Error('Invalid email format');
    }

    if (userData.age && (userData.age < 0 || userData.age > 150)) {
      throw new Error('Invalid age range');
    }

    // Pre-process data
    if (userData.email) {
      userData.email = userData.email.toLowerCase().trim();
    }

    return super.save(userData);
  }

  private isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

Repository Factory and Management

typescript
// Central repository management
class UserService {
  private userRepo: UserRepository;
  private profileRepo: SimpleRepository<Profile>;

  constructor(private client: PostgRESTClient) {
    this.userRepo = client.getCustomRepository(UserRepository, 'users');
    this.profileRepo = client.getRepository<Profile>('profiles');
  }

  async createUserWithProfile(userData: Partial<User>, profileData: Partial<Profile>) {
    // Create user first
    const user = await this.userRepo.save(userData);
    
    // Then create profile
    const profile = await this.profileRepo.save({
      ...profileData,
      user_id: user.id
    });

    return { user, profile };
  }

  async getUserDashboard(userId: number) {
    const [user, recentActivity, stats] = await Promise.all([
      this.userRepo.findOneOrFail({ id: userId }),
      this.userRepo.findUsersWithRecentActivity(30),
      this.getUserStats(userId)
    ]);

    return { user, recentActivity, stats };
  }

  private async getUserStats(userId: number) {
    // Custom statistics using query builder
    return this.userRepo
      .createQueryBuilder()
      .select(['COUNT(*) as total_posts'])
      .leftJoinAndSelect('posts', 'post')
      .where('id = :userId', { userId })
      .getOne();
  }
}

Testing Repository Methods

typescript
// Example unit test for custom repository
describe('UserRepository', () => {
  let userRepo: UserRepository;
  let mockClient: jest.Mocked<PostgRESTClient>;

  beforeEach(() => {
    // Setup mocked client
    mockClient = createMockClient();
    userRepo = mockClient.getCustomRepository(UserRepository, 'users');
  });

  it('should find active users', async () => {
    // Mock the expected response
    const mockUsers = [
      { id: 1, name: 'John', active: true, verified: true }
    ];

    jest.spyOn(userRepo, 'createQueryBuilder').mockReturnValue({
      where: jest.fn().mockReturnThis(),
      andWhere: jest.fn().mockReturnThis(),
      getMany: jest.fn().mockResolvedValue(mockUsers)
    } as any);

    const result = await userRepo.findActiveUsers();
    
    expect(result).toEqual(mockUsers);
    expect(userRepo.createQueryBuilder).toHaveBeenCalled();
  });
});

Best Practices Summary

✅ DO:

  • Use TypeScript generics for type safety
  • Implement parameter binding for security
  • Use custom repositories for business logic
  • Apply proper error handling
  • Use pagination for large datasets
  • Select only required columns
  • Implement proper validation
  • Use appropriate JOINs for related data
  • Cache frequently accessed data
  • Write unit tests for repository methods

❌ DON'T:

  • Query without filters on large tables
  • Concatenate user input into queries
  • Return all columns when only few are needed
  • Ignore error handling
  • Fetch all records without pagination
  • Put business logic in controllers
  • Skip input validation
  • Use eager loading excessively
  • Query inside loops
  • Forget to handle edge cases

🚀 Performance Tips:

  • Use database indexes for frequently queried columns
  • Implement caching strategies for read-heavy operations
  • Use connection pooling for high-traffic applications
  • Monitor query performance with logging
  • Use EXPLAIN ANALYZE for query optimization
  • Batch multiple operations when possible
  • Consider read replicas for read-heavy workloads

Released under the MIT License.