Skip to content

Advanced Queries Example

Comprehensive guide to advanced querying techniques in PGRestify.

Complex Filtering and Joins

typescript
import { createClient } from '@webcoded/pgrestify';

// Define interfaces for type safety
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
}

interface Post {
  id: number;
  user_id: number;
  title: string;
  content: string;
  tags: string[];
  created_at: string;
}

const client = createClient('http://localhost:3000');

// Modern approach: Relations array with aliases and multiple sorting
async function getAdvancedUserPostsModern() {
  const modernQuery = await client
    .from<User>('users')
    .select([
      'id AS user_id',
      'name AS author_name',
      'email AS contact_email',
      'posts.id AS post_id',
      'posts.title AS post_title',
      'posts.content AS post_content',
      'posts.tags',
      'posts.created_at AS published_date',
      'posts.comments.id AS comment_id',
      'posts.comments.content AS comment_text',
      'posts.comments.author.name AS commenter_name'
    ])
    .relations(['posts.comments.author'])
    .eq('role', 'admin')
    .gte('posts.created_at', '2023-01-01')
    .order('name')                                    // Primary: Author name
    .order('posts.created_at', { ascending: false })  // Secondary: Latest posts
    .order('posts.comments.created_at', { ascending: false })  // Tertiary: Recent comments
    .execute();

  return modernQuery.data;
}

// Traditional PostgREST syntax for comparison
async function getAdvancedUserPostsTraditional() {
  const traditionalQuery = await client
    .from<User>('users')
    .select(`
      id, 
      name, 
      email,
      posts:posts(
        id, 
        title, 
        content, 
        tags,
        comments:comments(
          id, 
          content, 
          author:users(name, email)
        )
      )
    `)
    .eq('role', 'admin')
    .gte('posts.created_at', '2023-01-01')
    .contains('posts.tags', ['typescript', 'postgresql'])
    .order('posts.created_at', { ascending: false })
    .limit(10)
    .execute();

  return complexQuery;
}

Aggregation and Grouping

typescript
// Advanced aggregation with grouping and filtering
async function getUserPostStatistics() {
  const aggregationQuery = await client
    .from<User>('users')
    .select(`
      role,
      count(*) as user_count,
      avg(posts:posts(count)) as avg_posts_per_user,
      sum(posts:posts(views)) as total_post_views,
      max(posts:posts(created_at)) as latest_post_date
    `)
    .groupBy('role')
    .having('count(*) > 5')
    .order('total_post_views', { ascending: false })
    .execute();

  return aggregationQuery;
}

Full-Text Search with Ranking

typescript
// Advanced full-text search with ranking
async function searchPostsWithRanking(searchTerm: string) {
  const searchQuery = await client
    .from<Post>('posts')
    .select(`
      *,
      ts_rank(to_tsvector(content), plainto_tsquery($1)) as rank
    `)
    .fts('content', searchTerm)
    .order('rank', { ascending: false })
    .limit(20)
    .execute();

  return searchQuery;
}

Advanced Examples: Combining Relations, Aliases, and Sorting

E-commerce Product Catalog

typescript
interface Product {
  id: number;
  name: string;
  price: number;
  category_id: number;
  brand_id: number;
  active: boolean;
}

// Advanced product search with comprehensive sorting
async function getProductCatalogAdvanced(filters: {
  categoryId?: number;
  brandId?: number;
  minPrice?: number;
  maxPrice?: number;
  searchTerm?: string;
}) {
  let query = client
    .from<Product>('products')
    .select([
      'id AS product_id',
      'name AS product_name',
      'price AS current_price',
      'description AS product_description',
      'category.name AS category_name',
      'category.slug AS category_slug',
      'brand.name AS brand_name',
      'brand.logo_url AS brand_logo',
      'reviews.average_rating AS avg_rating',
      'reviews.total_count AS review_count',
      'inventory.stock_quantity AS stock_level',
      'inventory.status AS availability_status'
    ])
    .relations(['category', 'brand', 'reviews', 'inventory'])
    .eq('active', true);

  // Apply dynamic filters
  if (filters.categoryId) {
    query = query.eq('category_id', filters.categoryId);
  }
  
  if (filters.brandId) {
    query = query.eq('brand_id', filters.brandId);
  }
  
  if (filters.minPrice) {
    query = query.gte('price', filters.minPrice);
  }
  
  if (filters.maxPrice) {
    query = query.lte('price', filters.maxPrice);
  }
  
  if (filters.searchTerm) {
    query = query.ilike('name', `%${filters.searchTerm}%`);
  }

  // Complex multi-level sorting
  return query
    .order('inventory.status')                     // In-stock first
    .order('category.name')                        // Group by category
    .order('reviews.average_rating', { ascending: false })  // Best rated
    .order('reviews.total_count', { ascending: false })     // Most reviewed
    .order('price')                                // Cheapest first
    .order('name')                                 // Alphabetical
    .execute();
}

