in response wrapper, the error field is optional

Dashboard widget query processing is now done in the dashboard-widget.
added tests for dashboard-widget.
This commit is contained in:
Jason Kulatunga 2023-07-26 18:54:55 -07:00
parent 779948a024
commit 07f0a1fbfc
No known key found for this signature in database
11 changed files with 593 additions and 205 deletions

View File

@ -1,5 +1,5 @@
export class ResponseWrapper {
data: any
success: boolean
error: string
error?: string
}

View File

@ -26,78 +26,4 @@ describe('FastenApiService', () => {
expect(service).toBeTruthy();
});
it('fhirPathMapQueryFn should generate a valid select map with aliases', () => {
//setup
let patient = {
"resourceType": "Patient",
"id": "example",
"address": [
{
"use": "home",
"city": "PleasantVille",
"type": "both",
"state": "Vic",
"line": [
"534 Erewhon St"
],
"postalCode": "3999",
"period": {
"start": "1974-12-25"
},
"district": "Rainbow",
"text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999"
}
]
}
let query = {
// use: string
select: ['address.use', "address.where(type='both').state as state"],
from: 'Patient',
where: {'id':'example'}
} as DashboardWidgetQuery
let query2 = {
// use: string
select: ['*', "address.where(type='both').use | address.city as joined"],
from: 'Patient',
where: {'id':'example'}
} as DashboardWidgetQuery
//test
let fn = service.fhirPathMapQueryFn(query)
expect(fn(patient)).toEqual({ "address.use": [ 'home' ], "state": [ 'Vic' ], 'id': 'example', 'resourceType': 'Patient' })
// let fn2 = service.fhirPathMapQueryFn(query2)
let fn2 = service.fhirPathMapQueryFn(query2)
expect(fn2(patient)).toEqual({
"joined": [ 'home', 'PleasantVille' ],
"*": {
"resourceType": "Patient",
"id": "example",
"address": [
{
"use": "home",
"city": "PleasantVille",
"type": "both",
"state": "Vic",
"line": [
"534 Erewhon St"
],
"postalCode": "3999",
"period": {
"start": "1974-12-25"
},
"district": "Rainbow",
"text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999"
}
]
}, 'id': 'example', 'resourceType': 'Patient' })
});
});

View File

