import type { Database, SqlValue } from 'sql.js'; import type { Recipe, CreateRecipeInput, UpdateRecipeInput, RecipeFilters } from '../types/recipe.js'; /** * RecipeRepository handles all database operations for recipes. */ export class RecipeRepository { constructor(private db: Database) {} /** * Find all recipes with optional filtering and pagination */ findAll(filters: RecipeFilters = {}): Recipe[] { const { search, offset = 0, limit = 50 } = filters; let sql = 'SELECT * FROM recipes'; const params: SqlValue[] = []; if (search) { sql += ' WHERE title LIKE ? OR description LIKE ? OR ingredients LIKE ?'; const searchPattern = `%${search}%`; params.push(searchPattern, searchPattern, searchPattern); } sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; params.push(limit, offset); const result = this.db.exec(sql, params); if (!result.length) return []; return this.rowsToRecipes(result[0]); } /** * Find a recipe by ID */ findById(id: number): Recipe | null { const result = this.db.exec('SELECT * FROM recipes WHERE id = ?', [id]); if (!result.length || !result[0].values.length) return null; const recipes = this.rowsToRecipes(result[0]); return recipes[0] || null; } /** * Create a new recipe */ create(input: CreateRecipeInput): Recipe { const now = Math.floor(Date.now() / 1000); const sql = ` INSERT INTO recipes ( title, description, ingredients, instructions, source_url, notes, servings, prep_time_minutes, cook_time_minutes, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `; this.db.run(sql, [ input.title, input.description || null, JSON.stringify(input.ingredients), JSON.stringify(input.instructions), input.source_url || null, input.notes || null, input.servings || null, input.prep_time_minutes || null, input.cook_time_minutes || null, now, now, ]); // Get the last inserted ID const result = this.db.exec('SELECT last_insert_rowid() as id'); const id = result[0].values[0][0] as number; return this.findById(id)!; } /** * Update an existing recipe */ update(id: number, input: UpdateRecipeInput): Recipe | null { const existing = this.findById(id); if (!existing) return null; const now = Math.floor(Date.now() / 1000); const fields: string[] = []; const params: SqlValue[] = []; // Build dynamic UPDATE query based on provided fields if (input.title !== undefined) { fields.push('title = ?'); params.push(input.title); } if (input.description !== undefined) { fields.push('description = ?'); params.push(input.description); } if (input.ingredients !== undefined) { fields.push('ingredients = ?'); params.push(JSON.stringify(input.ingredients)); } if (input.instructions !== undefined) { fields.push('instructions = ?'); params.push(JSON.stringify(input.instructions)); } if (input.source_url !== undefined) { fields.push('source_url = ?'); params.push(input.source_url); } if (input.notes !== undefined) { fields.push('notes = ?'); params.push(input.notes); } if (input.servings !== undefined) { fields.push('servings = ?'); params.push(input.servings); } if (input.prep_time_minutes !== undefined) { fields.push('prep_time_minutes = ?'); params.push(input.prep_time_minutes); } if (input.cook_time_minutes !== undefined) { fields.push('cook_time_minutes = ?'); params.push(input.cook_time_minutes); } // Always update updated_at fields.push('updated_at = ?'); params.push(now); // Add ID to params for WHERE clause params.push(id); const sql = `UPDATE recipes SET ${fields.join(', ')} WHERE id = ?`; this.db.run(sql, params); return this.findById(id); } /** * Delete a recipe */ delete(id: number): boolean { const existing = this.findById(id); if (!existing) return false; this.db.run('DELETE FROM recipes WHERE id = ?', [id]); return true; } /** * Count total recipes (for pagination) */ count(filters: RecipeFilters = {}): number { const { search } = filters; let sql = 'SELECT COUNT(*) as count FROM recipes'; const params: SqlValue[] = []; if (search) { sql += ' WHERE title LIKE ? OR description LIKE ? OR ingredients LIKE ?'; const searchPattern = `%${search}%`; params.push(searchPattern, searchPattern, searchPattern); } const result = this.db.exec(sql, params); return result[0].values[0][0] as number; } /** * Convert sql.js result rows to Recipe objects */ private rowsToRecipes(result: { columns: string[]; values: SqlValue[][] }): Recipe[] { return result.values.map((row) => { const recipe: Record = {}; result.columns.forEach((col, idx) => { recipe[col] = row[idx]; }); return { id: recipe.id as number, title: recipe.title as string, description: recipe.description as string | null, ingredients: JSON.parse(recipe.ingredients as string) as string[], instructions: JSON.parse(recipe.instructions as string) as string[], source_url: recipe.source_url as string | null, notes: recipe.notes as string | null, servings: recipe.servings as number | null, prep_time_minutes: recipe.prep_time_minutes as number | null, cook_time_minutes: recipe.cook_time_minutes as number | null, created_at: recipe.created_at as number, updated_at: recipe.updated_at as number, last_cooked_at: recipe.last_cooked_at as number | null, }; }); } }