Files
enkly-ai/services/conversation/internal/infrastructure/postgres/conversation_repo.go
enkly 79ce8dc4c0
Some checks failed
Go CI Pipeline / build_and_test (push) Failing after 22m0s
Init Project
2026-10-30 18:06:55 +01:00

126 lines
3.7 KiB
Go

// Package postgres implements conversation's domain repositories
// against the conversation database. One-for-one transplant of
// internal/db/conversations.go.
package postgres
import (
"context"
"database/sql"
"errors"
"enkly/pkg/pgkit"
"enkly/services/conversation/internal/domain"
)
// DefaultConversationTitle matches the column default in
// migrations/0001_init.sql.
const DefaultConversationTitle = "New conversation"
const conversationSelect = `
SELECT c.id, c.user_id, c.title, c.created_at, COALESCE(lm.last, c.created_at)
FROM conversations c
LEFT JOIN LATERAL (
SELECT max(m.created_at) AS last FROM messages m WHERE m.conversation_id = c.id
) lm ON true`
func scanConversation(row interface{ Scan(...any) error }) (*domain.Conversation, error) {
var c domain.Conversation
if err := row.Scan(&c.ID, &c.UserID, &c.Title, &c.CreatedAt, &c.LastMessageAt); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, domain.ErrNotFound
}
return nil, err
}
return &c, nil
}
type ConversationRepo struct {
DB *pgkit.DB
}
func NewConversationRepo(db *pgkit.DB) *ConversationRepo { return &ConversationRepo{DB: db} }
func (r *ConversationRepo) Create(ctx context.Context, userID int64, title string) (*domain.Conversation, error) {
if title == "" {
title = DefaultConversationTitle
}
var id int64
if err := r.DB.QueryRowContext(ctx,
`INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING id`,
userID, title).Scan(&id); err != nil {
return nil, err
}
return r.Get(ctx, userID, id)
}
func (r *ConversationRepo) Get(ctx context.Context, userID, id int64) (*domain.Conversation, error) {
return scanConversation(r.DB.QueryRowContext(ctx,
conversationSelect+` WHERE c.id = $1 AND c.user_id = $2`, id, userID))
}
func (r *ConversationRepo) List(ctx context.Context, userID int64) ([]*domain.Conversation, error) {
rows, err := r.DB.QueryContext(ctx,
conversationSelect+` WHERE c.user_id = $1 ORDER BY 5 DESC, c.id DESC LIMIT 200`, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var out []*domain.Conversation
for rows.Next() {
c, err := scanConversation(rows)
if err != nil {
return nil, err
}
out = append(out, c)
}
return out, rows.Err()
}
func (r *ConversationRepo) MostRecent(ctx context.Context, userID int64) (*domain.Conversation, error) {
return scanConversation(r.DB.QueryRowContext(ctx,
conversationSelect+` WHERE c.user_id = $1 ORDER BY 5 DESC, c.id DESC LIMIT 1`, userID))
}
func (r *ConversationRepo) Rename(ctx context.Context, userID, id int64, title string) (*domain.Conversation, error) {
res, err := r.DB.ExecContext(ctx,
`UPDATE conversations SET title = $3 WHERE id = $1 AND user_id = $2`, id, userID, title)
if err != nil {
return nil, err
}
if n, _ := res.RowsAffected(); n == 0 {
return nil, domain.ErrNotFound
}
return r.Get(ctx, userID, id)
}
func (r *ConversationRepo) AutoTitle(ctx context.Context, userID, id int64, title string) (bool, error) {
res, err := r.DB.ExecContext(ctx,
`UPDATE conversations SET title = $3 WHERE id = $1 AND user_id = $2 AND title = $4`,
id, userID, title, DefaultConversationTitle)
if err != nil {
return false, err
}
n, _ := res.RowsAffected()
return n > 0, nil
}
func (r *ConversationRepo) Delete(ctx context.Context, userID, id int64) error {
res, err := r.DB.ExecContext(ctx,
`DELETE FROM conversations WHERE id = $1 AND user_id = $2`, id, userID)
if err != nil {
return err
}
if n, _ := res.RowsAffected(); n == 0 {
return domain.ErrNotFound
}
return nil
}
func (r *ConversationRepo) CountMessages(ctx context.Context, conversationID int64) (int, error) {
var n int
err := r.DB.QueryRowContext(ctx,
`SELECT count(*) FROM messages WHERE conversation_id = $1`, conversationID).Scan(&n)
return n, err
}