🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
Módulo 3 - Aula 1

Queries Avançadas

Domine queries avançadas no tRPC: paginação infinita, filtros complexos, ordenação customizada e otimizações de performance.

75 min
Avançado
Queries

🎯 Por que queries avançadas são cruciais para SaaS?

Performance: Paginação e filtros reduzem latência e consumo de memória drasticamente.

UX Superior: Busca rápida e navegação fluida mantém usuários engajados.

📄 Paginação Cursor-Based

src/server/trpc/routers/post.ts
// 📁 src/server/trpc/routers/post.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const postRouter = router({
  // 📋 Paginação cursor-based infinita
  getInfinite: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(10),
      cursor: z.string().optional(), // 🔍 Cursor para paginação
      search: z.string().optional(),
      categoryId: z.string().optional(),
      sortBy: z.enum(['createdAt', 'title', 'views']).default('createdAt'),
      sortOrder: z.enum(['asc', 'desc']).default('desc'),
    }))
    .query(async ({ input, ctx }) => {
      const { limit, cursor, search, categoryId, sortBy, sortOrder } = input;
      
      // 🔍 Construir filtros dinamicamente
      const whereClause: any = {
        published: true,
        ...(search && {
          OR: [
            { title: { contains: search, mode: 'insensitive' } },
            { content: { contains: search, mode: 'insensitive' } },
            { author: { name: { contains: search, mode: 'insensitive' } } },
          ],
        }),
        ...(categoryId && { categoryId }),
      };

      // 🚀 Query otimizada com cursor
      const posts = await ctx.prisma.post.findMany({
        take: limit + 1, // ✨ Buscar um a mais para verificar se há próxima página
        cursor: cursor ? { id: cursor } : undefined,
        where: whereClause,
        orderBy: {
          [sortBy]: sortOrder,
        },
        include: {
          author: {
            select: {
              id: true,
              name: true,
              avatar: true,
            },
          },
          category: {
            select: {
              id: true,
              name: true,
              slug: true,
            },
          },
          _count: {
            select: {
              comments: true,
              likes: true,
            },
          },
        },
      });

      // 📊 Determinar próximo cursor
      let nextCursor: typeof cursor | undefined = undefined;
      if (posts.length > limit) {
        const nextItem = posts.pop(); // 🔄 Remove o item extra
        nextCursor = nextItem!.id;
      }

      return {
        posts,
        nextCursor,
        hasMore: !!nextCursor,
      };
    }),

  // 📈 Busca com facets e agregações
  search: publicProcedure
    .input(z.object({
      query: z.string().min(1),
      filters: z.object({
        categories: z.array(z.string()).optional(),
        authors: z.array(z.string()).optional(),
        dateRange: z.object({
          start: z.date().optional(),
          end: z.date().optional(),
        }).optional(),
        tags: z.array(z.string()).optional(),
      }).optional(),
      limit: z.number().min(1).max(50).default(20),
      page: z.number().min(1).default(1),
    }))
    .query(async ({ input, ctx }) => {
      const { query, filters, limit, page } = input;
      const skip = (page - 1) * limit;

      // 🔍 Construir query complexa
      const whereClause: any = {
        published: true,
        AND: [
          {
            OR: [
              { title: { contains: query, mode: 'insensitive' } },
              { content: { contains: query, mode: 'insensitive' } },
              { excerpt: { contains: query, mode: 'insensitive' } },
            ],
          },
          ...(filters?.categories && filters.categories.length > 0
            ? [{ categoryId: { in: filters.categories } }]
            : []),
          ...(filters?.authors && filters.authors.length > 0
            ? [{ authorId: { in: filters.authors } }]
            : []),
          ...(filters?.dateRange?.start
            ? [{ createdAt: { gte: filters.dateRange.start } }]
            : []),
          ...(filters?.dateRange?.end
            ? [{ createdAt: { lte: filters.dateRange.end } }]
            : []),
          ...(filters?.tags && filters.tags.length > 0
            ? [{ 
                tags: { 
                  some: { 
                    tag: { 
                      name: { in: filters.tags } 
                    } 
                  } 
                } 
              }]
            : []),
        ],
      };

      // 📊 Execução paralela de queries
      const [posts, totalCount] = await Promise.all([
        // 🔍 Posts principais
        ctx.prisma.post.findMany({
          where: whereClause,
          skip,
          take: limit,
          include: {
            author: {
              select: {
                id: true,
                name: true,
                avatar: true,
              },
            },
            category: true,
            tags: {
              include: {
                tag: true,
              },
            },
            _count: {
              select: {
                comments: true,
                likes: true,
              },
            },
          },
          orderBy: [
            { createdAt: 'desc' },
          ],
        }),

        // 🔢 Contagem total
        ctx.prisma.post.count({ where: whereClause }),
      ]);

      return {
        posts,
        pagination: {
          page,
          limit,
          total: totalCount,
          pages: Math.ceil(totalCount / limit),
          hasMore: skip + limit < totalCount,
        },
      };
    }),
});

