UNPKG

sysrot-hub

Version:

CLI de nueva generación para proyectos Next.js 14+ con IA multi-modelo, Web3 integration, internacionalización completa y roadmap realista 2025-2026

336 lines (292 loc) 9.13 kB
import { NextApiRequest, NextApiResponse } from 'next'; import { PrismaClient } from '@prisma/client'; import { getSession } from 'next-auth/react'; import { z } from 'zod'; const prisma = new PrismaClient(); // Validation schemas const createProductSchema = z.object({ name: z.string().min(1, 'Name is required'), slug: z.string().min(1, 'Slug is required'), description: z.string().optional(), shortDescription: z.string().optional(), price: z.number().positive('Price must be positive'), comparePrice: z.number().positive().optional(), sku: z.string().optional(), weight: z.number().positive().optional(), dimensions: z.string().optional(), categoryId: z.string().min(1, 'Category is required'), stock: z.number().int().min(0, 'Stock must be non-negative').default(0), lowStockAlert: z.number().int().min(0).default(5), status: z.enum(['ACTIVE', 'DRAFT', 'ARCHIVED']).default('ACTIVE'), featured: z.boolean().default(false), seoTitle: z.string().optional(), seoDescription: z.string().optional(), images: z.array(z.object({ url: z.string().url(), alt: z.string().optional(), position: z.number().int().min(0).default(0) })).optional(), tags: z.array(z.string()).optional(), variants: z.array(z.object({ name: z.string(), value: z.string(), price: z.number().optional(), sku: z.string().optional(), stock: z.number().int().min(0).default(0) })).optional() }); const updateProductSchema = createProductSchema.partial(); async function isAdmin(req: NextApiRequest): Promise<boolean> { const session = await getSession({ req }); return session?.user?.role === 'admin'; } export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { switch (req.method) { case 'GET': return await handleGet(req, res); case 'POST': return await handlePost(req, res); case 'PUT': return await handlePut(req, res); case 'DELETE': return await handleDelete(req, res); default: return res.status(405).json({ error: 'Method not allowed' }); } } catch (error) { console.error('API Error:', error); return res.status(500).json({ error: 'Internal server error' }); } finally { await prisma.$disconnect(); } } async function handleGet(req: NextApiRequest, res: NextApiResponse) { const { page = '1', limit = '10', category, search, status, featured, sortBy = 'createdAt', sortOrder = 'desc' } = req.query; const pageNum = parseInt(page as string); const limitNum = parseInt(limit as string); const skip = (pageNum - 1) * limitNum; const where: any = {}; if (category) { where.category = { slug: category }; } if (search) { where.OR = [ { name: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, { sku: { contains: search, mode: 'insensitive' } } ]; } if (status) { where.status = status; } if (featured !== undefined) { where.featured = featured === 'true'; } const [products, total] = await Promise.all([ prisma.product.findMany({ where, include: { category: true, images: { orderBy: { position: 'asc' } }, tags: true, variants: true, _count: { select: { reviews: true, orderItems: true } } }, orderBy: { [sortBy as string]: sortOrder }, skip, take: limitNum }), prisma.product.count({ where }) ]); // Calculate average rating for each product const productsWithRating = await Promise.all( products.map(async (product) => { const avgRating = await prisma.review.aggregate({ where: { productId: product.id, status: 'APPROVED' }, _avg: { rating: true } }); return { ...product, avgRating: avgRating._avg.rating || 0, reviewCount: product._count.reviews }; }) ); return res.status(200).json({ products: productsWithRating, pagination: { page: pageNum, limit: limitNum, total, pages: Math.ceil(total / limitNum) } }); } async function handlePost(req: NextApiRequest, res: NextApiResponse) { if (!(await isAdmin(req))) { return res.status(403).json({ error: 'Admin access required' }); } const validatedData = createProductSchema.parse(req.body); // Check if slug is unique const existingProduct = await prisma.product.findUnique({ where: { slug: validatedData.slug } }); if (existingProduct) { return res.status(400).json({ error: 'Product slug already exists' }); } // Check if SKU is unique (if provided) if (validatedData.sku) { const existingSku = await prisma.product.findUnique({ where: { sku: validatedData.sku } }); if (existingSku) { return res.status(400).json({ error: 'SKU already exists' }); } } const { images, tags, variants, ...productData } = validatedData; const product = await prisma.product.create({ data: { ...productData, images: { create: images || [] }, tags: { connectOrCreate: tags?.map(tagName => ({ where: { name: tagName }, create: { name: tagName, slug: tagName.toLowerCase().replace(/\s+/g, '-') } })) || [] }, variants: { create: variants || [] } }, include: { category: true, images: { orderBy: { position: 'asc' } }, tags: true, variants: true } }); return res.status(201).json(product); } async function handlePut(req: NextApiRequest, res: NextApiResponse) { if (!(await isAdmin(req))) { return res.status(403).json({ error: 'Admin access required' }); } const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Product ID is required' }); } const validatedData = updateProductSchema.parse(req.body); // Check if product exists const existingProduct = await prisma.product.findUnique({ where: { id } }); if (!existingProduct) { return res.status(404).json({ error: 'Product not found' }); } // Check slug uniqueness if being updated if (validatedData.slug && validatedData.slug !== existingProduct.slug) { const slugExists = await prisma.product.findUnique({ where: { slug: validatedData.slug } }); if (slugExists) { return res.status(400).json({ error: 'Product slug already exists' }); } } // Check SKU uniqueness if being updated if (validatedData.sku && validatedData.sku !== existingProduct.sku) { const skuExists = await prisma.product.findUnique({ where: { sku: validatedData.sku } }); if (skuExists) { return res.status(400).json({ error: 'SKU already exists' }); } } const { images, tags, variants, ...productData } = validatedData; // Update product with related data const product = await prisma.product.update({ where: { id }, data: { ...productData, ...(images && { images: { deleteMany: {}, create: images } }), ...(tags && { tags: { set: [], connectOrCreate: tags.map(tagName => ({ where: { name: tagName }, create: { name: tagName, slug: tagName.toLowerCase().replace(/\s+/g, '-') } })) } }), ...(variants && { variants: { deleteMany: {}, create: variants } }) }, include: { category: true, images: { orderBy: { position: 'asc' } }, tags: true, variants: true } }); return res.status(200).json(product); } async function handleDelete(req: NextApiRequest, res: NextApiResponse) { if (!(await isAdmin(req))) { return res.status(403).json({ error: 'Admin access required' }); } const { id } = req.query; if (!id || typeof id !== 'string') { return res.status(400).json({ error: 'Product ID is required' }); } // Check if product exists const existingProduct = await prisma.product.findUnique({ where: { id }, include: { orderItems: true } }); if (!existingProduct) { return res.status(404).json({ error: 'Product not found' }); } // Check if product has associated orders if (existingProduct.orderItems.length > 0) { return res.status(400).json({ error: 'Cannot delete product with existing orders. Archive it instead.' }); } await prisma.product.delete({ where: { id } }); return res.status(200).json({ message: 'Product deleted successfully' }); }