Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/hooks/OrderSignRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/Prefetch/OrderSignPrefetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export default interface OrderSignPrefetch {
patient?: string;
request?: string;
practitioner?: string;
[key: string]: string;
patient: string;
practitioner: string;
}
7 changes: 3 additions & 4 deletions src/hooks/Prefetch/OrderSignRequestPrefetch.ts
Original file line number Diff line number Diff line change
@@ -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;
}
2 changes: 1 addition & 1 deletion src/hooks/Prefetch/RequestPrefetch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Resource } from 'fhir/r4';

export default interface RequestPrefetch {
[key: string]: Resource;
[key: string]: Resource | undefined;
}
83 changes: 83 additions & 0 deletions src/hooks/Prefetch/hydrator/PrefetchHydrator.ts
Original file line number Diff line number Diff line change
@@ -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 };
1 change: 0 additions & 1 deletion src/hooks/rems.hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
141 changes: 70 additions & 71 deletions src/hooks/rems.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -135,17 +136,16 @@ 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(
'rems-order-sign',
'order-sign',
'REMS Requirement Lookup',
'REMS Requirement Lookup',
prefetch
hookPrefetch
);
const source = {
label: 'MCODE REMS Administrator Prototype',
Expand All @@ -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'));
Expand Down