when querying vital signs, use vital-signs category to find a list of all codes, which are then used to find the last 3 results for this code.

This commit is contained in:
Jason Kulatunga 2024-03-03 14:02:01 -08:00
parent 0f6e735e3a
commit 441c8ab964
No known key found for this signature in database
3 changed files with 142 additions and 22 deletions

View File

@ -706,3 +706,45 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenMultipleMod
"test_code", "test_code2", "test_code3", "test_code4", "test_code5", "test_code6", "00000000-0000-0000-0000-000000000000",
})
}
// Section Vital Signs Codes Lookup
func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_SectionVitalSigns_WithTokenGroupByNoModifier() {
//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{}{
"category": "vital-signs",
},
From: "Observation",
Aggregations: &models.QueryResourceAggregations{
GroupBy: &models.QueryResourceAggregation{Field: "code:code"},
},
})
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 (codeJson.value ->> '$.code') as label, count(*) as value",
"FROM fhir_observation as fhir, json_each(fhir.category) as categoryJson, json_each(fhir.code) as codeJson",
"WHERE ((categoryJson.value ->> '$.code' = ?)) AND (user_id = ?)",
"GROUP BY (codeJson.value ->> '$.code')",
"ORDER BY count(*) DESC",
}, " "), sqlString)
require.Equal(suite.T(), sqlParams, []interface{}{
"vital-signs",
"00000000-0000-0000-0000-000000000000",
})
}

View File

