tomodachi-share/src/app/api/mii/list/route.ts

115 lines
3.2 KiB
TypeScript

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,
})),
});
}