(FEAT): Orm work, not complete, but editing needs to be merged.
This commit is contained in:
parent
e81f2ec513
commit
6e28ebfe80
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -127,8 +127,12 @@ export default function RecipePage() {
|
||||
<MadeButton id={recipe.Id} />
|
||||
<ShareButton id={recipe.Id} />
|
||||
|
||||
{isAuthor && <DeleteButton clickHandler={deleteHandler} />}
|
||||
{isAuthor && <EditButton clickHandler={editHandler} />}
|
||||
{isAuthor && (
|
||||
<>
|
||||
<DeleteButton clickHandler={deleteHandler} />
|
||||
<EditButton clickHandler={editHandler} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
<div className="px-4 py-8 md:px-8">
|
||||
<h3 className="text-xl text-gray-800 font-semibold mb-2">About this recipe</h3>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user