Recipe Manager: Add schema migrations and seed file updates
This commit is contained in:
parent
b5a2588bc4
commit
18bb0ae069
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 };
|
||||||
Loading…
Reference in New Issue