@ -162,46 +162,11 @@ export class FastenApiService {
//TODO: add caching here, we dont want the same query to be run multiple times whne loading the dashboard.
// we should also add a way to invalidate the cache when a source is synced
queryResources(query?: DashboardWidgetQuery): Observable<any[]> {
//this function is special, as it returns the raw response, for processing in the DashboardWidgetComponent
queryResources(query?: DashboardWidgetQuery): Observable<ResponseWrapper> {
return this._httpClient.post<any>(`${GetEndpointAbsolutePath(globalThis.location, environment.fasten_api_endpoint_base)}/secure/query`, query)
.pipe(
map((response: ResponseWrapper) => {
console.log("RESPONSE", response)
//TODO: eventually do filtering in backend, however until sql-on-fhir project is completed, we'll be doing it here
//it's less preformant, but it's a temporary solution
if(!response.data || !response.data.length){
console.log("NO QUERY DATA FOUND")
return []
}
let results = response.data
.map((resource: ResourceFhir) => {
if (!resource.resource_raw) {
return null
}
return this.fhirPathMapQueryFn(query)(resource.resource_raw)
})
if(query.aggregation_type){
switch (query.aggregation_type) {
case "countBy":
return Object.entries(_[query.aggregation_type](results, ...(query.aggregation_params || []))).map(pair => {
return {key: pair[0], value: pair[1]}
})
break;
default:
throw new Error("unsupported aggregation type")
}
}
else {
return results
}
})
);
}
getResourceGraph(graphType?: string): Observable<{[resourceType: string]: ResourceFhir[]}> {
@ -272,56 +237,4 @@ export class FastenApiService {
return of(new BinaryModel(attachmentModel));
}
}
//private methods
// This function will convert DashboardWidgetQuery.select filters into a FHIRPath query strings and return the results
// as a map (keyed by the select alias)
// ie. `name.where(given='Jim')` will be converted to `Patient.name.where(given='Jim')`
// ie. `name.where(given='Jim') as GivenName` will be converted to `Patient.name.where(given='Jim')` and be stored in the returned map as GivenName`
// the returned map will always contain a `id` key, which will be the resource id and a `resourceType` key, which will be the resource type
fhirPathMapQueryFn(query: DashboardWidgetQuery): (rawResource: any) => { [name:string]: string | string[] | any } {
let selectPathFilters: { [name:string]: string } = query.select.reduce((selectAliasMap, selectPathFilter): { [name:string]: string } => {
let alias = selectPathFilter
let selectPath = selectPathFilter
if(selectPathFilter.indexOf(" as ") > -1){
let selectPathFilterParts = selectPathFilter.split(" as ")
selectPath = selectPathFilterParts[0] as string
alias = selectPathFilterParts[1] as string
} else if(selectPathFilter.indexOf(" AS ") > -1){
let selectPathFilterParts = selectPathFilter.split(" AS ")
selectPath = selectPathFilterParts[0] as string
alias = selectPathFilterParts[1] as string
}
selectAliasMap[alias] = selectPath
// if(selectPath == '*'){
// selectAliasMap[alias] = selectPath
// } else {
// selectAliasMap[alias] = `${query.from}.${selectPath}`
// }
return selectAliasMap
}, {})
// console.log(selectPathFilters)
return function(rawResource: any):{ [name:string]: string | string[] | any } {
let results = {}
for(let alias in selectPathFilters){
let selectPathFilter = selectPathFilters[alias]
if(selectPathFilter == '*'){
results[alias] = rawResource
} else {
results[alias] = fhirpath.evaluate(rawResource, selectPathFilter)
}
}
results["id"] = rawResource.id
results["resourceType"] = rawResource.resourceType
return results
}
}
}

View File

@ -9,6 +9,8 @@ import {HttpClient} from '@angular/common/http';
import weightFixture from "../fixtures/weight.json"
import claimsFixture from "../fixtures/claims.json"
import immunizationsFixture from "../fixtures/immunizations.json"
import encountersFixture from "../fixtures/encounters.json"
import {DashboardWidgetQuery} from '../../models/widget/dashboard-widget-query';
describe('DashboardWidgetComponent', () => {
let component: DashboardWidgetComponent;
@ -41,7 +43,7 @@ describe('DashboardWidgetComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
describe('processQueryResults', () => {
describe('chartProcessQueryResults', () => {
describe('Weight - SimpleLineChartWidget', () => {
it('should parse data', () => {
expect(component).toBeTruthy();
@ -73,22 +75,23 @@ describe('DashboardWidgetComponent', () => {
}
}
//test
component.processQueryResults([weightFixture['data']])
let processedQueryResponse = component.processQueryResourcesSelectClause(component.widgetConfig.queries[0].q, weightFixture)
component.chartProcessQueryResults([processedQueryResponse])
//assert
expect(component.isEmpty).toBeFalse()
expect(component.loading).toBeFalse()
expect(component.chartLabels).toEqual([
"4c0ab718-2236-4933-bb31-2ff3a62f3c97",
"6fa1b324-1951-4704-9257-41a8959c87d4",
"75b5050f-a52c-4838-a860-fdf643e99bb4",
"7bfe2e7e-0484-4fc7-90f5-35c2bace9742",
"84c89bf1-aead-481f-8cec-b1ef9683eddd",
"9a11be87-b435-4e39-b060-76b9d892f5d6",
"a425342e-cb4b-4391-b320-634fcb91ec79",
"a737caca-f54e-4896-b846-f4d1fa9b518c",
"b771e5b8-6c57-4b45-beb7-88f5a3676991",
"fc8b95c3-8068-4704-ac54-d3248ecdc64b"
['2012-04-23T12:22:44-04:00'],
['2016-05-16T12:22:44-04:00'],
['2010-04-12T12:22:44-04:00'],
['2014-05-05T12:22:44-04:00'],
['2011-04-18T12:22:44-04:00'],
['2019-06-03T12:22:44-04:00'],
['2015-05-11T12:22:44-04:00'],
['2013-04-29T12:22:44-04:00'],
['2017-05-22T12:22:44-04:00'],
['2018-05-28T12:22:44-04:00'],
])
expect(component.chartDatasets.length).toBe(1)
// @ts-ignore
@ -143,38 +146,169 @@ describe('DashboardWidgetComponent', () => {
}
}
//test
component.processQueryResults([claimsFixture['data'], immunizationsFixture['data']])
let processedClaimsQueryResponse = component.processQueryResourcesSelectClause(component.widgetConfig.queries[0].q, claimsFixture)
let processedImmunizationsQueryResponse = component.processQueryResourcesSelectClause(component.widgetConfig.queries[1].q, immunizationsFixture)
component.chartProcessQueryResults([processedClaimsQueryResponse, processedImmunizationsQueryResponse])
//assert
expect(component.isEmpty).toBeFalse()
expect(component.loading).toBeFalse()
expect(component.chartLabels).toEqual([
'2a332c10-0a12-4d96-a819-0b2a6bfae84a',
'31830307-f2ea-4aee-ab3a-d9623b5cfd2c',
'4738ca48-c949-4d1e-be77-71b7b81a1aa2',
'51ace308-124f-45fa-8d2a-d8e0d84716f7',
'684178bf-2231-4641-a581-9ecde3b3e60c',
'70435780-0fcf-4d08-af8d-a90cac6806d9',
'70db153f-b145-44a0-b8aa-aac646d01c24',
'720374cc-f64a-402e-9f07-940fc22ceafe',
'82c6b29a-453c-4bc7-b3e8-94bf997622d4',
'82f6e9a4-46ba-4d78-a0cb-d37609662918',
'85205d78-9fe4-48d6-979d-5697fa42aebc',
'a29483c2-7bdc-428d-aa5a-1777fe18b81a',
'c8ea36b5-1c0b-4488-9df4-6b101048eec5',
'd4ddd4b5-f57b-4304-a12b-b74914e79d88',
'd591e8f1-744b-464a-99ab-9131b970863c',
'd660e444-49a6-4633-a761-e95b12a5a8eb'
expect(component.chartLabels).toEqual([ //TODO: should this be 'Immunization' and 'Claim'?
'Immunization'
])
expect(component.chartDatasets.length).toBe(2)
// // @ts-ignore
expect(component.chartDatasets[0].data.length).toBe(22)
expect(component.chartDatasets[1].data.length).toBe(16)
expect(component.chartDatasets[0].data.length).toBe(1)
expect(component.chartDatasets[1].data.length).toBe(1)
// expect(component.chartDatasets.length).toBe(component.chartLabels.length)
});
})
describe('Recent Encounters - TableWidget', () => {
it('should parse data', () => {
expect(component).toBeTruthy();
//setup
component.widgetConfig = {
"title_text": "Recent Encounters",
"description_text": "Recent interactions with healthcare providers",
"x": 4,
"y": 10,
"width": 8,
"height": 4,
"item_type": "table-widget",
"queries": [{
"q": {
"select": [
"serviceProvider.display as institution",
"period.start as date",
"reasonCode.coding.display.first() as reason",
"participant.individual.display as provider"
],
"from": "Encounter",
"where": {}
}
}],
"parsing": {
"Id": "id",
"Institution": "institution",
"Reason": "reason",
"Provider": "provider"
}
}
//test
let processedEncountersQueryResponse = component.processQueryResourcesSelectClause(component.widgetConfig.queries[0].q, encountersFixture)
component.chartProcessQueryResults([processedEncountersQueryResponse])
//assert
expect(component.isEmpty).toBeFalse()
expect(component.loading).toBeFalse()
expect(component.chartLabels).toEqual([
'9ea75521-441c-41e4-96df-237219e5ca63',
'cb3ae560-0a65-41f8-9712-aa3c5766ffe5',
'47f5936b-ef68-4404-9183-8971e75e5a1d',
'd051d64b-5b2f-4465-92c3-4e693d54f653',
'cde6c902-957a-48c1-883f-38c808188fe3',
'9f4e40e3-6f6b-4959-b407-3926e9ed049c',
'cd4bfc24-8fff-4eb0-bd93-3c7054551514',
'9adf6236-72dc-4abf-ab7e-df370b02701c',
'9a6f9230-3549-43e1-83c8-f0fd740a24f3',
'919f002e-ff43-4ea6-8acb-be3f1139c59d',
'7a38b6bf-1787-4227-af08-ae10c29632e4',
'e44ec534-b752-4647-acd3-3794bc51db6d',
'51dec3d2-a098-4671-99bc-d7cf68561903',
'1a3bbe38-b7c3-43fc-b175-740adfcaa83a',
'3fba349b-e165-45f7-9cf2-66c391f293b6',
'24f73ec3-9b7a-42a9-be03-53bffd9b304c',
'92b439da-6fdd-48e5-aa8d-b0543f840a1b',
'd9a7c76f-dfe4-41c6-9924-82a405613f44',
])
expect(component.chartDatasets.length).toBe(1)
// // @ts-ignore
expect(component.chartDatasets[0].data.length).toBe(18)
// expect(component.chartDatasets.length).toBe(component.chartLabels.length)
});
})
describe('Vitals - ListWidget', () => {})
describe('Resource Aggregation - DonutWidget', () => {})
})
it('fhirPathMapQueryFn should generate a valid select map with aliases', () => {
//setup
let patient = {
"resourceType": "Patient",
"id": "example",
"address": [
{
"use": "home",
"city": "PleasantVille",
"type": "both",
"state": "Vic",
"line": [
"534 Erewhon St"
],
"postalCode": "3999",
"period": {
"start": "1974-12-25"
},
"district": "Rainbow",
"text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999"
}
]
}
let query = {
// use: string
select: ['address.use', "address.where(type='both').state as state"],
from: 'Patient',
where: {'id':'example'}
} as DashboardWidgetQuery
let query2 = {
// use: string
select: ['*', "address.where(type='both').use | address.city as joined"],
from: 'Patient',
where: {'id':'example'}
} as DashboardWidgetQuery
//test
let fn = component.fhirPathMapQueryFn(query)
expect(fn(patient)).toEqual({ "address.use": [ 'home' ], "state": [ 'Vic' ], 'id': 'example', 'resourceType': 'Patient' })
// let fn2 = service.fhirPathMapQueryFn(query2)
let fn2 = component.fhirPathMapQueryFn(query2)
expect(fn2(patient)).toEqual({
"joined": [ 'home', 'PleasantVille' ],
"*": {
"resourceType": "Patient",
"id": "example",
"address": [
{
"use": "home",
"city": "PleasantVille",
"type": "both",
"state": "Vic",
"line": [
"534 Erewhon St"
],
"postalCode": "3999",
"period": {
"start": "1974-12-25"
},
"district": "Rainbow",
"text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999"
}
]
}, 'id': 'example', 'resourceType': 'Patient' })
});
});

View File

@ -10,6 +10,11 @@ import {forkJoin} from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import * as _ from 'lodash';
import {LoadingWidgetComponent} from '../loading-widget/loading-widget.component';
import {ResponseWrapper} from '../../models/response-wrapper';
import {ResourceFhir} from '../../models/fasten/resource_fhir';
import {DashboardWidgetQuery} from '../../models/widget/dashboard-widget-query';
import fhirpath from 'fhirpath';
import {map} from 'rxjs/operators';
@Component({
standalone: true,
@ -41,7 +46,7 @@ export class DashboardWidgetComponent implements OnInit, DashboardWidgetComponen
// }[] = [];
chartDatasetsDefaults = [];
chartDatasets: ChartConfiguration<'line'>['data']['datasets'] = [];
chartLabels: string[] = [];
chartLabels: any[] | string[] = [];
chartOptions: ChartOptions = {}
// chartColors: any;
@ -55,8 +60,14 @@ export class DashboardWidgetComponent implements OnInit, DashboardWidgetComponen
}
this.loading = true
var currentThis = this
forkJoin(this.widgetConfig.queries.map(query => { return this.fastenApi.queryResources(query.q)})).subscribe(
this.processQueryResults.bind(currentThis),
forkJoin(this.widgetConfig.queries.map(query => {
return this.fastenApi.queryResources(query.q).pipe(
map((responseWrapper: ResponseWrapper) => {
return this.processQueryResourcesSelectClause(query.q, responseWrapper)
})
)
})).subscribe(
this.chartProcessQueryResults.bind(currentThis),
(error) => {
this.loading = false
},
@ -65,8 +76,8 @@ export class DashboardWidgetComponent implements OnInit, DashboardWidgetComponen
})
}
//requires widgetConfig to be specified
processQueryResults(queryResults: any[]) {
//process query requests for chartJS library. This is the default implementation, but can be overridden by child classes
chartProcessQueryResults(queryResults: any[]) {
try {
this.chartDatasets = []
@ -129,4 +140,92 @@ export class DashboardWidgetComponent implements OnInit, DashboardWidgetComponen
}
}
// This function will process the raw response from the Dashboard Query API call, which requires frontend processing of the select clause.
// it will call the fhirPathMapQueryFn which will extract FHIRPath values from the resource_raw field of the ResourceFhir object
// fhirPathMapQueryFn will also assign aliases where appropriate.
// `where` clause filtering is processed in the backend.
processQueryResourcesSelectClause(query: DashboardWidgetQuery, response: ResponseWrapper): any[]{
console.log("RESPONSE", response)
if(!response.data || !response.data.length){
console.log("NO QUERY DATA FOUND")
return []
}
let results = response.data
.map((resource: ResourceFhir) => {
if (!resource.resource_raw) {
return null
}
return this.fhirPathMapQueryFn(query)(resource.resource_raw)
})
if(query.aggregation_type){
switch (query.aggregation_type) {
case "countBy":
return Object.entries(_[query.aggregation_type](results, ...(query.aggregation_params || []))).map(pair => {
return {key: pair[0], value: pair[1]}
})
break;
default:
throw new Error("unsupported aggregation type")
}
}
else {
return results
}
}
// This function will convert DashboardWidgetQuery.select filters into a FHIRPath query strings and return the results
// as a map (keyed by the select alias)
// ie. `name.where(given='Jim')` will be converted to `Patient.name.where(given='Jim')`
// ie. `name.where(given='Jim') as GivenName` will be converted to `Patient.name.where(given='Jim')` and be stored in the returned map as GivenName`
// the returned map will always contain a `id` key, which will be the resource id and a `resourceType` key, which will be the resource type
fhirPathMapQueryFn(query: DashboardWidgetQuery): (rawResource: any) => { [name:string]: string | string[] | any } {
let selectPathFilters: { [name:string]: string } = query.select.reduce((selectAliasMap, selectPathFilter): { [name:string]: string } => {
let alias = selectPathFilter
let selectPath = selectPathFilter
if(selectPathFilter.indexOf(" as ") > -1){
let selectPathFilterParts = selectPathFilter.split(" as ")
selectPath = selectPathFilterParts[0] as string
alias = selectPathFilterParts[1] as string
} else if(selectPathFilter.indexOf(" AS ") > -1){
let selectPathFilterParts = selectPathFilter.split(" AS ")
selectPath = selectPathFilterParts[0] as string
alias = selectPathFilterParts[1] as string
}
selectAliasMap[alias] = selectPath
// if(selectPath == '*'){
// selectAliasMap[alias] = selectPath
// } else {
// selectAliasMap[alias] = `${query.from}.${selectPath}`
// }
return selectAliasMap
}, {})
// console.log(selectPathFilters)
return function(rawResource: any):{ [name:string]: string | string[] | any } {
let results = {}
for(let alias in selectPathFilters){
let selectPathFilter = selectPathFilters[alias]
if(selectPathFilter == '*'){
results[alias] = rawResource
} else {
results[alias] = fhirpath.evaluate(rawResource, selectPathFilter)
}
}
results["id"] = rawResource.id
results["resourceType"] = rawResource.resourceType
return results
}
}
}

View File

@ -0,0 +1,200 @@
{
"data": [{
"id": "11116c9e-ff18-4180-872f-a4aab7c537c7",
"created_at": "2023-06-21T10:30:33.641052-07:00",
"updated_at": "2023-06-21T10:30:33.641052-07:00",
"user_id": "8b6a16f4-4a5f-4b01-bf4c-93c73c9ff0e8",
"source_id": "9d6c6593-4349-4a35-830a-b6eef25b80cc",
"source_resource_type": "Patient",
"source_resource_id": "b426b062-8273-4b93-a907-de3176c0567d",
"sort_date": null,
"sort_title": null,
"resource_raw": {
"resourceType": "Patient",
"id": "b426b062-8273-4b93-a907-de3176c0567d",
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Generated by <a href=\"https://github.com/synthetichealth/synthea\">Synthea</a>.Version identifier: v2.4.0-404-ge7ce2295\n . Person seed: 4209933468692678950 Population seed: 0</div>"
},
"extension": [
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2106-3",
"display": "White"
}
},
{
"url": "text",
"valueString": "White"
}
]
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
"extension": [
{
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2186-5",
"display": "Not Hispanic or Latino"
}
},
{
"url": "text",
"valueString": "Not Hispanic or Latino"
}
]
},
{
"url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName",
"valueString": "Sanda877 Kshlerin58"
},
{
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
"valueCode": "M"
},
{
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace",
"valueAddress": {
"city": "Peabody",
"state": "Massachusetts",
"country": "US"
}
},
{
"url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years",
"valueDecimal": 0.0
},
{
"url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years",
"valueDecimal": 16.0
}
],
"identifier": [
{
"system": "https://github.com/synthetichealth/synthea",
"value": "262b819a-5193-404a-9787-b7f599358035"
},
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR",
"display": "Medical Record Number"
}
],
"text": "Medical Record Number"
},
"system": "http://hospital.smarthealthit.org",
"value": "262b819a-5193-404a-9787-b7f599358035"
},
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "SS",
"display": "Social Security Number"
}
],
"text": "Social Security Number"
},
"system": "http://hl7.org/fhir/sid/us-ssn",
"value": "999-19-7941"
},
{
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "DL",
"display": "Driver's License"
}
],
"text": "Driver's License"
},
"system": "urn:oid:2.16.840.1.113883.4.3.25",
"value": "S99986287"
}
],
"name": [
{
"use": "official",
"family": "Heller342",
"given": [
"Abraham100"
]
}
],
"telecom": [
{
"system": "phone",
"value": "555-803-7962",
"use": "home"
}
],
"gender": "male",
"birthDate": "2002-04-15",
"address": [
{
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/geolocation",
"extension": [
{
"url": "latitude",
"valueDecimal": 42.410027249980345
},
{
"url": "longitude",
"valueDecimal": -71.07762136029856
}
]
}
],
"line": [
"834 Hansen Run"
],
"city": "Somerville",
"state": "Massachusetts",
"postalCode": "02138",
"country": "US"
}
],
"maritalStatus": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus",
"code": "S",
"display": "Never Married"
}
],
"text": "Never Married"
},
"multipleBirthBoolean": false,
"communication": [
{
"language": {
"coding": [
{
"system": "urn:ietf:bcp:47",
"code": "en-US",
"display": "English"
}
],
"text": "English"
}
}
]
},
"related_resources": null
}],
"success": true
}