🔍 Filtros Dinâmicos Avançados

src/server/trpc/routers/product.ts
// 📁 src/server/trpc/routers/product.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

// 🎯 Schema de filtros complexos
const productFiltersSchema = z.object({
  search: z.string().optional(),
  category: z.string().optional(),
  priceRange: z.object({
    min: z.number().min(0).optional(),
    max: z.number().min(0).optional(),
  }).optional(),
  rating: z.number().min(1).max(5).optional(),
  inStock: z.boolean().optional(),
  brands: z.array(z.string()).optional(),
  attributes: z.record(z.string(), z.array(z.string())).optional(),
  sortBy: z.enum(['name', 'price', 'rating', 'created', 'popular']).default('created'),
  sortOrder: z.enum(['asc', 'desc']).default('desc'),
});

export const productRouter = router({
  // 🛍️ Busca de produtos com filtros avançados
  getFiltered: publicProcedure
    .input(z.object({
      filters: productFiltersSchema,
      pagination: z.object({
        page: z.number().min(1).default(1),
        limit: z.number().min(1).max(50).default(20),
      }),
    }))
    .query(async ({ input, ctx }) => {
      const { filters, pagination } = input;
      const { page, limit } = pagination;
      const skip = (page - 1) * limit;

      // 🔧 Função para construir where clause dinamicamente
      const buildWhereClause = () => {
        const conditions: any[] = [];

        // 🔍 Busca por texto
        if (filters.search) {
          conditions.push({
            OR: [
              { name: { contains: filters.search, mode: 'insensitive' } },
              { description: { contains: filters.search, mode: 'insensitive' } },
              { brand: { name: { contains: filters.search, mode: 'insensitive' } } },
            ],
          });
        }

        // 📂 Filtro por categoria
        if (filters.category) {
          conditions.push({
            categoryId: filters.category,
          });
        }

        // 💰 Filtro por faixa de preço
        if (filters.priceRange) {
          const priceCondition: any = {};
          if (filters.priceRange.min !== undefined) {
            priceCondition.gte = filters.priceRange.min;
          }
          if (filters.priceRange.max !== undefined) {
            priceCondition.lte = filters.priceRange.max;
          }
          if (Object.keys(priceCondition).length > 0) {
            conditions.push({ price: priceCondition });
          }
        }

        // ⭐ Filtro por rating
        if (filters.rating) {
          conditions.push({
            averageRating: { gte: filters.rating },
          });
        }

        // 📦 Filtro por estoque
        if (filters.inStock !== undefined) {
          conditions.push({
            stockQuantity: filters.inStock ? { gt: 0 } : { lte: 0 },
          });
        }

        // 🏷️ Filtro por marcas
        if (filters.brands && filters.brands.length > 0) {
          conditions.push({
            brand: {
              name: { in: filters.brands },
            },
          });
        }

        // 🔧 Filtros por atributos customizados
        if (filters.attributes && Object.keys(filters.attributes).length > 0) {
          Object.entries(filters.attributes).forEach(([key, values]) => {
            if (values.length > 0) {
              conditions.push({
                attributes: {
                  some: {
                    name: key,
                    value: { in: values },
                  },
                },
              });
            }
          });
        }

        return conditions.length > 0 ? { AND: conditions } : {};
      };

      // 📊 Função para construir orderBy
      const buildOrderBy = () => {
        const orderMap = {
          name: 'name',
          price: 'price',
          rating: 'averageRating',
          created: 'createdAt',
          popular: 'viewCount',
        };

        return {
          [orderMap[filters.sortBy]]: filters.sortOrder,
        };
      };

      const whereClause = buildWhereClause();
      const orderBy = buildOrderBy();

      // 🚀 Executar queries em paralelo
      const [products, totalCount, aggregations] = await Promise.all([
        // 🛍️ Produtos principais
        ctx.prisma.product.findMany({
          where: whereClause,
          skip,
          take: limit,
          orderBy,
          include: {
            brand: {
              select: {
                id: true,
                name: true,
                logo: true,
              },
            },
            category: {
              select: {
                id: true,
                name: true,
                slug: true,
              },
            },
            images: {
              take: 3,
              select: {
                url: true,
                alt: true,
              },
            },
            attributes: {
              select: {
                name: true,
                value: true,
              },
            },
            _count: {
              select: {
                reviews: true,
                orders: true,
              },
            },
          },
        }),

        // 🔢 Contagem total
        ctx.prisma.product.count({ where: whereClause }),

        // 📊 Agregações para filtros
        ctx.prisma.product.aggregate({
          where: whereClause,
          _min: {
            price: true,
          },
          _max: {
            price: true,
          },
          _avg: {
            price: true,
            averageRating: true,
          },
        }),
      ]);

      return {
        products,
        pagination: {
          page,
          limit,
          total: totalCount,
          pages: Math.ceil(totalCount / limit),
          hasMore: skip + limit < totalCount,
        },
        aggregations: {
          priceRange: {
            min: aggregations._min.price || 0,
            max: aggregations._max.price || 0,
          },
          averagePrice: aggregations._avg.price || 0,
          averageRating: aggregations._avg.averageRating || 0,
        },
      };
    }),
});

