veza/veza-backend-api/internal/services/comment_service.go
senke b103a09a25 chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 16:43:21 +01:00

223 lines
6.6 KiB
Go

package services
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"veza-backend-api/internal/models"
"go.uber.org/zap"
"gorm.io/gorm"
)
type CommentService struct {
db *gorm.DB
logger *zap.Logger
}
func NewCommentService(db *gorm.DB, logger *zap.Logger) *CommentService {
return &CommentService{
db: db,
logger: logger,
}
}
// CreateComment creates a new comment on a track
func (s *CommentService) CreateComment(ctx context.Context, trackID uuid.UUID, userID uuid.UUID, content string, timestamp float64, parentID *uuid.UUID) (*models.TrackComment, error) { // Updated trackID and parentID to uuid.UUID
// Verify if track exists
var track models.Track
if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil { // Updated query
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTrackNotFound
}
return nil, err
}
// Verify if parent comment exists (if reply)
if parentID != nil {
var parent models.TrackComment
if err := s.db.WithContext(ctx).First(&parent, "id = ?", *parentID).Error; err != nil { // Updated query
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrParentCommentNotFound
}
return nil, err
}
// Ensure parent belongs to the same track
if parent.TrackID != trackID {
return nil, ErrParentTrackMismatch
}
}
comment := &models.TrackComment{
TrackID: trackID,
UserID: userID,
Content: content,
Timestamp: timestamp,
ParentID: parentID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.db.WithContext(ctx).Create(comment).Error; err != nil {
s.logger.Error("Failed to create comment", zap.Error(err))
return nil, err
}
// Preload user info for response
if err := s.db.WithContext(ctx).Preload("User").First(comment, comment.ID).Error; err != nil {
return comment, nil // Return comment without user info if preload fails
}
s.logger.Info("Comment created",
zap.Any("comment_id", comment.ID),
zap.Any("track_id", trackID),
zap.String("user_id", userID.String()))
return comment, nil
}
// GetTrackCreatorID returns the creator user ID of a track (Phase 2.2)
func (s *CommentService) GetTrackCreatorID(ctx context.Context, trackID uuid.UUID) (uuid.UUID, error) {
var track models.Track
if err := s.db.WithContext(ctx).Select("user_id").First(&track, "id = ?", trackID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return uuid.Nil, ErrTrackNotFound
}
return uuid.Nil, err
}
return track.UserID, nil
}
// GetComments retrieves comments for a track
func (s *CommentService) GetComments(ctx context.Context, trackID uuid.UUID, page, limit int) ([]models.TrackComment, int64, error) { // Updated trackID to uuid.UUID
var comments []models.TrackComment
var total int64
offset := (page - 1) * limit
// Count total top-level comments (or all comments? usually top-level for pagination, replies fetched separately or nested)
// Here we fetch all top-level comments
query := s.db.WithContext(ctx).Model(&models.TrackComment{}).Where("track_id = ? AND parent_id IS NULL", trackID)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Fetch comments with user info and replies
// Note: Deep nesting of replies might require recursive query or multiple queries.
// For simplicity, we just preload direct replies or let frontend handle threading if flat list.
// Assuming flat list of top level + preloaded replies?
// Let's just fetch top level and preload their replies one level deep for now
err := query.
Preload("User").
Preload("Replies").
Preload("Replies.User").
Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&comments).Error
if err != nil {
s.logger.Error("Failed to get comments", zap.Error(err))
return nil, 0, err
}
return comments, total, nil
}
// UpdateComment updates a comment
func (s *CommentService) UpdateComment(ctx context.Context, commentID uuid.UUID, userID uuid.UUID, content string) (*models.TrackComment, error) { // Updated commentID to uuid.UUID
var comment models.TrackComment
if err := s.db.WithContext(ctx).First(&comment, "id = ?", commentID).Error; err != nil { // Updated query
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrCommentNotFound
}
return nil, err
}
// Check permission
if comment.UserID != userID {
return nil, ErrForbidden
}
comment.Content = content
comment.IsEdited = true
comment.UpdatedAt = time.Now()
if err := s.db.WithContext(ctx).Save(&comment).Error; err != nil {
s.logger.Error("Failed to update comment", zap.Error(err))
return nil, err
}
s.logger.Info("Comment updated",
zap.Any("comment_id", comment.ID), // Changed to zap.Any for uuid.UUID
zap.String("user_id", userID.String()))
return &comment, nil
}
// GetReplies retrieves replies for a given parent comment ID
func (s *CommentService) GetReplies(ctx context.Context, parentID uuid.UUID, page, limit int) ([]models.TrackComment, int64, error) { // Updated parentID to uuid.UUID
var replies []models.TrackComment
var total int64
offset := (page - 1) * limit
// Verify if parent comment exists
var parent models.TrackComment
if err := s.db.WithContext(ctx).First(&parent, "id = ?", parentID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, 0, ErrParentCommentNotFound
}
return nil, 0, err
}
// Count total replies
query := s.db.WithContext(ctx).Model(&models.TrackComment{}).Where("parent_id = ?", parentID)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Fetch replies with user info
err := query.
Preload("User").
Order("created_at ASC"). // Order by oldest first
Limit(limit).
Offset(offset).
Find(&replies).Error
if err != nil {
s.logger.Error("Failed to get replies", zap.Error(err))
return nil, 0, err
}
return replies, total, nil
}
// DeleteComment deletes a comment
// MIGRATION UUID: userID migré vers uuid.UUID, commentID reste int64
func (s *CommentService) DeleteComment(ctx context.Context, commentID uuid.UUID, userID uuid.UUID, isAdmin bool) error { // Updated commentID to uuid.UUID
var comment models.TrackComment
if err := s.db.WithContext(ctx).First(&comment, "id = ?", commentID).Error; err != nil { // Updated query
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrCommentNotFound
}
return err
}
// Check permission
if comment.UserID != userID && !isAdmin {
return ErrForbidden
}
// Soft delete or hard delete? Model has DeletedAt so soft delete
if err := s.db.WithContext(ctx).Delete(&comment).Error; err != nil {
s.logger.Error("Failed to delete comment", zap.Error(err))
return err
}
return nil
}