View File

@ -5,6 +5,9 @@ import {FastenApiService} from '../../services/fasten-api.service';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
import {RouterTestingModule} from '@angular/router/testing';
import {of} from 'rxjs';
import patientVitalsObservationFixture from "../fixtures/patient_vitals_observation.json"
import patientVitalsPatientFixture from "../fixtures/patient_vitals_patient.json"
describe('PatientVitalsWidgetComponent', () => {
let component: PatientVitalsWidgetComponent;
@ -28,6 +31,8 @@ describe('PatientVitalsWidgetComponent', () => {
]
})
.compileComponents();
mockedFastenApiService.queryResources.and.returnValue(of(patientVitalsObservationFixture));
fixture = TestBed.createComponent(PatientVitalsWidgetComponent);
component = fixture.componentInstance;
@ -37,4 +42,37 @@ describe('PatientVitalsWidgetComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
describe('vitalsProcessQueryResults', () => {
describe('PatientVitals - PatientVitalsWidget', () => {
it('should parse data', () => {
expect(component).toBeTruthy();
//test
let processedVitalsQueryResponse = component.processQueryResourcesSelectClause(component.widgetConfig.queries[0].q, patientVitalsObservationFixture)
let processedPatientQueryResponse = component.processQueryResourcesSelectClause(component.widgetConfig.queries[1].q, patientVitalsPatientFixture)
component.chartProcessQueryResults([processedVitalsQueryResponse, processedPatientQueryResponse])
//assert
// name: string = ''
// age: string = ''
// gender: string = ''
// vitalSigns: {
// display: string,
// code: string,
// date: string,
// value: string,
// unit: string
// }[] = []
expect(component.name).toEqual('Abraham100 Heller342')
expect(component.age).toEqual('21 years')
expect(component.gender).toEqual('male')
expect(component.vitalSigns.length).toEqual(16)
});
})
})
});

