CRUD Completo com Prisma
Agora que temos nossos modelos definidos, vamos implementar operações CRUD completas.
Repository Pattern com Prisma
typescript
// src/repositories/product.repository.tsimport { prisma } from '../lib/prisma';import { Prisma } from '@prisma/client'; export class ProductRepository { async findAll(params: { skip?: number; take?: number; where?: Prisma.ProductWhereInput; orderBy?: Prisma.ProductOrderByWithRelationInput; }) { const { skip, take, where, orderBy } = params; const [products, total] = await Promise.all([ prisma.product.findMany({ skip, take, where, orderBy, include: { category: true }, }), prisma.product.count({ where }), ]); return { products, total }; } async findById(id: string) { return prisma.product.findUnique({ where: { id }, include: { category: true, reviews: { include: { user: { select: { id: true, name: true } } }, orderBy: { createdAt: 'desc' }, take: 10, }, }, }); } async findBySlug(slug: string) { return prisma.product.findUnique({ where: { slug }, include: { category: true }, }); } async create(data: Prisma.ProductCreateInput) { return prisma.product.create({ data, include: { category: true }, }); } async update(id: string, data: Prisma.ProductUpdateInput) { return prisma.product.update({ where: { id }, data, include: { category: true }, }); } async delete(id: string) { return prisma.product.delete({ where: { id } }); } async updateStock(id: string, quantity: number) { return prisma.product.update({ where: { id }, data: { stock: { increment: quantity } }, }); }}Service com Lógica de Negócio
typescript
// src/services/product.service.tsimport { ProductRepository } from '../repositories/product.repository';import { NotFoundError, ConflictError } from '../errors';import { CreateProductDTO, UpdateProductDTO, ProductQuery } from '../schemas/product.schema'; export class ProductService { constructor(private productRepository: ProductRepository) {} async findAll(query: ProductQuery) { const { page, limit, search, category, minPrice, maxPrice, sortBy, sortOrder } = query; const where: any = { active: true }; if (search) { where.OR = [ { name: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, ]; } if (category) { where.categoryId = category; } if (minPrice || maxPrice) { where.price = {}; if (minPrice) where.price.gte = minPrice; if (maxPrice) where.price.lte = maxPrice; } const { products, total } = await this.productRepository.findAll({ skip: (page - 1) * limit, take: limit, where, orderBy: { [sortBy]: sortOrder }, }); return { data: products, meta: { page, limit, total, totalPages: Math.ceil(total / limit), }, }; } async findById(id: string) { const product = await this.productRepository.findById(id); if (!product) { throw new NotFoundError('Produto'); } return product; } async create(data: CreateProductDTO) { // Verificar se slug já existe const existing = await this.productRepository.findBySlug(data.slug); if (existing) { throw new ConflictError('Slug já está em uso'); } return this.productRepository.create({ ...data, category: { connect: { id: data.categoryId } }, }); } async update(id: string, data: UpdateProductDTO) { const existing = await this.productRepository.findById(id); if (!existing) { throw new NotFoundError('Produto'); } // Verificar slug se está sendo alterado if (data.slug && data.slug !== existing.slug) { const slugExists = await this.productRepository.findBySlug(data.slug); if (slugExists) { throw new ConflictError('Slug já está em uso'); } } const updateData: any = { ...data }; if (data.categoryId) { updateData.category = { connect: { id: data.categoryId } }; delete updateData.categoryId; } return this.productRepository.update(id, updateData); } async delete(id: string) { const existing = await this.productRepository.findById(id); if (!existing) { throw new NotFoundError('Produto'); } await this.productRepository.delete(id); }}Controller
typescript
// src/controllers/product.controller.tsimport { Request, Response } from 'express';import { ProductService } from '../services/product.service';import { success, paginated } from '../utils/responses'; export class ProductController { constructor(private productService: ProductService) {} findAll = async (req: Request, res: Response) => { const result = await this.productService.findAll(req.query as any); res.json(result); }; findById = async (req: Request, res: Response) => { const product = await this.productService.findById(req.params.id); res.json(success(product)); }; create = async (req: Request, res: Response) => { const product = await this.productService.create(req.body); res.status(201).json(success(product, 'Produto criado com sucesso')); }; update = async (req: Request, res: Response) => { const product = await this.productService.update(req.params.id, req.body); res.json(success(product, 'Produto atualizado com sucesso')); }; delete = async (req: Request, res: Response) => { await this.productService.delete(req.params.id); res.status(204).send(); };}Operações Avançadas
Transações
typescript
async createOrderWithItems(userId: string, items: OrderItemInput[]) { return prisma.$transaction(async (tx) => { // Calcular total e verificar estoque let total = 0; for (const item of items) { const product = await tx.product.findUnique({ where: { id: item.productId }, }); if (!product || product.stock < item.quantity) { throw new Error(`Estoque insuficiente para ${product?.name}`); } total += Number(product.price) * item.quantity; // Decrementar estoque await tx.product.update({ where: { id: item.productId }, data: { stock: { decrement: item.quantity } }, }); } // Criar pedido const order = await tx.order.create({ data: { userId, total, items: { create: items.map(item => ({ productId: item.productId, quantity: item.quantity, price: item.price, })), }, }, include: { items: true }, }); return order; });}Aggregations
typescript
// Estatísticas de produtos por categoriaconst stats = await prisma.product.groupBy({ by: ['categoryId'], _count: { id: true }, _avg: { price: true }, _sum: { stock: true },}); // Produtos mais vendidosconst topProducts = await prisma.orderItem.groupBy({ by: ['productId'], _sum: { quantity: true }, orderBy: { _sum: { quantity: 'desc' } }, take: 10,});Resumo
- ✅ Repository Pattern com Prisma
- ✅ Filtros, paginação e ordenação
- ✅ Transações para operações complexas
- ✅ Aggregations e estatísticas
- ✅ CRUD completo tipado
Próxima aula: Relacionamentos no Banco de Dados! 🔗