Pular para o conteúdoPedro Farbo
Lição 10 / 2555 min

CRUD Completo com Prisma

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! 🔗

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e