package repository import ( "context" "database/sql" "errors" "fmt" "tailly_clips/internal/domain" "time" ) var ( ErrClipNotFound = errors.New("clip not found") ) type ClipRepository interface { Create(ctx context.Context, clip *domain.Clip) error GetByID(ctx context.Context, id int) (*domain.Clip, error) GetByAuthorID(ctx context.Context, authorID, limit, offset int) ([]*domain.Clip, int, error) GetAll(ctx context.Context, limit, offset int) ([]*domain.Clip, int, error) Delete(ctx context.Context, id int) error IncrementLikesCount(ctx context.Context, clipID int) error DecrementLikesCount(ctx context.Context, clipID int) error IncrementCommentsCount(ctx context.Context, clipID int) error DecrementCommentsCount(ctx context.Context, clipID int) error GetClipURLs(ctx context.Context, clipID int) (string, string, error) GetClipWithURLs(ctx context.Context, clipID int) (*domain.Clip, error) } type clipRepository struct { db *sql.DB } func NewClipRepository(db *sql.DB) ClipRepository { return &clipRepository{db: db} } func (r *clipRepository) Create(ctx context.Context, clip *domain.Clip) error { if clip.VideoURL == "" { return fmt.Errorf("VideoURL cannot be empty") } if clip.ThumbnailURL == "" { return fmt.Errorf("ThumbnailURL cannot be empty") } if clip.AuthorID == 0 { return fmt.Errorf("AuthorID cannot be 0") } query := ` INSERT INTO clips (title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id ` err := r.db.QueryRowContext(ctx, query, clip.Title, clip.VideoURL, clip.ThumbnailURL, clip.AuthorID, clip.LikesCount, clip.CommentsCount, clip.CreatedAt, clip.UpdatedAt, ).Scan(&clip.ID) if err != nil { return fmt.Errorf("failed to create clip: %w", err) } return nil } func (r *clipRepository) GetByID(ctx context.Context, id int) (*domain.Clip, error) { query := ` SELECT id, title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at FROM clips WHERE id = $1 AND deleted_at IS NULL ` clip := &domain.Clip{} err := r.db.QueryRowContext(ctx, query, id).Scan( &clip.ID, &clip.Title, &clip.VideoURL, &clip.ThumbnailURL, &clip.AuthorID, &clip.LikesCount, &clip.CommentsCount, &clip.CreatedAt, &clip.UpdatedAt, ) if err == sql.ErrNoRows { return nil, ErrClipNotFound } if err != nil { return nil, fmt.Errorf("failed to get clip: %w", err) } return clip, nil } func (r *clipRepository) GetByAuthorID(ctx context.Context, authorID, limit, offset int) ([]*domain.Clip, int, error) { // Получаем общее количество countQuery := `SELECT COUNT(*) FROM clips WHERE author_id = $1 AND deleted_at IS NULL` var totalCount int err := r.db.QueryRowContext(ctx, countQuery, authorID).Scan(&totalCount) if err != nil { return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // Получаем клипы query := ` SELECT id, title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at FROM clips WHERE author_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC LIMIT $2 OFFSET $3 ` rows, err := r.db.QueryContext(ctx, query, authorID, limit, offset) if err != nil { return nil, 0, fmt.Errorf("failed to get clips: %w", err) } defer rows.Close() var clips []*domain.Clip for rows.Next() { clip := &domain.Clip{} err := rows.Scan( &clip.ID, &clip.Title, &clip.VideoURL, &clip.ThumbnailURL, &clip.AuthorID, &clip.LikesCount, &clip.CommentsCount, &clip.CreatedAt, &clip.UpdatedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan clip: %w", err) } clips = append(clips, clip) } if err = rows.Err(); err != nil { return nil, 0, fmt.Errorf("rows error: %w", err) } return clips, totalCount, nil } func (r *clipRepository) GetAll(ctx context.Context, limit, offset int) ([]*domain.Clip, int, error) { // Получаем общее количество countQuery := `SELECT COUNT(*) FROM clips WHERE deleted_at IS NULL` var totalCount int err := r.db.QueryRowContext(ctx, countQuery).Scan(&totalCount) if err != nil { return nil, 0, fmt.Errorf("failed to get total count: %w", err) } // Получаем клипы query := ` SELECT id, title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at FROM clips WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT $1 OFFSET $2 ` rows, err := r.db.QueryContext(ctx, query, limit, offset) if err != nil { return nil, 0, fmt.Errorf("failed to get clips: %w", err) } defer rows.Close() var clips []*domain.Clip for rows.Next() { clip := &domain.Clip{} err := rows.Scan( &clip.ID, &clip.Title, &clip.VideoURL, &clip.ThumbnailURL, &clip.AuthorID, &clip.LikesCount, &clip.CommentsCount, &clip.CreatedAt, &clip.UpdatedAt, ) if err != nil { return nil, 0, fmt.Errorf("failed to scan clip: %w", err) } clips = append(clips, clip) } if err = rows.Err(); err != nil { return nil, 0, fmt.Errorf("rows error: %w", err) } return clips, totalCount, nil } func (r *clipRepository) Delete(ctx context.Context, id int) error { query := ` UPDATE clips SET deleted_at = $1 WHERE id = $2 AND deleted_at IS NULL ` result, err := r.db.ExecContext(ctx, query, time.Now(), id) if err != nil { return fmt.Errorf("failed to delete clip: %w", err) } rowsAffected, err := result.RowsAffected() if err != nil { return fmt.Errorf("failed to get rows affected: %w", err) } if rowsAffected == 0 { return ErrClipNotFound } return nil } func (r *clipRepository) IncrementLikesCount(ctx context.Context, clipID int) error { query := ` UPDATE clips SET likes_count = likes_count + 1, updated_at = $1 WHERE id = $2 AND deleted_at IS NULL ` _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) if err != nil { return fmt.Errorf("failed to increment likes count: %w", err) } return nil } func (r *clipRepository) DecrementLikesCount(ctx context.Context, clipID int) error { query := ` UPDATE clips SET likes_count = GREATEST(likes_count - 1, 0), updated_at = $1 WHERE id = $2 AND deleted_at IS NULL ` _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) if err != nil { return fmt.Errorf("failed to decrement likes count: %w", err) } return nil } func (r *clipRepository) IncrementCommentsCount(ctx context.Context, clipID int) error { query := ` UPDATE clips SET comments_count = comments_count + 1, updated_at = $1 WHERE id = $2 AND deleted_at IS NULL ` _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) if err != nil { return fmt.Errorf("failed to increment comments count: %w", err) } return nil } func (r *clipRepository) DecrementCommentsCount(ctx context.Context, clipID int) error { query := ` UPDATE clips SET comments_count = GREATEST(comments_count - 1, 0), updated_at = $1 WHERE id = $2 AND deleted_at IS NULL ` _, err := r.db.ExecContext(ctx, query, time.Now(), clipID) if err != nil { return fmt.Errorf("failed to decrement comments count: %w", err) } return nil } func (r *clipRepository) GetClipURLs(ctx context.Context, clipID int) (string, string, error) { query := ` SELECT video_url, thumbnail_url FROM clips WHERE id = $1 AND deleted_at IS NULL ` var videoURL, thumbnailURL string err := r.db.QueryRowContext(ctx, query, clipID).Scan(&videoURL, &thumbnailURL) if err == sql.ErrNoRows { return "", "", ErrClipNotFound } if err != nil { return "", "", fmt.Errorf("failed to get clip URLs: %w", err) } return videoURL, thumbnailURL, nil } func (r *clipRepository) GetClipWithURLs(ctx context.Context, clipID int) (*domain.Clip, error) { query := ` SELECT id, title, video_url, thumbnail_url, author_id, likes_count, comments_count, created_at, updated_at FROM clips WHERE id = $1 AND deleted_at IS NULL ` clip := &domain.Clip{} err := r.db.QueryRowContext(ctx, query, clipID).Scan( &clip.ID, &clip.Title, &clip.VideoURL, &clip.ThumbnailURL, &clip.AuthorID, &clip.LikesCount, &clip.CommentsCount, &clip.CreatedAt, &clip.UpdatedAt, ) if err == sql.ErrNoRows { return nil, ErrClipNotFound } if err != nil { return nil, fmt.Errorf("failed to get clip: %w", err) } return clip, nil }