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
text/typescript
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' });
}