From 560693633e082252f1e0477eb065453bb4dbb699 Mon Sep 17 00:00:00 2001 From: Tamas Gal Date: Fri, 17 Oct 2025 12:41:58 +0200 Subject: [PATCH] Refactor backend database connection, migration, seed --- backend/db.go | 149 +++++++----------- backend/go.mod | 8 + backend/go.sum | 12 +- backend/main.go | 18 +-- ...0251017064937_00001_create_users_table.sql | 14 ++ ...1017065315_00002_create_settings_table.sql | 14 ++ ...17065447_00003_create_menu_items_table.sql | 13 ++ ...17065539_00004_create_selections_table.sql | 17 ++ ...251017065648_00005_create_orders_table.sql | 17 ++ 9 files changed, 163 insertions(+), 99 deletions(-) create mode 100644 backend/migrations/20251017064937_00001_create_users_table.sql create mode 100644 backend/migrations/20251017065315_00002_create_settings_table.sql create mode 100644 backend/migrations/20251017065447_00003_create_menu_items_table.sql create mode 100644 backend/migrations/20251017065539_00004_create_selections_table.sql create mode 100644 backend/migrations/20251017065648_00005_create_orders_table.sql diff --git a/backend/db.go b/backend/db.go index 0bc912a..d121de6 100644 --- a/backend/db.go +++ b/backend/db.go @@ -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) + } } diff --git a/backend/go.mod b/backend/go.mod index 2454ef1..545d8d4 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -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 diff --git a/backend/go.sum b/backend/go.sum index 41e20da..ca8a701 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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= diff --git a/backend/main.go b/backend/main.go index e10f01b..222a796 100644 --- a/backend/main.go +++ b/backend/main.go @@ -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", "*") diff --git a/backend/migrations/20251017064937_00001_create_users_table.sql b/backend/migrations/20251017064937_00001_create_users_table.sql new file mode 100644 index 0000000..77853d9 --- /dev/null +++ b/backend/migrations/20251017064937_00001_create_users_table.sql @@ -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 diff --git a/backend/migrations/20251017065315_00002_create_settings_table.sql b/backend/migrations/20251017065315_00002_create_settings_table.sql new file mode 100644 index 0000000..5af0581 --- /dev/null +++ b/backend/migrations/20251017065315_00002_create_settings_table.sql @@ -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 diff --git a/backend/migrations/20251017065447_00003_create_menu_items_table.sql b/backend/migrations/20251017065447_00003_create_menu_items_table.sql new file mode 100644 index 0000000..1f55220 --- /dev/null +++ b/backend/migrations/20251017065447_00003_create_menu_items_table.sql @@ -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 diff --git a/backend/migrations/20251017065539_00004_create_selections_table.sql b/backend/migrations/20251017065539_00004_create_selections_table.sql new file mode 100644 index 0000000..93e4797 --- /dev/null +++ b/backend/migrations/20251017065539_00004_create_selections_table.sql @@ -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 diff --git a/backend/migrations/20251017065648_00005_create_orders_table.sql b/backend/migrations/20251017065648_00005_create_orders_table.sql new file mode 100644 index 0000000..1f60989 --- /dev/null +++ b/backend/migrations/20251017065648_00005_create_orders_table.sql @@ -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