From c258ab44d4afd2cc38566231ff84fe0623556e44 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sun, 1 Oct 2023 13:16:02 -0700 Subject: [PATCH 01/11] adding filter and sort dropdowns on Lab page. --- .idea/dataSources.xml | 2 +- .../report-labs/report-labs.component.html | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 5b887de87..d542e1cbc 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -14,4 +14,4 @@ - \ No newline at end of file + diff --git a/frontend/src/app/pages/report-labs/report-labs.component.html b/frontend/src/app/pages/report-labs/report-labs.component.html index 8b926f3c0..84e370d4d 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.html +++ b/frontend/src/app/pages/report-labs/report-labs.component.html @@ -13,6 +13,30 @@

Observations

+
+ + +
+ +
+ + + +
+
+
From 6d831f6ee79cd1c982c6e257f8eac9a1b0870c15 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sun, 1 Oct 2023 17:05:55 -0700 Subject: [PATCH 02/11] make sure we can send limit and offset when querying. list the diagnostic reports in the dropdown for filtering. --- .gitignore | 1 + .../pkg/database/sqlite_repository_query.go | 52 ++++++++++++------- backend/pkg/models/query_resource.go | 8 +++ .../models/widget/dashboard-widget-query.ts | 4 +- .../report-labs/report-labs.component.html | 3 +- .../report-labs/report-labs.component.ts | 27 ++++++++++ 6 files changed, 72 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 28121a83c..c5d80bfe9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .idea/**/tasks.xml .idea/**/dictionaries .idea/**/shelf +.idea/dataSources.xml # Sensitive or high-churn files .idea/**/dataSources/ diff --git a/backend/pkg/database/sqlite_repository_query.go b/backend/pkg/database/sqlite_repository_query.go index a2480e728..cd95dead7 100644 --- a/backend/pkg/database/sqlite_repository_query.go +++ b/backend/pkg/database/sqlite_repository_query.go @@ -34,19 +34,20 @@ const ( const TABLE_ALIAS = "fhir" -//Allows users to use SearchParameters to query resources +// Allows users to use SearchParameters to query resources // Can generate simple or complex queries, depending on the SearchParameter type: // // eg. Simple // -// // eg. Complex // SELECT fhir.* // FROM fhir_observation as fhir, json_each(fhir.code) as codeJson // WHERE ( +// // (codeJson.value ->> '$.code' = "29463-7" AND codeJson.value ->> '$.system' = "http://loinc.org") // OR (codeJson.value ->> '$.code' = "3141-9" AND codeJson.value ->> '$.system' = "http://loinc.org") // OR (codeJson.value ->> '$.code' = "27113001" AND codeJson.value ->> '$.system' = "http://snomed.info/sct") +// // ) // AND (user_id = "6efcd7c5-3f29-4f0d-926d-a66ff68bbfc2") // GROUP BY `fhir`.`id` @@ -142,7 +143,7 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. //defaults selectClauses := []string{fmt.Sprintf("%s.*", TABLE_ALIAS)} groupClause := fmt.Sprintf("%s.id", TABLE_ALIAS) - orderClause := fmt.Sprintf("%s.sort_date ASC", TABLE_ALIAS) + orderClause := fmt.Sprintf("%s.sort_date DESC", TABLE_ALIAS) if query.Aggregations != nil { //Handle Aggregations @@ -210,12 +211,21 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. fromClauses = lo.Uniq(fromClauses) fromClauses = lo.Compact(fromClauses) - return sr.GormClient.WithContext(ctx). + fluentQuery := sr.GormClient.WithContext(ctx). Select(strings.Join(selectClauses, ", ")). Where(strings.Join(whereClauses, " AND "), whereNamedParameters). Group(groupClause). - Order(orderClause). - Table(strings.Join(fromClauses, ", ")), nil + Order(orderClause) + + //add limit and offset clauses if present + if query.Limit != nil { + fluentQuery = fluentQuery.Limit(*query.Limit) + } + if query.Offset != nil { + fluentQuery = fluentQuery.Offset(*query.Offset) + } + + return fluentQuery.Table(strings.Join(fromClauses, ", ")), nil } /// INTERNAL functionality. These functions are exported for testing, but are not available in the Interface @@ -227,14 +237,17 @@ type SearchParameter struct { Modifier string } -//Lists in the SearchParameterValueOperatorTree are AND'd together, and items within each SearchParameterValueOperatorTree list are OR'd together -//For example, the following would be AND'd together, and then OR'd with the next SearchParameterValueOperatorTree -// { -// {SearchParameterValue{Value: "foo"}, SearchParameterValue{Value: "bar"}} -// {SearchParameterValue{Value: "baz"}}, -// } -//This would result in the following SQL: -// (value = "foo" OR value = "bar") AND (value = "baz") +// Lists in the SearchParameterValueOperatorTree are AND'd together, and items within each SearchParameterValueOperatorTree list are OR'd together +// For example, the following would be AND'd together, and then OR'd with the next SearchParameterValueOperatorTree +// +// { +// {SearchParameterValue{Value: "foo"}, SearchParameterValue{Value: "bar"}} +// {SearchParameterValue{Value: "baz"}}, +// } +// +// This would result in the following SQL: +// +// (value = "foo" OR value = "bar") AND (value = "baz") type SearchParameterValueOperatorTree [][]SearchParameterValue type SearchParameterValue struct { @@ -243,7 +256,7 @@ type SearchParameterValue struct { SecondaryValues map[string]interface{} } -//SearchParameters are made up of parameter names and modifiers. For example, "name" and "name:exact" are both valid search parameters +// SearchParameters are made up of parameter names and modifiers. For example, "name" and "name:exact" are both valid search parameters // This function will parse the searchCodeWithModifier and return the SearchParameter func ProcessSearchParameter(searchCodeWithModifier string, searchParamTypeLookup map[string]string) (SearchParameter, error) { searchParameter := SearchParameter{} @@ -284,8 +297,9 @@ func ProcessSearchParameter(searchCodeWithModifier string, searchParamTypeLookup // top level is AND'd together, and each item within the lists are OR'd together // // For example, searchParamCodeValueOrValuesWithPrefix may be: -// "code": "29463-7,3141-9,27113001" -// "code": ["le29463-7", "gt3141-9", "27113001"] +// +// "code": "29463-7,3141-9,27113001" +// "code": ["le29463-7", "gt3141-9", "27113001"] func ProcessSearchParameterValueIntoOperatorTree(searchParameter SearchParameter, searchParamCodeValueOrValuesWithPrefix interface{}) (SearchParameterValueOperatorTree, error) { searchParamCodeValuesWithPrefix := []string{} @@ -416,7 +430,7 @@ func NamedParameterWithSuffix(parameterName string, suffix string) string { return fmt.Sprintf("%s_%s", parameterName, suffix) } -//SearchCodeToWhereClause converts a searchCode and searchCodeValue to a where clause and a map of named parameters +// SearchCodeToWhereClause converts a searchCode and searchCodeValue to a where clause and a map of named parameters func SearchCodeToWhereClause(searchParam SearchParameter, searchParamValue SearchParameterValue, namedParameterSuffix string) (string, map[string]interface{}, error) { //add named parameters to the lookup map. Basically, this is a map of all the named parameters that will be used in the where clause we're generating @@ -575,7 +589,7 @@ func AggregationParameterToClause(aggParameter SearchParameter) string { } } -//ProcessAggregationParameter processes the aggregation parameters which are fields with optional properties: +// ProcessAggregationParameter processes the aggregation parameters which are fields with optional properties: // Fields that are primitive types (number, uri) must not have any property specified: // eg. `probability` // diff --git a/backend/pkg/models/query_resource.go b/backend/pkg/models/query_resource.go index acad51185..64a4dac85 100644 --- a/backend/pkg/models/query_resource.go +++ b/backend/pkg/models/query_resource.go @@ -11,6 +11,8 @@ type QueryResource struct { Select []string `json:"select"` From string `json:"from"` Where map[string]interface{} `json:"where"` + Limit *int `json:"limit,omitempty"` + Offset *int `json:"offset,omitempty"` //aggregation fields Aggregations *QueryResourceAggregations `json:"aggregations"` @@ -56,7 +58,13 @@ func (q *QueryResource) Validate() error { if strings.Contains(q.Aggregations.OrderBy, " ") { return fmt.Errorf("order_by cannot have spaces (or aliases)") } + } + if q.Limit != nil && *q.Limit < 0 { + return fmt.Errorf("'limit' must be greater than or equal to zero") + } + if q.Offset != nil && *q.Offset < 0 { + return fmt.Errorf("'offset' must be greater than or equal to zero") } return nil diff --git a/frontend/src/app/models/widget/dashboard-widget-query.ts b/frontend/src/app/models/widget/dashboard-widget-query.ts index e573e851b..a47803f05 100644 --- a/frontend/src/app/models/widget/dashboard-widget-query.ts +++ b/frontend/src/app/models/widget/dashboard-widget-query.ts @@ -3,8 +3,8 @@ export class DashboardWidgetQuery { select: string[] from: string where: {[key: string]: string | string[]} - // limit: number - // offset: number + limit?: number + offset?: number //https://lodash.com/docs/4.17.15#unionBy aggregations?: { diff --git a/frontend/src/app/pages/report-labs/report-labs.component.html b/frontend/src/app/pages/report-labs/report-labs.component.html index 84e370d4d..b43d9f7b0 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.html +++ b/frontend/src/app/pages/report-labs/report-labs.component.html @@ -22,8 +22,7 @@

Observations

