Skip to content

Commit

Permalink
Adding new system settings table. (#409)
Browse files Browse the repository at this point in the history
* Adding new system settings table.
Adding associated database migration.
Added tests
Fixing tests for user-settings.

* updated migration, make sure default system settings are created.
Added database commands to create and update system settings.
Added generic response wrapper.

* make sure we can get related versions (fasten sources, onprem and desktop) when starting the app.
  • Loading branch information
AnalogJ committed Feb 9, 2024
1 parent dc97048 commit c85e829
Show file tree
Hide file tree
Showing 22 changed files with 560 additions and 20 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@ fasten-*.db-wal
fasten.db
fasten.db-shm
fasten.db-wal

backend/resources/related_versions.json
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ clean-backend:
.PHONY: dep-backend
dep-backend:
go mod tidy && go mod vendor
cd scripts && go generate ./...


.PHONY: test-backend
Expand Down
10 changes: 7 additions & 3 deletions backend/cmd/fasten/fasten.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/fastenhealth/fasten-onprem/backend/pkg/event_bus"
"github.com/fastenhealth/fasten-onprem/backend/pkg/version"
"github.com/fastenhealth/fasten-onprem/backend/pkg/web"
"github.com/fastenhealth/fasten-onprem/backend/resources"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"io"
Expand Down Expand Up @@ -114,10 +115,13 @@ func main() {
settingsData, err := json.Marshal(appconfig.AllSettings())
appLogger.Debug(string(settingsData), err)

relatedVersions, _ := resources.GetRelatedVersions()

webServer := web.AppEngine{
Config: appconfig,
Logger: appLogger,
EventBus: event_bus.NewEventBusServer(appLogger),
Config: appconfig,
Logger: appLogger,
EventBus: event_bus.NewEventBusServer(appLogger),
RelatedVersions: relatedVersions,
}
return webServer.Start()
},
Expand Down
9 changes: 9 additions & 0 deletions backend/pkg/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ type BackgroundJobSchedule string

type DatabaseRepositoryType string

type InstallationVerificationStatus string
type InstallationQuotaStatus string

const (
ResourceListPageSize int = 20

Expand Down Expand Up @@ -41,4 +44,10 @@ const (

DatabaseRepositoryTypeSqlite DatabaseRepositoryType = "sqlite"
DatabaseRepositoryTypePostgres DatabaseRepositoryType = "postgres"

InstallationVerificationStatusMissing InstallationVerificationStatus = "MISSING" //email is missing for this installation
InstallationVerificationStatusPending InstallationVerificationStatus = "PENDING" //email has not been verified
InstallationVerificationStatusVerified InstallationVerificationStatus = "VERIFIED" //email has been verified
InstallationQuotaStatusActive InstallationQuotaStatus = "ACTIVE"
InstallationQuotaStatusConsumed InstallationQuotaStatus = "CONSUMED"
)
40 changes: 40 additions & 0 deletions backend/pkg/database/gorm_repository_migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
_20231201122541 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20231201122541"
_0240114092806 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114092806"
_20240114103850 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240114103850"
_20240208112210 "github.com/fastenhealth/fasten-onprem/backend/pkg/database/migrations/20240208112210"
databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database"
sourceCatalog "github.com/fastenhealth/fasten-sources/catalog"
sourcePkg "github.com/fastenhealth/fasten-sources/pkg"
Expand Down Expand Up @@ -141,6 +142,45 @@ func (gr *GormRepository) Migrate() error {
)
},
},
{
ID: "20240208112210", // add system settings
Migrate: func(tx *gorm.DB) error {

err := tx.AutoMigrate(
&_20240208112210.SystemSettingEntry{},
)
if err != nil {
return err
}

//add the default system settings
defaultSystemSettings := []_20240208112210.SystemSettingEntry{
{
SettingKeyName: "installation_id",
SettingKeyDescription: "installation id is used to identify this installation when making external calls to Fasten Health, Inc. infrastructure. It does not contain any personally identifiable information",
SettingDataType: "string",
SettingValueString: "",
},
{
SettingKeyName: "installation_secret",
SettingKeyDescription: "installation secret is used to sign requests/updates to Fasten Health, Inc. infrastructure",
SettingDataType: "string",
SettingValueString: "",
},
}

for _, setting := range defaultSystemSettings {
tx.Logger.Info(context.Background(), fmt.Sprintf("Creating System Setting: %s", setting.SettingKeyName))

settingCreateResp := tx.Create(&setting)
if settingCreateResp.Error != nil {
tx.Logger.Error(context.Background(), fmt.Sprintf("An error occurred creating System Setting: %s", setting.SettingKeyName))
return settingCreateResp.Error
}
}
return nil
},
},
})

