From ed0cdf8d286453580fea800339b533739230bf21 Mon Sep 17 00:00:00 2001 From: Keeyan Date: Wed, 13 Mar 2024 19:07:42 -0400 Subject: [PATCH 1/7] add launch (#135) * add launch * lint --- src/hooks/hookResources.ts | 51 +++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 2bea88c7..c24ba2fc 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -383,7 +383,7 @@ export async function handleCardOrder( createSmartLink(requirement.name, requirement.appContext, contextRequest) ); if (patient && patient.resourceType === 'Patient') { - createQuestionnaireSuggestion(card, requirement, patient); + createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } smartLinkCount++; } @@ -394,7 +394,7 @@ export async function handleCardOrder( createSmartLink(requirement.name, requirement.appContext, contextRequest) ); if (patient && patient.resourceType === 'Patient') { - createQuestionnaireSuggestion(card, requirement, patient); + createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } smartLinkCount++; } @@ -405,7 +405,7 @@ export async function handleCardOrder( createSmartLink(requirement.name, requirement.appContext, contextRequest) ); if (patient && patient.resourceType === 'Patient') { - createQuestionnaireSuggestion(card, requirement, patient); + createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } smartLinkCount++; } @@ -494,7 +494,8 @@ export function handleHook( export function createQuestionnaireSuggestion( card: Card, requirement: Requirement, - patient: Patient + patient: Patient, + request: MedicationRequest ) { if (requirement.appContext && requirement.appContext.includes('=')) { const qArr = requirement.appContext.split('='); // break up into parts @@ -510,8 +511,8 @@ export function createQuestionnaireSuggestion( if (qUrl) { const action: Action = { type: 'create', - description: `Create task for "completion of ${requirement.name} Questionnaire`, - resource: createQuestionnaireCompletionTask(requirement.name, qUrl, patient) + description: `Create task for "completion of ${requirement.name} Questionnaire"`, + resource: createQuestionnaireCompletionTask(requirement, patient, qUrl, request) }; const suggestion: Suggestion = { label: `Add "Completion of ${requirement.name} Questionnaire" to task list`, @@ -522,9 +523,10 @@ export function createQuestionnaireSuggestion( } } export function createQuestionnaireCompletionTask( - questionnaireTitle: string, + requirement: Requirement, + patient: Patient, questionnaireUrl: string, - patient: Patient + request: MedicationRequest ) { const taskResource: Task = { resourceType: 'Task', @@ -535,10 +537,15 @@ export function createQuestionnaireCompletionTask( { system: 'http://hl7.org/fhir/uv/sdc/CodeSystem/temp', code: 'complete-questionnaire' + }, + { + system: 'http://hl7.org/fhir/smart-app-launch/CodeSystem/smart-codes', + code: 'launch-app-ehr', + display: 'Launch application using the SMART EHR launch' } ] }, - description: `Complete ${questionnaireTitle} Questionnaire`, + description: `Complete ${requirement.name} Questionnaire`, for: { reference: `${patient.resourceType}/${patient.id}` }, @@ -549,6 +556,32 @@ export function createQuestionnaireCompletionTask( text: 'questionnaire' }, valueCanonical: `${questionnaireUrl}` + }, + { + type: { + coding: [ + { + system: 'http://hl7.org/fhir/smart-app-launch/CodeSystem/smart-codes', + code: 'smartonfhir-application', + display: 'SMART on FHIR application URL.' + } + ] + }, + valueUrl: config.smart.endpoint + }, + { + type: { + coding: [ + { + system: 'http://hl7.org/fhir/smart-app-launch/CodeSystem/smart-codes', + code: 'smartonfhir-appcontext', + display: 'Application context related to this launch.' + } + ] + }, + valueString: `${requirement.appContext}&order=${JSON.stringify(request)}&coverage=${ + request?.insurance?.[0].reference + }` } ] }; From 023ea6fc126d0e465d74c25cb5f632762bac6114 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque <41444457+plarocque4@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:50:41 -0400 Subject: [PATCH 2/7] Add support for Addyi, a medication that does not have ETASU (#136) * Add support for Addyi, a medication that does not have ETASU and only has a medication guide. * run prettier * Tweak Mongoose schema types so there are defined types for Intellisense, remove unused imports, and change double equals to triple equals * Re-extend interfaces to use mongoose.Document and access Document._id --------- Co-authored-by: Joyce Quach --- src/fhir/models.ts | 108 ++++++++-------- src/fhir/utilities.ts | 49 ++++++-- src/hooks/hookResources.ts | 90 +++++++------ src/hooks/rems.patientview.ts | 9 +- src/lib/etasu.ts | 229 +++++++++++++++++++++++----------- 5 files changed, 306 insertions(+), 179 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index 934e09f9..b1015386 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -1,23 +1,36 @@ -import { Document, Schema, model } from 'mongoose'; +import { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; +import { Schema, model, Document } from 'mongoose'; + +export interface Requirement { + name: string; + description: string; + questionnaire: Questionnaire | null; + stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string; // From fhir4.Parameters.parameter.name + createNewCase: boolean; + resourceId: string; + requiredToDispense: boolean; + appContext: string | null; +} export interface Medication extends Document { name: string; codeSystem: string; code: string; - requirements: any; + requirements: Requirement[]; } -interface MetRequirements extends Document { +export interface MetRequirements extends Document { completed: boolean; - completedQuestionnaire: any; + completedQuestionnaire: QuestionnaireResponse | null; requirementName: string; requirementDescription: string; drugName: string; stakeholderId: string; - case_numbers: any; + case_numbers: string[]; + metRequirementId: string; } -interface RemsCase extends Document { +export interface RemsCase extends Document { case_number: string; status: string; drugName: string; @@ -25,29 +38,25 @@ interface RemsCase extends Document { patientFirstName: string; patientLastName: string; patientDOB: string; - metRequirements: any; + metRequirements: Partial[]; } const medicationCollectionSchema = new Schema({ - name: { type: 'String' }, - codeSystem: { type: 'string' }, - code: { type: 'string' }, - requirements: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string' }, - description: { type: 'string' }, - questionnaire: { type: 'object' }, - stakeholderType: { type: 'string' }, - createNewCase: { type: 'boolean' }, - resourceId: { type: 'string' }, - requiredToDispense: { type: 'boolean' }, - appContext: { type: 'string' } - } + name: { type: String }, + codeSystem: { type: String }, + code: { type: String }, + requirements: [ + { + name: { type: String }, + description: { type: String }, + questionnaire: { type: Schema.Types.Mixed, default: null }, + stakeholderType: { type: String }, + createNewCase: { type: Boolean }, + resourceId: { type: String }, + requiredToDispense: { type: Boolean }, + appContext: { type: String, default: null } } - } + ] }); medicationCollectionSchema.index({ name: 1 }, { unique: true }); @@ -58,14 +67,15 @@ export const medicationCollection = model( ); const metRequirementsSchema = new Schema({ - completed: { type: 'boolean' }, - completedQuestionnaire: { type: 'object' }, - requirementName: { type: 'string' }, - requirementDescription: { type: 'string' }, - drugName: { type: 'string' }, - stakeholderId: { type: 'string' }, - case_numbers: { type: 'array', items: { type: 'string' } } + completed: { type: Boolean }, + completedQuestionnaire: { type: Schema.Types.Mixed, default: null }, + requirementName: { type: String }, + requirementDescription: { type: String }, + drugName: { type: String }, + stakeholderId: { type: String }, + case_numbers: [{ type: String }] }); + metRequirementsSchema.index( { drugName: 1, requirementName: 1, stakeholderId: 1 }, { unique: true } @@ -77,26 +87,22 @@ export const metRequirementsCollection = model( ); const remsCaseCollectionSchema = new Schema({ - case_number: { type: 'string' }, - status: { type: 'string' }, - drugName: { type: 'string' }, - patientFirstName: { type: 'string' }, - patientLastName: { type: 'string' }, - patientDOB: { type: 'string' }, - drugCode: { type: 'string' }, - metRequirements: { - type: 'array', - items: { - type: 'object', - properties: { - metRequirementId: { type: 'number' }, - completed: { type: 'boolean' }, - stakeholderId: { type: 'string' }, - requirementName: { type: 'string' }, - requirementDescription: { type: 'string' } - } + case_number: { type: String }, + status: { type: String }, + drugName: { type: String }, + patientFirstName: { type: String }, + patientLastName: { type: String }, + patientDOB: { type: String }, + drugCode: { type: String }, + metRequirements: [ + { + metRequirementId: { type: String }, + completed: { type: Boolean }, + stakeholderId: { type: String }, + requirementName: { type: String }, + requirementDescription: { type: String } } - } + ] }); export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index aceb3703..f572c505 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -136,7 +136,8 @@ export class FhirUtilities { resourceId: 'TuralioRemsPatientEnrollment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioRemsPatientEnrollment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioRemsPatientEnrollment', + questionnaire: null }, { name: 'Prescriber Enrollment', @@ -146,7 +147,8 @@ export class FhirUtilities { resourceId: 'TuralioPrescriberEnrollmentForm', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioPrescriberEnrollmentForm' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioPrescriberEnrollmentForm', + questionnaire: null }, { name: 'Prescriber Knowledge Assessment', @@ -156,7 +158,8 @@ export class FhirUtilities { resourceId: 'TuralioPrescriberKnowledgeAssessment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioPrescriberKnowledgeAssessment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioPrescriberKnowledgeAssessment', + questionnaire: null }, { name: 'Pharmacist Enrollment', @@ -164,7 +167,9 @@ export class FhirUtilities { stakeholderType: 'pharmacist', createNewCase: false, resourceId: 'TuralioPharmacistEnrollment', - requiredToDispense: true + requiredToDispense: true, + appContext: null, + questionnaire: null }, { name: 'Patient Status Update', @@ -174,7 +179,8 @@ export class FhirUtilities { resourceId: 'TuralioRemsPatientStatus', requiredToDispense: false, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioRemsPatientStatus' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioRemsPatientStatus', + questionnaire: null } ] }, @@ -191,7 +197,8 @@ export class FhirUtilities { resourceId: 'TIRFRemsPatientEnrollment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFRemsPatientEnrollment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFRemsPatientEnrollment', + questionnaire: null }, { name: 'Prescriber Enrollment', @@ -201,7 +208,8 @@ export class FhirUtilities { resourceId: 'TIRFPrescriberEnrollmentForm', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFPrescriberEnrollmentForm' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFPrescriberEnrollmentForm', + questionnaire: null }, { name: 'Prescriber Knowledge Assessment', @@ -211,7 +219,8 @@ export class FhirUtilities { resourceId: 'TIRFPrescriberKnowledgeAssessment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFPrescriberKnowledgeAssessment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFPrescriberKnowledgeAssessment', + questionnaire: null }, { name: 'Pharmacist Enrollment', @@ -219,7 +228,9 @@ export class FhirUtilities { stakeholderType: 'pharmacist', createNewCase: false, resourceId: 'TIRFPharmacistEnrollmentForm', - requiredToDispense: true + requiredToDispense: true, + appContext: null, + questionnaire: null }, { name: 'Pharmacist Knowledge Assessment', @@ -227,7 +238,9 @@ export class FhirUtilities { stakeholderType: 'pharmacist', createNewCase: false, resourceId: 'TIRFPharmacistKnowledgeAssessment', - requiredToDispense: true + requiredToDispense: true, + appContext: null, + questionnaire: null } ] }, @@ -244,7 +257,8 @@ export class FhirUtilities { resourceId: 'IPledgeRemsPatientEnrollment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/IPledgeRemsPatientEnrollment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/IPledgeRemsPatientEnrollment', + questionnaire: null }, { name: 'Prescriber Enrollment', @@ -254,7 +268,8 @@ export class FhirUtilities { resourceId: 'IPledgeRemsPrescriberEnrollmentForm', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/IPledgeRemsPrescriberEnrollmentForm' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/IPledgeRemsPrescriberEnrollmentForm', + questionnaire: null }, { name: 'Pharmacist Enrollment', @@ -262,9 +277,17 @@ export class FhirUtilities { stakeholderType: 'pharmacist', createNewCase: false, resourceId: 'IPledgeRemsPharmacistEnrollmentForm', - requiredToDispense: true + requiredToDispense: true, + appContext: null, + questionnaire: null } ] + }, + { + name: 'Addyi', + codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: '1666386', + requirements: [] } ]; diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index c24ba2fc..1c08efa3 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -1,20 +1,8 @@ -import { - MedicationRequest, - Coding, - FhirResource, - Identifier, - Task, - Questionnaire, - Patient -} from 'fhir/r4'; +import { MedicationRequest, Coding, FhirResource, Task, Patient } from 'fhir/r4'; import Card, { Link, Suggestion, Action } from '../cards/Card'; -import { - HookPrefetch, - OrderSignPrefetch, - TypedRequestBody -} from '../rems-cds-hooks/resources/HookTypes'; +import { HookPrefetch, TypedRequestBody } from '../rems-cds-hooks/resources/HookTypes'; import config from '../config'; -import { medicationCollection, remsCaseCollection } from '../fhir/models'; +import { Requirement, medicationCollection, remsCaseCollection } from '../fhir/models'; import axios from 'axios'; import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService'; @@ -26,19 +14,11 @@ type HandleCallback = ( patient: FhirResource | undefined ) => Promise; -interface Requirement { - name: string; - description: string; - stakeholderType: string; - createNewCase: boolean; - resourceId: string; - requiredToDispense: boolean; - appContext?: string; -} export interface CardRule { links: Link[]; summary?: string; stakeholderType?: string; + cardDetails?: string; } export 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 @@ -70,7 +50,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'patient', - summary: 'Turalio REMS Patient Requirements' + summary: 'Turalio REMS Patient Requirements', + cardDetails: CARD_DETAILS }, { links: [ @@ -97,7 +78,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'prescriber', - summary: 'Turalio REMS Prescriber Requirements' + summary: 'Turalio REMS Prescriber Requirements', + cardDetails: CARD_DETAILS } ], '6064': [ @@ -133,7 +115,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'patient', - summary: 'iPledge/Isotretinoin REMS Patient Requirements' + summary: 'iPledge/Isotretinoin REMS Patient Requirements', + cardDetails: CARD_DETAILS }, { links: [ @@ -153,7 +136,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'prescriber', - summary: 'iPledge/Isotretinoin REMS Provider Requirements' + summary: 'iPledge/Isotretinoin REMS Provider Requirements', + cardDetails: CARD_DETAILS } ], '1237051': [ @@ -182,7 +166,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'patient', - summary: 'TIRF REMS Patient Requirements' + summary: 'TIRF REMS Patient Requirements', + cardDetails: CARD_DETAILS }, { links: [ @@ -202,7 +187,24 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'prescriber', - summary: 'TIRF REMS Prescriber Requirements' + summary: 'TIRF REMS Prescriber Requirements', + cardDetails: CARD_DETAILS + } + ], + '1666386': [ + { + links: [ + { + label: 'Medication Guide', + type: 'absolute', + url: new URL( + 'https://www.accessdata.fda.gov/drugsatfda_docs/rems/Addyi_2019_10_09_Medication_Guide.pdf' + ) + } + ], + stakeholderType: '', + summary: 'Addyi REMS Patient Information', + cardDetails: 'Please review safety documentation' } ] }; @@ -219,6 +221,10 @@ export const validCodes: Coding[] = [ { code: '6064', // iPledge system: 'http://www.nlm.nih.gov/research/umls/rxnorm' + }, + { + code: '1666386', // Addyi + system: 'http://www.nlm.nih.gov/research/umls/rxnorm' } ]; const source = { @@ -267,7 +273,7 @@ export function getFhirResource(token: string, req: TypedRequestBody) { } export function createSmartLink( requirementName: string, - appContext: string, + appContext: string | null, request: MedicationRequest | undefined ) { const newLink: Link = { @@ -335,6 +341,8 @@ export async function handleCardOrder( }) .exec(); + // count the total requirement for each type + // find a matching rems case for the patient and this drug to only return needed results const patientName = patient?.resourceType === 'Patient' ? patient?.name?.[0] : undefined; const patientBirth = patient?.resourceType === 'Patient' ? patient?.birthDate : undefined; @@ -354,29 +362,32 @@ export async function handleCardOrder( for (const rule of codeRule) { const card = new Card( rule.summary || medicationCode.display || 'Rems', - CARD_DETAILS, + rule.cardDetails || CARD_DETAILS, source, 'info' ); rule.links.forEach(function (e) { - if (e.type == 'absolute') { + if (e.type === 'absolute') { // no construction needed card.addLink(e); } }); + let smartLinkCountAdded = 0; let smartLinkCount = 0; // process the smart links from the medicationCollection // TODO: smart links should be built with discovered questionnaires, not hard coded ones if (drug) { for (const requirement of drug.requirements) { - if (requirement.stakeholderType == rule.stakeholderType) { + if (requirement.stakeholderType === rule.stakeholderType) { + smartLinkCount++; + // only add the link if the form has not already been processed / received if (etasu) { let found = false; for (const metRequirement of etasu.metRequirements) { - if (metRequirement.requirementName == requirement.name) { + if (metRequirement.requirementName === requirement.name) { found = true; if (!metRequirement.completed) { card.addLink( @@ -385,7 +396,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } - smartLinkCount++; + smartLinkCountAdded++; } } } @@ -396,7 +407,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } - smartLinkCount++; + smartLinkCountAdded++; } } else { // add all the required to dispense links if no etasu to check @@ -407,7 +418,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } - smartLinkCount++; + smartLinkCountAdded++; } } } @@ -415,7 +426,8 @@ export async function handleCardOrder( } // only add the card if there are smart links to needed forms - if (smartLinkCount > 0) { + // allow information only cards to be returned as well + if (smartLinkCountAdded > 0 || smartLinkCount === 0) { cardArray.push(card); } } diff --git a/src/hooks/rems.patientview.ts b/src/hooks/rems.patientview.ts index da123210..da33871e 100644 --- a/src/hooks/rems.patientview.ts +++ b/src/hooks/rems.patientview.ts @@ -2,15 +2,12 @@ import Card from '../cards/Card'; import { PatientViewHook, SupportedHooks, - PatientViewPrefetch, HookPrefetch } from '../rems-cds-hooks/resources/HookTypes'; import { medicationCollection, remsCaseCollection } from '../fhir/models'; import { ServicePrefetch, CdsService } from '../rems-cds-hooks/resources/CdsService'; import { Bundle, FhirResource, MedicationRequest } from 'fhir/r4'; -import { Link } from '../cards/Card'; -import config from '../config'; -import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator'; + import { codeMap, CARD_DETAILS, @@ -18,7 +15,6 @@ import { createSmartLink, handleHook } from './hookResources'; -import axios from 'axios'; interface TypedRequestBody extends Express.Request { body: PatientViewHook; @@ -50,6 +46,7 @@ function buildErrorCard(reason: string) { } const handler = (req: TypedRequestBody, res: any) => { + console.log('REMS patient-view hook'); // process the MedicationRequests to add the Medication into contained resources function processMedicationRequests(medicationRequestsBundle: Bundle) { medicationRequestsBundle?.entry?.forEach(entry => { @@ -162,7 +159,7 @@ const handler = (req: TypedRequestBody, res: any) => { } // loop through all of the ETASU requirements for this drug - const requirements = drug?.requirements; + const requirements = drug?.requirements || []; for (const requirement of requirements) { // find all of the matching patient forms if (requirement?.stakeholderType === 'patient') { diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index cd6d043c..149b27f1 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -4,10 +4,23 @@ import { medicationCollection, metRequirementsCollection, remsCaseCollection, - Medication + Medication, + RemsCase, + Requirement, + MetRequirements } from '../fhir/models'; import { getDrugCodeFromMedicationRequest } from '../hooks/hookResources'; import { uid } from 'uid'; +import { + Bundle, + Coding, + MedicationRequest, + MessageHeader, + Parameters, + Patient, + QuestionnaireResponse +} from 'fhir/r4'; +import { FilterQuery } from 'mongoose'; const router = Router(); // const medicationCollection = db.collection('medication-requirements'); @@ -24,6 +37,55 @@ router.get('/met/:caseId', async (req: Request, res: Response) => { res.send(await remsCaseCollection.findOne({ case_number: req.params.caseId })); }); +const getCaseInfo = async ( + remsCaseSearchDict: FilterQuery, + medicationSearchDict: FilterQuery +): Promise | null> => { + const foundRequirements = await remsCaseCollection.findOne(remsCaseSearchDict); + + // if there are no requirements, then return 'Approved' + if (!foundRequirements) { + // look for the medication by name in the medications list + const drug = await medicationCollection.findOne(medicationSearchDict).exec(); + + // iterate through each requirement of the drug + if (drug?.requirements.length === 0) { + // create simple rems request to return + const remsRequest: Pick< + RemsCase, + | 'status' + | 'drugName' + | 'drugCode' + | 'patientFirstName' + | 'patientLastName' + | 'patientDOB' + | 'metRequirements' + > = { + status: 'Approved', + drugName: drug?.name, + drugCode: drug?.code, + patientFirstName: remsCaseSearchDict.patientFirstName || '', + patientLastName: remsCaseSearchDict.patientLastName || '', + patientDOB: remsCaseSearchDict.patientDOB || '', + metRequirements: [] + }; + return remsRequest; + } + } + + // not a supported medication or requirements / record not created yet will return null + return foundRequirements; +}; + router.get( '/met/patient/:patientFirstName/:patientLastName/:patientDOB/drugCode/:drugCode', async (req: Request, res: Response) => { @@ -37,14 +99,17 @@ router.get( ' - ' + req.params.drugCode ); - const searchDict = { + const remsCaseSearchDict = { patientFirstName: req.params.patientFirstName, patientLastName: req.params.patientLastName, patientDOB: req.params.patientDOB, drugCode: req.params.drugCode }; + const medicationSearchDict = { + code: req.params.drugCode + }; - res.send(await remsCaseCollection.findOne(searchDict)); + res.send(await getCaseInfo(remsCaseSearchDict, medicationSearchDict)); } ); @@ -61,14 +126,17 @@ router.get( ' - ' + req.params.drugName ); - const searchDict = { + const remsCaseSearchDict = { patientFirstName: req.params.patientFirstName, patientLastName: req.params.patientLastName, patientDOB: req.params.patientDOB, drugName: req.params.drugName }; + const medicationSearchDict = { + name: req.params.drugName + }; - res.send(await remsCaseCollection.findOne(searchDict)); + res.send(await getCaseInfo(remsCaseSearchDict, medicationSearchDict)); } ); @@ -82,7 +150,10 @@ router.post('/reset', async (req: Request, res: Response) => { res.send('reset etasu database collections'); }); -const pushMetRequirements = (matchedMetReq: any, remsRequest: any) => { +const pushMetRequirements = ( + matchedMetReq: MetRequirements, + remsRequest: Pick +) => { remsRequest.metRequirements.push({ stakeholderId: matchedMetReq?.stakeholderId, completed: matchedMetReq?.completed, @@ -92,11 +163,14 @@ const pushMetRequirements = (matchedMetReq: any, remsRequest: any) => { }); }; -const createMetRequirements = async (metReq: any) => { +const createMetRequirements = async (metReq: Partial) => { return await metRequirementsCollection.create(metReq); }; -const createAndPushMetRequirements = async (metReq: any, remsRequest: any) => { +const createAndPushMetRequirements = async ( + metReq: Partial, + remsRequest: Pick +) => { try { const matchedMetReq = await createMetRequirements(metReq); pushMetRequirements(matchedMetReq, remsRequest); @@ -109,25 +183,35 @@ const createAndPushMetRequirements = async (metReq: any, remsRequest: any) => { }; const createMetRequirementAndNewCase = async ( - patient: any, + patient: Patient, drug: Medication, - requirement: any, - questionnaireResponse: any, + requirement: Requirement, + questionnaireResponse: QuestionnaireResponse, res: Response, - reqStakeholderReference: any, + reqStakeholderReference: string, practitionerReference: string, pharmacistReference: string, patientReference: string ) => { - const patientFirstName = patient.name[0].given[0]; - const patientLastName = patient.name[0].family; - const patientDOB = patient.birthDate; + const patientFirstName = patient.name?.[0].given?.[0] || ''; + const patientLastName = patient.name?.[0].family || ''; + const patientDOB = patient.birthDate || ''; let message = ''; const case_number = uid(); // create new rems request and add the created metReq to it let remsRequestCompletedStatus = 'Approved'; - const remsRequest: any = { + const remsRequest: Pick< + RemsCase, + | 'case_number' + | 'status' + | 'drugName' + | 'drugCode' + | 'patientFirstName' + | 'patientLastName' + | 'patientDOB' + | 'metRequirements' + > = { case_number: case_number, status: remsRequestCompletedStatus, drugName: drug?.name, @@ -190,7 +274,6 @@ const createMetRequirementAndNewCase = async ( // create the metReq that was submitted const newMetReq = { completed: false, - completedQuestionnaire: null, requirementName: requirement2.name, requirementDescription: requirement2.description, drugName: drug?.name, @@ -218,12 +301,12 @@ const createMetRequirementAndNewCase = async ( const createMetRequirementAndUpdateCase = async ( drug: Medication, - requirement: any, - questionnaireResponse: any, + requirement: Requirement, + questionnaireResponse: QuestionnaireResponse, res: Response, - reqStakeholderReference: any + reqStakeholderReference: string ) => { - let returnedMetReqDoc: any; + let returnedMetReqDoc; const matchedMetReq = await metRequirementsCollection .findOne({ @@ -248,7 +331,7 @@ const createMetRequirementAndUpdateCase = async ( }) .exec(); - for (const case_number of returnedMetReqDoc.case_numbers) { + for (const case_number of returnedMetReqDoc?.case_numbers || []) { // get the rems case to update, search by the case_number const remsRequestToUpdate = await remsCaseCollection .findOne({ @@ -257,12 +340,12 @@ const createMetRequirementAndUpdateCase = async ( .exec(); let foundUncompleted = false; - const metReqArray = remsRequestToUpdate?.metRequirements; + const metReqArray = remsRequestToUpdate?.metRequirements || []; // Check to see if there are any uncompleted requirements, if all have been completed then set status to approved - for (let i = 0; i < remsRequestToUpdate?.metRequirements.length; i++) { + for (let i = 0; i < metReqArray.length; i++) { const req4 = remsRequestToUpdate?.metRequirements[i]; // _id comparison would not work for some reason - if (req4.requirementName === matchedMetReq.requirementName) { + if (req4?.requirementName === matchedMetReq.requirementName) { metReqArray[i].completed = true; req4.completed = true; await remsCaseCollection.updateOne( @@ -270,7 +353,7 @@ const createMetRequirementAndUpdateCase = async ( { $set: { metRequirements: metReqArray } } ); } - if (!req4.completed) { + if (!req4?.completed) { foundUncompleted = true; } } @@ -282,13 +365,13 @@ const createMetRequirementAndUpdateCase = async ( } } } else { - // submitting the requirment but there is no case, create new met requirment + // submitting the requirement but there is no case, create new met requirement // create the metReq that was submitted const newMetReq = { completed: true, completedQuestionnaire: questionnaireResponse, requirementName: requirement.name, - requirementDescription: requirement.requirementDescription, + requirementDescription: requirement.description, drugName: drug?.name, stakeholderId: reqStakeholderReference, case_numbers: [] @@ -303,22 +386,21 @@ const createMetRequirementAndUpdateCase = async ( }; const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( - patient: any, + patient: Patient, drug: Medication, - requirement: any, - questionnaireResponse: any, + requirement: Requirement, + questionnaireResponse: QuestionnaireResponse, res: Response, - reqStakeholderReference: any + reqStakeholderReference: string ) => { // Find the specific case associated with an individual patient for the patient status form // Is it possible for there to be multiple cases for this patient and the same drug? - let returnedRemsRequestDoc: any; let returnRemsRequest = false; let message = ''; - const patientFirstName = patient.name[0].given[0]; - const patientLastName = patient.name[0].family; - const patientDOB = patient.birthDate; + const patientFirstName = patient.name?.[0].given?.[0] || ''; + const patientLastName = patient.name?.[0].family || ''; + const patientDOB = patient.birthDate || ''; const remsRequestToUpdate = await remsCaseCollection .findOne({ @@ -328,6 +410,7 @@ const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( drugCode: drug?.code }) .exec(); + // If you found a case for the patient status form to update if (remsRequestToUpdate) { if (remsRequestToUpdate.status === 'Approved') { @@ -349,7 +432,6 @@ const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( try { await remsRequestToUpdate.save(); returnRemsRequest = true; - returnedRemsRequestDoc = remsRequestToUpdate; } catch (e) { console.log(e); message = 'ERROR: failed to update rems case with requirement not needed to dispense'; @@ -370,7 +452,7 @@ const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( res.status(201); if (returnRemsRequest) { - res.send(returnedRemsRequestDoc); + res.send(remsRequestToUpdate); } else { res.send(message); } @@ -379,37 +461,40 @@ const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( router.post('/met', async (req: Request, res: Response) => { try { - const requestBody = req.body; + const requestBody = req.body as Bundle; // extract params and questionnaire response identifier - const params = getResource(requestBody, requestBody.entry[0].resource.focus?.[0]?.reference); - const questionnaireResponse = getQuestionnaireResponse(requestBody); - const questionnaireStringArray = questionnaireResponse.questionnaire.split('/'); - const requirementId = questionnaireStringArray[questionnaireStringArray.length - 1]; + const params = getResource( + requestBody, + (requestBody.entry?.[0]?.resource as MessageHeader)?.focus?.[0]?.reference || '' + ) as Parameters; + const questionnaireResponse = getQuestionnaireResponse(requestBody) as QuestionnaireResponse; + const questionnaireStringArray = questionnaireResponse?.questionnaire?.split('/'); + const requirementId = questionnaireStringArray?.[questionnaireStringArray.length - 1]; // stakeholder and medication references let prescriptionReference = ''; let practitionerReference = ''; let pharmacistReference = ''; let patientReference = ''; - for (const param of params.parameter) { + for (const param of params.parameter || []) { if (param.name === 'prescription') { - prescriptionReference = param.valueReference.reference; + prescriptionReference = param.valueReference?.reference || ''; } else if (param.name === 'prescriber') { - practitionerReference = param.valueReference.reference; + practitionerReference = param.valueReference?.reference || ''; } else if (param.name === 'pharmacy') { - pharmacistReference = param.valueReference.reference; + pharmacistReference = param.valueReference?.reference || ''; } else if (param.name === 'source-patient') { - patientReference = param.valueReference.reference; + patientReference = param.valueReference?.reference || ''; } } // obtain drug information from database - const prescription = getResource(requestBody, prescriptionReference); - const medicationCode = getDrugCodeFromMedicationRequest(prescription); + const prescription = getResource(requestBody, prescriptionReference) as MedicationRequest; + const medicationCode = getDrugCodeFromMedicationRequest(prescription) as Coding; const prescriptionSystem = medicationCode?.system; const prescriptionCode = medicationCode?.code; - const patient = getResource(requestBody, patientReference); + const patient = getResource(requestBody, patientReference) as Patient; const drug = await medicationCollection .findOne({ @@ -421,11 +506,11 @@ router.post('/met', async (req: Request, res: Response) => { if (drug) { for (const requirement of drug.requirements) { // figure out which stakeholder the req corresponds to - const reqStakeholder = requirement.stakeholderType; - const reqStakeholderReference = - reqStakeholder === 'prescriber' + const stakeholder = requirement.stakeholderType; + const stakeholderReference = + stakeholder === 'prescriber' ? practitionerReference - : reqStakeholder === 'pharmacist' + : stakeholder === 'pharmacist' ? pharmacistReference : patientReference; @@ -439,7 +524,7 @@ router.post('/met', async (req: Request, res: Response) => { requirement, questionnaireResponse, res, - reqStakeholderReference, + stakeholderReference, practitionerReference, pharmacistReference, patientReference @@ -447,14 +532,14 @@ router.post('/met', async (req: Request, res: Response) => { return; } else { - // If its not the patient status requirement + // If it's not the patient status requirement if (requirement.requiredToDispense) { await createMetRequirementAndUpdateCase( drug, requirement, questionnaireResponse, res, - reqStakeholderReference + stakeholderReference ); return; } else { @@ -464,7 +549,7 @@ router.post('/met', async (req: Request, res: Response) => { requirement, questionnaireResponse, res, - reqStakeholderReference + stakeholderReference ); return; } @@ -479,28 +564,32 @@ router.post('/met', async (req: Request, res: Response) => { } }); -const getResource = (bundle: { entry: any[] }, resourceReference: string) => { +const getResource = (bundle: Bundle, resourceReference: string) => { const temp = resourceReference.split('/'); const _resourceType = temp[0]; const _id = temp[1]; - for (let i = 0; i < bundle.entry.length; i++) { - if ( - bundle.entry[i].resource.resourceType === _resourceType && - bundle.entry[i].resource.id === _id - ) { - return bundle.entry[i].resource; + if (bundle.entry) { + for (let i = 0; i < bundle.entry.length; i++) { + if ( + bundle.entry[i].resource?.resourceType === _resourceType && + bundle.entry[i].resource?.id === _id + ) { + return bundle.entry[i].resource; + } } } return null; }; -const getQuestionnaireResponse = (bundle: { entry: any[] }) => { +const getQuestionnaireResponse = (bundle: Bundle) => { const _resourceType = 'QuestionnaireResponse'; - for (let i = 0; i < bundle.entry.length; i++) { - if (bundle.entry[i].resource.resourceType === _resourceType) { - return bundle.entry[i].resource; + if (bundle.entry) { + for (let i = 0; i < bundle.entry.length; i++) { + if (bundle.entry[i].resource?.resourceType === _resourceType) { + return bundle.entry[i].resource as QuestionnaireResponse; + } } } return null; From cd75f594c17d6a21c82a0afb0d03aa93c68efbe1 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque <41444457+plarocque4@users.noreply.github.com> Date: Wed, 27 Mar 2024 10:14:48 -0400 Subject: [PATCH 3/7] ETASU using GuidanceResponse (#137) * ETASU using GuidanceResponse * lint errors * lint fixes * rename a variable --- src/config.ts | 13 ++ src/fhir/guidanceResponseUtilities.ts | 139 ++++++++++++++++++ src/hooks/hookResources.ts | 10 +- src/hooks/rems.patientview.ts | 7 - src/lib/etasu.ts | 2 +- src/lib/schemas/models/Annotation.ts | 20 +++ src/lib/schemas/resources/GuidanceResponse.ts | 87 +++++++++++ src/services/guidanceresponse.service.ts | 63 ++++++++ 8 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 src/fhir/guidanceResponseUtilities.ts create mode 100644 src/lib/schemas/models/Annotation.ts create mode 100644 src/lib/schemas/resources/GuidanceResponse.ts create mode 100644 src/services/guidanceresponse.service.ts diff --git a/src/config.ts b/src/config.ts index 868644d5..595d3fc8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -115,6 +115,19 @@ export default { valueset: { service: './src/services/valueset.service.ts', versions: [fhirConstants.VERSIONS['4_0_0']] + }, + guidanceresponse: { + service: './src/services/guidanceresponse.service.ts', + versions: [fhirConstants.VERSIONS['4_0_0']], + operation: [ + { + name: 'rems-etasu', + route: '/$rems-etasu', + method: 'POST', + reference: + 'https://build.fhir.org/ig/HL7/fhir-medication-rems-ig/OperationDefinition-REMS-ETASU.html' + } + ] } } } diff --git a/src/fhir/guidanceResponseUtilities.ts b/src/fhir/guidanceResponseUtilities.ts new file mode 100644 index 00000000..1743ead3 --- /dev/null +++ b/src/fhir/guidanceResponseUtilities.ts @@ -0,0 +1,139 @@ +import { Parameters, Patient, GuidanceResponse } from 'fhir/r4'; +import container from '../lib/winston'; +import { RemsCase } from './models'; + +const MODULE_URI = 'https://build.fhir.org/ig/HL7/fhir-medication-rems-ig/'; + +export class GuidanceResponseUtilities { + static logger = container.get('application'); + + static translateStatus(etasuStatus: string | undefined) { + // translate the status + let remsStatus: + | 'success' + | 'data-requested' + | 'data-required' + | 'in-progress' + | 'failure' + | 'entered-in-error' = 'failure'; + if (etasuStatus === 'Pending') { + remsStatus = 'data-required'; + } else if (etasuStatus === 'Approved') { + remsStatus = 'success'; + } + return remsStatus; + } + + static processEtasuRequirements( + etasu: Pick< + RemsCase, + | 'drugName' + | 'status' + | 'drugCode' + | 'patientFirstName' + | 'patientLastName' + | 'patientDOB' + | 'metRequirements' + > | null + ) { + // create the output parameters + let addedRequirementCount = 0; + const outputParameters: Parameters = { + resourceType: 'Parameters', + id: 'etasuOutputParameters' + }; + + // create the Parameters for the individual ETASU + etasu?.metRequirements?.forEach(metRequirement => { + // create a GuidanceResponse to embed with the individual requirement for the ETASU + const etasuGuidanceResponse: GuidanceResponse = { + resourceType: 'GuidanceResponse', + status: metRequirement?.completed ? 'success' : 'data-required', + moduleUri: MODULE_URI, + subject: { + reference: metRequirement?.stakeholderId + }, + note: [ + { + text: metRequirement?.requirementName ? metRequirement?.requirementName : 'unknown' + } + ] + }; + + addedRequirementCount++; + + const parameter = { + //TODO: remove spaces from name? + name: metRequirement?.requirementName + ? metRequirement?.requirementName + : 'requirement' + addedRequirementCount, + resource: etasuGuidanceResponse + }; + + // add the ETASU requirement GuidanceResponse to the outputParameters + if (!outputParameters?.parameter) { + outputParameters.parameter = [parameter]; + } else { + outputParameters.parameter?.push(parameter); + } + }); + + return outputParameters; + } + + static createEtasuGuidanceResponse( + etasu: Pick< + RemsCase, + | 'drugName' + | 'status' + | 'drugCode' + | 'patientFirstName' + | 'patientLastName' + | 'patientDOB' + | 'metRequirements' + > | null, + patient: Patient | undefined + ) { + const remsStatus = this.translateStatus(etasu?.status); + + // create a GuidanceResponse representing the rems etasu status + const guidanceResponse: GuidanceResponse = { + resourceType: 'GuidanceResponse', + status: remsStatus, + moduleUri: MODULE_URI + }; + + // optionally add the patient as the subject if the ID is available + if (patient?.id) { + guidanceResponse.subject = { + reference: 'Patient/' + patient?.id + }; + } + + // process and add the etasu requirements as output parameters + const outputParameters = this.processEtasuRequirements(etasu); + + if (outputParameters?.parameter) { + // set the output parameters + guidanceResponse.outputParameters = { + reference: '#' + outputParameters.id + }; + + // add the contained parameters + guidanceResponse.contained = [outputParameters]; + } + + // create the return Parameters containing the GuidanceResponse for the ETASU + const returnParameters: Parameters = { + resourceType: 'Parameters', + parameter: [ + { + name: 'rems-etasu', + resource: guidanceResponse + } + ] + }; + + return returnParameters; + } +} diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 1c08efa3..8d828a64 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -373,7 +373,7 @@ export async function handleCardOrder( } }); - let smartLinkCountAdded = 0; + let unmetRequirementSmartLinkCount = 0; let smartLinkCount = 0; // process the smart links from the medicationCollection @@ -396,7 +396,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } - smartLinkCountAdded++; + unmetRequirementSmartLinkCount++; } } } @@ -407,7 +407,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } - smartLinkCountAdded++; + unmetRequirementSmartLinkCount++; } } else { // add all the required to dispense links if no etasu to check @@ -418,7 +418,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient, contextRequest); } - smartLinkCountAdded++; + unmetRequirementSmartLinkCount++; } } } @@ -427,7 +427,7 @@ export async function handleCardOrder( // only add the card if there are smart links to needed forms // allow information only cards to be returned as well - if (smartLinkCountAdded > 0 || smartLinkCount === 0) { + if (unmetRequirementSmartLinkCount > 0 || smartLinkCount === 0) { cardArray.push(card); } } diff --git a/src/hooks/rems.patientview.ts b/src/hooks/rems.patientview.ts index da33871e..57f278af 100644 --- a/src/hooks/rems.patientview.ts +++ b/src/hooks/rems.patientview.ts @@ -37,13 +37,6 @@ const source = { label: 'MCODE REMS Administrator Prototype', url: new URL('https://github.com/mcode/rems-admin') }; -function buildErrorCard(reason: string) { - const errorCard = new Card('Bad Request', reason, source, 'warning'); - const cards = { - cards: [errorCard.card] - }; - return cards; -} const handler = (req: TypedRequestBody, res: any) => { console.log('REMS patient-view hook'); diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 149b27f1..b74c5a06 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -37,7 +37,7 @@ router.get('/met/:caseId', async (req: Request, res: Response) => { res.send(await remsCaseCollection.findOne({ case_number: req.params.caseId })); }); -const getCaseInfo = async ( +export const getCaseInfo = async ( remsCaseSearchDict: FilterQuery, medicationSearchDict: FilterQuery ): Promise( + { + authorReference: { + type: Reference, + default: void 0 + }, + time: { + type: Date, + default: void 0 + }, + text: { + type: String, + default: void 0 + } + }, + { _id: false } +); diff --git a/src/lib/schemas/resources/GuidanceResponse.ts b/src/lib/schemas/resources/GuidanceResponse.ts new file mode 100644 index 00000000..1ecbeb8b --- /dev/null +++ b/src/lib/schemas/resources/GuidanceResponse.ts @@ -0,0 +1,87 @@ +import mongoose, { model } from 'mongoose'; +import { GuidanceResponse } from 'fhir/r4'; +import Identifier from '../models/Identifier'; +import Reference from '../models/Reference'; +import CodeableConcept from '../models/CodeableConcept'; +import DomainResource from './DomainResource'; +import Library from './Library'; +import ValueSet from './ValueSet'; +import Annotation from '../models/Annotation'; +import DataRequirement from '../models/DataRequirement'; + +function GuidanceResponseSchema() { + const GuidanceResponseInterface = { + ...DomainResource, + requestIdentifier: { + type: Identifier, + default: void 0 + }, + identifier: { + type: [Identifier], + default: void 0 + }, + moduleUri: { + type: String, + default: void 0 + }, + status: { + type: String, + default: void 0 + }, + subject: { + type: Reference, + default: void 0 + }, + encounter: { + type: Reference, + default: void 0 + }, + occurenceDateTime: { + type: Date, + default: void 0 + }, + performer: { + type: Reference, + default: void 0 + }, + reasonCode: { + type: [CodeableConcept], + default: void 0 + }, + reasonReference: { + type: [Reference], + default: void 0 + }, + note: { + type: [Annotation], + default: void 0 + }, + evaluationMessage: { + type: [Reference], + default: void 0 + }, + outputParameters: { + type: Reference, + default: void 0 + }, + result: { + type: Reference, + default: void 0 + }, + dataRequirement: { + type: [DataRequirement], + default: void 0 + } + }; + return new mongoose.Schema(GuidanceResponseInterface, { versionKey: false }); +} + +const qSchema = GuidanceResponseSchema(); +qSchema.add({ + contained: { + type: [qSchema, Library, ValueSet], + default: void 0 + } +}); +const GuidanceResponseModel = model('GuidanceResponse', qSchema); +export default GuidanceResponseModel; diff --git a/src/services/guidanceresponse.service.ts b/src/services/guidanceresponse.service.ts new file mode 100644 index 00000000..d16f372b --- /dev/null +++ b/src/services/guidanceresponse.service.ts @@ -0,0 +1,63 @@ +import { FhirUtilities } from '../fhir/utilities'; +import { GuidanceResponseUtilities } from '../fhir/guidanceResponseUtilities'; +import GuidanceResponseModel from '../lib/schemas/resources/GuidanceResponse'; +import { Parameters, Medication, Patient } from 'fhir/r4'; +import { getCaseInfo } from '../lib/etasu'; + +module.exports.searchById = async (args: any) => { + const { id } = args; + console.log('GuidanceResponse >>> searchById: -- ' + id); + return await GuidanceResponseModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); +}; + +module.exports.create = async (args: any, req: any) => { + console.log('GuidanceResponse >>> create'); + const resource = req.req.body; + const { base_version } = args; + return await FhirUtilities.store(resource, GuidanceResponseModel, base_version); +}; + +const getMedicationCode = (medication: Medication | undefined) => { + // grab the medication drug code from the Medication resource + let drugCode = null; + medication?.code?.coding?.forEach(medCode => { + if (medCode?.system?.endsWith('rxnorm')) { + drugCode = medCode?.code; + } + }); + return drugCode; +}; + +module.exports.remsEtasu = async (args: any, context: any, logger: any) => { + logger.info('Running GuidanceResponse rems-etasu check /$rems-etasu'); + + const parameters: Parameters = args?.resource; + let patient: Patient | undefined; + let medication: Medication | undefined; + + parameters?.parameter?.forEach(param => { + if (param?.name === 'patient' && param?.resource?.resourceType === 'Patient') { + patient = param.resource; + } else if (param?.name === 'medication' && param?.resource?.resourceType === 'Medication') { + medication = param.resource; + } + }); + + const drugCode = getMedicationCode(medication); + + // grab the patient demographics from the Patient resource in the parameters + const remsCaseSearchDict = { + patientFirstName: patient?.name?.[0]?.given, + patientLastName: patient?.name?.[0]?.family, + patientDOB: patient?.birthDate, + drugCode: drugCode + }; + + const medicationSearchDict = { + code: drugCode + }; + + const etasu = await getCaseInfo(remsCaseSearchDict, medicationSearchDict); + + return GuidanceResponseUtilities.createEtasuGuidanceResponse(etasu, patient); +}; From b4ad31141e672ff2ccd983f9ca8a247be34fb792 Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Mon, 1 Apr 2024 11:08:43 -0400 Subject: [PATCH 4/7] support retrieval of all related etasu --- src/lib/etasu.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index b74c5a06..6f45898d 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -113,6 +113,28 @@ router.get( } ); +router.get( + '/met/patient/:patientFirstName/:patientLastName/:patientDOB', + async (req: Request, res: Response) => { + console.log( + 'get etasu of patient: ' + + req.params.patientFirstName + + ' ' + + req.params.patientLastName + + ' ' + + req.params.patientDOB + ); + const searchDict = { + patientFirstName: req.params.patientFirstName, + patientLastName: req.params.patientLastName, + patientDOB: req.params.patientDOB, + }; + + res.send(await remsCaseCollection.find(searchDict)); + } +); + + router.get( '/met/patient/:patientFirstName/:patientLastName/:patientDOB/drug/:drugName', async (req: Request, res: Response) => { From b169e455b27b7bdb6fe60d7891ac84a06a41566c Mon Sep 17 00:00:00 2001 From: KeeyanGhoreshi Date: Mon, 1 Apr 2024 11:16:33 -0400 Subject: [PATCH 5/7] lint --- src/lib/etasu.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 6f45898d..4923b7f6 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -127,14 +127,13 @@ router.get( const searchDict = { patientFirstName: req.params.patientFirstName, patientLastName: req.params.patientLastName, - patientDOB: req.params.patientDOB, + patientDOB: req.params.patientDOB }; res.send(await remsCaseCollection.find(searchDict)); } ); - router.get( '/met/patient/:patientFirstName/:patientLastName/:patientDOB/drug/:drugName', async (req: Request, res: Response) => { From c4dd7a0c102acaca32d0a9c30c8e1862c72e5f69 Mon Sep 17 00:00:00 2001 From: Keeyan Date: Fri, 5 Apr 2024 14:08:04 -0400 Subject: [PATCH 6/7] update controller to handle medrequest (#139) * update controller to handle medrequest * lint * accomodate ref * linting --- src/services/guidanceresponse.service.ts | 45 +++++++++++++++++++----- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/services/guidanceresponse.service.ts b/src/services/guidanceresponse.service.ts index d16f372b..2ee16646 100644 --- a/src/services/guidanceresponse.service.ts +++ b/src/services/guidanceresponse.service.ts @@ -1,7 +1,7 @@ import { FhirUtilities } from '../fhir/utilities'; import { GuidanceResponseUtilities } from '../fhir/guidanceResponseUtilities'; import GuidanceResponseModel from '../lib/schemas/resources/GuidanceResponse'; -import { Parameters, Medication, Patient } from 'fhir/r4'; +import { Parameters, Medication, Patient, MedicationRequest, FhirResource } from 'fhir/r4'; import { getCaseInfo } from '../lib/etasu'; module.exports.searchById = async (args: any) => { @@ -17,14 +17,37 @@ module.exports.create = async (args: any, req: any) => { return await FhirUtilities.store(resource, GuidanceResponseModel, base_version); }; -const getMedicationCode = (medication: Medication | undefined) => { +const getMedicationCode = ( + medication: Medication | MedicationRequest | undefined +): string | undefined => { // grab the medication drug code from the Medication resource - let drugCode = null; - medication?.code?.coding?.forEach(medCode => { - if (medCode?.system?.endsWith('rxnorm')) { - drugCode = medCode?.code; + let drugCode; + if (medication?.resourceType == 'Medication') { + medication?.code?.coding?.forEach(medCode => { + if (medCode?.system?.endsWith('rxnorm')) { + drugCode = medCode?.code; + } + }); + } else { + if (medication?.medicationCodeableConcept) { + medication?.medicationCodeableConcept?.coding?.forEach(medCode => { + if (medCode.system?.endsWith('rxnorm')) { + drugCode = medCode.code; + } + }); + } else if (medication?.medicationReference) { + const ref = medication.medicationReference.reference; + if (ref?.startsWith('#')) { + const containedRef = ref.slice(1); + const match = medication.contained?.find(res => { + return res.id === containedRef; + }); + if (match?.resourceType === 'Medication') { + return getMedicationCode(match); + } + } } - }); + } return drugCode; }; @@ -33,12 +56,16 @@ module.exports.remsEtasu = async (args: any, context: any, logger: any) => { const parameters: Parameters = args?.resource; let patient: Patient | undefined; - let medication: Medication | undefined; + let medication: Medication | MedicationRequest | undefined; parameters?.parameter?.forEach(param => { if (param?.name === 'patient' && param?.resource?.resourceType === 'Patient') { patient = param.resource; - } else if (param?.name === 'medication' && param?.resource?.resourceType === 'Medication') { + } else if ( + param?.name === 'medication' && + (param?.resource?.resourceType === 'Medication' || + param.resource?.resourceType === 'MedicationRequest') + ) { medication = param.resource; } }); From dc50fbae9fb71bdef26c674241aa2e2122da670a Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Mon, 8 Apr 2024 11:18:12 -0400 Subject: [PATCH 7/7] Fix typos and remove unused imports while updating VS Code Workspace spell check ignore list --- .../resources/Questionnaire-R4-IPledge.json | 2 +- .../Questionnaire-R4-PhysicalExam.json | 20 ++--- .../Questionnaire-R4-Subjective.json | 12 +-- .../ValueSet-R4-condition-clinical.json | 2 +- .../resources/Questionnaire-R4-Turalio.json | 88 +++++++++++-------- src/config.ts | 8 +- src/fhir/questionnaireUtilities.ts | 2 +- src/fhir/utilities.ts | 1 - src/hooks/hookResources.ts | 2 +- src/hooks/rems.patientview.ts | 2 +- src/lib/etasu.ts | 6 +- src/lib/vsac_cache.ts | 83 ++++++++--------- src/rems-cds-hooks | 2 +- src/services/questionnaire.service.ts | 19 ---- test/fixtures/patient.json | 30 ++----- test/server.test.ts | 2 +- test/vsac_cache.test.ts | 30 +++---- tsconfig.json | 2 +- 18 files changed, 145 insertions(+), 168 deletions(-) diff --git a/src/cds-library/CRD-DTR/IPledge/R4/resources/Questionnaire-R4-IPledge.json b/src/cds-library/CRD-DTR/IPledge/R4/resources/Questionnaire-R4-IPledge.json index f869aaea..1f17f6a3 100644 --- a/src/cds-library/CRD-DTR/IPledge/R4/resources/Questionnaire-R4-IPledge.json +++ b/src/cds-library/CRD-DTR/IPledge/R4/resources/Questionnaire-R4-IPledge.json @@ -379,7 +379,7 @@ }, { "linkId": "6.5", - "text": "4. I understand that some patients while taking isotretinoin or soon after stopping isotetrinoin, have become depressed or developed other serious mental problems. Symptoms of depression include sad, anxious or empty mood, irritability, acting on dangerous impulses, anger, loss of pleasure or interest in social or sport activities, sleeping too much or too little, changes in weight or appetite, school or work performance going down, or trouble concentrating. Some patients taking isotretinoin have had thoughts about hurting themselves or putting an end to their own lives (suicidal thoughts). Some people tried to end their own lives, and some people have ended their own lives. There were reports that some of these people did not appear depressed. There have been reports of patients on isotretinoin becoming aggressive or violent. Some people have had other signs of depression while taking isotretinoin (see #7).", + "text": "4. I understand that some patients while taking isotretinoin or soon after stopping isotretinoin, have become depressed or developed other serious mental problems. Symptoms of depression include sad, anxious or empty mood, irritability, acting on dangerous impulses, anger, loss of pleasure or interest in social or sport activities, sleeping too much or too little, changes in weight or appetite, school or work performance going down, or trouble concentrating. Some patients taking isotretinoin have had thoughts about hurting themselves or putting an end to their own lives (suicidal thoughts). Some people tried to end their own lives, and some people have ended their own lives. There were reports that some of these people did not appear depressed. There have been reports of patients on isotretinoin becoming aggressive or violent. Some people have had other signs of depression while taking isotretinoin (see #7).", "type": "boolean", "required": true }, diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PhysicalExam.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PhysicalExam.json index 8a0ae2e1..40369de4 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PhysicalExam.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PhysicalExam.json @@ -1,16 +1,16 @@ { "resourceType": "Questionnaire", "id": "physical-exam", - "name": "Physican Exam Module", + "name": "Physical Exam Module", "url": "http://hl7.org/fhir/Questionnaire/physical-exam", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", "http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-questionnaire-r4" ] - }, + }, "status": "draft", - "item":[ + "item": [ { "linkId": "OBJ.PE", "code": [ @@ -32,7 +32,7 @@ "system": "http://loinc.org" } ], - "type": "string", + "type": "string", "text": "General Appearance" }, { @@ -44,7 +44,7 @@ "system": "http://loinc.org" } ], - "type": "string", + "type": "string", "text": "Head and Face" }, { @@ -104,7 +104,7 @@ "system": "http://loinc.org" } ], - "type": "string", + "type": "string", "text": "Cardiovascular System" }, { @@ -172,12 +172,12 @@ "code": [ { "code": "71401-4", - "display": "Extremeties", + "display": "Extremities", "system": "http://loinc.org" } ], "type": "string", - "text": "Extremeties" + "text": "Extremities" }, { "linkId": "PE.14", @@ -249,5 +249,5 @@ } ] } - ] -} \ No newline at end of file + ] +} diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Subjective.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Subjective.json index 6493ef6c..e0114ef8 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Subjective.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Subjective.json @@ -9,15 +9,15 @@ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", "http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-questionnaire-r4" ] - }, + }, "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/cqf-library", "valueCanonical": "http://hl7.org/fhir/us/davinci-dtr/Library/BasicClinicalInfo-prepopulation" } - ], + ], "status": "draft", - "item":[ + "item": [ { "linkId": "SUB", "text": "Subjective", @@ -78,7 +78,7 @@ "linkId": "MED.1.4", "text": "Frequency", "type": "open-choice", - "answerValueSet":"http://hl7.org/fhir/ValueSet/timing-abbreviation" + "answerValueSet": "http://hl7.org/fhir/ValueSet/timing-abbreviation" }, { "linkId": "MED.1.5", @@ -107,9 +107,9 @@ "text": "Allergy", "type": "open-choice", "repeats": true, - "anwerOption": [] + "answerOption": [] } ] } ] -} \ No newline at end of file +} diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/ValueSet-R4-condition-clinical.json b/src/cds-library/CRD-DTR/Shared/R4/resources/ValueSet-R4-condition-clinical.json index a3566410..5c938610 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/ValueSet-R4-condition-clinical.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/ValueSet-R4-condition-clinical.json @@ -53,7 +53,7 @@ "extension": [ { "url": "http://hl7.org/fhir/StructureDefinition/valueset-definition", - "valueString": "The subject is experiencing a re-occurence or repeating of a previously resolved condition, e.g. urinary tract infection, pancreatitis, cholangitis, conjunctivitis." + "valueString": "The subject is experiencing a re-occurrence or repeating of a previously resolved condition, e.g. urinary tract infection, pancreatitis, cholangitis, conjunctivitis." } ], "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", diff --git a/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-Turalio.json b/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-Turalio.json index 7142bd5c..2b095c5e 100644 --- a/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-Turalio.json +++ b/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-Turalio.json @@ -4,9 +4,7 @@ "name": "TuralioRemsPatientEnrollment", "title": "Turalio Rems Patient Enrollment", "status": "draft", - "subjectType": [ - "Patient" - ], + "subjectType": ["Patient"], "date": "2020-05-20", "publisher": "Da Vinci DTR", "extension": [ @@ -230,7 +228,7 @@ "code": "White", "display": "White" } - } + } ] }, { @@ -466,14 +464,16 @@ } } ], - "item" : [ + "item": [ { "linkId": "3.1.1", "text": "Laboratory Test", "type": "string", - "initial": [{ - "valueString": "AST or SGOT" - }], + "initial": [ + { + "valueString": "AST or SGOT" + } + ], "readOnly": true }, { @@ -524,14 +524,16 @@ } } ], - "item" : [ + "item": [ { "linkId": "3.1.1", "text": "Laboratory Test", "type": "string", - "initial": [{ - "valueString": "ALT or SGPT" - }], + "initial": [ + { + "valueString": "ALT or SGPT" + } + ], "readOnly": true }, { @@ -582,14 +584,16 @@ } } ], - "item" : [ + "item": [ { "linkId": "3.1.1", "text": "Laboratory Test", "type": "string", - "initial": [{ - "valueString": "GGT" - }], + "initial": [ + { + "valueString": "GGT" + } + ], "readOnly": true }, { @@ -640,14 +644,16 @@ } } ], - "item" : [ + "item": [ { "linkId": "3.1.1", "text": "Laboratory Test", "type": "string", - "initial": [{ - "valueString": "Total Bilirubin" - }], + "initial": [ + { + "valueString": "Total Bilirubin" + } + ], "readOnly": true }, { @@ -698,14 +704,16 @@ } } ], - "item" : [ + "item": [ { "linkId": "3.1.1", "text": "Laboratory Test", "type": "string", - "initial": [{ - "valueString": "Direct Bilirubin" - }], + "initial": [ + { + "valueString": "Direct Bilirubin" + } + ], "readOnly": true }, { @@ -756,14 +764,16 @@ } } ], - "item" : [ + "item": [ { "linkId": "3.1.1", "text": "Laboratory Test", "type": "string", - "initial": [{ - "valueString": "Alkaline Phosphatase" - }], + "initial": [ + { + "valueString": "Alkaline Phosphatase" + } + ], "readOnly": true }, { @@ -814,14 +824,16 @@ } } ], - "item" : [ + "item": [ { "linkId": "3.1.1", "text": "Laboratory Test", "type": "string", - "initial": [{ - "valueString": "Albumin" - }], + "initial": [ + { + "valueString": "Albumin" + } + ], "readOnly": true }, { @@ -872,14 +884,16 @@ } } ], - "item" : [ + "item": [ { "linkId": "3.1.1", "text": "Laboratory Test", "type": "string", - "initial": [{ - "valueString": "PT/INR" - }], + "initial": [ + { + "valueString": "PT/INR" + } + ], "readOnly": true }, { @@ -1169,4 +1183,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/src/config.ts b/src/config.ts index 595d3fc8..74c8617a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,12 +3,12 @@ import 'dotenv/config'; import * as env from 'env-var'; // Set up whitelist -const whitelist_env = env.get('WHITELIST').asArray() || false; +const whitelistEnv = env.get('WHITELIST').asArray() || false; -// If no whitelist is present, disable cors -// If it's length is 1, set it to a string, so * works +// If no whitelist is present, disable CORS +// If its length is 1, set it to a string, so * works // If there are multiple, keep them as an array -const whitelist = whitelist_env && whitelist_env.length === 1 ? whitelist_env[0] : whitelist_env; +const whitelist = whitelistEnv && whitelistEnv.length === 1 ? whitelistEnv[0] : whitelistEnv; export default { server: { port: env.get('PORT').asInt(), diff --git a/src/fhir/questionnaireUtilities.ts b/src/fhir/questionnaireUtilities.ts index 64b30207..c64572e2 100644 --- a/src/fhir/questionnaireUtilities.ts +++ b/src/fhir/questionnaireUtilities.ts @@ -98,7 +98,7 @@ export class QuestionnaireUtilities { } } } - // On load of new library, finds ValueSets in codefilters and + // On load of new library, finds ValueSets in code filters and // loads them as well static async processLibraryCodeFilters(library: Library) { const returnValue = this.vsacCache.cacheLibrary(library); diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index f572c505..72d79676 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -121,7 +121,6 @@ export class FhirUtilities { } static async populateDB() { - // prepopulateDB const medications = [ { name: 'Turalio', diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 8d828a64..42f4ee91 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -21,7 +21,7 @@ export interface CardRule { cardDetails?: string; } export 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 +// TODO: this codemap should be replaced with a system similar to original CRD questionnaire package operation // the app doesn't necessarily have to use CQL for this. export const codeMap: { [key: string]: CardRule[] } = { '2183126': [ diff --git a/src/hooks/rems.patientview.ts b/src/hooks/rems.patientview.ts index 57f278af..9be0cd3c 100644 --- a/src/hooks/rems.patientview.ts +++ b/src/hooks/rems.patientview.ts @@ -169,7 +169,7 @@ const handler = (req: TypedRequestBody, res: any) => { } } - // if not in the list of metReuirements, add it as well + // if not in the list of metRequirements, add it as well if (!found) { card.addLink(createSmartLink(requirement.name, requirement.appContext, request)); smartLinkCount++; diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 4923b7f6..f4558116 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -23,10 +23,6 @@ import { import { FilterQuery } from 'mongoose'; const router = Router(); -// const medicationCollection = db.collection('medication-requirements'); -// const metRequirementsCollection = db.collection('met-requirements'); -// const remsCaseCollection = db.collection('rems-case'); - // etasu endpoints router.get('/:drug', async (req: Request, res: Response) => { res.send(await medicationCollection.findOne({ name: req.params.drug })); @@ -262,7 +258,7 @@ const createMetRequirementAndNewCase = async ( return res; } - // iterate through all other reqs again to create corresponding false metReqs / assign to existing + // iterate through all other requirements again to create corresponding false metRequirements / assign to existing for (const requirement2 of drug.requirements) { // skip if the req found is the same as in the outer loop and has already been processed // && If the requirement is not the patient Status Form (when requiredToDispense == false) diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index 087fa4e0..927d20a6 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -32,33 +32,33 @@ class VsacCache { /** * - * @param library The library to cache valuesets for - * @param forceReload flag to force reaching valuesets already cached - * @returns Map of caching results url: {valueSet, error, cached} + * @param library The library to cache ValueSets for + * @param forceReload flag to force reaching ValueSets already cached + * @returns Map of caching results url: {ValueSet, error, cached} */ async cacheLibrary(library: Library, forceReload = false) { - const valueSets = this.collectLibraryValuesets(library); - return await this.cacheValuesets(valueSets, forceReload); + const valueSets = this.collectLibraryValueSets(library); + return await this.cacheValueSets(valueSets, forceReload); } /** * - * @param obj Questionnaire|item object to cache valuesets for - * @param forceReload flag to force reaching valuesets already cached + * @param obj Questionnaire|item object to cache ValueSets for + * @param forceReload flag to force reaching ValueSets already cached * @returns Map of caching results url: {valueSet, error, cached} */ async cacheQuestionnaireItems(obj: any, forceReload = false) { - const valueSets = this.collectQuestionnaireValuesets(obj); - return await this.cacheValuesets(valueSets, forceReload); + const valueSets = this.collectQuestionnaireValueSets(obj); + return await this.cacheValueSets(valueSets, forceReload); } /** * - * @param library The fhir Library to download valuesets from - * @returns a Set that includes all of the valueset urls found in the Library + * @param library The fhir Library to download ValueSets from + * @returns a Set that includes all of the ValueSet urls found in the Library */ - collectLibraryValuesets(library: Library) { + collectLibraryValueSets(library: Library) { // ensure only unique values return new Set(fhirpath.evaluate(library, 'Library.dataRequirement.codeFilter.valueSet')); } @@ -66,35 +66,35 @@ class VsacCache { /** * * @param obj the Questionnaire object or item to collect answerValueSet urls from - * @returns a Set that includes all of the valuesets in the passed object. This returns values for sub items as well + * @returns a Set that includes all of the ValueSets in the passed object. This returns values for sub items as well */ - collectQuestionnaireValuesets(obj: any) { + collectQuestionnaireValueSets(obj: any) { const items = obj.item; - let valuesets = new Set(); + let valueSets = new Set(); items.forEach(async (item: any) => { if (item.answerValueSet) { - valuesets.add(item.answerValueSet); + valueSets.add(item.answerValueSet); } if (item.item) { - valuesets = new Set([...valuesets, ...this.collectQuestionnaireValuesets(item)]); + valueSets = new Set([...valueSets, ...this.collectQuestionnaireValueSets(item)]); } }); // ensure only unique values - return valuesets; + return valueSets; } /** * - * @param valueSets The valusets to cache - * @param forceReload flag to force downloading and caching of the valuesets - * @returns a Map with the return values from caching the valuesets. + * @param valueSets The ValueSets to cache + * @param forceReload flag to force downloading and caching of the ValueSets + * @returns a Map with the return values from caching the ValueSets. */ - async cacheValuesets(valueSets: Set | [], forceReload = false) { + async cacheValueSets(valueSets: Set | [], forceReload = false) { const values = Array.from(valueSets); const results: ValueSet[] = []; await Promise.all( values.map(async vs => { - const vsResource = await this.downloadAndCacheValueset(vs, forceReload); + const vsResource = await this.downloadAndCacheValueSet(vs, forceReload); if (vsResource) { results.push(vsResource); } @@ -107,16 +107,16 @@ class VsacCache { * * @param idOrUrl the Url to download * @param forceReload flag to force recaching already cached values - * @returns Map that contains results url: {cached, valueSet, error} + * @returns Map that contains results url: {cached, ValueSet, error} */ - async downloadAndCacheValueset(idOrUrl: string, forceReload = false) { + async downloadAndCacheValueSet(idOrUrl: string, forceReload = false) { const isVsCached = await this.isCached(idOrUrl); if (forceReload || !isVsCached) { - const vs = await this.downloadValueset(idOrUrl); + const vs = await this.downloadValueSet(idOrUrl); if (vs.error) { console.log('Error Downloading ', idOrUrl, typeof vs.error); } else if (vs.valueSet) { - await this.storeValueSet(this.getValuesetId(idOrUrl), vs.valueSet); + await this.storeValueSet(this.getValueSetId(idOrUrl), vs.valueSet); vs.cached = true; } return vs.valueSet; @@ -128,11 +128,11 @@ class VsacCache { /** * * @param idOrUrl the url to download - * @returns Map that contains results url: {valueset, error} + * @returns Map that contains results url: {ValueSet, error} */ - async downloadValueset(idOrUrl: string) { + async downloadValueSet(idOrUrl: string) { const retValue: ValueSetMapEntry = {}; - const vsUrl = this.gtValuesetURL(idOrUrl); + const vsUrl = this.gtValueSetURL(idOrUrl); const headers: any = { Accept: 'application/json+fhir' }; @@ -140,11 +140,12 @@ class VsacCache { // this will only add headers to vsac urls const isBaseUrlVsac = this.baseUrl.find(str => vsUrl.startsWith(str)); if (isBaseUrlVsac) { - headers['Authorization'] = 'Basic ' + Buffer.from('apikey:' + this.apiKey).toString('base64'); + headers['Authorization'] = + 'Basic ' + Buffer.from('API key:' + this.apiKey).toString('base64'); isVsac = true; } - // this will try to download valuesets that are not in vsac as well based on the - // connonical url passed in. + // this will try to download ValueSets that are not in vsac as well based on the + // canonical url passed in. let url = vsUrl; if (isBaseUrlVsac) { url = vsUrl + '/$expand'; @@ -164,7 +165,7 @@ class VsacCache { retValue.error = error; } } else { - retValue.error = 'Cannot download non vsac valuesets: ' + url; + retValue.error = 'Cannot download non VSAC ValueSets: ' + url; } return retValue; @@ -176,15 +177,15 @@ class VsacCache { * @returns true or false */ async isCached(idOrUrl: string): Promise { - const id = this.getValuesetId(idOrUrl); + const id = this.getValueSetId(idOrUrl); // Query our collection for this observation return await ValueSetModel.findOne({ id: id.toString() }); } /** - * Stores a valueset in the cache. This currently only works for new inserts and will not update + * Stores a ValueSet in the cache. This currently only works for new inserts and will not update * any resources currently cached. This will be updated with a move to Mongo. - * @param vs the valueset to cache + * @param vs the ValueSet to cache */ async storeValueSet(id: string, vs: ValueSet) { if (!vs.id) { @@ -198,7 +199,7 @@ class VsacCache { * @param idOrUrl the url to cache * @returns identifier used to cache the vs */ - getValuesetId(idOrUrl: string) { + getValueSetId(idOrUrl: string) { // is this a url or an id if (idOrUrl.startsWith('http://') || idOrUrl.startsWith('https://')) { const url = new URL(idOrUrl); @@ -213,7 +214,7 @@ class VsacCache { * @param idOrUrl the url to cache * @returns identifier used to cache the vs */ - gtValuesetURL(idOrUrl: string) { + gtValueSetURL(idOrUrl: string) { // is this a url or an id if (idOrUrl.startsWith('http://') || idOrUrl.startsWith('https://')) { return idOrUrl; @@ -223,8 +224,8 @@ class VsacCache { return path; } /** - * Clear all of the cached valuesets - * This currently does not work since merging and updating to use tingo. Drop collection in tingo is broken + * Clear all of the cached ValueSets + * This currently does not work since merging and updating to use TingoDB. Drop collection in TingoDB is broken * */ clearCache() { diff --git a/src/rems-cds-hooks b/src/rems-cds-hooks index 94a78e8c..eb7fd1e1 160000 --- a/src/rems-cds-hooks +++ b/src/rems-cds-hooks @@ -1 +1 @@ -Subproject commit 94a78e8cd27734938ec41858f8d0ca4028da5f21 +Subproject commit eb7fd1e1dc68d9d14b1de6e01a57262accfaa7c1 diff --git a/src/services/questionnaire.service.ts b/src/services/questionnaire.service.ts index d183570f..2bc52529 100644 --- a/src/services/questionnaire.service.ts +++ b/src/services/questionnaire.service.ts @@ -26,22 +26,3 @@ module.exports.questionnairePackage = async (args: any, context: any, logger: an throw result; } }; - -// module.exports.questionnairePackage = (args: any, context: any, logger: any) => { -// logger.info('Running Questionnaire Package /:id/$questionnaire-package'); -// return new Promise((resolve, reject) => { -// const { id } = args; -// const doc = QuestionnaireModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); -// doc.then(async result => { -// if (result) { -// const unprocessedQ: Questionnaire = result.toObject(); -// const parameters = await QuestionnaireUtilities.createPackageFromQuestionnaire( -// unprocessedQ -// ); -// resolve(parameters); -// } else { -// reject(result); -// } -// }); -// }); -// }; diff --git a/test/fixtures/patient.json b/test/fixtures/patient.json index ddd20dde..733af784 100644 --- a/test/fixtures/patient.json +++ b/test/fixtures/patient.json @@ -30,24 +30,16 @@ { "use": "official", "family": "Chalmers", - "given": [ - "Peter", - "James" - ] + "given": ["Peter", "James"] }, { "use": "usual", - "given": [ - "Jim" - ] + "given": ["Jim"] }, { "use": "maiden", "family": "Windsor", - "given": [ - "Peter", - "James" - ], + "given": ["Peter", "James"], "period": { "end": "2002" } @@ -93,10 +85,8 @@ { "use": "home", "type": "both", - "text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999", - "line": [ - "534 Erewhon St" - ], + "text": "534 Erewhon St PleasantVille, Rainbow, Vic 3999", + "line": ["534 Erewhon St"], "city": "PleasantVille", "district": "Rainbow", "state": "Vic", @@ -128,9 +118,7 @@ } ] }, - "given": [ - "Bénédicte" - ] + "given": ["Bénédicte"] }, "telecom": [ { @@ -141,9 +129,7 @@ "address": { "use": "home", "type": "both", - "line": [ - "534 Erewhon St" - ], + "line": ["534 Erewhon St"], "city": "PleasantVille", "district": "Rainbow", "state": "Vic", @@ -161,4 +147,4 @@ "managingOrganization": { "reference": "Organization/1" } -} \ No newline at end of file +} diff --git a/test/server.test.ts b/test/server.test.ts index fa527904..8b57a17b 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -79,7 +79,7 @@ describe('REMSServer class', () => { serverListen.close(); }); - it('should be able to initilize a server', () => { + it('should be able to initialize a server', () => { const newServer = initialize(config); expect(newServer).to.be.instanceOf(REMSServer); expect(newServer).to.have.property('app'); diff --git a/test/vsac_cache.test.ts b/test/vsac_cache.test.ts index 887855e6..9436db86 100644 --- a/test/vsac_cache.test.ts +++ b/test/vsac_cache.test.ts @@ -22,7 +22,7 @@ describe('VsacCache', () => { // the server for CI testing with someones api credentials it('should be able to collect valueset references from Library Resources', async () => { - const valueSets = client.collectLibraryValuesets(library); + const valueSets = client.collectLibraryValueSets(library); expect(valueSets).to.deep.equal( new Set([ 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.85', @@ -32,13 +32,13 @@ describe('VsacCache', () => { }); it('should be able to collect valueset references from Questionnaire Resources', async () => { - const valueSets = client.collectQuestionnaireValuesets(questionnaire); + const valueSets = client.collectQuestionnaireValueSets(questionnaire); expect(valueSets).to.deep.equal( new Set(['http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked']) ); }); - it('should be able to cache valuesets in Library Resources', async function () { + it('should be able to cache ValueSets in Library Resources', async function () { const mockRequest = nock('http://cts.nlm.nih.gov/fhir'); mockRequest @@ -48,7 +48,7 @@ describe('VsacCache', () => { .get('/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand') .reply(200, generateValueset('2.16.840.1.113762.1.4.1219.35')); - const valueSets = client.collectLibraryValuesets(library); + const valueSets = client.collectLibraryValueSets(library); valueSets.forEach(async function (vs) { expect(await client.isCached(vs)).to.be.false; }); @@ -63,13 +63,13 @@ describe('VsacCache', () => { } }); - it('should be able to cache valuesets in Questionnaire Resources', async () => { + it('should be able to cache ValueSet in Questionnaire Resources', async () => { const mockRequest = nock('http://terminology.hl7.org/'); mockRequest .get('/ValueSet/yes-no-unknown-not-asked') .reply(200, generateValueset('yes-no-unknown-not-asked')); - const valueSets = client.collectQuestionnaireValuesets(questionnaire); + const valueSets = client.collectQuestionnaireValueSets(questionnaire); valueSets.forEach(async vs => { expect(await client.isCached(vs)).to.be.false; }); @@ -83,7 +83,7 @@ describe('VsacCache', () => { } }); - it.skip('should be not load valuesets already cached unless forced', async () => { + it.skip('should be not load ValueSets already cached unless forced', async () => { const mockRequest = nock('http://terminology.hl7.org'); const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'; @@ -91,23 +91,23 @@ describe('VsacCache', () => { .get('/ValueSet/yes-no-unknown-not-asked') .reply(200, generateValueset('yes-no-unknown-not-asked')); try { - client.collectQuestionnaireValuesets(questionnaire); + client.collectQuestionnaireValueSets(questionnaire); expect(await client.isCached(vs)).to.be.false; - await client.downloadAndCacheValueset(vs); + await client.downloadAndCacheValueSet(vs); expect(await client.isCached(vs)).to.be.true; mockRequest .get('/ValueSet/yes-no-unknown-not-asked') .reply(200, generateValueset('yes-no-unknown-not-asked')); - let update = await client.downloadAndCacheValueset(vs); + let update = await client.downloadAndCacheValueSet(vs); expect(update).to.be.undefined; mockRequest .get('/ValueSet/yes-no-unknown-not-asked') .reply(200, generateValueset('yes-no-unknown-not-asked')); - update = await client.downloadAndCacheValueset(vs, true); + update = await client.downloadAndCacheValueSet(vs, true); expect(update).to.be.true; } finally { @@ -115,14 +115,14 @@ describe('VsacCache', () => { } }); - it('should be able to handle errors downloading valuesests', async () => { + it('should be able to handle errors downloading ValueSets', async () => { const mockRequest = nock('http://terminology.hl7.org/'); const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'; mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(404, ''); expect(await client.isCached(vs)).to.be.null; let err; try { - err = await client.downloadAndCacheValueset(vs); + err = await client.downloadAndCacheValueSet(vs); expect(err).to.be.undefined; } catch (e) { @@ -133,9 +133,9 @@ describe('VsacCache', () => { } }); - it('Should not attempt to download non-vsac valuesets if configured to do so', async () => { + it('Should not attempt to download non-vsac ValueSets if configured to do so', async () => { client.onlyVsac = true; - const err = await client.downloadAndCacheValueset('http://localhost:9999/vs/1234'); + const err = await client.downloadAndCacheValueSet('http://localhost:9999/vs/1234'); expect(err).to.be.undefined; }); }); diff --git a/tsconfig.json b/tsconfig.json index 64fa6021..ead45709 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,7 +21,7 @@ "jsx": "react-jsx", "paths": { "*": [ - "src/typings/*" + "./src/typings/*" ] } },