From 6e28ebfe80ff7f084478e1f724382c0e8e8d641a Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Sat, 7 Feb 2026 11:17:00 -0700 Subject: [PATCH] (FEAT): Orm work, not complete, but editing needs to be merged. --- internal/app/service/recipe_service.go | 2 +- internal/domain/recipe/repository.go | 2 +- .../database/repository/recipe_repository.go | 339 +++++++++--------- web/src/pages/Recipe.tsx | 8 +- 4 files changed, 184 insertions(+), 167 deletions(-) diff --git a/internal/app/service/recipe_service.go b/internal/app/service/recipe_service.go index e455bf4..d02c1b2 100644 --- a/internal/app/service/recipe_service.go +++ b/internal/app/service/recipe_service.go @@ -131,7 +131,7 @@ func (s *RecipeService) DeleteRecipe(userId, recipeId int) error { return fmt.Errorf("User id does not match. Do you own the target recipe?") } - return s.recipeRepository.DeleteRecipe(recipeId) + return s.recipeRepository.DeleteRecipe(recipeId, userId) } // GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore, diff --git a/internal/domain/recipe/repository.go b/internal/domain/recipe/repository.go index 9d4d305..55a2dc9 100644 --- a/internal/domain/recipe/repository.go +++ b/internal/domain/recipe/repository.go @@ -3,7 +3,7 @@ package domain type RecipeRepository interface { CreateRecipe(recipe *Recipe) error EditRecipe(recipe *Recipe, userId int) error - DeleteRecipe(recipeId int) error + DeleteRecipe(recipeId, userId int) error GetRecipe(id int, userId *int) (*Recipe, error) GetRecipes(ids []int, userId *int) ([]Recipe, error) SearchRecipes(filters SearchFilters, userId *int, favorites bool) ([]int, error) diff --git a/internal/infrastructure/database/repository/recipe_repository.go b/internal/infrastructure/database/repository/recipe_repository.go index afc86b9..7e5cc76 100644 --- a/internal/infrastructure/database/repository/recipe_repository.go +++ b/internal/infrastructure/database/repository/recipe_repository.go @@ -3,7 +3,6 @@ package repository import ( "database/sql" "encoding/json" - "errors" "fmt" "strings" "time" @@ -34,26 +33,7 @@ func NewRecipeRepository(db *sqlx.DB) domain.RecipeRepository { // be bubbled to the caller. The recipe parameter is passed by reference and will therefore be updated // directly and the new fields (ID, created) can be accessed upon success. func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { - tx, err := r.db.Begin() - if err != nil { - return err - } - - query := `INSERT INTO recipes ( - title, description, instructions, serves, difficulty, - duration, category, ingredients, userid, modified, created - ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11 - ) RETURNING id;` - - // NOTE: Data steps - // cast duration to JSON - // convert ingredients to store type - // cast store type to JSON - // extract string instructions from type - // cast category to string - // use nil for the modified time - + // Convert data into a readable format durationJSON, err := json.Marshal(recipe.Duration) if err != nil { return err @@ -74,27 +54,46 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { instructions[i] = instruction.Content } - var id int - if err = tx.QueryRow( - query, - recipe.Title, - recipe.Description, - pq.Array(instructions), - recipe.Serves, - recipe.Difficulty, - durationJSON, - string(recipe.Category), - ingredientsJSON, - recipe.UserId, - nil, - recipe.Created, - ).Scan(&id); err != nil { - tx.Rollback() - return err + psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) + + query := psql. + Insert("recipes"). + Columns( + "title", + "description", + "instructions", + "serves", + "difficulty", + "duration", + "category", + "ingredients", + "userid", + "modified", + "created", + ). + Values( + recipe.Title, + recipe.Description, + pq.Array(instructions), + recipe.Serves, + recipe.Difficulty, + durationJSON, + string(recipe.Category), + ingredientsJSON, + recipe.UserId, + nil, + recipe.Created, + ). + Suffix("RETURNING id") + + _sql, args, err := query.ToSql() + if err != nil { + return fmt.Errorf("Failed to construct query: %w", err) } - if err := tx.Commit(); err != nil { - return err + var id int + if err := r.db.Get(&id, _sql, args...); err != nil { + return fmt.Errorf("Failed to create recipe: %w", err) } // Set the new ID @@ -107,36 +106,9 @@ func (r *RecipeRepository) CreateRecipe(recipe *domain.Recipe) error { // function will fail - it will not know what recipe to edit. func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error { if recipe.Id <= 0 { - return fmt.Errorf("[ERROR] Recipe must contain an ID. Cannot edit unknown recipe.") + return fmt.Errorf("Recipe must contain an ID. Cannot edit unknown recipe.") } - tx, err := r.db.Begin() - if err != nil { - return err - } - - // This query will ensure the userId matches the owner. - query := `UPDATE recipes SET - title = $1, - description = $2, - instructions = $3, - serves = $4, - difficulty = $5, - duration = $6, - category = $7, - ingredients = $8, - modified = $9 - WHERE id = $10 - AND userid = $11;` - - // NOTE: Data steps - // cast duration to JSON - // convert ingredients to store type - // cast store type to JSON - // extract string instructions from type - // cast category to string - // use nil for the modified time - durationJSON, err := json.Marshal(recipe.Duration) if err != nil { return err @@ -157,38 +129,38 @@ func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error { instructions[i] = instruction.Content } - result, err := tx.Exec( - query, - recipe.Title, - recipe.Description, - pq.Array(instructions), - recipe.Serves, - recipe.Difficulty, - durationJSON, - string(recipe.Category), - ingredientsJSON, - time.Now().UTC(), - recipe.Id, - userId, - ) + psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) + + query := psql. + Update("recipes"). + Set("title", recipe.Title). + Set("description", recipe.Description). + Set("instructions", pq.Array(instructions)). + Set("serves", recipe.Serves). + Set("difficulty", recipe.Difficulty). + Set("duration", durationJSON). + Set("category", string(recipe.Category)). + Set("ingredients", ingredientsJSON). + Set("modified", time.Now().UTC()). + Where(sq.Eq{ + "id": recipe.Id, + "userid": userId, + }) + + _sql, args, err := query.ToSql() if err != nil { - tx.Rollback() - return err + return fmt.Errorf("Failed to construct query: %w", err) } - rows, err := result.RowsAffected() + result, err := r.db.Exec(_sql, args...) if err != nil { - tx.Rollback() - return err + return fmt.Errorf("Failed to update recipe: %w", err) } - if rows != 1 { - tx.Rollback() - return fmt.Errorf("[ERROR] Modified an unexpected number of rows. Expected 1, modified %d.", rows) - } - - if err := tx.Commit(); err != nil { + if rows, err := result.RowsAffected(); err != nil { return err + } else if rows != 1 { + return fmt.Errorf("Modified an unexpected number of rows. Expected 1, modified %d.", rows) } return nil @@ -197,17 +169,35 @@ func (r *RecipeRepository) EditRecipe(recipe *domain.Recipe, userId int) error { // DeleteRecipe deletes a recipe in the database. This is done by setting the deleted field to true. // This will create a "soft delete" effect. This function does not validate that the user is the owner, // so the caller should validate the owner. If any errors occur, they will be returned to the caller. -func (r *RecipeRepository) DeleteRecipe(recipeId int) error { - query := "UPDATE recipes SET deleted = TRUE WHERE id = $1" +func (r *RecipeRepository) DeleteRecipe(recipeId, userId int) error { + psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) - result, err := r.db.Exec(query, recipeId) + query := psql. + Update("recipes"). + Set("deleted", true). + Set("modified", time.Now().UTC()). + Where(sq.Eq{ + "id": recipeId, + "userid": userId, + "deleted": false, + }) + + sql, args, err := query.ToSql() if err != nil { - return err + return fmt.Errorf("Failed to build delete query: %w", err) } - rows, _ := result.RowsAffected() + result, err := r.db.Exec(sql, args...) + if err != nil { + return fmt.Errorf("Failed to delete recipe: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("Failed to get rows affects: %w", err) + } if rows != 1 { - return fmt.Errorf("[ERROR] Incorrect number of rows modified. Expected 1, received %d.", rows) + return fmt.Errorf("Incorrect number of rows modified. Expected 1, received %d.", rows) } return nil @@ -646,28 +636,26 @@ func (r *RecipeRepository) UpdateRecipeTags(recipe domain.Recipe, tags []string) // // This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored // and the standard "not-found" error will be returned. -func (r *RecipeRepository) GetUserRecipesIds(user_id int) ([]int, error) { - query := ` - SELECT id - FROM recipes - WHERE userid = $1 AND deleted = false - ORDER BY created DESC; - ` +func (r *RecipeRepository) GetUserRecipesIds(userId int) ([]int, error) { + psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) - rows, err := r.db.Query(query, user_id) + query := psql. + Select("id"). + From("recipes"). + Where(sq.Eq{ + "userid": userId, + "deleted": false, + }). + OrderBy("created DESC") + + _sql, args, err := query.ToSql() if err != nil { - return nil, fmt.Errorf("Failed to query DB for user recipes. %s\n", err.Error()) + return []int{}, fmt.Errorf("Failed to construct SQL query: %w", err) } - defer rows.Close() var ids []int - for rows.Next() { - var r_id int - if err := rows.Scan(&r_id); err != nil { - return []int{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error()) - } - - ids = append(ids, r_id) + if err := r.db.Select(&ids, _sql, args...); err != nil { + return []int{}, fmt.Errorf("Failed to get user recipes: %w", err) } return ids, nil @@ -681,28 +669,29 @@ func (r *RecipeRepository) GetUserRecipesIds(user_id int) ([]int, error) { // // This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored // and the standard "not-found" error will be returned. -func (r *RecipeRepository) GetUserFavoriteRecipesIds(id int) ([]int, error) { - query := ` - SELECT r.id - FROM favorites f - JOIN recipes r ON r.id = f.recipeid - WHERE f.userid = $1 AND deleted = false - ORDER BY f.created DESC; - ` - rows, err := r.db.Query(query, id) +func (r *RecipeRepository) GetUserFavoriteRecipesIds(userId int) ([]int, error) { + psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) + + query := psql. + Select("r.id"). + From("favorites f"). + Join("recipes r on r.id = f.recipeid"). + Where(sq.Eq{ + "f.userid": userId, + "deleted": false, + }). + OrderBy("f.created DESC") + + _sql, args, err := query.ToSql() if err != nil { - return nil, fmt.Errorf("Failed to query DB for user recipes. %s\n", err.Error()) + return []int{}, fmt.Errorf("Failed to construct SQL query: %w", err) } - defer rows.Close() + + fmt.Println(_sql) var ids []int - for rows.Next() { - var r_id int - if err := rows.Scan(&r_id); err != nil { - return []int{}, fmt.Errorf("Failed to scan ID from db. %s\n", err.Error()) - } - - ids = append(ids, r_id) + if err := r.db.Select(&ids, _sql, args...); err != nil { + return []int{}, fmt.Errorf("Failed to get users' favorite recipes: %w", err) } return ids, nil @@ -753,15 +742,24 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) return nil } - query := ` - SELECT COUNT(*) - FROM favorites - WHERE recipeid = $1 AND userid = $2; - ` + psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) + + query := psql. + Select("COUNT(*)"). + From("favorites"). + Where(sq.Eq{ + "recipeid": recipe.Id, + "userid": userId, + }) + + _sql, args, err := query.ToSql() + if err != nil { + return fmt.Errorf("Failed to construct SQL query: %w", err) + } var count int - if err := r.db.QueryRow(query, recipe.Id, userId).Scan(&count); err != nil { - return fmt.Errorf("Failed to get recipe favorite. %s", err.Error()) + if err := r.db.Get(&count, _sql, args...); err != nil { + return fmt.Errorf("Failed to get recipe favorite status: %w", err) } recipe.Favorite = count > 0 @@ -774,41 +772,56 @@ func (r *RecipeRepository) GetRecipeFavorite(recipe *domain.Recipe, userId int) // table and return it. If there is no entry, nil will be returned. Any errors will be bubbled to // the caller. All that is returned is the recipe ID, that way the caller can handle the fetching. func (r *RecipeRepository) GetRecipeOfTheWeekId(userId *int) (*int, error) { - query := ` - SELECT - r.id - FROM recipes r - JOIN recipeoftheweek rw ON rw.recipeid = r.id - WHERE r.deleted = false - ORDER BY rw.created DESC - LIMIT 1; - ` + psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) - var id int - if err := r.db.QueryRow(query).Scan(&id); err != nil { - if errors.Is(err, sql.ErrNoRows) { + query := psql. + Select("r.id"). + From("recipes r"). + Join("recipeoftheweek rw ON rw.recipeid = r.id"). + Where(sq.Eq{"r.deleted": false}). + OrderBy("rw.created DESC"). + Limit(1) + + _sql, args, err := query.ToSql() + if err != nil { + return nil, fmt.Errorf("Failed to build SQL query: %w", err) + } + + var recipeId int + if err := r.db.Get(&recipeId, _sql, args...); err != nil { + if err == sql.ErrNoRows { return nil, nil } return nil, fmt.Errorf("Failed to locate recipe in database: %s", err.Error()) } - return &id, nil + return &recipeId, nil } // IsRecipeOwner takes two required arguments: a user id and a recipe id. This function queries the DB // to check if the user is the owner of the provided recipe. Any error will be bubbled to the caller. func (r *RecipeRepository) IsRecipeOwner(userId, recipeId int) (bool, error) { - query := ` - SELECT - userid - FROM recipes - WHERE deleted = false - AND id = $1; - ` + psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar) + + query := psql. + Select("userid"). + From("recipes"). + Where(sq.Eq{ + "id": recipeId, + "deleted": false, + }) + + _sql, args, err := query.ToSql() + if err != nil { + return false, err + } var recipeOwnerId int - if err := r.db.QueryRow(query, recipeId).Scan(&recipeOwnerId); err != nil { - return false, fmt.Errorf("Failed to get recipe owner id: %s", err.Error()) + if err := r.db.Get(&recipeOwnerId, _sql, args...); err != nil { + if err == sql.ErrNoRows { + return false, nil + } + return false, fmt.Errorf("Failed to get recipe owner id: %w", err) } return recipeOwnerId == userId, nil diff --git a/web/src/pages/Recipe.tsx b/web/src/pages/Recipe.tsx index 469ce18..00a5fa1 100644 --- a/web/src/pages/Recipe.tsx +++ b/web/src/pages/Recipe.tsx @@ -127,8 +127,12 @@ export default function RecipePage() { - {isAuthor && } - {isAuthor && } + {isAuthor && ( + <> + + + + )}

About this recipe