From 18bb0ae06911011ccaab12d8735575e24814b235 Mon Sep 17 00:00:00 2001 From: Paul Huliganga Date: Sat, 28 Mar 2026 19:59:18 -0400 Subject: [PATCH] Recipe Manager: Add schema migrations and seed file updates --- src/backend/db/schemaMigrations.js | 20 ++ src/backend/db/schemaMigrations.ts | 21 ++ src/backend/db/seed.ts | 497 +++++++++++++++++++++++++++++ 3 files changed, 538 insertions(+) create mode 100644 src/backend/db/schemaMigrations.js create mode 100644 src/backend/db/schemaMigrations.ts create mode 100644 src/backend/db/seed.ts diff --git a/src/backend/db/schemaMigrations.js b/src/backend/db/schemaMigrations.js new file mode 100644 index 0000000..3b3b0f9 --- /dev/null +++ b/src/backend/db/schemaMigrations.js @@ -0,0 +1,20 @@ +function hasColumn(db, tableName, columnName) { + const pragma = db.exec(`PRAGMA table_info(${tableName})`); + if (!pragma.length) return false; + return pragma[0].values.some((row) => row[1] === columnName); +} + +/** + * Idempotent runtime migrations for existing DB files. + * @param {import('sql.js').Database} db + */ +export function applyRuntimeMigrations(db) { + const hasRecipesTable = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='recipes'"); + if (!hasRecipesTable.length || !hasRecipesTable[0].values.length) { + return; + } + + if (!hasColumn(db, 'recipes', 'image_url')) { + db.exec('ALTER TABLE recipes ADD COLUMN image_url TEXT'); + } +} diff --git a/src/backend/db/schemaMigrations.ts b/src/backend/db/schemaMigrations.ts new file mode 100644 index 0000000..b206b9c --- /dev/null +++ b/src/backend/db/schemaMigrations.ts @@ -0,0 +1,21 @@ +import type { Database } from 'sql.js'; + +function hasColumn(db: Database, tableName: string, columnName: string): boolean { + const pragma = db.exec(`PRAGMA table_info(${tableName})`); + if (!pragma.length) return false; + return pragma[0].values.some((row) => row[1] === columnName); +} + +/** + * Idempotent runtime migrations for existing DB files. + */ +export function applyRuntimeMigrations(db: Database): void { + const hasRecipesTable = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='recipes'"); + if (!hasRecipesTable.length || !hasRecipesTable[0].values.length) { + return; + } + + if (!hasColumn(db, 'recipes', 'image_url')) { + db.exec('ALTER TABLE recipes ADD COLUMN image_url TEXT'); + } +} diff --git a/src/backend/db/seed.ts b/src/backend/db/seed.ts new file mode 100644 index 0000000..50eccda --- /dev/null +++ b/src/backend/db/seed.ts @@ -0,0 +1,497 @@ +import initSqlJs from 'sql.js'; +import fs from 'fs'; +import path from 'path'; +import { applyRuntimeMigrations } from './schemaMigrations.js'; + +interface SeedIngredient { + quantity?: string; + unit?: string; + item: string; + notes?: string; +} + +interface SeedStep { + instruction: string; +} + +interface SeedRecipe { + title: string; + description: string; + servings: number; + prep_time_minutes: number; + cook_time_minutes: number; + source_url: string; + image_url: string; + tags: string[]; + ingredients: SeedIngredient[]; + steps: SeedStep[]; +} + +const DATA_DIR = path.resolve(process.cwd(), 'data'); +const DB_PATH = process.env.DATABASE_PATH || path.join(DATA_DIR, 'recipes.db'); +const SCHEMA_PATH = path.resolve(process.cwd(), 'src/backend/db/schema.sql'); + +const seedRecipes: SeedRecipe[] = [ + { + title: 'Spaghetti Bolognese', + description: 'Rich tomato and beef sauce simmered and served over spaghetti.', + servings: 4, + prep_time_minutes: 15, + cook_time_minutes: 50, + source_url: 'https://www.bbcgoodfood.com/recipes/best-spaghetti-bolognese-recipe', + image_url: 'https://images.unsplash.com/photo-1622973536968-3ead9e780960?auto=format&fit=crop&w=1600&q=80', + tags: ['Dinner', 'Pasta', 'Beef', 'Family Favorite'], + ingredients: [ + { quantity: '1', unit: 'lb', item: 'ground beef' }, + { quantity: '1', unit: 'tbsp', item: 'olive oil' }, + { quantity: '1', item: 'yellow onion', notes: 'diced' }, + { quantity: '2', unit: 'cloves', item: 'garlic', notes: 'minced' }, + { quantity: '28', unit: 'oz', item: 'crushed tomatoes' }, + { quantity: '2', unit: 'tbsp', item: 'tomato paste' }, + { quantity: '1', unit: 'tsp', item: 'dried oregano' }, + { quantity: '12', unit: 'oz', item: 'spaghetti' }, + { quantity: 'to taste', item: 'salt and black pepper' } + ], + steps: [ + { instruction: 'Heat olive oil in a large pot and sauté onion for 4 to 5 minutes until softened.' }, + { instruction: 'Add garlic and cook 30 seconds, then add beef and cook until browned.' }, + { instruction: 'Stir in tomato paste, crushed tomatoes, oregano, salt, and pepper.' }, + { instruction: 'Simmer uncovered for 35 to 40 minutes, stirring occasionally.' }, + { instruction: 'Cook spaghetti in salted boiling water until al dente, then drain.' }, + { instruction: 'Serve sauce over spaghetti.' } + ] + }, + { + title: 'Chicken Tacos', + description: 'Quick skillet chicken tacos with lime and warm tortillas.', + servings: 4, + prep_time_minutes: 15, + cook_time_minutes: 20, + source_url: 'https://www.allrecipes.com/recipe/70935/taqueria-style-tacos-carne-asada/', + image_url: 'https://images.unsplash.com/photo-1613514785940-daed07799d9b?auto=format&fit=crop&w=1600&q=80', + tags: ['Dinner', 'Mexican', 'Chicken', 'Quick'], + ingredients: [ + { quantity: '1.5', unit: 'lb', item: 'boneless chicken thighs', notes: 'sliced' }, + { quantity: '2', unit: 'tbsp', item: 'olive oil' }, + { quantity: '1', unit: 'tbsp', item: 'chili powder' }, + { quantity: '1', unit: 'tsp', item: 'ground cumin' }, + { quantity: '1/2', unit: 'tsp', item: 'garlic powder' }, + { quantity: '8', item: 'corn tortillas' }, + { quantity: '1', item: 'lime', notes: 'cut in wedges' }, + { quantity: '1', unit: 'cup', item: 'shredded lettuce' }, + { quantity: '1/2', unit: 'cup', item: 'salsa' } + ], + steps: [ + { instruction: 'Toss chicken with oil, chili powder, cumin, garlic powder, and salt.' }, + { instruction: 'Cook chicken in a hot skillet for 8 to 10 minutes until cooked through.' }, + { instruction: 'Warm tortillas in a dry pan or microwave.' }, + { instruction: 'Fill tortillas with chicken, lettuce, and salsa.' }, + { instruction: 'Finish with lime juice and serve.' } + ] + }, + { + title: 'Classic Roast Chicken and Potatoes', + description: 'Simple roast chicken with crispy potatoes and pan juices.', + servings: 4, + prep_time_minutes: 20, + cook_time_minutes: 80, + source_url: 'https://www.seriouseats.com/butterflied-roasted-chicken-with-quick-jus-recipe', + image_url: 'https://images.unsplash.com/photo-1518492104633-130d0cc84637?auto=format&fit=crop&w=1600&q=80', + tags: ['Dinner', 'Chicken', 'Roast', 'Family Favorite'], + ingredients: [ + { quantity: '1', item: 'whole chicken', notes: 'about 4 pounds' }, + { quantity: '2', unit: 'lb', item: 'Yukon Gold potatoes', notes: 'cut into chunks' }, + { quantity: '2', unit: 'tbsp', item: 'olive oil' }, + { quantity: '1', unit: 'tsp', item: 'kosher salt' }, + { quantity: '1/2', unit: 'tsp', item: 'black pepper' }, + { quantity: '1', unit: 'tsp', item: 'dried thyme' }, + { quantity: '1', item: 'lemon', notes: 'halved' } + ], + steps: [ + { instruction: 'Preheat oven to 425°F (220°C).' }, + { instruction: 'Pat chicken dry and season inside and out with salt, pepper, and thyme.' }, + { instruction: 'Toss potatoes with olive oil and a pinch of salt, then spread in a roasting pan.' }, + { instruction: 'Place chicken over potatoes and add lemon halves to pan.' }, + { instruction: 'Roast for 70 to 80 minutes until chicken reaches 165°F in the thickest part.' }, + { instruction: 'Rest 10 minutes before carving and serving with potatoes.' } + ] + }, + { + title: 'Fudgy Brownies', + description: 'Dense chocolate brownies with crackly tops.', + servings: 12, + prep_time_minutes: 15, + cook_time_minutes: 30, + source_url: 'https://sallysbakingaddiction.com/seriously-fudgy-homemade-brownies/', + image_url: 'https://images.unsplash.com/photo-1606313564200-e75d5e30476f?auto=format&fit=crop&w=1600&q=80', + tags: ['Dessert', 'Baking', 'Chocolate'], + ingredients: [ + { quantity: '10', unit: 'tbsp', item: 'unsalted butter' }, + { quantity: '1 1/4', unit: 'cups', item: 'granulated sugar' }, + { quantity: '3/4', unit: 'cup', item: 'cocoa powder' }, + { quantity: '2', item: 'large eggs' }, + { quantity: '1', unit: 'tsp', item: 'vanilla extract' }, + { quantity: '1/2', unit: 'cup', item: 'all-purpose flour' }, + { quantity: '1/4', unit: 'tsp', item: 'salt' }, + { quantity: '1/2', unit: 'cup', item: 'chocolate chips' } + ], + steps: [ + { instruction: 'Preheat oven to 350°F (175°C) and line an 8x8 pan with parchment.' }, + { instruction: 'Melt butter, whisk in sugar and cocoa until smooth.' }, + { instruction: 'Whisk in eggs one at a time, then vanilla.' }, + { instruction: 'Fold in flour, salt, and chocolate chips just until combined.' }, + { instruction: 'Spread batter in pan and bake 25 to 30 minutes.' }, + { instruction: 'Cool completely before slicing.' } + ] + }, + { + title: 'Fluffy Buttermilk Pancakes', + description: 'Classic breakfast pancakes, light and tender.', + servings: 4, + prep_time_minutes: 10, + cook_time_minutes: 15, + source_url: 'https://www.kingarthurbaking.com/recipes/buttermilk-pancakes-recipe', + image_url: 'https://images.unsplash.com/photo-1528207776546-365bb710ee93?auto=format&fit=crop&w=1600&q=80', + tags: ['Breakfast', 'Quick', 'Family Favorite'], + ingredients: [ + { quantity: '2', unit: 'cups', item: 'all-purpose flour' }, + { quantity: '2', unit: 'tbsp', item: 'sugar' }, + { quantity: '2', unit: 'tsp', item: 'baking powder' }, + { quantity: '1/2', unit: 'tsp', item: 'baking soda' }, + { quantity: '1/2', unit: 'tsp', item: 'salt' }, + { quantity: '2', unit: 'cups', item: 'buttermilk' }, + { quantity: '2', item: 'large eggs' }, + { quantity: '3', unit: 'tbsp', item: 'melted butter' } + ], + steps: [ + { instruction: 'Whisk dry ingredients in a large bowl.' }, + { instruction: 'Whisk buttermilk, eggs, and melted butter in a second bowl.' }, + { instruction: 'Pour wet into dry and stir gently until just combined.' }, + { instruction: 'Cook 1/4-cup portions on a greased skillet over medium heat until bubbles form.' }, + { instruction: 'Flip and cook until golden on both sides.' } + ] + }, + { + title: 'Classic Beef Chili', + description: 'Hearty chili with beef, beans, tomatoes, and warm spices.', + servings: 6, + prep_time_minutes: 20, + cook_time_minutes: 60, + source_url: 'https://www.simplyrecipes.com/recipes/beef_chili/', + image_url: 'https://images.unsplash.com/photo-1547592166-23ac45744acd?auto=format&fit=crop&w=1600&q=80', + tags: ['Dinner', 'Beef', 'One Pot', 'Meal Prep'], + ingredients: [ + { quantity: '1.5', unit: 'lb', item: 'ground beef' }, + { quantity: '1', item: 'onion', notes: 'diced' }, + { quantity: '1', item: 'bell pepper', notes: 'diced' }, + { quantity: '3', unit: 'cloves', item: 'garlic', notes: 'minced' }, + { quantity: '2', unit: 'tbsp', item: 'chili powder' }, + { quantity: '1', unit: 'tsp', item: 'ground cumin' }, + { quantity: '28', unit: 'oz', item: 'diced tomatoes' }, + { quantity: '15', unit: 'oz', item: 'kidney beans', notes: 'drained' }, + { quantity: '1', unit: 'cup', item: 'beef broth' } + ], + steps: [ + { instruction: 'Brown beef in a large pot and drain excess fat if needed.' }, + { instruction: 'Add onion, pepper, and garlic; cook until softened.' }, + { instruction: 'Stir in chili powder and cumin and cook 1 minute.' }, + { instruction: 'Add tomatoes, beans, broth, and salt; bring to simmer.' }, + { instruction: 'Simmer uncovered 45 minutes until thickened.' } + ] + }, + { + title: 'Chicken and Vegetable Stir-Fry', + description: 'Fast weeknight stir-fry with savory garlic-ginger sauce.', + servings: 4, + prep_time_minutes: 15, + cook_time_minutes: 15, + source_url: 'https://www.recipetineats.com/chicken-stir-fry/', + image_url: 'https://images.unsplash.com/photo-1512058564366-18510be2db19?auto=format&fit=crop&w=1600&q=80', + tags: ['Dinner', 'Chicken', 'Quick', 'Asian-Inspired'], + ingredients: [ + { quantity: '1', unit: 'lb', item: 'chicken breast', notes: 'thinly sliced' }, + { quantity: '2', unit: 'cups', item: 'broccoli florets' }, + { quantity: '1', item: 'red bell pepper', notes: 'sliced' }, + { quantity: '1', unit: 'cup', item: 'snap peas' }, + { quantity: '2', unit: 'tbsp', item: 'soy sauce' }, + { quantity: '1', unit: 'tbsp', item: 'oyster sauce' }, + { quantity: '1', unit: 'tbsp', item: 'sesame oil' }, + { quantity: '2', unit: 'cloves', item: 'garlic', notes: 'minced' }, + { quantity: '1', unit: 'tsp', item: 'fresh ginger', notes: 'grated' } + ], + steps: [ + { instruction: 'Mix soy sauce, oyster sauce, and sesame oil in a small bowl.' }, + { instruction: 'Heat a wok or large skillet over high heat and cook chicken until just done.' }, + { instruction: 'Add vegetables, garlic, and ginger; stir-fry 3 to 4 minutes.' }, + { instruction: 'Pour in sauce and cook 1 to 2 minutes until glossy.' }, + { instruction: 'Serve hot with rice.' } + ] + }, + { + title: 'Tomato Basil Soup', + description: 'Creamy tomato soup finished with fresh basil.', + servings: 4, + prep_time_minutes: 10, + cook_time_minutes: 30, + source_url: 'https://www.loveandlemons.com/tomato-basil-soup-recipe/', + image_url: 'https://images.unsplash.com/photo-1547592180-85f173990554?auto=format&fit=crop&w=1600&q=80', + tags: ['Soup', 'Vegetarian', 'Lunch', 'One Pot'], + ingredients: [ + { quantity: '2', unit: 'tbsp', item: 'olive oil' }, + { quantity: '1', item: 'yellow onion', notes: 'chopped' }, + { quantity: '2', unit: 'cloves', item: 'garlic', notes: 'minced' }, + { quantity: '28', unit: 'oz', item: 'crushed tomatoes' }, + { quantity: '2', unit: 'cups', item: 'vegetable broth' }, + { quantity: '1/3', unit: 'cup', item: 'heavy cream' }, + { quantity: '1/4', unit: 'cup', item: 'fresh basil', notes: 'chopped' }, + { quantity: 'to taste', item: 'salt and black pepper' } + ], + steps: [ + { instruction: 'Sauté onion in olive oil until softened, about 5 minutes.' }, + { instruction: 'Add garlic and cook 30 seconds.' }, + { instruction: 'Add tomatoes and broth, then simmer 20 minutes.' }, + { instruction: 'Blend soup until smooth using an immersion blender.' }, + { instruction: 'Stir in cream and basil; season to taste and serve.' } + ] + }, + { + title: 'Creamy Baked Mac and Cheese', + description: 'Comfort-food macaroni with cheddar and a crispy top.', + servings: 6, + prep_time_minutes: 20, + cook_time_minutes: 30, + source_url: 'https://www.thechunkychef.com/family-favorite-baked-mac-and-cheese/', + image_url: 'https://images.unsplash.com/photo-1543339308-43e59d6b73a6?auto=format&fit=crop&w=1600&q=80', + tags: ['Dinner', 'Pasta', 'Vegetarian', 'Family Favorite'], + ingredients: [ + { quantity: '1', unit: 'lb', item: 'elbow macaroni' }, + { quantity: '4', unit: 'tbsp', item: 'butter' }, + { quantity: '1/4', unit: 'cup', item: 'all-purpose flour' }, + { quantity: '3', unit: 'cups', item: 'milk' }, + { quantity: '2 1/2', unit: 'cups', item: 'sharp cheddar', notes: 'shredded' }, + { quantity: '1/2', unit: 'cup', item: 'parmesan', notes: 'grated' }, + { quantity: '1/2', unit: 'cup', item: 'breadcrumbs' }, + { quantity: 'to taste', item: 'salt and black pepper' } + ], + steps: [ + { instruction: 'Preheat oven to 375°F (190°C). Cook macaroni until just al dente.' }, + { instruction: 'Make a roux with butter and flour in a saucepan.' }, + { instruction: 'Whisk in milk and cook until thickened.' }, + { instruction: 'Stir in cheddar and parmesan until melted; season with salt and pepper.' }, + { instruction: 'Combine sauce and macaroni, transfer to baking dish, top with breadcrumbs.' }, + { instruction: 'Bake 20 minutes until bubbling and golden.' } + ] + }, + { + title: 'Chicken Caesar Salad', + description: 'Romaine, grilled chicken, croutons, and parmesan with Caesar dressing.', + servings: 4, + prep_time_minutes: 15, + cook_time_minutes: 10, + source_url: 'https://www.bonappetit.com/recipe/classic-caesar-salad', + image_url: 'https://images.unsplash.com/photo-1546793665-c74683f339c1?auto=format&fit=crop&w=1600&q=80', + tags: ['Lunch', 'Salad', 'Chicken', 'Quick'], + ingredients: [ + { quantity: '2', item: 'chicken breasts' }, + { quantity: '2', unit: 'heads', item: 'romaine lettuce', notes: 'chopped' }, + { quantity: '1', unit: 'cup', item: 'croutons' }, + { quantity: '1/2', unit: 'cup', item: 'parmesan', notes: 'shaved or grated' }, + { quantity: '1/2', unit: 'cup', item: 'Caesar dressing' }, + { quantity: '1', unit: 'tbsp', item: 'olive oil' }, + { quantity: 'to taste', item: 'salt and black pepper' } + ], + steps: [ + { instruction: 'Season chicken with salt and pepper, then cook in olive oil until done.' }, + { instruction: 'Rest chicken for 5 minutes and slice.' }, + { instruction: 'Toss romaine with Caesar dressing.' }, + { instruction: 'Top salad with sliced chicken, croutons, and parmesan.' } + ] + }, + { + title: 'Vegetable Fried Rice', + description: 'Weeknight fried rice with mixed vegetables and scrambled egg.', + servings: 4, + prep_time_minutes: 10, + cook_time_minutes: 15, + source_url: 'https://www.seriouseats.com/easy-vegetable-fried-rice-recipe', + image_url: 'https://images.unsplash.com/photo-1603133872878-684f208fb84b?auto=format&fit=crop&w=1600&q=80', + tags: ['Dinner', 'Quick', 'Vegetarian', 'Asian-Inspired'], + ingredients: [ + { quantity: '4', unit: 'cups', item: 'cooked jasmine rice', notes: 'day-old preferred' }, + { quantity: '2', item: 'eggs', notes: 'beaten' }, + { quantity: '1', unit: 'cup', item: 'frozen peas and carrots' }, + { quantity: '3', item: 'green onions', notes: 'sliced' }, + { quantity: '2', unit: 'tbsp', item: 'soy sauce' }, + { quantity: '1', unit: 'tbsp', item: 'sesame oil' }, + { quantity: '1', unit: 'tbsp', item: 'neutral oil' }, + { quantity: '2', unit: 'cloves', item: 'garlic', notes: 'minced' } + ], + steps: [ + { instruction: 'Heat neutral oil in a large skillet or wok over medium-high heat.' }, + { instruction: 'Scramble eggs quickly, then transfer to a plate.' }, + { instruction: 'Cook vegetables and garlic for 2 minutes.' }, + { instruction: 'Add rice and stir-fry until hot and lightly crisped.' }, + { instruction: 'Return eggs, then add soy sauce, sesame oil, and green onions.' }, + { instruction: 'Toss to combine and serve.' } + ] + }, + { + title: 'Chewy Chocolate Chip Cookies', + description: 'Classic bakery-style cookies with crisp edges and chewy centers.', + servings: 24, + prep_time_minutes: 15, + cook_time_minutes: 12, + source_url: 'https://joyfoodsunshine.com/the-most-amazing-chocolate-chip-cookies/', + image_url: 'https://images.unsplash.com/photo-1499636136210-6f4ee915583e?auto=format&fit=crop&w=1600&q=80', + tags: ['Dessert', 'Baking', 'Family Favorite'], + ingredients: [ + { quantity: '1', unit: 'cup', item: 'unsalted butter', notes: 'softened' }, + { quantity: '3/4', unit: 'cup', item: 'brown sugar' }, + { quantity: '1/2', unit: 'cup', item: 'granulated sugar' }, + { quantity: '2', item: 'large eggs' }, + { quantity: '2', unit: 'tsp', item: 'vanilla extract' }, + { quantity: '2 1/4', unit: 'cups', item: 'all-purpose flour' }, + { quantity: '1', unit: 'tsp', item: 'baking soda' }, + { quantity: '1', unit: 'tsp', item: 'salt' }, + { quantity: '2', unit: 'cups', item: 'chocolate chips' } + ], + steps: [ + { instruction: 'Preheat oven to 375°F (190°C) and line baking sheets.' }, + { instruction: 'Cream butter, brown sugar, and granulated sugar until fluffy.' }, + { instruction: 'Beat in eggs and vanilla.' }, + { instruction: 'Mix in flour, baking soda, and salt until just combined.' }, + { instruction: 'Fold in chocolate chips.' }, + { instruction: 'Scoop dough onto baking sheets and bake 10 to 12 minutes.' } + ] + } +]; + +function ensureDataDir() { + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } +} + +function scalar(db: any, sql: string, params: any[] = []): any { + const result = db.exec(sql, params); + if (!result.length || !result[0].values.length) return null; + return result[0].values[0][0]; +} + +async function seedDatabase() { + ensureDataDir(); + + const SQL = await initSqlJs(); + const db = fs.existsSync(DB_PATH) + ? new SQL.Database(fs.readFileSync(DB_PATH)) + : new SQL.Database(); + + // Ensure schema exists for empty/new DB files. + const hasRecipesTable = scalar(db, "SELECT name FROM sqlite_master WHERE type='table' AND name='recipes'"); + if (!hasRecipesTable) { + const schema = fs.readFileSync(SCHEMA_PATH, 'utf8'); + db.exec(schema); + } + applyRuntimeMigrations(db); + + let createdCount = 0; + let updatedCount = 0; + + const ensureTagId = (name: string): number => { + const existingId = scalar(db, 'SELECT id FROM tags WHERE name = ? LIMIT 1', [name]); + if (existingId) return existingId as number; + db.run('INSERT INTO tags (name) VALUES (?)', [name]); + return scalar(db, 'SELECT last_insert_rowid()') as number; + }; + + for (const recipe of seedRecipes) { + const now = Math.floor(Date.now() / 1000); + const existingId = scalar(db, 'SELECT id FROM recipes WHERE title = ? LIMIT 1', [recipe.title]) as number | null; + + let recipeId: number; + + if (existingId) { + recipeId = existingId; + db.run( + `UPDATE recipes + SET description = ?, servings = ?, prep_time_minutes = ?, cook_time_minutes = ?, source_url = ?, image_url = ?, updated_at = ? + WHERE id = ?`, + [ + recipe.description, + recipe.servings, + recipe.prep_time_minutes, + recipe.cook_time_minutes, + recipe.source_url, + recipe.image_url, + now, + recipeId + ] + ); + + db.run('DELETE FROM ingredients WHERE recipe_id = ?', [recipeId]); + db.run('DELETE FROM steps WHERE recipe_id = ?', [recipeId]); + db.run('DELETE FROM recipe_tags WHERE recipe_id = ?', [recipeId]); + + updatedCount += 1; + } else { + db.run( + `INSERT INTO recipes (title, description, servings, prep_time_minutes, cook_time_minutes, source_url, image_url, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + recipe.title, + recipe.description, + recipe.servings, + recipe.prep_time_minutes, + recipe.cook_time_minutes, + recipe.source_url, + recipe.image_url, + now, + now + ] + ); + recipeId = scalar(db, 'SELECT last_insert_rowid()') as number; + createdCount += 1; + } + + recipe.ingredients.forEach((ingredient, idx) => { + db.run( + 'INSERT INTO ingredients (recipe_id, position, quantity, unit, item, notes) VALUES (?, ?, ?, ?, ?, ?)', + [ + recipeId, + idx, + ingredient.quantity ?? null, + ingredient.unit ?? null, + ingredient.item, + ingredient.notes ?? null + ] + ); + }); + + recipe.steps.forEach((step, idx) => { + db.run( + 'INSERT INTO steps (recipe_id, position, instruction) VALUES (?, ?, ?)', + [recipeId, idx, step.instruction] + ); + }); + + recipe.tags.forEach((tagName) => { + const tagId = ensureTagId(tagName); + db.run('INSERT INTO recipe_tags (recipe_id, tag_id) VALUES (?, ?)', [recipeId, tagId]); + }); + } + + const data = db.export(); + fs.writeFileSync(DB_PATH, Buffer.from(data)); + db.close(); + + console.log(`Seed complete. Created ${createdCount}, updated ${updatedCount}, total seed recipes ${seedRecipes.length}.`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + seedDatabase().catch((error) => { + console.error('Seed failed:', error); + process.exit(1); + }); +} + +export { seedDatabase, seedRecipes };