- Add try-catch around JSON.parse with fail(400) for malformed input - Validate effort against allowed values ['Easy','Medium','Hard'] - Fix NaN risk: Number(serves)||undefined instead of Number(serves) - Add action tests for create/update: validation, JSON.parse crash, success, API error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
99 lines
3.2 KiB
TypeScript
99 lines
3.2 KiB
TypeScript
import { error, redirect, fail } from '@sveltejs/kit';
|
|
import type { PageServerLoad, Actions } from './$types';
|
|
import { apiClient } from '$lib/server/api';
|
|
|
|
const VALID_EFFORTS = ['Easy', 'Medium', 'Hard'];
|
|
|
|
export const load: PageServerLoad = async ({ fetch, params }) => {
|
|
const api = apiClient(fetch);
|
|
const [recipeResult, tagsResult] = await Promise.all([
|
|
api.GET('/v1/recipes/{id}', { params: { path: { id: params.id } } }),
|
|
api.GET('/v1/tags', {})
|
|
]);
|
|
|
|
if (recipeResult.error || !recipeResult.data) {
|
|
error(404, 'Recipe not found');
|
|
}
|
|
|
|
const recipe = recipeResult.data;
|
|
const allTags = tagsResult.data ?? [];
|
|
const categories = allTags
|
|
.filter((t) => t.tagType === 'category')
|
|
.map((t) => ({ id: t.id!, name: t.name!, tagType: t.tagType }));
|
|
|
|
return {
|
|
recipe: {
|
|
id: recipe.id!,
|
|
name: recipe.name!,
|
|
serves: recipe.serves,
|
|
cookTimeMin: recipe.cookTimeMin,
|
|
effort: recipe.effort,
|
|
heroImageUrl: recipe.heroImageUrl,
|
|
ingredients: (recipe.ingredients ?? []).map((ing) => ({
|
|
name: ing.name ?? '',
|
|
quantity: ing.quantity ?? 0,
|
|
unit: ing.unit ?? ''
|
|
})),
|
|
steps: (recipe.steps ?? [])
|
|
.sort((a, b) => (a.stepNumber ?? 0) - (b.stepNumber ?? 0))
|
|
.map((s) => ({ instruction: s.instruction ?? '' })),
|
|
tagIds: (recipe.tags ?? []).map((t) => t.id!)
|
|
},
|
|
categories
|
|
};
|
|
};
|
|
|
|
export const actions: Actions = {
|
|
update: async ({ request, fetch, params }) => {
|
|
const formData = await request.formData();
|
|
const name = formData.get('name') as string;
|
|
const serves = formData.get('serves');
|
|
const cookTimeMin = formData.get('cookTimeMin');
|
|
const effort = formData.get('effort') as string;
|
|
const ingredientsJson = formData.get('ingredientsJson') as string;
|
|
const stepsJson = formData.get('stepsJson') as string;
|
|
const tagIds = formData.getAll('tagIds') as string[];
|
|
|
|
if (!name?.trim()) return fail(422, { error: 'Name ist erforderlich' });
|
|
if (!effort || !VALID_EFFORTS.includes(effort))
|
|
return fail(422, { error: 'Ungültiger Schwierigkeitsgrad' });
|
|
if (!tagIds.length) return fail(422, { error: 'Mindestens eine Kategorie ist erforderlich' });
|
|
|
|
let parsedIngredients: unknown[];
|
|
let parsedSteps: unknown[];
|
|
try {
|
|
parsedIngredients = JSON.parse(ingredientsJson || '[]');
|
|
parsedSteps = JSON.parse(stepsJson || '[]');
|
|
} catch {
|
|
return fail(400, { error: 'Ungültige Formulardaten' });
|
|
}
|
|
|
|
const api = apiClient(fetch);
|
|
const { error: apiError } = await api.PUT('/v1/recipes/{id}', {
|
|
params: { path: { id: params.id } },
|
|
body: {
|
|
name: name.trim(),
|
|
serves: serves ? Number(serves) || undefined : undefined,
|
|
cookTimeMin: cookTimeMin ? Number(cookTimeMin) || undefined : undefined,
|
|
effort,
|
|
ingredients: (parsedIngredients as { name: string; quantity: string; unit: string }[])
|
|
.filter((ing) => ing.name?.trim())
|
|
.map((ing, i) => ({
|
|
newIngredientName: ing.name.trim(),
|
|
quantity: Number(ing.quantity) || 0,
|
|
unit: ing.unit || '',
|
|
sortOrder: i
|
|
})),
|
|
steps: (parsedSteps as string[])
|
|
.filter((s) => s?.trim())
|
|
.map((s, i) => ({ stepNumber: i + 1, instruction: s.trim() })),
|
|
tagIds
|
|
}
|
|
});
|
|
|
|
if (apiError) return fail(500, { error: 'Fehler beim Speichern' });
|
|
|
|
redirect(303, '/recipes');
|
|
}
|
|
};
|