@ -10,6 +10,7 @@ import (
"github.com/fastenhealth/fasten-onprem/backend/pkg/utils/ips"
"github.com/fastenhealth/gofhir-models/fhir401"
"github.com/google/uuid"
"github.com/samber/lo"
"log"
"time"
)
@ -39,15 +40,15 @@ func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Conte
// 6. Create the IPS Bundle
//Step 1. Generate the IPS Section Lists
summarySectionResources, err := gr.GetInternationalPatientSummarySectionResources(ctx)
summarySectionQueryResults, err := gr.getInternationalPatientSummarySectionResources(ctx)
if err != nil {
return nil, nil, err
}
//Step 2. Create the Composition Section
compositionSections := []fhir401.CompositionSection{}
for sectionType, sectionResources := range summarySectionResources {
compositionSection, err := generateIPSCompositionSection(narrativeEngine, sectionType, sectionResources)
for sectionType, sectionQueryResultsList := range summarySectionQueryResults {
compositionSection, err := generateIPSCompositionSection(narrativeEngine, sectionType, sectionQueryResultsList)
if err != nil {
return nil, nil, err
}
@ -146,14 +147,14 @@ func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Conte
// TODO: Add the Patient to the bundle
// TODO: Add the Fasten Health Organization to the bundle
// Add all the resources to the bundle
for _, sectionResources := range summarySectionResources {
for _, resource := range sectionResources {
ipsBundle.Entry = append(ipsBundle.Entry, fhir401.BundleEntry{
Resource: json.RawMessage(resource.GetResourceRaw()),
})
}
}
// TODO: Add all the resources to the bundle
//for _, sectionResources := range summarySectionResources {
// for _, resource := range sectionResources {
// ipsBundle.Entry = append(ipsBundle.Entry, fhir401.BundleEntry{
// Resource: json.RawMessage(resource.GetResourceRaw()),
// })
// }
//}
return ipsBundle, ipsComposition, nil
}
@ -162,18 +163,18 @@ func (gr *GormRepository) GetInternationalPatientSummaryBundle(ctx context.Conte
// 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) GetInternationalPatientSummarySectionResources(ctx context.Context) (map[pkg.IPSSections][]database.IFhirResourceModel, error) {
func (gr *GormRepository) getInternationalPatientSummarySectionResources(ctx context.Context) (map[pkg.IPSSections][]any, error) {
summarySectionResources := map[pkg.IPSSections][]database.IFhirResourceModel{}
summarySectionResources := map[pkg.IPSSections][]any{}
// generate queries for each IPS Section
for ndx, _ := range pkg.IPSSectionsList {
sectionName := pkg.IPSSectionsList[ndx]
//initialize the section
summarySectionResources[sectionName] = []database.IFhirResourceModel{}
summarySectionResources[sectionName] = []any{}
queries, err := generateIPSSectionQueries(sectionName)
queries, err := gr.generateIPSSectionQueries(ctx, sectionName)
if err != nil {
return nil, err
}
@ -184,17 +185,17 @@ func (gr *GormRepository) GetInternationalPatientSummarySectionResources(ctx con
return nil, err
}
resultsList := convertUnknownInterfaceToFhirSlice(results)
//resultsList := convertUnknownInterfaceToFhirSlice(results)
//TODO: generate resource narrative
summarySectionResources[sectionName] = append(summarySectionResources[sectionName], resultsList...)
summarySectionResources[sectionName] = append(summarySectionResources[sectionName], results)
}
}
return summarySectionResources, nil
}
func generateIPSCompositionSection(narrativeEngine *ips.Narrative, sectionType pkg.IPSSections, resources []database.IFhirResourceModel) (*fhir401.CompositionSection, error) {
func generateIPSCompositionSection(narrativeEngine *ips.Narrative, sectionType pkg.IPSSections, queryResultsList []any) (*fhir401.CompositionSection, error) {
sectionTitle, sectionCode, err := generateIPSSectionHeaderInfo(sectionType)
if err != nil {
return nil, err
@ -204,6 +205,11 @@ func generateIPSCompositionSection(narrativeEngine *ips.Narrative, sectionType p
Title: &sectionTitle,
Code: &sectionCode,
}
//database.IFhirResourceModel
resources := flattenQueryResultsToResourcesList(queryResultsList)
if len(resources) == 0 {
section.EmptyReason = &fhir401.CodeableConcept{
Text: stringPtr("No data available"),
@ -246,7 +252,7 @@ func generateIPSCompositionSection(narrativeEngine *ips.Narrative, sectionType p
// https://github.com/jddamore/fhir-ips-server/blob/main/docs/Summary_Creation_Steps.md
// Generate Resource Queries for each IPS Section
func generateIPSSectionQueries(sectionType pkg.IPSSections) ([]models.QueryResource, error) {
func (gr *GormRepository) generateIPSSectionQueries(ctx context.Context, sectionType pkg.IPSSections) ([]models.QueryResource, error) {
queries := []models.QueryResource{}
switch sectionType {
@ -306,14 +312,59 @@ func generateIPSSectionQueries(sectionType pkg.IPSSections) ([]models.QueryResou
})
break
case pkg.IPSSectionsVitalSigns:
//TODO: group by code, sort by date, limit to the most recent 3
queries = append(queries, models.QueryResource{
//lets query the database for this user, getting a list of unique codes, associated with this category ('vital-signs').
//our goal is to retrieve the 3 most recent values for each code.
vitalSignsGrouped, err := gr.QueryResources(ctx, models.QueryResource{
Select: nil,
From: "Observation",
Where: map[string]interface{}{
"category": "vital-signs",
},
Aggregations: &models.QueryResourceAggregations{
GroupBy: &models.QueryResourceAggregation{Field: "code:code"},
},
})
if err != nil {
return nil, err
}
vitalSignsGroupedByCodeList, ok := vitalSignsGrouped.([]map[string]any)
if !ok {
return nil, fmt.Errorf("could not decode vital signs grouped by code")
}
//known codes related to vital signs: https://www.hl7.org/fhir/R4/valueset-observation-vitalsignresult.html#definition
vitalSignCodes := []string{
"85353-1", "9279-1", "8867-4", "2708-6", "8310-5", "8302-2", "9843-4", "29463-7", "39156-5", "85354-9", "8480-6", "8462-4", "8478-0",
}
for ndx, _ := range vitalSignsGroupedByCodeList {
//now that we have a list of codes that are tagged as vital-signs.
if labelValue, labelValueOk := vitalSignsGroupedByCodeList[ndx]["label"]; labelValueOk {
if labelValueStr, labeValueStrOk := labelValue.(*interface{}); labeValueStrOk {
vitalSignCodes = append(vitalSignCodes, (*labelValueStr).(string))
} else {
gr.Logger.Warnf("could not cast vital-sign codes to string")
}
} else {
gr.Logger.Warnf("could not retrieve vital-sign group-by clause label value")
}
}
vitalSignCodes = lo.Uniq(vitalSignCodes)
limit := 3
//group by code, sort by date, limit to the most recent 3
for ndx, _ := range vitalSignCodes {
queries = append(queries, models.QueryResource{
Select: nil,
From: "Observation",
Where: map[string]interface{}{
"code": vitalSignCodes[ndx],
},
Limit: &limit,
})
}
break
case pkg.IPSSectionsSocialHistory:
queries = append(queries, models.QueryResource{
@ -614,11 +665,38 @@ func convertUnknownInterfaceToFhirSlice(unknown interface{}) []database.IFhirRes
for ndx, _ := range fhirSlice {
results = append(results, &fhirSlice[ndx])
}
default:
log.Panicf("could not detect type for query results fhir resource list: %v", fhirSlice)
}
return results
}
// query results may be a list of database.IFhirResourceModel or a map[string][]database.IFhirResourceModel (if we're using aggregations/grouping)
func flattenQueryResultsToResourcesList(queryResultsList []any) []database.IFhirResourceModel {
resources := []database.IFhirResourceModel{}
for ndx, _ := range queryResultsList {
queryResults := queryResultsList[ndx]
switch queryResultsTyped := queryResults.(type) {
case []map[string]any:
//aggregated resources
for andx, _ := range queryResultsTyped {
queryResultsGrouped := queryResultsTyped[andx]
resources = append(resources, convertUnknownInterfaceToFhirSlice(queryResultsGrouped)...)
}
case interface{}:
//list of resources
resources = append(resources, convertUnknownInterfaceToFhirSlice(queryResultsTyped)...)
default:
log.Panicf("Unknown Resource Structure: %v", queryResultsTyped)
}
}
return resources
}
func generateIPSSectionNarrative(sectionType pkg.IPSSections, resources []models.ResourceBase) string {
return ""
}

View File

@ -30,7 +30,7 @@ Date: Immunization.occurrenceDateTime || Immunization.occurrenceString
<td>{{pluckList "text" ($entry.VaccineCode | parseList) | uniq | join "," }}</td> {{/* Immunization */}}
<td>{{pluckList "code" ($entry.Status | parseList) | uniq | join "," }}</td> {{/* Status */}}
<td th:insert="~{IpsUtilityFragments :: concatDoseNumber (list=*{getProtocolApplied()})}">Dose Number</td> {{/* Comments - TODO: use FHIRPath */}}
<td th:insert="~{IpsUtilityFragments :: renderOrganization (orgRef=*{getManufacturer()})}">Manufacturer</td> {{/* Manufacturer - TODO: use FHIRPath */}}
<td>{{pluckList "display" ($entry.Manufacturer | parseList) | uniq | join "," }}</td> {{/* Manufacturer */}}
<td>{{$entry.LotNumber}}</td> {{/* Lot Number */}}
<td th:insert="~{IpsUtilityFragments :: concat (list=*{getNote()},attr='text')}">Comments</td>
<td>{{$entry.Date | date "2006-01-02"}}</td> {{/* Performed Date */}}