384 lines
11 KiB
Go
384 lines
11 KiB
Go
package repository
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
domain "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
|
_ "github.com/lib/pq"
|
|
)
|
|
|
|
type EngagementRepository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// Compile-time check to ensure the EngagementRepository implements domain.EngagementRepository
|
|
var _ domain.EngagementRepository = (*EngagementRepository)(nil)
|
|
|
|
// NewUserRepository creates a user repository object which is used by the user service to access
|
|
// the database. Any user related database operations will take place in this repository.
|
|
func NewEngagementRepository(db *sql.DB) domain.EngagementRepository {
|
|
return &EngagementRepository{db: db}
|
|
}
|
|
|
|
// AddUserEngagement creates an engagement record in the database with the user ID provided. This
|
|
// function does not accept an entity ID as it should be used when there is no need to reference
|
|
// 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.
|
|
func (r *EngagementRepository) AddUserEngagement(userId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) {
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return domain.Engagement{}, err
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO Engagements (
|
|
type, message, entity, userid, created
|
|
) VALUES (
|
|
$1, $2, NULL, $3, $4
|
|
) RETURNING *;
|
|
`
|
|
|
|
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,
|
|
&engUserId,
|
|
&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 {
|
|
return domain.Engagement{}, err
|
|
}
|
|
|
|
// Is user is valid
|
|
if engUserId.Valid {
|
|
engagement.UserId = int(engUserId.Int32)
|
|
}
|
|
|
|
return engagement, nil
|
|
}
|
|
|
|
// 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
|
|
// bubbled to the caller.
|
|
//
|
|
// TODO: Disallow users to "make" the same recipe more than once a day
|
|
func (r *EngagementRepository) AddUserEntityEngagement(userId, entityId int, message string, engagementType domain.EngagementType) (domain.Engagement, error) {
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return domain.Engagement{}, err
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO Engagements (
|
|
type, message, entity, userid, created
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5
|
|
) RETURNING *;
|
|
`
|
|
|
|
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,
|
|
&engUserId,
|
|
&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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
return domain.Engagement{}, err
|
|
}
|
|
|
|
// Is user is valid
|
|
if userId.Valid {
|
|
engagement.UserId = int(userId.Int32)
|
|
}
|
|
|
|
return engagement, nil
|
|
}
|
|
|
|
// GetUserEngagement returns a list of the users most recent engagement entries. The number of records
|
|
// is determined by the limit passed into this function. The results are sorted, newest-to-oldest.
|
|
func (r *EngagementRepository) GetUserEngagement(userId, limit int) ([]domain.Engagement, error) {
|
|
query := `
|
|
SELECT * FROM Engagements
|
|
WHERE Userid = $1
|
|
ORDER BY created DESC LIMIT $2;
|
|
`
|
|
|
|
rows, err := r.db.Query(query, userId, limit)
|
|
if err != nil {
|
|
return []domain.Engagement{}, fmt.Errorf("Failed to get user engagements. %s", err.Error())
|
|
}
|
|
defer rows.Close()
|
|
|
|
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,
|
|
&engUserId,
|
|
&engagement.Created,
|
|
); err != nil {
|
|
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)
|
|
}
|
|
|
|
return engagements, err
|
|
}
|
|
|
|
// GetUserEngagementFiltered returns a list of the users most recent engagement entries of a provided
|
|
// type. The number of records is determined by the limit passed into this function. The results are
|
|
// sorted, newest-to-oldest. Only results of the provided engagementType will be returned.
|
|
func (r *EngagementRepository) GetUserEngagementFiltered(userId, limit int, engagementType domain.EngagementType) ([]domain.Engagement, error) {
|
|
query := `
|
|
SELECT id, type, message, entity, userid, created
|
|
FROM (
|
|
SELECT
|
|
*,
|
|
ROW_NUMBER() OVER (PARTITION BY entity ORDER BY created DESC) as rn
|
|
FROM Engagements
|
|
WHERE Userid = $1 AND type = $2
|
|
) AS subquery
|
|
WHERE rn = 1
|
|
ORDER BY created DESC
|
|
LIMIT $3;
|
|
`
|
|
|
|
rows, err := r.db.Query(query, userId, engagementType, limit)
|
|
if err != nil {
|
|
return []domain.Engagement{}, fmt.Errorf("Failed to get user engagements. %s", err.Error())
|
|
}
|
|
defer rows.Close()
|
|
|
|
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,
|
|
&engUserId,
|
|
&engagement.Created,
|
|
); err != nil {
|
|
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)
|
|
}
|
|
|
|
return engagements, err
|
|
}
|
|
|
|
// 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
|
|
// errors will be bubbled to the caller.
|
|
func (r *EngagementRepository) UserFavoriteRecipeToggle(userId, recipeId int) (bool, error) {
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
query := `
|
|
SELECT id
|
|
FROM favorites
|
|
WHERE userid = $1 AND recipeid = $2
|
|
`
|
|
|
|
var id int
|
|
|
|
err = tx.QueryRow(query, userId, recipeId).Scan(&id)
|
|
if err != nil {
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
tx.Rollback()
|
|
return false, fmt.Errorf("Failed to get recipe favorite. %s", err.Error())
|
|
}
|
|
}
|
|
|
|
// Means we should create
|
|
var success bool
|
|
if id == 0 {
|
|
createQuery := "INSERT INTO favorites (userid, recipeid, created) VALUES ($1, $2, $3);"
|
|
|
|
if result, err := tx.Exec(createQuery, userId, recipeId, time.Now()); err != nil {
|
|
tx.Rollback()
|
|
return false, fmt.Errorf("Failed to create recipe favorite. %s", err.Error())
|
|
} else {
|
|
rows, _ := result.RowsAffected()
|
|
success = rows == 1
|
|
}
|
|
} else {
|
|
deleteQuery := "DELETE FROM favorites WHERE id = $1;"
|
|
|
|
if result, err := tx.Exec(deleteQuery, id); err != nil {
|
|
tx.Rollback()
|
|
return false, fmt.Errorf("Failed to remove recipe favorite. %s", err.Error())
|
|
} else {
|
|
rows, _ := result.RowsAffected()
|
|
success = rows == 1
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
tx.Rollback()
|
|
return false, err
|
|
}
|
|
|
|
return success, nil
|
|
}
|