// Repository pattern for complex business logic
const productRepo = client.getRepository<Product>('products');

async function getFeaturedProductsAdvanced() {
  return productRepo
    .createQueryBuilder()
    .select([
      'id AS product_id',
      'name AS product_name',
      'price AS current_price',
      'featured_until AS feature_expires',
      'category.name AS category_name',
      'brand.name AS brand_name',
      'reviews.average_rating AS rating',
      'sales.total_sold AS units_sold',
      'inventory.stock_quantity AS stock'
    ])
    .relations(['category', 'brand', 'reviews', 'sales', 'inventory'])
    .where('featured = :featured', { featured: true })
    .andWhere('featured_until > :now', { now: new Date() })
    .andWhere('inventory.stock_quantity > :minStock', { minStock: 0 })
    .orderBy('sales.total_sold', 'DESC')           // Best sellers first
    .addOrderBy('reviews.average_rating', 'DESC')  // Then by rating
    .addOrderBy('featured_until', 'ASC')           // Expiring soon first
    .addOrderBy('price', 'ASC')                    // Then by price
    .limit(20)
    .getMany();
}

Content Management System

typescript
interface Article {
  id: number;
  title: string;
  content: string;
  author_id: number;
  category_id: number;
  published: boolean;
  created_at: string;
}

// Advanced CMS query with comprehensive relationships
async function getCMSContentAdvanced() {
  return client
    .from<Article>('articles')
    .select([
      'id AS article_id',
      'title AS article_title',
      'excerpt AS article_excerpt',
      'published_at AS publication_date',
      'author.first_name AS author_first_name',
      'author.last_name AS author_last_name',
      'author.bio AS author_bio',
      'author.avatar_url AS author_avatar',
      'category.name AS category_name',
      'category.slug AS category_slug',
      'tags.name AS tag_name',
      'comments.content AS comment_text',
      'comments.author.name AS commenter_name',
      'comments.created_at AS comment_date',
      'analytics.view_count AS total_views',
      'analytics.share_count AS total_shares'
    ])
    .relations([
      'author', 
      'category', 
      'tags', 
      'comments.author', 
      'analytics'
    ])
    .eq('published', true)
    .gte('published_at', '2024-01-01')
    .order('analytics.view_count', { ascending: false })    // Most viewed first
    .order('published_at', { ascending: false })            // Recent content
    .order('comments.created_at', { ascending: false })     // Recent engagement
    .order('category.name')                                 // Group by category
    .execute();
}

// Advanced search across multiple content types
async function searchContentAdvanced(searchTerm: string) {
  const [articles, tutorials, videos] = await Promise.all([
    // Articles with comprehensive data
    client
      .from('articles')
      .select([
        'id AS content_id',
        '"article" AS content_type',
        'title AS content_title',
        'excerpt AS content_excerpt',
        'author.name AS author_name',
        'category.name AS category_name',
        'published_at AS content_date',
        'analytics.view_count AS popularity_score'
      ])
      .relations(['author', 'category', 'analytics'])
      .textSearch('title,content', searchTerm)
      .eq('published', true)
      .order('analytics.view_count', { ascending: false })
      .order('published_at', { ascending: false })
      .limit(10)
      .execute(),

    // Tutorials with difficulty and duration
    client
      .from('tutorials')
      .select([
        'id AS content_id',
        '"tutorial" AS content_type',
        'title AS content_title',
        'description AS content_excerpt',
        'difficulty_level AS difficulty',
        'estimated_duration AS duration',
        'instructor.name AS author_name',
        'topic.name AS category_name',
        'created_at AS content_date',
        'enrollments.total_count AS popularity_score'
      ])
      .relations(['instructor', 'topic', 'enrollments'])
      .textSearch('title,description', searchTerm)
      .eq('active', true)
      .order('difficulty_level')  // Beginner first
      .order('enrollments.total_count', { ascending: false })
      .order('created_at', { ascending: false })
      .limit(10)
      .execute(),

    // Videos with view statistics
    client
      .from('videos')
      .select([
        'id AS content_id',
        '"video" AS content_type',
        'title AS content_title',
        'description AS content_excerpt',
        'duration_seconds AS video_duration',
        'creator.name AS author_name',
        'playlist.name AS category_name',
        'uploaded_at AS content_date',
        'stats.view_count AS popularity_score'
      ])
      .relations(['creator', 'playlist', 'stats'])
      .textSearch('title,description', searchTerm)
      .eq('published', true)
      .order('stats.view_count', { ascending: false })
      .order('uploaded_at', { ascending: false })
      .limit(10)
      .execute()
  ]);

  // Combine and sort all results by popularity
  const allContent = [
    ...(articles.data || []),
    ...(tutorials.data || []),
    ...(videos.data || [])
  ].sort((a, b) => {
    // Multi-criteria sorting in JavaScript
    if (b.popularity_score !== a.popularity_score) {
      return b.popularity_score - a.popularity_score;
    }
    return new Date(b.content_date).getTime() - new Date(a.content_date).getTime();
  });

  return allContent;
}

