From 45275e56a91906ac1a7a8ed52291f7c0926bd649 Mon Sep 17 00:00:00 2001 From: Hendrik Date: Fri, 17 Apr 2026 15:23:00 +0200 Subject: [PATCH] feat(api): add recipe detail, search, rating, favorite, cooked, comments endpoints Co-Authored-By: Claude Opus 4.7 (1M context) --- src/routes/api/recipes/[id]/+server.ts | 47 +++++++++++++++++++ .../api/recipes/[id]/comments/+server.ts | 40 ++++++++++++++++ src/routes/api/recipes/[id]/cooked/+server.ts | 22 +++++++++ .../api/recipes/[id]/favorite/+server.ts | 31 ++++++++++++ src/routes/api/recipes/[id]/rating/+server.ts | 36 ++++++++++++++ src/routes/api/recipes/search/+server.ts | 11 +++++ 6 files changed, 187 insertions(+) create mode 100644 src/routes/api/recipes/[id]/+server.ts create mode 100644 src/routes/api/recipes/[id]/comments/+server.ts create mode 100644 src/routes/api/recipes/[id]/cooked/+server.ts create mode 100644 src/routes/api/recipes/[id]/favorite/+server.ts create mode 100644 src/routes/api/recipes/[id]/rating/+server.ts create mode 100644 src/routes/api/recipes/search/+server.ts diff --git a/src/routes/api/recipes/[id]/+server.ts b/src/routes/api/recipes/[id]/+server.ts new file mode 100644 index 0000000..941130e --- /dev/null +++ b/src/routes/api/recipes/[id]/+server.ts @@ -0,0 +1,47 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { deleteRecipe, getRecipeById } from '$lib/server/recipes/repository'; +import { + listComments, + listCookingLog, + listRatings, + renameRecipe +} from '$lib/server/recipes/actions'; + +const RenameSchema = z.object({ title: z.string().min(1).max(200) }); + +function parseId(raw: string): number { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); + return id; +} + +export const GET: RequestHandler = async ({ params }) => { + const id = parseId(params.id!); + const db = getDb(); + const recipe = getRecipeById(db, id); + if (!recipe) error(404, { message: 'Recipe not found' }); + const ratings = listRatings(db, id); + const comments = listComments(db, id); + const cooking_log = listCookingLog(db, id); + const avg_stars = + ratings.length === 0 ? null : ratings.reduce((s, r) => s + r.stars, 0) / ratings.length; + return json({ recipe, ratings, comments, cooking_log, avg_stars }); +}; + +export const PATCH: RequestHandler = async ({ params, request }) => { + const id = parseId(params.id!); + const body = await request.json().catch(() => null); + const parsed = RenameSchema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + renameRecipe(getDb(), id, parsed.data.title); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async ({ params }) => { + const id = parseId(params.id!); + deleteRecipe(getDb(), id); + return json({ ok: true }); +}; diff --git a/src/routes/api/recipes/[id]/comments/+server.ts b/src/routes/api/recipes/[id]/comments/+server.ts new file mode 100644 index 0000000..4814240 --- /dev/null +++ b/src/routes/api/recipes/[id]/comments/+server.ts @@ -0,0 +1,40 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { addComment, deleteComment, listComments } from '$lib/server/recipes/actions'; + +const Schema = z.object({ + profile_id: z.number().int().positive(), + text: z.string().min(1).max(2000) +}); + +const DeleteSchema = z.object({ comment_id: z.number().int().positive() }); + +function parseId(raw: string): number { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); + return id; +} + +export const GET: RequestHandler = async ({ params }) => { + const id = parseId(params.id!); + return json(listComments(getDb(), id)); +}; + +export const POST: RequestHandler = async ({ params, request }) => { + const id = parseId(params.id!); + const body = await request.json().catch(() => null); + const parsed = Schema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + const cid = addComment(getDb(), id, parsed.data.profile_id, parsed.data.text); + return json({ id: cid }, { status: 201 }); +}; + +export const DELETE: RequestHandler = async ({ request }) => { + const body = await request.json().catch(() => null); + const parsed = DeleteSchema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + deleteComment(getDb(), parsed.data.comment_id); + return json({ ok: true }); +}; diff --git a/src/routes/api/recipes/[id]/cooked/+server.ts b/src/routes/api/recipes/[id]/cooked/+server.ts new file mode 100644 index 0000000..fc933cc --- /dev/null +++ b/src/routes/api/recipes/[id]/cooked/+server.ts @@ -0,0 +1,22 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { logCooked } from '$lib/server/recipes/actions'; + +const Schema = z.object({ profile_id: z.number().int().positive() }); + +function parseId(raw: string): number { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); + return id; +} + +export const POST: RequestHandler = async ({ params, request }) => { + const id = parseId(params.id!); + const body = await request.json().catch(() => null); + const parsed = Schema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + const entry = logCooked(getDb(), id, parsed.data.profile_id); + return json(entry, { status: 201 }); +}; diff --git a/src/routes/api/recipes/[id]/favorite/+server.ts b/src/routes/api/recipes/[id]/favorite/+server.ts new file mode 100644 index 0000000..5c82f09 --- /dev/null +++ b/src/routes/api/recipes/[id]/favorite/+server.ts @@ -0,0 +1,31 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { addFavorite, removeFavorite } from '$lib/server/recipes/actions'; + +const Schema = z.object({ profile_id: z.number().int().positive() }); + +function parseId(raw: string): number { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); + return id; +} + +export const PUT: RequestHandler = async ({ params, request }) => { + const id = parseId(params.id!); + const body = await request.json().catch(() => null); + const parsed = Schema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + addFavorite(getDb(), id, parsed.data.profile_id); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async ({ params, request }) => { + const id = parseId(params.id!); + const body = await request.json().catch(() => null); + const parsed = Schema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + removeFavorite(getDb(), id, parsed.data.profile_id); + return json({ ok: true }); +}; diff --git a/src/routes/api/recipes/[id]/rating/+server.ts b/src/routes/api/recipes/[id]/rating/+server.ts new file mode 100644 index 0000000..0ea926a --- /dev/null +++ b/src/routes/api/recipes/[id]/rating/+server.ts @@ -0,0 +1,36 @@ +import type { RequestHandler } from './$types'; +import { json, error } from '@sveltejs/kit'; +import { z } from 'zod'; +import { getDb } from '$lib/server/db'; +import { clearRating, setRating } from '$lib/server/recipes/actions'; + +const Schema = z.object({ + profile_id: z.number().int().positive(), + stars: z.number().int().min(1).max(5) +}); + +const DeleteSchema = z.object({ profile_id: z.number().int().positive() }); + +function parseId(raw: string): number { + const id = Number(raw); + if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' }); + return id; +} + +export const PUT: RequestHandler = async ({ params, request }) => { + const id = parseId(params.id!); + const body = await request.json().catch(() => null); + const parsed = Schema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + setRating(getDb(), id, parsed.data.profile_id, parsed.data.stars); + return json({ ok: true }); +}; + +export const DELETE: RequestHandler = async ({ params, request }) => { + const id = parseId(params.id!); + const body = await request.json().catch(() => null); + const parsed = DeleteSchema.safeParse(body); + if (!parsed.success) error(400, { message: 'Invalid body' }); + clearRating(getDb(), id, parsed.data.profile_id); + return json({ ok: true }); +}; diff --git a/src/routes/api/recipes/search/+server.ts b/src/routes/api/recipes/search/+server.ts new file mode 100644 index 0000000..83d554b --- /dev/null +++ b/src/routes/api/recipes/search/+server.ts @@ -0,0 +1,11 @@ +import type { RequestHandler } from './$types'; +import { json } from '@sveltejs/kit'; +import { getDb } from '$lib/server/db'; +import { listRecentRecipes, searchLocal } from '$lib/server/recipes/search-local'; + +export const GET: RequestHandler = async ({ url }) => { + const q = url.searchParams.get('q')?.trim() ?? ''; + const limit = Math.min(Number(url.searchParams.get('limit') ?? 30), 100); + const hits = q.length >= 1 ? searchLocal(getDb(), q, limit) : listRecentRecipes(getDb(), limit); + return json({ query: q, hits }); +};