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>
122 lines
4.1 KiB
TypeScript
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 });
|
|
};
|