diff --git a/internal/app/handlers/page_handler.go b/internal/app/handlers/page_handler.go index a5c4ec0..7c5c48d 100644 --- a/internal/app/handlers/page_handler.go +++ b/internal/app/handlers/page_handler.go @@ -129,15 +129,21 @@ func RecipePage(ctx *gin.Context) { } // Add engagement - if user != nil { - if _, err = deps.EngagementService.UserViewRecipe(user.Id, recipe.Id); err != nil { + if loggedIn { + fmt.Println("CALLING USER VIEW") + if _, err = deps.EngagementService.UserViewRecipe(*userId, recipe.Id); err != nil { + fmt.Printf("ERROR: %s\n", err.Error()) + ctx.JSON(400, err.Error()) + return + } + } else { + fmt.Println("CALLING VIEW") + if _, err = deps.EngagementService.ViewRecipe(recipe.Id); err != nil { fmt.Printf("ERROR: %s\n", err.Error()) ctx.JSON(400, err.Error()) return } } - // TODO: Need handling for anon viewing of the recipe - // I also do not really like that this runs on refresh, might need some better handling title := "Potion - View Recipe" page := pages.RecipePage(*recipe, *user, loggedIn) diff --git a/internal/app/service/engagement_service.go b/internal/app/service/engagement_service.go index 362b9ad..9e7bf92 100644 --- a/internal/app/service/engagement_service.go +++ b/internal/app/service/engagement_service.go @@ -25,6 +25,20 @@ func NewEngagementService(engagementRepository domain.EngagementRepository, reci } } +// ViewRecipe requires a user ID and a recipe ID to create an engagement record in the database. +// A message will be generated using the recipe data and then used to add a view engagement to the +// database. +func (s *EngagementService) ViewRecipe(recipeId int) (domain.Engagement, error) { + recipe, err := s.recipeRepository.GetRecipe(recipeId, nil) + if err != nil { + return domain.Engagement{}, err + } + + message := fmt.Sprintf("Viewed \"%s\"", recipe.Title) + + return s.engagementRepository.AddEntityEngagement(recipeId, message, domain.EngagementViewed) +} + // UserViewRecipe requires a user ID and a recipe ID to create an engagement record in the database. // A message will be generated using the recipe data and then used to add a view engagement to the // database. diff --git a/internal/domain/engagement/repository.go b/internal/domain/engagement/repository.go index 906a08e..294b794 100644 --- a/internal/domain/engagement/repository.go +++ b/internal/domain/engagement/repository.go @@ -3,6 +3,8 @@ package domain type EngagementRepository interface { AddUserEngagement(userId int, message string, engagementType EngagementType) (Engagement, error) AddUserEntityEngagement(userId, entityId int, message string, engagementType EngagementType) (Engagement, error) + AddEngagement(message string, engagementType EngagementType) (Engagement, error) + AddEntityEngagement(entityId int, message string, engagementType EngagementType) (Engagement, error) GetUserEngagement(userId, limit int) ([]Engagement, error) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) } diff --git a/internal/domain/engagement/service.go b/internal/domain/engagement/service.go index 815ac47..a4dd0fc 100644 --- a/internal/domain/engagement/service.go +++ b/internal/domain/engagement/service.go @@ -1,6 +1,7 @@ package domain type EngagementService interface { + ViewRecipe(recipeId int) (Engagement, error) UserViewRecipe(userId, recipeId int) (Engagement, error) UserFavoriteRecipe(userId, recipeId int) (Engagement, error) UserMakeRecipe(userId, recipeId int) (Engagement, error) diff --git a/internal/infrastructure/database/repository/engagement_repository.go b/internal/infrastructure/database/repository/engagement_repository.go index e869262..16cdc9b 100644 --- a/internal/infrastructure/database/repository/engagement_repository.go +++ b/internal/infrastructure/database/repository/engagement_repository.go @@ -44,12 +44,13 @@ func (r *EngagementRepository) AddUserEngagement(userId int, message string, eng ` var engagement domain.Engagement + var engUserId sql.NullInt32 if err := tx.QueryRow(query, engagementType, message, userId, time.Now()).Scan( &engagement.Id, &engagement.Type, &engagement.Message, &engagement.Entity, - &engagement.UserId, + &engUserId, &engagement.Created, ); err != nil { tx.Rollback() @@ -61,10 +62,15 @@ func (r *EngagementRepository) AddUserEngagement(userId int, message string, eng return domain.Engagement{}, err } + // Is user is valid + if engUserId.Valid { + engagement.UserId = int(engUserId.Int32) + } + return engagement, nil } -// AddUserEngagement creates an engagement record in the database with the user ID provided. This +// AddUserEntityEngagement creates an engagement record in the database with the user ID provided. This // function requires an entity ID as it should be used when there is a reference to external an // entity. The message should be provided, but a blank string ("") is acceptable. The engagement // type parameter determines the labeling of the engagement in the database. Any errors will be @@ -87,12 +93,13 @@ func (r *EngagementRepository) AddUserEntityEngagement(userId, entityId int, mes ` var engagement domain.Engagement + var engUserId sql.NullInt32 if err := tx.QueryRow(query, engagementType, message, entityId, userId, time.Now()).Scan( &engagement.Id, &engagement.Type, &engagement.Message, &engagement.Entity, - &engagement.UserId, + &engUserId, &engagement.Created, ); err != nil { tx.Rollback() @@ -104,6 +111,133 @@ func (r *EngagementRepository) AddUserEntityEngagement(userId, entityId int, mes return domain.Engagement{}, err } + // Is user is valid + if engUserId.Valid { + engagement.UserId = int(engUserId.Int32) + } + + return engagement, nil +} + +// AddEngagement creates an engagement record in the database without any user. This function does +// not accept an entity ID as it should be used when there is no need to reference an entity or user. +// The message should be provided, but a blank string ("") is acceptable. The engagement type +// parameter determines the labeling of the engagement in the database. Any errors will be bubbled +// to the caller. +// +// List of allowed engagements: viewed, shared +func (r *EngagementRepository) AddEngagement(message string, engagementType domain.EngagementType) (domain.Engagement, error) { + // Prevent invalid engagement types + switch engagementType { + case domain.EngagementViewed: + case domain.EngagementShared: + break + case domain.EngagementMade: + case domain.EngagementLiked: + case domain.EngagementReviewed: + case domain.EngagementRated: + return domain.Engagement{}, fmt.Errorf("Attempting to use disallowed anonymous engagement type.") + } + + tx, err := r.db.Begin() + if err != nil { + tx.Rollback() + return domain.Engagement{}, err + } + + query := ` + INSERT INTO Engagements ( + type, message, entity, userid, created + ) VALUES ( + $1, $2, NULL, NULL, $3 + ) RETURNING *; + ` + + var engagement domain.Engagement + var userId sql.NullInt32 + if err := tx.QueryRow(query, engagementType, message, time.Now()).Scan( + &engagement.Id, + &engagement.Type, + &engagement.Message, + &engagement.Entity, + &userId, + &engagement.Created, + ); err != nil { + tx.Rollback() + return domain.Engagement{}, fmt.Errorf("Failed to insert engagement into database. %s", err.Error()) + } + + if err := tx.Commit(); err != nil { + tx.Rollback() + return domain.Engagement{}, err + } + + // Is user is valid + if userId.Valid { + engagement.UserId = int(userId.Int32) + } + + return engagement, nil +} + +// AddEntityEngagement creates an engagement record in the database without any user. This function +// requires an entity ID as it should be used when there is a reference to external an entity. +// The message should be provided, but a blank string ("") is acceptable. The engagement type +// parameter determines the labeling of the engagement in the database. Any errors will be +// bubbled to the caller. +// +// List of allowed engagements: viewed, shared +func (r *EngagementRepository) AddEntityEngagement(entityId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) { + // Prevent invalid engagement types + switch engagementType { + case domain.EngagementViewed: + case domain.EngagementShared: + break + case domain.EngagementMade: + case domain.EngagementLiked: + case domain.EngagementReviewed: + case domain.EngagementRated: + return domain.Engagement{}, fmt.Errorf("Attempting to use disallowed anonymous engagement type.") + } + + tx, err := r.db.Begin() + if err != nil { + tx.Rollback() + return domain.Engagement{}, err + } + + query := ` + INSERT INTO Engagements ( + type, message, entity, userid, created + ) VALUES ( + $1, $2, $3, NULL, $4 + ) RETURNING *; + ` + + var engagement domain.Engagement + var userId sql.NullInt32 + if err := tx.QueryRow(query, engagementType, message, entityId, time.Now()).Scan( + &engagement.Id, + &engagement.Type, + &engagement.Message, + &engagement.Entity, + &userId, + &engagement.Created, + ); err != nil { + tx.Rollback() + return domain.Engagement{}, fmt.Errorf("Failed to insert engagement into database. %s", err.Error()) + } + + if err := tx.Commit(); err != nil { + tx.Rollback() + return domain.Engagement{}, err + } + + // Is user is valid + if userId.Valid { + engagement.UserId = int(userId.Int32) + } + return engagement, nil } @@ -132,18 +266,24 @@ func (r *EngagementRepository) GetUserEngagement(userId, limit int) ([]domain.En var engagements []domain.Engagement for rows.Next() { var engagement domain.Engagement + var engUserId sql.NullInt32 if err := rows.Scan( &engagement.Id, &engagement.Type, &engagement.Message, &engagement.Entity, - &engagement.UserId, + &engUserId, &engagement.Created, ); err != nil { tx.Rollback() return []domain.Engagement{}, fmt.Errorf("Failed to scan user engagement. %s", err.Error()) } + // Add user if valid + if engUserId.Valid { + engagement.UserId = int(engUserId.Int32) + } + engagements = append(engagements, engagement) } @@ -157,7 +297,7 @@ func (r *EngagementRepository) GetUserEngagement(userId, limit int) ([]domain.En // UserFavoriteRecipeToggle toggles the status of a users favorite of a recipe. If the user has already // favorited the provided recipe, the database entry will be delete, hence removing the favorite. Otherwise, -// an entry will be created. The NEW status of the users favorite will be returned as the boolean. Any +// an entry will be created. The NEW status of the users favorite will be returned as the boolean. Any // errors will be bubbled to the caller. func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) { tx, err := r.db.Begin() @@ -173,7 +313,7 @@ func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (b ` var id int - + err = tx.QueryRow(query, userId, recipeId).Scan(&id) if err != nil { if !errors.Is(err, sql.ErrNoRows) {