import { NextRequest } from "next/server"; import { Prisma } from "@prisma/client"; import { z } from "zod"; import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { querySchema } from "@/lib/schemas"; import { RateLimit } from "@/lib/rate-limit"; const searchSchema = z.object({ q: querySchema.optional(), sort: z.enum(["newest", "likes"], { message: "Sort must be either 'newest' or 'likes'" }).default("newest"), tags: z .string() .optional() .transform((value) => value ?.split(",") .map((tag) => tag.trim()) .filter((tag) => tag.length > 0) ), // todo: incorporate tagsSchema // Profiles userId: z.coerce .number({ message: "User ID must be a number" }) .int({ message: "User ID must be an integer" }) .positive({ message: "User ID must be valid" }) .optional(), // Pages limit: z.coerce .number({ message: "Limit must be a number" }) .int({ message: "Limit must be an integer" }) .min(1, { message: "Limit must be at least 1" }) .max(100, { message: "Limit cannot be more than 100" }) .optional(), page: z.coerce .number({ message: "Page must be a number" }) .int({ message: "Page must be an integer" }) .min(1, { message: "Page must be at least 1" }) .optional(), }); export async function GET(request: NextRequest) { const session = await auth(); const rateLimit = new RateLimit(request, 30); const check = await rateLimit.handle(); if (check) return check; const parsed = searchSchema.safeParse(Object.fromEntries(request.nextUrl.searchParams)); if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.errors[0].message }, 400); const { q: query, sort, tags, userId, page = 1, limit = 24 } = parsed.data; const where: Prisma.MiiWhereInput = { // Searching ...(query && { OR: [{ name: { contains: query, mode: "insensitive" } }, { tags: { has: query } }], }), // Tag filtering ...(tags && tags.length > 0 && { tags: { hasEvery: tags } }), // Profiles ...(userId && { userId }), }; // Sorting by likes or newest const orderBy: Prisma.MiiOrderByWithRelationInput = sort === "likes" ? { likedBy: { _count: "desc" } } : { createdAt: "desc" }; const select: Prisma.MiiSelect = { id: true, // Don't show when userId is specified ...(!userId && { user: { select: { id: true, username: true, }, }, }), name: true, imageCount: true, tags: true, createdAt: true, // Mii liked check ...(session?.user?.id && { likedBy: { where: { userId: Number(session.user.id) }, select: { userId: true }, }, }), // Like count _count: { select: { likedBy: true }, }, }; const skip = (page - 1) * limit; const [totalCount, filteredCount, list] = await Promise.all([ prisma.mii.count({ where: { ...where, userId } }), prisma.mii.count({ where, skip, take: limit }), prisma.mii.findMany({ where, orderBy, select, skip: (page - 1) * limit, take: limit }), ]); return rateLimit.sendResponse({ total: totalCount, filtered: filteredCount, lastPage: Math.ceil(totalCount / limit), miis: list.map(({ _count, likedBy, ...rest }) => ({ ...rest, likes: _count.likedBy, isLiked: session?.user?.id ? likedBy.length > 0 : false, })), }); }