diff --git a/src/hooks/OrderSignRequest.ts b/src/hooks/OrderSignRequest.ts index 1a69b90a..d75b04cf 100644 --- a/src/hooks/OrderSignRequest.ts +++ b/src/hooks/OrderSignRequest.ts @@ -3,6 +3,7 @@ import { Url } from 'url'; import OrderSignRequestPrefetch from './Prefetch/OrderSignRequestPrefetch'; // https://cds-hooks.hl7.org/1.0/#fhir-resource-access interface FhirAuthorization { + access_token: string; token_type: string; expires_in: number; scope: string; diff --git a/src/hooks/Prefetch/OrderSignPrefetch.ts b/src/hooks/Prefetch/OrderSignPrefetch.ts index 08d7a453..3f992c09 100644 --- a/src/hooks/Prefetch/OrderSignPrefetch.ts +++ b/src/hooks/Prefetch/OrderSignPrefetch.ts @@ -1,5 +1,5 @@ export default interface OrderSignPrefetch { - patient?: string; - request?: string; - practitioner?: string; + [key: string]: string; + patient: string; + practitioner: string; } diff --git a/src/hooks/Prefetch/OrderSignRequestPrefetch.ts b/src/hooks/Prefetch/OrderSignRequestPrefetch.ts index 39b37738..1424b01d 100644 --- a/src/hooks/Prefetch/OrderSignRequestPrefetch.ts +++ b/src/hooks/Prefetch/OrderSignRequestPrefetch.ts @@ -1,7 +1,6 @@ -import { MedicationRequest, Patient, Practitioner } from 'fhir/r4'; +import { Patient, Practitioner } from 'fhir/r4'; import RequestPrefetch from './RequestPrefetch'; export default interface OrderSignRequestPrefetch extends RequestPrefetch { - patient: Patient; - request: MedicationRequest; - practitioner: Practitioner; + patient?: Patient; + practitioner?: Practitioner; } diff --git a/src/hooks/Prefetch/RequestPrefetch.ts b/src/hooks/Prefetch/RequestPrefetch.ts index efd8714d..82ce49aa 100644 --- a/src/hooks/Prefetch/RequestPrefetch.ts +++ b/src/hooks/Prefetch/RequestPrefetch.ts @@ -1,5 +1,5 @@ import { Resource } from 'fhir/r4'; export default interface RequestPrefetch { - [key: string]: Resource; + [key: string]: Resource | undefined; } diff --git a/src/hooks/Prefetch/hydrator/PrefetchHydrator.ts b/src/hooks/Prefetch/hydrator/PrefetchHydrator.ts new file mode 100644 index 00000000..09bf3cee --- /dev/null +++ b/src/hooks/Prefetch/hydrator/PrefetchHydrator.ts @@ -0,0 +1,83 @@ +import OrderSignRequest from '../../OrderSignRequest'; +import OrderSignPrefetch from '../OrderSignPrefetch'; +import axios from 'axios'; +function jsonPath(json: any, path: string) { + // Use a regular expression to find array accessors in the form of "[i]" + const arrayRegex = /\[(\d+)\]/g; + + // Use the regex to find all the array accessors in the path + let match; + while ((match = arrayRegex.exec(path)) !== null) { + // Get the index of the array element to access + const index = match[1]; + + // Use the index to replace the array accessor in the path with the corresponding property accessor + path = path.replace(match[0], `.${index}`); + } + + // Split the path into its individual components + const pathComponents = path.split('.'); + + // Use reduce to iterate over the path components and get the corresponding value from the JSON object + return pathComponents.reduce((obj, key) => { + // If the key doesn't exist, return undefined + if (!obj || !Object.prototype.hasOwnProperty.call(obj, key)) return undefined; + + // Otherwise, return the value at the key + return obj[key]; + }, json); +} +function replaceTokens(str: string, json: any): string { + // Use a regular expression to find tokens in the form of "{{token}}" + const tokenRegex = /{{([\w.]+)}}/g; + + // Use the regex to find all the tokens in the string + let match; + while ((match = tokenRegex.exec(str)) !== null) { + // Get the token from the match + const token = match[1]; + + // Use the token to get the corresponding value from the JSON object + const value = jsonPath(json, token); + + // Replace the token in the original string with the value + str = str.replace(match[0], value); + } + + // Return the modified string + return str; +} +function resolveToken(token: string, context: OrderSignRequest) { + const fulfilledToken = replaceTokens(token, context); + const ehrUrl = `${context.fhirServer}/${fulfilledToken}`; + const access_token = context.fhirAuthorization.access_token; + const options = { + method: 'GET', + headers: { + Authorization: `Bearer ${access_token}` + } + }; + const response = axios(ehrUrl, options); + return response.then(e => { + return e.data; + }); +} +function hydrate(template: OrderSignPrefetch, request: OrderSignRequest) { + let prefetch = request.prefetch; + if (!prefetch) { + prefetch = {}; + } + // Find unfulfilled prefetch elements and resolve them using + // the defined prefetch template + const promises = Object.keys(template).map(key => { + if (!Object.prototype.hasOwnProperty.call(prefetch, key)) { + // prefetch was not fulfilled + return resolveToken(template[key], request).then(data => { + Object.assign(prefetch, { [key]: data }); + }); + } + }); + + return Promise.all(promises).then(() => prefetch); +} +export { hydrate }; diff --git a/src/hooks/rems.hook.test.ts b/src/hooks/rems.hook.test.ts index fa75f45b..488f41be 100644 --- a/src/hooks/rems.hook.test.ts +++ b/src/hooks/rems.hook.test.ts @@ -5,7 +5,6 @@ describe('hook: test rems', () => { test('should have definition and handler', () => { const prefetch = { patient: 'Patient/{{context.patientId}}', - request: 'MedicationRequest?_id={{context.draftOrders.MedicationRequest.id}}', practitioner: 'Practitioner/{{context.userId}}' }; const expectedDefinition = new OrderSign( diff --git a/src/hooks/rems.hook.ts b/src/hooks/rems.hook.ts index 37c183ed..99d9a299 100644 --- a/src/hooks/rems.hook.ts +++ b/src/hooks/rems.hook.ts @@ -5,6 +5,7 @@ import OrderSignPrefetch from './Prefetch/OrderSignPrefetch'; import { Coding } from 'fhir/r4'; import { Link } from '../cards/Card'; import config from '../config'; +import { hydrate } from './Prefetch/hydrator/PrefetchHydrator'; const CARD_DETAILS = 'Documentation Required, please complete form via Smart App link.'; // TODO: this codemap should be replaced with a system similar to original CRD's questionnaire package operation @@ -135,9 +136,8 @@ interface TypedRequestBody extends Express.Request { body: OrderSignRequest; } -const prefetch: OrderSignPrefetch = { +const hookPrefetch: OrderSignPrefetch = { patient: 'Patient/{{context.patientId}}', - request: 'MedicationRequest?_id={{context.draftOrders.MedicationRequest.id}}', practitioner: 'Practitioner/{{context.userId}}' }; const definition = new OrderSign( @@ -145,7 +145,7 @@ const definition = new OrderSign( 'order-sign', 'REMS Requirement Lookup', 'REMS Requirement Lookup', - prefetch + hookPrefetch ); const source = { label: 'MCODE REMS Administrator Prototype', @@ -162,80 +162,79 @@ function buildErrorCard(reason: string) { const handler = (req: TypedRequestBody, res: any) => { console.log('REMS order-sign hook'); try { - const context = req.body.context; - const contextRequest = context.draftOrders?.entry?.[0].resource; - const prefetch = req.body.prefetch; - const patient = prefetch?.patient; - const prefetchRequest = prefetch?.request; - const practitioner = prefetch?.practitioner; - const npi = practitioner?.identifier; + hydrate(hookPrefetch, req.body).then(hydratedPrefetch => { + const context = req.body.context; + const contextRequest = context.draftOrders?.entry?.[0].resource; + const patient = hydratedPrefetch?.patient; + const prefetchRequest = hydratedPrefetch?.request; + const practitioner = hydratedPrefetch?.practitioner; + const npi = practitioner?.identifier; + console.log(' Practitioner: ' + practitioner?.id + ' NPI: ' + npi); + console.log(' Patient: ' + patient?.id); - console.log(' MedicationRequest: ' + prefetchRequest?.id); - console.log(' Practitioner: ' + practitioner?.id + ' NPI: ' + npi); - console.log(' Patient: ' + patient?.id); - - // verify a MedicationRequest was sent - if (contextRequest && contextRequest.resourceType !== 'MedicationRequest') { - res.json(buildErrorCard('DraftOrders does not contain a MedicationRequest')); - return; - } + // verify a MedicationRequest was sent + if (contextRequest && contextRequest.resourceType !== 'MedicationRequest') { + res.json(buildErrorCard('DraftOrders does not contain a MedicationRequest')); + return; + } - // verify ids - if ( - patient?.id && - patient.id.replace('Patient/', '') !== context.patientId.replace('Patient/', '') - ) { - res.json(buildErrorCard('Context patientId does not match prefetch Patient ID')); - return; - } - if ( - practitioner?.id && - practitioner.id.replace('Practitioner/', '') !== context.userId.replace('Practitioner/', '') - ) { - res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID')); - return; - } - if ( - prefetchRequest?.id && - contextRequest && - contextRequest.id && - prefetchRequest.id.replace('MedicationRequest/', '') !== - contextRequest.id.replace('MedicationRequest/', '') - ) { - res.json(buildErrorCard('Context draftOrder does not match prefetch MedicationRequest ID')); - return; - } + // verify ids + if ( + patient?.id && + patient.id.replace('Patient/', '') !== context.patientId.replace('Patient/', '') + ) { + res.json(buildErrorCard('Context patientId does not match prefetch Patient ID')); + return; + } + if ( + practitioner?.id && + practitioner.id.replace('Practitioner/', '') !== context.userId.replace('Practitioner/', '') + ) { + res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID')); + return; + } + if ( + prefetchRequest?.id && + contextRequest && + contextRequest.id && + prefetchRequest.id.replace('MedicationRequest/', '') !== + contextRequest.id.replace('MedicationRequest/', '') + ) { + res.json(buildErrorCard('Context draftOrder does not match prefetch MedicationRequest ID')); + return; + } - const medicationCode = contextRequest?.medicationCodeableConcept?.coding?.[0]; - if (medicationCode && medicationCode.code) { - const returnCard = validCodes.some(e => { - return e.code === medicationCode.code && e.system === medicationCode.system; - }); - if (returnCard) { - const card = new Card(medicationCode.display || 'Rems', CARD_DETAILS, source, 'info'); - const links = codeMap[medicationCode.code]; - links.forEach(e => { - if (e.type == 'absolute') { - // no construction needed - card.addLink(e); - } else { - // link is SMART - // TODO: smart links should be built with discovered questionnaires, not hard coded ones - e.appContext = `${e.appContext}&order=${JSON.stringify(contextRequest)}&coverage=${ - contextRequest.insurance?.[0].reference - }`; - card.addLink(e); - } - }); - res.json({ - cards: [card] + const medicationCode = contextRequest?.medicationCodeableConcept?.coding?.[0]; + if (medicationCode && medicationCode.code) { + const returnCard = validCodes.some(e => { + return e.code === medicationCode.code && e.system === medicationCode.system; }); + if (returnCard) { + const card = new Card(medicationCode.display || 'Rems', CARD_DETAILS, source, 'info'); + const links = codeMap[medicationCode.code]; + links.forEach(e => { + if (e.type == 'absolute') { + // no construction needed + card.addLink(e); + } else { + // link is SMART + // TODO: smart links should be built with discovered questionnaires, not hard coded ones + e.appContext = `${e.appContext}&order=${JSON.stringify(contextRequest)}&coverage=${ + contextRequest.insurance?.[0].reference + }`; + card.addLink(e); + } + }); + res.json({ + cards: [card] + }); + } else { + res.json(buildErrorCard('Unsupported code')); + } } else { - res.json(buildErrorCard('Unsupported code')); + res.json(buildErrorCard('MedicationRequest does not contain a code')); } - } else { - res.json(buildErrorCard('MedicationRequest does not contain a code')); - } + }); } catch (error) { console.log(error); res.json(buildErrorCard('Unknown Error'));