⚛️ Frontend com React Query

src/components/products/product-list.tsx
// 📁 src/components/products/product-list.tsx
'use client';

import { useState, useMemo } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { trpc } from '@/lib/trpc';
import { ProductCard } from './product-card';
import { ProductFilters } from './product-filters';
import { LoadingSpinner } from '@/components/ui/loading-spinner';

interface ProductListProps {
  initialFilters?: {
    category?: string;
    search?: string;
  };
}

export function ProductList({ initialFilters = {} }: ProductListProps) {
  // 🎯 Estado dos filtros
  const [filters, setFilters] = useState({
    search: initialFilters.search || '',
    category: initialFilters.category || '',
    priceRange: { min: undefined, max: undefined },
    rating: undefined,
    inStock: undefined,
    brands: [],
    attributes: {},
    sortBy: 'created' as const,
    sortOrder: 'desc' as const,
  });

  // 📊 Query infinita para produtos
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
    isLoading,
    error,
  } = trpc.post.getInfinite.useInfiniteQuery(
    {
      ...filters,
      limit: 20,
    },
    {
      getNextPageParam: (lastPage) => lastPage.nextCursor,
      staleTime: 5 * 60 * 1000, // 5 minutos
      cacheTime: 10 * 60 * 1000, // 10 minutos
    }
  );

  // 📊 Produtos flattened de todas as páginas
  const products = useMemo(() => {
    return data?.pages.flatMap((page) => page.posts) || [];
  }, [data]);

  // 🔄 Função para atualizar filtros
  const updateFilters = (newFilters: Partial<typeof filters>) => {
    setFilters((prev) => ({
      ...prev,
      ...newFilters,
    }));
  };

  // 🔄 Função para limpar filtros
  const clearFilters = () => {
    setFilters({
      search: '',
      category: '',
      priceRange: { min: undefined, max: undefined },
      rating: undefined,
      inStock: undefined,
      brands: [],
      attributes: {},
      sortBy: 'created',
      sortOrder: 'desc',
    });
  };

  // 🔄 Função para carregar mais produtos
  const loadMore = () => {
    if (hasNextPage && !isFetchingNextPage) {
      fetchNextPage();
    }
  };

  if (isLoading) {
    return (
      <div className="flex items-center justify-center py-12">
        <LoadingSpinner size="lg" />
      </div>
    );
  }

  if (error) {
    return (
      <div className="text-center py-12">
        <p className="text-red-400">Erro ao carregar produtos: {error.message}</p>
      </div>
    );
  }

  return (
    <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
      {/* 🔍 Sidebar de filtros */}
      <div className="lg:col-span-1">
        <ProductFilters
          filters={filters}
          onUpdateFilters={updateFilters}
          onClearFilters={clearFilters}
        />
      </div>

      {/* 📋 Lista de produtos */}
      <div className="lg:col-span-3">
        {/* 📊 Cabeçalho com resultados */}
        <div className="flex items-center justify-between mb-6">
          <h2 className="text-2xl font-bold text-white">
            Produtos {products.length > 0 && (
              <span className="text-gray-400">({products.length})</span>
            )}
          </h2>
          
          {/* 🔄 Ordenação */}
          <select
            value={filters.sortBy + '-' + filters.sortOrder}
            onChange={(e) => {
              const [sortBy, sortOrder] = e.target.value.split('-');
              updateFilters({ 
                sortBy: sortBy as any, 
                sortOrder: sortOrder as any 
              });
            }}
            className="bg-gray-800 text-white border border-gray-600 rounded px-3 py-2"
          >
            <option value="created-desc">Mais Recentes</option>
            <option value="created-asc">Mais Antigos</option>
            <option value="title-asc">A-Z</option>
            <option value="title-desc">Z-A</option>
            <option value="views-desc">Mais Visualizados</option>
          </select>
        </div>

        {/* 🛍️ Grid de produtos */}
        {products.length === 0 ? (
          <div className="text-center py-12">
            <p className="text-gray-400">Nenhum produto encontrado</p>
          </div>
        ) : (
          <>
            <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
              {products.map((product) => (
                <ProductCard key={product.id} product={product} />
              ))}
            </div>

            {/* 📄 Botão para carregar mais */}
            {hasNextPage && (
              <div className="text-center mt-8">
                <button
                  onClick={loadMore}
                  disabled={isFetchingNextPage}
                  className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
                >
                  {isFetchingNextPage ? (
                    <>
                      <LoadingSpinner size="sm" className="mr-2" />
                      Carregando...
                    </>
                  ) : (
                    'Carregar Mais'
                  )}
                </button>
              </div>
            )}
          </>
        )}
      </div>
    </div>
  );
}