// run when database is empty
Expand Down
68 changes: 67 additions & 1 deletion backend/pkg/database/gorm_repository_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,77 @@ package database
import (
"context"
"fmt"

"github.com/fastenhealth/fasten-onprem/backend/pkg/models"
"github.com/google/uuid"
)

// LoadSystemSettings will retrieve settings from the database, and return a SystemSettings struct
func (gr *GormRepository) LoadSystemSettings(ctx context.Context) (*models.SystemSettings, error) {

settingsEntries := []models.SystemSettingEntry{}
if err := gr.GormClient.
WithContext(ctx).
Find(&settingsEntries).Error; err != nil {
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
}

settings := models.SystemSettings{}
for _, settingsEntry := range settingsEntries {
err := settings.FromSystemSettingsEntry(&settingsEntry)
if err != nil {
return nil, fmt.Errorf("Could not get settings from DB: %v", err)
}
}

return &settings, nil
}

// testing
// SaveSystemSettings will update save the settings to the database.
func (gr *GormRepository) SaveSystemSettings(ctx context.Context, newSettings *models.SystemSettings) error {

//retrieve current settings from the database
currentSettingsEntries := []models.SystemSettingEntry{}

if err := gr.GormClient.
WithContext(ctx).
Find(&currentSettingsEntries).Error; err != nil {
return fmt.Errorf("Could not get settings from DB: %v", err)
}

//update settingsEntries

newSettingsEntries, err := newSettings.ToSystemSettingsEntry(currentSettingsEntries)
if err != nil {
return fmt.Errorf("merge new settings with DB: %v", err)
}

for ndx, settingsEntry := range newSettingsEntries {

var upsertErr error
if settingsEntry.ID == uuid.Nil {
//create new entry
upsertErr = gr.GormClient.
WithContext(ctx).
Model(&models.SystemSettingEntry{}).
Create(&settingsEntry).Error
} else {
// store in database.
upsertErr = gr.GormClient.
WithContext(ctx).
Model(&models.SystemSettingEntry{}).
Where([]uuid.UUID{settingsEntry.ID}).
Select("setting_value_numeric", "setting_value_string", "setting_value_bool", "setting_value_array").
Updates(newSettingsEntries[ndx]).Error
}

if upsertErr != nil {
return err
}
}
return nil
}

