Refactor backend database connection, migration, seed

This commit is contained in:
2025-10-17 12:41:58 +02:00
parent 176032e54b
commit 560693633e
9 changed files with 163 additions and 99 deletions

View File

@@ -2,16 +2,19 @@ package main
import (
"database/sql"
"embed"
"fmt"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/pressly/goose/v3"
_ "modernc.org/sqlite"
)
//go:embed migrations/*.sql
var embedMigrations embed.FS
// Create new sqlite database
// If filepath parameter is empty
func NewDatabaseConnection(dataDir string, dbName string) (*sql.DB, error) {
@@ -35,21 +38,47 @@ func NewDatabaseConnection(dataDir string, dbName string) (*sql.DB, error) {
return db, nil
}
// database
//
// Manage database connection to ./app.db
// Handles CRUD
func database(db *sql.DB) *sql.DB {
if db == nil {
slog.Error("No connection to the database!")
func MigrateDatabase(db *sql.DB) {
dialect := "sqlite3"
goose.SetBaseFS(embedMigrations)
if err := goose.SetDialect(dialect); err != nil {
slog.Error("Database dialect error", "dialect", dialect, "error", err)
os.Exit(1)
}
// Seed default menu items if empty
row := db.QueryRow("SELECT COUNT(*) FROM menu_items")
if err := goose.Up(db, "migrations"); err != nil {
slog.Error("Database migration error", "error", err)
os.Exit(1)
}
migratedVersion, err := goose.GetDBVersion(db)
if err != nil {
slog.Error("Datatabase migration version check error", "error", err)
os.Exit(1)
}
slog.Info("Database up to date", "version", migratedVersion)
}
func SeedDatabase(db *sql.DB, superadminPassword string) error {
seededValue := "false"
row := db.QueryRow("SELECT value FROM settings WHERE key='seeded';")
_ = row.Scan(&seededValue)
if seededValue == "true" {
slog.Info("Database already seeded with default data, skipping")
return nil
}
slog.Info("Database empty, seeding with default values")
row = db.QueryRow("SELECT COUNT(*) FROM menu_items")
var count int
_ = row.Scan(&count)
if count == 0 {
log.Println("Seeding default menu_items")
slog.Info("Seeding default menu_items")
defaults := []struct {
category string
name string
@@ -85,88 +114,32 @@ func database(db *sql.DB) *sql.DB {
for _, d := range defaults {
_, err := db.Exec("INSERT INTO menu_items (category, name) VALUES (?, ?)", d.category, d.name)
if err != nil {
log.Println("Error seeding menu item:", err)
return fmt.Errorf("Error seeding menu_items table: %w", err)
}
}
} else {
slog.Info("Table menu_items already seeded, skipping")
}
// Create users table
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role INTEGER NOT NULL DEFAULT 2
)
`)
_, err := db.Exec(`INSERT OR REPLACE INTO settings (key, value) VALUES ('seeded', 'true')`)
if err != nil {
log.Fatal(err)
return fmt.Errorf("Error saving setting table: %w", err)
}
// Create settings table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
`)
if err != nil {
log.Fatal(err)
}
updateSuperadminPassword(db, superadminPassword)
// Seed default finalize_time if missing
_, err = db.Exec(`INSERT OR IGNORE INTO settings (key, value) VALUES ('finalize_time', '10:30')`)
if err != nil {
log.Fatal(err)
}
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
name TEXT NOT NULL
)
`)
if err != nil {
log.Fatal(err)
}
// Create selections table (latest choice per user)
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS selections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
main TEXT NOT NULL,
side TEXT NOT NULL,
soup TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
`)
if err != nil {
log.Fatal(err)
}
// Create orders table (all placed orders for history + realtime updates)
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
soup TEXT,
main TEXT NOT NULL,
side TEXT NOT NULL,
created_at TEXT NOT NULL
)
`)
if err != nil {
log.Fatal(err)
}
// Add status column if it doesn't exist
_, err = db.Exec(`ALTER TABLE orders ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`)
if err != nil && !strings.Contains(err.Error(), "duplicate column name") {
log.Fatal(err)
}
return db
return nil
}
func updateSuperadminPassword(db *sql.DB, superadminPassword string) {
_, err := db.Exec(`INSERT OR REPLACE INTO users (id, username, password, role)
VALUES (
1,
'superadmin',
?,
COALESCE((SELECT role FROM users WHERE id = 1), '0')
)`, superadminPassword)
if err != nil {
slog.Error("Error setting superadmin password", "error", err)
}
}

View File

@@ -11,11 +11,19 @@ require (
modernc.org/sqlite v1.39.0
)
require (
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.16.0 // indirect
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pressly/goose/v3 v3.26.0
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/sys v0.34.0 // indirect

View File

@@ -14,16 +14,24 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM=
github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=

View File

@@ -23,14 +23,8 @@ func main() {
slog.Warn("Cannot find .env file, using system env variables")
}
adminUser := os.Getenv("ADMIN_USER")
adminPass := os.Getenv("ADMIN_PASS")
if adminUser == "" {
slog.Error("Missing ADMIN_USER env variable")
os.Exit(1)
}
if adminPass == "" {
slog.Error("Missing ADMIN_PASS env variable")
os.Exit(1)
@@ -49,11 +43,17 @@ func main() {
slog.Error("Databse error", "error", err)
os.Exit(1)
}
db := database(dbConnection)
defer db.Close()
// Migrate database
MigrateDatabase(dbConnection)
SeedDatabase(dbConnection, adminPass)
updateSuperadminPassword(dbConnection, adminPass)
// db := database(dbConnection)
defer dbConnection.Close()
// Create App
app := handlers.NewApp(db, handlers.NewBroker())
app := handlers.NewApp(dbConnection, handlers.NewBroker())
// Create server
srv := NewServer(app, "0.0.0.0:7153", "*")

View File

@@ -0,0 +1,14 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
role INTEGER NOT NULL DEFAULT 2
)
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS users;
-- +goose StatementEnd

View File

@@ -0,0 +1,14 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT INTO settings (key, value) VALUES ('finalize_time', '10:30');
INSERT INTO settings (key, value) VALUES ('seeded', 'false');
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS settings;
-- +goose StatementEnd

View File

@@ -0,0 +1,13 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL,
name TEXT NOT NULL
)
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS menu_items;
-- +goose StatementEnd

View File

@@ -0,0 +1,17 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS selections (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
main TEXT NOT NULL,
side TEXT NOT NULL,
soup TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id)
)
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS selections;
-- +goose StatementEnd

View File

@@ -0,0 +1,17 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
soup TEXT,
main TEXT NOT NULL,
side TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT NOT NULL
)
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE IF EXISTS orders;
-- +goose StatementEnd