veza/veza-backend-api/internal/repositories/chat_message_repository.go
2026-03-06 18:58:37 +01:00

193 lines
6.2 KiB
Go

package repositories
import (
"context"
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
"veza-backend-api/internal/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ChatMessageRepository struct {
db *gorm.DB
}
func NewChatMessageRepository(db *gorm.DB) *ChatMessageRepository {
return &ChatMessageRepository{db: db}
}
func (r *ChatMessageRepository) DB() *gorm.DB {
return r.db
}
func (r *ChatMessageRepository) Create(ctx context.Context, msg *models.ChatMessage) error {
return r.db.WithContext(ctx).Create(msg).Error
}
func (r *ChatMessageRepository) GetByID(ctx context.Context, id uuid.UUID) (*models.ChatMessage, error) {
var msg models.ChatMessage
err := r.db.WithContext(ctx).Where("id = ?", id).First(&msg).Error
if err != nil {
return nil, fmt.Errorf("message not found: %w", err)
}
return &msg, nil
}
func (r *ChatMessageRepository) Update(ctx context.Context, msg *models.ChatMessage) error {
return r.db.WithContext(ctx).Save(msg).Error
}
func (r *ChatMessageRepository) SoftDelete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).
Model(&models.ChatMessage{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"is_deleted": true,
"updated_at": time.Now(),
}).Error
}
func (r *ChatMessageRepository) GetConversationMessages(ctx context.Context, conversationID uuid.UUID, limit, offset int) ([]models.ChatMessage, error) {
var messages []models.ChatMessage
err := r.db.WithContext(ctx).
Preload("Sender").
Where("room_id = ? AND is_deleted = ?", conversationID, false).
Order("created_at DESC").
Limit(limit).
Offset(offset).
Find(&messages).Error
if err != nil {
return nil, fmt.Errorf("failed to get conversation messages: %w", err)
}
return messages, nil
}
// ConversationMessagesWithCursorResult holds messages and next cursor for cursor-based pagination (v0.931).
type ConversationMessagesWithCursorResult struct {
Messages []models.ChatMessage
NextCursor string
}
// GetConversationMessagesWithCursor uses keyset pagination on (created_at, id) for consistent performance.
// Cursor format: base64(created_at_unix_nano|uuid). Order DESC (newest first), so cursor yields older messages.
func (r *ChatMessageRepository) GetConversationMessagesWithCursor(ctx context.Context, conversationID uuid.UUID, limit int, cursor string) (*ConversationMessagesWithCursorResult, error) {
if limit <= 0 {
limit = 50
}
if limit > 100 {
limit = 100
}
query := r.db.WithContext(ctx).
Where("room_id = ? AND is_deleted = ?", conversationID, false)
var cursorCreatedAt int64
var cursorID uuid.UUID
if cursor != "" {
decoded, err := base64.RawURLEncoding.DecodeString(cursor)
if err == nil {
parts := strings.SplitN(string(decoded), "|", 2)
if len(parts) == 2 {
if ts, err := strconv.ParseInt(parts[0], 10, 64); err == nil {
cursorCreatedAt = ts
}
if uid, err := uuid.Parse(parts[1]); err == nil {
cursorID = uid
}
}
}
}
if cursor != "" && (cursorCreatedAt != 0 || cursorID != uuid.Nil) {
query = query.Where("(created_at, id) < (?, ?)", time.Unix(0, cursorCreatedAt), cursorID)
}
query = query.Preload("Sender").Order("created_at DESC, id DESC").Limit(limit + 1)
var messages []models.ChatMessage
if err := query.Find(&messages).Error; err != nil {
return nil, fmt.Errorf("failed to get conversation messages: %w", err)
}
var nextCursor string
if len(messages) > limit {
last := messages[limit-1]
nextCursor = base64.RawURLEncoding.EncodeToString([]byte(
fmt.Sprintf("%d|%s", last.CreatedAt.UnixNano(), last.ID.String())))
messages = messages[:limit]
}
return &ConversationMessagesWithCursorResult{Messages: messages, NextCursor: nextCursor}, nil
}
func (r *ChatMessageRepository) GetMessagesBefore(ctx context.Context, roomID uuid.UUID, beforeID uuid.UUID, limit int) ([]models.ChatMessage, error) {
var refMsg models.ChatMessage
if err := r.db.WithContext(ctx).Where("id = ?", beforeID).First(&refMsg).Error; err != nil {
return nil, fmt.Errorf("reference message not found: %w", err)
}
var messages []models.ChatMessage
err := r.db.WithContext(ctx).
Where("room_id = ? AND is_deleted = ? AND created_at < ?", roomID, false, refMsg.CreatedAt).
Order("created_at DESC").
Limit(limit).
Find(&messages).Error
if err != nil {
return nil, fmt.Errorf("failed to get messages before: %w", err)
}
return messages, nil
}
func (r *ChatMessageRepository) GetMessagesAfter(ctx context.Context, roomID uuid.UUID, afterID uuid.UUID, limit int) ([]models.ChatMessage, error) {
var refMsg models.ChatMessage
if err := r.db.WithContext(ctx).Where("id = ?", afterID).First(&refMsg).Error; err != nil {
return nil, fmt.Errorf("reference message not found: %w", err)
}
var messages []models.ChatMessage
err := r.db.WithContext(ctx).
Where("room_id = ? AND is_deleted = ? AND created_at > ?", roomID, false, refMsg.CreatedAt).
Order("created_at ASC").
Limit(limit).
Find(&messages).Error
if err != nil {
return nil, fmt.Errorf("failed to get messages after: %w", err)
}
return messages, nil
}
func (r *ChatMessageRepository) GetMessagesSince(ctx context.Context, roomID uuid.UUID, since time.Time, limit int) ([]models.ChatMessage, error) {
var messages []models.ChatMessage
err := r.db.WithContext(ctx).
Where("room_id = ? AND is_deleted = ? AND created_at > ?", roomID, false, since).
Order("created_at ASC").
Limit(limit).
Find(&messages).Error
if err != nil {
return nil, fmt.Errorf("failed to get messages since: %w", err)
}
return messages, nil
}
func (r *ChatMessageRepository) Search(ctx context.Context, roomID uuid.UUID, query string, limit, offset int) ([]models.ChatMessage, int64, error) {
tsQuery := "plainto_tsquery('simple', ?)"
var total int64
r.db.WithContext(ctx).Model(&models.ChatMessage{}).
Where("room_id = ? AND is_deleted = ? AND content_tsv @@ "+tsQuery, roomID, false, query).
Count(&total)
var messages []models.ChatMessage
err := r.db.WithContext(ctx).
Where("room_id = ? AND is_deleted = ? AND content_tsv @@ "+tsQuery, roomID, false, query).
Order("ts_rank(content_tsv, " + tsQuery + ") DESC, created_at DESC").
Limit(limit).
Offset(offset).
Find(&messages).Error
if err != nil {
return nil, 0, fmt.Errorf("failed to search messages: %w", err)
}
return messages, total, nil
}