NextWks/src/core/db/db.go

119 lines
2.8 KiB
Go

package db
import (
"database/sql"
"fmt"
"os"
"path/filepath"
_ "modernc.org/sqlite"
)
// Database wraps the SQLite connection and provides migration helpers.
type Database struct {
DB *sql.DB
}
// Initialize opens (or creates) the SQLite database at the given path.
func Initialize(dbPath string) (*Database, error) {
// Ensure the data directory exists
dir := filepath.Dir(dbPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("create data directory: %w", err)
}
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
// Enable WAL mode for better concurrency
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
return nil, fmt.Errorf("enable WAL mode: %w", err)
}
// Enable foreign keys
if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
return nil, fmt.Errorf("enable foreign keys: %w", err)
}
return &Database{DB: db}, nil
}
// Migrate runs automatic schema migrations on startup.
func (d *Database) Migrate() error {
migrations := []string{
`users`,
`sessions`,
`audit_logs`,
}
// Verify all required tables exist
for _, table := range migrations {
if err := d.ensureTable(table); err != nil {
return fmt.Errorf("ensure table %s: %w", table, err)
}
}
return nil
}
func (d *Database) ensureTable(name string) error {
switch name {
case "users":
// Create table if it doesn't exist
_, err := d.DB.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL DEFAULT 'user',
groups TEXT NOT NULL DEFAULT '',
password_hash TEXT NOT NULL,
disabled INTEGER NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return err
}
// Migrate: add role column if missing (for existing databases)
d.DB.Exec(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'`)
return nil
case "sessions":
_, err := d.DB.Exec(`
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
expires_at DATETIME NOT NULL
)
`)
return err
case "audit_logs":
_, err := d.DB.Exec(`
CREATE TABLE IF NOT EXISTS audit_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
action TEXT NOT NULL,
actor TEXT NOT NULL,
target TEXT,
details TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
return err
}
return fmt.Errorf("unknown table: %s", name)
}
// Close cleanly shuts down the database connection.
func (d *Database) Close() error {
return d.DB.Close()
}