Pull Proclamations & Categories from API and store them in the database
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
|||||||
"rhemawavetech.com/rhemawave/internal/mixer"
|
"rhemawavetech.com/rhemawave/internal/mixer"
|
||||||
"rhemawavetech.com/rhemawave/internal/page"
|
"rhemawavetech.com/rhemawave/internal/page"
|
||||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||||
|
"rhemawavetech.com/rhemawave/internal/sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -27,9 +28,7 @@ const (
|
|||||||
volumeStepSize = 16
|
volumeStepSize = 16
|
||||||
openGLMajorVersion = 3
|
openGLMajorVersion = 3
|
||||||
openGLMinorVersion = 3
|
openGLMinorVersion = 3
|
||||||
apiURL = "http://localhost:8000"
|
apiURL = "http://localhost:5173/api/v1"
|
||||||
apiPath = "/api/v1/licenseKey/validate"
|
|
||||||
audioFileURL = "https://files.neilclack.com/audio.wav"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Application struct {
|
type Application struct {
|
||||||
@@ -56,7 +55,6 @@ func Run() {
|
|||||||
var result *gorm.DB
|
var result *gorm.DB
|
||||||
|
|
||||||
logger.Info("Starting Application")
|
logger.Info("Starting Application")
|
||||||
logger.Info("Finding $HOME")
|
|
||||||
|
|
||||||
homeDir, err = os.UserHomeDir()
|
homeDir, err = os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,7 +62,6 @@ func Run() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Constructing DataDir Path")
|
|
||||||
dataDir = filepath.Join(homeDir, ".local", "share", "rhemawave")
|
dataDir = filepath.Join(homeDir, ".local", "share", "rhemawave")
|
||||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
logger.Error("unable to create data directory %s %v", dataDir, err)
|
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.AddFile(filepath.Join(dataDir, "log.txt"))
|
||||||
|
|
||||||
logger.Info("Connecting to database")
|
|
||||||
|
|
||||||
DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite"))
|
DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("application failed to connect to the database: %v", err)
|
logger.Error("application failed to connect to the database: %v", err)
|
||||||
@@ -141,6 +136,11 @@ func Run() {
|
|||||||
H: h,
|
H: h,
|
||||||
RefreshConfig: app.refreshCache,
|
RefreshConfig: app.refreshCache,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sync the proclamations from the API to the Database
|
||||||
|
sync.SyncProclamations(apiURL, DB)
|
||||||
|
sync.SyncCategories(apiURL, DB)
|
||||||
|
|
||||||
app.runEventLoop()
|
app.runEventLoop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func Connect(dbpath string) (*gorm.DB, error) {
|
|||||||
// Migrate the database schema
|
// Migrate the database schema
|
||||||
func Migrate(db *gorm.DB) error {
|
func Migrate(db *gorm.DB) error {
|
||||||
// Pass all models to be migrated to this function call.
|
// 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 err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ type Log struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Proclamation struct {
|
type Proclamation struct {
|
||||||
gorm.Model
|
|
||||||
ID string `gorm:"primaryKey"`
|
ID string `gorm:"primaryKey"`
|
||||||
Name string
|
Name string
|
||||||
Content string
|
Content string
|
||||||
@@ -37,3 +36,9 @@ type Proclamation struct {
|
|||||||
Available bool
|
Available bool
|
||||||
Price float64
|
Price float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProclamationCategory struct {
|
||||||
|
ID int `gorm:"primaryKey"`
|
||||||
|
Name string
|
||||||
|
ParentId int
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user