feat: add frontend + backend + database to retrieve and compute news from Yahoo
This commit is contained in:
538
backend/internal/models/repository.go
Normal file
538
backend/internal/models/repository.go
Normal file
@ -0,0 +1,538 @@
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
func (r *Repository) UpsertArticle(sourceID, title, content, url string, publishedAt *time.Time) (*Article, error) {
|
||||
a := &Article{}
|
||||
var pa sql.NullTime
|
||||
if publishedAt != nil {
|
||||
pa = sql.NullTime{Time: *publishedAt, Valid: true}
|
||||
}
|
||||
err := r.db.QueryRow(`
|
||||
INSERT INTO articles (source_id, title, content, url, published_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (url) DO UPDATE SET title=EXCLUDED.title, content=EXCLUDED.content
|
||||
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)
|
||||
return a, err
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user