View File

@ -72,12 +72,12 @@ export class PatientVitalsWidgetComponent extends DashboardWidgetComponent imple
} as DashboardWidgetConfig
super.ngOnInit();
this.chartDatasetsSubject.subscribe(this.processQueryResults.bind(this))
this.chartDatasetsSubject.subscribe(this.vitalsProcessQueryResults.bind(this))
}
//process query results
processQueryResults(queryResults: any[]): void {
vitalsProcessQueryResults(queryResults: any[]): void {
if(!queryResults || queryResults.length < 2){
return
}

View File

@ -4,6 +4,7 @@ import { TableWidgetComponent } from './table-widget.component';
import {FastenApiService} from '../../services/fasten-api.service';
import {HTTP_CLIENT_TOKEN} from '../../dependency-injection';
import {HttpClient} from '@angular/common/http';
import encountersFixture from "../fixtures/encounters.json"
describe('TableWidgetComponent', () => {
let component: TableWidgetComponent;
@ -36,4 +37,81 @@ describe('TableWidgetComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
describe('tableProcessQueryResults', () => {
describe('Recent Encounters - TableWidget', () => {
it('should parse data', () => {
expect(component).toBeTruthy();
//setup
component.widgetConfig = {
"title_text": "Recent Encounters",
"description_text": "Recent interactions with healthcare providers",
"x": 4,
"y": 10,
"width": 8,
"height": 4,
"item_type": "table-widget",
"queries": [{
"q": {
"select": [
"serviceProvider.display as institution",
"period.start as date",
"reasonCode.coding.display.first() as reason",
"participant.individual.display as provider"
],
"from": "Encounter",
"where": {}
}
}],
"parsing": {
"Id": "id",
"Institution": "institution",
"Reason": "reason",
"Provider": "provider"
}
}
//test
let processedQueryResponse = component.processQueryResourcesSelectClause(component.widgetConfig.queries[0].q, encountersFixture)
component.chartProcessQueryResults([processedQueryResponse])
//assert
expect(component.keys).toEqual([
'id',
'institution',
'reason',
'provider',
])
expect(component.headers).toEqual([
'Id',
'Institution',
'Reason',
'Provider',
])
expect(component.rows).toEqual([
[ '9ea75521-441c-41e4-96df-237219e5ca63', 'PCP1336', '', 'Dr. Mariela993 Schamberger479'],
[ 'cb3ae560-0a65-41f8-9712-aa3c5766ffe5', 'PCP1336', '', 'Dr. Mariela993 Schamberger479'],
[ '47f5936b-ef68-4404-9183-8971e75e5a1d', 'HALLMARK HEALTH SYSTEM', 'Acute bronchitis (disorder)', 'Dr. Renato359 Jenkins714' ],
[ 'd051d64b-5b2f-4465-92c3-4e693d54f653', 'PCP1336', '', 'Dr. Mariela993 Schamberger479'],
[ 'cde6c902-957a-48c1-883f-38c808188fe3', 'PCP1336', '', 'Dr. Mariela993 Schamberger479' ],
[ '9f4e40e3-6f6b-4959-b407-3926e9ed049c', 'PCP1336', '', 'Dr. Mariela993 Schamberger479' ],
[ 'cd4bfc24-8fff-4eb0-bd93-3c7054551514', 'PROMPT CARE WALK-IN CLINIC', '', 'Dr. Candyce305 Prohaska837' ],
[ '9adf6236-72dc-4abf-ab7e-df370b02701c', 'HALLMARK HEALTH SYSTEM', 'Otitis media', 'Dr. Renato359 Jenkins714' ],
[ '9a6f9230-3549-43e1-83c8-f0fd740a24f3', 'HALLMARK HEALTH SYSTEM', 'Viral sinusitis (disorder)', 'Dr. Renato359 Jenkins714' ],
[ '919f002e-ff43-4ea6-8acb-be3f1139c59d', 'HALLMARK HEALTH SYSTEM', 'Viral sinusitis (disorder)', 'Dr. Renato359 Jenkins714' ],
[ '7a38b6bf-1787-4227-af08-ae10c29632e4', 'HALLMARK HEALTH SYSTEM', 'Viral sinusitis (disorder)', 'Dr. Renato359 Jenkins714' ],
[ 'e44ec534-b752-4647-acd3-3794bc51db6d', 'PCP1336', '', 'Dr. Mariela993 Schamberger479' ],
[ '51dec3d2-a098-4671-99bc-d7cf68561903', 'PCP1336', '', 'Dr. Mariela993 Schamberger479' ],
[ '1a3bbe38-b7c3-43fc-b175-740adfcaa83a', 'PCP1336', '', 'Dr. Mariela993 Schamberger479' ],
[ '3fba349b-e165-45f7-9cf2-66c391f293b6', 'HALLMARK HEALTH SYSTEM', 'Viral sinusitis (disorder)', 'Dr. Renato359 Jenkins714' ],
[ '24f73ec3-9b7a-42a9-be03-53bffd9b304c', 'HALLMARK HEALTH SYSTEM' , 'Acute viral pharyngitis (disorder)', 'Dr. Renato359 Jenkins714' ],
[ '92b439da-6fdd-48e5-aa8d-b0543f840a1b', 'PCP1336', '', 'Dr. Mariela993 Schamberger479' ],
[ 'd9a7c76f-dfe4-41c6-9924-82a405613f44', 'PCP1336', '', 'Dr. Mariela993 Schamberger479' ],
])
});
})
})
});

View File

@ -18,10 +18,10 @@ export class TableWidgetComponent extends DashboardWidgetComponent implements O
rows: any[] = []
ngOnInit(): void {
super.ngOnInit()
this.chartDatasetsSubject.subscribe(this.processQueryResults.bind(this))
this.chartDatasetsSubject.subscribe(this.tableProcessQueryResults.bind(this))
}
processQueryResults(queryResults: any[]) {
tableProcessQueryResults(queryResults: any[]) {
if(!queryResults || queryResults.length < 1){
return
}