feat: settings service — key-value store with upsert
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
62133d8966
commit
4de47352cd
41
internal/settings/service.go
Normal file
41
internal/settings/service.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
|
type SettingsService struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSettingsService(db *sql.DB) *SettingsService {
|
||||||
|
return &SettingsService{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) Get(key string) (string, error) {
|
||||||
|
var value string
|
||||||
|
err := s.db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) GetDefault(key, defaultValue string) string {
|
||||||
|
val, err := s.Get(key)
|
||||||
|
if err != nil || val == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) Set(key, value string) error {
|
||||||
|
_, err := s.db.Exec(
|
||||||
|
"INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = ?",
|
||||||
|
key, value, value,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsService) Delete(key string) error {
|
||||||
|
_, err := s.db.Exec("DELETE FROM settings WHERE key = ?", key)
|
||||||
|
return err
|
||||||
|
}
|
||||||
70
internal/settings/service_test.go
Normal file
70
internal/settings/service_test.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
package settings
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/vstockwell/wraith/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTestDB(t *testing.T) *SettingsService {
|
||||||
|
t.Helper()
|
||||||
|
d, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := db.Migrate(d); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { d.Close() })
|
||||||
|
return NewSettingsService(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetAndGet(t *testing.T) {
|
||||||
|
s := setupTestDB(t)
|
||||||
|
|
||||||
|
if err := s.Set("theme", "dracula"); err != nil {
|
||||||
|
t.Fatalf("Set() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := s.Get("theme")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get() error: %v", err)
|
||||||
|
}
|
||||||
|
if val != "dracula" {
|
||||||
|
t.Errorf("Get() = %q, want %q", val, "dracula")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMissing(t *testing.T) {
|
||||||
|
s := setupTestDB(t)
|
||||||
|
|
||||||
|
val, err := s.Get("nonexistent")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Get() error: %v", err)
|
||||||
|
}
|
||||||
|
if val != "" {
|
||||||
|
t.Errorf("Get() = %q, want empty string", val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetOverwrites(t *testing.T) {
|
||||||
|
s := setupTestDB(t)
|
||||||
|
|
||||||
|
s.Set("key", "value1")
|
||||||
|
s.Set("key", "value2")
|
||||||
|
|
||||||
|
val, _ := s.Get("key")
|
||||||
|
if val != "value2" {
|
||||||
|
t.Errorf("Get() = %q, want %q", val, "value2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWithDefault(t *testing.T) {
|
||||||
|
s := setupTestDB(t)
|
||||||
|
|
||||||
|
val := s.GetDefault("missing", "fallback")
|
||||||
|
if val != "fallback" {
|
||||||
|
t.Errorf("GetDefault() = %q, want %q", val, "fallback")
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user