From 8d2384139116847b1142810b48ed649cd5c07de8 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Mon, 23 Mar 2026 17:35:52 -0500 Subject: [PATCH] Pull Proclamations & Categories from API and store them in the database --- internal/application/application.go | 14 +++--- internal/db/newDb.go | 2 +- internal/db/schema.go | 7 ++- internal/netReq/netReq.go | 26 ----------- internal/netReq/netReq_test.go | 19 -------- internal/netreq/netReq.go | 54 +++++++++++++++++++++++ internal/netreq/netReq_test.go | 26 +++++++++++ internal/sync/sync.go | 67 +++++++++++++++++++++++++++++ 8 files changed, 161 insertions(+), 54 deletions(-) delete mode 100644 internal/netReq/netReq.go delete mode 100644 internal/netReq/netReq_test.go create mode 100644 internal/netreq/netReq.go create mode 100644 internal/netreq/netReq_test.go create mode 100644 internal/sync/sync.go diff --git a/internal/application/application.go b/internal/application/application.go index 13885ca..bb49cb1 100644 --- a/internal/application/application.go +++ b/internal/application/application.go @@ -20,6 +20,7 @@ import ( "rhemawavetech.com/rhemawave/internal/mixer" "rhemawavetech.com/rhemawave/internal/page" "rhemawavetech.com/rhemawave/internal/shaderManager" + "rhemawavetech.com/rhemawave/internal/sync" ) const ( @@ -27,9 +28,7 @@ const ( volumeStepSize = 16 openGLMajorVersion = 3 openGLMinorVersion = 3 - apiURL = "http://localhost:8000" - apiPath = "/api/v1/licenseKey/validate" - audioFileURL = "https://files.neilclack.com/audio.wav" + apiURL = "http://localhost:5173/api/v1" ) type Application struct { @@ -56,7 +55,6 @@ func Run() { var result *gorm.DB logger.Info("Starting Application") - logger.Info("Finding $HOME") homeDir, err = os.UserHomeDir() if err != nil { @@ -64,7 +62,6 @@ func Run() { return } - logger.Info("Constructing DataDir Path") dataDir = filepath.Join(homeDir, ".local", "share", "rhemawave") if err := os.MkdirAll(dataDir, 0755); err != nil { logger.Error("unable to create data directory %s %v", dataDir, err) @@ -82,8 +79,6 @@ func Run() { logger.AddFile(filepath.Join(dataDir, "log.txt")) - logger.Info("Connecting to database") - DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite")) if err != nil { logger.Error("application failed to connect to the database: %v", err) @@ -141,6 +136,11 @@ func Run() { H: h, RefreshConfig: app.refreshCache, }) + + // Sync the proclamations from the API to the Database + sync.SyncProclamations(apiURL, DB) + sync.SyncCategories(apiURL, DB) + app.runEventLoop() } diff --git a/internal/db/newDb.go b/internal/db/newDb.go index 185ff06..e535cd4 100644 --- a/internal/db/newDb.go +++ b/internal/db/newDb.go @@ -41,7 +41,7 @@ func Connect(dbpath string) (*gorm.DB, error) { // Migrate the database schema func Migrate(db *gorm.DB) error { // Pass all models to be migrated to this function call. - if err := db.AutoMigrate(&AppConfig{}, &Log{}, &Proclamation{}); err != nil { + if err := db.AutoMigrate(&AppConfig{}, &Log{}, &Proclamation{}, &ProclamationCategory{}); err != nil { return err } return nil diff --git a/internal/db/schema.go b/internal/db/schema.go index e10515d..83231cf 100644 --- a/internal/db/schema.go +++ b/internal/db/schema.go @@ -27,7 +27,6 @@ type Log struct { } type Proclamation struct { - gorm.Model ID string `gorm:"primaryKey"` Name string Content string @@ -37,3 +36,9 @@ type Proclamation struct { Available bool Price float64 } + +type ProclamationCategory struct { + ID int `gorm:"primaryKey"` + Name string + ParentId int +} diff --git a/internal/netReq/netReq.go b/internal/netReq/netReq.go deleted file mode 100644 index a01adfa..0000000 --- a/internal/netReq/netReq.go +++ /dev/null @@ -1,26 +0,0 @@ -package netreq - -import ( - "fmt" - "io" - "net/http" -) - -// Makes a GET request to the specified URL and returns the JSON response -// as a byte slice. If the request fails, it returns an error. -func GetJSON(url string) ([]byte, error) { - fmt.Println(url) - res, err := http.Get(url) - if err != nil { - return nil, err - } - body, err := io.ReadAll(res.Body) - res.Body.Close() - if res.StatusCode > 299 { - return nil, fmt.Errorf("request failed with status code %d: %s", res.StatusCode, string(body)) - } - if err != nil { - return nil, err - } - return body, nil -} diff --git a/internal/netReq/netReq_test.go b/internal/netReq/netReq_test.go deleted file mode 100644 index ecebb23..0000000 --- a/internal/netReq/netReq_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package netreq - -import ( - "fmt" - "testing" -) - -func TestGet(t *testing.T) { - url := "http://localhost:5173/api/v1/proclamations" - body, err := GetJSON(url) - if err != nil { - t.Fatalf("GetJSON failed: %v", err) - } - if len(body) == 0 { - t.Fatal("GetJSON returned empty body") - } - cnt := string(body) - fmt.Printf("%s\n", cnt) -} diff --git a/internal/netreq/netReq.go b/internal/netreq/netReq.go new file mode 100644 index 0000000..17e0331 --- /dev/null +++ b/internal/netreq/netReq.go @@ -0,0 +1,54 @@ +package netreq + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// Makes a GET request to the specified URL and returns the JSON response +// as a byte slice. If the request fails, it returns an error. +func GetJSON[T any](url string) ([]T, error) { + fmt.Println("Fetching:", url) + res, err := http.Get(url) + if err != nil { + return nil, err + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if res.StatusCode > 299 { + return nil, fmt.Errorf("status %d: %s", res.StatusCode, string(body)) + } + + // 1. Trim whitespace to find the first actual JSON character + trimmed := bytes.TrimSpace(body) + if len(trimmed) == 0 { + return nil, nil + } + + var result []T + + // 2. Determine if we are looking at an Array [ ] or an Object { } + if trimmed[0] == '[' { + // It's already an array, unmarshal directly + err = json.Unmarshal(body, &result) + } else { + // It's a single object, unmarshal into a single instance and wrap it + var single T + err = json.Unmarshal(body, &single) + result = []T{single} + } + + if err != nil { + return nil, fmt.Errorf("failed to parse JSON: %w", err) + } + + return result, nil +} diff --git a/internal/netreq/netReq_test.go b/internal/netreq/netReq_test.go new file mode 100644 index 0000000..6f2ee35 --- /dev/null +++ b/internal/netreq/netReq_test.go @@ -0,0 +1,26 @@ +package netreq + +import ( + "fmt" + "testing" + + "rhemawavetech.com/rhemawave/internal/db" +) + +func TestGet(t *testing.T) { + url := "http://localhost:5173/api/v1/proclamations" + + // Explicitly tell GetJSON to return a slice of db.Proclamation + proclamations, err := GetJSON[db.Proclamation](url) + + if err != nil { + t.Fatalf("GetJSON failed: %v", err) + } + + if len(proclamations) == 0 { + t.Fatal("GetJSON returned no records") + } + + // You can now inspect the actual struct fields instead of raw strings + fmt.Printf("Fetched %d records. First ID: %s\n", len(proclamations), proclamations[0].ID) +} diff --git a/internal/sync/sync.go b/internal/sync/sync.go new file mode 100644 index 0000000..c32b53d --- /dev/null +++ b/internal/sync/sync.go @@ -0,0 +1,67 @@ +package sync + +import ( + "fmt" + "regexp" + + "gorm.io/gorm" + "rhemawavetech.com/rhemawave/internal/db" + "rhemawavetech.com/rhemawave/internal/logger" + "rhemawavetech.com/rhemawave/internal/netreq" +) + +var htmlRegex = regexp.MustCompile(`<[^>]*>`) + +// SyncProclamations fetches data from the API, cleans it, and updates the local DB. +// It returns the slice of proclamations so the UI can update immediately. +func SyncProclamations(apiURL string, database *gorm.DB) ([]db.Proclamation, error) { + fullURL := fmt.Sprintf("%s/proclamations", apiURL) + + // 1. Fetch from API + proclamations, err := netreq.GetJSON[db.Proclamation](fullURL) + if err != nil { + // If we're offline or API is down, we log it but don't crash. + // The UI can still use whatever is currently in the DB. + logger.Error("Proclamation sync failed, skipping update: %v", err) + return nil, err + } + + // 2. Data Transformation (Cleaning) + for i := range proclamations { + proclamations[i].Content = htmlRegex.ReplaceAllString(proclamations[i].Content, "") + } + + // 3. Persist to Database + // Using Transaction ensures we don't end up with partial data if something fails + err = database.Transaction(func(tx *gorm.DB) error { + if err := tx.Save(&proclamations).Error; err != nil { + return err + } + return nil + }) + + if err != nil { + logger.Error("Failed to save synced proclamations: %v", err) + return nil, err + } + + logger.Info("Successfully synced proclamations: %d", len(proclamations)) + return proclamations, nil +} + +func SyncCategories(apiURL string, database *gorm.DB) ([]db.ProclamationCategory, error) { + fullURL := fmt.Sprintf("%s/categories", apiURL) + categories, err := netreq.GetJSON[db.ProclamationCategory](fullURL) + if err != nil { + logger.Error("Category sync failed, skipping update: %v", err) + return nil, err + } + err = database.Transaction(func(tx *gorm.DB) error { + if err := tx.Save(&categories).Error; err != nil { + return err + } + return nil + }) + logger.Info("Successfully synced proclamation categories: %d", len(categories)) + return categories, nil +}