⚡ Otimizações de Performance

src/server/trpc/routers/optimized-queries.ts
// 📁 src/server/trpc/routers/optimized-queries.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';

export const optimizedRouter = router({
  // 🚀 Query com cache inteligente
  getPostsWithCache: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(10),
      cursor: z.string().optional(),
      tags: z.array(z.string()).optional(),
    }))
    .query(async ({ input, ctx }) => {
      const { limit, cursor, tags } = input;
      
      // 🔑 Chave de cache baseada nos parâmetros
      const cacheKey = 'posts-' + JSON.stringify({ limit, cursor, tags });
      
      // 📊 Verificar cache Redis/Memcached
      const cached = await ctx.cache?.get(cacheKey);
      if (cached) {
        return JSON.parse(cached);
      }

      // 🗄️ Query otimizada com select específico
      const posts = await ctx.prisma.post.findMany({
        take: limit + 1,
        cursor: cursor ? { id: cursor } : undefined,
        where: tags ? {
          tags: {
            some: {
              tag: {
                name: { in: tags },
              },
            },
          },
        } : undefined,
        // 🎯 Selecionar apenas campos necessários
        select: {
          id: true,
          title: true,
          excerpt: true,
          slug: true,
          createdAt: true,
          updatedAt: true,
          author: {
            select: {
              id: true,
              name: true,
              avatar: true,
            },
          },
          category: {
            select: {
              id: true,
              name: true,
              slug: true,
            },
          },
          _count: {
            select: {
              comments: true,
              likes: true,
            },
          },
        },
        orderBy: {
          createdAt: 'desc',
        },
      });

      let nextCursor: string | undefined;
      if (posts.length > limit) {
        const nextItem = posts.pop();
        nextCursor = nextItem!.id;
      }

      const result = {
        posts,
        nextCursor,
        hasMore: !!nextCursor,
      };

      // 💾 Salvar no cache por 5 minutos
      await ctx.cache?.set(cacheKey, JSON.stringify(result), 300);

      return result;
    }),

  // 📊 Agregações otimizadas
  getPostStats: publicProcedure
    .input(z.object({
      authorId: z.string().optional(),
      categoryId: z.string().optional(),
      dateRange: z.object({
        start: z.date(),
        end: z.date(),
      }).optional(),
    }))
    .query(async ({ input, ctx }) => {
      const { authorId, categoryId, dateRange } = input;

      // 🔍 Construir filtros
      const whereClause: any = {
        published: true,
        ...(authorId && { authorId }),
        ...(categoryId && { categoryId }),
        ...(dateRange && {
          createdAt: {
            gte: dateRange.start,
            lte: dateRange.end,
          },
        }),
      };

      // 📊 Executar agregações em paralelo
      const [
        totalPosts,
        totalViews,
        totalComments,
        totalLikes,
        topAuthors,
        topCategories,
      ] = await Promise.all([
        // 📝 Total de posts
        ctx.prisma.post.count({ where: whereClause }),

        // 👀 Total de visualizações
        ctx.prisma.post.aggregate({
          where: whereClause,
          _sum: {
            views: true,
          },
        }),

        // 💬 Total de comentários
        ctx.prisma.comment.count({
          where: {
            post: whereClause,
          },
        }),

        // ❤️ Total de curtidas
        ctx.prisma.like.count({
          where: {
            post: whereClause,
          },
        }),

        // 👥 Top autores
        ctx.prisma.user.findMany({
          where: {
            posts: {
              some: whereClause,
            },
          },
          select: {
            id: true,
            name: true,
            avatar: true,
            _count: {
              select: {
                posts: true,
              },
            },
          },
          orderBy: {
            posts: {
              _count: 'desc',
            },
          },
          take: 5,
        }),

        // 📂 Top categorias
        ctx.prisma.category.findMany({
          where: {
            posts: {
              some: whereClause,
            },
          },
          select: {
            id: true,
            name: true,
            slug: true,
            _count: {
              select: {
                posts: true,
              },
            },
          },
          orderBy: {
            posts: {
              _count: 'desc',
            },
          },
          take: 5,
        }),
      ]);

      return {
        overview: {
          totalPosts,
          totalViews: totalViews._sum.views || 0,
          totalComments,
          totalLikes,
        },
        topAuthors,
        topCategories,
      };
    }),
});

⚡ Dicas de Performance para Queries

Select Específico:Sempre use select para buscar apenas os campos necessários.

Índices de Banco:Crie índices para campos usados em WHERE e ORDER BY.

Cache Inteligente:Use Redis para cachear queries complexas e agregações.

Paginação Cursor:Prefira cursor-based para melhor performance em grandes datasets.