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:
parent
779948a024
commit
07f0a1fbfc
|
@ -1,5 +1,5 @@
|
|||
export class ResponseWrapper {
|
||||
data: any
|
||||
success: boolean
|
||||
error: string
|
||||
error?: string
|
||||
}
|
||||
|
|
|
@ -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' })
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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' })
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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' ],
|
||||
])
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue