Saltar al contenidoPedro Farbo
Lección 24 / 2560 min

Proyecto E-commerce - Parte 2

Proyecto E-commerce - Parte 2

Implementamos los módulos de productos, categorías y reviews.

Schema Prisma

prisma
// prisma/schema.prismamodel Category {  id        String    @id @default(uuid())  name      String    @unique  slug      String    @unique  products  Product[]  createdAt DateTime  @default(now())} model Product {  id          String      @id @default(uuid())  name        String  slug        String      @unique  description String?  price       Decimal     @db.Decimal(10, 2)  stock       Int         @default(0)  images      String[]  featured    Boolean     @default(false)  category    Category    @relation(fields: [categoryId], references: [id])  categoryId  String  reviews     Review[]  orderItems  OrderItem[]  createdAt   DateTime    @default(now())  updatedAt   DateTime    @updatedAt   @@index([categoryId])  @@index([slug])} model Review {  id        String   @id @default(uuid())  rating    Int  comment   String?  user      User     @relation(fields: [userId], references: [id])  userId    String  product   Product  @relation(fields: [productId], references: [id])  productId String  createdAt DateTime @default(now())   @@unique([userId, productId])}

Product Service

typescript
// src/services/product.service.tsimport { Prisma } from '@prisma/client';import { prisma } from '../config/database';import { AppError } from '../errors/AppError';import { cacheService } from './cache.service'; interface FindAllParams {  page?: number;  limit?: number;  search?: string;  categoryId?: string;  minPrice?: number;  maxPrice?: number;  featured?: boolean;  sortBy?: string;  order?: 'asc' | 'desc';} export class ProductService {  async findAll(params: FindAllParams) {    const {      page = 1,      limit = 20,      search,      categoryId,      minPrice,      maxPrice,      featured,      sortBy = 'createdAt',      order = 'desc',    } = params;     const cacheKey = `products:${JSON.stringify(params)}`;    const cached = await cacheService.get(cacheKey);    if (cached) return cached;     const where: Prisma.ProductWhereInput = {};     if (search) {      where.OR = [        { name: { contains: search, mode: 'insensitive' } },        { description: { contains: search, mode: 'insensitive' } },      ];    }    if (categoryId) where.categoryId = categoryId;    if (featured !== undefined) where.featured = featured;    if (minPrice || maxPrice) {      where.price = {};      if (minPrice) where.price.gte = minPrice;      if (maxPrice) where.price.lte = maxPrice;    }     const skip = (page - 1) * limit;     const [products, total] = await Promise.all([      prisma.product.findMany({        where,        skip,        take: limit,        orderBy: { [sortBy]: order },        include: {          category: true,          _count: { select: { reviews: true } },        },      }),      prisma.product.count({ where }),    ]);     const result = {      data: products,      pagination: { page, limit, total, totalPages: Math.ceil(total / limit) },    };     await cacheService.set(cacheKey, result, 1800);    return result;  }   async findBySlug(slug: string) {    const product = await prisma.product.findUnique({      where: { slug },      include: {        category: true,        reviews: {          include: { user: { select: { id: true, name: true } } },          orderBy: { createdAt: 'desc' },          take: 10,        },      },    });     if (!product) throw new AppError('Producto no encontrado', 404);    return product;  }   async create(data: CreateProductDTO) {    const slug = this.generateSlug(data.name);    const exists = await prisma.product.findUnique({ where: { slug } });    if (exists) throw new AppError('Producto ya existe', 409);     const product = await prisma.product.create({      data: { ...data, slug },      include: { category: true },    });     await cacheService.delPattern('products:*');    return product;  }   async update(id: string, data: UpdateProductDTO) {    await this.findById(id);    const product = await prisma.product.update({      where: { id },      data,      include: { category: true },    });    await cacheService.delPattern('products:*');    return product;  }   async delete(id: string) {    await this.findById(id);    await prisma.product.delete({ where: { id } });    await cacheService.delPattern('products:*');  }   private generateSlug(name: string) {    return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');  }   private async findById(id: string) {    const product = await prisma.product.findUnique({ where: { id } });    if (!product) throw new AppError('Producto no encontrado', 404);    return product;  }}

Category Service

typescript
// src/services/category.service.tsimport { prisma } from '../config/database';import { AppError } from '../errors/AppError'; export class CategoryService {  async findAll() {    return prisma.category.findMany({      include: { _count: { select: { products: true } } },      orderBy: { name: 'asc' },    });  }   async findBySlug(slug: string) {    const category = await prisma.category.findUnique({      where: { slug },      include: { products: { take: 10 } },    });    if (!category) throw new AppError('Categoría no encontrada', 404);    return category;  }   async create(data: { name: string }) {    const slug = data.name.toLowerCase().replace(/\s+/g, '-');    return prisma.category.create({ data: { ...data, slug } });  }   async delete(id: string) {    const hasProducts = await prisma.product.count({ where: { categoryId: id } });    if (hasProducts > 0) {      throw new AppError('Categoría tiene productos', 400);    }    return prisma.category.delete({ where: { id } });  }}

Review Service

typescript
// src/services/review.service.tsimport { prisma } from '../config/database';import { AppError } from '../errors/AppError'; export class ReviewService {  async create(userId: string, productId: string, data: { rating: number; comment?: string }) {    const exists = await prisma.review.findUnique({      where: { userId_productId: { userId, productId } },    });    if (exists) throw new AppError('Ya has dejado una reseña', 409);     return prisma.review.create({      data: { ...data, userId, productId },      include: { user: { select: { id: true, name: true } } },    });  }   async delete(id: string, userId: string, isAdmin: boolean) {    const review = await prisma.review.findUnique({ where: { id } });    if (!review) throw new AppError('Reseña no encontrada', 404);    if (!isAdmin && review.userId !== userId) {      throw new AppError('No autorizado', 403);    }    return prisma.review.delete({ where: { id } });  }}

Rutas

typescript
// src/routes/product.routes.tsconst router = Router(); router.get('/', asyncHandler(controller.index));router.get('/:slug', asyncHandler(controller.show));router.post('/', authenticate, authorize('ADMIN'), validate(createSchema), asyncHandler(controller.store));router.put('/:id', authenticate, authorize('ADMIN'), validate(updateSchema), asyncHandler(controller.update));router.delete('/:id', authenticate, authorize('ADMIN'), asyncHandler(controller.destroy));router.post('/:id/reviews', authenticate, validate(reviewSchema), asyncHandler(controller.addReview)); export { router as productRoutes };

Resumen Parte 2

  • ✅ CRUD de productos con cache
  • ✅ CRUD de categorías
  • ✅ Sistema de reviews
  • ✅ Filtros y paginación

Próxima parte: Módulo de Pedidos y Finalización! 🚀

¿Te gustó el contenido? ¡Tu contribución ayuda a mantener todo online y gratuito!

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