193 lines
6.2 KiB
Go
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
|
|
}
|