261 lines
9.1 KiB
Go
261 lines
9.1 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
|
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
|
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
|
|
)
|
|
|
|
// RecipeService implements the domain.RecipeService defined in the domain module.
|
|
type RecipeService struct {
|
|
recipeRepository domain.RecipeRepository
|
|
engagementRepository domainEngagement.EngagementRepository
|
|
}
|
|
|
|
// Compile-time check to ensure the RecipeService implements domain.RecipeService
|
|
var _ domain.RecipeService = (*RecipeService)(nil)
|
|
|
|
// NewRecipeService creates a user service object which can be passed into the context. The service
|
|
// requires a recipe repository which it will use to hit the database when needed.
|
|
func NewRecipeService(recipeRepository domain.RecipeRepository, engagementRepository domainEngagement.EngagementRepository) domain.RecipeService {
|
|
return &RecipeService{
|
|
recipeRepository: recipeRepository,
|
|
engagementRepository: engagementRepository,
|
|
}
|
|
}
|
|
|
|
// CreateRecipe creates a recipe in the database using the recipe repository. This function requires
|
|
// all the data to be present, though validation does not occur in this function. However, the UI
|
|
// will enforce validation, as will the database. Errors will be returned to the caller when they
|
|
// occur.
|
|
//
|
|
// TODO: Implement validation in the API.
|
|
// TODO: Implement image creation.
|
|
func (s *RecipeService) CreateRecipe(ctx *gin.Context) (*domain.Recipe, error) {
|
|
// Ensure user is logged in
|
|
if !domainServer.IsLoggedIn(ctx) {
|
|
return nil, fmt.Errorf("User is not logged in.")
|
|
}
|
|
|
|
userId := ctx.MustGet("userId").(int)
|
|
var req domain.CreateRecipeRequest
|
|
|
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
recipe := domain.Recipe{
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Instructions: req.Instructions,
|
|
Serves: req.Serves,
|
|
Difficulty: req.Difficulty,
|
|
Duration: req.Duration,
|
|
Category: req.Category,
|
|
Ingredients: req.Ingredients,
|
|
Sections: req.Sections,
|
|
UserId: userId,
|
|
Created: time.Now(),
|
|
}
|
|
|
|
if err := s.recipeRepository.CreateRecipe(&recipe); err != nil {
|
|
return &recipe, err
|
|
}
|
|
|
|
// TODO: Upload the image
|
|
// if req.image != nil {
|
|
// }
|
|
|
|
// Create the tags
|
|
if len(req.Tags) > 0 {
|
|
if err := s.recipeRepository.CreateRecipeTags(recipe, req.Tags); err != nil {
|
|
return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error())
|
|
}
|
|
}
|
|
|
|
return &recipe, nil
|
|
}
|
|
|
|
func (s *RecipeService) EditRecipe(ctx *gin.Context, recipeId, userId int) (*domain.Recipe, error) {
|
|
var req domain.EditRecipeRequest
|
|
|
|
if err := ctx.ShouldBindJSON(&req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if recipeId != req.Id {
|
|
return nil, fmt.Errorf("[ERROR] Mismatched recipe IDs provided. Given %d and %d.", recipeId, req.Id)
|
|
}
|
|
|
|
recipe := domain.Recipe{
|
|
Id: recipeId,
|
|
Title: req.Title,
|
|
Description: req.Description,
|
|
Instructions: req.Instructions,
|
|
Serves: req.Serves,
|
|
Difficulty: req.Difficulty,
|
|
Duration: req.Duration,
|
|
Category: req.Category,
|
|
Ingredients: req.Ingredients,
|
|
Sections: req.Sections,
|
|
}
|
|
|
|
if err := s.recipeRepository.EditRecipe(&recipe, userId); err != nil {
|
|
return &recipe, err
|
|
}
|
|
|
|
// Update the tags
|
|
if len(req.Tags) > 0 {
|
|
if err := s.recipeRepository.UpdateRecipeTags(recipe, req.Tags); err != nil {
|
|
return &recipe, fmt.Errorf("Failed to attach/create tags. %s\n", err.Error())
|
|
}
|
|
}
|
|
|
|
return &recipe, nil
|
|
}
|
|
|
|
// DeleteRecipe deletes a recipe in the database using the recipe repository. This function requires
|
|
// the userId of the requesting user - to ensure the user is the owner of the recipe. Any errors will
|
|
// be returned to caller when/if they occur.
|
|
func (s *RecipeService) DeleteRecipe(userId, recipeId int) error {
|
|
recipe, err := s.recipeRepository.GetRecipe(recipeId, &userId)
|
|
if recipe == nil || err != nil {
|
|
return fmt.Errorf("Recipe does not exist or has been relocated. Please try again. %s", err.Error())
|
|
}
|
|
|
|
if recipe.UserId != userId {
|
|
return fmt.Errorf("User id does not match. Do you own the target recipe?")
|
|
}
|
|
|
|
return s.recipeRepository.DeleteRecipe(recipeId)
|
|
}
|
|
|
|
// GetRecipe will get a recipe via its ID. Any errors will be bubbled to the caller. Furthermore,
|
|
// if the recipe is nil, an error will be returned, so the caller does not need to check for a nil
|
|
// recipe (e.g., if the error is nil the recipe exists)
|
|
//
|
|
// A userId should be provided to allow the favorite status to be updated. Without a userId (nil),
|
|
// the favorite status will return false, not because its not a favorite, but because it cannot find
|
|
// out!
|
|
func (s *RecipeService) GetRecipe(id int, userId *int) (*domain.Recipe, error) {
|
|
recipe, err := s.recipeRepository.GetRecipe(id, userId)
|
|
|
|
if recipe == nil && err == nil {
|
|
return nil, fmt.Errorf("Recipe does not exist or has been relocated. Please try again.")
|
|
}
|
|
|
|
return recipe, err
|
|
}
|
|
|
|
// SearchRecipes will search the database using the filters provided. The recipes can be passed into
|
|
// a template and displayed in the UI as the search result. A more detailed definition of the
|
|
// filters is provided below.
|
|
//
|
|
// Each input is given a bit value (e.g., 00001 for 1) and will be passed
|
|
// back to this handler as an array. The values are then added together
|
|
// and will result in a integer which represents bit values. These bits
|
|
// can then be passed to the repository and are then parsed to determine
|
|
// which filters should be applied.
|
|
// Parsing these is simple, for each filter option, use the bitwise and (&)
|
|
// operator with the value we expect for the filter. When 1, we can ensure
|
|
// the filter is provided.
|
|
// A function `isBitActive` in the recipe repository provides an example of
|
|
// testing of testing the filter parsing.
|
|
//
|
|
// The favorites parameter is used to only return filters favorited by the userId provided.
|
|
func (s *RecipeService) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]domain.Recipe, error) {
|
|
ids, err := s.recipeRepository.SearchRecipes(filters, userId, favorites)
|
|
if err != nil {
|
|
return []domain.Recipe{}, err
|
|
}
|
|
|
|
return s.recipeRepository.GetRecipes(ids, userId)
|
|
}
|
|
|
|
// GetUserRecipes returns a list of the recipes that the user has created. The user's
|
|
// ID should be provided. Any errors will be bubbled to the caller.
|
|
func (s *RecipeService) GetUserRecipes(userId int) ([]domain.Recipe, error) {
|
|
ids, err := s.recipeRepository.GetUserRecipesIds(userId)
|
|
if err != nil {
|
|
return []domain.Recipe{}, err
|
|
}
|
|
|
|
return s.recipeRepository.GetRecipes(ids, &userId)
|
|
}
|
|
|
|
// GetUserFavoriteRecipes returns a list of the recipes that the user has marked as a
|
|
// favorite. The user's ID should be provided. Any errors will be bubbled to the caller.
|
|
func (s *RecipeService) GetUserFavoriteRecipes(userId int) ([]domain.Recipe, error) {
|
|
ids, err := s.recipeRepository.GetUserFavoriteRecipesIds(userId)
|
|
if err != nil {
|
|
return []domain.Recipe{}, err
|
|
}
|
|
|
|
return s.recipeRepository.GetRecipes(ids, &userId)
|
|
}
|
|
|
|
// GetUserViewedRecipes returns a list of the most recent x (limit) recipes viewed by a user, from
|
|
// the provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to
|
|
// the caller.
|
|
func (s *RecipeService) GetUserViewedRecipes(userId, limit int) ([]domain.Recipe, error) {
|
|
engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementViewed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ids := make([]int, len(engagement))
|
|
for i, eng := range engagement {
|
|
ids[i] = eng.Entity
|
|
}
|
|
|
|
return s.recipeRepository.GetRecipes(ids, &userId)
|
|
}
|
|
|
|
// GetUserMadeRecipes returns a list of the most recent x (limit) recipes made by a user, from the
|
|
// provided userId. This will return a list of size 'limit'. Any errors will be bubbled up to the
|
|
// caller.
|
|
func (s *RecipeService) GetUserMadeRecipes(userId, limit int) ([]domain.Recipe, error) {
|
|
engagement, err := s.engagementRepository.GetUserEngagementFiltered(userId, limit, domainEngagement.EngagementMade)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ids := make([]int, len(engagement))
|
|
for i, eng := range engagement {
|
|
ids[i] = eng.Entity
|
|
}
|
|
|
|
return s.recipeRepository.GetRecipes(ids, &userId)
|
|
}
|
|
|
|
// GetRecipeOfTheWeek searches for the most recent recipe of the week. If there is not a value,
|
|
// the recipe will be nil. Any errors will be bubbled to the caller.
|
|
func (s *RecipeService) GetRecipeOfTheWeek(userId *int) (*domain.Recipe, error) {
|
|
id, err := s.recipeRepository.GetRecipeOfTheWeekId(userId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if id == nil {
|
|
return nil, fmt.Errorf("[ERROR] Recipe of the week ID could not be found. It may not exist.")
|
|
}
|
|
|
|
return s.recipeRepository.GetRecipe(*id, userId)
|
|
}
|
|
|
|
// IsRecipeOwner takes an optional userId and a recipeId. If the userId is nil (not given) this
|
|
// function will return false. Otherwise, it will query the database to find out of the user is
|
|
// the owner of the recipe. Any error will be bubbled to the caller.
|
|
func (s *RecipeService) IsRecipeOwner(userId *int, recipeId int) (bool, error) {
|
|
// No user, obviously not the user.
|
|
if userId == nil {
|
|
return false, nil
|
|
}
|
|
|
|
return s.recipeRepository.IsRecipeOwner(*userId, recipeId)
|
|
}
|