Analytics Dashboard

typescript
// Complex analytics query with multiple aggregations
async function getAnalyticsDashboard(dateRange: { start: string; end: string }) {
  return client
    .from('page_views')
    .select([
      'page.title AS page_title',
      'page.url AS page_url',
      'page.category.name AS page_category',
      'user.country AS visitor_country',
      'user.device_type AS device_type',
      'session.referrer_domain AS traffic_source',
      'count(*) AS total_views',
      'count(DISTINCT user_id) AS unique_visitors',
      'avg(session.duration_seconds) AS avg_session_duration',
      'sum(CASE WHEN converted = true THEN 1 ELSE 0 END) AS conversions'
    ])
    .relations(['page.category', 'user', 'session'])
    .gte('created_at', dateRange.start)
    .lte('created_at', dateRange.end)
    .groupBy([
      'page.title',
      'page.url', 
      'page.category.name',
      'user.country',
      'user.device_type',
      'session.referrer_domain'
    ])
    .having('count(*) > 10')  // Minimum views threshold
    .order('total_views', { ascending: false })         // Most viewed
    .order('unique_visitors', { ascending: false })     // Most unique visitors
    .order('conversions', { ascending: false })         // Most conversions
    .order('page.category.name')                        // Group by category
    .limit(100)
    .execute();
}

Dynamic Query Builder

typescript
// Advanced dynamic query builder with all features
class AdvancedQueryBuilder {
  private client: any;
  
  constructor(client: any) {
    this.client = client;
  }

  async buildDynamicQuery(options: {
    table: string;
    fields: Array<{ field: string; alias?: string }>;
    relations: string[];
    filters: Array<{ field: string; operator: string; value: any }>;
    sorts: Array<{ field: string; direction: 'asc' | 'desc' }>;
    limit?: number;
  }) {
    // Build select array with aliases
    const selectFields = options.fields.map(field => {
      return field.alias ? `${field.field} AS ${field.alias}` : field.field;
    });

    // Start building the query
    let query = this.client
      .from(options.table)
      .select(selectFields);

    // Add relations
    if (options.relations.length > 0) {
      query = query.relations(options.relations);
    }

    // Add dynamic filters
    options.filters.forEach(filter => {
      switch (filter.operator) {
        case 'eq':
          query = query.eq(filter.field, filter.value);
          break;
        case 'gte':
          query = query.gte(filter.field, filter.value);
          break;
        case 'lte':
          query = query.lte(filter.field, filter.value);
          break;
        case 'ilike':
          query = query.ilike(filter.field, `%${filter.value}%`);
          break;
        case 'in':
          query = query.in(filter.field, filter.value);
          break;
      }
    });

    // Add dynamic sorting
    options.sorts.forEach(sort => {
      query = query.order(sort.field, { ascending: sort.direction === 'asc' });
    });

    // Add limit if specified
    if (options.limit) {
      query = query.limit(options.limit);
    }

    return query.execute();
  }
}

// Usage example
const queryBuilder = new AdvancedQueryBuilder(client);

