125 lines
3.1 KiB
TypeScript
125 lines
3.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import {
|
|
db,
|
|
recipes,
|
|
users,
|
|
eq,
|
|
and,
|
|
or,
|
|
ilike,
|
|
sql,
|
|
desc,
|
|
} from "@epicure/db";
|
|
|
|
const VALID_DIETARY = ["vegan", "vegetarian", "glutenFree", "dairyFree"] as const;
|
|
type DietaryTag = (typeof VALID_DIETARY)[number];
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const { searchParams } = req.nextUrl;
|
|
|
|
// --- Parse & validate required param ---
|
|
const q = (searchParams.get("q") ?? "").trim().slice(0, 200);
|
|
if (!q) {
|
|
return NextResponse.json(
|
|
{ error: "Query parameter 'q' is required and must not be empty." },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
// --- Optional params ---
|
|
const difficultyParam = searchParams.get("difficulty");
|
|
const difficulty =
|
|
difficultyParam === "easy" ||
|
|
difficultyParam === "medium" ||
|
|
difficultyParam === "hard"
|
|
? (difficultyParam as "easy" | "medium" | "hard")
|
|
: undefined;
|
|
|
|
const maxMinsRaw = searchParams.get("maxMins");
|
|
const maxMins =
|
|
maxMinsRaw !== null && !Number.isNaN(Number(maxMinsRaw))
|
|
? Number(maxMinsRaw)
|
|
: undefined;
|
|
|
|
const limitRaw = searchParams.get("limit");
|
|
const limit = Math.min(
|
|
limitRaw !== null && !Number.isNaN(Number(limitRaw))
|
|
? Math.max(1, Number(limitRaw))
|
|
: 20,
|
|
50
|
|
);
|
|
|
|
const offsetRaw = searchParams.get("offset");
|
|
const offset =
|
|
offsetRaw !== null && !Number.isNaN(Number(offsetRaw))
|
|
? Math.max(0, Number(offsetRaw))
|
|
: 0;
|
|
|
|
const dietaryRaw = searchParams.get("dietary");
|
|
const dietaryTags: DietaryTag[] = dietaryRaw
|
|
? (dietaryRaw
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter((s): s is DietaryTag =>
|
|
(VALID_DIETARY as readonly string[]).includes(s)
|
|
))
|
|
: [];
|
|
|
|
// --- Build WHERE conditions ---
|
|
const conditions = [
|
|
eq(recipes.visibility, "public"),
|
|
or(
|
|
ilike(recipes.title, `%${q}%`),
|
|
ilike(recipes.description, `%${q}%`)
|
|
)!,
|
|
];
|
|
|
|
if (difficulty) {
|
|
conditions.push(eq(recipes.difficulty, difficulty));
|
|
}
|
|
|
|
if (maxMins !== undefined) {
|
|
conditions.push(
|
|
sql`(${recipes.prepMins} + ${recipes.cookMins}) <= ${maxMins}`
|
|
);
|
|
}
|
|
|
|
for (const tag of dietaryTags) {
|
|
conditions.push(sql`${recipes.dietaryTags}->>${tag} = 'true'`);
|
|
}
|
|
|
|
const where = and(...conditions);
|
|
|
|
// --- Main data query ---
|
|
const rows = await db
|
|
.select({
|
|
id: recipes.id,
|
|
title: recipes.title,
|
|
description: recipes.description,
|
|
difficulty: recipes.difficulty,
|
|
baseServings: recipes.baseServings,
|
|
prepMins: recipes.prepMins,
|
|
cookMins: recipes.cookMins,
|
|
authorId: recipes.authorId,
|
|
authorName: users.name,
|
|
createdAt: recipes.createdAt,
|
|
})
|
|
.from(recipes)
|
|
.innerJoin(users, eq(recipes.authorId, users.id))
|
|
.where(where)
|
|
.orderBy(desc(recipes.createdAt))
|
|
.limit(limit)
|
|
.offset(offset);
|
|
|
|
// --- Count query ---
|
|
const countResult = await db
|
|
.select({ total: sql<number>`count(*)::int` })
|
|
.from(recipes)
|
|
.innerJoin(users, eq(recipes.authorId, users.id))
|
|
.where(where);
|
|
|
|
const total = countResult[0]?.total ?? 0;
|
|
|
|
return NextResponse.json({ data: rows, total, limit, offset });
|
|
}
|