366 lines
8.0 KiB
Go
366 lines
8.0 KiB
Go
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) //nolint:errcheck
|
|
|
|
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
|
|
}
|