637 lines
21 KiB
Go
637 lines
21 KiB
Go
package models
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Repository struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewRepository(db *sql.DB) *Repository {
|
|
return &Repository{db: db}
|
|
}
|
|
|
|
// ── Users ──────────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) CreateUser(email, passwordHash string, role Role) (*User, error) {
|
|
u := &User{}
|
|
err := r.db.QueryRow(`
|
|
INSERT INTO users (email, password_hash, role)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, email, password_hash, role, created_at, updated_at`,
|
|
email, passwordHash, role,
|
|
).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt)
|
|
return u, err
|
|
}
|
|
|
|
func (r *Repository) GetUserByEmail(email string) (*User, error) {
|
|
u := &User{}
|
|
err := r.db.QueryRow(`
|
|
SELECT id, email, password_hash, role, created_at, updated_at
|
|
FROM users WHERE email = $1`, email,
|
|
).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return u, err
|
|
}
|
|
|
|
func (r *Repository) GetUserByID(id string) (*User, error) {
|
|
u := &User{}
|
|
err := r.db.QueryRow(`
|
|
SELECT id, email, password_hash, role, created_at, updated_at
|
|
FROM users WHERE id = $1`, id,
|
|
).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return u, err
|
|
}
|
|
|
|
func (r *Repository) ListUsers() ([]User, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT id, email, password_hash, role, created_at, updated_at
|
|
FROM users ORDER BY created_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var users []User
|
|
for rows.Next() {
|
|
var u User
|
|
if err := rows.Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
users = append(users, u)
|
|
}
|
|
return users, nil
|
|
}
|
|
|
|
func (r *Repository) UpdateUser(id, email string, role Role) (*User, error) {
|
|
u := &User{}
|
|
err := r.db.QueryRow(`
|
|
UPDATE users SET email=$1, role=$2, updated_at=NOW()
|
|
WHERE id=$3
|
|
RETURNING id, email, password_hash, role, created_at, updated_at`,
|
|
email, role, id,
|
|
).Scan(&u.ID, &u.Email, &u.PasswordHash, &u.Role, &u.CreatedAt, &u.UpdatedAt)
|
|
return u, err
|
|
}
|
|
|
|
func (r *Repository) DeleteUser(id string) error {
|
|
_, err := r.db.Exec(`DELETE FROM users WHERE id=$1`, id)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) CountAdmins() (int, error) {
|
|
var count int
|
|
err := r.db.QueryRow(`SELECT COUNT(*) FROM users WHERE role='admin'`).Scan(&count)
|
|
return count, err
|
|
}
|
|
|
|
// ── User Assets ────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) GetUserAssets(userID string) ([]UserAsset, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT id, user_id, symbol, name, created_at
|
|
FROM user_assets WHERE user_id=$1 ORDER BY symbol`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var assets []UserAsset
|
|
for rows.Next() {
|
|
var a UserAsset
|
|
if err := rows.Scan(&a.ID, &a.UserID, &a.Symbol, &a.Name, &a.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
assets = append(assets, a)
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
func (r *Repository) AddUserAsset(userID, symbol, name string) (*UserAsset, error) {
|
|
a := &UserAsset{}
|
|
err := r.db.QueryRow(`
|
|
INSERT INTO user_assets (user_id, symbol, name)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (user_id, symbol) DO UPDATE SET name=EXCLUDED.name
|
|
RETURNING id, user_id, symbol, name, created_at`,
|
|
userID, strings.ToUpper(symbol), name,
|
|
).Scan(&a.ID, &a.UserID, &a.Symbol, &a.Name, &a.CreatedAt)
|
|
return a, err
|
|
}
|
|
|
|
func (r *Repository) RemoveUserAsset(userID, symbol string) error {
|
|
_, err := r.db.Exec(`
|
|
DELETE FROM user_assets WHERE user_id=$1 AND symbol=$2`,
|
|
userID, strings.ToUpper(symbol))
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) GetAllWatchedSymbols() ([]string, error) {
|
|
rows, err := r.db.Query(`SELECT DISTINCT symbol FROM user_assets ORDER BY symbol`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var symbols []string
|
|
for rows.Next() {
|
|
var s string
|
|
if err := rows.Scan(&s); err != nil {
|
|
return nil, err
|
|
}
|
|
symbols = append(symbols, s)
|
|
}
|
|
return symbols, nil
|
|
}
|
|
|
|
// ── Sources ────────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) ListSources() ([]Source, error) {
|
|
rows, err := r.db.Query(`SELECT id, name, type, enabled, created_at FROM sources ORDER BY name`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var sources []Source
|
|
for rows.Next() {
|
|
var s Source
|
|
if err := rows.Scan(&s.ID, &s.Name, &s.Type, &s.Enabled, &s.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
sources = append(sources, s)
|
|
}
|
|
return sources, nil
|
|
}
|
|
|
|
func (r *Repository) GetSourceByType(sourceType string) (*Source, error) {
|
|
s := &Source{}
|
|
err := r.db.QueryRow(`
|
|
SELECT id, name, type, enabled, created_at FROM sources WHERE type=$1`, sourceType,
|
|
).Scan(&s.ID, &s.Name, &s.Type, &s.Enabled, &s.CreatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return s, err
|
|
}
|
|
|
|
func (r *Repository) UpdateSource(id string, enabled bool) error {
|
|
_, err := r.db.Exec(`UPDATE sources SET enabled=$1 WHERE id=$2`, enabled, id)
|
|
return err
|
|
}
|
|
|
|
// ── Articles ───────────────────────────────────────────────────────────────
|
|
|
|
// InsertArticleIfNew insère l'article uniquement s'il n'existe pas déjà (par URL).
|
|
// Retourne (article, true, nil) si inséré, (nil, false, nil) si déjà présent.
|
|
func (r *Repository) InsertArticleIfNew(sourceID, title, content, url string, publishedAt *time.Time) (*Article, bool, error) {
|
|
var pa sql.NullTime
|
|
if publishedAt != nil {
|
|
pa = sql.NullTime{Time: *publishedAt, Valid: true}
|
|
}
|
|
a := &Article{}
|
|
err := r.db.QueryRow(`
|
|
INSERT INTO articles (source_id, title, content, url, published_at)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (url) DO NOTHING
|
|
RETURNING id, source_id, title, content, url, published_at, created_at`,
|
|
sourceID, title, content, url, pa,
|
|
).Scan(&a.ID, &a.SourceID, &a.Title, &a.Content, &a.URL, &a.PublishedAt, &a.CreatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, false, nil // déjà présent
|
|
}
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
return a, true, nil
|
|
}
|
|
|
|
func (r *Repository) AddArticleSymbol(articleID, symbol string) error {
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO article_symbols (article_id, symbol)
|
|
VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
|
articleID, strings.ToUpper(symbol))
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListArticles(symbol string, limit, offset int) ([]Article, error) {
|
|
query := `
|
|
SELECT a.id, a.source_id, s.name, a.title, a.content, a.url, a.published_at, a.created_at
|
|
FROM articles a
|
|
JOIN sources s ON s.id = a.source_id`
|
|
args := []interface{}{}
|
|
if symbol != "" {
|
|
query += `
|
|
JOIN article_symbols asy ON asy.article_id = a.id AND asy.symbol = $1`
|
|
args = append(args, strings.ToUpper(symbol))
|
|
}
|
|
query += ` ORDER BY a.published_at DESC NULLS LAST, a.created_at DESC`
|
|
query += fmt.Sprintf(` LIMIT $%d OFFSET $%d`, len(args)+1, len(args)+2)
|
|
args = append(args, limit, offset)
|
|
|
|
rows, err := r.db.Query(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var articles []Article
|
|
for rows.Next() {
|
|
var a Article
|
|
if err := rows.Scan(&a.ID, &a.SourceID, &a.SourceName, &a.Title, &a.Content, &a.URL, &a.PublishedAt, &a.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
articles = append(articles, a)
|
|
}
|
|
return articles, nil
|
|
}
|
|
|
|
func (r *Repository) GetArticleByID(id string) (*Article, error) {
|
|
a := &Article{}
|
|
err := r.db.QueryRow(`
|
|
SELECT a.id, a.source_id, s.name, a.title, a.content, a.url, a.published_at, a.created_at
|
|
FROM articles a JOIN sources s ON s.id=a.source_id
|
|
WHERE a.id=$1`, id,
|
|
).Scan(&a.ID, &a.SourceID, &a.SourceName, &a.Title, &a.Content, &a.URL, &a.PublishedAt, &a.CreatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return a, err
|
|
}
|
|
|
|
func (r *Repository) GetRecentArticles(hours int) ([]Article, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT a.id, a.source_id, s.name, a.title, a.content, a.url, a.published_at, a.created_at
|
|
FROM articles a
|
|
JOIN sources s ON s.id = a.source_id
|
|
WHERE COALESCE(a.published_at, a.created_at) > NOW() - ($1 * INTERVAL '1 hour')
|
|
ORDER BY a.published_at DESC NULLS LAST, a.created_at DESC`, hours)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var articles []Article
|
|
for rows.Next() {
|
|
var a Article
|
|
if err := rows.Scan(&a.ID, &a.SourceID, &a.SourceName, &a.Title, &a.Content, &a.URL, &a.PublishedAt, &a.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
articles = append(articles, a)
|
|
}
|
|
return articles, nil
|
|
}
|
|
|
|
func (r *Repository) GetRecentArticlesForSymbols(symbols []string, hours int) ([]Article, error) {
|
|
if len(symbols) == 0 {
|
|
return nil, nil
|
|
}
|
|
placeholders := make([]string, len(symbols))
|
|
args := []interface{}{hours}
|
|
for i, s := range symbols {
|
|
placeholders[i] = fmt.Sprintf("$%d", i+2)
|
|
args = append(args, strings.ToUpper(s))
|
|
}
|
|
query := fmt.Sprintf(`
|
|
SELECT DISTINCT a.id, a.source_id, s.name, a.title, a.content, a.url, a.published_at, a.created_at
|
|
FROM articles a
|
|
JOIN sources s ON s.id = a.source_id
|
|
JOIN article_symbols asy ON asy.article_id = a.id
|
|
WHERE asy.symbol IN (%s)
|
|
AND a.created_at > NOW() - ($1 * INTERVAL '1 hour')
|
|
ORDER BY a.published_at DESC NULLS LAST, a.created_at DESC`,
|
|
strings.Join(placeholders, ","))
|
|
rows, err := r.db.Query(query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var articles []Article
|
|
for rows.Next() {
|
|
var a Article
|
|
if err := rows.Scan(&a.ID, &a.SourceID, &a.SourceName, &a.Title, &a.Content, &a.URL, &a.PublishedAt, &a.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
articles = append(articles, a)
|
|
}
|
|
return articles, nil
|
|
}
|
|
|
|
// ── Scrape Credentials ─────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) GetCredentials(sourceID string) (*ScrapeCredential, error) {
|
|
c := &ScrapeCredential{}
|
|
err := r.db.QueryRow(`
|
|
SELECT id, source_id, username, password_encrypted, updated_at
|
|
FROM scrape_credentials WHERE source_id=$1`, sourceID,
|
|
).Scan(&c.ID, &c.SourceID, &c.Username, &c.PasswordEncrypted, &c.UpdatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return c, err
|
|
}
|
|
|
|
func (r *Repository) UpsertCredentials(sourceID, username, passwordEncrypted string) error {
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO scrape_credentials (source_id, username, password_encrypted)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (source_id) DO UPDATE
|
|
SET username=EXCLUDED.username, password_encrypted=EXCLUDED.password_encrypted, updated_at=NOW()`,
|
|
sourceID, username, passwordEncrypted)
|
|
return err
|
|
}
|
|
|
|
// ── Scrape Jobs ────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) CreateScrapeJob(sourceID string) (*ScrapeJob, error) {
|
|
j := &ScrapeJob{}
|
|
err := r.db.QueryRow(`
|
|
INSERT INTO scrape_jobs (source_id) VALUES ($1)
|
|
RETURNING id, source_id, status, started_at, finished_at, articles_found, error_msg, created_at`,
|
|
sourceID,
|
|
).Scan(&j.ID, &j.SourceID, &j.Status, &j.StartedAt, &j.FinishedAt, &j.ArticlesFound, &j.ErrorMsg, &j.CreatedAt)
|
|
return j, err
|
|
}
|
|
|
|
func (r *Repository) UpdateScrapeJob(id, status string, articlesFound int, errMsg string) error {
|
|
var finishedAt *time.Time
|
|
if status == "done" || status == "error" {
|
|
now := time.Now()
|
|
finishedAt = &now
|
|
}
|
|
_, err := r.db.Exec(`
|
|
UPDATE scrape_jobs
|
|
SET status=$1, articles_found=$2, error_msg=$3, finished_at=$4,
|
|
started_at=CASE WHEN status='pending' THEN NOW() ELSE started_at END
|
|
WHERE id=$5`,
|
|
status, articlesFound, errMsg, finishedAt, id)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListScrapeJobs(limit int) ([]ScrapeJob, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT j.id, j.source_id, s.name, j.status, j.started_at, j.finished_at,
|
|
j.articles_found, j.error_msg, j.created_at
|
|
FROM scrape_jobs j JOIN sources s ON s.id=j.source_id
|
|
ORDER BY j.created_at DESC LIMIT $1`, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var jobs []ScrapeJob
|
|
for rows.Next() {
|
|
var j ScrapeJob
|
|
if err := rows.Scan(&j.ID, &j.SourceID, &j.SourceName, &j.Status, &j.StartedAt,
|
|
&j.FinishedAt, &j.ArticlesFound, &j.ErrorMsg, &j.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
jobs = append(jobs, j)
|
|
}
|
|
return jobs, nil
|
|
}
|
|
|
|
// ── AI Providers ───────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) ListAIProviders() ([]AIProvider, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT id, name, api_key_encrypted, model, endpoint, is_active, created_at
|
|
FROM ai_providers ORDER BY created_at`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var providers []AIProvider
|
|
for rows.Next() {
|
|
var p AIProvider
|
|
if err := rows.Scan(&p.ID, &p.Name, &p.APIKeyEncrypted, &p.Model, &p.Endpoint, &p.IsActive, &p.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
providers = append(providers, p)
|
|
}
|
|
return providers, nil
|
|
}
|
|
|
|
func (r *Repository) GetAIProviderByID(id string) (*AIProvider, error) {
|
|
p := &AIProvider{}
|
|
err := r.db.QueryRow(`
|
|
SELECT id, name, api_key_encrypted, model, endpoint, is_active, created_at
|
|
FROM ai_providers WHERE id=$1`, id,
|
|
).Scan(&p.ID, &p.Name, &p.APIKeyEncrypted, &p.Model, &p.Endpoint, &p.IsActive, &p.CreatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return p, err
|
|
}
|
|
|
|
func (r *Repository) GetActiveAIProvider() (*AIProvider, error) {
|
|
p := &AIProvider{}
|
|
err := r.db.QueryRow(`
|
|
SELECT id, name, api_key_encrypted, model, endpoint, is_active, created_at
|
|
FROM ai_providers WHERE is_active=TRUE LIMIT 1`,
|
|
).Scan(&p.ID, &p.Name, &p.APIKeyEncrypted, &p.Model, &p.Endpoint, &p.IsActive, &p.CreatedAt)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
return p, err
|
|
}
|
|
|
|
func (r *Repository) CreateAIProvider(name, apiKeyEncrypted, model, endpoint string) (*AIProvider, error) {
|
|
p := &AIProvider{}
|
|
err := r.db.QueryRow(`
|
|
INSERT INTO ai_providers (name, api_key_encrypted, model, endpoint)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, name, api_key_encrypted, model, endpoint, is_active, created_at`,
|
|
name, apiKeyEncrypted, model, endpoint,
|
|
).Scan(&p.ID, &p.Name, &p.APIKeyEncrypted, &p.Model, &p.Endpoint, &p.IsActive, &p.CreatedAt)
|
|
return p, err
|
|
}
|
|
|
|
func (r *Repository) UpdateAIProvider(id, apiKeyEncrypted, model, endpoint string) error {
|
|
_, err := r.db.Exec(`
|
|
UPDATE ai_providers SET api_key_encrypted=$1, model=$2, endpoint=$3 WHERE id=$4`,
|
|
apiKeyEncrypted, model, endpoint, id)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) SetActiveAIProvider(id string) error {
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if _, err := tx.Exec(`UPDATE ai_providers SET is_active=FALSE`); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(`UPDATE ai_providers SET is_active=TRUE WHERE id=$1`, id); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (r *Repository) DeleteAIProvider(id string) error {
|
|
_, err := r.db.Exec(`DELETE FROM ai_providers WHERE id=$1`, id)
|
|
return err
|
|
}
|
|
|
|
// ── Summaries ──────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) CreateSummary(userID, content string, providerID *string) (*Summary, error) {
|
|
s := &Summary{}
|
|
err := r.db.QueryRow(`
|
|
INSERT INTO summaries (user_id, content, ai_provider_id)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, user_id, content, ai_provider_id, generated_at`,
|
|
userID, content, providerID,
|
|
).Scan(&s.ID, &s.UserID, &s.Content, &s.AIProviderID, &s.GeneratedAt)
|
|
return s, err
|
|
}
|
|
|
|
func (r *Repository) ListSummaries(userID string, limit int) ([]Summary, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT id, user_id, content, ai_provider_id, generated_at
|
|
FROM summaries WHERE user_id=$1
|
|
ORDER BY generated_at DESC LIMIT $2`, userID, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var summaries []Summary
|
|
for rows.Next() {
|
|
var s Summary
|
|
if err := rows.Scan(&s.ID, &s.UserID, &s.Content, &s.AIProviderID, &s.GeneratedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
summaries = append(summaries, s)
|
|
}
|
|
return summaries, nil
|
|
}
|
|
|
|
// ── Settings ───────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) GetSetting(key string) (string, error) {
|
|
var value string
|
|
err := r.db.QueryRow(`SELECT value FROM settings WHERE key=$1`, key).Scan(&value)
|
|
if err == sql.ErrNoRows {
|
|
return "", nil
|
|
}
|
|
return value, err
|
|
}
|
|
|
|
func (r *Repository) SetSetting(key, value string) error {
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO settings (key, value) VALUES ($1, $2)
|
|
ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value`,
|
|
key, value)
|
|
return err
|
|
}
|
|
|
|
// ── Schedule ───────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) ListScheduleSlots() ([]ScheduleSlot, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT id, day_of_week, hour, minute FROM scrape_schedules
|
|
ORDER BY day_of_week, hour, minute`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var slots []ScheduleSlot
|
|
for rows.Next() {
|
|
var s ScheduleSlot
|
|
if err := rows.Scan(&s.ID, &s.DayOfWeek, &s.Hour, &s.Minute); err != nil {
|
|
return nil, err
|
|
}
|
|
slots = append(slots, s)
|
|
}
|
|
return slots, nil
|
|
}
|
|
|
|
func (r *Repository) ReplaceSchedule(slots []ScheduleSlot) error {
|
|
tx, err := r.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
if _, err := tx.Exec(`DELETE FROM scrape_schedules`); err != nil {
|
|
return err
|
|
}
|
|
for _, s := range slots {
|
|
if _, err := tx.Exec(
|
|
`INSERT INTO scrape_schedules (day_of_week, hour, minute) VALUES ($1, $2, $3)
|
|
ON CONFLICT (day_of_week, hour, minute) DO NOTHING`,
|
|
s.DayOfWeek, s.Hour, s.Minute,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// ── Settings ───────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) ListSettings() ([]Setting, error) {
|
|
rows, err := r.db.Query(`SELECT key, value FROM settings ORDER BY key`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var settings []Setting
|
|
for rows.Next() {
|
|
var s Setting
|
|
if err := rows.Scan(&s.Key, &s.Value); err != nil {
|
|
return nil, err
|
|
}
|
|
settings = append(settings, s)
|
|
}
|
|
return settings, nil
|
|
}
|
|
|
|
// ── Reports ────────────────────────────────────────────────────────────────
|
|
|
|
func (r *Repository) CreatePendingReport(userID string, summaryID *string, excerpt, question string) (*Report, error) {
|
|
rep := &Report{}
|
|
err := r.db.QueryRow(`
|
|
INSERT INTO reports (user_id, summary_id, context_excerpt, question, answer, status)
|
|
VALUES ($1, $2, $3, $4, '', 'generating')
|
|
RETURNING id, user_id, summary_id, context_excerpt, question, answer, status, error_msg, created_at`,
|
|
userID, summaryID, excerpt, question,
|
|
).Scan(&rep.ID, &rep.UserID, &rep.SummaryID, &rep.ContextExcerpt, &rep.Question, &rep.Answer, &rep.Status, &rep.ErrorMsg, &rep.CreatedAt)
|
|
return rep, err
|
|
}
|
|
|
|
func (r *Repository) UpdateReport(id, status, answer, errorMsg string) error {
|
|
_, err := r.db.Exec(`
|
|
UPDATE reports SET status=$1, answer=$2, error_msg=$3 WHERE id=$4`,
|
|
status, answer, errorMsg, id)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListReports(userID string) ([]Report, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT id, user_id, summary_id, context_excerpt, question, answer, status, error_msg, created_at
|
|
FROM reports WHERE user_id=$1
|
|
ORDER BY created_at DESC`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var reports []Report
|
|
for rows.Next() {
|
|
var rep Report
|
|
if err := rows.Scan(&rep.ID, &rep.UserID, &rep.SummaryID, &rep.ContextExcerpt, &rep.Question, &rep.Answer, &rep.Status, &rep.ErrorMsg, &rep.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
reports = append(reports, rep)
|
|
}
|
|
return reports, nil
|
|
}
|
|
|
|
func (r *Repository) DeleteReport(id, userID string) error {
|
|
_, err := r.db.Exec(`DELETE FROM reports WHERE id=$1 AND user_id=$2`, id, userID)
|
|
return err
|
|
}
|