Files
kochwas/src/routes/api/recipes/[id]/+server.ts
hsiegeln a1baf7f30a feat(db): section_heading roundtrip in recipe-repository
INSERT/SELECT in insertRecipe, replaceIngredients und getRecipeById
um section_heading ergänzt. IngredientSchema im PATCH-Endpoint sowie
Ingredient-Fixtures in search-local-, scaler- und repository-Tests
auf das neue Pflichtfeld aktualisiert.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 14:55:46 +02:00

122 lines
4.1 KiB
TypeScript

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