first commit
This commit is contained in:
37
app/api/products/[slug]/route.ts
Normal file
37
app/api/products/[slug]/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
export async function GET(request: NextRequest, props: { params: Promise<{ slug: string }> }) {
|
||||
const params = await props.params;
|
||||
try {
|
||||
const product = await prisma.product.findUnique({
|
||||
where: {
|
||||
slug: params.slug,
|
||||
isActive: true
|
||||
},
|
||||
include: {
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Product not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(product)
|
||||
} catch (error) {
|
||||
console.error('Error fetching product by slug:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch product' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
214
app/api/products/route.ts
Normal file
214
app/api/products/route.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from '@/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { productSchema } from '@/lib/validations/product'
|
||||
import { DatabaseOptimizer } from '@/lib/database-optimizer'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const category = searchParams.get('category')
|
||||
const search = searchParams.get('search')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const limit = parseInt(searchParams.get('limit') || '12')
|
||||
const isAdmin = searchParams.get('admin') === 'true'
|
||||
const skip = (page - 1) * limit
|
||||
|
||||
// Create cache key for this specific query
|
||||
const cacheKey = `products:${category || 'all'}:${search || 'none'}:${page}:${limit}:${isAdmin}`
|
||||
|
||||
// Try to get from cache first
|
||||
const cached = await DatabaseOptimizer.getCachedData(cacheKey)
|
||||
if (cached && !isAdmin) { // Don't cache admin requests
|
||||
return NextResponse.json({
|
||||
...cached,
|
||||
_performance: {
|
||||
responseTime: Date.now() - startTime,
|
||||
cached: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const where: any = {}
|
||||
|
||||
// Only filter by isActive if not admin request
|
||||
if (!isAdmin) {
|
||||
where.isActive = true
|
||||
// Add stock filter for better SEO (only show available products)
|
||||
where.stock = { gt: 0 }
|
||||
}
|
||||
|
||||
if (category) {
|
||||
where.categoryId = category
|
||||
}
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ name: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
{
|
||||
category: {
|
||||
name: { contains: search, mode: 'insensitive' }
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Optimize queries with parallel execution and selective field inclusion
|
||||
const [products, total, categoryStats] = await Promise.all([
|
||||
// Optimized products query with caching
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`products_list_${JSON.stringify(where)}_${skip}_${limit}`,
|
||||
() => prisma.product.findMany({
|
||||
where,
|
||||
include: {
|
||||
category: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
}
|
||||
},
|
||||
reviews: {
|
||||
select: {
|
||||
rating: true,
|
||||
createdAt: true,
|
||||
},
|
||||
take: 5, // Limit reviews for performance
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
}
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
reviews: true,
|
||||
orderItems: true, // For popularity metrics
|
||||
}
|
||||
}
|
||||
},
|
||||
skip,
|
||||
take: limit,
|
||||
orderBy: [
|
||||
{ isActive: 'desc' }, // Active products first
|
||||
{ stock: 'desc' }, // In-stock products next
|
||||
{ createdAt: 'desc' }, // Then by creation date
|
||||
],
|
||||
}),
|
||||
300 // Cache for 5 minutes
|
||||
),
|
||||
// Optimized count query
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`products_count_${JSON.stringify(where)}`,
|
||||
() => prisma.product.count({ where }),
|
||||
600 // Cache for 10 minutes
|
||||
),
|
||||
// Optimized category stats
|
||||
DatabaseOptimizer.executeOptimizedQuery(
|
||||
`category_stats_${category || 'all'}`,
|
||||
() => prisma.product.groupBy({
|
||||
by: ['categoryId'],
|
||||
where: { isActive: true, stock: { gt: 0 } },
|
||||
_count: {
|
||||
categoryId: true,
|
||||
},
|
||||
orderBy: {
|
||||
_count: {
|
||||
categoryId: 'desc'
|
||||
}
|
||||
}
|
||||
}),
|
||||
900 // Cache for 15 minutes
|
||||
)
|
||||
])
|
||||
|
||||
|
||||
// Calculate advanced metrics for SEO and performance
|
||||
const productsWithMetrics = products.map(product => ({
|
||||
...product,
|
||||
averageRating: product.reviews.length > 0
|
||||
? Math.round((product.reviews.reduce((acc, review) => acc + review.rating, 0) / product.reviews.length) * 10) / 10
|
||||
: null,
|
||||
reviewCount: product._count.reviews,
|
||||
popularityScore: product._count.orderItems,
|
||||
hasRecentReviews: product.reviews.some(review =>
|
||||
new Date(review.createdAt) > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
),
|
||||
isPopular: product._count.orderItems > 10,
|
||||
inStock: product.stock > 0,
|
||||
lowStock: product.stock > 0 && product.stock <= 10,
|
||||
// Remove internal data from response
|
||||
reviews: undefined,
|
||||
_count: undefined,
|
||||
}))
|
||||
|
||||
const responseData = {
|
||||
products: productsWithMetrics,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
seo: {
|
||||
totalProducts: total,
|
||||
categoryDistribution: categoryStats,
|
||||
hasProducts: total > 0
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the response for non-admin requests
|
||||
if (!isAdmin) {
|
||||
await DatabaseOptimizer.setCachedData(cacheKey, responseData, 300)
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
...responseData,
|
||||
_performance: {
|
||||
responseTime: Date.now() - startTime,
|
||||
cached: false
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Products API error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth()
|
||||
if (!session?.user || session.user.role !== 'ADMIN') {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
const validatedData = productSchema.parse(body)
|
||||
|
||||
// Generate SKU and slug if not provided
|
||||
const sku = validatedData.sku || `SKU-${Date.now()}`
|
||||
const slug = validatedData.slug || validatedData.name.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '')
|
||||
|
||||
const product = await prisma.product.create({
|
||||
data: {
|
||||
...validatedData,
|
||||
sku,
|
||||
slug
|
||||
},
|
||||
include: {
|
||||
category: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Invalidate product caches after creation
|
||||
DatabaseOptimizer.invalidateCache('products')
|
||||
DatabaseOptimizer.invalidateCache('category_stats')
|
||||
|
||||
return NextResponse.json(product, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('Create product error:', error)
|
||||
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user