- - +
diff --git a/frontend/src/app/pages/report-labs/report-labs.component.ts b/frontend/src/app/pages/report-labs/report-labs.component.ts index 0c89ed7da..6f449ed28 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.ts +++ b/frontend/src/app/pages/report-labs/report-labs.component.ts @@ -17,12 +17,39 @@ export class ReportLabsComponent implements OnInit { isEmptyReport = false + diagnosticReports: ResourceFhir[] = [] + constructor( private fastenApi: FastenApiService, ) { } ngOnInit(): void { this.loading = true + + this.fastenApi.queryResources({ + select: ["*"], + from: "DiagnosticReport", + where: { + "category": "http://terminology.hl7.org/CodeSystem/v2-0074|LAB", + }, + limit: 5, + }).subscribe(results => { + this.diagnosticReports = results.data + console.log("ALL DIAGNOSTIC REPORTS", results) + }) + + this.fastenApi.queryResources({ + select: ["*"], + from: "Observation", + where: {}, + aggregations: { + order_by: "code:code" + } + }).subscribe(results => { + console.log("OBSERVATIONS GROUPED", results) + }) + + this.fastenApi.getResources("Observation").subscribe(results => { this.loading = false results = results || [] From af2344ec00fa38a701c9696810450d02d16984af Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Sun, 1 Oct 2023 19:15:24 -0700 Subject: [PATCH 03/11] add sort_date as a DB field that can be queried. make sure that order_by is automatically desc for date fields. --- backend/pkg/database/sqlite_repository_query.go | 9 +++++++-- backend/pkg/models/database/fhir_account.go | 1 + backend/pkg/models/database/fhir_adverse_event.go | 1 + .../pkg/models/database/fhir_allergy_intolerance.go | 1 + backend/pkg/models/database/fhir_appointment.go | 1 + backend/pkg/models/database/fhir_binary.go | 1 + backend/pkg/models/database/fhir_care_plan.go | 1 + backend/pkg/models/database/fhir_care_team.go | 1 + backend/pkg/models/database/fhir_claim.go | 1 + backend/pkg/models/database/fhir_claim_response.go | 1 + backend/pkg/models/database/fhir_composition.go | 1 + backend/pkg/models/database/fhir_condition.go | 1 + backend/pkg/models/database/fhir_consent.go | 1 + backend/pkg/models/database/fhir_coverage.go | 1 + .../database/fhir_coverage_eligibility_request.go | 1 + .../database/fhir_coverage_eligibility_response.go | 1 + backend/pkg/models/database/fhir_device.go | 1 + backend/pkg/models/database/fhir_device_request.go | 1 + .../pkg/models/database/fhir_diagnostic_report.go | 1 + .../pkg/models/database/fhir_document_manifest.go | 1 + .../pkg/models/database/fhir_document_reference.go | 1 + backend/pkg/models/database/fhir_encounter.go | 1 + backend/pkg/models/database/fhir_endpoint.go | 1 + .../pkg/models/database/fhir_enrollment_request.go | 1 + .../pkg/models/database/fhir_enrollment_response.go | 1 + .../models/database/fhir_explanation_of_benefit.go | 1 + .../models/database/fhir_family_member_history.go | 1 + backend/pkg/models/database/fhir_goal.go | 1 + backend/pkg/models/database/fhir_imaging_study.go | 1 + backend/pkg/models/database/fhir_immunization.go | 1 + backend/pkg/models/database/fhir_insurance_plan.go | 1 + backend/pkg/models/database/fhir_location.go | 1 + backend/pkg/models/database/fhir_media.go | 1 + backend/pkg/models/database/fhir_medication.go | 1 + .../database/fhir_medication_administration.go | 1 + .../pkg/models/database/fhir_medication_dispense.go | 1 + .../pkg/models/database/fhir_medication_request.go | 1 + .../models/database/fhir_medication_statement.go | 1 + backend/pkg/models/database/fhir_nutrition_order.go | 1 + backend/pkg/models/database/fhir_observation.go | 1 + backend/pkg/models/database/fhir_organization.go | 1 + .../database/fhir_organization_affiliation.go | 1 + backend/pkg/models/database/fhir_patient.go | 1 + backend/pkg/models/database/fhir_person.go | 1 + backend/pkg/models/database/fhir_practitioner.go | 1 + .../pkg/models/database/fhir_practitioner_role.go | 1 + backend/pkg/models/database/fhir_procedure.go | 1 + backend/pkg/models/database/fhir_provenance.go | 1 + backend/pkg/models/database/fhir_questionnaire.go | 1 + .../models/database/fhir_questionnaire_response.go | 1 + backend/pkg/models/database/fhir_related_person.go | 1 + backend/pkg/models/database/fhir_schedule.go | 1 + backend/pkg/models/database/fhir_service_request.go | 1 + backend/pkg/models/database/fhir_slot.go | 1 + backend/pkg/models/database/fhir_specimen.go | 1 + .../pkg/models/database/fhir_vision_prescription.go | 1 + backend/pkg/models/database/generate.go | 13 +++++++------ 57 files changed, 69 insertions(+), 8 deletions(-) diff --git a/backend/pkg/database/sqlite_repository_query.go b/backend/pkg/database/sqlite_repository_query.go index cd95dead7..36832008f 100644 --- a/backend/pkg/database/sqlite_repository_query.go +++ b/backend/pkg/database/sqlite_repository_query.go @@ -162,7 +162,7 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. //process order by clause if len(query.Aggregations.OrderBy) > 0 { - orderAsc := true + orderAsc := true //default to ascending, switch to desc if parameter is a date type. if !strings.HasPrefix(query.Aggregations.OrderBy, "count(*)") { orderAggregationParam, err := ProcessAggregationParameter(query.Aggregations.OrderBy, searchCodeToTypeLookup) if err != nil { @@ -174,6 +174,11 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. } fromClauses = append(fromClauses, orderAggregationFromClause) + //if the order by is a date type, we need to order by DESC (most recent first) + if orderAggregationParam.Type == SearchParameterTypeDate { + orderAsc = false + } + orderClause = AggregationParameterToClause(orderAggregationParam) if orderAsc { orderClause = fmt.Sprintf("%s ASC", orderClause) @@ -619,7 +624,7 @@ func ProcessAggregationParameter(aggregationFieldWithProperty string, searchPara } //primitive types should not have a modifier, we need to throw an error - if aggregationParameter.Type == SearchParameterTypeNumber || aggregationParameter.Type == SearchParameterTypeUri || aggregationParameter.Type == SearchParameterTypeKeyword { + if aggregationParameter.Type == SearchParameterTypeNumber || aggregationParameter.Type == SearchParameterTypeUri || aggregationParameter.Type == SearchParameterTypeKeyword || aggregationParameter.Type == SearchParameterTypeDate { if len(aggregationParameter.Modifier) > 0 { return aggregationParameter, fmt.Errorf("primitive aggregation parameter %s cannot have a property (%s)", aggregationParameter.Name, aggregationParameter.Modifier) } diff --git a/backend/pkg/models/database/fhir_account.go b/backend/pkg/models/database/fhir_account.go index dcee74941..4671e2872 100644 --- a/backend/pkg/models/database/fhir_account.go +++ b/backend/pkg/models/database/fhir_account.go @@ -62,6 +62,7 @@ func (s *FhirAccount) GetSearchParameters() map[string]string { "owner": "reference", "period": "date", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_adverse_event.go b/backend/pkg/models/database/fhir_adverse_event.go index 74d1eaf65..d9e74ffca 100644 --- a/backend/pkg/models/database/fhir_adverse_event.go +++ b/backend/pkg/models/database/fhir_adverse_event.go @@ -85,6 +85,7 @@ func (s *FhirAdverseEvent) GetSearchParameters() map[string]string { "resultingcondition": "reference", "seriousness": "token", "severity": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_allergy_intolerance.go b/backend/pkg/models/database/fhir_allergy_intolerance.go index 505db41aa..0835409ae 100644 --- a/backend/pkg/models/database/fhir_allergy_intolerance.go +++ b/backend/pkg/models/database/fhir_allergy_intolerance.go @@ -165,6 +165,7 @@ func (s *FhirAllergyIntolerance) GetSearchParameters() map[string]string { "recorder": "reference", "route": "token", "severity": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_appointment.go b/backend/pkg/models/database/fhir_appointment.go index ff1a69530..ecfc61edb 100644 --- a/backend/pkg/models/database/fhir_appointment.go +++ b/backend/pkg/models/database/fhir_appointment.go @@ -101,6 +101,7 @@ func (s *FhirAppointment) GetSearchParameters() map[string]string { "serviceCategory": "token", "serviceType": "token", "slot": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_binary.go b/backend/pkg/models/database/fhir_binary.go index 0822a213f..1ad1f8fea 100644 --- a/backend/pkg/models/database/fhir_binary.go +++ b/backend/pkg/models/database/fhir_binary.go @@ -40,6 +40,7 @@ func (s *FhirBinary) GetSearchParameters() map[string]string { "language": "token", "lastUpdated": "date", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_care_plan.go b/backend/pkg/models/database/fhir_care_plan.go index 1652fb732..a70720b94 100644 --- a/backend/pkg/models/database/fhir_care_plan.go +++ b/backend/pkg/models/database/fhir_care_plan.go @@ -167,6 +167,7 @@ func (s *FhirCarePlan) GetSearchParameters() map[string]string { "performer": "reference", "profile": "reference", "replaces": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_care_team.go b/backend/pkg/models/database/fhir_care_team.go index b158e16f4..ed31a6446 100644 --- a/backend/pkg/models/database/fhir_care_team.go +++ b/backend/pkg/models/database/fhir_care_team.go @@ -119,6 +119,7 @@ func (s *FhirCareTeam) GetSearchParameters() map[string]string { "lastUpdated": "date", "participant": "reference", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_claim.go b/backend/pkg/models/database/fhir_claim.go index 4913a654d..36fe8a129 100644 --- a/backend/pkg/models/database/fhir_claim.go +++ b/backend/pkg/models/database/fhir_claim.go @@ -101,6 +101,7 @@ func (s *FhirClaim) GetSearchParameters() map[string]string { "procedureUdi": "reference", "profile": "reference", "provider": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_claim_response.go b/backend/pkg/models/database/fhir_claim_response.go index 4328773c6..130b09ee8 100644 --- a/backend/pkg/models/database/fhir_claim_response.go +++ b/backend/pkg/models/database/fhir_claim_response.go @@ -78,6 +78,7 @@ func (s *FhirClaimResponse) GetSearchParameters() map[string]string { "profile": "reference", "request": "reference", "requestor": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_composition.go b/backend/pkg/models/database/fhir_composition.go index 382dc7063..8ba2f4b7a 100644 --- a/backend/pkg/models/database/fhir_composition.go +++ b/backend/pkg/models/database/fhir_composition.go @@ -169,6 +169,7 @@ func (s *FhirComposition) GetSearchParameters() map[string]string { "relatedId": "token", "relatedRef": "reference", "section": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_condition.go b/backend/pkg/models/database/fhir_condition.go index 678f948b1..bbd8eb223 100644 --- a/backend/pkg/models/database/fhir_condition.go +++ b/backend/pkg/models/database/fhir_condition.go @@ -167,6 +167,7 @@ func (s *FhirCondition) GetSearchParameters() map[string]string { "profile": "reference", "recordedDate": "date", "severity": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_consent.go b/backend/pkg/models/database/fhir_consent.go index c4de0055e..d9173f455 100644 --- a/backend/pkg/models/database/fhir_consent.go +++ b/backend/pkg/models/database/fhir_consent.go @@ -147,6 +147,7 @@ func (s *FhirConsent) GetSearchParameters() map[string]string { "purpose": "token", "scope": "token", "securityLabel": "token", + "sort_date": "date", "sourceReference": "reference", "source_id": "keyword", "source_resource_id": "keyword", diff --git a/backend/pkg/models/database/fhir_coverage.go b/backend/pkg/models/database/fhir_coverage.go index 99c1e15fb..b74c6b1ee 100644 --- a/backend/pkg/models/database/fhir_coverage.go +++ b/backend/pkg/models/database/fhir_coverage.go @@ -74,6 +74,7 @@ func (s *FhirCoverage) GetSearchParameters() map[string]string { "payor": "reference", "policyHolder": "reference", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_coverage_eligibility_request.go b/backend/pkg/models/database/fhir_coverage_eligibility_request.go index 1f49ea143..df6719541 100644 --- a/backend/pkg/models/database/fhir_coverage_eligibility_request.go +++ b/backend/pkg/models/database/fhir_coverage_eligibility_request.go @@ -63,6 +63,7 @@ func (s *FhirCoverageEligibilityRequest) GetSearchParameters() map[string]string "lastUpdated": "date", "profile": "reference", "provider": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_coverage_eligibility_response.go b/backend/pkg/models/database/fhir_coverage_eligibility_response.go index a99e7df6c..894b879e7 100644 --- a/backend/pkg/models/database/fhir_coverage_eligibility_response.go +++ b/backend/pkg/models/database/fhir_coverage_eligibility_response.go @@ -71,6 +71,7 @@ func (s *FhirCoverageEligibilityResponse) GetSearchParameters() map[string]strin "profile": "reference", "request": "reference", "requestor": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_device.go b/backend/pkg/models/database/fhir_device.go index 63403ae50..e89cc32c3 100644 --- a/backend/pkg/models/database/fhir_device.go +++ b/backend/pkg/models/database/fhir_device.go @@ -76,6 +76,7 @@ func (s *FhirDevice) GetSearchParameters() map[string]string { "model": "string", "organization": "reference", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_device_request.go b/backend/pkg/models/database/fhir_device_request.go index 02411eb5b..e08015a61 100644 --- a/backend/pkg/models/database/fhir_device_request.go +++ b/backend/pkg/models/database/fhir_device_request.go @@ -171,6 +171,7 @@ func (s *FhirDeviceRequest) GetSearchParameters() map[string]string { "priorRequest": "reference", "profile": "reference", "requester": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_diagnostic_report.go b/backend/pkg/models/database/fhir_diagnostic_report.go index 9757362a3..2f9bf83d3 100644 --- a/backend/pkg/models/database/fhir_diagnostic_report.go +++ b/backend/pkg/models/database/fhir_diagnostic_report.go @@ -182,6 +182,7 @@ func (s *FhirDiagnosticReport) GetSearchParameters() map[string]string { "profile": "reference", "result": "reference", "resultsInterpreter": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_document_manifest.go b/backend/pkg/models/database/fhir_document_manifest.go index 6d76cbbb5..260cb34cc 100644 --- a/backend/pkg/models/database/fhir_document_manifest.go +++ b/backend/pkg/models/database/fhir_document_manifest.go @@ -114,6 +114,7 @@ func (s *FhirDocumentManifest) GetSearchParameters() map[string]string { "recipient": "reference", "relatedId": "token", "relatedRef": "reference", + "sort_date": "date", "source": "uri", "source_id": "keyword", "source_resource_id": "keyword", diff --git a/backend/pkg/models/database/fhir_document_reference.go b/backend/pkg/models/database/fhir_document_reference.go index 07fe9c7dc..4a2b77093 100644 --- a/backend/pkg/models/database/fhir_document_reference.go +++ b/backend/pkg/models/database/fhir_document_reference.go @@ -170,6 +170,7 @@ func (s *FhirDocumentReference) GetSearchParameters() map[string]string { "relation": "token", "securityLabel": "token", "setting": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_encounter.go b/backend/pkg/models/database/fhir_encounter.go index 7a16bcd62..f779aa7b9 100644 --- a/backend/pkg/models/database/fhir_encounter.go +++ b/backend/pkg/models/database/fhir_encounter.go @@ -174,6 +174,7 @@ func (s *FhirEncounter) GetSearchParameters() map[string]string { "reasonCode": "token", "reasonReference": "reference", "serviceProvider": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_endpoint.go b/backend/pkg/models/database/fhir_endpoint.go index 0cc339552..189f80bcd 100644 --- a/backend/pkg/models/database/fhir_endpoint.go +++ b/backend/pkg/models/database/fhir_endpoint.go @@ -63,6 +63,7 @@ func (s *FhirEndpoint) GetSearchParameters() map[string]string { "organization": "reference", "payloadType": "token", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_enrollment_request.go b/backend/pkg/models/database/fhir_enrollment_request.go index 5128441ff..ea43a2095 100644 --- a/backend/pkg/models/database/fhir_enrollment_request.go +++ b/backend/pkg/models/database/fhir_enrollment_request.go @@ -50,6 +50,7 @@ func (s *FhirEnrollmentRequest) GetSearchParameters() map[string]string { "language": "token", "lastUpdated": "date", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_enrollment_response.go b/backend/pkg/models/database/fhir_enrollment_response.go index e55324217..72630aeca 100644 --- a/backend/pkg/models/database/fhir_enrollment_response.go +++ b/backend/pkg/models/database/fhir_enrollment_response.go @@ -51,6 +51,7 @@ func (s *FhirEnrollmentResponse) GetSearchParameters() map[string]string { "lastUpdated": "date", "profile": "reference", "request": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_explanation_of_benefit.go b/backend/pkg/models/database/fhir_explanation_of_benefit.go index 9f74ba197..5ffaceed9 100644 --- a/backend/pkg/models/database/fhir_explanation_of_benefit.go +++ b/backend/pkg/models/database/fhir_explanation_of_benefit.go @@ -102,6 +102,7 @@ func (s *FhirExplanationOfBenefit) GetSearchParameters() map[string]string { "procedureUdi": "reference", "profile": "reference", "provider": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_family_member_history.go b/backend/pkg/models/database/fhir_family_member_history.go index e9d2708c6..4235b2482 100644 --- a/backend/pkg/models/database/fhir_family_member_history.go +++ b/backend/pkg/models/database/fhir_family_member_history.go @@ -141,6 +141,7 @@ func (s *FhirFamilyMemberHistory) GetSearchParameters() map[string]string { "profile": "reference", "relationship": "token", "sex": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_goal.go b/backend/pkg/models/database/fhir_goal.go index 661447d63..0bd30d31f 100644 --- a/backend/pkg/models/database/fhir_goal.go +++ b/backend/pkg/models/database/fhir_goal.go @@ -98,6 +98,7 @@ func (s *FhirGoal) GetSearchParameters() map[string]string { "lastUpdated": "date", "lifecycleStatus": "token", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_imaging_study.go b/backend/pkg/models/database/fhir_imaging_study.go index fa34de7e9..b4dab0b12 100644 --- a/backend/pkg/models/database/fhir_imaging_study.go +++ b/backend/pkg/models/database/fhir_imaging_study.go @@ -134,6 +134,7 @@ func (s *FhirImagingStudy) GetSearchParameters() map[string]string { "reason": "token", "referrer": "reference", "series": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_immunization.go b/backend/pkg/models/database/fhir_immunization.go index 67ad23d15..e79a91f71 100644 --- a/backend/pkg/models/database/fhir_immunization.go +++ b/backend/pkg/models/database/fhir_immunization.go @@ -149,6 +149,7 @@ func (s *FhirImmunization) GetSearchParameters() map[string]string { "reasonCode": "token", "reasonReference": "reference", "series": "string", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_insurance_plan.go b/backend/pkg/models/database/fhir_insurance_plan.go index 151608965..36b6355f5 100644 --- a/backend/pkg/models/database/fhir_insurance_plan.go +++ b/backend/pkg/models/database/fhir_insurance_plan.go @@ -91,6 +91,7 @@ func (s *FhirInsurancePlan) GetSearchParameters() map[string]string { "ownedBy": "reference", "phonetic": "string", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_location.go b/backend/pkg/models/database/fhir_location.go index 5967d568e..61c5f78ab 100644 --- a/backend/pkg/models/database/fhir_location.go +++ b/backend/pkg/models/database/fhir_location.go @@ -91,6 +91,7 @@ func (s *FhirLocation) GetSearchParameters() map[string]string { "organization": "reference", "partof": "reference", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_media.go b/backend/pkg/models/database/fhir_media.go index 255d49674..a5d16c62f 100644 --- a/backend/pkg/models/database/fhir_media.go +++ b/backend/pkg/models/database/fhir_media.go @@ -81,6 +81,7 @@ func (s *FhirMedia) GetSearchParameters() map[string]string { "operator": "reference", "profile": "reference", "site": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_medication.go b/backend/pkg/models/database/fhir_medication.go index f4a0b5c4e..cdb6c2a20 100644 --- a/backend/pkg/models/database/fhir_medication.go +++ b/backend/pkg/models/database/fhir_medication.go @@ -92,6 +92,7 @@ func (s *FhirMedication) GetSearchParameters() map[string]string { "lotNumber": "token", "manufacturer": "reference", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_medication_administration.go b/backend/pkg/models/database/fhir_medication_administration.go index 74dc39a7c..19aa216c1 100644 --- a/backend/pkg/models/database/fhir_medication_administration.go +++ b/backend/pkg/models/database/fhir_medication_administration.go @@ -150,6 +150,7 @@ func (s *FhirMedicationAdministration) GetSearchParameters() map[string]string { "reasonGiven": "token", "reasonNotGiven": "token", "request": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_medication_dispense.go b/backend/pkg/models/database/fhir_medication_dispense.go index 0c67965df..70f8446ac 100644 --- a/backend/pkg/models/database/fhir_medication_dispense.go +++ b/backend/pkg/models/database/fhir_medication_dispense.go @@ -156,6 +156,7 @@ func (s *FhirMedicationDispense) GetSearchParameters() map[string]string { "profile": "reference", "receiver": "reference", "responsibleparty": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_medication_request.go b/backend/pkg/models/database/fhir_medication_request.go index b872cbcd1..ef0581a96 100644 --- a/backend/pkg/models/database/fhir_medication_request.go +++ b/backend/pkg/models/database/fhir_medication_request.go @@ -170,6 +170,7 @@ func (s *FhirMedicationRequest) GetSearchParameters() map[string]string { "priority": "token", "profile": "reference", "requester": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_medication_statement.go b/backend/pkg/models/database/fhir_medication_statement.go index 0caadf812..f4ffb143e 100644 --- a/backend/pkg/models/database/fhir_medication_statement.go +++ b/backend/pkg/models/database/fhir_medication_statement.go @@ -141,6 +141,7 @@ func (s *FhirMedicationStatement) GetSearchParameters() map[string]string { "medication": "reference", "partOf": "reference", "profile": "reference", + "sort_date": "date", "source": "reference", "source_id": "keyword", "source_resource_id": "keyword", diff --git a/backend/pkg/models/database/fhir_nutrition_order.go b/backend/pkg/models/database/fhir_nutrition_order.go index a4d8d870b..6e658ef62 100644 --- a/backend/pkg/models/database/fhir_nutrition_order.go +++ b/backend/pkg/models/database/fhir_nutrition_order.go @@ -130,6 +130,7 @@ func (s *FhirNutritionOrder) GetSearchParameters() map[string]string { "oraldiet": "token", "profile": "reference", "provider": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_observation.go b/backend/pkg/models/database/fhir_observation.go index 3ca5ddfc5..aeb73b468 100644 --- a/backend/pkg/models/database/fhir_observation.go +++ b/backend/pkg/models/database/fhir_observation.go @@ -234,6 +234,7 @@ func (s *FhirObservation) GetSearchParameters() map[string]string { "partOf": "reference", "performer": "reference", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_organization.go b/backend/pkg/models/database/fhir_organization.go index 1ea42fa28..c003a5468 100644 --- a/backend/pkg/models/database/fhir_organization.go +++ b/backend/pkg/models/database/fhir_organization.go @@ -88,6 +88,7 @@ func (s *FhirOrganization) GetSearchParameters() map[string]string { "partof": "reference", "phonetic": "string", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_organization_affiliation.go b/backend/pkg/models/database/fhir_organization_affiliation.go index 140389ca2..7c02c0287 100644 --- a/backend/pkg/models/database/fhir_organization_affiliation.go +++ b/backend/pkg/models/database/fhir_organization_affiliation.go @@ -94,6 +94,7 @@ func (s *FhirOrganizationAffiliation) GetSearchParameters() map[string]string { "profile": "reference", "role": "token", "service": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_patient.go b/backend/pkg/models/database/fhir_patient.go index d31332ef4..8c3b21fc9 100644 --- a/backend/pkg/models/database/fhir_patient.go +++ b/backend/pkg/models/database/fhir_patient.go @@ -223,6 +223,7 @@ func (s *FhirPatient) GetSearchParameters() map[string]string { "phone": "token", "phonetic": "string", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_person.go b/backend/pkg/models/database/fhir_person.go index fe68cf26d..bbb4cf361 100644 --- a/backend/pkg/models/database/fhir_person.go +++ b/backend/pkg/models/database/fhir_person.go @@ -197,6 +197,7 @@ func (s *FhirPerson) GetSearchParameters() map[string]string { "practitioner": "reference", "profile": "reference", "relatedperson": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_practitioner.go b/backend/pkg/models/database/fhir_practitioner.go index 53c7d47b1..f0df49476 100644 --- a/backend/pkg/models/database/fhir_practitioner.go +++ b/backend/pkg/models/database/fhir_practitioner.go @@ -197,6 +197,7 @@ func (s *FhirPractitioner) GetSearchParameters() map[string]string { "phone": "token", "phonetic": "string", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_practitioner_role.go b/backend/pkg/models/database/fhir_practitioner_role.go index aaa4a8e6a..4c41f6912 100644 --- a/backend/pkg/models/database/fhir_practitioner_role.go +++ b/backend/pkg/models/database/fhir_practitioner_role.go @@ -114,6 +114,7 @@ func (s *FhirPractitionerRole) GetSearchParameters() map[string]string { "profile": "reference", "role": "token", "service": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_procedure.go b/backend/pkg/models/database/fhir_procedure.go index 01de05e79..1f7237d24 100644 --- a/backend/pkg/models/database/fhir_procedure.go +++ b/backend/pkg/models/database/fhir_procedure.go @@ -183,6 +183,7 @@ func (s *FhirProcedure) GetSearchParameters() map[string]string { "profile": "reference", "reasonCode": "token", "reasonReference": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_provenance.go b/backend/pkg/models/database/fhir_provenance.go index 7e456ba5c..38ec9ebdb 100644 --- a/backend/pkg/models/database/fhir_provenance.go +++ b/backend/pkg/models/database/fhir_provenance.go @@ -74,6 +74,7 @@ func (s *FhirProvenance) GetSearchParameters() map[string]string { "profile": "reference", "recorded": "date", "signatureType": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_questionnaire.go b/backend/pkg/models/database/fhir_questionnaire.go index 95184e919..4e6bf6d99 100644 --- a/backend/pkg/models/database/fhir_questionnaire.go +++ b/backend/pkg/models/database/fhir_questionnaire.go @@ -103,6 +103,7 @@ func (s *FhirQuestionnaire) GetSearchParameters() map[string]string { "name": "string", "profile": "reference", "publisher": "string", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_questionnaire_response.go b/backend/pkg/models/database/fhir_questionnaire_response.go index dafa8ceaf..fc1daab1e 100644 --- a/backend/pkg/models/database/fhir_questionnaire_response.go +++ b/backend/pkg/models/database/fhir_questionnaire_response.go @@ -77,6 +77,7 @@ func (s *FhirQuestionnaireResponse) GetSearchParameters() map[string]string { "partOf": "reference", "profile": "reference", "questionnaire": "reference", + "sort_date": "date", "source": "reference", "source_id": "keyword", "source_resource_id": "keyword", diff --git a/backend/pkg/models/database/fhir_related_person.go b/backend/pkg/models/database/fhir_related_person.go index 8e8573ebe..2f87ae3d8 100644 --- a/backend/pkg/models/database/fhir_related_person.go +++ b/backend/pkg/models/database/fhir_related_person.go @@ -189,6 +189,7 @@ func (s *FhirRelatedPerson) GetSearchParameters() map[string]string { "phonetic": "string", "profile": "reference", "relationship": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_schedule.go b/backend/pkg/models/database/fhir_schedule.go index 41e56589a..2651d0e21 100644 --- a/backend/pkg/models/database/fhir_schedule.go +++ b/backend/pkg/models/database/fhir_schedule.go @@ -67,6 +67,7 @@ func (s *FhirSchedule) GetSearchParameters() map[string]string { "profile": "reference", "serviceCategory": "token", "serviceType": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_service_request.go b/backend/pkg/models/database/fhir_service_request.go index 2860bedb3..54d9c9942 100644 --- a/backend/pkg/models/database/fhir_service_request.go +++ b/backend/pkg/models/database/fhir_service_request.go @@ -182,6 +182,7 @@ func (s *FhirServiceRequest) GetSearchParameters() map[string]string { "replaces": "reference", "requester": "reference", "requisition": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_slot.go b/backend/pkg/models/database/fhir_slot.go index abdf0e7dc..5820d5ecc 100644 --- a/backend/pkg/models/database/fhir_slot.go +++ b/backend/pkg/models/database/fhir_slot.go @@ -69,6 +69,7 @@ func (s *FhirSlot) GetSearchParameters() map[string]string { "schedule": "reference", "serviceCategory": "token", "serviceType": "token", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_specimen.go b/backend/pkg/models/database/fhir_specimen.go index 55eb9b271..a63a2480d 100644 --- a/backend/pkg/models/database/fhir_specimen.go +++ b/backend/pkg/models/database/fhir_specimen.go @@ -78,6 +78,7 @@ func (s *FhirSpecimen) GetSearchParameters() map[string]string { "lastUpdated": "date", "parent": "reference", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/fhir_vision_prescription.go b/backend/pkg/models/database/fhir_vision_prescription.go index 4bced74f8..282c50a9a 100644 --- a/backend/pkg/models/database/fhir_vision_prescription.go +++ b/backend/pkg/models/database/fhir_vision_prescription.go @@ -107,6 +107,7 @@ func (s *FhirVisionPrescription) GetSearchParameters() map[string]string { "lastUpdated": "date", "prescriber": "reference", "profile": "reference", + "sort_date": "date", "source_id": "keyword", "source_resource_id": "keyword", "source_resource_type": "keyword", diff --git a/backend/pkg/models/database/generate.go b/backend/pkg/models/database/generate.go index f629eb270..54b553c33 100644 --- a/backend/pkg/models/database/generate.go +++ b/backend/pkg/models/database/generate.go @@ -209,6 +209,7 @@ func main() { d[jen.Lit("source_uri")] = jen.Lit("keyword") d[jen.Lit("source_resource_id")] = jen.Lit("keyword") d[jen.Lit("source_resource_type")] = jen.Lit("keyword") + d[jen.Lit("sort_date")] = jen.Lit("date") })) g.Return(jen.Id("searchParameters")) @@ -563,8 +564,8 @@ func main() { } -//TODO: should we do this, or allow all resources instead of just USCore? -//The dataabase would be full of empty data, but we'd be more flexible & future-proof.. supporting other countries, etc. +// TODO: should we do this, or allow all resources instead of just USCore? +// The dataabase would be full of empty data, but we'd be more flexible & future-proof.. supporting other countries, etc. var AllowedResources = []string{ "Account", "AdverseEvent", @@ -623,7 +624,7 @@ var AllowedResources = []string{ "VisionPrescription", } -//simple field types are not json encoded in the DB and are always single values (not arrays) +// simple field types are not json encoded in the DB and are always single values (not arrays) func isSimpleFieldType(fieldType string) bool { switch fieldType { case "number", "uri", "date": @@ -636,8 +637,8 @@ func isSimpleFieldType(fieldType string) bool { return true } -//https://hl7.org/fhir/search.html#token -//https://hl7.org/fhir/r4/valueset-search-param-type.html +// https://hl7.org/fhir/search.html#token +// https://hl7.org/fhir/r4/valueset-search-param-type.html func mapFieldType(fieldType string) string { switch fieldType { case "number": @@ -661,7 +662,7 @@ func mapFieldType(fieldType string) string { } } -//https://www.sqlite.org/datatype3.html +// https://www.sqlite.org/datatype3.html func mapGormType(fieldType string) string { // gorm:"type:text;serializer:json" From 01c293bf40df8eb36802080671cc6884788a3dfa Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 2 Oct 2023 13:42:41 -0700 Subject: [PATCH 04/11] provide mechanism to call functions when doing aggregations. provide a standardized format for token aggregation ($.system|$.code) --- .../pkg/database/sqlite_repository_query.go | 112 ++++++++++----- .../sqlite_repository_query_sql_test.go | 135 ++++++++++++++++-- backend/pkg/models/query_resource.go | 55 ++++--- backend/pkg/models/query_resource_test.go | 17 ++- .../pkg/web/handler/dashboard/default.json | 6 +- 5 files changed, 257 insertions(+), 68 deletions(-) diff --git a/backend/pkg/database/sqlite_repository_query.go b/backend/pkg/database/sqlite_repository_query.go index 36832008f..3740c8500 100644 --- a/backend/pkg/database/sqlite_repository_query.go +++ b/backend/pkg/database/sqlite_repository_query.go @@ -18,18 +18,19 @@ import ( type SearchParameterType string const ( - SearchParameterTypeNumber SearchParameterType = "number" - SearchParameterTypeDate SearchParameterType = "date" + //simple types + SearchParameterTypeNumber SearchParameterType = "number" + SearchParameterTypeDate SearchParameterType = "date" + SearchParameterTypeUri SearchParameterType = "uri" + SearchParameterTypeKeyword SearchParameterType = "keyword" //this is a literal/string primitive. + + //complex types SearchParameterTypeString SearchParameterType = "string" SearchParameterTypeToken SearchParameterType = "token" SearchParameterTypeReference SearchParameterType = "reference" - SearchParameterTypeUri SearchParameterType = "uri" SearchParameterTypeQuantity SearchParameterType = "quantity" SearchParameterTypeComposite SearchParameterType = "composite" SearchParameterTypeSpecial SearchParameterType = "special" - - SearchParameterTypeKeyword SearchParameterType = "keyword" //this is a literal/string primitive. - ) const TABLE_ALIAS = "fhir" @@ -58,7 +59,7 @@ func (sr *SqliteRepository) QueryResources(ctx context.Context, query models.Que return nil, err } - if query.Aggregations != nil && (len(query.Aggregations.GroupBy) > 0 || len(query.Aggregations.CountBy) > 0) { + if query.Aggregations != nil && (query.Aggregations.GroupBy != nil || query.Aggregations.CountBy != nil) { results := []map[string]interface{}{} clientResp := sqlQuery.Find(&results) return results, clientResp.Error @@ -148,27 +149,30 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. //Handle Aggregations - if len(query.Aggregations.CountBy) > 0 { + if query.Aggregations.CountBy != nil { //populate the group by and order by clause with the count by values - query.Aggregations.OrderBy = "count(*) DESC" + query.Aggregations.OrderBy = &models.QueryResourceAggregation{ + Field: "*", + Function: "count", + } query.Aggregations.GroupBy = query.Aggregations.CountBy - if query.Aggregations.GroupBy == "*" { + if query.Aggregations.GroupBy.Field == "*" { //we need to get the count of all resources, so we need to remove the group by clause and replace it by // `source_resource_type` which will be the same for all resources - query.Aggregations.GroupBy = "source_resource_type" + query.Aggregations.GroupBy.Field = "source_resource_type" } } //process order by clause - if len(query.Aggregations.OrderBy) > 0 { + if query.Aggregations.OrderBy != nil { orderAsc := true //default to ascending, switch to desc if parameter is a date type. - if !strings.HasPrefix(query.Aggregations.OrderBy, "count(*)") { - orderAggregationParam, err := ProcessAggregationParameter(query.Aggregations.OrderBy, searchCodeToTypeLookup) + if !(query.Aggregations.OrderBy.Field == "*") { + orderAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.OrderBy, searchCodeToTypeLookup) if err != nil { return nil, err } - orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam) + orderAggregationFromClause, err := SearchCodeToFromClause(orderAggregationParam.SearchParameter) if err != nil { return nil, err } @@ -186,17 +190,17 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. orderClause = fmt.Sprintf("%s DESC", orderClause) } } else { - orderClause = query.Aggregations.OrderBy + orderClause = fmt.Sprintf("%s(%s) DESC", query.Aggregations.OrderBy.Function, query.Aggregations.OrderBy.Field) } } //process group by clause - if len(query.Aggregations.GroupBy) > 0 { - groupAggregationParam, err := ProcessAggregationParameter(query.Aggregations.GroupBy, searchCodeToTypeLookup) + if query.Aggregations.GroupBy != nil { + groupAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.GroupBy, searchCodeToTypeLookup) if err != nil { return nil, err } - groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam) + groupAggregationFromClause, err := SearchCodeToFromClause(groupAggregationParam.SearchParameter) if err != nil { return nil, err } @@ -205,8 +209,22 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. groupClause = AggregationParameterToClause(groupAggregationParam) selectClauses = []string{ fmt.Sprintf("%s as %s", groupClause, "label"), - "count(*) as value", } + + if query.Aggregations.OrderBy == nil || query.Aggregations.OrderBy.Field == "*" { + selectClauses = append(selectClauses, fmt.Sprintf("%s as %s", "count(*)", "value")) + orderClause = fmt.Sprintf("%s DESC", "count(*)") + } else { + //use the orderBy aggregation as the value + orderAggregationParam, err := ProcessAggregationParameter(*query.Aggregations.OrderBy, searchCodeToTypeLookup) + if err != nil { + return nil, err + } + + orderSelectClause := AggregationParameterToClause(orderAggregationParam) + selectClauses = append(selectClauses, fmt.Sprintf("%s as %s", orderSelectClause, "value")) + } + } } @@ -216,21 +234,22 @@ func (sr *SqliteRepository) sqlQueryResources(ctx context.Context, query models. fromClauses = lo.Uniq(fromClauses) fromClauses = lo.Compact(fromClauses) - fluentQuery := sr.GormClient.WithContext(ctx). + sqlQuery := sr.GormClient.WithContext(ctx). Select(strings.Join(selectClauses, ", ")). Where(strings.Join(whereClauses, " AND "), whereNamedParameters). Group(groupClause). - Order(orderClause) + Order(orderClause). + Table(strings.Join(fromClauses, ", ")) //add limit and offset clauses if present if query.Limit != nil { - fluentQuery = fluentQuery.Limit(*query.Limit) + sqlQuery = sqlQuery.Limit(*query.Limit) } if query.Offset != nil { - fluentQuery = fluentQuery.Offset(*query.Offset) + sqlQuery = sqlQuery.Offset(*query.Offset) } - return fluentQuery.Table(strings.Join(fromClauses, ", ")), nil + return sqlQuery, nil } /// INTERNAL functionality. These functions are exported for testing, but are not available in the Interface @@ -242,6 +261,11 @@ type SearchParameter struct { Modifier string } +type AggregationParameter struct { + SearchParameter + Function string //count, sum, avg, min, max, etc +} + // Lists in the SearchParameterValueOperatorTree are AND'd together, and items within each SearchParameterValueOperatorTree list are OR'd together // For example, the following would be AND'd together, and then OR'd with the next SearchParameterValueOperatorTree // @@ -584,14 +608,31 @@ func SearchCodeToFromClause(searchParam SearchParameter) (string, error) { return "", nil } -func AggregationParameterToClause(aggParameter SearchParameter) string { +func AggregationParameterToClause(aggParameter AggregationParameter) string { + var clause string + switch aggParameter.Type { - case SearchParameterTypeQuantity, SearchParameterTypeToken, SearchParameterTypeString: + case SearchParameterTypeQuantity, SearchParameterTypeString: //setup the clause - return fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier) + clause = fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier) + case SearchParameterTypeToken: + //modifier is optional for token types. + if aggParameter.Modifier != "" { + clause = fmt.Sprintf("(%sJson.value ->> '$.%s')", aggParameter.Name, aggParameter.Modifier) + } else { + //if no modifier is specified, use the system and code to generate the clause + //((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code')) + clause = fmt.Sprintf("((%sJson.value ->> '$.system') || '|' || (%sJson.value ->> '$.code'))", aggParameter.Name, aggParameter.Name) + } + default: - return fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name) + clause = fmt.Sprintf("%s.%s", TABLE_ALIAS, aggParameter.Name) } + + if len(aggParameter.Function) > 0 { + clause = fmt.Sprintf("%s(%s)", aggParameter.Function, clause) + } + return clause } // ProcessAggregationParameter processes the aggregation parameters which are fields with optional properties: @@ -602,12 +643,15 @@ func AggregationParameterToClause(aggParameter SearchParameter) string { // eg. `identifier:code` // // if the a property is specified, its set as the modifier, and used when generating the SQL query groupBy, orderBy, etc clause -func ProcessAggregationParameter(aggregationFieldWithProperty string, searchParamTypeLookup map[string]string) (SearchParameter, error) { - aggregationParameter := SearchParameter{} +func ProcessAggregationParameter(aggregationFieldWithFn models.QueryResourceAggregation, searchParamTypeLookup map[string]string) (AggregationParameter, error) { + aggregationParameter := AggregationParameter{ + SearchParameter: SearchParameter{}, + Function: aggregationFieldWithFn.Function, + } //determine the searchCode searchCodeModifier //TODO: this is only applicable to string, token, reference and uri type (however unknown names & modifiers are ignored) - if aggregationFieldParts := strings.SplitN(aggregationFieldWithProperty, ":", 2); len(aggregationFieldParts) == 2 { + if aggregationFieldParts := strings.SplitN(aggregationFieldWithFn.Field, ":", 2); len(aggregationFieldParts) == 2 { aggregationParameter.Name = aggregationFieldParts[0] aggregationParameter.Modifier = aggregationFieldParts[1] } else { @@ -618,7 +662,7 @@ func ProcessAggregationParameter(aggregationFieldWithProperty string, searchPara //next, determine the searchCodeType for this Resource (or throw an error if it is unknown) searchParamTypeStr, searchParamTypeOk := searchParamTypeLookup[aggregationParameter.Name] if !searchParamTypeOk { - return aggregationParameter, fmt.Errorf("unknown search parameter: %s", aggregationParameter.Name) + return aggregationParameter, fmt.Errorf("unknown search parameter in aggregation: %s", aggregationParameter.Name) } else { aggregationParameter.Type = SearchParameterType(searchParamTypeStr) } @@ -628,6 +672,8 @@ func ProcessAggregationParameter(aggregationFieldWithProperty string, searchPara if len(aggregationParameter.Modifier) > 0 { return aggregationParameter, fmt.Errorf("primitive aggregation parameter %s cannot have a property (%s)", aggregationParameter.Name, aggregationParameter.Modifier) } + } else if aggregationParameter.Type == SearchParameterTypeToken { + //modifier is optional for token types } else { //complex types must have a modifier if len(aggregationParameter.Modifier) == 0 { diff --git a/backend/pkg/database/sqlite_repository_query_sql_test.go b/backend/pkg/database/sqlite_repository_query_sql_test.go index 51e61b6a8..c872f97d2 100644 --- a/backend/pkg/database/sqlite_repository_query_sql_test.go +++ b/backend/pkg/database/sqlite_repository_query_sql_test.go @@ -98,7 +98,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL() { "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", "WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)", "GROUP BY `fhir`.`id`", - "ORDER BY fhir.sort_date ASC", + "ORDER BY fhir.sort_date DESC", }, " "), sqlString) require.Equal(suite.T(), sqlParams, []interface{}{ @@ -136,7 +136,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithMultipleWhereCon "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson, json_each(fhir.category) as categoryJson", "WHERE ((codeJson.value ->> '$.code' = ?)) AND ((categoryJson.value ->> '$.code' = ?)) AND (user_id = ?)", "GROUP BY `fhir`.`id`", - "ORDER BY fhir.sort_date ASC", + "ORDER BY fhir.sort_date DESC", }, " "), sqlString) require.Equal(suite.T(), sqlParams, []interface{}{ @@ -158,7 +158,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveOrderBy "activityCode": "test_code", }, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{OrderBy: "instantiatesUri"}, + Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "instantiatesUri"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -193,7 +193,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithKeywordOrderByAg Select: []string{}, Where: map[string]interface{}{}, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{OrderBy: "id"}, + Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "id"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -230,7 +230,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexOrderByAg "code": "test_code", }, From: "Observation", - Aggregations: &models.QueryResourceAggregations{OrderBy: "valueString:value"}, + Aggregations: &models.QueryResourceAggregations{OrderBy: &models.QueryResourceAggregation{Field: "valueString:value"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -267,7 +267,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithPrimitiveCountBy "activityCode": "test_code", }, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{CountBy: "instantiatesUri"}, + Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "instantiatesUri"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -304,7 +304,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithKeywordCountByAg "activityCode": "test_code", }, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{CountBy: "source_resource_type"}, + Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "source_resource_type"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -339,7 +339,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithWildcardCountByA Select: []string{}, Where: map[string]interface{}{}, From: "CarePlan", - Aggregations: &models.QueryResourceAggregations{CountBy: "*"}, + Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "*"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -376,7 +376,7 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAg "code": "test_code", }, From: "Observation", - Aggregations: &models.QueryResourceAggregations{CountBy: "code:code"}, + Aggregations: &models.QueryResourceAggregations{CountBy: &models.QueryResourceAggregation{Field: "code:code"}}, }) require.NoError(suite.T(), err) var results []map[string]interface{} @@ -398,3 +398,120 @@ func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexCountByAg "test_code", "00000000-0000-0000-0000-000000000000", }) } + +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithComplexGroupByWithOrderByMaxFnAggregation() { + //setup + sqliteRepo := suite.TestRepository.(*SqliteRepository) + 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", + }, + From: "Observation", + Aggregations: &models.QueryResourceAggregations{ + GroupBy: &models.QueryResourceAggregation{Field: "code:code"}, + OrderBy: &models.QueryResourceAggregation{Field: "sort_date", Function: "max"}, + }, + }) + 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, max(fhir.sort_date) as value", + "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", + "WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?)", + "GROUP BY (codeJson.value ->> '$.code')", + "ORDER BY max(fhir.sort_date) DESC", + }, " "), sqlString) + require.Equal(suite.T(), sqlParams, []interface{}{ + "test_code", "00000000-0000-0000-0000-000000000000", + }) +} + +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenGroupByNoModifier() { + //setup + sqliteRepo := suite.TestRepository.(*SqliteRepository) + 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{}{}, + From: "Observation", + Aggregations: &models.QueryResourceAggregations{ + GroupBy: &models.QueryResourceAggregation{Field: "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 ->> '$.system') || '|' || (codeJson.value ->> '$.code')) as label, count(*) as value", + "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", + "WHERE (user_id = ?)", + "GROUP BY ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code'))", + "ORDER BY count(*) DESC", + }, " "), sqlString) + require.Equal(suite.T(), sqlParams, []interface{}{ + "00000000-0000-0000-0000-000000000000", + }) +} + +func (suite *RepositorySqlTestSuite) TestQueryResources_SQL_WithTokenGroupByNoModifierWithLimit() { + //setup + sqliteRepo := suite.TestRepository.(*SqliteRepository) + sqliteRepo.GormClient = sqliteRepo.GormClient.Session(&gorm.Session{DryRun: true}) + + //test + authContext := context.WithValue(context.Background(), pkg.ContextKeyTypeAuthUsername, "test_username") + + limit := 10 + sqlQuery, err := sqliteRepo.sqlQueryResources(authContext, models.QueryResource{ + Select: []string{}, + Where: map[string]interface{}{}, + From: "Observation", + Limit: &limit, + Aggregations: &models.QueryResourceAggregations{ + GroupBy: &models.QueryResourceAggregation{Field: "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 ->> '$.system') || '|' || (codeJson.value ->> '$.code')) as label, count(*) as value", + "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", + "WHERE (user_id = ?)", + "GROUP BY ((codeJson.value ->> '$.system') || '|' || (codeJson.value ->> '$.code'))", + "ORDER BY count(*) DESC", + "LIMIT 10", + }, " "), sqlString) + require.Equal(suite.T(), sqlParams, []interface{}{ + "00000000-0000-0000-0000-000000000000", + }) +} diff --git a/backend/pkg/models/query_resource.go b/backend/pkg/models/query_resource.go index 64a4dac85..88c3f5848 100644 --- a/backend/pkg/models/query_resource.go +++ b/backend/pkg/models/query_resource.go @@ -19,10 +19,15 @@ type QueryResource struct { } type QueryResourceAggregations struct { - CountBy string `json:"count_by"` //alias for both groupby and orderby, cannot be used together + CountBy *QueryResourceAggregation `json:"count_by,omitempty"` //alias for both groupby and orderby, cannot be used together - GroupBy string `json:"group_by"` - OrderBy string `json:"order_by"` + GroupBy *QueryResourceAggregation `json:"group_by,omitempty"` + OrderBy *QueryResourceAggregation `json:"order_by,omitempty"` +} + +type QueryResourceAggregation struct { + Field string `json:"field"` + Function string `json:"fn"` } func (q *QueryResource) Validate() error { @@ -38,26 +43,44 @@ func (q *QueryResource) Validate() error { if len(q.Select) > 0 { return fmt.Errorf("cannot use 'select' and 'aggregations' together") } - if len(q.Aggregations.CountBy) > 0 { - if len(q.Aggregations.GroupBy) > 0 { - return fmt.Errorf("cannot use 'count_by' and 'group_by' together") + + if q.Aggregations.CountBy != nil { + if len(q.Aggregations.CountBy.Field) == 0 { + return fmt.Errorf("if 'count_by' is present, field must be populated") } - if len(q.Aggregations.OrderBy) > 0 { - return fmt.Errorf("cannot use 'count_by' and 'order_by' together") + if strings.Contains(q.Aggregations.CountBy.Field, " ") { + return fmt.Errorf("count_by cannot have spaces (or aliases)") } } - if len(q.Aggregations.CountBy) == 0 && len(q.Aggregations.OrderBy) == 0 && len(q.Aggregations.GroupBy) == 0 { - return fmt.Errorf("aggregations must have at least one of 'count_by', 'group_by', or 'order_by'") + if q.Aggregations.GroupBy != nil { + if len(q.Aggregations.GroupBy.Field) == 0 { + return fmt.Errorf("if 'group_by' is present, field must be populated") + } + if strings.Contains(q.Aggregations.GroupBy.Field, " ") { + return fmt.Errorf("group_by cannot have spaces (or aliases)") + } } - if strings.Contains(q.Aggregations.CountBy, " ") { - return fmt.Errorf("count_by cannot have spaces (or aliases)") + if q.Aggregations.OrderBy != nil { + if len(q.Aggregations.OrderBy.Field) == 0 { + return fmt.Errorf("if 'order_by' is present, field must be populated") + } + if strings.Contains(q.Aggregations.OrderBy.Field, " ") { + return fmt.Errorf("order_by cannot have spaces (or aliases)") + } } - if strings.Contains(q.Aggregations.GroupBy, " ") { - return fmt.Errorf("group_by cannot have spaces (or aliases)") + + if q.Aggregations.CountBy != nil { + if q.Aggregations.GroupBy != nil { + return fmt.Errorf("cannot use 'count_by' and 'group_by' together") + } + if q.Aggregations.OrderBy != nil { + return fmt.Errorf("cannot use 'count_by' and 'order_by' together") + } } - if strings.Contains(q.Aggregations.OrderBy, " ") { - return fmt.Errorf("order_by cannot have spaces (or aliases)") + if q.Aggregations.CountBy == nil && q.Aggregations.OrderBy == nil && q.Aggregations.GroupBy == nil { + return fmt.Errorf("aggregations must have at least one of 'count_by', 'group_by', or 'order_by'") } + } if q.Limit != nil && *q.Limit < 0 { diff --git a/backend/pkg/models/query_resource_test.go b/backend/pkg/models/query_resource_test.go index 90a1d90eb..a75806e1a 100644 --- a/backend/pkg/models/query_resource_test.go +++ b/backend/pkg/models/query_resource_test.go @@ -13,15 +13,18 @@ func TestQueryResource_Validate(t *testing.T) { }{ {QueryResource{Use: "test"}, "'use' is not supported yet", true}, {QueryResource{}, "'from' is required", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test"}}, "", false}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: ""}}}, "if 'count_by' is present, field must be populated", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: &QueryResourceAggregation{Field: ""}}}, "if 'group_by' is present, field must be populated", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: &QueryResourceAggregation{Field: ""}}}, "if 'order_by' is present, field must be populated", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}}}, "", false}, {QueryResource{Select: []string{"test"}, From: "test", Aggregations: &QueryResourceAggregations{}}, "cannot use 'select' and 'aggregations' together", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test", GroupBy: "test"}}, "cannot use 'count_by' and 'group_by' together", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test", OrderBy: "test"}}, "cannot use 'count_by' and 'order_by' together", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}, GroupBy: &QueryResourceAggregation{Field: "test"}}}, "cannot use 'count_by' and 'group_by' together", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test"}, OrderBy: &QueryResourceAggregation{Field: "test"}}}, "cannot use 'count_by' and 'order_by' together", true}, {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{}}, "aggregations must have at least one of 'count_by', 'group_by', or 'order_by'", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test:property"}}, "", false}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: "test:property as HELLO"}}, "count_by cannot have spaces (or aliases)", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: "test:property as HELLO"}}, "group_by cannot have spaces (or aliases)", true}, - {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: "test:property as HELLO"}}, "order_by cannot have spaces (or aliases)", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test:property"}}}, "", false}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{CountBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "count_by cannot have spaces (or aliases)", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{GroupBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "group_by cannot have spaces (or aliases)", true}, + {QueryResource{From: "test", Aggregations: &QueryResourceAggregations{OrderBy: &QueryResourceAggregation{Field: "test:property as HELLO"}}}, "order_by cannot have spaces (or aliases)", true}, } //test && assert diff --git a/backend/pkg/web/handler/dashboard/default.json b/backend/pkg/web/handler/dashboard/default.json index da8851315..0f4df93c7 100644 --- a/backend/pkg/web/handler/dashboard/default.json +++ b/backend/pkg/web/handler/dashboard/default.json @@ -132,7 +132,7 @@ "from": "Observation", "where": {}, "aggregations":{ - "count_by": "code:code" + "count_by": {"field": "code:code" } } } }], @@ -158,7 +158,7 @@ "from": "Immunization", "where": {}, "aggregations":{ - "count_by": "*" + "count_by": {"field": "*" } } } }, @@ -171,7 +171,7 @@ "from": "Claim", "where": {}, "aggregations":{ - "count_by": "*" + "count_by": {"field": "*" } } } }], From 623c9ab663ba61695a78ee3d940438b307b18006 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 2 Oct 2023 14:27:35 -0700 Subject: [PATCH 05/11] adding tests for processing aggregation parameters. make sure frontend code uses new aggregation parameter structure. --- .../database/sqlite_repository_query_test.go | 51 ++++++++++++++++++- .../models/widget/dashboard-widget-query.ts | 11 ++-- .../report-labs/report-labs.component.ts | 8 ++- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/backend/pkg/database/sqlite_repository_query_test.go b/backend/pkg/database/sqlite_repository_query_test.go index 6bbb1d005..add11cb5e 100644 --- a/backend/pkg/database/sqlite_repository_query_test.go +++ b/backend/pkg/database/sqlite_repository_query_test.go @@ -204,6 +204,55 @@ func TestSearchCodeToFromClause(t *testing.T) { } +//Aggregation tests + +// mimic tests from https://hl7.org/fhir/r4/search.html#token +func TestProcessAggregationParameter(t *testing.T) { + //setup + t.Parallel() + var processSearchParameterTests = []struct { + aggregationFieldWithFn models.QueryResourceAggregation // input + searchParameterLookup map[string]string // input (allowed search parameters) + expected AggregationParameter + expectedError bool // expected result + }{ + //primitive types + {models.QueryResourceAggregation{Field: "test"}, map[string]string{"test": "keyword"}, AggregationParameter{SearchParameter: SearchParameter{Type: "keyword", Name: "test", Modifier: ""}}, false}, + {models.QueryResourceAggregation{Field: "test"}, map[string]string{"test": "number"}, AggregationParameter{SearchParameter: SearchParameter{Type: "number", Name: "test", Modifier: ""}}, false}, + {models.QueryResourceAggregation{Field: "test"}, map[string]string{"test": "uri"}, AggregationParameter{SearchParameter: SearchParameter{Type: "uri", Name: "test", Modifier: ""}}, false}, + {models.QueryResourceAggregation{Field: "test"}, map[string]string{"test": "date"}, AggregationParameter{SearchParameter: SearchParameter{Type: "date", Name: "test", Modifier: ""}}, false}, + + {models.QueryResourceAggregation{Field: "test:hello"}, map[string]string{"test": "keyword"}, AggregationParameter{SearchParameter: SearchParameter{Type: "date", Name: "test", Modifier: ""}}, true}, //cannot have a modifier + {models.QueryResourceAggregation{Field: "test:hello"}, map[string]string{"test": "number"}, AggregationParameter{SearchParameter: SearchParameter{Type: "date", Name: "test", Modifier: ""}}, true}, //cannot have a modifier + {models.QueryResourceAggregation{Field: "test:hello"}, map[string]string{"test": "uri"}, AggregationParameter{SearchParameter: SearchParameter{Type: "date", Name: "test", Modifier: ""}}, true}, //cannot have a modifier + {models.QueryResourceAggregation{Field: "test:hello"}, map[string]string{"test": "date"}, AggregationParameter{SearchParameter: SearchParameter{Type: "date", Name: "test", Modifier: ""}}, true}, //cannot have a modifier + + //complex types + {models.QueryResourceAggregation{Field: "test"}, map[string]string{"test": "reference"}, AggregationParameter{SearchParameter: SearchParameter{Type: "reference", Name: "test", Modifier: ""}}, true}, //complex types should throw an error when missing modifier + {models.QueryResourceAggregation{Field: "test"}, map[string]string{"test": "string"}, AggregationParameter{SearchParameter: SearchParameter{Type: "string", Name: "test", Modifier: ""}}, true}, //complex types should throw an error when missing modifier + {models.QueryResourceAggregation{Field: "test"}, map[string]string{"test": "quantity"}, AggregationParameter{SearchParameter: SearchParameter{Type: "quantity", Name: "test", Modifier: ""}}, true}, //complex types should throw an error when missing modifier + + {models.QueryResourceAggregation{Field: "test:hello"}, map[string]string{"test": "reference"}, AggregationParameter{SearchParameter: SearchParameter{Type: "reference", Name: "test", Modifier: "hello"}}, false}, + {models.QueryResourceAggregation{Field: "test:hello"}, map[string]string{"test": "string"}, AggregationParameter{SearchParameter: SearchParameter{Type: "string", Name: "test", Modifier: "hello"}}, false}, + {models.QueryResourceAggregation{Field: "test:hello"}, map[string]string{"test": "quantity"}, AggregationParameter{SearchParameter: SearchParameter{Type: "quantity", Name: "test", Modifier: "hello"}}, false}, + + //token type + {models.QueryResourceAggregation{Field: "code"}, map[string]string{"code": "token"}, AggregationParameter{SearchParameter: SearchParameter{Type: "token", Name: "code", Modifier: ""}}, false}, + {models.QueryResourceAggregation{Field: "code:code"}, map[string]string{"code": "token"}, AggregationParameter{SearchParameter: SearchParameter{Type: "token", Name: "code", Modifier: "code"}}, false}, + } + + //test && assert + for ndx, tt := range processSearchParameterTests { + actual, actualErr := ProcessAggregationParameter(tt.aggregationFieldWithFn, tt.searchParameterLookup) + if tt.expectedError { + require.Error(t, actualErr, "Expected error but got none for processAggregationParameterTests[%d] %s", ndx, tt.aggregationFieldWithFn) + } else { + require.NoError(t, actualErr, "Expected no error but got one for processAggregationParameterTests[%d] %s", ndx, tt.aggregationFieldWithFn) + require.Equal(t, tt.expected, actual) + } + } +} + func (suite *RepositoryTestSuite) TestQueryResources_SQL() { //setup fakeConfig := mock_config.NewMockInterface(suite.MockCtrl) @@ -245,7 +294,7 @@ func (suite *RepositoryTestSuite) TestQueryResources_SQL() { "SELECT fhir.*", "FROM fhir_observation as fhir, json_each(fhir.code) as codeJson", "WHERE ((codeJson.value ->> '$.code' = ?)) AND (user_id = ?) GROUP BY `fhir`.`id`", - "ORDER BY fhir.sort_date ASC"}, " ")) + "ORDER BY fhir.sort_date DESC"}, " ")) require.Equal(suite.T(), sqlParams, []interface{}{ "test_code", "00000000-0000-0000-0000-000000000000", }) diff --git a/frontend/src/app/models/widget/dashboard-widget-query.ts b/frontend/src/app/models/widget/dashboard-widget-query.ts index a47803f05..d9d0a11f7 100644 --- a/frontend/src/app/models/widget/dashboard-widget-query.ts +++ b/frontend/src/app/models/widget/dashboard-widget-query.ts @@ -8,10 +8,15 @@ export class DashboardWidgetQuery { //https://lodash.com/docs/4.17.15#unionBy aggregations?: { - count_by?: string, //alias for groupBy and orderBy - group_by?: string, - order_by?: string, + count_by?: DashboardWidgetQueryAggregation, //alias for groupBy and orderBy + group_by?: DashboardWidgetQueryAggregation, + order_by?: DashboardWidgetQueryAggregation, } // aggregation_params?: string[] // aggregation_type?: 'countBy' | 'groupBy' | 'orderBy' // | 'minBy' | 'maxBy' | 'sumBy' // 'orderBy' | 'sortBy' | } + +export class DashboardWidgetQueryAggregation { + field: string + fn?: string +} diff --git a/frontend/src/app/pages/report-labs/report-labs.component.ts b/frontend/src/app/pages/report-labs/report-labs.component.ts index 6f449ed28..29252173a 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.ts +++ b/frontend/src/app/pages/report-labs/report-labs.component.ts @@ -43,7 +43,13 @@ export class ReportLabsComponent implements OnInit { from: "Observation", where: {}, aggregations: { - order_by: "code:code" + order_by: { + field: "sort_date", + fn: "max" + }, + group_by: { + field: "code", + } } }).subscribe(results => { console.log("OBSERVATIONS GROUPED", results) From f3b1c7e28310c6b98c3d8a324c3f123724c5a9ab Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 2 Oct 2023 14:37:57 -0700 Subject: [PATCH 06/11] fixing dashboard widgets with built-in queries containing aggregations. --- .../dashboard-widget/dashboard-widget.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/widgets/dashboard-widget/dashboard-widget.component.spec.ts b/frontend/src/app/widgets/dashboard-widget/dashboard-widget.component.spec.ts index 473a4a305..b7928fdc5 100644 --- a/frontend/src/app/widgets/dashboard-widget/dashboard-widget.component.spec.ts +++ b/frontend/src/app/widgets/dashboard-widget/dashboard-widget.component.spec.ts @@ -124,7 +124,7 @@ describe('DashboardWidgetComponent', () => { "where": {}, "aggregations":{ - "count_by": "source_resource_type" + "count_by": {"field": "source_resource_type"} }, } }, @@ -138,7 +138,7 @@ describe('DashboardWidgetComponent', () => { "where": {}, "aggregations":{ - "count_by": "source_resource_type" + "count_by": {"field": "source_resource_type"} }, } }], From 595541f3a0cd96ff8a99fd18e50b2894d15f52a5 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 2 Oct 2023 16:07:25 -0700 Subject: [PATCH 07/11] correctly handle query for token with system but no code. --- backend/pkg/database/sqlite_repository_query.go | 17 ++++++++--------- .../database/sqlite_repository_query_test.go | 2 ++ .../pages/report-labs/report-labs.component.ts | 6 ++++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/backend/pkg/database/sqlite_repository_query.go b/backend/pkg/database/sqlite_repository_query.go index 3740c8500..e3a6e2349 100644 --- a/backend/pkg/database/sqlite_repository_query.go +++ b/backend/pkg/database/sqlite_repository_query.go @@ -417,12 +417,8 @@ func ProcessSearchParameterValue(searchParameter SearchParameter, searchValueWit } } else if len(searchParameterValueParts) == 2 { //if theres 2 parts, first is always system, second is always the code. Either one may be emty. If both are emty this is invalid. - if len(searchParameterValueParts[0]) > 0 { - searchParameterValue.SecondaryValues[searchParameter.Name+"System"] = searchParameterValueParts[0] - } - if len(searchParameterValueParts[1]) > 0 { - searchParameterValue.Value = searchParameterValueParts[1] - } + searchParameterValue.SecondaryValues[searchParameter.Name+"System"] = searchParameterValueParts[0] + searchParameterValue.Value = searchParameterValueParts[1] if len(searchParameterValueParts[0]) == 0 && len(searchParameterValueParts[1]) == 0 { return searchParameterValue, fmt.Errorf("invalid search parameter value: (%s=%s)", searchParameter.Name, searchParameterValue.Value) } @@ -571,7 +567,10 @@ func SearchCodeToWhereClause(searchParam SearchParameter, searchParamValue Searc //TODO: support ":text" modifier //setup the clause - clause := fmt.Sprintf("%sJson.value ->> '$.code' = @%s", searchParam.Name, NamedParameterWithSuffix(searchParam.Name, namedParameterSuffix)) + clause := []string{} + if searchParamValue.Value.(string) != "" { + clause = append(clause, fmt.Sprintf("%sJson.value ->> '$.code' = @%s", searchParam.Name, NamedParameterWithSuffix(searchParam.Name, namedParameterSuffix))) + } //append the code and/or system clauses (if required) //this looks like unnecessary code, however its required to ensure consistent tests @@ -580,10 +579,10 @@ func SearchCodeToWhereClause(searchParam SearchParameter, searchParamValue Searc for _, k := range allowedSecondaryKeys { namedParameterKey := fmt.Sprintf("%s%s", searchParam.Name, strings.Title(k)) if _, ok := searchParamValue.SecondaryValues[namedParameterKey]; ok { - clause += fmt.Sprintf(` AND %sJson.value ->> '$.%s' = @%s`, searchParam.Name, k, NamedParameterWithSuffix(namedParameterKey, namedParameterSuffix)) + clause = append(clause, fmt.Sprintf(`%sJson.value ->> '$.%s' = @%s`, searchParam.Name, k, NamedParameterWithSuffix(namedParameterKey, namedParameterSuffix))) } } - return fmt.Sprintf("(%s)", clause), searchClauseNamedParams, nil + return fmt.Sprintf("(%s)", strings.Join(clause, " AND ")), searchClauseNamedParams, nil case SearchParameterTypeKeyword: //setup the clause diff --git a/backend/pkg/database/sqlite_repository_query_test.go b/backend/pkg/database/sqlite_repository_query_test.go index add11cb5e..b6a9168c8 100644 --- a/backend/pkg/database/sqlite_repository_query_test.go +++ b/backend/pkg/database/sqlite_repository_query_test.go @@ -103,6 +103,8 @@ func TestProcessSearchParameterValue(t *testing.T) { {SearchParameter{Type: "token", Name: "identifier", Modifier: "otype"}, "http://terminology.hl7.org/CodeSystem/v2-0203|MR|446053", SearchParameterValue{Value: "MR|446053", Prefix: "", SecondaryValues: map[string]interface{}{"identifierSystem": "http://terminology.hl7.org/CodeSystem/v2-0203"}}, false}, {SearchParameter{Type: "token", Name: "code", Modifier: ""}, "|", SearchParameterValue{}, true}, //empty value should throw an error {SearchParameter{Type: "token", Name: "code", Modifier: ""}, "", SearchParameterValue{}, true}, //empty value should throw an error + {SearchParameter{Type: "token", Name: "code", Modifier: ""}, "http://acme.org/conditions/codes|", SearchParameterValue{Value: "", Prefix: "", SecondaryValues: map[string]interface{}{"codeSystem": "http://acme.org/conditions/codes"}}, false}, + {SearchParameter{Type: "token", Name: "code", Modifier: ""}, "|807-1", SearchParameterValue{Value: "807-1", Prefix: "", SecondaryValues: map[string]interface{}{"codeSystem": ""}}, false}, {SearchParameter{Type: "quantity", Name: "valueQuantity", Modifier: ""}, "5.4|http://unitsofmeasure.org|mg", SearchParameterValue{Value: float64(5.4), Prefix: "", SecondaryValues: map[string]interface{}{"valueQuantitySystem": "http://unitsofmeasure.org", "valueQuantityCode": "mg"}}, false}, {SearchParameter{Type: "quantity", Name: "valueQuantity", Modifier: ""}, "5.40e-3|http://unitsofmeasure.org|g", SearchParameterValue{Value: float64(0.0054), Prefix: "", SecondaryValues: map[string]interface{}{"valueQuantitySystem": "http://unitsofmeasure.org", "valueQuantityCode": "g"}}, false}, diff --git a/frontend/src/app/pages/report-labs/report-labs.component.ts b/frontend/src/app/pages/report-labs/report-labs.component.ts index 29252173a..25bc1d045 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.ts +++ b/frontend/src/app/pages/report-labs/report-labs.component.ts @@ -39,9 +39,11 @@ export class ReportLabsComponent implements OnInit { }) this.fastenApi.queryResources({ - select: ["*"], + select: [], from: "Observation", - where: {}, + where: { + "code": "http://loinc.org|,urn:oid:2.16.840.1.113883.6.1|", + }, aggregations: { order_by: { field: "sort_date", From 61528ac643528595e309e7345d0ebac5fb7d2cdd Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 2 Oct 2023 20:43:24 -0700 Subject: [PATCH 08/11] paginated lab results page. --- .../report-labs/report-labs.component.html | 12 ++ .../report-labs/report-labs.component.ts | 160 +++++++++++++----- 2 files changed, 128 insertions(+), 44 deletions(-) diff --git a/frontend/src/app/pages/report-labs/report-labs.component.html b/frontend/src/app/pages/report-labs/report-labs.component.html index b43d9f7b0..7825c8ee5 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.html +++ b/frontend/src/app/pages/report-labs/report-labs.component.html @@ -44,6 +44,18 @@

Observations

[observationCode]="observationGroup.key" [observationTitle]="observationGroupTitles[observationGroup.key]" > + + + + + + diff --git a/frontend/src/app/pages/report-labs/report-labs.component.ts b/frontend/src/app/pages/report-labs/report-labs.component.ts index 25bc1d045..e621ae6f1 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.ts +++ b/frontend/src/app/pages/report-labs/report-labs.component.ts @@ -3,6 +3,21 @@ import {FastenApiService} from '../../services/fasten-api.service'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {ResourceFhir} from '../../models/fasten/resource_fhir'; import * as fhirpath from 'fhirpath'; +import {Observable} from 'rxjs'; +import {flatMap, map} from 'rxjs/operators'; +import {ResponseWrapper} from '../../models/response-wrapper'; +import {ActivatedRoute} from '@angular/router'; + +class ObservationGroup {[key: string]: ResourceFhir[]} +class ObservationGroupInfo { + observationGroups: ObservationGroup = {} + observationGroupTitles: {[key: string]: string} = {} +} +class LabResultCodeByDate { + label: string //lab result coding (system|code) + value: string //lab result date +} + @Component({ selector: 'app-report-labs', @@ -12,7 +27,13 @@ import * as fhirpath from 'fhirpath'; export class ReportLabsComponent implements OnInit { loading: boolean = false - observationGroups: {[key: string]: ResourceFhir[]} = {} + currentPage: number = 0 + pageSize: number = 10 + allObservationGroups: string[] = [] + + + //currentPage data + observationGroups: ObservationGroup = {} observationGroupTitles: {[key: string]: string} = {} isEmptyReport = false @@ -21,72 +42,123 @@ export class ReportLabsComponent implements OnInit { constructor( private fastenApi: FastenApiService, + private activatedRoute: ActivatedRoute, ) { } ngOnInit(): void { this.loading = true - this.fastenApi.queryResources({ - select: ["*"], - from: "DiagnosticReport", - where: { - "category": "http://terminology.hl7.org/CodeSystem/v2-0074|LAB", - }, - limit: 5, - }).subscribe(results => { - this.diagnosticReports = results.data - console.log("ALL DIAGNOSTIC REPORTS", results) - }) + this.populateReports() - this.fastenApi.queryResources({ - select: [], - from: "Observation", - where: { - "code": "http://loinc.org|,urn:oid:2.16.840.1.113883.6.1|", - }, - aggregations: { - order_by: { - field: "sort_date", - fn: "max" - }, - group_by: { - field: "code", - } - } - }).subscribe(results => { - console.log("OBSERVATIONS GROUPED", results) + this.findLabResultCodesSortedByLatest().subscribe((data) => { + // this.loading = false + console.log("ALL lab result codes", data) + this.allObservationGroups = data.map((item) => item.label) + return this.populateObservationsForCurrentPage() }) + } + //using the current list of allObservationGroups, retrieve a list of observations, group them by observationGroup, and set the observationGroupTitles + populateObservationsForCurrentPage(){ - this.fastenApi.getResources("Observation").subscribe(results => { - this.loading = false - results = results || [] - console.log("ALL OBSERVATIONS", results) - - //loop though all observations, group by "code.system": "http://loinc.org" - for(let observation of results){ - let observationGroup = fhirpath.evaluate(observation.resource_raw, "Observation.code.coding.where(system='http://loinc.org').first().code")[0] - this.observationGroups[observationGroup] = this.observationGroups[observationGroup] ? this.observationGroups[observationGroup] : [] - this.observationGroups[observationGroup].push(observation) + let observationGroups = this.allObservationGroups.slice(this.currentPage * this.pageSize, (this.currentPage + 1) * this.pageSize) - if(!this.observationGroupTitles[observationGroup]){ - this.observationGroupTitles[observationGroup] = fhirpath.evaluate(observation.resource_raw, "Observation.code.coding.where(system='http://loinc.org').first().display")[0] - } - } + this.loading = true + this.getObservationsByCodes(observationGroups).subscribe((data) => { + this.loading = false + this.observationGroups = data.observationGroups + this.observationGroupTitles = data.observationGroupTitles - this.isEmptyReport = !!!results.length + this.isEmptyReport = !!!Object.keys(this.observationGroups).length }, error => { this.loading = false this.isEmptyReport = true }) + } + //get a list of all lab codes associated with a diagnostic report + findLabResultCodesFilteredToReport(diagnosticReport: ResourceFhir): Observable { + return null + } + //get a list of all unique lab codes ordered by latest date + findLabResultCodesSortedByLatest(): Observable { + return this.fastenApi.queryResources({ + select: [], + from: "Observation", + where: { + "code": "http://loinc.org|,urn:oid:2.16.840.1.113883.6.1|", + }, + aggregations: { + order_by: { + field: "sort_date", + fn: "max" + }, + group_by: { + field: "code", + } + } + }) + .pipe( + map((response: ResponseWrapper) => { + return response.data as LabResultCodeByDate[] + }), + ) + } + //get a list of the last 10 lab results + populateReports(){ + return this.fastenApi.queryResources({ + select: ["*"], + from: "DiagnosticReport", + where: { + "category": "http://terminology.hl7.org/CodeSystem/v2-0074|LAB", + }, + limit: 10, + }).subscribe(results => { + this.diagnosticReports = results.data + }) + } + isEmpty(obj: any) { return Object.keys(obj).length === 0; } + //private methods + + //get a list of observations that have a matching code + private getObservationsByCodes(codes: string[]): Observable{ + return this.fastenApi.queryResources({ + select: [], + from: "Observation", + where: { + "code": codes.join(","), + } + }).pipe( + map((response: ResponseWrapper) => { + + let observationGroups: ObservationGroup = {} + let observationGroupTitles: {[key: string]: string} = {} + + //loop though all observations, group by "code.system": "http://loinc.org" + for(let observation of response.data){ + let observationGroup = fhirpath.evaluate(observation.resource_raw, "Observation.code.coding.where(system='http://loinc.org').first().code")[0] + observationGroups[observationGroup] = observationGroups[observationGroup] ? observationGroups[observationGroup] : [] + observationGroups[observationGroup].push(observation) + + if(!observationGroupTitles[observationGroup]){ + observationGroupTitles[observationGroup] = fhirpath.evaluate(observation.resource_raw, "Observation.code.coding.where(system='http://loinc.org').first().display")[0] + } + } + + return { + observationGroups: observationGroups, + observationGroupTitles: observationGroupTitles + } + }) + ); + } } From 74a3fd4b4e534413fc202d40d0e6ed1569761d28 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Mon, 2 Oct 2023 22:36:57 -0700 Subject: [PATCH 09/11] lab report filtering is working. --- frontend/src/app/app-routing.module.ts | 1 + .../report-labs/report-labs.component.html | 5 +- .../report-labs/report-labs.component.ts | 86 ++++++++++++++++--- 3 files changed, 78 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 440ac052c..aec8b3d05 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -42,6 +42,7 @@ const routes: Routes = [ { path: 'patient-profile', component: PatientProfileComponent, canActivate: [ IsAuthenticatedAuthGuard] }, { path: 'medical-history', component: MedicalHistoryComponent, canActivate: [ IsAuthenticatedAuthGuard] }, { path: 'labs', component: ReportLabsComponent, canActivate: [ IsAuthenticatedAuthGuard] }, + { path: 'labs/report/:source_id/:resource_type/:resource_id', component: ReportLabsComponent, canActivate: [ IsAuthenticatedAuthGuard] }, // { path: 'general-pages', loadChildren: () => import('./general-pages/general-pages.module').then(m => m.GeneralPagesModule) }, // { path: 'ui-elements', loadChildren: () => import('./ui-elements/ui-elements.module').then(m => m.UiElementsModule) }, diff --git a/frontend/src/app/pages/report-labs/report-labs.component.html b/frontend/src/app/pages/report-labs/report-labs.component.html index 7825c8ee5..f9df16a3a 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.html +++ b/frontend/src/app/pages/report-labs/report-labs.component.html @@ -22,7 +22,10 @@

Observations

- +
diff --git a/frontend/src/app/pages/report-labs/report-labs.component.ts b/frontend/src/app/pages/report-labs/report-labs.component.ts index e621ae6f1..6b27b74ef 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.ts +++ b/frontend/src/app/pages/report-labs/report-labs.component.ts @@ -3,10 +3,10 @@ import {FastenApiService} from '../../services/fasten-api.service'; import {NgbModal} from '@ng-bootstrap/ng-bootstrap'; import {ResourceFhir} from '../../models/fasten/resource_fhir'; import * as fhirpath from 'fhirpath'; -import {Observable} from 'rxjs'; -import {flatMap, map} from 'rxjs/operators'; +import {forkJoin, Observable} from 'rxjs'; +import {flatMap, map, mergeMap} from 'rxjs/operators'; import {ResponseWrapper} from '../../models/response-wrapper'; -import {ActivatedRoute} from '@angular/router'; +import {ActivatedRoute, Params} from '@angular/router'; class ObservationGroup {[key: string]: ResourceFhir[]} class ObservationGroupInfo { @@ -27,11 +27,16 @@ class LabResultCodeByDate { export class ReportLabsComponent implements OnInit { loading: boolean = false - currentPage: number = 0 + currentPage: number = 1 //1-based index due to the way the pagination component works pageSize: number = 10 allObservationGroups: string[] = [] + //diagnostic report data + reportSourceId: string = '' + reportResourceType: string = '' + reportResourceId: string = '' + //currentPage data observationGroups: ObservationGroup = {} observationGroupTitles: {[key: string]: string} = {} @@ -50,19 +55,43 @@ export class ReportLabsComponent implements OnInit { this.populateReports() - this.findLabResultCodesSortedByLatest().subscribe((data) => { - // this.loading = false - console.log("ALL lab result codes", data) - this.allObservationGroups = data.map((item) => item.label) - return this.populateObservationsForCurrentPage() - }) + + //determine if we're requesting all results or just a single report + //source_id/:resource_type/:resource_id + + this.activatedRoute.params.subscribe((routeParams: Params) => { + this.reportSourceId = routeParams['source_id'] + this.reportResourceType = routeParams['resource_type'] + this.reportResourceId = routeParams['resource_id'] + console.log("Selected Report changed!", this.reportSourceId,this.reportResourceType, this.reportResourceId) + + if(this.reportSourceId && this.reportResourceType && this.reportResourceId){ + //we're requesting a single report + console.log("REQUSTING REPORT", this.reportSourceId, this.reportResourceType, this.reportResourceId) + this.findLabResultCodesFilteredToReport(this.reportSourceId, this.reportResourceType, this.reportResourceId).subscribe((data) => { + console.log("REPORT result codes", data) + this.allObservationGroups = data + this.currentPage = 1 //reset to first page when changing report + return this.populateObservationsForCurrentPage() + }) + } else { + this.findLabResultCodesSortedByLatest().subscribe((data) => { + // this.loading = false + console.log("ALL lab result codes", data) + this.allObservationGroups = data.map((item) => item.label) + return this.populateObservationsForCurrentPage() + }) + } + }); + } //using the current list of allObservationGroups, retrieve a list of observations, group them by observationGroup, and set the observationGroupTitles populateObservationsForCurrentPage(){ - let observationGroups = this.allObservationGroups.slice(this.currentPage * this.pageSize, (this.currentPage + 1) * this.pageSize) + let observationGroups = this.allObservationGroups.slice((this.currentPage-1) * this.pageSize, this.currentPage * this.pageSize) + console.log("FILTERED OBSERVATION GROUPS", observationGroups, (this.currentPage -1) * this.pageSize, this.currentPage * this.pageSize) this.loading = true this.getObservationsByCodes(observationGroups).subscribe((data) => { this.loading = false @@ -78,8 +107,39 @@ export class ReportLabsComponent implements OnInit { } //get a list of all lab codes associated with a diagnostic report - findLabResultCodesFilteredToReport(diagnosticReport: ResourceFhir): Observable { - return null + findLabResultCodesFilteredToReport(sourceId, resourceType, resourceId): Observable { + return this.fastenApi.getResources(resourceType, sourceId, resourceId) + .pipe( + mergeMap((diagnosticReports) => { + let diagnosticReport = diagnosticReports?.[0] + console.log("diagnosticReport", diagnosticReport) + + //get a list of all the observations associated with this report + let observationIds = fhirpath.evaluate(diagnosticReport.resource_raw, "DiagnosticReport.result.reference") + + //request each observation, and find the lab codes associated with each + let requests = [] + for(let observationId of observationIds){ + let observationIdParts = observationId.split("/") + requests.push(this.fastenApi.getResources(observationIdParts[0], diagnosticReport.source_id, observationIdParts[1])) + } + + return forkJoin(requests) + }), + map((results:ResourceFhir[][]) => { + let allObservationGroups = [] + + //for each result, loop through the observations and find the loinc code + for(let result of results){ + for(let observation of result){ + let observationGroup = fhirpath.evaluate(observation.resource_raw, "Observation.code.coding.where(system='http://loinc.org').first().code")[0] + allObservationGroups.push('http://loinc.org|' + observationGroup) + } + } + console.log("FOUND REPORT LAB CODES", allObservationGroups) + return allObservationGroups + }) + ) } //get a list of all unique lab codes ordered by latest date From 8e39cf60a797565218d1ba4c5b5537eb4257eaca Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 3 Oct 2023 13:05:48 -0700 Subject: [PATCH 10/11] include report information in the header if filtered to report. --- .../pages/report-labs/report-labs.component.html | 13 +++++++++++++ .../app/pages/report-labs/report-labs.component.ts | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/frontend/src/app/pages/report-labs/report-labs.component.html b/frontend/src/app/pages/report-labs/report-labs.component.html index f9df16a3a..1b674b490 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.html +++ b/frontend/src/app/pages/report-labs/report-labs.component.html @@ -8,6 +8,19 @@ + + +
+
+

Report Info

+
+
+ +
+
+ + +
diff --git a/frontend/src/app/pages/report-labs/report-labs.component.ts b/frontend/src/app/pages/report-labs/report-labs.component.ts index 6b27b74ef..1e307ad82 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.ts +++ b/frontend/src/app/pages/report-labs/report-labs.component.ts @@ -7,6 +7,9 @@ import {forkJoin, Observable} from 'rxjs'; import {flatMap, map, mergeMap} from 'rxjs/operators'; import {ResponseWrapper} from '../../models/response-wrapper'; import {ActivatedRoute, Params} from '@angular/router'; +import {FastenDisplayModel} from '../../../lib/models/fasten/fasten-display-model'; +import {fhirModelFactory} from '../../../lib/models/factory'; +import {ResourceType} from '../../../lib/models/constants'; class ObservationGroup {[key: string]: ResourceFhir[]} class ObservationGroupInfo { @@ -36,6 +39,7 @@ export class ReportLabsComponent implements OnInit { reportSourceId: string = '' reportResourceType: string = '' reportResourceId: string = '' + reportDisplayModel: FastenDisplayModel = null //currentPage data observationGroups: ObservationGroup = {} @@ -113,6 +117,8 @@ export class ReportLabsComponent implements OnInit { mergeMap((diagnosticReports) => { let diagnosticReport = diagnosticReports?.[0] console.log("diagnosticReport", diagnosticReport) + this.reportDisplayModel = fhirModelFactory(diagnosticReport.source_resource_type as ResourceType, diagnosticReport) + //get a list of all the observations associated with this report let observationIds = fhirpath.evaluate(diagnosticReport.resource_raw, "DiagnosticReport.result.reference") From f26447c3fcbbacfbd49628e847a77cc0a58c7691 Mon Sep 17 00:00:00 2001 From: Jason Kulatunga Date: Tue, 3 Oct 2023 13:21:33 -0700 Subject: [PATCH 11/11] make sure the currently selected report is highlighted. --- .../app/pages/report-labs/report-labs.component.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/pages/report-labs/report-labs.component.html b/frontend/src/app/pages/report-labs/report-labs.component.html index 1b674b490..9990d1712 100644 --- a/frontend/src/app/pages/report-labs/report-labs.component.html +++ b/frontend/src/app/pages/report-labs/report-labs.component.html @@ -29,13 +29,17 @@

Observations