2022-12-02 20:40:58 -07:00
package database
import (
"context"
"encoding/json"
2023-07-08 08:43:30 -06:00
"errors"
2022-12-02 20:40:58 -07:00
"fmt"
sourceModel "github.com/fastenhealth/fasten-sources/clients/models"
2023-01-10 20:23:47 -07:00
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg"
2022-12-02 20:40:58 -07:00
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/config"
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models"
2023-07-08 08:43:30 -06:00
databaseModel "github.com/fastenhealth/fastenhealth-onprem/backend/pkg/models/database"
2023-01-10 20:23:47 -07:00
"github.com/fastenhealth/fastenhealth-onprem/backend/pkg/utils"
2022-12-02 20:40:58 -07:00
"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" ) )
2023-03-21 09:04:43 -06:00
fastenRepo := SqliteRepository {
2022-12-03 13:48:35 -07:00
AppConfig : appConfig ,
Logger : globalLogger ,
GormClient : database ,
}
2023-07-08 08:43:30 -06:00
//TODO: automigrate for now, this should be replaced with a migration tool once the DB has stabilized.
2023-03-21 09:04:43 -06:00
err = fastenRepo . 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
}
2023-07-08 08:43:30 -06:00
//automigrate Fhir Resource Tables
err = databaseModel . Migrate ( fastenRepo . GormClient )
if err != nil {
return nil , err
}
2022-12-02 20:40:58 -07:00
// create/update admin user
2023-07-08 08:43:30 -06:00
//TODO: determine if this admin user is ncessary
//SECURITY: validate this user is necessary
2022-12-02 20:40:58 -07:00
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 )
}
2023-03-21 09:04:43 -06:00
return & fastenRepo , nil
2022-12-02 20:40:58 -07:00
}
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 { } ,
2023-03-21 09:04:43 -06:00
& models . Glossary { } ,
2022-12-03 13:48:35 -07:00
)
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
2023-07-08 08:43:30 -06:00
result := sr . GormClient . WithContext ( ctx ) . Where ( models . User { Username : username } ) . First ( & foundUser )
2022-12-02 20:40:58 -07:00
return & foundUser , result . Error
}
2023-01-15 12:07:41 -07:00
//TODO: check for error, right now we return a nil which may cause a panic.
func ( sr * SqliteRepository ) GetCurrentUser ( ctx context . Context ) ( * models . User , error ) {
2023-01-10 20:23:47 -07:00
username := ctx . Value ( pkg . ContextKeyTypeAuthUsername )
2022-12-03 20:16:32 -07:00
if username == nil {
2023-07-08 08:43:30 -06:00
ginCtx , ginCtxOk := ctx . ( * gin . Context )
if ! ginCtxOk {
return nil , fmt . Errorf ( "could not convert context to gin context" )
}
var exists bool
username , exists = ginCtx . Get ( pkg . ContextKeyTypeAuthUsername )
if ! exists {
return nil , fmt . Errorf ( "could not extract username from context" )
}
2022-12-03 20:16:32 -07:00
}
2022-12-02 20:40:58 -07:00
var currentUser models . User
2023-07-08 08:43:30 -06:00
usernameStr , usernameStrOk := username . ( string )
if ! usernameStrOk {
return nil , fmt . Errorf ( "could not convert username to string: %v" , username )
}
result := sr . GormClient .
WithContext ( ctx ) .
First ( & currentUser , map [ string ] interface { } { "username" : usernameStr } )
2022-12-02 20:40:58 -07:00
2023-01-15 12:07:41 -07:00
if result . Error != nil {
return nil , fmt . Errorf ( "could not retrieve current user: %v" , result . Error )
}
return & currentUser , nil
2022-12-02 20:40:58 -07:00
}
2023-03-21 09:04:43 -06:00
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Glossary
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func ( sr * SqliteRepository ) CreateGlossaryEntry ( ctx context . Context , glossaryEntry * models . Glossary ) error {
2023-07-08 08:43:30 -06:00
record := sr . GormClient . WithContext ( ctx ) . Create ( glossaryEntry )
2023-03-21 09:04:43 -06:00
if record . Error != nil {
return record . Error
}
return nil
}
func ( sr * SqliteRepository ) GetGlossaryEntry ( ctx context . Context , code string , codeSystem string ) ( * models . Glossary , error ) {
var foundGlossaryEntry models . Glossary
2023-07-08 08:43:30 -06:00
result := sr . GormClient . WithContext ( ctx ) .
Where ( models . Glossary { Code : code , CodeSystem : codeSystem } ) .
First ( & foundGlossaryEntry )
2023-03-21 09:04:43 -06:00
return & foundGlossaryEntry , result . Error
}
2022-12-02 20:40:58 -07:00
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2023-01-10 20:23:47 -07:00
// Summary
2022-12-02 20:40:58 -07:00
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetSummary ( ctx context . Context ) ( * models . Summary , error ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
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 { }
2023-07-08 08:43:30 -06:00
resourceTypes := databaseModel . GetAllowedResourceTypes ( )
for _ , resourceType := range resourceTypes {
tableName , err := databaseModel . GetTableNameByResourceType ( resourceType )
if err != nil {
return nil , err
}
var count int64
result := sr . GormClient . WithContext ( ctx ) .
Table ( tableName ) .
Where ( models . OriginBase {
UserID : currentUser . ID ,
} ) .
Count ( & count )
if result . Error != nil {
return nil , result . Error
}
if count == 0 {
continue //don't add resource counts if the count is 0
}
resourceCountResults = append ( resourceCountResults , map [ string ] interface { } {
"resource_type" : resourceType ,
"count" : count ,
} )
2022-12-02 20:40:58 -07:00
}
// 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
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2023-07-08 08:43:30 -06:00
//This function will create a new resource if it does not exist, or update an existing resource if it does exist.
//It will also create associations between fhir resources
2023-08-17 12:14:33 -06:00
//This function is called directly by fasten-sources
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
2023-07-14 20:52:02 -06:00
source := sourceCredential . ( * models . SourceCredential )
2022-12-02 20:40:58 -07:00
2023-07-08 08:43:30 -06:00
//convert from a raw resource (from fasten-sources) to a ResourceFhir (which matches the database models)
wrappedResourceModel := & models . ResourceBase {
2022-12-02 20:40:58 -07:00
OriginBase : models . OriginBase {
ModelBase : models . ModelBase { } ,
UserID : source . UserID ,
SourceID : source . ID ,
SourceResourceID : rawResource . SourceResourceID ,
SourceResourceType : rawResource . SourceResourceType ,
} ,
2023-07-08 08:43:30 -06:00
SortTitle : rawResource . SortTitle ,
SortDate : rawResource . SortDate ,
ResourceRaw : datatypes . JSON ( rawResource . ResourceRaw ) ,
RelatedResource : nil ,
2022-12-17 16:10:19 -07:00
}
2023-08-17 12:14:33 -06:00
if len ( rawResource . SourceUri ) > 0 {
wrappedResourceModel . SourceUri = & rawResource . SourceUri
}
2022-12-17 16:10:19 -07:00
//create associations
//note: we create the association in the related_resources table **before** the model actually exists.
2023-04-22 23:08:58 -06:00
//note: these associations are not reciprocal, (i.e. if Procedure references Location, Location may not reference Procedure)
2023-01-10 20:23:47 -07:00
if rawResource . ReferencedResources != nil && len ( rawResource . ReferencedResources ) > 0 {
2022-12-17 16:10:19 -07:00
for _ , referencedResource := range rawResource . ReferencedResources {
parts := strings . Split ( referencedResource , "/" )
2022-12-21 20:51:02 -07:00
if len ( parts ) != 2 {
continue
}
2022-12-17 16:10:19 -07:00
2023-07-08 08:43:30 -06:00
relatedResource := & models . ResourceBase {
2022-12-21 20:51:02 -07:00
OriginBase : models . OriginBase {
SourceID : source . ID ,
SourceResourceType : parts [ 0 ] ,
SourceResourceID : parts [ 1 ] ,
} ,
2023-07-08 08:43:30 -06:00
RelatedResource : nil ,
2022-12-21 20:51:02 -07:00
}
2023-04-22 23:08:58 -06:00
err := sr . AddResourceAssociation (
ctx ,
2023-07-14 20:52:02 -06:00
source ,
2023-04-22 23:08:58 -06:00
wrappedResourceModel . SourceResourceType ,
wrappedResourceModel . SourceResourceID ,
2023-07-14 20:52:02 -06:00
source ,
2023-04-22 23:08:58 -06:00
relatedResource . SourceResourceType ,
relatedResource . SourceResourceID ,
)
2022-12-21 20:51:02 -07:00
if err != nil {
2023-07-08 08:43:30 -06:00
return false , err
2022-12-17 16:10:19 -07:00
}
}
2022-12-02 20:40:58 -07:00
}
2023-01-10 20:23:47 -07:00
return sr . UpsertResource ( ctx , wrappedResourceModel )
}
2023-07-08 08:43:30 -06:00
// UpsertResource
// this method will upsert a resource, however it will not create associations.
// UPSERT operation
// - call FindOrCreate
// - check if the resource exists
// - if it does not exist, insert it
// - if no error during FindOrCreate && no rows affected (nothing was created)
// - update the resource using Updates operation
func ( sr * SqliteRepository ) UpsertResource ( ctx context . Context , wrappedResourceModel * models . ResourceBase ) ( bool , error ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return false , currentUserErr
}
wrappedResourceModel . UserID = currentUser . ID
2023-01-10 20:23:47 -07:00
cachedResourceRaw := wrappedResourceModel . ResourceRaw
2023-07-08 08:43:30 -06:00
sr . Logger . Infof ( "insert/update FHIRResource (%v) %v" , wrappedResourceModel . SourceResourceType , wrappedResourceModel . SourceResourceID )
wrappedFhirResourceModel , err := databaseModel . NewFhirResourceModelByType ( wrappedResourceModel . SourceResourceType )
if err != nil {
return false , err
}
2023-07-14 20:52:02 -06:00
2023-07-08 08:43:30 -06:00
wrappedFhirResourceModel . SetOriginBase ( wrappedResourceModel . OriginBase )
2023-08-01 21:29:35 -06:00
wrappedFhirResourceModel . SetSortTitle ( wrappedResourceModel . SortTitle )
wrappedFhirResourceModel . SetSortDate ( wrappedResourceModel . SortDate )
2023-08-17 12:14:33 -06:00
wrappedFhirResourceModel . SetSourceUri ( wrappedResourceModel . SourceUri )
2023-07-14 20:52:02 -06:00
//TODO: this takes too long, we need to find a way to do this processing faster or in the background async.
2023-07-08 08:43:30 -06:00
err = wrappedFhirResourceModel . PopulateAndExtractSearchParameters ( json . RawMessage ( wrappedResourceModel . ResourceRaw ) )
if err != nil {
2023-08-01 21:29:35 -06:00
sr . Logger . Warnf ( "ignoring: an error occurred while extracting SearchParameters using FHIRPath (%s/%s): %v" , wrappedResourceModel . SourceResourceType , wrappedResourceModel . SourceResourceID , err )
//wrappedFhirResourceModel.SetResourceRaw(wrappedResourceModel.ResourceRaw)
2023-07-08 08:43:30 -06:00
}
2023-07-14 20:52:02 -06:00
2022-12-03 13:48:35 -07:00
createResult := sr . GormClient . WithContext ( ctx ) . Where ( models . OriginBase {
2023-07-08 08:43:30 -06:00
SourceID : wrappedFhirResourceModel . GetSourceID ( ) ,
SourceResourceID : wrappedFhirResourceModel . GetSourceResourceID ( ) ,
SourceResourceType : wrappedFhirResourceModel . GetSourceResourceType ( ) , //TODO: and UpdatedAt > old UpdatedAt
} ) . Omit ( "RelatedResource.*" ) . FirstOrCreate ( wrappedFhirResourceModel )
2022-12-02 20:40:58 -07:00
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.
2023-01-10 20:23:47 -07:00
if wrappedResourceModel . ResourceRaw . String ( ) != string ( cachedResourceRaw ) {
2023-07-08 08:43:30 -06:00
updateResult := createResult . Omit ( "RelatedResource.*" ) . Updates ( wrappedResourceModel )
2022-12-02 20:40:58 -07:00
return updateResult . RowsAffected > 0 , updateResult . Error
} else {
return false , nil
}
} else {
//resource was created
return createResult . RowsAffected > 0 , createResult . Error
}
}
2023-07-08 08:43:30 -06:00
func ( sr * SqliteRepository ) ListResources ( ctx context . Context , queryOptions models . ListResourceQueryOptions ) ( [ ] models . ResourceBase , error ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
2022-12-02 20:40:58 -07:00
2023-07-08 08:43:30 -06:00
queryParam := models . OriginBase {
UserID : currentUser . ID ,
2022-12-02 20:40:58 -07:00
}
if len ( queryOptions . SourceResourceType ) > 0 {
2023-07-08 08:43:30 -06:00
queryParam . SourceResourceType = queryOptions . SourceResourceType
2022-12-02 20:40:58 -07:00
}
if len ( queryOptions . SourceID ) > 0 {
sourceUUID , err := uuid . Parse ( queryOptions . SourceID )
if err != nil {
return nil , err
}
2023-07-08 08:43:30 -06:00
queryParam . SourceID = sourceUUID
2022-12-02 20:40:58 -07:00
}
2022-12-17 16:10:19 -07:00
if len ( queryOptions . SourceResourceID ) > 0 {
2023-07-08 08:43:30 -06:00
queryParam . SourceResourceID = queryOptions . SourceResourceID
2022-12-17 16:10:19 -07:00
}
2022-12-02 20:40:58 -07:00
manifestJson , _ := json . MarshalIndent ( queryParam , "" , " " )
2023-07-08 08:43:30 -06:00
sr . Logger . Debugf ( "THE QUERY OBJECT===========> %v" , string ( manifestJson ) )
2022-12-02 20:40:58 -07:00
2023-07-08 08:43:30 -06:00
var wrappedResourceModels [ ] models . ResourceBase
2022-12-17 16:10:19 -07:00
queryBuilder := sr . GormClient . WithContext ( ctx )
2023-07-08 08:43:30 -06:00
if len ( queryOptions . SourceResourceType ) > 0 {
tableName , err := databaseModel . GetTableNameByResourceType ( queryOptions . SourceResourceType )
if err != nil {
return nil , err
}
2023-08-11 23:44:34 -06:00
queryBuilder = queryBuilder .
2023-07-08 08:43:30 -06:00
Where ( queryParam ) .
2023-08-11 23:44:34 -06:00
Table ( tableName )
2022-12-02 20:40:58 -07:00
2023-08-11 23:44:34 -06:00
if queryOptions . Limit > 0 {
queryBuilder = queryBuilder . Limit ( queryOptions . Limit ) . Offset ( queryOptions . Offset )
}
return wrappedResourceModels , queryBuilder . Find ( & wrappedResourceModels ) . Error
2023-07-08 08:43:30 -06:00
} else {
2023-08-11 23:44:34 -06:00
if queryOptions . Limit > 0 {
queryBuilder = queryBuilder . Limit ( queryOptions . Limit ) . Offset ( queryOptions . Offset )
}
2023-07-08 08:43:30 -06:00
//there is no FHIR Resource name specified, so we're querying across all FHIR resources
return sr . getResourcesFromAllTables ( queryBuilder , queryParam )
}
2022-12-02 20:40:58 -07:00
}
2023-07-08 08:43:30 -06:00
func ( sr * SqliteRepository ) GetResourceByResourceTypeAndId ( ctx context . Context , sourceResourceType string , sourceResourceId string ) ( * models . ResourceBase , error ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
2023-07-08 08:43:30 -06:00
tableName , err := databaseModel . GetTableNameByResourceType ( sourceResourceType )
if err != nil {
return nil , err
}
queryParam := models . OriginBase {
UserID : currentUser . ID ,
SourceResourceType : sourceResourceType ,
SourceResourceID : sourceResourceId ,
2022-12-02 20:40:58 -07:00
}
2023-07-08 08:43:30 -06:00
var wrappedResourceModel models . ResourceBase
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( queryParam ) .
2023-07-08 08:43:30 -06:00
Table ( tableName ) .
2022-12-02 20:40:58 -07:00
First ( & wrappedResourceModel )
return & wrappedResourceModel , results . Error
}
2023-07-08 08:43:30 -06:00
// we need to figure out how to get the source resource type from the source resource id, or if we're searching across every table :(
func ( sr * SqliteRepository ) GetResourceBySourceId ( ctx context . Context , sourceId string , sourceResourceId string ) ( * models . ResourceBase , error ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
2022-12-02 20:40:58 -07:00
sourceIdUUID , err := uuid . Parse ( sourceId )
if err != nil {
return nil , err
}
2023-07-08 08:43:30 -06:00
queryParam := models . OriginBase {
UserID : currentUser . ID ,
SourceID : sourceIdUUID ,
SourceResourceID : sourceResourceId ,
2022-12-02 20:40:58 -07:00
}
2023-07-08 08:43:30 -06:00
//there is no FHIR Resource name specified, so we're querying across all FHIR resources
wrappedResourceModels , err := sr . getResourcesFromAllTables ( sr . GormClient . WithContext ( ctx ) , queryParam )
if len ( wrappedResourceModels ) > 0 {
return & wrappedResourceModels [ 0 ] , err
} else {
return nil , fmt . Errorf ( "no resource found with source id %s and source resource id %s" , sourceId , sourceResourceId )
}
2022-12-02 20:40:58 -07:00
}
// Get the patient for each source (for the current user)
2023-07-08 08:43:30 -06:00
func ( sr * SqliteRepository ) GetPatientForSources ( ctx context . Context ) ( [ ] models . ResourceBase , error ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
2022-12-02 20:40:58 -07:00
2023-07-08 08:43:30 -06:00
//SELECT * FROM resource_bases WHERE user_id = "" and source_resource_type = "Patient" GROUP BY source_id
2022-12-02 20:40:58 -07:00
2023-07-08 08:43:30 -06:00
tableName , err := databaseModel . GetTableNameByResourceType ( "Patient" )
if err != nil {
return nil , err
}
2022-12-02 20:40:58 -07:00
2023-07-08 08:43:30 -06:00
var wrappedResourceModels [ ] models . ResourceBase
2022-12-03 13:48:35 -07:00
results := sr . GormClient . WithContext ( ctx ) .
2022-12-03 20:16:32 -07:00
//Group("source_id"). //broken in Postgres.
2022-12-02 20:40:58 -07:00
Where ( models . OriginBase {
2023-01-15 12:07:41 -07:00
UserID : currentUser . ID ,
2022-12-02 20:40:58 -07:00
SourceResourceType : "Patient" ,
} ) .
2023-07-08 08:43:30 -06:00
Table ( tableName ) .
2022-12-02 20:40:58 -07:00
Find ( & wrappedResourceModels )
return wrappedResourceModels , results . Error
}
2022-12-17 16:10:19 -07:00
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Resource Associations
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2023-07-08 08:43:30 -06:00
// verifyAssociationPermission ensure that the sources are "owned" by the same user, and that the user is the current user
func ( sr * SqliteRepository ) verifyAssociationPermission ( ctx context . Context , sourceUserID uuid . UUID , relatedSourceUserID uuid . UUID ) error {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return currentUserErr
}
2023-04-22 23:08:58 -06:00
if sourceUserID != relatedSourceUserID {
2022-12-17 16:10:19 -07:00
return fmt . Errorf ( "user id's must match when adding associations" )
2023-04-22 23:08:58 -06:00
} else if sourceUserID != currentUser . ID {
2022-12-17 16:10:19 -07:00
return fmt . Errorf ( "user id's must match current user" )
}
2022-12-21 20:51:02 -07:00
return nil
}
func ( sr * SqliteRepository ) AddResourceAssociation ( ctx context . Context , source * models . SourceCredential , resourceType string , resourceId string , relatedSource * models . SourceCredential , relatedResourceType string , relatedResourceId string ) error {
2023-04-22 23:08:58 -06:00
//ensure that the sources are "owned" by the same user
2023-07-08 08:43:30 -06:00
err := sr . verifyAssociationPermission ( ctx , source . UserID , relatedSource . UserID )
2023-04-22 23:08:58 -06:00
if err != nil {
return err
}
2023-07-08 08:43:30 -06:00
err = sr . GormClient . WithContext ( ctx ) . Table ( "related_resources" ) . Create ( map [ string ] interface { } {
"resource_base_user_id" : source . UserID ,
"resource_base_source_id" : source . ID ,
"resource_base_source_resource_type" : resourceType ,
"resource_base_source_resource_id" : resourceId ,
"related_resource_user_id" : relatedSource . UserID ,
"related_resource_source_id" : relatedSource . ID ,
"related_resource_source_resource_type" : relatedResourceType ,
"related_resource_source_resource_id" : relatedResourceId ,
2022-12-17 16:10:19 -07:00
} ) . Error
2023-07-08 08:43:30 -06:00
uniqueConstraintError := errors . New ( "constraint failed: UNIQUE constraint failed" )
if err != nil {
if strings . HasPrefix ( err . Error ( ) , uniqueConstraintError . Error ( ) ) {
sr . Logger . Warnf ( "Ignoring an error when creating a related_resource association for %s/%s: %v" , resourceType , resourceId , err )
//we can safely ignore this error
return nil
}
}
return err
2022-12-17 16:10:19 -07:00
}
func ( sr * SqliteRepository ) RemoveResourceAssociation ( ctx context . Context , source * models . SourceCredential , resourceType string , resourceId string , relatedSource * models . SourceCredential , relatedResourceType string , relatedResourceId string ) error {
2023-04-22 23:08:58 -06:00
//ensure that the sources are "owned" by the same user
2023-07-08 08:43:30 -06:00
err := sr . verifyAssociationPermission ( ctx , source . UserID , relatedSource . UserID )
2023-04-22 23:08:58 -06:00
if err != nil {
return err
2022-12-17 16:10:19 -07:00
}
2023-07-08 08:43:30 -06:00
//manually delete association
results := sr . GormClient . WithContext ( ctx ) .
//Table("related_resources").
Delete ( & models . RelatedResource { } , map [ string ] interface { } {
"resource_base_user_id" : source . UserID ,
"resource_base_source_id" : source . ID ,
"resource_base_source_resource_type" : resourceType ,
"resource_base_source_resource_id" : resourceId ,
"related_resource_user_id" : relatedSource . UserID ,
"related_resource_source_id" : relatedSource . ID ,
"related_resource_source_resource_type" : relatedResourceType ,
"related_resource_source_resource_id" : relatedResourceId ,
} )
if results . Error != nil {
return results . Error
} else if results . RowsAffected == 0 {
return fmt . Errorf ( "no association found for %s/%s and %s/%s" , resourceType , resourceId , relatedResourceType , relatedResourceId )
}
return nil
2022-12-17 16:10:19 -07:00
}
2023-07-08 08:43:30 -06:00
func ( sr * SqliteRepository ) FindResourceAssociationsByTypeAndId ( ctx context . Context , source * models . SourceCredential , resourceType string , resourceId string ) ( [ ] models . RelatedResource , error ) {
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
if source . UserID != currentUser . ID {
return nil , fmt . Errorf ( "source credential must match the current user id" )
}
// SELECT * FROM related_resources WHERE user_id = "53c1e930-63af-46c9-b760-8e83cbc1abd9";
var relatedResources [ ] models . RelatedResource
result := sr . GormClient . WithContext ( ctx ) .
Where ( models . RelatedResource {
ResourceBaseUserID : currentUser . ID ,
ResourceBaseSourceID : source . ID ,
ResourceBaseSourceResourceType : resourceType ,
ResourceBaseSourceResourceID : resourceId ,
} ) .
Find ( & relatedResources )
return relatedResources , result . Error
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Resource Composition (Grouping)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2023-01-10 20:23:47 -07:00
// AddResourceComposition
// this will group resources together into a "Composition" -- primarily to group related Encounters & Conditions into one semantic root.
// algorithm:
// - find source for each resource
2023-07-08 08:43:30 -06:00
// - (SECURITY) ensure the current user and the source for each resource matches
2023-01-10 20:23:47 -07:00
// - check if there is a Composition resource Type already.
// - if Composition type already exists:
// - update "relatesTo" field with additional data.
// - else:
// - Create a Composition resource type (populated with "relatesTo" references to all provided Resources)
// - add AddResourceAssociation for all resources linked to the Composition resource
// - store the Composition resource
2023-07-08 08:43:30 -06:00
//TODO: determine if we should be using a List Resource instead of a Composition resource
func ( sr * SqliteRepository ) AddResourceComposition ( ctx context . Context , compositionTitle string , resources [ ] * models . ResourceBase ) error {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return currentUserErr
}
2023-01-10 20:23:47 -07:00
//generate placeholder source
placeholderSource := models . SourceCredential { UserID : currentUser . ID , SourceType : "manual" , ModelBase : models . ModelBase { ID : uuid . MustParse ( "00000000-0000-0000-0000-000000000000" ) } }
2023-07-08 08:43:30 -06:00
existingCompositionResources := [ ] * models . ResourceBase { }
rawResourceLookupTable := map [ string ] * models . ResourceBase { }
2023-01-10 20:23:47 -07:00
//find the source for each resource we'd like to merge. (for ownership verification)
sourceLookup := map [ uuid . UUID ] * models . SourceCredential { }
for _ , resource := range resources {
if resource . SourceResourceType == pkg . FhirResourceTypeComposition {
//skip, Composition resources don't have a valid SourceCredential
existingCompositionResources = append ( existingCompositionResources , resource )
//compositions may include existing resources, make sure we handle these
2023-07-08 08:43:30 -06:00
for _ , related := range resource . RelatedResource {
2023-01-10 20:23:47 -07:00
rawResourceLookupTable [ fmt . Sprintf ( "%s/%s" , related . SourceResourceType , related . SourceResourceID ) ] = related
}
continue
}
if _ , sourceOk := sourceLookup [ resource . SourceID ] ; ! sourceOk {
//source has not been added yet, lets query for it.
sourceCred , err := sr . GetSource ( ctx , resource . SourceID . String ( ) )
if err != nil {
return fmt . Errorf ( "could not find source %s" , resource . SourceID . String ( ) )
}
sourceLookup [ resource . SourceID ] = sourceCred
}
rawResourceLookupTable [ fmt . Sprintf ( "%s/%s" , resource . SourceResourceType , resource . SourceResourceID ) ] = resource
}
2023-07-08 08:43:30 -06:00
// SECURITY: ensure the current user and the source for each resource matches
2023-01-10 20:23:47 -07:00
for _ , source := range sourceLookup {
if source . UserID != currentUser . ID {
return fmt . Errorf ( "source must be owned by the current user: %s vs %s" , source . UserID , currentUser . ID )
}
}
// - check if there is a Composition resource Type already.
2023-07-08 08:43:30 -06:00
var compositionResource * models . ResourceBase
2023-01-10 20:23:47 -07:00
if len ( existingCompositionResources ) > 0 {
//- if Composition type already exists in this set
// - update "relatesTo" field with additional data.
compositionResource = existingCompositionResources [ 0 ]
2023-07-08 08:43:30 -06:00
//disassociate all existing remaining composition resources.
2023-01-10 20:23:47 -07:00
for _ , existingCompositionResource := range existingCompositionResources [ 1 : ] {
2023-07-08 08:43:30 -06:00
for _ , relatedResource := range existingCompositionResource . RelatedResource {
2023-01-10 20:23:47 -07:00
if err := sr . RemoveResourceAssociation (
ctx ,
& placeholderSource ,
existingCompositionResource . SourceResourceType ,
existingCompositionResource . SourceResourceID ,
sourceLookup [ relatedResource . SourceID ] ,
relatedResource . SourceResourceType ,
relatedResource . SourceResourceID ,
) ; err != nil {
//ignoring errors, could be due to duplicate edges
return fmt . Errorf ( "an error occurred while removing resource association: %v" , err )
}
}
//remove this resource
2023-07-08 08:43:30 -06:00
compositionTable , err := databaseModel . GetTableNameByResourceType ( "Composition" )
if err != nil {
return fmt . Errorf ( "an error occurred while finding Composition resource table: %v" , err )
}
//TODO: we may need to delete with using the FhirComposition struct type
deleteResult := sr . GormClient . WithContext ( ctx ) .
Table ( compositionTable ) .
Delete ( existingCompositionResource )
if deleteResult . Error != nil {
return fmt . Errorf ( "an error occurred while removing Composition resource(%s/%s): %v" , existingCompositionResource . SourceResourceType , existingCompositionResource . SourceID , err )
} else if deleteResult . RowsAffected != 1 {
return fmt . Errorf ( "composition resource was not deleted %s/%s" , existingCompositionResource . SourceResourceType , existingCompositionResource . SourceID )
2023-01-10 20:23:47 -07:00
}
}
} else {
//- else:
// - Create a Composition resource type (populated with "relatesTo" references to all provided Resources)
2023-07-08 08:43:30 -06:00
compositionResource = & models . ResourceBase {
2023-01-10 20:23:47 -07:00
OriginBase : models . OriginBase {
UserID : placeholderSource . UserID , //
SourceID : placeholderSource . ID , //Empty SourceID expected ("0000-0000-0000-0000")
SourceResourceType : pkg . FhirResourceTypeComposition ,
SourceResourceID : uuid . New ( ) . String ( ) ,
} ,
}
}
// - Generate an "updated" RawResource json blob
rawCompositionResource := models . ResourceComposition {
Title : compositionTitle ,
RelatesTo : [ ] models . ResourceCompositionRelatesTo { } ,
}
for relatedResourceKey , _ := range rawResourceLookupTable {
rawCompositionResource . RelatesTo = append ( rawCompositionResource . RelatesTo , models . ResourceCompositionRelatesTo {
Target : models . ResourceCompositionRelatesToTarget {
TargetReference : models . ResourceCompositionRelatesToTargetReference {
Reference : relatedResourceKey ,
} ,
} ,
} )
}
rawResourceJson , err := json . Marshal ( rawCompositionResource )
if err != nil {
return err
}
compositionResource . ResourceRaw = rawResourceJson
2023-01-15 11:25:46 -07:00
compositionResource . SortTitle = & compositionTitle
2023-07-08 08:43:30 -06:00
compositionResource . RelatedResource = utils . SortResourcePtrListByDate ( resources )
compositionResource . SortDate = compositionResource . RelatedResource [ 0 ] . SortDate
2023-01-15 11:25:46 -07:00
2023-01-10 20:23:47 -07:00
//store the Composition resource
_ , err = sr . UpsertResource ( ctx , compositionResource )
if err != nil {
return err
}
// - add AddResourceAssociation for all resources linked to the Composition resource
for _ , resource := range rawResourceLookupTable {
if err := sr . AddResourceAssociation (
ctx ,
& placeholderSource ,
compositionResource . SourceResourceType ,
compositionResource . SourceResourceID ,
sourceLookup [ resource . SourceID ] ,
resource . SourceResourceType ,
resource . SourceResourceID ,
) ; err != nil {
2023-07-08 08:43:30 -06:00
return err
2023-01-10 20:23:47 -07:00
}
}
return nil
}
2022-12-02 20:40:58 -07:00
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SourceCredential
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) CreateSource ( ctx context . Context , sourceCreds * models . SourceCredential ) error {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return currentUserErr
}
sourceCreds . UserID = currentUser . ID
2022-12-02 20:40:58 -07:00
//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
}
2023-07-20 22:43:37 -06:00
func ( sr * SqliteRepository ) UpdateSource ( ctx context . Context , sourceCreds * models . SourceCredential ) error {
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return currentUserErr
}
sourceCreds . UserID = currentUser . ID
//Assign will **always** update the source credential in the DB with data passed into this function.
return sr . GormClient . WithContext ( ctx ) .
Where ( models . SourceCredential {
ModelBase : models . ModelBase { ID : sourceCreds . ID } ,
UserID : sourceCreds . UserID ,
SourceType : sourceCreds . SourceType ,
} ) . Updates ( models . SourceCredential {
AccessToken : sourceCreds . AccessToken ,
RefreshToken : sourceCreds . RefreshToken ,
ExpiresAt : sourceCreds . ExpiresAt ,
DynamicClientId : sourceCreds . DynamicClientId ,
DynamicClientRegistrationMode : sourceCreds . DynamicClientRegistrationMode ,
DynamicClientJWKS : sourceCreds . DynamicClientJWKS ,
} ) . Error
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetSource ( ctx context . Context , sourceId string ) ( * models . SourceCredential , error ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
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 ) .
2023-01-15 12:07:41 -07:00
Where ( models . SourceCredential { UserID : currentUser . ID , ModelBase : models . ModelBase { ID : sourceUUID } } ) .
2022-12-02 20:40:58 -07:00
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 ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
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
2023-07-08 08:43:30 -06:00
// SELECT source_resource_type as resource_type, COUNT(*) as count FROM resource_bases WHERE source_id = "53c1e930-63af-46c9-b760-8e83cbc1abd9" GROUP BY source_resource_type;
2022-12-02 20:40:58 -07:00
var resourceTypeCounts [ ] map [ string ] interface { }
2023-07-08 08:43:30 -06:00
resourceTypes := databaseModel . GetAllowedResourceTypes ( )
for _ , resourceType := range resourceTypes {
tableName , err := databaseModel . GetTableNameByResourceType ( resourceType )
if err != nil {
return nil , err
}
var count int64
result := sr . GormClient . WithContext ( ctx ) .
Table ( tableName ) .
Where ( models . OriginBase {
UserID : currentUser . ID ,
SourceID : sourceUUID ,
} ) .
Count ( & count )
if result . Error != nil {
return nil , result . Error
}
if count == 0 {
continue //don't add resource counts if the count is 0
}
resourceTypeCounts = append ( resourceTypeCounts , map [ string ] interface { } {
"source_id" : sourceId ,
"resource_type" : resourceType ,
"count" : count ,
} )
2022-12-02 20:40:58 -07:00
}
sourceSummary . ResourceTypeCounts = resourceTypeCounts
//set patient
2023-07-08 08:43:30 -06:00
patientTableName , err := databaseModel . GetTableNameByResourceType ( "Patient" )
if err != nil {
return nil , err
}
var wrappedPatientResourceModel models . ResourceBase
patientResults := sr . GormClient . WithContext ( ctx ) .
2022-12-02 20:40:58 -07:00
Where ( models . OriginBase {
2023-01-15 12:07:41 -07:00
UserID : currentUser . ID ,
2022-12-02 20:40:58 -07:00
SourceResourceType : "Patient" ,
SourceID : sourceUUID ,
} ) .
2023-07-08 08:43:30 -06:00
Table ( patientTableName ) .
2022-12-02 20:40:58 -07:00
First ( & wrappedPatientResourceModel )
2023-07-08 08:43:30 -06:00
if patientResults . Error != nil {
return nil , patientResults . Error
2022-12-02 20:40:58 -07:00
}
sourceSummary . Patient = & wrappedPatientResourceModel
return sourceSummary , nil
}
2022-12-03 12:08:14 -07:00
func ( sr * SqliteRepository ) GetSources ( ctx context . Context ) ( [ ] models . SourceCredential , error ) {
2023-01-15 12:07:41 -07:00
currentUser , currentUserErr := sr . GetCurrentUser ( ctx )
if currentUserErr != nil {
return nil , currentUserErr
}
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 ) .
2023-01-15 12:07:41 -07:00
Where ( models . SourceCredential { UserID : currentUser . ID } ) .
2022-12-02 20:40:58 -07:00
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 ""
}
2023-07-08 08:43:30 -06:00
//Internal function
// This function will return a list of resources from all FHIR tables in the database
// The query allows us to set the source id, source resource id, source resource type
//SECURITY: this function assumes the user has already been authenticated
//TODO: theres probably a more efficient way of doing this with GORM
func ( sr * SqliteRepository ) getResourcesFromAllTables ( queryBuilder * gorm . DB , queryParam models . OriginBase ) ( [ ] models . ResourceBase , error ) {
wrappedResourceModels := [ ] models . ResourceBase { }
resourceTypes := databaseModel . GetAllowedResourceTypes ( )
for _ , resourceType := range resourceTypes {
tableName , err := databaseModel . GetTableNameByResourceType ( resourceType )
if err != nil {
return nil , err
}
var tempWrappedResourceModels [ ] models . ResourceBase
results := queryBuilder .
Where ( queryParam ) .
Table ( tableName ) .
Find ( & tempWrappedResourceModels )
if results . Error != nil {
return nil , results . Error
}
wrappedResourceModels = append ( wrappedResourceModels , tempWrappedResourceModels ... )
}
return wrappedResourceModels , nil
}