Skip to content

Commit

Permalink
Merge pull request #790 from FluxNotes/improve-reference-query
Browse files Browse the repository at this point in the history
Improve queries in Smart on FHIR
  • Loading branch information
Dtphelan1 authored Oct 16, 2019
2 parents 14ec01c + 2bf43ae commit 6422b85
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 7 deletions.
6 changes: 6 additions & 0 deletions src/__test__/backend/dataaccess/DataAccess.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ const mockSmartClient = {
};
return Promise.resolve({ data });
},
read: function(type, id) {
const data = {
entry: hardCodedFHIRPatient.entry.filter(e => e['resource']['resourceType'] === type && e['resource']['id'] === id)
};
return Promise.resolve({ data });
},
conformance: function(_options) {
const data = {
rest: [{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const sampleObservationSearchData = {
entry: hardCodedFHIRPatient.entry.filter(e => e['resource']['resourceType'] === 'Observation')
};

const sampleReferenceResult = hardCodedFHIRPatient.entry.filter(e => e['resource']['resourceType'] === 'Encounter');


describe('SMART on FHIR data source', function() {
const originalWindowFHIR = window.FHIR;
Expand Down Expand Up @@ -83,7 +85,9 @@ describe('SMART on FHIR data source', function() {
.get('/fhir/Patient?_id=1078857')
.reply(200, samplePatientSearchData)
.get('/fhir/Observation?patient=1078857')
.reply(200, sampleObservationSearchData);
.reply(200, sampleObservationSearchData)
.get('/fhir/Encounter/6a8bc97e-3ba1-4fb5-a478-4b5bd888c793') // reference retrieval
.reply(200, sampleReferenceResult);
// in this case it doesn't need to fetch the metadata since the resourceTypes are manually specified

const dataSource = new McodeV09SmartOnFhirDataSource({ resourceTypes: ['Patient', 'Observation'] });
Expand Down
8 changes: 4 additions & 4 deletions src/dataaccess/HardCodedFHIRPatient.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@
"text": "Prediabetes"
},
"patient": {
"reference": "urn:uuid:eb3cdde1-0f08-4ec3-b926-e071979ec7e1"
"reference": "Patient/eb3cdde1-0f08-4ec3-b926-e071979ec7e1"
},
"encounter": {
"reference": "urn:uuid:6a8bc97e-3ba1-4fb5-a478-4b5bd888c793"
"reference": "Encounter/6a8bc97e-3ba1-4fb5-a478-4b5bd888c793"
},
"onsetDateTime": "2011-07-12T11:19:32-04:00",
"dateRecorded": "2011-07-12T11:19:32-04:00",
Expand Down Expand Up @@ -175,10 +175,10 @@
"text": "Body Height"
},
"subject": {
"reference": "urn:uuid:eb3cdde1-0f08-4ec3-b926-e071979ec7e1"
"reference": "Patient/eb3cdde1-0f08-4ec3-b926-e071979ec7e1"
},
"encounter": {
"reference": "urn:uuid:6a8bc97e-3ba1-4fb5-a478-4b5bd888c793"
"reference": "Encounter/6a8bc97e-3ba1-4fb5-a478-4b5bd888c793"
},
"effectiveDateTime": "2011-07-12T11:19:32-04:00",
"issued": "2011-07-12T11:19:32.283-04:00",
Expand Down
67 changes: 65 additions & 2 deletions src/dataaccess/McodeV09SmartOnFhirDataSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class McodeV09SmartOnFhirDataSource extends IDataSource {
};

this.resourceTypes = props && props.resourceTypes;
this.referencedEntries = new Set();
}
getGestalt() {
return this._gestalt;
Expand All @@ -48,6 +49,11 @@ class McodeV09SmartOnFhirDataSource extends IDataSource {
});
}

_fetchById(resourceType, resourceId) {
return this._client.patient.api.read({ type: resourceType, id: resourceId })
.then(response => this._handleResponseEntry(response.data));
}

_fetchAll(resourceType) {
// the FHIR server can return as many or as few results as it would like
// so we have to follow links to get the "next" set of results.
Expand All @@ -68,13 +74,35 @@ class McodeV09SmartOnFhirDataSource extends IDataSource {
.then(response => this._handleResponseBundle(response.data, prevBundle));
}

// If we retrieve a single entry, wrap it in a bundle-like object so we can process it similarly to other bundles later
_handleResponseEntry(entry) {
// If already a bundle, return as is
if (entry.resourceType && entry.resourceType === 'Bundle') return entry;
return { resourceType: 'Bundle', entry: [ { resource: entry } ] };
}

_handleResponseBundle(newBundle, prevBundle=null) {
if (prevBundle && newBundle && newBundle.entry) {
// merge the entries from the previous bundle into the new one
newBundle.entry.unshift(...prevBundle.entry);
newBundle.total = newBundle.entry.length;
}

if (newBundle && newBundle.entry) {
// Check for any value on an entry that is a reference
newBundle.entry.forEach(entry => {
for (const key in entry.resource) {
if (typeof entry.resource[key] === 'object' && entry.resource[key].reference) {
const referenceValue = entry.resource[key].reference;
// Only add new entries. Don't add the patient because we already have that.
if (!this.referencedEntries.has(referenceValue) && !referenceValue.endsWith(`Patient/${this._client.patient.id}`)) {
this.referencedEntries.add(referenceValue);
}
}
}
});
}

const nextURL = newBundle.link && newBundle.link.find(l => l.relation === 'next');
if (nextURL) {
// if the bundle includes a link to the next one,
Expand All @@ -86,6 +114,14 @@ class McodeV09SmartOnFhirDataSource extends IDataSource {
}
}

// Filter out resources that can only support searching based on patient/subject. Also include the Patient resource type.
resourceTypeFilter = (res) => {
if (res && res.searchInclude) {
return res.searchInclude.includes(`${res.type}:patient`) || res.searchInclude.includes(`${res.type}:subject`) || res.type === 'Patient';
}
return true;
}

fetchResources() {
let promise = this._getClientAsync(); // have to ensure the client is loaded first

Expand All @@ -94,7 +130,7 @@ class McodeV09SmartOnFhirDataSource extends IDataSource {
} else {
promise = promise
.then(client => client.patient.api.conformance({}))
.then(metadata => metadata.data.rest[0].resource.map(res => res.type));
.then(metadata => metadata.data.rest[0].resource.filter(this.resourceTypeFilter).map(res => res.type));
}

return promise
Expand All @@ -108,7 +144,34 @@ class McodeV09SmartOnFhirDataSource extends IDataSource {
queries.push(result);
}

return Promise.all(queries);
const referenceQueries = [];
let nonEmptyResources = [];
return Promise.all(queries)
.then((resources) => {
// After the first pass of queries are resolved, fetch any additional resources that were
// referenced that have not yet been fetched.

// Filter out any searchset bundles that don't have any entries
nonEmptyResources = nonEmptyResources.concat(resources.filter(res => res.entry));
this.referencedEntries.forEach((key) => {
const referenceArray = key.split('/');
let resourceId = '';
let resourceType = '';
// Using the last two entries of the array supports getting the type and id from references
// of types resourceType/resourceId and http://fhirserver/resourceType/resourceId
if (referenceArray.length >= 2) {
[ resourceType, resourceId ] = referenceArray.slice(referenceArray.length - 2);
}

// Check if we have already fetched the referenced resource. Only fetch ones we don't yet have
const alreadyFetched = nonEmptyResources.some(res => res.entry.some(entry => entry.resource.id === resourceId));
if (!alreadyFetched) {
const refResult = this._fetchById(resourceType, resourceId);
referenceQueries.push(refResult);
}
});
return Promise.all(referenceQueries);
}).then(refResources => nonEmptyResources.concat(refResources));
});
}

Expand Down

0 comments on commit 6422b85

Please sign in to comment.