package repository import ( "context" "database/sql" "errors" "fmt" "tailly_clips/internal/domain" ) var ( ErrLikeNotFound = errors.New("like not found") ErrLikeAlreadyExists = errors.New("like already exists") ) type LikeRepository interface { Create(ctx context.Context, like *domain.ClipLike) error GetByID(ctx context.Context, id int) (*domain.ClipLike, error) GetByClipID(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipLike, int, error) GetByUserID(ctx context.Context, userID, limit, offset int) ([]*domain.ClipLike, int, error) CheckExists(ctx context.Context, clipID, userID int) (bool, error) Delete(ctx context.Context, clipID, userID int) error DeleteByClipID(ctx context.Context, clipID int) error DeleteByUserID(ctx context.Context, userID int) error } type likeRepository struct { db *sql.DB } func NewLikeRepository(db *sql.DB) LikeRepository { return &likeRepository{db: db} } func (r *likeRepository) Create(ctx context.Context, like *domain.ClipLike) error { query := ` INSERT INTO clip_likes (clip_id, user_id, created_at) VALUES ($1, $2, $3) RETURNING id ` err := r.db.QueryRowContext(ctx, query, like.ClipID, like.UserID, like.CreatedAt, ).Scan(&like.ID) if err != nil { if isDuplicateKeyError(err) { return ErrLikeAlreadyExists } return fmt.Errorf("failed to create like: %w", err) } return nil } func (r *likeRepository) GetByID(ctx context.Context, id int) (*domain.ClipLike, error) { query := ` SELECT id, clip_id, user_id, created_at FROM clip_likes WHERE id = $1 ` like := &domain.ClipLike{} err := r.db.QueryRowContext(ctx, query, id).Scan( &like.ID, &like.ClipID, &like.UserID, &like.CreatedAt, ) if err == sql.ErrNoRows { return nil, ErrLikeNotFound } if err != nil { return nil, fmt.Errorf("failed to get like: %w", err) } return like, nil } func (r *likeRepository) GetByClipID(ctx context.Context, clipID, limit, offset int) ([]*domain.ClipLike, int, error) { // Получаем общее количество countQuery := `SELECT COUNT(*) FROM clip_likes WHERE clip_id = $1` var totalCount int err := r.db.QueryRowContext(ctx, countQuery, clipID).Scan(&totalCount) if err != nil { return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // Получаем лайки query := ` SELECT id, clip_id, user_id, created_at FROM clip_likes WHERE clip_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3 ` rows, err := r.db.QueryContext(ctx, query, clipID, limit, offset) if err != nil { return nil, 0, fmt.Errorf("failed to get likes: %w", err) } defer rows.Close() var likes []*domain.ClipLike for rows.Next() { like := &domain.ClipLike{} err := rows.Scan( &like.ID, &like.ClipID, &like.UserID, &like.CreatedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan like: %w", err) } likes = append(likes, like) } if err = rows.Err(); err != nil { return nil, 0, fmt.Errorf("rows error: %w", err) } return likes, totalCount, nil } func (r *likeRepository) GetByUserID(ctx context.Context, userID, limit, offset int) ([]*domain.ClipLike, int, error) { // Получаем общее количество countQuery := `SELECT COUNT(*) FROM clip_likes WHERE user_id = $1` var totalCount int err := r.db.QueryRowContext(ctx, countQuery, userID).Scan(&totalCount) if err != nil { return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // Получаем лайки query := ` SELECT id, clip_id, user_id, created_at FROM clip_likes WHERE user_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3 ` rows, err := r.db.QueryContext(ctx, query, userID, limit, offset) if err != nil { return nil, 0, fmt.Errorf("failed to get likes: %w", err) } defer rows.Close() var likes []*domain.ClipLike for rows.Next() { like := &domain.ClipLike{} err := rows.Scan( &like.ID, &like.ClipID, &like.UserID, &like.CreatedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan like: %w", err) } likes = append(likes, like) } if err = rows.Err(); err != nil { return nil, 0, fmt.Errorf("rows error: %w", err) } return likes, totalCount, nil } func (r *likeRepository) CheckExists(ctx context.Context, clipID, userID int) (bool, error) { query := ` SELECT EXISTS( SELECT 1 FROM clip_likes WHERE clip_id = $1 AND user_id = $2 ) ` var exists bool err := r.db.QueryRowContext(ctx, query, clipID, userID).Scan(&exists) if err != nil { return false, fmt.Errorf("failed to check like existence: %w", err) } return exists, nil } func (r *likeRepository) Delete(ctx context.Context, clipID, userID int) error { query := ` DELETE FROM clip_likes WHERE clip_id = $1 AND user_id = $2 ` result, err := r.db.ExecContext(ctx, query, clipID, userID) if err != nil { return fmt.Errorf("failed to delete like: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { return ErrLikeNotFound } return nil } func (r *likeRepository) DeleteByClipID(ctx context.Context, clipID int) error { query := `DELETE FROM clip_likes WHERE clip_id = $1` _, err := r.db.ExecContext(ctx, query, clipID) if err != nil { return fmt.Errorf("failed to delete likes by clip ID: %w", err) } return nil } func (r *likeRepository) DeleteByUserID(ctx context.Context, userID int) error { query := `DELETE FROM clip_likes WHERE user_id = $1` _, err := r.db.ExecContext(ctx, query, userID) if err != nil { return fmt.Errorf("failed to delete likes by user ID: %w", err) } return nil } func isDuplicateKeyError(err error) bool { // Проверка на дубликат ключа (зависит от драйвера БД) // Для PostgreSQL обычно содержит "duplicate key" return err != nil && (err.Error() == "pq: duplicate key value violates unique constraint" || err.Error() == "ERROR: duplicate key value violates unique constraint") }