From 68859546c0209e4621c80d6f781e9a2a48da41b9 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 20 Feb 2024 10:22:19 -0800 Subject: [PATCH] adding tests to ensure that the same field with multiple modifiers is correctly handled. adding IPS query logic. Adding IPS sections. TODO: tests broken. --- backend/pkg/constants.go | 17 ++ backend/pkg/database/gorm_repository_query.go | 10 + .../gorm_repository_query_sql_test.go | 79 +++++- .../pkg/database/gorm_repository_summary.go | 241 ++++++++++++++++++ 4 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 backend/pkg/database/gorm_repository_summary.go diff --git a/backend/pkg/constants.go b/backend/pkg/constants.go index 8062ffe0..e87ed5bf 100644 --- a/backend/pkg/constants.go +++ b/backend/pkg/constants.go @@ -10,6 +10,8 @@ type DatabaseRepositoryType string type InstallationVerificationStatus string type InstallationQuotaStatus string +type IPSSections string + const ( ResourceListPageSize int = 20 @@ -50,4 +52,19 @@ const ( InstallationVerificationStatusVerified InstallationVerificationStatus = "VERIFIED" //email has been verified InstallationQuotaStatusActive InstallationQuotaStatus = "ACTIVE" InstallationQuotaStatusConsumed InstallationQuotaStatus = "CONSUMED" + + IPSSectionsMedicationSummary IPSSections = "medication_summary" + IPSSectionsAllergiesIntolerances IPSSections = "allergies_intolerances" + IPSSectionsProblemList IPSSections = "problem_list" + IPSSectionsImmunizations IPSSections = "immunizations" + IPSSectionsHistoryOfProcedures IPSSections = "history_of_procedures" + IPSSectionsMedicalDevices IPSSections = "medical_devices" + IPSSectionsDiagnosticResults IPSSections = "diagnostic_results" + IPSSectionsVitalSigns IPSSections = "vital_signs" + IPSSectionsHistoryOfIllnesses IPSSections = "history_of_illnesses" + IPSSectionsPregnancy IPSSections = "pregnancy" + IPSSectionsSocialHistory IPSSections = "social_history" + IPSSectionsPlanOfCare IPSSections = "plan_of_care" + IPSSectionsFunctionalStatus IPSSections = "functional_status" + IPSSectionsAdvanceDirectives IPSSections = "advance_directives" ) diff --git a/backend/pkg/database/gorm_repository_query.go b/backend/pkg/database/gorm_repository_query.go index 029a48d7..562cb4a0 100644 --- a/backend/pkg/database/gorm_repository_query.go +++ b/backend/pkg/database/gorm_repository_query.go @@ -3,6 +3,7 @@ package database import ( "context" "fmt" + "log" "sort" "strconv" "strings" @@ -230,6 +231,15 @@ func (gr *GormRepository) sqlQueryResources(ctx context.Context, query models.Qu } } + log.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + log.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + log.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + log.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + log.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + log.Printf("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + log.Printf("whereClauses: %v", whereClauses) + log.Printf("whereNamedParameters: %v", whereNamedParameters) + //ensure Where and From clauses are unique whereClauses = lo.Uniq(whereClauses) whereClauses = lo.Compact(whereClauses) diff --git a/backend/pkg/database/gorm_repository_query_sql_test.go b/backend/pkg/database/gorm_repository_query_sql_test.go index 14b04e06..3605574a 100644 --- a/backend/pkg/database/gorm_repository_query_sql_test.go +++ b/backend/pkg/database/gorm_repository_query_sql_test.go @@ -186,7 +186,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenWithNotModi }) } -func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenMultipleValuesWithNotModifier() { +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenMultipleANDValuesWithNotModifier() { //setup sqliteRepo := suite.TestRepository.(*GormRepository) sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true}) @@ -197,7 +197,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenMultipleVal sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{ Select: []string{}, Where: map[string]interface{}{ - "code:not": []string{"test_code", "test_code2"}, + "code:not": []string{"test_code", "test_code2"}, //AND condition }, From: "Observation", }) @@ -223,6 +223,81 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenMultipleVal }) } +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenMultipleORValues() { + //setup + sqliteRepo := suite.TestRepository.(*GormRepository) + sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true}) + + //test + authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username") + + sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{ + Select: []string{}, + Where: map[string]interface{}{ + "code": "test_code,test_code2", //OR condition + }, + From: "Observation", + }) + require.NoError(suite.T(), err) + var results []map[string]interface{} + statement := sqlQuery.Find(&results).Statement + sqlString := statement.SQL.String() + sqlParams := statement.Vars + + //assert + require.NoError(suite.T(), err) + require.Equal(suite.T(), + strings.Join([]string{ + "SELECT fhir.*", + "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", + "WHERE ((codeJson.value ->> '$.code' = ?)) OR ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)", + "GROUP BY `fhir`.`id`", + "ORDER BY fhir.sort_date DESC", + }, " "), + sqlString) + require.Equal(suite.T(), sqlParams, []interface{}{ + "test_code", "test_code2", "00000000-0000-0000-0000-000000000000", + }) +} + +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenMixedMultipleANDORValues() { + //setup + sqliteRepo := suite.TestRepository.(*GormRepository) + sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true}) + + //test + authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username") + + sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{ + Select: []string{}, + Where: map[string]interface{}{ + "code:not": []string{"test_code", "test_code2", "test_code3"}, //AND condition + "code": "test_code4,test_code5,test_code6", //OR condition + }, + From: "Observation", + }) + require.NoError(suite.T(), err) + var results []map[string]interface{} + statement := sqlQuery.Find(&results).Statement + sqlString := statement.SQL.String() + sqlParams := statement.Vars + + //assert + require.NoError(suite.T(), err) + require.Equal(suite.T(), + strings.Join([]string{ + "SELECT fhir.*", + "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", + "WHERE ((codeJson.value ->> '$.code' <> ?)) AND ((codeJson.value ->> '$.code' <> ?)) AND ((codeJson.value ->> '$.code' <> ?)) AND ((codeJson.value ->> '$.code' = ?) OR (codeJson.value ->> '$.code' = ?) OR (codeJson.value ->> '$.code' = ?)) AND (user_id = ?)", + "GROUP BY `fhir`.`id`", + "ORDER BY fhir.sort_date DESC", + }, " "), + sqlString) + require.Equal(suite.T(), sqlParams, []interface{}{ + "test_code", "test_code2", "test_code3", "test_code4", "00000000-0000-0000-0000-000000000000", + }) +} + func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveOrderByAggregation() { //setup sqliteRepo := suite.TestRepository.(*GormRepository) diff --git a/backend/pkg/database/gorm_repository_summary.go b/backend/pkg/database/gorm_repository_summary.go new file mode 100644 index 00000000..1749904e --- /dev/null +++ b/backend/pkg/database/gorm_repository_summary.go @@ -0,0 +1,241 @@ +package database + +import ( + "context" + "github.com/fastenhealth/fasten-onprem/backend/pkg" + "github.com/fastenhealth/fasten-onprem/backend/pkg/models" + databaseModel "github.com/fastenhealth/fasten-onprem/backend/pkg/models/database" +) + +// GetInternationalPatientSummary will generate an IPS bundle, which can then be used to generate a IPS QR code, PDF or JSON bundle +// The IPS bundle will contain a summary of all the data in the system, including a list of all sources, and the main Patient +// See: https://github.com/fastenhealth/fasten-onprem/issues/170 +// See: https://github.com/jddamore/fhir-ips-server/blob/main/docs/Summary_Creation_Steps.md +func (gr *GormRepository) GetInternationalPatientSummary(ctx context.Context) (*models.Summary, error) { + currentUser, currentUserErr := gr.GetCurrentUser(ctx) + if currentUserErr != nil { + return nil, currentUserErr + } + + // we want a count of all resources for this user by type + var resourceCountResults []map[string]interface{} + + resourceTypes := databaseModel.GetAllowedResourceTypes() + for _, resourceType := range resourceTypes { + tableName, err := databaseModel.GetTableNameByResourceType(resourceType) + if err != nil { + return nil, err + } + var count int64 + + gr.QueryResources(ctx, models.QueryResource{ + Use: "", + Select: nil, + From: "", + Where: nil, + Limit: nil, + Offset: nil, + Aggregations: nil, + }) + + result := gr.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, + }) + } + + // we want a list of all sources (when they were last updated) + sources, err := gr.GetSources(ctx) + if err != nil { + return nil, err + } + + // we want the main Patient for each source + patients, err := gr.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 +} + +// https://github.com/jddamore/fhir-ips-server/blob/main/docs/Summary_Creation_Steps.md +func generateIPSSectionQueries(sectionType pkg.IPSSections) ([]models.QueryResource, error) { + + queries := []models.QueryResource{} + switch sectionType { + case pkg.IPSSectionsAllergiesIntolerances: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "AllergyIntolerance", + Where: map[string]interface{}{ + "clinicalStatus:not": []string{"inactive", "resolved"}, + "verificationStatus:not": []string{"entered-in-error"}, + }, + }) + break + case pkg.IPSSectionsProblemList: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Condition", + Where: map[string]interface{}{ + "clinicalStatus:not": []string{"inactive", "resolved"}, + "verificationStatus:not": []string{"entered-in-error"}, + }, + }) + break + case pkg.IPSSectionsMedicationSummary: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "MedicationStatement", + Where: map[string]interface{}{ + "status": "active,intended,unknown,on-hold", + }, + }) + queries = append(queries, models.QueryResource{ + Select: nil, + From: "MedicationRequest", + Where: map[string]interface{}{ + "status": "active,unknown,on-hold", + }, + }) + break + case pkg.IPSSectionsDiagnosticResults: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "DiagnosticReport", + Where: map[string]interface{}{ + "category": "LAB", + }, + }) + + //TODO: group by code, sort by date, limit to the most recent 3 + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Observation", + Where: map[string]interface{}{ + "category": "laboratory", + "status:not": "preliminary", + }, + }) + break + case pkg.IPSSectionsVitalSigns: + //TODO: group by code, sort by date, limit to the most recent 3 + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Observation", + Where: map[string]interface{}{ + "category": "vital-signs", + }, + }) + break + case pkg.IPSSectionsSocialHistory: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Observation", + Where: map[string]interface{}{ + "category": "social-history", + "status:not": "preliminary", + }, + }) + break + case pkg.IPSSectionsPregnancy: + //TODO: determine the code for pregnancy from IPS specification + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Observation", + Where: map[string]interface{}{ + "status:not": "preliminary", + }, + }) + break + case pkg.IPSSectionsImmunizations: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Immunization", + Where: map[string]interface{}{ + "status:not": "entered-in-error", + }, + }) + break + case pkg.IPSSectionsAdvanceDirectives: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Consent", + Where: map[string]interface{}{ + "status": "active", + }, + }) + break + case pkg.IPSSectionsFunctionalStatus: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "ClinicalImpression", + Where: map[string]interface{}{ + "status": "in-progress,completed", + }, + }) + break + case pkg.IPSSectionsMedicalDevices: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Device", + Where: map[string]interface{}{ + "status": "entered-in-error", + }, + }) + break + case pkg.IPSSectionsHistoryOfIllnesses: + //TODO: last updated date should be older than 5 years (dateTime or period.high) + //TODO: check if where clause with multiple modifiers for the same field works as expected + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Condition", + Where: map[string]interface{}{ + "clinicalStatus:not": []string{"entered-in-error"}, + "clinicalStatus": "inactive,remission,resolved", + }, + }) + break + case pkg.IPSSectionsPlanOfCare: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "CarePlan", + Where: map[string]interface{}{ + "status": "active,on-hold,unknown", + }, + }) + break + case pkg.IPSSectionsHistoryOfProcedures: + queries = append(queries, models.QueryResource{ + Select: nil, + From: "Procedure", + Where: map[string]interface{}{ + "status:not": []string{"entered-in-error", "not-done"}, + }, + }) + } + + return queries, nil +}