From a1baf7f30acb5a0cbc9ef56c252a9fb2e5bbd142 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:55:46 +0200 Subject: [PATCH] feat(db): section_heading roundtrip in recipe-repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/lib/server/recipes/repository.ts | 14 ++++---- src/routes/api/recipes/[id]/+server.ts | 3 +- tests/integration/recipe-repository.test.ts | 36 ++++++++++++++++++--- tests/integration/search-local.test.ts | 2 +- tests/unit/scaler.test.ts | 3 +- 5 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/lib/server/recipes/repository.ts b/src/lib/server/recipes/repository.ts index 24d54ee..6a97c17 100644 --- a/src/lib/server/recipes/repository.ts +++ b/src/lib/server/recipes/repository.ts @@ -64,11 +64,11 @@ export function insertRecipe(db: Database.Database, recipe: Recipe): number { const id = Number(info.lastInsertRowid); const insIng = db.prepare( - `INSERT INTO ingredient(recipe_id, position, quantity, unit, name, note, raw_text) - VALUES (?, ?, ?, ?, ?, ?, ?)` + `INSERT INTO ingredient(recipe_id, position, quantity, unit, name, note, raw_text, section_heading) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` ); for (const ing of recipe.ingredients) { - insIng.run(id, ing.position, ing.quantity, ing.unit, ing.name, ing.note, ing.raw_text); + insIng.run(id, ing.position, ing.quantity, ing.unit, ing.name, ing.note, ing.raw_text, ing.section_heading); } const insStep = db.prepare( @@ -104,7 +104,7 @@ export function getRecipeById(db: Database.Database, id: number): Recipe | null const ingredients = db .prepare( - `SELECT position, quantity, unit, name, note, raw_text + `SELECT position, quantity, unit, name, note, raw_text, section_heading FROM ingredient WHERE recipe_id = ? ORDER BY position` ) .all(id) as Ingredient[]; @@ -215,11 +215,11 @@ export function replaceIngredients( const tx = db.transaction(() => { db.prepare('DELETE FROM ingredient WHERE recipe_id = ?').run(recipeId); const ins = db.prepare( - `INSERT INTO ingredient(recipe_id, position, quantity, unit, name, note, raw_text) - VALUES (?, ?, ?, ?, ?, ?, ?)` + `INSERT INTO ingredient(recipe_id, position, quantity, unit, name, note, raw_text, section_heading) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` ); for (const ing of ingredients) { - ins.run(recipeId, ing.position, ing.quantity, ing.unit, ing.name, ing.note, ing.raw_text); + ins.run(recipeId, ing.position, ing.quantity, ing.unit, ing.name, ing.note, ing.raw_text, ing.section_heading); } refreshFts(db, recipeId); }); diff --git a/src/routes/api/recipes/[id]/+server.ts b/src/routes/api/recipes/[id]/+server.ts index 5448d9d..37f73b0 100644 --- a/src/routes/api/recipes/[id]/+server.ts +++ b/src/routes/api/recipes/[id]/+server.ts @@ -24,7 +24,8 @@ const IngredientSchema = z.object({ 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) + raw_text: z.string().max(500), + section_heading: z.string().max(200).nullable() }); const StepSchema = z.object({ diff --git a/tests/integration/recipe-repository.test.ts b/tests/integration/recipe-repository.test.ts index 6cd0ebd..2821469 100644 --- a/tests/integration/recipe-repository.test.ts +++ b/tests/integration/recipe-repository.test.ts @@ -70,7 +70,8 @@ describe('recipe repository', () => { unit: 'g', name: 'Pancetta', note: null, - raw_text: '200 g Pancetta' + raw_text: '200 g Pancetta', + section_heading: null } ], tags: ['Italienisch'] @@ -118,13 +119,13 @@ describe('recipe repository', () => { baseRecipe({ title: 'Pasta', ingredients: [ - { position: 1, quantity: 200, unit: 'g', name: 'Pancetta', note: null, raw_text: '200 g Pancetta' } + { position: 1, quantity: 200, unit: 'g', name: 'Pancetta', note: null, raw_text: '200 g Pancetta', section_heading: null } ] }) ); replaceIngredients(db, id, [ - { position: 1, quantity: 500, unit: 'g', name: 'Nudeln', note: null, raw_text: '500 g Nudeln' }, - { position: 2, quantity: 2, unit: null, name: 'Eier', note: null, raw_text: '2 Eier' } + { position: 1, quantity: 500, unit: 'g', name: 'Nudeln', note: null, raw_text: '500 g Nudeln', section_heading: null }, + { position: 2, quantity: 2, unit: null, name: 'Eier', note: null, raw_text: '2 Eier', section_heading: null } ]); const loaded = getRecipeById(db, id); expect(loaded?.ingredients.length).toBe(2); @@ -154,4 +155,31 @@ describe('recipe repository', () => { const loaded = getRecipeById(db, id); expect(loaded?.steps.map((s) => s.text)).toEqual(['Erst', 'Dann']); }); + + it('persistiert section_heading und gibt es beim Laden zurueck', () => { + const db = openInMemoryForTest(); + const recipe = baseRecipe({ + title: 'Torte', + ingredients: [ + { position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '200 g Mehl', section_heading: 'Für den Teig' }, + { position: 2, quantity: 100, unit: 'g', name: 'Zucker', note: null, raw_text: '100 g Zucker', section_heading: null }, + { position: 3, quantity: 300, unit: 'g', name: 'Beeren', note: null, raw_text: '300 g Beeren', section_heading: 'Für die Füllung' } + ] + }); + const id = insertRecipe(db, recipe); + const loaded = getRecipeById(db, id); + expect(loaded!.ingredients[0].section_heading).toBe('Für den Teig'); + expect(loaded!.ingredients[1].section_heading).toBeNull(); + expect(loaded!.ingredients[2].section_heading).toBe('Für die Füllung'); + }); + + it('replaceIngredients persistiert section_heading', () => { + const db = openInMemoryForTest(); + const id = insertRecipe(db, baseRecipe({ title: 'X' })); + replaceIngredients(db, id, [ + { position: 1, quantity: null, unit: null, name: 'A', note: null, raw_text: 'A', section_heading: 'Kopf' } + ]); + const loaded = getRecipeById(db, id); + expect(loaded!.ingredients[0].section_heading).toBe('Kopf'); + }); }); diff --git a/tests/integration/search-local.test.ts b/tests/integration/search-local.test.ts index 8b53639..81b0299 100644 --- a/tests/integration/search-local.test.ts +++ b/tests/integration/search-local.test.ts @@ -48,7 +48,7 @@ describe('searchLocal', () => { recipe({ title: 'Pasta', ingredients: [ - { position: 1, quantity: 200, unit: 'g', name: 'Pancetta', note: null, raw_text: '' } + { position: 1, quantity: 200, unit: 'g', name: 'Pancetta', note: null, raw_text: '', section_heading: null } ] }) ); diff --git a/tests/unit/scaler.test.ts b/tests/unit/scaler.test.ts index 5edcef2..472ac31 100644 --- a/tests/unit/scaler.test.ts +++ b/tests/unit/scaler.test.ts @@ -8,7 +8,8 @@ const mk = (q: number | null, unit: string | null, name: string): Ingredient => unit, name, note: null, - raw_text: '' + raw_text: '', + section_heading: null }); describe('roundQuantity', () => {