Repository Management (EntityManager-Style)
PGRestify's client provides EntityManager-like functionality through its repository factory methods, offering a centralized interface for managing repositories and coordinating database operations across multiple tables.
Overview
The client's repository management provides:
- Repository Factory: Create and manage repository instances via
getRepository()
- Custom Repository Support: Register and retrieve custom repository classes via
getCustomRepository()
- Cache Management: Centralized cache control across all repositories
- Transaction Coordination: Coordinate operations across multiple tables
- Type Safety: Full TypeScript support with generic repository creation
Basic DataManager Usage
Getting Repository Instances
Repositories are available directly through the main client:
import { createClient } from '@webcoded/pgrestify';
interface User {
id: string;
email: string;
first_name: string;
last_name: string;
active: boolean;
created_at: string;
}
interface Post {
id: string;
title: string;
content: string;
author_id: string;
published: boolean;
created_at: string;
}
// Create client
const client = createClient({ url: 'http://localhost:3000' });
// Get repositories directly from client
const userRepository = client.getRepository<User>('users');
const postRepository = client.getRepository<Post>('posts');
Repository Management
The client creates and manages repository instances:
async function repositoryManagement() {
// Get repositories via client
const userRepo1 = client.getRepository<User>('users');
const userRepo2 = client.getRepository<User>('users');
// Different tables get different repositories
const postRepo = client.getRepository<Post>('posts');
// Use repositories for operations
const users = await userRepo1.find();
const posts = await postRepo.findBy({ published: true });
console.log(`Found ${users.length} users and ${posts.length} published posts`);
}
Advanced DataManager Features
Multi-Table Operations
Coordinate operations across multiple tables:
async function multiTableOperations() {
const userRepository = client.getRepository<User>('users');
const postRepository = client.getRepository<Post>('posts');
// Create user and their first post
const createUserWithPost = async (userData: Partial<User>, postData: Partial<Post>) => {
try {
// Create user first
const createdUsers = await userRepository.insert(userData);
const newUser = createdUsers[0];
if (!newUser) {
throw new Error('Failed to create user');
}
// Create post linked to new user
const postWithAuthor = {
...postData,
author_id: newUser.id
};
const createdPosts = await postRepository.insert(postWithAuthor);
const newPost = createdPosts[0];
return {
user: newUser,
post: newPost
};
} catch (error) {
// In a real transaction, we'd rollback the user creation
// PostgREST doesn't support transactions, so handle cleanup manually
console.error('Failed to create user with post:', error);
throw error;
}
};
const result = await createUserWithPost(
{
email: 'author@example.com',
first_name: 'John',
last_name: 'Author',
active: true
},
{
title: 'My First Post',
content: 'This is my first blog post!',
published: true
}
);
console.log('Created user and post:', result);
}
Bulk Operations Across Tables
async function bulkOperationsAcrossTables() {
const userRepository = client.getRepository<User>('users');
const postRepository = client.getRepository<Post>('posts');
// Get users and their post counts
const getUsersWithPostCounts = async () => {
const users = await userRepository.find();
const usersWithCounts = await Promise.all(
users.map(async (user) => {
const postCount = await postRepository.count({ author_id: user.id });
return {
...user,
post_count: postCount
};
})
);
return usersWithCounts;
};
const usersWithCounts = await getUsersWithPostCounts();
console.log('Users with post counts:', usersWithCounts);
// Bulk update inactive users and unpublish their posts
const deactivateUsersAndPosts = async (userIds: string[]) => {
try {
// Update users
const updatedUsers = await userRepository
.createQueryBuilder()
.where('id IN (:...ids)', { ids: userIds })
.update({ active: false })
.execute();
// Update their posts
const updatedPosts = await postRepository
.createQueryBuilder()
.where('author_id IN (:...ids)', { ids: userIds })
.update({ published: false })
.execute();
return {
users: updatedUsers.data?.length || 0,
posts: updatedPosts.data?.length || 0
};
} catch (error) {
console.error('Bulk deactivation failed:', error);
throw error;
}
};
// Deactivate specific users
const result = await deactivateUsersAndPosts(['user1', 'user2', 'user3']);
console.log(`Deactivated ${result.users} users and unpublished ${result.posts} posts`);
}
Custom Repository Support
Using Custom Repositories
The client can work with custom repository classes:
import { CustomRepositoryBase } from '@webcoded/pgrestify';
// Custom User Repository
class UserRepository extends CustomRepositoryBase<User> {
async findActiveUsers(): Promise<User[]> {
return this.findBy({ active: true });
}
async findByEmail(email: string): Promise<User | null> {
return this.findOne({ email });
}
async findUsersCreatedAfter(date: string): Promise<User[]> {
return this.createQueryBuilder()
.where('created_at >= :date', { date })
.orderBy('created_at', 'DESC')
.getMany();
}
async getUserStats(userId: string) {
const postRepository = client.getRepository<Post>('posts');
const user = await this.findOne({ id: userId });
if (!user) return null;
const [postCount, publishedCount] = await Promise.all([
postRepository.createQueryBuilder().where('author_id = :userId', { userId }).getCount(),
postRepository.createQueryBuilder().where('author_id = :userId AND published = :published', { userId, published: true }).getCount()
]);
return {
user,
statistics: {
total_posts: postCount,
published_posts: publishedCount,
draft_posts: postCount - publishedCount
}
};
}
}
// Custom Post Repository
class PostRepository extends CustomRepositoryBase<Post> {
async findPublishedPosts(): Promise<Post[]> {
return this.findBy({ published: true });
}
async findPostsByAuthor(authorId: string): Promise<Post[]> {
return this.findBy({ author_id: authorId });
}
async findRecentPosts(limit: number = 10): Promise<Post[]> {
return this.createQueryBuilder()
.where('published = :published', { published: true })
.orderBy('created_at', 'DESC')
.limit(limit)
.getMany();
}
async searchPosts(query: string): Promise<Post[]> {
return this.createQueryBuilder()
.where('title ILIKE :query', { query: `%${query}%` })
.orWhere('content ILIKE :query', { query: `%${query}%` })
.andWhere('published = :published', { published: true })
.orderBy('created_at', 'DESC')
.getMany();
}
}
// Using custom repositories
async function useCustomRepositories() {
// Get custom repository instances
const userRepo = client.getCustomRepository(UserRepository, 'users');
const postRepo = client.getCustomRepository(PostRepository, 'posts');
// Use custom methods
const activeUsers = await userRepo.findActiveUsers();
const recentPosts = await postRepo.findRecentPosts(5);
const userStats = await userRepo.getUserStats('user-123');
console.log('Active users:', activeUsers.length);
console.log('Recent posts:', recentPosts.length);
console.log('User stats:', userStats);
// Search functionality
const searchResults = await postRepo.searchPosts('typescript');
console.log('Search results:', searchResults.length);
}
Repository Inheritance Patterns
// Base repository with common functionality
abstract class BaseRepository<T extends Record<string, unknown>> extends CustomRepositoryBase<T> {
async findActive(): Promise<T[]> {
return this.findBy({ active: true } as Partial<T>);
}
async findCreatedBetween(startDate: string, endDate: string): Promise<T[]> {
return this.createQueryBuilder()
.where('created_at >= :start', { start: startDate })
.andWhere('created_at <= :end', { end: endDate })
.orderBy('created_at', 'DESC')
.getMany();
}
async softDeleteWithTimestamp(where: Partial<T>): Promise<T[]> {
return this.update(where, {
deleted_at: new Date().toISOString()
} as Partial<T>);
}
}
// Specific repositories extending base
class EnhancedUserRepository extends BaseRepository<User> {
async findAdmins(): Promise<User[]> {
return this.findBy({ role: 'admin', active: true } as Partial<User>);
}
}
class EnhancedPostRepository extends BaseRepository<Post> {
async findFeaturedPosts(): Promise<Post[]> {
return this.findBy({ featured: true, published: true } as Partial<Post>);
}
}
// Usage
const enhancedUserRepo = client.getCustomRepository(EnhancedUserRepository, 'users');
const enhancedPostRepo = client.getCustomRepository(EnhancedPostRepository, 'posts');
const activeUsers = await enhancedUserRepo.findActive();
const adminUsers = await enhancedUserRepo.findAdmins();
const featuredPosts = await enhancedPostRepo.findFeaturedPosts();
Transaction-Like Operations
While PostgREST doesn't support traditional database transactions, the client provides coordination mechanisms:
async function transactionLikeOperations() {
// Pseudo-transaction: coordinate multiple operations
const executeAsTransaction = async <T>(
operations: (client: typeof client) => Promise<T>
): Promise<T> => {
try {
// Execute operations
const result = await operations(client);
// If we get here, all operations succeeded
console.log('All operations completed successfully');
return result;
} catch (error) {
// Log the error and re-throw
console.error('Transaction-like operation failed:', error);
// In a real transaction system, we'd rollback here
// With PostgREST, you'd need to implement compensation logic
throw error;
}
};
// Use the pseudo-transaction
const result = await executeAsTransaction(async (client) => {
const userRepo = client.getRepository<User>('users');
const postRepo = client.getRepository<Post>('posts');
// Create user
const users = await userRepo.insert({
email: 'transactional@example.com',
first_name: 'Trans',
last_name: 'Actional',
active: true
});
const user = users[0];
if (!user) throw new Error('Failed to create user');
// Create post for user
const posts = await postRepo.insert({
title: 'Transactional Post',
content: 'This post was created in a pseudo-transaction',
author_id: user.id,
published: true
});
const post = posts[0];
if (!post) {
// Cleanup: delete the user if post creation failed
await userRepo.delete({ id: user.id });
throw new Error('Failed to create post');
}
return { user, post };
});
console.log('Transaction result:', result);
}
Cache Management
The client provides centralized cache control:
async function cacheManagement() {
// Clear all caches managed by client
client.clearCache();
// Get repositories after cache clear
const userRepository = client.getRepository<User>('users');
const postRepository = client.getRepository<Post>('posts');
// Perform operations (will not use cached data)
const users = await userRepository.find();
const posts = await postRepository.find();
console.log(`Fresh data: ${users.length} users, ${posts.length} posts`);
// Repository-specific cache operations would use the underlying query builder's cache
const cachedUsers = await userRepository.createQueryBuilder()
.select(['id', 'email', 'first_name', 'last_name'])
.getMany();
// The underlying cache handles individual query caching
}
Error Handling and Best Practices
Comprehensive Error Handling
import { PostgRESTError } from '@webcoded/pgrestify';
async function robustDataOperations() {
const userRepository = client.getRepository<User>('users');
const postRepository = client.getRepository<Post>('posts');
try {
// Validate input
const email = 'test@example.com';
if (!email || !email.includes('@')) {
throw new Error('Valid email required');
}
// Check if user exists
const existingUser = await userRepository.findOne({ email });
if (existingUser) {
throw new Error('User already exists');
}
// Create user with error handling
const users = await userRepository.insert({
email,
first_name: 'Test',
last_name: 'User',
active: true
});
if (!users || users.length === 0) {
throw new Error('Failed to create user');
}
const user = users[0];
console.log('Created user:', user.email);
// Create welcome post
const posts = await postRepository.insert({
title: 'Welcome!',
content: 'Welcome to our platform!',
author_id: user.id,
published: true
});
return { user, post: posts[0] };
} catch (error) {
if (error instanceof PostgRESTError) {
// Handle PostgREST-specific errors
switch (error.statusCode) {
case 400:
console.error('Bad request:', error.message);
break;
case 401:
console.error('Unauthorized:', error.message);
break;
case 403:
console.error('Forbidden:', error.message);
break;
case 409:
console.error('Conflict (likely unique constraint):', error.message);
break;
default:
console.error('PostgREST error:', error);
}
} else {
// Handle application errors
console.error('Application error:', error);
}
throw error; // Re-throw for caller to handle
}
}
Performance Optimization
async function optimizedOperations() {
const userRepository = client.getRepository<User>('users');
const postRepository = client.getRepository<Post>('posts');
// Batch operations instead of loops
const userIds = ['id1', 'id2', 'id3', 'id4', 'id5'];
// Efficient: single query
const users = await userRepository.createQueryBuilder()
.whereIn('id', userIds)
.getMany();
// Efficient: single query with joins
const postsWithAuthors = await postRepository
.createQueryBuilder()
.leftJoinAndSelect('users', 'author')
.whereIn('author_id', userIds)
.getMany();
// Group posts by author
const postsByAuthor = (postsWithAuthors.data || []).reduce((acc, post) => {
const authorId = post.author_id;
if (!acc[authorId]) acc[authorId] = [];
acc[authorId].push(post);
return acc;
}, {} as Record<string, Post[]>);
console.log('Users:', users.length);
console.log('Posts by author:', Object.keys(postsByAuthor).length);
}
DataManager vs Direct Repository Usage
When to Use Repository Management
// Good: Complex operations across multiple tables
async function complexBusinessLogic() {
const userRepo = client.getRepository<User>('users');
const postRepo = client.getRepository<Post>('posts');
const commentRepo = client.getRepository('comments');
// Coordinate operations across tables
return { userRepo, postRepo, commentRepo };
}
// Good: Custom repository management
async function customRepositoryUsage() {
const customUserRepo = client.getCustomRepository(UserRepository, 'users');
return customUserRepo.findActiveUsers();
}
When Direct Repository Creation Might Be Better
// Simple: Single table operations
async function simpleUserOperation() {
// Could create repository directly if you prefer
const client = createClient({ url: 'http://localhost:3000' });
// Repository management is built into the client
const userRepo = client.getRepository<User>('users');
return userRepo.find();
}
Summary
PGRestify's repository management provides:
- Centralized Repository Management: Factory pattern for repository creation via
getRepository()
- Custom Repository Support: Register and use domain-specific repository classes via
getCustomRepository()
- Multi-Table Coordination: Orchestrate operations across multiple tables
- Cache Management: Centralized control over caching behavior
- Type Safety: Full TypeScript support with generic repository creation
- Error Handling: Consistent error handling patterns across repositories
The client's repository management is the recommended way to work with multiple repositories and coordinate complex operations in PGRestify, providing a familiar EntityManager-like interface for developers coming from ORM.