// LoadSettings will retrieve settings from the database, store them in the AppConfig object, and return a Settings struct
func (gr *GormRepository) LoadUserSettings(ctx context.Context) (*models.UserSettings, error) {
currentUser, currentUserErr := gr.GetCurrentUser(ctx)
Expand Down
2 changes: 2 additions & 0 deletions backend/pkg/database/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ type DatabaseRepository interface {
ListBackgroundJobs(ctx context.Context, queryOptions models.BackgroundJobQueryOptions) ([]models.BackgroundJob, error)

//settings
LoadSystemSettings(ctx context.Context) (*models.SystemSettings, error)
SaveSystemSettings(ctx context.Context, newSettings *models.SystemSettings) error
LoadUserSettings(ctx context.Context) (*models.UserSettings, error)
SaveUserSettings(context.Context, *models.UserSettings) error
PopulateDefaultUserSettings(ctx context.Context, userId uuid.UUID) error
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package _20240208112210

import "github.com/fastenhealth/fasten-onprem/backend/pkg/models"

// SystemSettingEntry matches a setting row in the database
type SystemSettingEntry struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
models.ModelBase

SettingKeyName string `json:"setting_key_name" gorm:"not null;index:,unique,composite:system_setting_key_name"`
SettingKeyDescription string `json:"setting_key_description"`
SettingDataType string `json:"setting_data_type"`

SettingValueNumeric int `json:"setting_value_numeric"`
SettingValueString string `json:"setting_value_string"`
SettingValueBool bool `json:"setting_value_bool"`
SettingValueArray []string `json:"setting_value_array" gorm:"column:setting_value_array;type:text;serializer:json"`
}

func (s SystemSettingEntry) TableName() string {
return "system_settings"
}
13 changes: 13 additions & 0 deletions backend/pkg/models/installation_registration_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package models

type InstallationRegistrationRequest struct {
// AdministratorEmail specifies email address for the administrator of the installation
AdministratorEmail string `json:"administrator_email,omitempty"` //opt-in

SoftwareArchitecture string `json:"software_architecture,omitempty"`
SoftwareOS string `json:"software_os,omitempty"`

FastenDesktopVersion string `json:"fasten_desktop_version,omitempty"`
FastenOnpremVersion string `json:"fasten_onprem_version,omitempty"`
FastenSourcesVersion string `json:"fasten_sources_version,omitempty"`
}
22 changes: 22 additions & 0 deletions backend/pkg/models/installation_registration_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package models

import (
"github.com/fastenhealth/fasten-onprem/backend/pkg"
"time"
)

type InstallationRegistrationResponse struct {
// InstallationID specifies client identifier string. REQUIRED
InstallationID string `json:"installation_id"`

// InstallationSecret specifies client secret string. OPTIONAL
InstallationSecret string `json:"installation_secret"`

// InstallationIDIssuedAt specifies time at which the client identifier was issued. OPTIONAL
InstallationIDIssuedAt time.Time `json:"installation_id_issued_at"`

VerificationStatus pkg.InstallationVerificationStatus `json:"verification_status"`
QuotaStatus pkg.InstallationQuotaStatus `json:"quota_status"`

*InstallationRegistrationRequest `json:",inline"`
}
6 changes: 6 additions & 0 deletions backend/pkg/models/response_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ type ResponseWrapper struct {
Error string `json:"error"`
Data interface{} `json:"data"`
}

type ResponseWrapperTyped[T any] struct {
Success bool `json:"success"`
Error string `json:"error"`
Data T `json:"data"`
}
20 changes: 20 additions & 0 deletions backend/pkg/models/system_setting_entry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package models

// SystemSettingEntry matches a setting row in the database
type SystemSettingEntry struct {
//GORM attributes, see: http://gorm.io/docs/conventions.html
ModelBase

SettingKeyName string `json:"setting_key_name" gorm:"not null;index:,unique,composite:system_setting_key_name"`
SettingKeyDescription string `json:"setting_key_description"`
SettingDataType string `json:"setting_data_type"`

SettingValueNumeric int `json:"setting_value_numeric"`
SettingValueString string `json:"setting_value_string"`
SettingValueBool bool `json:"setting_value_bool"`
SettingValueArray []string `json:"setting_value_array" gorm:"column:setting_value_array;type:text;serializer:json"`
}

func (s SystemSettingEntry) TableName() string {
return "system_settings"
}
70 changes: 70 additions & 0 deletions backend/pkg/models/system_settings.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package models

import (
"reflect"
)

type SystemSettings struct {
InstallationID string `json:"installation_id"`
InstallationSecret string `json:"installation_secret"`
}

// see https://gist.github.com/lelandbatey/a5c957b537bed39d1d6fb202c3b8de06
func (s *SystemSettings) FromSystemSettingsEntry(entry *SystemSettingEntry) error {

structType := reflect.ValueOf(s).Elem()

for i := 0; i < structType.NumField(); i++ {
typeField := structType.Type().Field(i)

if jsonTagValue := typeField.Tag.Get("json"); jsonTagValue == entry.SettingKeyName {
//fmt.Println("found field", field.Name)
if entry.SettingDataType == "numeric" {
structType.Field(i).SetInt(int64(entry.SettingValueNumeric))
} else if entry.SettingDataType == "string" {
structType.Field(i).SetString(entry.SettingValueString)
} else if entry.SettingDataType == "bool" {
structType.Field(i).SetBool(entry.SettingValueBool)
} else if entry.SettingDataType == "array" {
structType.Field(i).Set(reflect.ValueOf(entry.SettingValueArray))
}
break
}
}

//if entry.SettingKeyName == "dashboard_locations" {
// s.DashboardLocations = entry.SettingValueArray
//}
return nil
}

func (s *SystemSettings) ToSystemSettingsEntry(entries []SystemSettingEntry) ([]SystemSettingEntry, error) {

structType := reflect.ValueOf(s).Elem()

fieldNameNdxLookup := map[string]int{}

for i := 0; i < structType.NumField(); i++ {
typeField := structType.Type().Field(i)
jsonTagValue := typeField.Tag.Get("json")
fieldNameNdxLookup[jsonTagValue] = i
}

for ndx, entry := range entries {
fieldId := fieldNameNdxLookup[entry.SettingKeyName]

if entry.SettingDataType == "numeric" {
entries[ndx].SettingValueNumeric = int(structType.Field(fieldId).Int())
} else if entry.SettingDataType == "string" {
entries[ndx].SettingValueString = structType.Field(fieldId).String()
} else if entry.SettingDataType == "bool" {
entries[ndx].SettingValueBool = structType.Field(fieldId).Bool()
} else if entry.SettingDataType == "array" {
sliceVal := structType.Field(fieldId).Slice(0, structType.Field(fieldId).Len())

entries[ndx].SettingValueArray = sliceVal.Interface().([]string)
}
}

return entries, nil
}

0 comments on commit c85e829

Please sign in to comment.