const dynamicResult = await queryBuilder.buildDynamicQuery({
  table: 'orders',
  fields: [
    { field: 'id', alias: 'order_id' },
    { field: 'total', alias: 'order_total' },
    { field: 'customer.name', alias: 'customer_name' },
    { field: 'items.product.name', alias: 'product_name' }
  ],
  relations: ['customer', 'items.product'],
  filters: [
    { field: 'status', operator: 'eq', value: 'completed' },
    { field: 'created_at', operator: 'gte', value: '2024-01-01' },
    { field: 'total', operator: 'gte', value: 100 }
  ],
  sorts: [
    { field: 'created_at', direction: 'desc' },
    { field: 'total', direction: 'desc' },
    { field: 'customer.name', direction: 'asc' }
  ],
  limit: 50
});

Conditional Aggregation

typescript
// Conditional aggregation with case statements
async function getUserActivityStats() {
  const conditionalQuery = await client
    .from<User>('users')
    .select(`
      role,
      count(*) as total_users,
      sum(case when posts:posts(count) > 10 then 1 else 0 end) as power_users,
      sum(case when posts:posts(created_at) >= '2023-01-01' then 1 else 0 end) as recent_active_users,
      avg(posts:posts(views)) as avg_post_views
    `)
    .groupBy('role')
    .having('total_users > 0')
    .execute();

  return conditionalQuery;
}

Nested Filtering

typescript
// Nested filtering with complex conditions
async function getFilteredUserPosts() {
  const nestedFilterQuery = await client
    .from<User>('users')
    .select(`
      id, 
      name,
      posts:posts!inner(
        id, 
        title, 
        content
      )
    `)
    .eq('role', 'admin')
    .gte('posts.views', 100)
    .contains('posts.tags', ['tutorial'])
    .execute();

  return nestedFilterQuery;
}

Window Functions

typescript
// Window functions for advanced analytics
async function getRankedPosts() {
  const windowFunctionQuery = await client
    .from<Post>('posts')
    .select(`
      *,
      rank() over (partition by category order by views desc) as category_rank,
      dense_rank() over (order by views desc) as overall_rank
    `)
    .limit(50)
    .execute();

  return windowFunctionQuery;
}

Time-Based Aggregation

typescript
// Time-based aggregation and trend analysis
async function getMonthlyPostTrends() {
  const timeBasedQuery = await client
    .from<Post>('posts')
    .select(`
      date_trunc('month', created_at) as month,
      count(*) as post_count,
      sum(views) as total_views,
      avg(views) as avg_monthly_views
    `)
    .groupBy('date_trunc(month, created_at)')
    .order('month')
    .execute();

  return timeBasedQuery;
}

Complex Filtering with Multiple Conditions

typescript
// Advanced filtering with multiple complex conditions
async function getAdvancedFilteredUsers() {
  const multiConditionQuery = await client
    .from<User>('users')
    .select(`
      id, 
      name, 
      email,
      posts:posts(id, title)
    `)
    .and(
      'role.eq.admin',
      'posts.count.gte.5',
      'created_at.gte.2023-01-01'
    )
    .order('posts.count', { ascending: false })
    .limit(10)
    .execute();

  return multiConditionQuery;
}

Performance Optimization

typescript
// Query optimization techniques
async function optimizedQuery() {
  const optimizedResult = await client
    .from<Post>('posts')
    .select('id, title, summary') // Select only necessary fields
    .eq('published', true)
    .order('created_at', { ascending: false })
    .limit(100)
    .cache(true) // Enable caching
    .execute();

  return optimizedResult;
}

Error Handling in Complex Queries

typescript
async function safeAdvancedQuery() {
  try {
    const result = await client
      .from<User>('users')
      .select(`
        id, 
        name, 
        posts:posts(id, title)
      `)
      .eq('role', 'admin')
      .execute();

    return result;
  } catch (error) {
    if (error.name === 'QueryBuilderError') {
      console.error('Query failed:', error.message);
      
      // Implement fallback or error recovery
      return [];
    }
    
    throw error; // Re-throw unexpected errors
  }
}

Best Practices

  • Use type generics for type safety
  • Select only necessary columns
  • Apply filters early in the query
  • Use server-side aggregations
  • Implement proper error handling
  • Leverage caching for repeated queries
  • Use window functions for advanced analytics
  • Be mindful of query complexity

Performance Considerations

  • Minimize data transfer
  • Use server-side filtering
  • Implement appropriate indexing
  • Cache frequently used queries
  • Monitor query performance
  • Use pagination for large datasets
  • Optimize query complexity

Released under the MIT License.