import type { RequestHandler } from './$types'; import { json, error } from '@sveltejs/kit'; import { z } from 'zod'; import { getDb } from '$lib/server/db'; import { parsePositiveIntParam, validateBody } from '$lib/server/api-helpers'; import { deleteRecipe, getRecipeById, replaceIngredients, replaceSteps, updateRecipeMeta } from '$lib/server/recipes/repository'; import { listComments, listCookingLog, listRatings, renameRecipe, setRecipeHiddenFromRecent } from '$lib/server/recipes/actions'; const IngredientSchema = z.object({ position: z.number().int().nonnegative(), quantity: z.number().nullable(), unit: z.string().max(30).nullable(), name: z.string().min(1).max(200), note: z.string().max(300).nullable(), raw_text: z.string().max(500), section_heading: z.string().max(200).nullable() }); const StepSchema = z.object({ position: z.number().int().positive(), text: z.string().min(1).max(4000) }); const PatchSchema = z .object({ title: z.string().min(1).max(200).optional(), description: z.string().max(2000).nullable().optional(), servings_default: z.number().int().nonnegative().nullable().optional(), servings_unit: z.string().max(30).nullable().optional(), prep_time_min: z.number().int().nonnegative().nullable().optional(), cook_time_min: z.number().int().nonnegative().nullable().optional(), total_time_min: z.number().int().nonnegative().nullable().optional(), cuisine: z.string().max(60).nullable().optional(), category: z.string().max(60).nullable().optional(), ingredients: z.array(IngredientSchema).optional(), steps: z.array(StepSchema).optional(), hidden_from_recent: z.boolean().optional() }) .refine((v) => Object.keys(v).length > 0, { message: 'Empty patch' }); export const GET: RequestHandler = async ({ params }) => { const id = parsePositiveIntParam(params.id, '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 = parsePositiveIntParam(params.id, 'id'); const body = await request.json().catch(() => null); const p = validateBody(body, PatchSchema); const db = getDb(); // Spezielle Kurz-Updates (bleiben als Sonderfall, weil sie FTS triggern // bzw. andere Tabellen mitpflegen). if (p.title !== undefined && Object.keys(p).length === 1) { renameRecipe(db, id, p.title); return json({ ok: true }); } if (p.hidden_from_recent !== undefined && Object.keys(p).length === 1) { setRecipeHiddenFromRecent(db, id, p.hidden_from_recent); return json({ ok: true }); } // Voller Edit-Modus-Patch. const hasMeta = p.title !== undefined || p.description !== undefined || p.servings_default !== undefined || p.servings_unit !== undefined || p.prep_time_min !== undefined || p.cook_time_min !== undefined || p.total_time_min !== undefined || p.cuisine !== undefined || p.category !== undefined; if (hasMeta) { updateRecipeMeta(db, id, { title: p.title, description: p.description, servings_default: p.servings_default, servings_unit: p.servings_unit, prep_time_min: p.prep_time_min, cook_time_min: p.cook_time_min, total_time_min: p.total_time_min, cuisine: p.cuisine, category: p.category }); } if (p.ingredients !== undefined) { replaceIngredients(db, id, p.ingredients); } if (p.steps !== undefined) { replaceSteps(db, id, p.steps); } if (p.hidden_from_recent !== undefined) { setRecipeHiddenFromRecent(db, id, p.hidden_from_recent); } return json({ ok: true, recipe: getRecipeById(db, id) }); }; export const DELETE: RequestHandler = async ({ params }) => { const id = parsePositiveIntParam(params.id, 'id'); deleteRecipe(getDb(), id); return json({ ok: true }); };