package recipe import ( "context" "errors" "fmt" "github.com/food-ai/backend/internal/locale" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) // Repository handles persistence for recipes and their relational sub-tables. type Repository struct { pool *pgxpool.Pool } // NewRepository creates a new Repository. func NewRepository(pool *pgxpool.Pool) *Repository { return &Repository{pool: pool} } // GetByID returns a recipe with its ingredients and steps. // Text is resolved for the language stored in ctx (English fallback). // Returns nil, nil if not found. func (r *Repository) GetByID(ctx context.Context, id string) (*Recipe, error) { lang := locale.FromContext(ctx) const q = ` SELECT r.id, r.dish_id, r.source, r.difficulty, r.prep_time_min, r.cook_time_min, r.servings, r.calories_per_serving, r.protein_per_serving, r.fat_per_serving, r.carbs_per_serving, r.fiber_per_serving, rt.notes, r.created_at, r.updated_at FROM recipes r LEFT JOIN recipe_translations rt ON rt.recipe_id = r.id AND rt.lang = $2 WHERE r.id = $1` row := r.pool.QueryRow(ctx, q, id, lang) rec, err := scanRecipe(row) if errors.Is(err, pgx.ErrNoRows) { return nil, nil } if err != nil { return nil, fmt.Errorf("get recipe %s: %w", id, err) } if err := r.loadIngredients(ctx, rec, lang); err != nil { return nil, err } if err := r.loadSteps(ctx, rec, lang); err != nil { return nil, err } return rec, nil } // Count returns the total number of recipes. func (r *Repository) Count(ctx context.Context) (int, error) { var n int if err := r.pool.QueryRow(ctx, `SELECT count(*) FROM recipes`).Scan(&n); err != nil { return 0, fmt.Errorf("count recipes: %w", err) } return n, nil } // loadIngredients fills rec.Ingredients from recipe_ingredients. func (r *Repository) loadIngredients(ctx context.Context, rec *Recipe, lang string) error { rows, err := r.pool.Query(ctx, ` SELECT ri.id, ri.ingredient_id, COALESCE(rit.name, ri.name) AS name, ri.amount, ri.unit_code, ri.is_optional, ri.sort_order FROM recipe_ingredients ri LEFT JOIN recipe_ingredient_translations rit ON rit.ri_id = ri.id AND rit.lang = $2 WHERE ri.recipe_id = $1 ORDER BY ri.sort_order`, rec.ID, lang) if err != nil { return fmt.Errorf("load ingredients for recipe %s: %w", rec.ID, err) } defer rows.Close() for rows.Next() { var ing RecipeIngredient if err := rows.Scan( &ing.ID, &ing.IngredientID, &ing.Name, &ing.Amount, &ing.UnitCode, &ing.IsOptional, &ing.SortOrder, ); err != nil { return fmt.Errorf("scan ingredient: %w", err) } rec.Ingredients = append(rec.Ingredients, ing) } return rows.Err() } // loadSteps fills rec.Steps from recipe_steps. func (r *Repository) loadSteps(ctx context.Context, rec *Recipe, lang string) error { rows, err := r.pool.Query(ctx, ` SELECT rs.id, rs.step_number, COALESCE(rst.description, rs.description) AS description, rs.timer_seconds, rs.image_url FROM recipe_steps rs LEFT JOIN recipe_step_translations rst ON rst.step_id = rs.id AND rst.lang = $2 WHERE rs.recipe_id = $1 ORDER BY rs.step_number`, rec.ID, lang) if err != nil { return fmt.Errorf("load steps for recipe %s: %w", rec.ID, err) } defer rows.Close() for rows.Next() { var s RecipeStep if err := rows.Scan( &s.ID, &s.StepNumber, &s.Description, &s.TimerSeconds, &s.ImageURL, ); err != nil { return fmt.Errorf("scan step: %w", err) } rec.Steps = append(rec.Steps, s) } return rows.Err() } // --- scan helpers --- func scanRecipe(row pgx.Row) (*Recipe, error) { var rec Recipe err := row.Scan( &rec.ID, &rec.DishID, &rec.Source, &rec.Difficulty, &rec.PrepTimeMin, &rec.CookTimeMin, &rec.Servings, &rec.CaloriesPerServing, &rec.ProteinPerServing, &rec.FatPerServing, &rec.CarbsPerServing, &rec.FiberPerServing, &rec.Notes, &rec.CreatedAt, &rec.UpdatedAt, ) if err != nil { return nil, err } rec.Ingredients = []RecipeIngredient{} rec.Steps = []RecipeStep{} return &rec, nil }