import type Database from 'better-sqlite3'; import type { Ingredient, Recipe, Step } from '$lib/types'; type RecipeRow = { id: number; title: string; description: string | null; source_url: string | null; source_domain: string | null; image_path: string | null; servings_default: number | null; servings_unit: string | null; prep_time_min: number | null; cook_time_min: number | null; total_time_min: number | null; cuisine: string | null; category: string | null; }; function ensureTagIds(db: Database.Database, names: string[]): number[] { const insert = db.prepare('INSERT OR IGNORE INTO tag(name) VALUES (?)'); const select = db.prepare('SELECT id FROM tag WHERE name = ?'); const ids: number[] = []; for (const name of names) { const trimmed = name.trim(); if (!trimmed) continue; insert.run(trimmed); const row = select.get(trimmed) as { id: number }; ids.push(row.id); } return ids; } function refreshFts(db: Database.Database, recipeId: number): void { // Trigger the AFTER UPDATE trigger which rebuilds the FTS row with current ingredients + tags. db.prepare('UPDATE recipe SET title = title WHERE id = ?').run(recipeId); } export function insertRecipe(db: Database.Database, recipe: Recipe): number { const tx = db.transaction((): number => { const info = db .prepare( `INSERT INTO recipe (title, description, source_url, source_domain, image_path, servings_default, servings_unit, prep_time_min, cook_time_min, total_time_min, cuisine, category) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) .run( recipe.title, recipe.description, recipe.source_url, recipe.source_domain, recipe.image_path, recipe.servings_default, recipe.servings_unit, recipe.prep_time_min, recipe.cook_time_min, recipe.total_time_min, recipe.cuisine, recipe.category ); const id = Number(info.lastInsertRowid); const insIng = db.prepare( `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, ing.section_heading); } const insStep = db.prepare( 'INSERT INTO step(recipe_id, position, text) VALUES (?, ?, ?)' ); for (const step of recipe.steps) { insStep.run(id, step.position, step.text); } const tagIds = ensureTagIds(db, recipe.tags); const linkTag = db.prepare( 'INSERT OR IGNORE INTO recipe_tag(recipe_id, tag_id) VALUES (?, ?)' ); for (const tid of tagIds) linkTag.run(id, tid); refreshFts(db, id); return id; }); return tx(); } export function getRecipeById(db: Database.Database, id: number): Recipe | null { const row = db .prepare( `SELECT id, title, description, source_url, source_domain, image_path, servings_default, servings_unit, prep_time_min, cook_time_min, total_time_min, cuisine, category FROM recipe WHERE id = ?` ) .get(id) as RecipeRow | undefined; if (!row) return null; const ingredients = db .prepare( `SELECT position, quantity, unit, name, note, raw_text, section_heading FROM ingredient WHERE recipe_id = ? ORDER BY position` ) .all(id) as Ingredient[]; const steps = db .prepare('SELECT position, text FROM step WHERE recipe_id = ? ORDER BY position') .all(id) as Step[]; const tagRows = db .prepare( `SELECT t.name FROM tag t JOIN recipe_tag rt ON rt.tag_id = t.id WHERE rt.recipe_id = ? ORDER BY t.name` ) .all(id) as { name: string }[]; return { id: row.id, title: row.title, description: row.description, source_url: row.source_url, source_domain: row.source_domain, image_path: row.image_path, servings_default: row.servings_default, servings_unit: row.servings_unit, prep_time_min: row.prep_time_min, cook_time_min: row.cook_time_min, total_time_min: row.total_time_min, cuisine: row.cuisine, category: row.category, ingredients, steps, tags: tagRows.map((t) => t.name) }; } export function getRecipeIdBySourceUrl( db: Database.Database, url: string ): number | null { const row = db.prepare('SELECT id FROM recipe WHERE source_url = ?').get(url) as | { id: number } | undefined; return row?.id ?? null; } export function deleteRecipe(db: Database.Database, id: number): void { db.prepare('DELETE FROM recipe WHERE id = ?').run(id); } export type RecipeMetaPatch = { title?: string; description?: string | null; servings_default?: number | null; servings_unit?: string | null; prep_time_min?: number | null; cook_time_min?: number | null; total_time_min?: number | null; cuisine?: string | null; category?: string | null; }; export function updateRecipeMeta( db: Database.Database, id: number, patch: RecipeMetaPatch ): void { const fields: string[] = []; const values: unknown[] = []; for (const key of [ 'title', 'description', 'servings_default', 'servings_unit', 'prep_time_min', 'cook_time_min', 'total_time_min', 'cuisine', 'category' ] as const) { if (patch[key] !== undefined) { fields.push(`${key} = ?`); values.push(patch[key]); } } if (fields.length === 0) return; fields.push('updated_at = CURRENT_TIMESTAMP'); db.prepare(`UPDATE recipe SET ${fields.join(', ')} WHERE id = ?`).run(...values, id); } export function updateImagePath( db: Database.Database, id: number, filename: string | null ): void { db.prepare('UPDATE recipe SET image_path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run( filename, id ); } export function replaceIngredients( db: Database.Database, recipeId: number, ingredients: Ingredient[] ): void { 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, section_heading) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` ); for (const ing of ingredients) { ins.run(recipeId, ing.position, ing.quantity, ing.unit, ing.name, ing.note, ing.raw_text, ing.section_heading); } refreshFts(db, recipeId); }); tx(); } export function replaceSteps( db: Database.Database, recipeId: number, steps: Step[] ): void { const tx = db.transaction(() => { db.prepare('DELETE FROM step WHERE recipe_id = ?').run(recipeId); const ins = db.prepare( 'INSERT INTO step(recipe_id, position, text) VALUES (?, ?, ?)' ); for (const step of steps) { ins.run(recipeId, step.position, step.text); } }); tx(); }