mirror of
https://github.com/trafficlunar/tomodachi-share.git
synced 2026-06-15 03:54:47 +00:00
feat: public search and mii data API routes
- also with an API reference that is not done
This commit is contained in:
parent
20ac1ea280
commit
196f9d4640
7 changed files with 302 additions and 34 deletions
92
src/app/api/search/route.ts
Normal file
92
src/app/api/search/route.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
import crypto from "crypto";
|
||||
import seedrandom from "seedrandom";
|
||||
|
||||
import { searchSchema } from "@/lib/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const rateLimit = new RateLimit(request, 24, "/api/search");
|
||||
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.issues[0].message }, 400);
|
||||
|
||||
const { q: query, sort, tags, gender, page = 1, limit = 24, seed } = parsed.data;
|
||||
|
||||
const where: Prisma.MiiWhereInput = {
|
||||
// Searching
|
||||
...(query && {
|
||||
OR: [{ name: { contains: query, mode: "insensitive" } }, { tags: { has: query } }, { description: { contains: query, mode: "insensitive" } }],
|
||||
}),
|
||||
// Tag filtering
|
||||
...(tags && tags.length > 0 && { tags: { hasEvery: tags } }),
|
||||
// Gender
|
||||
...(gender && { gender: { equals: gender } }),
|
||||
};
|
||||
|
||||
const select: Prisma.MiiSelect = {
|
||||
id: true,
|
||||
name: true,
|
||||
imageCount: true,
|
||||
tags: true,
|
||||
createdAt: true,
|
||||
gender: true,
|
||||
// Like count
|
||||
_count: {
|
||||
select: { likedBy: true },
|
||||
},
|
||||
};
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
if (sort === "random") {
|
||||
// Use seed for consistent random results
|
||||
const randomSeed = seed || crypto.randomInt(0, 1_000_000_000);
|
||||
|
||||
// Get all IDs that match the where conditions
|
||||
const matchingIds = await prisma.mii.findMany({
|
||||
where,
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (matchingIds.length === 0) return;
|
||||
|
||||
const rng = seedrandom(randomSeed.toString());
|
||||
|
||||
// Randomize all IDs using the Durstenfeld algorithm
|
||||
for (let i = matchingIds.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(rng() * (i + 1));
|
||||
[matchingIds[i], matchingIds[j]] = [matchingIds[j], matchingIds[i]];
|
||||
}
|
||||
|
||||
// Convert to number[] array and return paginated results
|
||||
return rateLimit.sendResponse(matchingIds.slice(skip, skip + limit).map((i) => i.id));
|
||||
} else {
|
||||
// Sorting by likes, newest, or oldest
|
||||
let orderBy: Prisma.MiiOrderByWithRelationInput[];
|
||||
|
||||
if (sort === "likes") {
|
||||
orderBy = [{ likedBy: { _count: "desc" } }, { name: "asc" }];
|
||||
} else if (sort === "oldest") {
|
||||
orderBy = [{ createdAt: "asc" }, { name: "asc" }];
|
||||
} else {
|
||||
// default to newest
|
||||
orderBy = [{ createdAt: "desc" }, { name: "asc" }];
|
||||
}
|
||||
|
||||
const list = await prisma.mii.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
select: { id: true },
|
||||
skip,
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return rateLimit.sendResponse(list.map((mii) => mii.id));
|
||||
}
|
||||
}
|
||||
49
src/app/mii/[id]/data/route.ts
Normal file
49
src/app/mii/[id]/data/route.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { NextRequest } from "next/server";
|
||||
|
||||
import { idSchema } from "@/lib/schemas";
|
||||
import { RateLimit } from "@/lib/rate-limit";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
||||
const rateLimit = new RateLimit(request, 200, "/mii/data");
|
||||
const check = await rateLimit.handle();
|
||||
if (check) return check;
|
||||
|
||||
const { id: slugId } = await params;
|
||||
const parsed = idSchema.safeParse(slugId);
|
||||
if (!parsed.success) return rateLimit.sendResponse({ error: parsed.error.issues[0].message }, 400);
|
||||
const miiId = parsed.data;
|
||||
|
||||
const data = await prisma.mii.findUnique({
|
||||
where: { id: miiId },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
_count: {
|
||||
select: {
|
||||
likedBy: true,
|
||||
},
|
||||
},
|
||||
imageCount: true,
|
||||
tags: true,
|
||||
description: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
gender: true,
|
||||
islandName: true,
|
||||
allowedCopying: true,
|
||||
createdAt: true,
|
||||
user: { select: { id: true, username: true, name: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return rateLimit.sendResponse({ error: "Mii not found" }, 404);
|
||||
}
|
||||
|
||||
const { _count, ...rest } = data;
|
||||
return rateLimit.sendResponse({
|
||||
...rest,
|
||||
likes: _count.likedBy,
|
||||
});
|
||||
}
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
import Link from "next/link";
|
||||
|
||||
import { MiiGender, Prisma } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { z } from "zod";
|
||||
|
||||
import crypto from "crypto";
|
||||
import seedrandom from "seedrandom";
|
||||
|
||||
import { querySchema } from "@/lib/schemas";
|
||||
import { searchSchema } from "@/lib/schemas";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
|
|
@ -25,36 +24,6 @@ interface Props {
|
|||
inLikesPage?: boolean; // Self-explanatory
|
||||
}
|
||||
|
||||
const searchSchema = z.object({
|
||||
q: querySchema.optional(),
|
||||
sort: z.enum(["likes", "newest", "oldest", "random"], { error: "Sort must be either 'likes', 'newest', 'oldest', or 'random'" }).default("newest"),
|
||||
tags: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) =>
|
||||
value
|
||||
?.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag.length > 0)
|
||||
),
|
||||
gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', or 'FEMALE'" }).optional(),
|
||||
// todo: incorporate tagsSchema
|
||||
// Pages
|
||||
limit: z.coerce
|
||||
.number({ error: "Limit must be a number" })
|
||||
.int({ error: "Limit must be an integer" })
|
||||
.min(1, { error: "Limit must be at least 1" })
|
||||
.max(100, { error: "Limit cannot be more than 100" })
|
||||
.optional(),
|
||||
page: z.coerce
|
||||
.number({ error: "Page must be a number" })
|
||||
.int({ error: "Page must be an integer" })
|
||||
.min(1, { error: "Page must be at least 1" })
|
||||
.optional(),
|
||||
// Random sort
|
||||
seed: z.coerce.number({ error: "Seed must be a number" }).int({ error: "Seed must be an integer" }).optional(),
|
||||
});
|
||||
|
||||
export default async function MiiList({ searchParams, userId, inLikesPage }: Props) {
|
||||
const session = await auth();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { MiiGender } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
// profanity censoring bypasses the regex in some of these but I think it's funny
|
||||
|
|
@ -39,6 +40,36 @@ export const idSchema = z.coerce
|
|||
.int({ error: "ID must be an integer" })
|
||||
.positive({ error: "ID must be valid" });
|
||||
|
||||
export const searchSchema = z.object({
|
||||
q: querySchema.optional(),
|
||||
sort: z.enum(["likes", "newest", "oldest", "random"], { error: "Sort must be either 'likes', 'newest', 'oldest', or 'random'" }).default("newest"),
|
||||
tags: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((value) =>
|
||||
value
|
||||
?.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag.length > 0)
|
||||
),
|
||||
gender: z.enum(MiiGender, { error: "Gender must be either 'MALE', or 'FEMALE'" }).optional(),
|
||||
// todo: incorporate tagsSchema
|
||||
// Pages
|
||||
limit: z.coerce
|
||||
.number({ error: "Limit must be a number" })
|
||||
.int({ error: "Limit must be an integer" })
|
||||
.min(1, { error: "Limit must be at least 1" })
|
||||
.max(100, { error: "Limit cannot be more than 100" })
|
||||
.optional(),
|
||||
page: z.coerce
|
||||
.number({ error: "Page must be a number" })
|
||||
.int({ error: "Page must be an integer" })
|
||||
.min(1, { error: "Page must be at least 1" })
|
||||
.optional(),
|
||||
// Random sort
|
||||
seed: z.coerce.number({ error: "Seed must be a number" }).int({ error: "Seed must be an integer" }).optional(),
|
||||
});
|
||||
|
||||
// Account Info
|
||||
export const usernameSchema = z
|
||||
.string()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue