From a560825b8975d50d2cd364076307a15853f0369e Mon Sep 17 00:00:00 2001 From: kghoreshi Date: Tue, 9 Aug 2022 15:10:31 -0400 Subject: [PATCH 1/4] relaunch from appContext response --- src/App.jsx | 81 ++++---- .../QuestionnaireForm/QuestionnaireForm.jsx | 5 +- src/index.js | 2 - src/util/fetchArtifacts.js | 185 +++++++++++++++++- src/util/util.js | 6 + 5 files changed, 232 insertions(+), 47 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index dc86b8bd..2e8643dd 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom' import "./App.css"; import cqlfhir from "cql-exec-fhir"; import executeElm from "./elmExecutor/executeElm"; -import fetchArtifacts from "./util/fetchArtifacts"; +import {fetchArtifacts, fetchArtifactsOperation, fetchFromQuestionnaireResponse} from "./util/fetchArtifacts"; import fetchFhirVersion from "./util/fetchFhirVersion"; import { buildFhirUrl } from "./util/util"; import PriorAuth from "./components/PriorAuth/PriorAuth"; @@ -39,7 +39,7 @@ export default class App extends Component { priorAuthClaim: null, specialtyRxBundle: null, cqlPrepopulationResults: null, - deviceRequest: null, + orderResource: null, bundle: null, filter: true, filterChecked: true, @@ -73,11 +73,13 @@ export default class App extends Component { this.standaloneLaunch = this.standaloneLaunch.bind(this); this.filter = this.filter.bind(this); this.onFilterCheckboxRefChange = this.onFilterCheckboxRefChange.bind(this); + this.fetchResourcesAndExecuteCql = this.fetchResourcesAndExecuteCql.bind(this); } componentDidMount() { if(!this.props.standalone) { this.ehrLaunch(false); + // fetchArtifactsOperation(this.appContext, this.smart, this.consoleLog); } } @@ -104,61 +106,63 @@ export default class App extends Component { ehrLaunch(isContainedQuestionnaire, questionnaire) { // Temporary indication before full supports for relaunch is implemented - if(!this.appContext.request) { - alert("Supports for relaunch will be added in the near future!"); - this.consoleLog("Supports for relaunch will be added in the near future!", "errorClass"); - return; + if(this.appContext.response) { + // start relaunch + fetchFromQuestionnaireResponse(this.appContext.response, this.smart).then((relaunchContext) => { + this.setState({response: relaunchContext.response}) + this.fetchResourcesAndExecuteCql(relaunchContext.order, relaunchContext.coverage, relaunchContext.questionnaire); + }); + } else { + if(!this.appContext.order) { + alert("Supports for relaunch will be added in the near future!"); + this.consoleLog("Supports for relaunch will be added in the near future!", "errorClass"); + return; + } + this.consoleLog("fetching artifacts", "infoClass"); + this.setState({ + isFetchingArtifacts: true + }) + const reloadQuestionnaire = questionnaire !== undefined; + this.setState({reloadQuestionnaire}); + this.fetchResourcesAndExecuteCql(this.appContext.order, this.appContext.coverage, this.appContext.questionnaire); } - const deviceRequest = JSON.parse(this.appContext.request.replace(/\\/g,"")); - this.consoleLog("fetching artifacts", "infoClass"); - this.setState({ - isFetchingArtifacts: true - }) - const reloadQuestionnaire = questionnaire !== undefined; - + } + + fetchResourcesAndExecuteCql(order, coverage, questionnaire) { fetchFhirVersion(this.props.smart.state.serverUrl) .then(fhirVersion => { this.fhirVersion = fhirVersion; - fetchArtifacts( - this.props.FHIR_PREFIX, - this.props.FILE_PREFIX, - !isContainedQuestionnaire ? this.appContext.template : questionnaire, - this.fhirVersion, - this.smart, - this.consoleLog, - isContainedQuestionnaire - ) + fetchArtifactsOperation(order, coverage, questionnaire, this.smart, this.consoleLog) .then(artifacts => { console.log("fetched needed artifacts:", artifacts); - + const orderResource = artifacts.order; let fhirWrapper = this.getFhirWrapper(this.fhirVersion); - this.setState({ questionnaire: artifacts.questionnaire }); - this.setState({ deviceRequest: deviceRequest }); + this.setState({ orderResource: orderResource }); this.setState({ isAdaptiveFormWithoutExtension: artifacts.questionnaire.meta && artifacts.questionnaire.meta.profile && artifacts.questionnaire.meta.profile.includes("http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-adapt") && (artifacts.questionnaire.extension === undefined || !artifacts.questionnaire.extension.includes(e => e.url === "http://hl7.org/fhir/StructureDefinition/cqf-library")) }); this.setState({ }); // execute for each main library return Promise.all( artifacts.mainLibraryElms.map(mainLibraryElm => { let parameterObj; - if (deviceRequest.resourceType === "DeviceRequest") { + if (orderResource.resourceType === "DeviceRequest") { parameterObj = { - device_request: fhirWrapper.wrap(deviceRequest) + device_request: fhirWrapper.wrap(orderResource) }; } else if ( - deviceRequest.resourceType === "ServiceRequest" + orderResource.resourceType === "ServiceRequest" ) { parameterObj = { - service_request: fhirWrapper.wrap(deviceRequest) + service_request: fhirWrapper.wrap(orderResource) }; - } else if (deviceRequest.resourceType === "MedicationRequest") { + } else if (orderResource.resourceType === "MedicationRequest") { parameterObj = { - medication_request: fhirWrapper.wrap(deviceRequest) + medication_request: fhirWrapper.wrap(orderResource) }; - } else if (deviceRequest.resourceType === "MedicationDispense") { + } else if (orderResource.resourceType === "MedicationDispense") { parameterObj = { - medication_dispense: fhirWrapper.wrap(deviceRequest) + medication_dispense: fhirWrapper.wrap(orderResource) }; } @@ -194,7 +198,7 @@ export default class App extends Component { return executeElm( this.smart, this.fhirVersion, - deviceRequest, + orderResource, executionInputs, this.consoleLog ); @@ -247,13 +251,11 @@ export default class App extends Component { this.setState({ bundle: fullBundle, cqlPrepopulationResults: allLibrariesResults, - isFetchingArtifacts: false, - reloadQuestionnaire + isFetchingArtifacts: false }); }); }); } - getFhirWrapper(fhirVersion) { if (fhirVersion == "r4") { return cqlfhir.FHIRWrapper.FHIRv400(); @@ -615,8 +617,9 @@ export default class App extends Component { ) : ( { + parameters.parameter.push({"name": "coverage", "resource": coverage}); + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/fhir+json' }, + body: JSON.stringify(parameters) + }; + fetch(`${questionnaire}/$questionnaire-package`, requestOptions) + .then(handleFetchErrors) + .then((e)=> {return e.json()}).then((result) => { + // TODO: Handle multiple questionnaires + const bundle = result.parameter[0].resource.entry; + const questionnaire = bundle.find((e) => e.resource.resourceType === "Questionnaire")?.resource; + + retVal.questionnaire = questionnaire; + retVal.isAdaptiveFormWithoutExtension = questionnaire.extension && questionnaire.extension.length > 0; + + findQuestionnaireEmbeddedCql(questionnaire.item); + searchBundle(questionnaire, bundle); + console.log(retVal); + resolve(retVal); + }) + }) + } + + function searchBundle(questionnaire, bundle) { + if (questionnaire.extension !== undefined) { + // grab all main elm urls + // R4 resources use cqf library. + var mainElmReferences = questionnaire.extension.filter(ext => ext.url == "http://hl7.org/fhir/StructureDefinition/cqf-library") + .map(lib => lib.valueCanonical); + bundle.forEach((entry) => { + var resource = entry.resource; + if(resource.resourceType === "Library") { + const base64elmData = resource.content.filter(c => c.contentType == "application/elm+json")[0].data; + // parse the json string + let elm = JSON.parse(Buffer.from(base64elmData, 'base64')); + if (mainElmReferences.find((mainElmReference) => { + return resource.url === mainElmReference + })){ + // set the elm where it needs to be + retVal.mainLibraryElms.push(elm); + elmLibraryMaps[elm.library.identifier.id] = resource; + retVal.mainLibraryMaps = elmLibraryMaps; + } else { + retVal.dependentElms.push(elm); + } + } else if(resource.resourceType === "ValueSet") { + retVal.valueSets.push(resource); + } + }) + // mainElmReferences.forEach((mainElmReference) => { + // console.log(mainElmReference); + // var libraryResource = bundle.find((e)=>{return e.resource.url === mainElmReference})?.resource; + // if(libraryResource) { + // const base64elmData = libraryResource.content.filter(c => c.contentType == "application/elm+json")[0].data; + // Buffer.from(base64elmData, 'base64'); + + // // parse the json string + // let elm = JSON.parse(elmString); + + // // set the elm where it needs to be + // retVal.mainLibraryElms.push(elm); + // elmLibraryMaps[elm.library.identifier.id] = libraryResource; + // retVal.mainLibraryMaps = elmLibraryMaps; + // } + + // }); + } + } + // recursively searches questionnaire for + // embedded cql and puts it in the main + // elm library list + function findQuestionnaireEmbeddedCql(inputItems) { + if(!inputItems) { + return; + } + inputItems.forEach(item => { + const itemExtensions = item.extension; + if(item.extension) { + let findEmbeddedCql = item.extension.find(ext => + ext.url === "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-initialExpression" + && ext.valueExpression && ext.valueExpression.language === "application/elm+json"); + + if(findEmbeddedCql) { + const itemLibrary = JSON.parse(findEmbeddedCql.valueExpression.expression); + itemLibrary.library.identifier= { + id: "LibraryLinkId" + item.linkId, + version: "0.0.1" + }; + elmLibraryMaps[itemLibrary.library.identifier.id] = itemLibrary; + retVal.mainLibraryMaps = elmLibraryMaps; + retVal.mainLibraryElms.push(itemLibrary); + } + } + + if(item.item !== undefined && item.item.length > 0) { + findQuestionnaireEmbeddedCql(item.item); + } + }); + } -function fetchArtifacts(fhirPrefix, filePrefix, questionnaireReference, fhirVersion, smart, consoleLog, isContainedQuestionnaire) { + if(isRequestReference(order)) { + smart.request(order).then((orderResource) => { + completeOperation(orderResource); + }) + } else { + const orderResource = JSON.parse(order.replace(/\\/g,"")); + completeOperation(orderResource) + } + + }) +} +function fetchArtifacts(questionnaireReference, fhirVersion, smart, consoleLog, isContainedQuestionnaire) { return new Promise(function(resolve, reject) { function handleFetchErrors(response) { @@ -273,4 +419,37 @@ function fetchArtifacts(fhirPrefix, filePrefix, questionnaireReference, fhirVers }); } -export default fetchArtifacts; \ No newline at end of file +function fetchFromQuestionnaireResponse(response, smart) { + const relaunchContext = { + questionnaire: null, + order: null, + coverage: null, + response: null, + } + + return new Promise(function(resolve, reject) { + smart.request(response).then((res) => { + console.log(res); + relaunchContext.questionnaire = res.questionnaire; + relaunchContext.response = res; + if(res.extension) { + const extensions = res.extension.filter((ext) => ext.url === "http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/context") + extensions.forEach((ext) => { + if(ext.valueReference.type === "Coverage") { + relaunchContext.coverage = ext.valueReference.reference; + } else { + relaunchContext.order = ext.valueReference.reference; + } + }) + } + resolve(relaunchContext); + }) + }) + +} + +export { + fetchArtifacts, + fetchArtifactsOperation, + fetchFromQuestionnaireResponse +}; \ No newline at end of file diff --git a/src/util/util.js b/src/util/util.js index 98f71172..3ef2f5d4 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -8,6 +8,11 @@ function findValueByPrefix(object, prefix) { } } +function isRequestReference(reference) { + const re = /^(?:DeviceRequest|MedicationRequest|ServiceRequest)\/\w+$/ + return re.exec(reference)?true:false; +} + function buildFhirUrl(reference, fhirPrefix, fhirVersion) { if (reference.startsWith("http")) { var endIndex = reference.lastIndexOf("/"); @@ -155,6 +160,7 @@ function searchQuestionnaire(questionnaire, attestation) { } export { + isRequestReference, findValueByPrefix, getListOfChoices, postToLogs, From cabcdb0108850b370723f36021e7989c4525ecf4 Mon Sep 17 00:00:00 2001 From: kghoreshi Date: Tue, 16 Aug 2022 15:49:38 -0400 Subject: [PATCH 2/4] implement search by order --- src/App.jsx | 41 ++++++++++++++----- .../QuestionnaireForm/QuestionnaireForm.jsx | 12 ++++-- src/util/fetchArtifacts.js | 21 +++++++++- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 2e8643dd..7a223660 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,7 +3,7 @@ import ReactDOM from 'react-dom' import "./App.css"; import cqlfhir from "cql-exec-fhir"; import executeElm from "./elmExecutor/executeElm"; -import {fetchArtifacts, fetchArtifactsOperation, fetchFromQuestionnaireResponse} from "./util/fetchArtifacts"; +import {fetchArtifacts, fetchArtifactsOperation, fetchFromQuestionnaireResponse, searchByOrder} from "./util/fetchArtifacts"; import fetchFhirVersion from "./util/fetchFhirVersion"; import { buildFhirUrl } from "./util/util"; import PriorAuth from "./components/PriorAuth/PriorAuth"; @@ -104,27 +104,46 @@ export default class App extends Component { }) } + ehrLaunch(isContainedQuestionnaire, questionnaire) { - // Temporary indication before full supports for relaunch is implemented - if(this.appContext.response) { + let acOrder = this.appContext.order; + let acCoverage = this.appContext.coverage; + let acQuestionnaire = this.appContext.questionnaire; + let acResponse = this.appContext.response; + if(acOrder && acCoverage && !acQuestionnaire && !acResponse) { + // TODO: There's an additional case where you could launch + // with just the order/coverage by invoking the operation + // but I think the endpoint extension on coverage which + // would facilitate that is going away in ballot. + searchByOrder(acOrder, this.smart).then((res) => { + // TODO: Don't know how to deal with multiple QRs + // Let user pick with a UI? Force orders to + // uniquely identify QRs? + // for now just pick the first one + acResponse = res[0].resource; + acQuestionnaire = acResponse.questionnaire; + this.setState({response: acResponse}); + this.fetchResourcesAndExecuteCql(acOrder, acCoverage, acQuestionnaire); + }); + } else if(acResponse) { // start relaunch - fetchFromQuestionnaireResponse(this.appContext.response, this.smart).then((relaunchContext) => { + // TODO: could potentially pass order to this function and avoid + // needing to search the QR context extension for it + // which would also support QRs without the extension. + fetchFromQuestionnaireResponse(acResponse, this.smart).then((relaunchContext) => { this.setState({response: relaunchContext.response}) this.fetchResourcesAndExecuteCql(relaunchContext.order, relaunchContext.coverage, relaunchContext.questionnaire); }); - } else { - if(!this.appContext.order) { - alert("Supports for relaunch will be added in the near future!"); - this.consoleLog("Supports for relaunch will be added in the near future!", "errorClass"); - return; - } + } else if(acQuestionnaire && acOrder && acCoverage){ this.consoleLog("fetching artifacts", "infoClass"); this.setState({ isFetchingArtifacts: true }) const reloadQuestionnaire = questionnaire !== undefined; this.setState({reloadQuestionnaire}); - this.fetchResourcesAndExecuteCql(this.appContext.order, this.appContext.coverage, this.appContext.questionnaire); + this.fetchResourcesAndExecuteCql(acOrder, acCoverage, acQuestionnaire); + } else { + alert("invalid app context") } } diff --git a/src/components/QuestionnaireForm/QuestionnaireForm.jsx b/src/components/QuestionnaireForm/QuestionnaireForm.jsx index dedfb4a8..e7b8a6dd 100644 --- a/src/components/QuestionnaireForm/QuestionnaireForm.jsx +++ b/src/components/QuestionnaireForm/QuestionnaireForm.jsx @@ -227,8 +227,14 @@ export default class QuestionnaireForm extends Component { let count = 0; - partialResponses.entry.forEach(r => { - if (r.resource.questionnaire.includes(this.props.qform.id)) { + partialResponses.entry.forEach(bundleEntry => { + let questionnaireId = null; + if(bundleEntry.resource.contained) { + questionnaireId = bundleEntry.resource?.contained[0]?.id; + } + const questionaireIdUrl = bundleEntry.resource.questionnaire; + + if (this.props.qform.id === questionnaireId || questionaireIdUrl.includes(this.props.qform.id)) { count = count + 1; // add the option to the popupOptions let date = new Date(bundleEntry.resource.authored); @@ -906,7 +912,7 @@ export default class QuestionnaireForm extends Component { }; this.addAuthorToResponse(qr, this.getPractitioner()); - qr.questionnaire = this.appContext.questionnaire; + qr.questionnaire = this.appContext.questionnaire?this.appContext.questionnaire:this.props.response.questionnaire; console.log("GetQuestionnaireResponse final QuestionnaireResponse: ", qr); const request = this.props.deviceRequest; diff --git a/src/util/fetchArtifacts.js b/src/util/fetchArtifacts.js index 59259b44..f8f60275 100644 --- a/src/util/fetchArtifacts.js +++ b/src/util/fetchArtifacts.js @@ -147,6 +147,7 @@ function fetchArtifactsOperation(order, coverage, questionnaire, smart, consoleL }) } + function fetchArtifacts(questionnaireReference, fhirVersion, smart, consoleLog, isContainedQuestionnaire) { return new Promise(function(resolve, reject) { @@ -448,8 +449,26 @@ function fetchFromQuestionnaireResponse(response, smart) { } +function searchByOrder(order, smart) { + let requestId; + if(isRequestReference(order)){ + requestId = order; + } else { + const orderResource = JSON.parse(order.replace(/\\/g,"")); + requestId = `${orderResource.resourceType}/${orderResource.id}` + } + return new Promise(function(resolve, reject) { + smart.request(`QuestionnaireResponse?context=${requestId}`).then((res) => { + if(res.entry) { + resolve(res.entry) + } + }) + }) +} + export { fetchArtifacts, fetchArtifactsOperation, - fetchFromQuestionnaireResponse + fetchFromQuestionnaireResponse, + searchByOrder }; \ No newline at end of file From 0609ea0470ed0585830705d24404613352c03789 Mon Sep 17 00:00:00 2001 From: kghoreshi Date: Mon, 22 Aug 2022 17:50:33 -0400 Subject: [PATCH 3/4] minor standalone change --- src/App.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 7a223660..026ab2ec 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -84,11 +84,10 @@ export default class App extends Component { } standaloneLaunch(patient, response) { - const template = `Questionnaire/${response.questionnaire}`; fetchFhirVersion(this.props.smart.state.serverUrl) .then(fhirVersion => { this.fhirVersion = fhirVersion; - const questionnaireUrl = buildFhirUrl(template, this.props.FHIR_PREFIX, this.fhirVersion); + const questionnaireUrl = response.questionnaire; fetch(questionnaireUrl).then(r => r.json()) .then(questionnaire => { this.setState({ questionnaire: questionnaire }); From 3dd4443cbc93cdc417fc59a5333ae08ad92f94fb Mon Sep 17 00:00:00 2001 From: kghoreshi Date: Tue, 23 Aug 2022 16:40:25 -0400 Subject: [PATCH 4/4] fix util bug --- src/util/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/util.js b/src/util/util.js index 3ef2f5d4..e366c3ca 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -9,7 +9,7 @@ function findValueByPrefix(object, prefix) { } function isRequestReference(reference) { - const re = /^(?:DeviceRequest|MedicationRequest|ServiceRequest)\/\w+$/ + const re = /^(?:DeviceRequest|MedicationRequest|ServiceRequest)\/.+$/ return re.exec(reference)?true:false; }