(FEAT): Implemented anon engagements.

This required some fixing of old repo methods, since the nullable user
id was a bit hard to parse. But it should be working now.
This commit is contained in:
Hayden Hargreaves 2025-07-15 20:14:32 -07:00
parent 7e355d5eda
commit e4c1a575be
5 changed files with 173 additions and 10 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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)
}

View File

@ -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)

View File

@ -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) {