2022-12-02 20:40:58 -07:00
package database
import (
"context"
"encoding/json"
"fmt"
sourceModel "github.com/fastenhealth/fasten-sources/clients/models"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"gorm.io/datatypes"
"gorm.io/gorm"
"net/url"
"strings"
)
func NewRepository ( appConfig config . Interface , globalLogger logrus . FieldLogger ) ( DatabaseRepository , error ) {
//backgroundContext := context.Background()
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Gorm/SQLite setup
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
globalLogger . Infof ( "Trying to connect to sqlite db: %s\n" , appConfig . GetString ( "database.location" ) )
// When a transaction cannot lock the database, because it is already locked by another one,
// SQLite by default throws an error: database is locked. This behavior is usually not appropriate when
// concurrent access is needed, typically when multiple processes write to the same database.
// PRAGMA busy_timeout lets you set a timeout or a handler for these events. When setting a timeout,
// SQLite will try the transaction multiple times within this timeout.
// fixes #341
// https://rsqlite.r-dbi.org/reference/sqlitesetbusyhandler
// retrying for 30000 milliseconds, 30seconds - this would be unreasonable for a distributed multi-tenant application,
// but should be fine for local usage.
pragmaStr := sqlitePragmaString ( map [ string ] string {
"busy_timeout" : "30000" ,
"foreign_keys" : "ON" ,
} )
database , err := gorm . Open ( sqlite . Open ( appConfig . GetString ( "database.location" ) + pragmaStr ) , & gorm . Config {
//TODO: figure out how to log database queries again.
2022-12-03 13:48:35 -07:00
//Logger: Logger
2022-12-02 20:40:58 -07:00
DisableForeignKeyConstraintWhenMigrating : true ,
} )
if strings . ToUpper ( appConfig . GetString ( "log.level" ) ) == "DEBUG" {
database = database . Debug ( ) //set debug globally
}
if err != nil {
return nil , fmt . Errorf ( "Failed to connect to database! - %v" , err )
}
globalLogger . Infof ( "Successfully connected to fasten sqlite db: %s\n" , appConfig . GetString ( "database.location" ) )
2022-12-03 13:48:35 -07:00
deviceRepo := SqliteRepository {
AppConfig : appConfig ,
Logger : globalLogger ,
GormClient : database ,
}
2022-12-02 20:40:58 -07:00
//TODO: automigrate for now
2022-12-03 13:48:35 -07:00
err = deviceRepo . Migrate ( )
2022-12-02 20:40:58 -07:00
if err != nil {
2022-12-03 13:48:35 -07:00
return nil , err
2022-12-02 20:40:58 -07:00
}
// create/update admin user
adminUser := models . User { }
err = database . FirstOrCreate ( & adminUser , models . User { Username : "admin" } ) . Error
if err != nil {
return nil , fmt . Errorf ( "Failed to create admin user! - %v" , err )
}
return & deviceRepo , nil
}
2022-12-03 12:08:14 -07:00
type SqliteRepository struct {
2022-12-03 13:48:35 -07:00
AppConfig config . Interface
Logger logrus . FieldLogger
2022-12-02 20:40:58 -07:00
2022-12-03 13:48:35 -07:00
GormClient * gorm . DB
}
func ( sr * SqliteRepository ) Migrate ( ) error {
err := sr . GormClient . AutoMigrate (
& models . User { } ,
& models . SourceCredential { } ,
& models . ResourceFhir { } ,
)
if err != nil {
return fmt . Errorf ( "Failed to automigrate! - %v" , err )
}
return nil
2022-12-02 20:40:58 -07:00
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) Close ( ) error {
2022-12-02 20:40:58 -07:00
return nil
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// User
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) CreateUser ( ctx context . Context , user * models . User ) error {
2022-12-02 20:40:58 -07:00
if err := user . HashPassword ( user . Password ) ; err != nil {
return err
}
2022-12-03 13:48:35 -07:00
record := sr . GormClient . Create ( user )
2022-12-02 20:40:58 -07:00
if record . Error != nil {
return record . Error
}
return nil
}
2022-12-03 13:48:35 -07:00
func ( sr * SqliteRepository ) GetUserByUsername ( ctx context . Context , username string ) ( * models . User , error ) {
2022-12-02 20:40:58 -07:00
var foundUser models . User
2022-12-03 13:48:35 -07:00
result := sr . GormClient . Where ( models . User { Username : username } ) . First ( & foundUser )
2022-12-02 20:40:58 -07:00
return & foundUser , result . Error
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetCurrentUser ( ctx context . Context ) * models . User {
2022-12-02 20:40:58 -07:00
ginCtx := ctx . ( * gin . Context )
var currentUser models . User
2022-12-03 13:48:35 -07:00
sr . GormClient . First ( & currentUser , models . User { Username : ginCtx . MustGet ( "AUTH_USERNAME" ) . ( string ) } )
2022-12-02 20:40:58 -07:00
return & currentUser
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// User
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetSummary ( ctx context . Context ) ( * models . Summary , error ) {
2022-12-02 20:40:58 -07:00
// we want a count of all resources for this user by type
var resourceCountResults [ ] map [ string ] interface { }
//group by resource type and return counts
// SELECT source_resource_type as resource_type, COUNT(*) as count FROM resource_fhirs WHERE source_id = "53c1e930-63af-46c9-b760-8e83cbc1abd9" GROUP BY source_resource_type;
2022-12-03 13:48:35 -07:00
result := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Model ( models . ResourceFhir { } ) .
Select ( "source_id, source_resource_type as resource_type, count(*) as count" ) .
Group ( "source_resource_type" ) .
Where ( models . OriginBase {
UserID : sr . GetCurrentUser ( ctx ) . ID ,
} ) .
Scan ( & resourceCountResults )
if result . Error != nil {
return nil , result . Error
}
// we want a list of all sources (when they were last updated)
sources , err := sr . GetSources ( ctx )
if err != nil {
return nil , err
}
// we want the main Patient for each source
patients , err := sr . GetPatientForSources ( ctx )
if err != nil {
return nil , err
}
if resourceCountResults == nil {
resourceCountResults = [ ] map [ string ] interface { } { }
}
summary := & models . Summary {
Sources : sources ,
ResourceTypeCounts : resourceCountResults ,
Patients : patients ,
}
return summary , nil
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Resource
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) UpsertRawResource ( ctx context . Context , sourceCredential sourceModel . SourceCredential , rawResource sourceModel . RawResourceFhir ) ( bool , error ) {
2022-12-02 20:40:58 -07:00
source := sourceCredential . ( models . SourceCredential )
wrappedResourceModel := & models . ResourceFhir {
OriginBase : models . OriginBase {
ModelBase : models . ModelBase { } ,
UserID : source . UserID ,
SourceID : source . ID ,
SourceResourceID : rawResource . SourceResourceID ,
SourceResourceType : rawResource . SourceResourceType ,
} ,
ResourceRaw : datatypes . JSON ( rawResource . ResourceRaw ) ,
}
2022-12-03 13:48:35 -07:00
sr . Logger . Infof ( "insert/update (%v) %v" , rawResource . SourceResourceType , rawResource . SourceResourceID )
2022-12-02 20:40:58 -07:00
2022-12-03 13:48:35 -07:00
createResult := sr . GormClient . WithContext ( ctx ) . Where ( models . OriginBase {
2022-12-02 20:40:58 -07:00
SourceID : wrappedResourceModel . GetSourceID ( ) ,
SourceResourceID : wrappedResourceModel . GetSourceResourceID ( ) ,
SourceResourceType : wrappedResourceModel . GetSourceResourceType ( ) , //TODO: and UpdatedAt > old UpdatedAt
} ) . FirstOrCreate ( wrappedResourceModel )
if createResult . Error != nil {
return false , createResult . Error
} else if createResult . RowsAffected == 0 {
//at this point, wrappedResourceModel contains the data found in the database.
// check if the database resource matches the new resource.
if wrappedResourceModel . ResourceRaw . String ( ) != string ( rawResource . ResourceRaw ) {
updateResult := createResult . Updates ( wrappedResourceModel )
return updateResult . RowsAffected > 0 , updateResult . Error
} else {
return false , nil
}
} else {
//resource was created
return createResult . RowsAffected > 0 , createResult . Error
}
//return results.RowsAffected > 0, results.Error
2022-12-03 13:48:35 -07:00
//if sr.GormClient.Debug().WithContext(ctx).
2022-12-02 20:40:58 -07:00
// Where(models.OriginBase{
// SourceID: wrappedResourceModel.GetSourceID(),
// SourceResourceID: wrappedResourceModel.GetSourceResourceID(),
// SourceResourceType: wrappedResourceModel.GetSourceResourceType(), //TODO: and UpdatedAt > old UpdatedAt
// }).Updates(wrappedResourceModel).RowsAffected == 0 {
2022-12-03 13:48:35 -07:00
// sr.Logger.Infof("resource does not exist, creating: %s %s %s", wrappedResourceModel.GetSourceID(), wrappedResourceModel.GetSourceResourceID(), wrappedResourceModel.GetSourceResourceType())
// return sr.GormClient.Debug().Create(wrappedResourceModel).Error
2022-12-02 20:40:58 -07:00
//}
//return nil
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) UpsertResource ( ctx context . Context , resourceModel * models . ResourceFhir ) error {
2022-12-03 13:48:35 -07:00
sr . Logger . Infof ( "insert/update (%T) %v" , resourceModel , resourceModel )
2022-12-02 20:40:58 -07:00
2022-12-03 13:48:35 -07:00
if sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( models . OriginBase {
SourceID : resourceModel . GetSourceID ( ) ,
SourceResourceID : resourceModel . GetSourceResourceID ( ) ,
SourceResourceType : resourceModel . GetSourceResourceType ( ) , //TODO: and UpdatedAt > old UpdatedAt
} ) . Updates ( resourceModel ) . RowsAffected == 0 {
2022-12-03 13:48:35 -07:00
sr . Logger . Infof ( "resource does not exist, creating: %s %s %s" , resourceModel . GetSourceID ( ) , resourceModel . GetSourceResourceID ( ) , resourceModel . GetSourceResourceType ( ) )
return sr . GormClient . Create ( resourceModel ) . Error
2022-12-02 20:40:58 -07:00
}
return nil
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) ListResources ( ctx context . Context , queryOptions models . ListResourceQueryOptions ) ( [ ] models . ResourceFhir , error ) {
2022-12-02 20:40:58 -07:00
queryParam := models . ResourceFhir {
OriginBase : models . OriginBase {
UserID : sr . GetCurrentUser ( ctx ) . ID ,
} ,
}
if len ( queryOptions . SourceResourceType ) > 0 {
queryParam . OriginBase . SourceResourceType = queryOptions . SourceResourceType
}
if len ( queryOptions . SourceID ) > 0 {
sourceUUID , err := uuid . Parse ( queryOptions . SourceID )
if err != nil {
return nil , err
}
queryParam . OriginBase . SourceID = sourceUUID
}
manifestJson , _ := json . MarshalIndent ( queryParam , "" , " " )
2022-12-03 13:48:35 -07:00
sr . Logger . Infof ( "THE QUERY OBJECT===========> %v" , string ( manifestJson ) )
2022-12-02 20:40:58 -07:00
var wrappedResourceModels [ ] models . ResourceFhir
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( queryParam ) .
Find ( & wrappedResourceModels )
return wrappedResourceModels , results . Error
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetResourceBySourceType ( ctx context . Context , sourceResourceType string , sourceResourceId string ) ( * models . ResourceFhir , error ) {
2022-12-02 20:40:58 -07:00
queryParam := models . ResourceFhir {
OriginBase : models . OriginBase {
UserID : sr . GetCurrentUser ( ctx ) . ID ,
SourceResourceType : sourceResourceType ,
SourceResourceID : sourceResourceId ,
} ,
}
var wrappedResourceModel models . ResourceFhir
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( queryParam ) .
First ( & wrappedResourceModel )
return & wrappedResourceModel , results . Error
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetResourceBySourceId ( ctx context . Context , sourceId string , sourceResourceId string ) ( * models . ResourceFhir , error ) {
2022-12-02 20:40:58 -07:00
sourceIdUUID , err := uuid . Parse ( sourceId )
if err != nil {
return nil , err
}
queryParam := models . ResourceFhir {
OriginBase : models . OriginBase {
UserID : sr . GetCurrentUser ( ctx ) . ID ,
SourceID : sourceIdUUID ,
SourceResourceID : sourceResourceId ,
} ,
}
var wrappedResourceModel models . ResourceFhir
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( queryParam ) .
First ( & wrappedResourceModel )
return & wrappedResourceModel , results . Error
}
// Get the patient for each source (for the current user)
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetPatientForSources ( ctx context . Context ) ( [ ] models . ResourceFhir , error ) {
2022-12-02 20:40:58 -07:00
//SELECT * FROM resource_fhirs WHERE user_id = "" and source_resource_type = "Patient" GROUP BY source_id
//var sourceCred models.SourceCredential
2022-12-03 13:48:35 -07:00
//results := sr.GormClient.WithContext(ctx).
2022-12-02 20:40:58 -07:00
// Where(models.SourceCredential{UserID: sr.GetCurrentUser(ctx).ID, ModelBase: models.ModelBase{ID: sourceUUID}}).
// First(&sourceCred)
var wrappedResourceModels [ ] models . ResourceFhir
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Model ( models . ResourceFhir { } ) .
Group ( "source_id" ) .
Where ( models . OriginBase {
UserID : sr . GetCurrentUser ( ctx ) . ID ,
SourceResourceType : "Patient" ,
} ) .
Find ( & wrappedResourceModels )
return wrappedResourceModels , results . Error
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SourceCredential
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) CreateSource ( ctx context . Context , sourceCreds * models . SourceCredential ) error {
2022-12-02 20:40:58 -07:00
sourceCreds . UserID = sr . GetCurrentUser ( ctx ) . ID
//Assign will **always** update the source credential in the DB with data passed into this function.
2022-12-03 13:48:35 -07:00
return sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( models . SourceCredential {
UserID : sourceCreds . UserID ,
SourceType : sourceCreds . SourceType ,
Patient : sourceCreds . Patient } ) .
Assign ( * sourceCreds ) . FirstOrCreate ( sourceCreds ) . Error
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetSource ( ctx context . Context , sourceId string ) ( * models . SourceCredential , error ) {
2022-12-02 20:40:58 -07:00
sourceUUID , err := uuid . Parse ( sourceId )
if err != nil {
return nil , err
}
var sourceCred models . SourceCredential
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( models . SourceCredential { UserID : sr . GetCurrentUser ( ctx ) . ID , ModelBase : models . ModelBase { ID : sourceUUID } } ) .
First ( & sourceCred )
return & sourceCred , results . Error
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetSourceSummary ( ctx context . Context , sourceId string ) ( * models . SourceSummary , error ) {
2022-12-02 20:40:58 -07:00
sourceUUID , err := uuid . Parse ( sourceId )
if err != nil {
return nil , err
}
sourceSummary := & models . SourceSummary { }
source , err := sr . GetSource ( ctx , sourceId )
if err != nil {
return nil , err
}
sourceSummary . Source = source
//group by resource type and return counts
// SELECT source_resource_type as resource_type, COUNT(*) as count FROM resource_fhirs WHERE source_id = "53c1e930-63af-46c9-b760-8e83cbc1abd9" GROUP BY source_resource_type;
var resourceTypeCounts [ ] map [ string ] interface { }
2022-12-03 13:48:35 -07:00
result := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Model ( models . ResourceFhir { } ) .
Select ( "source_id, source_resource_type as resource_type, count(*) as count" ) .
Group ( "source_resource_type" ) .
Where ( models . OriginBase {
UserID : sr . GetCurrentUser ( ctx ) . ID ,
SourceID : sourceUUID ,
} ) .
Scan ( & resourceTypeCounts )
if result . Error != nil {
return nil , result . Error
}
sourceSummary . ResourceTypeCounts = resourceTypeCounts
//set patient
var wrappedPatientResourceModel models . ResourceFhir
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( models . OriginBase {
UserID : sr . GetCurrentUser ( ctx ) . ID ,
SourceResourceType : "Patient" ,
SourceID : sourceUUID ,
} ) .
First ( & wrappedPatientResourceModel )
if results . Error != nil {
return nil , result . Error
}
sourceSummary . Patient = & wrappedPatientResourceModel
return sourceSummary , nil
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetSources ( ctx context . Context ) ( [ ] models . SourceCredential , error ) {
2022-12-02 20:40:58 -07:00
var sourceCreds [ ] models . SourceCredential
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( models . SourceCredential { UserID : sr . GetCurrentUser ( ctx ) . ID } ) .
Find ( & sourceCreds )
return sourceCreds , results . Error
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utilities
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func sqlitePragmaString ( pragmas map [ string ] string ) string {
q := url . Values { }
for key , val := range pragmas {
q . Add ( "_pragma" , key + "=" + val )
}
queryStr := q . Encode ( )
if len ( queryStr ) > 0 {
return "?" + queryStr
}
return ""
}