package postgresql import ( "context" "errors" "fmt" "recipes/internal/domain/models" "recipes/internal/storage" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) type Storage struct { db *pgxpool.Pool } func New(ctx context.Context, user, password, addr, dbname string) (*Storage, error) { const op = "storage.postgresql.New" pool, err := pgxpool.New(ctx, fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", user, password, addr, dbname)) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } return &Storage{db: pool}, nil } // AddRecipe adds recipe to db. func (s *Storage) AddRecipe(ctx context.Context, recipe models.Recipe) error { const op = "storage.postgresql.AddRecipe" // open transaction tx, err := s.db.Begin(ctx) if err != nil { return fmt.Errorf("%s: %w", op, err) } // refactor error handling // rollback transaction on return defer tx.Rollback(ctx) var id uint // insert recipe err = tx.QueryRow( ctx, "insert into recipe (title, description, image, cooking_time, servings, cal) values ($1, $2, $3, $4, $5, $6) returning id", recipe.Title, recipe.Description, recipe.Image, recipe.CookingTime, recipe.ServingsNum, recipe.Calories, ).Scan(&id) if err != nil { return fmt.Errorf("%s: %w", op, err) } // insert ingredients for _, r := range recipe.Ingredients { var ing_id uint err = tx.QueryRow( ctx, "insert into recipe_ingredients_group (recipe_id, title) values ($1, $2) returning id", id, r.Title, ).Scan(&ing_id) if err != nil { return fmt.Errorf("%s: %w", op, err) } for _, i := range r.Ingredients { _, err = tx.Exec( ctx, "insert into recipe_ingredients (recipe_ingredients_group_id, ingredient) values ($1, $2)", ing_id, i, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } } } // insert steps for i, step := range recipe.Recipe_steps { _, err = tx.Exec( ctx, "insert into recipe_steps (recipe_id, step_num, step_text) values ($1, $2, $3)", id, i, step, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } } // insert advices for _, a := range recipe.Advices { _, err = tx.Exec( ctx, "insert into recipe_advices (recipe_id, advice) values ($1, $2)", id, a, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } } // insert categories for _, c := range recipe.Categories { _, err = tx.Exec( ctx, "insert into recipe_categories (recipe_id, category) values ($1, $2)", id, c, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } } // commit transaction if err := tx.Commit(ctx); err != nil { return fmt.Errorf("%s: %w", op, err) } return nil } // RecipeExists returns true if recipe is exists. func (s *Storage) RecipeExists(ctx context.Context, title string) (bool, error) { const op = "storage.postgresql.RecipeExists" var exists bool err := s.db.QueryRow( ctx, "select exists(select id from recipe where title = $1)", title, ).Scan(&exists) if err != nil { return false, fmt.Errorf("%s: %w", op, err) } return exists, nil } // GetRecipes gets recipes by offset and limit. func (s *Storage) GetRecipes(ctx context.Context, offset, limit int) ([]models.Recipe, error) { const op = "storage.postgresql.GetRecipes" rows, err := s.db.Query( ctx, "select id, title, image, cooking_time, cal from recipe order by id desc offset $1 limit $2", offset, limit, ) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } defer rows.Close() var recipes []models.Recipe = make([]models.Recipe, 0, limit) for rows.Next() { var r models.Recipe err = rows.Scan( &r.ID, &r.Title, &r.Image, &r.CookingTime, &r.Calories, ) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } recipes = append(recipes, r) } return recipes, nil } // GetRecipe gets recipe by id. func (s *Storage) GetRecipe(ctx context.Context, r_id uint) (models.Recipe, error) { const op = "storage.postgresql.GetRecipe" var recipe models.Recipe err := s.db.QueryRow( ctx, "select title, description, image, cooking_time, servings, cal from recipe where id = $1", r_id, ).Scan(&recipe) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return models.Recipe{}, storage.ErrRecipeNotFound } return models.Recipe{}, fmt.Errorf("%s: %w", op, err) } err = s.AddRecipeInformation(ctx, &recipe) if err != nil { return models.Recipe{}, fmt.Errorf("%s: %w", op, err) } return recipe, nil } // AddRecipeInformation adds to recipe struct info about ingredients, steps, advices, categories. func (s *Storage) AddRecipeInformation(ctx context.Context, r *models.Recipe) error { const op = "storage.postgresql.AddRecipeInformation" // select ingredients ingredientsg_rows, err := s.db.Query( ctx, "select id, title from recipe_ingredients_group where recipe_id = $1", r.ID, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } defer ingredientsg_rows.Close() for ingredientsg_rows.Next() { var ingredients_group_id uint var recipe_ingredients models.RecipeIngredients err = ingredientsg_rows.Scan( &ingredients_group_id, &recipe_ingredients.Title, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } ingredient_rows, err := s.db.Query( ctx, "select ingredient from recipe_ingredients where recipe_ingredients_group_id = $1", ingredients_group_id, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } defer ingredient_rows.Close() for ingredient_rows.Next() { var ingredient string err = ingredient_rows.Scan( &ingredient, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } recipe_ingredients.Ingredients = append(recipe_ingredients.Ingredients, ingredient) } r.Ingredients = append(r.Ingredients, recipe_ingredients) } // select steps step_rows, err := s.db.Query(ctx, "select step_text from recipe_steps where recipe_id = $1 order by step_num", r.ID) if err != nil { return fmt.Errorf("%s: %w", op, err) } defer step_rows.Close() for step_rows.Next() { var step string err = step_rows.Scan( &step, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } r.Recipe_steps = append(r.Recipe_steps, step) } // select advices advice_rows, err := s.db.Query(ctx, "select advice from recipe_advices where recipe_id = $1", r.ID) if err != nil { return fmt.Errorf("%s: %w", op, err) } defer advice_rows.Close() for advice_rows.Next() { var advice string err = advice_rows.Scan( &advice, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } r.Advices = append(r.Advices, advice) } // select categories category_rows, err := s.db.Query(ctx, "select category from recipe_categories where recipe_id = $1", r.ID) if err != nil { return fmt.Errorf("%s: %w", op, err) } defer category_rows.Close() for category_rows.Next() { var category string err = category_rows.Scan( &category, ) if err != nil { return fmt.Errorf("%s: %w", op, err) } r.Categories = append(r.Categories, category) } return nil } // GetRecipesByCategory gets recipes by category, offset and limit. func (s *Storage) GetRecipesByCategory(ctx context.Context, offset, limit int, category string) ([]models.Recipe, error) { const op = "storage.postgresql.GetRecipesByCategory" rows, err := s.db.Query( ctx, "select r.id, r.title, r.image, r.cooking_time, r.cal from recipe r inner join recipe_categories c on r.id = c.recipe_id where c.category = $1 order by r.id limit $2 offset $3", category, limit, offset, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return nil, storage.ErrCategoryNotFound } return nil, fmt.Errorf("%s: %w", op, err) } defer rows.Close() var recipes []models.Recipe = make([]models.Recipe, 0, limit) for rows.Next() { var r models.Recipe err = rows.Scan( &r.ID, &r.Title, &r.Image, &r.CookingTime, &r.Calories, ) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } err = s.AddRecipeInformation(ctx, &r) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } recipes = append(recipes, r) } return recipes, nil }