From 92ee91393458698b5603d23b6e1fb0534f0b16d2 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Mon, 5 Dec 2022 16:46:58 -0500 Subject: [PATCH 1/8] WIP --- src/__tests__/fixtures/library.json | 59 + src/__tests__/fixtures/questionnaire.json | 3527 +++++++++++++++++++++ src/__tests__/vsac_cache.test.js | 44 + src/lib/vsac_cache.ts | 75 + 4 files changed, 3705 insertions(+) create mode 100644 src/__tests__/fixtures/library.json create mode 100644 src/__tests__/fixtures/questionnaire.json create mode 100644 src/__tests__/vsac_cache.test.js create mode 100644 src/lib/vsac_cache.ts diff --git a/src/__tests__/fixtures/library.json b/src/__tests__/fixtures/library.json new file mode 100644 index 00000000..ca123617 --- /dev/null +++ b/src/__tests__/fixtures/library.json @@ -0,0 +1,59 @@ +{ + "resourceType": "Library", + "id": "HomeBloodGlucoseMonitorFaceToFace-prepopulation", + "url": "http://hl7.org/fhir/us/davinci-dtr/Library/HomeBloodGlucoseMonitorFaceToFace-prepopulation", + "name": "HomeBloodGlucoseMonitorFaceToFace-prepopulation", + "version": "0.0.1", + "title": "Blood Glucose Monitor Face To Face Prepopulation", + "status": "draft", + "type": { + "coding": [ + { + "code": "logic-library" + } + ] + }, + "relatedArtifact": [ + { + "type": "depends-on", + "resource": "Library/FHIRHelpers-4.0.0" + }, + { + "type": "depends-on", + "resource": "Library/CDS_Connect_Commons_for_FHIRv400" + }, + { + "type": "depends-on", + "resource": "Library/DTRHelpers" + } + ], + "dataRequirement": [ + { + "type": "Condition", + "codeFilter": [ + { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.35" + } + ] + }, + { + "type": "MedicationValueSet", + "codeFilter": [ + { + "path": "code", + "valueSet": "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.85" + } + ] + }, + { + "type": "MedicationStatement" + } + ], + "content": [ + { + "contentType": "application/elm+json", + "url": "files/HomeBloodGlucoseMonitor/r4/HomeBloodGlucoseMonitorFaceToFacePrepopulation-0.0.1.cql" + } + ] +} \ No newline at end of file diff --git a/src/__tests__/fixtures/questionnaire.json b/src/__tests__/fixtures/questionnaire.json new file mode 100644 index 00000000..338e199b --- /dev/null +++ b/src/__tests__/fixtures/questionnaire.json @@ -0,0 +1,3527 @@ +{ + "resourceType": "Questionnaire", + "id": "review-of-system", + "meta": { + "profile": [ + "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", + "http://hl7.org/fhir/us/davinci-dtr/StructureDefinition/dtr-questionnaire-r4" + ] + }, + "name": "Review Of System Module", + "status": "draft", + "item": [ + { + "linkId": "ROS", + "code": [ + { + "code": "71406-3", + "display": "Review of Systems", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Review of Systems", + "item": [ + { + "linkId": "ROS.1", + "code": [ + { + "code": "71407-1", + "display": "Constitutional / General", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Constitutional / General", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.1.1", + "code": [ + { + "code": "8943002", + "display": "Weight gain", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Weight gain", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.1.2", + "code": [ + { + "code": "89362005", + "display": "Weight loss", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Weight loss", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.1.3", + "code": [ + { + "code": "44186003", + "display": "Sleeping problems", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Sleeping problems", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.1.4", + "code": [ + { + "code": "84229001", + "display": "Fatigue", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Fatigue", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.1.5", + "code": [ + { + "code": "386725007", + "display": "Fever", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Fever", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.1.6", + "code": [ + { + "code": "43724002", + "display": "Chills", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Chills", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.1.7", + "code": [ + { + "code": "42984000", + "display": "Night sweats", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Night sweats", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.1.8", + "code": [ + { + "code": "52613005", + "display": "Excessive sweating", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Excessive sweating", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.1.9", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.2", + "code": [ + { + "code": "71408-9", + "display": "Eye", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Eye", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.2.1", + "code": [ + { + "code": "24982008", + "display": "Diplopia", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Diplopia", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.2.2", + "code": [ + { + "code": "225582009", + "display": "Glasses", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Glasses", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.2.3", + "code": [ + { + "code": "285049007", + "display": "Contact lenses", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Contact lenses", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "code": [ + { + "code": "75705005", + "display": "Redness", + "system": "http://snomed.info/sct" + } + ], + "linkId": "ROS.2.4", + "type": "choice", + "text": "Redness", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.2.5", + "code": [ + { + "code": "18628002", + "display": "Discharge", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Discharge", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.2.6", + "code": [ + { + "code": "111516008", + "display": "Blurred vision", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Blurred vision", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.2.7", + "code": [ + { + "code": "23986001", + "display": "Glaucoma", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Glaucoma", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.2.8", + "code": [ + { + "code": "193570009", + "display": "Cataracts", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Cataracts", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.2.9", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.3", + "code": [ + { + "code": "71409-7", + "display": "Ear-nose-mouth-throat", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Ear-nose-mouth-throat", + "item": [ + { + "linkId": "ROS.3.1", + "type": "group", + "text": "Nose", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.1.1", + "code": [ + { + "code": "249366005", + "display": "Epistaxis", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Epistaxis", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.1.2", + "code": [ + { + "code": "36971009", + "display": "Frequent sinus infections", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Frequent sinus infections", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.1.3", + "code": [ + { + "code": "64531003", + "display": "Discharge", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Discharge", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.1.4", + "code": [ + { + "code": "736499003", + "display": "Polyps", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Polyps", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "type": "string", + "linkId": "ROS.3.1.5", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.3.2", + "type": "group", + "text": "Ear", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.2.1", + "code": [ + { + "code": "60862001", + "display": "Tinnitus", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Tinnitus", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "type": "choice", + "code": [ + { + "code": "300132001", + "display": "Discharge", + "system": "http://snomed.info/sct" + } + ], + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.2.2", + "text": "Discharge", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "type": "choice", + "code": [ + { + "code": "15188001", + "display": "Hearing loss", + "system": "http://snomed.info/sct" + } + ], + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.2.3", + "text": "Hearing loss", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "type": "string", + "code": [], + "linkId": "ROS.3.2.4", + "text": "Other" + } + ] + }, + { + "type": "group", + "code": [], + "linkId": "ROS.3.3", + "text": "Mouth", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.3.1", + "code": [ + { + "code": "288939007", + "display": "Odynaphagia", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Odynaphagia", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.3.2", + "code": [ + { + "code": "46557008", + "display": "Tooth disorder", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Tooth disorder", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.3.3", + "code": [ + { + "code": "278615005", + "display": "Uses dentures", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Uses dentures", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.3.3.4", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.3.4", + "type": "group", + "text": "Throat", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.3.4.1", + "code": [ + { + "code": "50219008", + "display": "Hoarseness", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Hoarseness", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.3.4.2", + "type": "string", + "text": "Other" + } + ] + } + ] + }, + { + "linkId": "ROS.4", + "code": [ + { + "code": "71410-5", + "display": "Cardiovascular", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Cardiovascular System", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.4.1", + "code": [ + { + "code": "29857009", + "display": "Chest pain", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Chest pain", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.4.2", + "code": [ + { + "code": "80313002", + "display": "Palpitations", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Palpitations", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.4.3", + "code": [ + { + "code": "62744007", + "display": "Orthopnea", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Orthopnea", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.4.4", + "code": [ + { + "code": "88610006", + "display": "Murmur", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Murmur", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.4.5", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.5", + "code": [ + { + "code": "71411-3", + "display": "Respiratory", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Respiratory System", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.5.1", + "code": [ + { + "code": "49727002", + "display": "Cough", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Cough", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.5.2", + "code": [ + { + "code": "66857006", + "display": "Hemoptysis", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Hemoptysis", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.5.3", + "code": [ + { + "code": "267036007", + "display": "Shortness of breath", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Shortness of breath", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.5.4", + "code": [ + { + "code": "248599002", + "display": "Excess sputum production", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Excess sputum production", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.5.5", + "code": [ + { + "code": "56018004", + "display": "Wheezing", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Wheezing", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.5.6", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.6", + "code": [ + { + "code": "71412-1", + "display": "Gastrointestinal System", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Gastrointestinal", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.1", + "code": [ + { + "code": "399122003", + "display": "Swallowing problem", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Swallowing problem", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.2", + "code": [ + { + "code": "21522001", + "display": "Abdominal pain", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Abdominal pain", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.3", + "code": [ + { + "code": "16331000", + "display": "Heartburn", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Heartburn", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.4", + "code": [ + { + "code": "422587007", + "display": "Nausea", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Nausea", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.5", + "code": [ + { + "code": "422400008", + "display": "Vomiting", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Vomiting", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.6", + "code": [ + { + "code": "8765009", + "display": "Hematemesis", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Hematemesis", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.7", + "code": [ + { + "code": "14760008", + "display": "Constipation", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Constipation", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.8", + "code": [ + { + "code": "62315008", + "display": "Diarrhea", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Diarrhea", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.9", + "code": [ + { + "code": "2901004", + "display": "Melena", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Melena", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.10", + "code": [ + { + "code": "405729008", + "display": "Blood in stool", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Blood in stool", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.11", + "code": [ + { + "code": "40845000", + "display": "Ulcer", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Ulcer", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.6.12", + "code": [ + { + "code": "18165001", + "display": "Jaundice", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Jaundice", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.6.13", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.7", + "code": [ + { + "code": "71413-9", + "display": "Genitourinary", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Genitourinary System", + "item": [ + { + "linkId": "ROS.7.1", + "type": "group", + "text": "Urination", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.1.1", + "code": [ + { + "code": "162116003", + "display": "Increased frequency", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Increased frequency", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.1.2", + "code": [ + { + "code": "75088002", + "display": "Urgency", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Urgency", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.1.3", + "code": [ + { + "code": "5972002", + "display": "Hesitancy", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Hesitancy", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.1.4", + "code": [ + { + "code": "48340000", + "display": "Incontinence", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Incontinence", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.1.5", + "code": [ + { + "code": "49650001", + "display": "Dysuria", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Dysuria", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.1.6", + "code": [ + { + "code": "28442001", + "display": "Polyuria", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Polyuria", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.1.7", + "code": [ + { + "code": "139394000", + "display": "Nocturia", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Nocturia", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.1.8", + "code": [ + { + "code": "34436003", + "display": "Hematuria", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Hematuria", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + } + ] + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.7.2", + "code": [ + { + "code": "247355005", + "display": "Flank pain", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Flank pain", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.7.3", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.8", + "code": [ + { + "code": "71414-7", + "display": "Musculoskeletal", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Musculoskeletal System", + "item": [ + { + "linkId": "ROS.8.1", + "type": "group", + "text": "Joint", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.1.1", + "code": [ + { + "code": "279069000", + "display": "Pain", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Pain", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.1.2", + "code": [ + { + "code": "300887003", + "display": "Swelling", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Swelling", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.1.3", + "code": [ + { + "code": "84445001", + "display": "Stiffness", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Stiffness", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.1.4", + "code": [ + { + "code": "72704001", + "display": "Fracture", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Fracture", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.1.5", + "code": [ + { + "code": "70733008", + "display": "Range of motion limitation", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Range of motion limitation", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.1.6", + "code": [ + { + "code": "417893002", + "display": "Deformity", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Deformity", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + } + ] + }, + { + "linkId": "ROS.8.2", + "type": "group", + "text": "Muscle", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.2.1", + "code": [ + { + "code": "68962001", + "display": "Pain", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Pain", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.2.2", + "code": [ + { + "code": "82470000", + "display": "Fasciculation", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Fasciculation", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.2.3", + "code": [ + { + "code": "88092000", + "display": "Atrophy", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Atrophy", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.2.4", + "code": [ + { + "code": "26544005", + "display": "Weakness", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Weakness", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.8.2.5", + "code": [ + { + "code": "55300003", + "display": "Cramps", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Cramps", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + } + ] + }, + { + "linkId": "ROS.8.3", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.9", + "code": [ + { + "code": "71415-4", + "display": "Skin", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Skin", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.9.1", + "code": [ + { + "code": "399912005", + "display": "Pressure ulcer", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Pressure ulcer", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.9.2", + "code": [ + { + "code": "271807003", + "display": "Rash", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Rash", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.9.7", + "code": [ + { + "code": "43116000", + "display": "Eczema", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Eczema", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.9.8", + "code": [ + { + "code": "418363000", + "display": "Pruritus", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Pruritus", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.9.3", + "code": [ + { + "code": "247493001", + "display": "Splitting nail", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Splitting nail", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.9.4", + "code": [ + { + "code": "89704006", + "display": "Pitting nail", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Pitting nail", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.9.5", + "code": [ + { + "code": "278040002", + "display": "Hair loss", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Hair loss", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.9.6", + "code": [ + { + "code": "271607001", + "display": "Excessive hair growth", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Excessive hair growth", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.9.9", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.10", + "code": [ + { + "code": "71416-2", + "display": "Neurologic System", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Neurological", + "item": [ + { + "type": "choice", + "code": [ + { + "code": "91175000", + "display": "Seizure", + "system": "http://snomed.info/sct" + } + ], + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.1", + "text": "Seizure", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.2", + "code": [ + { + "code": "44077006", + "display": "Numbness", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Numbness", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.3", + "code": [ + { + "code": "62507009", + "display": "Tingling", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Tingling", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.4", + "code": [ + { + "code": "55406008", + "display": "Increased pain to touch", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Increased pain to touch", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.5", + "code": [ + { + "code": "279079003", + "display": "Dysesthesia", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Dysesthesia", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.6", + "code": [ + { + "code": "25064002", + "display": "Headache", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Headache", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.7", + "code": [ + { + "code": "41786007", + "display": "Weakness", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Weakness", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.8", + "code": [ + { + "code": "404640003", + "display": "Dizziness", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Dizziness", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.9", + "code": [ + { + "code": "271594007", + "display": "Fainting", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Fainting", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.10", + "code": [ + { + "code": "44695005", + "display": "Paralysis", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Paralysis", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.11", + "code": [ + { + "code": "26079004", + "display": "Tremors", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Tremors", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.12", + "code": [ + { + "code": "267078001", + "display": "Involuntary movements", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Involuntary movements", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.13", + "code": [ + { + "code": "22631008", + "display": "Unstable gait", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Unstable gait", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.14", + "code": [ + { + "code": "161898004", + "display": "Fall", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Fall", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.15", + "code": [ + { + "code": "386807006", + "display": "Impaired memory", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Impaired memory", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "drop-down", + "display": "Drop down" + } + ], + "text": "Drop down" + } + } + ], + "linkId": "ROS.10.17", + "code": [ + { + "code": "26329005", + "display": "Poor concentration", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Poor concentration", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.10.16", + "code": [ + { + "code": "229683000", + "display": "Speech disorders", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Speech disorders", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.10.18", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.11", + "code": [ + { + "code": "71417-0", + "display": "Psychiatric System", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Psychiatric", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.11.1", + "code": [ + { + "code": "7011001", + "display": "Hallucinations", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Hallucinations", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.11.2", + "code": [ + { + "code": "2073000", + "display": "Delusions", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Delusions", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.11.3", + "code": [ + { + "code": "366979004", + "display": "Depressed mood", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Depressed mood", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.11.4", + "code": [ + { + "code": "48694002", + "display": "Anxiety", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Anxiety", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.11.5", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.12", + "code": [ + { + "code": "71418-8", + "display": "Endocrine", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Endocrine", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.12.1", + "code": [ + { + "code": "69215007", + "display": "Heat intolerance", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Heat intolerance", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.12.2", + "code": [ + { + "code": "80585000", + "display": "Cold intolerance", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Cold intolerance", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.12.3", + "code": [ + { + "code": "3716002", + "display": "Goiter", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Goiter", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.12.4", + "code": [ + { + "code": "80182007", + "display": "Menstrual irregularity", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Menstrual irregularity", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.12.5", + "code": [ + { + "code": "170951000", + "display": "Menopausal symptoms", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Menopausal symptoms", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "type": "group", + "code": [], + "linkId": "ROS.12.6", + "text": "Breast", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.12.6.1", + "code": [ + { + "code": "89164003", + "display": "Mass/tumor", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Mass/tumor", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.12.6.2", + "code": [ + { + "code": "55222007", + "display": "Tenderness", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Tenderness", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.12.6.3", + "code": [ + { + "code": "54302000", + "display": "Nipple discharge", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Nipple discharge", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.12.6.4", + "code": [ + { + "code": "4754008", + "display": "Gynecomastia", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Gynecomastia", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + } + ] + }, + { + "linkId": "ROS.12.7", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.13", + "code": [ + { + "code": "71419-6", + "display": "Hematologic - Lymphatic System", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Hematologic - lymphatic", + "item": [ + { + "linkId": "ROS.13.1", + "type": "group", + "text": "Swollen glands", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.13.1.1", + "code": [ + { + "code": "425061006", + "display": "Neck", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Neck", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.13.1.2", + "code": [ + { + "code": "127189005", + "display": "Axilla", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Axilla", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button", + "display": "Radio Button" + } + ], + "text": "Radio Button" + } + } + ], + "linkId": "ROS.13.1.3", + "code": [ + { + "code": "127199000", + "display": "Groin", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Groin", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + } + ] + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.13.2", + "code": [ + { + "code": "79654002", + "display": "Edema", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Edema", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.13.3", + "code": [ + { + "code": "63491006", + "display": "Claudication", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Claudication", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.13.4", + "code": [ + { + "code": "128060009", + "display": "Varicose veins", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Varicose veins", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.13.5", + "code": [ + { + "code": "64156001", + "display": "Thrombophlebitis", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Thrombophlebitis", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.13.6", + "code": [ + { + "code": "271737000", + "display": "Anemia", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Anemia", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.13.7", + "code": [ + { + "code": "125667009", + "display": "Bruising", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Bruising", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.13.8", + "code": [ + { + "code": "64779008", + "display": "Bleeding disorder", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Bleeding disorder", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.13.9", + "code": [ + { + "code": "95344007", + "display": "Lower extremity ulcer", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Lower extremity ulcer", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.13.10", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.14", + "code": [ + { + "code": "71420-4", + "display": "Allergic/immunologic", + "system": "http://loinc.org" + } + ], + "type": "group", + "text": "Allergic/immunologic", + "item": [ + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.14.1", + "code": [ + { + "code": "247472004", + "display": "Hives", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Hives", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://hl7.org/fhir/questionnaire-item-control", + "code": "radio-button" + } + ] + } + } + ], + "linkId": "ROS.14.2", + "code": [ + { + "code": "39579001", + "display": "Anaphylaxis", + "system": "http://snomed.info/sct" + } + ], + "type": "choice", + "text": "Anaphylaxis", + "answerValueSet": "http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked" + }, + { + "linkId": "ROS.14.3", + "type": "string", + "text": "Other" + } + ] + }, + { + "linkId": "ROS.15", + "type": "display", + "text": "A review of systems is an inventory of body systems obtained through a series of questions seeking to identify signs and/or symptoms which the patient may be experiencing or has experienced", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/questionnaire-itemControl", + "valueCodeableConcept": { + "text": "Help-Button", + "coding": [ + { + "code": "help", + "display": "Help-Button", + "system": "http://hl7.org/fhir/questionnaire-item-control" + } + ] + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/__tests__/vsac_cache.test.js b/src/__tests__/vsac_cache.test.js new file mode 100644 index 00000000..2d60726c --- /dev/null +++ b/src/__tests__/vsac_cache.test.js @@ -0,0 +1,44 @@ + +import VsacCache from '../lib/vsac_cache'; +import library from './fixtures/library.json'; +import questionnaire from './fixtures/questionnaire.json'; +describe('VsacCache', () => { + + let client = new VsacCache('./tmp',process.env.apiKey); + beforeEach(() => { + jest.resetModules(); + + }); + + // need to mock the server endpoints to we do not require hitting + // the server for CI testing with someones api credentials + + + test('should be able to cache valuesets in Library Resources', async () => { + expect(getCacheCount()).toEqual(0); + await client.cacheLibrary(library); + expect(getCacheCount()).toEqual(0); + }); + + test('should be able to cache valuesets in Questionnaire Resources', async () => { + expect(getCacheCount()).toEqual(0); + await client.cacheLibrary(questionnaire); + expect(getCacheCount()).toEqual(0); + }); + + + test('should be able to force reload of valuesets', async () => { + expect(getCacheCount()).toEqual(0); + await client.cacheLibrary(library, true); + expect(getCacheCount()).toEqual(0); + }); + + test('should be not load valuesets already cached', async () => { + expect(getCacheCount()).toEqual(0); + await client.cacheLibrary(library, true); + expect(getCacheCount()).toEqual(0); + await client.cacheLibrary(library, true); + }); + + +}); \ No newline at end of file diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts new file mode 100644 index 00000000..7142f331 --- /dev/null +++ b/src/lib/vsac_cache.ts @@ -0,0 +1,75 @@ +import axios from 'axios'; +import fhirpath from 'fhirpath'; +import fs from 'fs'; +class VsacCache { + + cacheDir: string; + apiKey: string; + baseUrl: string; + + constructor(cacheDir: string, apiKey: string, baseUrl = 'https://cts.nlm.nih.gov/fhir/') { + this.cacheDir = cacheDir; + this.apiKey = apiKey; + this.baseUrl = baseUrl; + } + + + async cacheLibrary(library: any, forceReload = false) { + const valueSets = fhirpath.evaluate(library, 'Library.dataRequirements.codeFilter.valueset'); + valueSets.forEach(async vs => { + await this.downloadAndCacheValueset(vs, forceReload); + }); + } + + async cacheQuestionnaireItems(obj: any, forceReload = false) { + const items = obj.item; + items.forEach(async (item: any) => { + if (item.answerValueSet) { + this.downloadAndCacheValueset(item.answerValueSet, forceReload); + } + if (item.item) { + this.cacheQuestionnaireItems(item, forceReload); + } + }); + + } + + async downloadAndCacheValueset(connonical: string, forceReload = false) { + console.log('fetching vs ' + connonical); + if (forceReload || !this.isCached(connonical)) { + const vs = this.downloadValueset(connonical); + this.storeValueSet(vs); + return vs; + } + } + + + async downloadValueset(connonical: string) { + console.log('fetching vs ' + connonical); + const headers: any = {}; + // this will only add headers to vsac urls + if (connonical.startsWith(this.baseUrl)) { + headers['AUTHORIZATION'] = Buffer.from(':' + this.apiKey).toString('base64'); + } + // this will try to download valuesets that are not in vsac as well based on the + // connonical url passed in. + return await axios.get(connonical, { + headers: headers + }); + } + + async isCached(url: string) { + const id = ''; + const fileName = `${this.cacheDir}/${id}`; + return fs.existsSync(fileName); + } + + async storeValueSet(vs: any) { + if (vs && vs.id) { + const fileName = `${this.cacheDir}/${vs.id}`; + fs.writeFileSync(fileName, JSON.stringify(vs)); + } + } +} + +export default VsacCache; \ No newline at end of file From 6af787b5a96afbad7e6cf45c8883c5984bb995f2 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Tue, 6 Dec 2022 16:50:07 -0500 Subject: [PATCH 2/8] WIP: adding functionality from cds-hooks branch --- package.json | 5 + src/__tests__/vsac_cache.test.js | 44 ----- src/{ => lib}/__tests__/fixtures/library.json | 0 .../__tests__/fixtures/questionnaire.json | 0 src/lib/__tests__/vsac_cache.test.ts | 86 +++++++++ src/lib/vsac_cache.ts | 165 +++++++++++++++--- 6 files changed, 231 insertions(+), 69 deletions(-) delete mode 100644 src/__tests__/vsac_cache.test.js rename src/{ => lib}/__tests__/fixtures/library.json (100%) rename src/{ => lib}/__tests__/fixtures/questionnaire.json (100%) create mode 100644 src/lib/__tests__/vsac_cache.test.ts diff --git a/package.json b/package.json index dafb56ff..ab35f761 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,9 @@ "url": "git+ssh://git@bitbucket.org/asymmetrik/carejourney-cds.git" }, "jest": { + "moduleNameMapper": { + "axios": "axios/dist/node/axios.cjs" + }, "preset": "ts-jest", "testEnvironment": "node", "transform": { @@ -51,10 +54,12 @@ "dependencies": { "@projecttacoma/node-fhir-server-core": "^2.2.8", "@types/fhir": "^0.0.35", + "axios": "^1.2.1", "body-parser": "^1.19.0", "conventional-changelog-cli": "^2.0.34", "cors": "^2.8.5", "express": "^4.17.1", + "fhirpath": "^3.2.0", "lodash": "^4.17.19", "moment": "^2.24.0", "moment-timezone": "^0.5.40", diff --git a/src/__tests__/vsac_cache.test.js b/src/__tests__/vsac_cache.test.js deleted file mode 100644 index 2d60726c..00000000 --- a/src/__tests__/vsac_cache.test.js +++ /dev/null @@ -1,44 +0,0 @@ - -import VsacCache from '../lib/vsac_cache'; -import library from './fixtures/library.json'; -import questionnaire from './fixtures/questionnaire.json'; -describe('VsacCache', () => { - - let client = new VsacCache('./tmp',process.env.apiKey); - beforeEach(() => { - jest.resetModules(); - - }); - - // need to mock the server endpoints to we do not require hitting - // the server for CI testing with someones api credentials - - - test('should be able to cache valuesets in Library Resources', async () => { - expect(getCacheCount()).toEqual(0); - await client.cacheLibrary(library); - expect(getCacheCount()).toEqual(0); - }); - - test('should be able to cache valuesets in Questionnaire Resources', async () => { - expect(getCacheCount()).toEqual(0); - await client.cacheLibrary(questionnaire); - expect(getCacheCount()).toEqual(0); - }); - - - test('should be able to force reload of valuesets', async () => { - expect(getCacheCount()).toEqual(0); - await client.cacheLibrary(library, true); - expect(getCacheCount()).toEqual(0); - }); - - test('should be not load valuesets already cached', async () => { - expect(getCacheCount()).toEqual(0); - await client.cacheLibrary(library, true); - expect(getCacheCount()).toEqual(0); - await client.cacheLibrary(library, true); - }); - - -}); \ No newline at end of file diff --git a/src/__tests__/fixtures/library.json b/src/lib/__tests__/fixtures/library.json similarity index 100% rename from src/__tests__/fixtures/library.json rename to src/lib/__tests__/fixtures/library.json diff --git a/src/__tests__/fixtures/questionnaire.json b/src/lib/__tests__/fixtures/questionnaire.json similarity index 100% rename from src/__tests__/fixtures/questionnaire.json rename to src/lib/__tests__/fixtures/questionnaire.json diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts new file mode 100644 index 00000000..d224cf1a --- /dev/null +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -0,0 +1,86 @@ + +import VsacCache from '../vsac_cache'; +import library from './fixtures/library.json'; +import questionnaire from './fixtures/questionnaire.json'; +describe('VsacCache', () => { + + let client = new VsacCache('./tmp', process.env["VSAC_API_KEY"] ?? "2c1d55c3-3484-4902-b645-25f3a4974ce6"); + beforeEach(() => { + jest.resetModules(); + }); + + // need to mock the server endpoints to we do not require hitting + // the server for CI testing with someones api credentials + + test('should be able to collect valueset references from Library Resources', async () => { + let valueSets = client.collectLibraryValuesets(library); + expect(valueSets).toEqual(new Set(["http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.85", + "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.35"])); + }); + + test('should be able to collect valueset references from Questionnaire Resources', async () => { + let valueSets = client.collectQuestionnaireValuesets(questionnaire); + expect(valueSets).toEqual(new Set(["http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked"])); + }); + + test('should be able to cache valuesets in Library Resources', async () => { + client.clearCache(); + const valueSets = client.collectLibraryValuesets(library); + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeFalsy(); + }); + + await client.cacheLibrary(library); + + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeTruthy(); + }); + }); + + test('should be able to cache valuesets in Questionnaire Resources', async () => { + client.clearCache(); + const valueSets = client.collectQuestionnaireValuesets(questionnaire); + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeFalsy(); + }); + + await client.cacheQuestionnaireItems(questionnaire); + + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeTruthy(); + }); + }); + + + test('should be not load valuesets already cached unless forced', async () => { + client.clearCache(); + const valueSets = client.collectQuestionnaireValuesets(questionnaire); + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeFalsy(); + }); + + let cached = await client.cacheQuestionnaireItems(questionnaire); + + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeTruthy(); + }); + + const vs = valueSets.values().next().value; + let update = await client.downloadAndCacheValueset(vs); + expect(update.get("cached")).toBeFalsy(); + + update = await client.downloadAndCacheValueset(vs, true); + expect(update.get("cached")).toBeTruthy(); + + }); + + test("Should be able to handle errors downloading valuesets", async () => { + client.clearCache(); + const err = await client.downloadAndCacheValueset("http://localhost:9999/vs/1234"); + expect(err.get("error")).toBeDefined(); + + }) + + + +}); \ No newline at end of file diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index 7142f331..06fff3ad 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -1,73 +1,188 @@ import axios from 'axios'; import fhirpath from 'fhirpath'; import fs from 'fs'; +import { stringify } from 'querystring'; + class VsacCache { cacheDir: string; apiKey: string; baseUrl: string; - constructor(cacheDir: string, apiKey: string, baseUrl = 'https://cts.nlm.nih.gov/fhir/') { + constructor(cacheDir: string, apiKey: string, baseUrl = 'http://cts.nlm.nih.gov/fhir/') { this.cacheDir = cacheDir; this.apiKey = apiKey; this.baseUrl = baseUrl; } + /** + * + * @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: any, forceReload = false) { - const valueSets = fhirpath.evaluate(library, 'Library.dataRequirements.codeFilter.valueset'); - valueSets.forEach(async vs => { - await this.downloadAndCacheValueset(vs, forceReload); - }); + const valueSets = this.collectLibraryValuesets(library); + return await this.cacheValuesets(valueSets); } + /** + * + * @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); + } + + /** + * + * @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: any) { + // ensure only unique values + return new Set(fhirpath.evaluate(library, 'Library.dataRequirement.codeFilter.valueSet')); + } + + /** + * + * @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 + */ + collectQuestionnaireValuesets(obj: any) { const items = obj.item; + let valuesets = new Set() items.forEach(async (item: any) => { if (item.answerValueSet) { - this.downloadAndCacheValueset(item.answerValueSet, forceReload); + valuesets.add(item.answerValueSet) } if (item.item) { - this.cacheQuestionnaireItems(item, forceReload); + valuesets = new Set([...valuesets, ...this.collectQuestionnaireValuesets(item)]); } }); + // ensure only unique values + 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. + */ + async cacheValuesets(valueSets: Set | [], forceReload = false) { + const values = Array.from(valueSets) + const results = new Map(); + return await Promise.all(values.map(async vs => { + return results.set(vs, await this.downloadAndCacheValueset(vs, forceReload)); + })); } + /** + * + * @param connonical the Url to download + * @param forceReload flag to force recaching already cached values + * @returns Map that contains results url: {cached, valueSet, error} + */ async downloadAndCacheValueset(connonical: string, forceReload = false) { - console.log('fetching vs ' + connonical); - if (forceReload || !this.isCached(connonical)) { - const vs = this.downloadValueset(connonical); - this.storeValueSet(vs); + if (forceReload || !(this.isCached(connonical))) { + const vs = await this.downloadValueset(connonical); + if (vs.get("error")) { + console.log("Error Downloading ", connonical) + console.log(vs.get("error").message); + } + else if (vs.get("valueSet")) { + this.storeValueSet(connonical, vs.get("valueSet")); + vs.set("cached", true); + } return vs; } + const ret = new Map(); + ret.set("cached", false); + return ret; } - + /** + * + * @param connonical the url to download + * @returns Map that contains results url: {valueset, error} + */ async downloadValueset(connonical: string) { - console.log('fetching vs ' + connonical); - const headers: any = {}; + const retValue = new Map(); + const headers: any = { + "Accept": "application/json+fhir" + }; // this will only add headers to vsac urls if (connonical.startsWith(this.baseUrl)) { - headers['AUTHORIZATION'] = Buffer.from(':' + this.apiKey).toString('base64'); + headers['Authorization'] = "Basic " + Buffer.from(':' + this.apiKey).toString('base64'); } // this will try to download valuesets that are not in vsac as well based on the // connonical url passed in. - return await axios.get(connonical, { - headers: headers - }); + let url = connonical; + if (connonical.startsWith(this.baseUrl)) { + url = url + "/$expand"; + } + // axios cleanup + await process.nextTick(() => { }); + + try { + console.log("Downloading vs " + url) + const vs = await axios.get(url, { + headers: headers + }); + retValue.set("valueSet", vs.data); + } catch (error: any) { + retValue.set("error", error) + } + return retValue; } - async isCached(url: string) { - const id = ''; - const fileName = `${this.cacheDir}/${id}`; + /** + * + * @param connonical url to test if already cached + * @returns true or false + */ + isCached(connonical: string) { + const fileName = this.getCacheName(connonical) return fs.existsSync(fileName); } - async storeValueSet(vs: any) { - if (vs && vs.id) { - const fileName = `${this.cacheDir}/${vs.id}`; - fs.writeFileSync(fileName, JSON.stringify(vs)); + /** + * + * @param connonical url key to cache + * @param vs the valueset to cache + */ + storeValueSet(connonical: string, vs: any) { + const fileName = this.getCacheName(connonical) + fs.writeFileSync(fileName, JSON.stringify(vs)); + } + + /** + * + * @param connonical the url to cache + * @returns identifier used to cache the vs + */ + getCacheName(connonical: string) { + const url = new URL(connonical); + let parts = url.pathname.split("/") + return `${this.cacheDir}/${parts[parts.length - 1]}`; + } + + /** + * Clear all of the cached valuesets + */ + clearCache() { + try { + let files = fs.readdirSync(this.cacheDir) + files.map(file => fs.unlinkSync(`${this.cacheDir}/${file}`)) + + } catch (err) { + console.log(err); } } } From 832887635d1f845f5ad2eac0f8b1e24bf9e1dabd Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 9 Dec 2022 13:13:14 -0500 Subject: [PATCH 3/8] Restructured caching, added flag to cache utility to allow/disallow downloading of valuesets from other servers (this bypasses adding auth header). Added mocks to tests and updated tests --- .gitignore | 3 +- package.json | 2 + src/lib/__tests__/fixtures/valueSet.json | 1 + src/lib/__tests__/vsac_cache.test.ts | 104 +++++++++++++++++------ src/lib/vsac_cache.ts | 28 +++--- 5 files changed, 102 insertions(+), 36 deletions(-) create mode 100644 src/lib/__tests__/fixtures/valueSet.json diff --git a/.gitignore b/.gitignore index a56b10b3..409a8867 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ COVERAGE/ logs/ node_modules/ dist/ -.idea/ \ No newline at end of file +.idea/ +.DS_Store \ No newline at end of file diff --git a/package.json b/package.json index ab35f761..6280c6d4 100644 --- a/package.json +++ b/package.json @@ -80,11 +80,13 @@ "@types/nodemon": "^1.19.2", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", + "axios-mock-adapter": "^1.21.2", "eslint": "^8.28.0", "eslint-config-prettier": "^6.10.1", "jest": "^27.4.5", "jest-extended": "^1.2.0", "json-diff": "^0.9.0", + "nock": "^13.2.9", "nodemon": "^2.0.20", "prettier": "^2.0.5", "ts-jest": "^27.1.2", diff --git a/src/lib/__tests__/fixtures/valueSet.json b/src/lib/__tests__/fixtures/valueSet.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/src/lib/__tests__/fixtures/valueSet.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index d224cf1a..36c5f399 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -2,10 +2,15 @@ import VsacCache from '../vsac_cache'; import library from './fixtures/library.json'; import questionnaire from './fixtures/questionnaire.json'; +import valueSet from './fixtures/valueSet.json' +import axios from "axios"; +import nock from 'nock' + describe('VsacCache', () => { + let client = new VsacCache('./tmp', "test_key"); - let client = new VsacCache('./tmp', process.env["VSAC_API_KEY"] ?? "2c1d55c3-3484-4902-b645-25f3a4974ce6"); beforeEach(() => { + client.onlyVsac = false; jest.resetModules(); }); @@ -18,66 +23,115 @@ describe('VsacCache', () => { "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.35"])); }); + test('should be able to collect valueset references from Questionnaire Resources', async () => { let valueSets = client.collectQuestionnaireValuesets(questionnaire); expect(valueSets).toEqual(new Set(["http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked"])); }); + test('should be able to cache valuesets in Library Resources', async () => { client.clearCache(); + + const mockRequest = nock('http://cts.nlm.nih.gov/fhir'); + console.log('Bearer ' + Buffer.from(":test_key").toString('base64')); + + mockRequest.get("/ValueSet/2.16.840.1.113762.1.4.1219.85/$expand").reply(200, JSON.stringify(valueSet)); + mockRequest.get("/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand").reply(200, JSON.stringify(valueSet)); + const valueSets = client.collectLibraryValuesets(library); valueSets.forEach(vs => { expect(client.isCached(vs)).toBeFalsy(); }); - await client.cacheLibrary(library); + try { + await client.cacheLibrary(library); + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeTruthy(); + }); + } finally { + mockRequest.done(); + } - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeTruthy(); - }); }); test('should be able to cache valuesets in Questionnaire Resources', async () => { client.clearCache(); + const mockRequest = nock('http://terminology.hl7.org/'); + console.log('Bearer ' + Buffer.from(":test_key").toString('base64')); + mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); + const valueSets = client.collectQuestionnaireValuesets(questionnaire); valueSets.forEach(vs => { expect(client.isCached(vs)).toBeFalsy(); }); - await client.cacheQuestionnaireItems(questionnaire); - - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeTruthy(); - }); + try { + await client.cacheQuestionnaireItems(questionnaire); + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeTruthy(); + }); + } finally { + mockRequest.done(); + } }); test('should be not load valuesets already cached unless forced', async () => { client.clearCache(); - const valueSets = client.collectQuestionnaireValuesets(questionnaire); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeFalsy(); - }); + let mockRequest = nock('http://terminology.hl7.org/'); + mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); + try { + const valueSets = client.collectQuestionnaireValuesets(questionnaire); + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeFalsy(); + }); - let cached = await client.cacheQuestionnaireItems(questionnaire); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeTruthy(); - }); + let cached = await client.cacheQuestionnaireItems(questionnaire); + + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeTruthy(); + }); + + const vs = valueSets.values().next().value; + let update = await client.downloadAndCacheValueset(vs); + expect(update.get("cached")).toBeFalsy(); + + mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); + update = await client.downloadAndCacheValueset(vs, true); + + expect(update.get("cached")).toBeTruthy(); + } finally { + mockRequest.done(); + } - const vs = valueSets.values().next().value; - let update = await client.downloadAndCacheValueset(vs); - expect(update.get("cached")).toBeFalsy(); + }); + + test('should be able to handle errors downloading valuesests', async () => { + client.clearCache(); + const mockRequest = nock('http://terminology.hl7.org/'); + console.log('Bearer ' + Buffer.from(":test_key").toString('base64')); + mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(404, ""); - update = await client.downloadAndCacheValueset(vs, true); - expect(update.get("cached")).toBeTruthy(); + const valueSets = client.collectQuestionnaireValuesets(questionnaire); + valueSets.forEach(vs => { + expect(client.isCached(vs)).toBeFalsy(); + }); + try { + const err = await client.downloadAndCacheValueset("http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked"); + expect(err.get("error")).toBeDefined() + } finally { + mockRequest.done(); + } }); - test("Should be able to handle errors downloading valuesets", async () => { + test("Should not attempt tp download non-vsac valuesets if configured to do so", async () => { client.clearCache(); + client.onlyVsac = true; const err = await client.downloadAndCacheValueset("http://localhost:9999/vs/1234"); - expect(err.get("error")).toBeDefined(); + expect(err.get("error")).toEqual("Cannot download non vsac valuesets: http://localhost:9999/vs/1234") }) diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index 06fff3ad..91fa9b0d 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -8,11 +8,13 @@ class VsacCache { cacheDir: string; apiKey: string; baseUrl: string; + onlyVsac: boolean; - constructor(cacheDir: string, apiKey: string, baseUrl = 'http://cts.nlm.nih.gov/fhir/') { + constructor(cacheDir: string, apiKey: string, baseUrl = 'http://cts.nlm.nih.gov/fhir/', onlyVsac = false) { this.cacheDir = cacheDir; this.apiKey = apiKey; this.baseUrl = baseUrl; + this.onlyVsac = onlyVsac; } @@ -117,9 +119,11 @@ class VsacCache { const headers: any = { "Accept": "application/json+fhir" }; + let isVsac = false; // this will only add headers to vsac urls if (connonical.startsWith(this.baseUrl)) { headers['Authorization'] = "Basic " + Buffer.from(':' + 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. @@ -129,16 +133,20 @@ class VsacCache { } // axios cleanup await process.nextTick(() => { }); - - try { - console.log("Downloading vs " + url) - const vs = await axios.get(url, { - headers: headers - }); - retValue.set("valueSet", vs.data); - } catch (error: any) { - retValue.set("error", error) + if ((this.onlyVsac && isVsac) || !this.onlyVsac) { + try { + console.log("Downloading vs " + url) + const vs = await axios.get(url, { + headers: headers + }); + retValue.set("valueSet", vs.data); + } catch (error: any) { + retValue.set("error", error) + } + }else{ + retValue.set("error", "Cannot download non vsac valuesets: "+ url); } + return retValue; } From d3a9971f56c917b86dc92ef2733742b69cd53bdc Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 9 Dec 2022 13:28:21 -0500 Subject: [PATCH 4/8] removing loging statements and move cache clearing to beforeAll funciton --- src/lib/__tests__/vsac_cache.test.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index 36c5f399..120635ba 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -10,6 +10,7 @@ describe('VsacCache', () => { let client = new VsacCache('./tmp', "test_key"); beforeEach(() => { + client.clearCache(); client.onlyVsac = false; jest.resetModules(); }); @@ -31,10 +32,9 @@ describe('VsacCache', () => { test('should be able to cache valuesets in Library Resources', async () => { - client.clearCache(); + const mockRequest = nock('http://cts.nlm.nih.gov/fhir'); - console.log('Bearer ' + Buffer.from(":test_key").toString('base64')); mockRequest.get("/ValueSet/2.16.840.1.113762.1.4.1219.85/$expand").reply(200, JSON.stringify(valueSet)); mockRequest.get("/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand").reply(200, JSON.stringify(valueSet)); @@ -56,9 +56,8 @@ describe('VsacCache', () => { }); test('should be able to cache valuesets in Questionnaire Resources', async () => { - client.clearCache(); + const mockRequest = nock('http://terminology.hl7.org/'); - console.log('Bearer ' + Buffer.from(":test_key").toString('base64')); mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); const valueSets = client.collectQuestionnaireValuesets(questionnaire); @@ -78,7 +77,7 @@ describe('VsacCache', () => { test('should be not load valuesets already cached unless forced', async () => { - client.clearCache(); + let mockRequest = nock('http://terminology.hl7.org/'); mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); try { @@ -109,9 +108,8 @@ describe('VsacCache', () => { }); test('should be able to handle errors downloading valuesests', async () => { - client.clearCache(); + const mockRequest = nock('http://terminology.hl7.org/'); - console.log('Bearer ' + Buffer.from(":test_key").toString('base64')); mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(404, ""); const valueSets = client.collectQuestionnaireValuesets(questionnaire); @@ -128,7 +126,6 @@ describe('VsacCache', () => { }); test("Should not attempt tp download non-vsac valuesets if configured to do so", async () => { - client.clearCache(); client.onlyVsac = true; const err = await client.downloadAndCacheValueset("http://localhost:9999/vs/1234"); expect(err.get("error")).toEqual("Cannot download non vsac valuesets: http://localhost:9999/vs/1234") From f2aee3d13b99752ab7cf38f2868d5782fa092e56 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Wed, 18 Jan 2023 16:32:56 -0500 Subject: [PATCH 5/8] merging fhir-server code base and updating to use fhir-server db. Some functionality is in a state of limbo until we move to mongo. Mainly the ability to force reloading of currently cached valuesets. --- src/fhir/utilities.ts | 17 ++-- src/lib/TingoDatabase.ts | 2 +- src/lib/__tests__/fixtures/valueSet.json | 2 +- src/lib/__tests__/vsac_cache.test.ts | 52 ++++++---- src/lib/vsac_cache.ts | 116 ++++++++++++++++------- 5 files changed, 126 insertions(+), 63 deletions(-) diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 6c4955f9..43656512 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -7,6 +7,7 @@ import { Globals } from '../globals'; import * as path from 'path'; import * as fs from 'fs'; import * as process from 'process'; +import crypto from "crypto"; const re = /(?:\.([^.]+))?$/; @@ -35,6 +36,7 @@ export class FhirUtilities { return resolveSchema(baseVersion, 'Meta'); }; + static async store(resource: any, resolve: any, reject: any, baseVersion = '4_0_0') { const db = Globals.database; @@ -42,7 +44,7 @@ export class FhirUtilities { let id = ''; if (!resource.id) { // If no resource ID was provided, generate one. - id = self.crypto.randomUUID(); + id = crypto.randomUUID(); } else { id = resource.id; } @@ -76,6 +78,7 @@ export class FhirUtilities { } const Resource = resolveSchema(baseVersion, resource.resourceType); + const fhirResource = new Resource(resource); // Create the resource's metadata @@ -86,7 +89,7 @@ export class FhirUtilities { }); if (collectionString === '') { - return reject(' Unsupported FHIR Resource Type'); + reject(' Unsupported FHIR Resource Type'); } const collection = db.collection(collectionString); @@ -103,7 +106,8 @@ export class FhirUtilities { collection.insert(doc, (err: any) => { if (err) { console.log(' Error with %s.create: ', resource.resourceType, err.message); - return reject(err); + reject(err); + return; } else { console.log(' Successfully added ' + resource.resourceType + ' -- ' + id); } @@ -112,12 +116,13 @@ export class FhirUtilities { const history_collection = db.collection(historyCollectionString); // Insert our patient record to history but don't assign _id - return history_collection.insert(history_doc, (err2: any) => { + history_collection.insert(history_doc, (err2: any) => { if (err2) { console.log(' Error with %sHistory.create: ', resource.resourceType, err2.message); - return reject(err2); + reject(err2); + return; } - return resolve({ id: doc.id, resource_version: doc.meta.versionId }); + resolve({ id: doc.id, resource_version: doc.meta.versionId }); }); }); } diff --git a/src/lib/TingoDatabase.ts b/src/lib/TingoDatabase.ts index 9d0a7e08..3f0cd96a 100644 --- a/src/lib/TingoDatabase.ts +++ b/src/lib/TingoDatabase.ts @@ -19,6 +19,6 @@ export class TingoDatabase extends Database { const tingo = new tingodb(); this.database = new tingo.Db(this.location, {}); this.client = ''; - return resolve(this.database); + resolve(this.database); }); } diff --git a/src/lib/__tests__/fixtures/valueSet.json b/src/lib/__tests__/fixtures/valueSet.json index 9e26dfee..a1007b07 100644 --- a/src/lib/__tests__/fixtures/valueSet.json +++ b/src/lib/__tests__/fixtures/valueSet.json @@ -1 +1 @@ -{} \ No newline at end of file +{"resourceType" : "ValueSet"} \ No newline at end of file diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index 120635ba..d9045342 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -3,14 +3,28 @@ import VsacCache from '../vsac_cache'; import library from './fixtures/library.json'; import questionnaire from './fixtures/questionnaire.json'; import valueSet from './fixtures/valueSet.json' -import axios from "axios"; +import fs from 'fs'; import nock from 'nock' +import { TingoDatabase } from '../TingoDatabase'; +import { Globals } from '../../globals'; describe('VsacCache', () => { - let client = new VsacCache('./tmp', "test_key"); - + let client = new VsacCache('./tmp', '2c1d55c3-3484-4902-b645-25f3a4974ce6'); + let dbClient = new TingoDatabase( { + location: './tingo_db', + options: '' + }); + dbClient.connect(); + Globals.databaseClient = dbClient.client; + Globals.database = dbClient.database; + beforeEach(() => { - client.clearCache(); + Globals.database.close(); + fs.rmSync("./tingo_db", { recursive: true, force: true }); + dbClient.connect(); + Globals.databaseClient = dbClient.client; + Globals.database = dbClient.database; + // client.clearCache(); client.onlyVsac = false; jest.resetModules(); }); @@ -40,14 +54,14 @@ describe('VsacCache', () => { mockRequest.get("/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand").reply(200, JSON.stringify(valueSet)); const valueSets = client.collectLibraryValuesets(library); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeFalsy(); + valueSets.forEach(async vs => { + expect(await client.isCached(vs)).toBeFalsy(); }); try { await client.cacheLibrary(library); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeTruthy(); + valueSets.forEach(async vs => { + expect(await client.isCached(vs)).toBeTruthy(); }); } finally { mockRequest.done(); @@ -61,14 +75,14 @@ describe('VsacCache', () => { mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); const valueSets = client.collectQuestionnaireValuesets(questionnaire); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeFalsy(); + valueSets.forEach(async vs => { + expect(await client.isCached(vs)).toBeFalsy(); }); try { await client.cacheQuestionnaireItems(questionnaire); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeTruthy(); + valueSets.forEach(async vs => { + expect(await client.isCached(vs)).toBeTruthy(); }); } finally { mockRequest.done(); @@ -76,21 +90,21 @@ describe('VsacCache', () => { }); - test('should be not load valuesets already cached unless forced', async () => { + test.skip('should be not load valuesets already cached unless forced', async () => { let mockRequest = nock('http://terminology.hl7.org/'); mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); try { const valueSets = client.collectQuestionnaireValuesets(questionnaire); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeFalsy(); + valueSets.forEach( async vs => { + expect(await client.isCached(vs)).toBeFalsy(); }); let cached = await client.cacheQuestionnaireItems(questionnaire); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeTruthy(); + valueSets.forEach(async vs => { + expect(await client.isCached(vs)).toBeTruthy(); }); const vs = valueSets.values().next().value; @@ -113,8 +127,8 @@ describe('VsacCache', () => { mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(404, ""); const valueSets = client.collectQuestionnaireValuesets(questionnaire); - valueSets.forEach(vs => { - expect(client.isCached(vs)).toBeFalsy(); + valueSets.forEach(async vs => { + expect(await client.isCached(vs)).toBeFalsy(); }); try { diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index 91fa9b0d..cb68fea8 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -2,19 +2,23 @@ import axios from 'axios'; import fhirpath from 'fhirpath'; import fs from 'fs'; import { stringify } from 'querystring'; - +import { FhirUtilities } from '../fhir/utilities'; +import { Globals } from '../globals'; +import constants from '../constants'; class VsacCache { cacheDir: string; apiKey: string; baseUrl: string; onlyVsac: boolean; + base_version: string; - constructor(cacheDir: string, apiKey: string, baseUrl = 'http://cts.nlm.nih.gov/fhir/', onlyVsac = false) { + constructor(cacheDir: string, apiKey: string, baseUrl = 'http://cts.nlm.nih.gov/fhir/', onlyVsac = false, base_version = '4_0_0') { this.cacheDir = cacheDir; this.apiKey = apiKey; this.baseUrl = baseUrl; this.onlyVsac = onlyVsac; + this.base_version = base_version; } @@ -87,19 +91,20 @@ class VsacCache { /** * - * @param connonical the Url to download + * @param idOrUrl the Url to download * @param forceReload flag to force recaching already cached values * @returns Map that contains results url: {cached, valueSet, error} */ - async downloadAndCacheValueset(connonical: string, forceReload = false) { - if (forceReload || !(this.isCached(connonical))) { - const vs = await this.downloadValueset(connonical); + async downloadAndCacheValueset(idOrUrl: string, forceReload = false) { + if (forceReload || !(await this.isCached(idOrUrl))) { + const vs = await this.downloadValueset(idOrUrl); if (vs.get("error")) { - console.log("Error Downloading ", connonical) + console.log("Error Downloading ", idOrUrl) console.log(vs.get("error").message); } else if (vs.get("valueSet")) { - this.storeValueSet(connonical, vs.get("valueSet")); + + await this.storeValueSet(this.getValuesetId(idOrUrl), vs.get("valueSet")); vs.set("cached", true); } return vs; @@ -111,24 +116,25 @@ class VsacCache { /** * - * @param connonical the url to download + * @param idOrUrl the url to download * @returns Map that contains results url: {valueset, error} */ - async downloadValueset(connonical: string) { + async downloadValueset(idOrUrl: string) { const retValue = new Map(); + let vsUrl = this.gtValuesetURL(idOrUrl); const headers: any = { "Accept": "application/json+fhir" }; let isVsac = false; // this will only add headers to vsac urls - if (connonical.startsWith(this.baseUrl)) { + if (vsUrl.startsWith(this.baseUrl)) { headers['Authorization'] = "Basic " + Buffer.from(':' + 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. - let url = connonical; - if (connonical.startsWith(this.baseUrl)) { + let url = vsUrl; + if (vsUrl.startsWith(this.baseUrl)) { url = url + "/$expand"; } // axios cleanup @@ -143,8 +149,8 @@ class VsacCache { } catch (error: any) { retValue.set("error", error) } - }else{ - retValue.set("error", "Cannot download non vsac valuesets: "+ url); + } else { + retValue.set("error", "Cannot download non vsac valuesets: " + url); } return retValue; @@ -152,46 +158,84 @@ class VsacCache { /** * - * @param connonical url to test if already cached + * @param idOrUrl url to test if already cached * @returns true or false */ - isCached(connonical: string) { - const fileName = this.getCacheName(connonical) - return fs.existsSync(fileName); + async isCached(idOrUrl: string) { + let id = this.getValuesetId(idOrUrl); + + // Grab an instance of our DB and collection + const db = Globals.database; + const collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}`); + // Query our collection for this observation + return await new Promise(( resolve, reject) => { + collection.findOne({ id: id}, (err: any, valueSet: any) => { + if (err) { + console.log('Error with ValueSet.searchById: ', err); + reject(err); + } + if (valueSet) { + resolve(valueSet); + } + resolve(null); + }); + }); } /** - * - * @param connonical url key to cache + * 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 */ - storeValueSet(connonical: string, vs: any) { - const fileName = this.getCacheName(connonical) - fs.writeFileSync(fileName, JSON.stringify(vs)); + async storeValueSet( id: string, vs: any) { + if(!vs.id){vs.id = id} + await new Promise((resolve, reject) => FhirUtilities.store(vs, resolve, reject)); } /** * - * @param connonical the url to cache + * @param idOrUrl the url to cache * @returns identifier used to cache the vs */ - getCacheName(connonical: string) { - const url = new URL(connonical); - let parts = url.pathname.split("/") - return `${this.cacheDir}/${parts[parts.length - 1]}`; + getValuesetId(idOrUrl: string) { + // is this a url or an id + if (idOrUrl.startsWith('http://') || idOrUrl.startsWith('https://')) { + const url = new URL(idOrUrl); + let parts = url.pathname.split("/") + return parts[parts.length - 1]; + } + return idOrUrl; } + /** + * + * @param idOrUrl the url to cache + * @returns identifier used to cache the vs + */ + gtValuesetURL(idOrUrl: string) { + // is this a url or an id + if (idOrUrl.startsWith('http://') || idOrUrl.startsWith('https://')) { + return idOrUrl; + } + let path = `${this.baseUrl}/ValueSet/${idOrUrl}`; + path = path.replace("//","/"); + 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 + * */ clearCache() { - try { - let files = fs.readdirSync(this.cacheDir) - files.map(file => fs.unlinkSync(`${this.cacheDir}/${file}`)) - - } catch (err) { - console.log(err); - } + // drop the collection + try{ + const db = Globals.database; + let collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}`); + if(collection){collection.drop(console.log); + let history_collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}_History`); + if(history_collection){history_collection.drop(console.log);} + } + }catch(e){ } } } From fb1db5418025d63f898f7f2a16393d51dfac521e Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 19 Jan 2023 12:13:15 -0500 Subject: [PATCH 6/8] Fixing linting issues --- src/fhir/utilities.ts | 2 +- src/lib/__tests__/vsac_cache.test.ts | 52 +++++++++++++-------------- src/lib/vsac_cache.ts | 54 ++++++++++++++-------------- 3 files changed, 55 insertions(+), 53 deletions(-) diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 43656512..b658f0b7 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -7,7 +7,7 @@ import { Globals } from '../globals'; import * as path from 'path'; import * as fs from 'fs'; import * as process from 'process'; -import crypto from "crypto"; +import crypto from 'crypto'; const re = /(?:\.([^.]+))?$/; diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index d9045342..a8e4f8b8 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -2,15 +2,15 @@ import VsacCache from '../vsac_cache'; import library from './fixtures/library.json'; import questionnaire from './fixtures/questionnaire.json'; -import valueSet from './fixtures/valueSet.json' +import valueSet from './fixtures/valueSet.json'; import fs from 'fs'; -import nock from 'nock' +import nock from 'nock'; import { TingoDatabase } from '../TingoDatabase'; import { Globals } from '../../globals'; describe('VsacCache', () => { - let client = new VsacCache('./tmp', '2c1d55c3-3484-4902-b645-25f3a4974ce6'); - let dbClient = new TingoDatabase( { + const client = new VsacCache('./tmp', '2c1d55c3-3484-4902-b645-25f3a4974ce6'); + const dbClient = new TingoDatabase( { location: './tingo_db', options: '' }); @@ -20,7 +20,7 @@ describe('VsacCache', () => { beforeEach(() => { Globals.database.close(); - fs.rmSync("./tingo_db", { recursive: true, force: true }); + fs.rmSync('./tingo_db', { recursive: true, force: true }); dbClient.connect(); Globals.databaseClient = dbClient.client; Globals.database = dbClient.database; @@ -33,15 +33,15 @@ describe('VsacCache', () => { // the server for CI testing with someones api credentials test('should be able to collect valueset references from Library Resources', async () => { - let valueSets = client.collectLibraryValuesets(library); - expect(valueSets).toEqual(new Set(["http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.85", - "http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.35"])); + const valueSets = client.collectLibraryValuesets(library); + expect(valueSets).toEqual(new Set(['http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.85', + 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.35'])); }); test('should be able to collect valueset references from Questionnaire Resources', async () => { - let valueSets = client.collectQuestionnaireValuesets(questionnaire); - expect(valueSets).toEqual(new Set(["http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked"])); + const valueSets = client.collectQuestionnaireValuesets(questionnaire); + expect(valueSets).toEqual(new Set(['http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'])); }); @@ -50,8 +50,8 @@ describe('VsacCache', () => { const mockRequest = nock('http://cts.nlm.nih.gov/fhir'); - mockRequest.get("/ValueSet/2.16.840.1.113762.1.4.1219.85/$expand").reply(200, JSON.stringify(valueSet)); - mockRequest.get("/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand").reply(200, JSON.stringify(valueSet)); + mockRequest.get('/ValueSet/2.16.840.1.113762.1.4.1219.85/$expand').reply(200, JSON.stringify(valueSet)); + mockRequest.get('/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand').reply(200, JSON.stringify(valueSet)); const valueSets = client.collectLibraryValuesets(library); valueSets.forEach(async vs => { @@ -72,7 +72,7 @@ describe('VsacCache', () => { test('should be able to cache valuesets in Questionnaire Resources', async () => { const mockRequest = nock('http://terminology.hl7.org/'); - mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); + mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); const valueSets = client.collectQuestionnaireValuesets(questionnaire); valueSets.forEach(async vs => { @@ -92,8 +92,8 @@ describe('VsacCache', () => { test.skip('should be not load valuesets already cached unless forced', async () => { - let mockRequest = nock('http://terminology.hl7.org/'); - mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); + const mockRequest = nock('http://terminology.hl7.org/'); + mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); try { const valueSets = client.collectQuestionnaireValuesets(questionnaire); valueSets.forEach( async vs => { @@ -101,7 +101,7 @@ describe('VsacCache', () => { }); - let cached = await client.cacheQuestionnaireItems(questionnaire); + const cached = await client.cacheQuestionnaireItems(questionnaire); valueSets.forEach(async vs => { expect(await client.isCached(vs)).toBeTruthy(); @@ -109,12 +109,12 @@ describe('VsacCache', () => { const vs = valueSets.values().next().value; let update = await client.downloadAndCacheValueset(vs); - expect(update.get("cached")).toBeFalsy(); + expect(update.get('cached')).toBeFalsy(); - mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(200, JSON.stringify(valueSet)); + mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); update = await client.downloadAndCacheValueset(vs, true); - expect(update.get("cached")).toBeTruthy(); + expect(update.get('cached')).toBeTruthy(); } finally { mockRequest.done(); } @@ -124,7 +124,7 @@ describe('VsacCache', () => { test('should be able to handle errors downloading valuesests', async () => { const mockRequest = nock('http://terminology.hl7.org/'); - mockRequest.get("/ValueSet/yes-no-unknown-not-asked").reply(404, ""); + mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(404, ''); const valueSets = client.collectQuestionnaireValuesets(questionnaire); valueSets.forEach(async vs => { @@ -132,19 +132,19 @@ describe('VsacCache', () => { }); try { - const err = await client.downloadAndCacheValueset("http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked"); - expect(err.get("error")).toBeDefined() + const err = await client.downloadAndCacheValueset('http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'); + expect(err.get('error')).toBeDefined(); } finally { mockRequest.done(); } }); - test("Should not attempt tp download non-vsac valuesets if configured to do so", async () => { + test('Should not attempt tp download non-vsac valuesets if configured to do so', async () => { client.onlyVsac = true; - const err = await client.downloadAndCacheValueset("http://localhost:9999/vs/1234"); - expect(err.get("error")).toEqual("Cannot download non vsac valuesets: http://localhost:9999/vs/1234") + const err = await client.downloadAndCacheValueset('http://localhost:9999/vs/1234'); + expect(err.get('error')).toEqual('Cannot download non vsac valuesets: http://localhost:9999/vs/1234'); - }) + }); diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index cb68fea8..ded4ae65 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -62,10 +62,10 @@ class VsacCache { */ 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)]); @@ -82,7 +82,7 @@ class VsacCache { * @returns a Map with the return values from caching the valuesets. */ async cacheValuesets(valueSets: Set | [], forceReload = false) { - const values = Array.from(valueSets) + const values = Array.from(valueSets); const results = new Map(); return await Promise.all(values.map(async vs => { return results.set(vs, await this.downloadAndCacheValueset(vs, forceReload)); @@ -98,19 +98,19 @@ class VsacCache { async downloadAndCacheValueset(idOrUrl: string, forceReload = false) { if (forceReload || !(await this.isCached(idOrUrl))) { const vs = await this.downloadValueset(idOrUrl); - if (vs.get("error")) { - console.log("Error Downloading ", idOrUrl) - console.log(vs.get("error").message); + if (vs.get('error')) { + console.log('Error Downloading ', idOrUrl); + console.log(vs.get('error').message); } - else if (vs.get("valueSet")) { + else if (vs.get('valueSet')) { - await this.storeValueSet(this.getValuesetId(idOrUrl), vs.get("valueSet")); - vs.set("cached", true); + await this.storeValueSet(this.getValuesetId(idOrUrl), vs.get('valueSet')); + vs.set('cached', true); } return vs; } const ret = new Map(); - ret.set("cached", false); + ret.set('cached', false); return ret; } @@ -121,36 +121,36 @@ class VsacCache { */ async downloadValueset(idOrUrl: string) { const retValue = new Map(); - let vsUrl = this.gtValuesetURL(idOrUrl); + const vsUrl = this.gtValuesetURL(idOrUrl); const headers: any = { - "Accept": "application/json+fhir" + 'Accept': 'application/json+fhir' }; let isVsac = false; // this will only add headers to vsac urls if (vsUrl.startsWith(this.baseUrl)) { - headers['Authorization'] = "Basic " + Buffer.from(':' + this.apiKey).toString('base64'); + headers['Authorization'] = 'Basic ' + Buffer.from(':' + 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. let url = vsUrl; if (vsUrl.startsWith(this.baseUrl)) { - url = url + "/$expand"; + url = url + '/$expand'; } // axios cleanup - await process.nextTick(() => { }); + await process.nextTick(() => { const v = 1;}); if ((this.onlyVsac && isVsac) || !this.onlyVsac) { try { - console.log("Downloading vs " + url) + console.log('Downloading vs ' + url); const vs = await axios.get(url, { headers: headers }); - retValue.set("valueSet", vs.data); + retValue.set('valueSet', vs.data); } catch (error: any) { - retValue.set("error", error) + retValue.set('error', error); } } else { - retValue.set("error", "Cannot download non vsac valuesets: " + url); + retValue.set('error', 'Cannot download non vsac valuesets: ' + url); } return retValue; @@ -162,7 +162,7 @@ class VsacCache { * @returns true or false */ async isCached(idOrUrl: string) { - let id = this.getValuesetId(idOrUrl); + const id = this.getValuesetId(idOrUrl); // Grab an instance of our DB and collection const db = Globals.database; @@ -188,7 +188,7 @@ class VsacCache { * @param vs the valueset to cache */ async storeValueSet( id: string, vs: any) { - if(!vs.id){vs.id = id} + if(!vs.id){vs.id = id;} await new Promise((resolve, reject) => FhirUtilities.store(vs, resolve, reject)); } @@ -201,7 +201,7 @@ class VsacCache { // is this a url or an id if (idOrUrl.startsWith('http://') || idOrUrl.startsWith('https://')) { const url = new URL(idOrUrl); - let parts = url.pathname.split("/") + const parts = url.pathname.split('/'); return parts[parts.length - 1]; } return idOrUrl; @@ -218,7 +218,7 @@ class VsacCache { return idOrUrl; } let path = `${this.baseUrl}/ValueSet/${idOrUrl}`; - path = path.replace("//","/"); + path = path.replace('//','/'); return path ; } /** @@ -230,12 +230,14 @@ class VsacCache { // drop the collection try{ const db = Globals.database; - let collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}`); + const collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}`); if(collection){collection.drop(console.log); - let history_collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}_History`); + const history_collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}_History`); if(history_collection){history_collection.drop(console.log);} } - }catch(e){ } + }catch(e){ + console.error(e); + } } } From cd3f67597e24d6452c49d8b7e10af4144cfc4324 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 19 Jan 2023 12:15:43 -0500 Subject: [PATCH 7/8] Making things pretty --- src/fhir/utilities.ts | 17 ++-- src/lib/__tests__/vsac_cache.test.ts | 62 ++++++------ src/lib/vsac_cache.ts | 143 +++++++++++++++------------ 3 files changed, 116 insertions(+), 106 deletions(-) diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index b658f0b7..134647a8 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -36,7 +36,6 @@ export class FhirUtilities { return resolveSchema(baseVersion, 'Meta'); }; - static async store(resource: any, resolve: any, reject: any, baseVersion = '4_0_0') { const db = Globals.database; @@ -78,7 +77,7 @@ export class FhirUtilities { } const Resource = resolveSchema(baseVersion, resource.resourceType); - + const fhirResource = new Resource(resource); // Create the resource's metadata @@ -89,7 +88,7 @@ export class FhirUtilities { }); if (collectionString === '') { - reject(' Unsupported FHIR Resource Type'); + reject(' Unsupported FHIR Resource Type'); } const collection = db.collection(collectionString); @@ -106,8 +105,8 @@ export class FhirUtilities { collection.insert(doc, (err: any) => { if (err) { console.log(' Error with %s.create: ', resource.resourceType, err.message); - reject(err); - return; + reject(err); + return; } else { console.log(' Successfully added ' + resource.resourceType + ' -- ' + id); } @@ -116,13 +115,13 @@ export class FhirUtilities { const history_collection = db.collection(historyCollectionString); // Insert our patient record to history but don't assign _id - history_collection.insert(history_doc, (err2: any) => { + history_collection.insert(history_doc, (err2: any) => { if (err2) { console.log(' Error with %sHistory.create: ', resource.resourceType, err2.message); - reject(err2); - return; + reject(err2); + return; } - resolve({ id: doc.id, resource_version: doc.meta.versionId }); + resolve({ id: doc.id, resource_version: doc.meta.versionId }); }); }); } diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index a8e4f8b8..cca862c6 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -1,4 +1,3 @@ - import VsacCache from '../vsac_cache'; import library from './fixtures/library.json'; import questionnaire from './fixtures/questionnaire.json'; @@ -10,14 +9,14 @@ import { Globals } from '../../globals'; describe('VsacCache', () => { const client = new VsacCache('./tmp', '2c1d55c3-3484-4902-b645-25f3a4974ce6'); - const dbClient = new TingoDatabase( { - location: './tingo_db', - options: '' - }); + const dbClient = new TingoDatabase({ + location: './tingo_db', + options: '' + }); dbClient.connect(); Globals.databaseClient = dbClient.client; Globals.database = dbClient.database; - + beforeEach(() => { Globals.database.close(); fs.rmSync('./tingo_db', { recursive: true, force: true }); @@ -29,29 +28,35 @@ describe('VsacCache', () => { jest.resetModules(); }); - // need to mock the server endpoints to we do not require hitting - // the server for CI testing with someones api credentials + // need to mock the server endpoints to we do not require hitting + // the server for CI testing with someones api credentials test('should be able to collect valueset references from Library Resources', async () => { const valueSets = client.collectLibraryValuesets(library); - expect(valueSets).toEqual(new Set(['http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.85', - 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.35'])); + expect(valueSets).toEqual( + new Set([ + 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.85', + 'http://cts.nlm.nih.gov/fhir/ValueSet/2.16.840.1.113762.1.4.1219.35' + ]) + ); }); - test('should be able to collect valueset references from Questionnaire Resources', async () => { const valueSets = client.collectQuestionnaireValuesets(questionnaire); - expect(valueSets).toEqual(new Set(['http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'])); + expect(valueSets).toEqual( + new Set(['http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked']) + ); }); - test('should be able to cache valuesets in Library Resources', async () => { - - const mockRequest = nock('http://cts.nlm.nih.gov/fhir'); - mockRequest.get('/ValueSet/2.16.840.1.113762.1.4.1219.85/$expand').reply(200, JSON.stringify(valueSet)); - mockRequest.get('/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand').reply(200, JSON.stringify(valueSet)); + mockRequest + .get('/ValueSet/2.16.840.1.113762.1.4.1219.85/$expand') + .reply(200, JSON.stringify(valueSet)); + mockRequest + .get('/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand') + .reply(200, JSON.stringify(valueSet)); const valueSets = client.collectLibraryValuesets(library); valueSets.forEach(async vs => { @@ -66,11 +71,9 @@ describe('VsacCache', () => { } finally { mockRequest.done(); } - }); test('should be able to cache valuesets in Questionnaire Resources', async () => { - const mockRequest = nock('http://terminology.hl7.org/'); mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); @@ -89,18 +92,15 @@ describe('VsacCache', () => { } }); - test.skip('should be not load valuesets already cached unless forced', async () => { - const mockRequest = nock('http://terminology.hl7.org/'); mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); try { const valueSets = client.collectQuestionnaireValuesets(questionnaire); - valueSets.forEach( async vs => { + valueSets.forEach(async vs => { expect(await client.isCached(vs)).toBeFalsy(); }); - const cached = await client.cacheQuestionnaireItems(questionnaire); valueSets.forEach(async vs => { @@ -118,11 +118,9 @@ describe('VsacCache', () => { } finally { mockRequest.done(); } - }); test('should be able to handle errors downloading valuesests', async () => { - const mockRequest = nock('http://terminology.hl7.org/'); mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(404, ''); @@ -132,7 +130,9 @@ describe('VsacCache', () => { }); try { - const err = await client.downloadAndCacheValueset('http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'); + const err = await client.downloadAndCacheValueset( + 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked' + ); expect(err.get('error')).toBeDefined(); } finally { mockRequest.done(); @@ -142,10 +142,8 @@ describe('VsacCache', () => { test('Should not attempt tp download non-vsac valuesets if configured to do so', async () => { client.onlyVsac = true; const err = await client.downloadAndCacheValueset('http://localhost:9999/vs/1234'); - expect(err.get('error')).toEqual('Cannot download non vsac valuesets: http://localhost:9999/vs/1234'); - + expect(err.get('error')).toEqual( + 'Cannot download non vsac valuesets: http://localhost:9999/vs/1234' + ); }); - - - -}); \ No newline at end of file +}); diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index ded4ae65..0c08252f 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -6,14 +6,19 @@ import { FhirUtilities } from '../fhir/utilities'; import { Globals } from '../globals'; import constants from '../constants'; class VsacCache { - cacheDir: string; apiKey: string; baseUrl: string; onlyVsac: boolean; base_version: string; - constructor(cacheDir: string, apiKey: string, baseUrl = 'http://cts.nlm.nih.gov/fhir/', onlyVsac = false, base_version = '4_0_0') { + constructor( + cacheDir: string, + apiKey: string, + baseUrl = 'http://cts.nlm.nih.gov/fhir/', + onlyVsac = false, + base_version = '4_0_0' + ) { this.cacheDir = cacheDir; this.apiKey = apiKey; this.baseUrl = baseUrl; @@ -21,9 +26,8 @@ class VsacCache { this.base_version = base_version; } - /** - * + * * @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} @@ -34,7 +38,7 @@ class VsacCache { } /** - * + * * @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} @@ -46,17 +50,17 @@ class VsacCache { } /** - * + * * @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: any) { - // ensure only unique values + // ensure only unique values return new Set(fhirpath.evaluate(library, 'Library.dataRequirement.codeFilter.valueSet')); } /** - * + * * @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 */ @@ -71,27 +75,29 @@ class VsacCache { valuesets = new Set([...valuesets, ...this.collectQuestionnaireValuesets(item)]); } }); - // ensure only unique values + // ensure only unique values 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 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. */ async cacheValuesets(valueSets: Set | [], forceReload = false) { const values = Array.from(valueSets); const results = new Map(); - return await Promise.all(values.map(async vs => { - return results.set(vs, await this.downloadAndCacheValueset(vs, forceReload)); - })); + return await Promise.all( + values.map(async vs => { + return results.set(vs, await this.downloadAndCacheValueset(vs, forceReload)); + }) + ); } /** - * - * @param idOrUrl the Url to download + * + * @param idOrUrl the Url to download * @param forceReload flag to force recaching already cached values * @returns Map that contains results url: {cached, valueSet, error} */ @@ -101,9 +107,7 @@ class VsacCache { if (vs.get('error')) { console.log('Error Downloading ', idOrUrl); console.log(vs.get('error').message); - } - else if (vs.get('valueSet')) { - + } else if (vs.get('valueSet')) { await this.storeValueSet(this.getValuesetId(idOrUrl), vs.get('valueSet')); vs.set('cached', true); } @@ -115,15 +119,15 @@ class VsacCache { } /** - * - * @param idOrUrl the url to download + * + * @param idOrUrl the url to download * @returns Map that contains results url: {valueset, error} */ async downloadValueset(idOrUrl: string) { const retValue = new Map(); const vsUrl = this.gtValuesetURL(idOrUrl); const headers: any = { - 'Accept': 'application/json+fhir' + Accept: 'application/json+fhir' }; let isVsac = false; // this will only add headers to vsac urls @@ -131,14 +135,16 @@ class VsacCache { headers['Authorization'] = 'Basic ' + Buffer.from(':' + 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 + // connonical url passed in. let url = vsUrl; if (vsUrl.startsWith(this.baseUrl)) { url = url + '/$expand'; } - // axios cleanup - await process.nextTick(() => { const v = 1;}); + // axios cleanup + await process.nextTick(() => { + const v = 1; + }); if ((this.onlyVsac && isVsac) || !this.onlyVsac) { try { console.log('Downloading vs ' + url); @@ -157,43 +163,45 @@ class VsacCache { } /** - * + * * @param idOrUrl url to test if already cached * @returns true or false */ async isCached(idOrUrl: string) { const id = this.getValuesetId(idOrUrl); - // Grab an instance of our DB and collection - const db = Globals.database; - const collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}`); - // Query our collection for this observation - return await new Promise(( resolve, reject) => { - collection.findOne({ id: id}, (err: any, valueSet: any) => { - if (err) { - console.log('Error with ValueSet.searchById: ', err); + // Grab an instance of our DB and collection + const db = Globals.database; + const collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}`); + // Query our collection for this observation + return await new Promise((resolve, reject) => { + collection.findOne({ id: id }, (err: any, valueSet: any) => { + if (err) { + console.log('Error with ValueSet.searchById: ', err); reject(err); - } - if (valueSet) { - resolve(valueSet); - } - resolve(null); - }); + } + if (valueSet) { + resolve(valueSet); + } + resolve(null); + }); }); } /** - * 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. + * 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 */ - async storeValueSet( id: string, vs: any) { - if(!vs.id){vs.id = id;} + async storeValueSet(id: string, vs: any) { + if (!vs.id) { + vs.id = id; + } await new Promise((resolve, reject) => FhirUtilities.store(vs, resolve, reject)); } /** - * + * * @param idOrUrl the url to cache * @returns identifier used to cache the vs */ @@ -208,37 +216,42 @@ class VsacCache { } /** - * - * @param idOrUrl the url to cache - * @returns identifier used to cache the vs - */ + * + * @param idOrUrl the url to cache + * @returns identifier used to cache the vs + */ gtValuesetURL(idOrUrl: string) { // is this a url or an id if (idOrUrl.startsWith('http://') || idOrUrl.startsWith('https://')) { - return idOrUrl; + return idOrUrl; } let path = `${this.baseUrl}/ValueSet/${idOrUrl}`; - path = path.replace('//','/'); - return path ; + path = path.replace('//', '/'); + return path; } /** - * Clear all of the cached valuesets + * Clear all of the cached valuesets * This currently does not work since merging and updating to use tingo. Drop collection in tingo is broken - * + * */ clearCache() { - // drop the collection - try{ - const db = Globals.database; + // drop the collection + try { + const db = Globals.database; const collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}`); - if(collection){collection.drop(console.log); - const history_collection = db.collection(`${constants.COLLECTION.VALUESET}_${this.base_version}_History`); - if(history_collection){history_collection.drop(console.log);} + if (collection) { + collection.drop(console.log); + const history_collection = db.collection( + `${constants.COLLECTION.VALUESET}_${this.base_version}_History` + ); + if (history_collection) { + history_collection.drop(console.log); + } } - }catch(e){ + } catch (e) { console.error(e); } } } -export default VsacCache; \ No newline at end of file +export default VsacCache; From 19ea24e377dbe0598640a1b075f3cc167320df92 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Mon, 23 Jan 2023 15:28:09 -0500 Subject: [PATCH 8/8] updating node to version 14 --- .github/workflows/ci-workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index a0ea4b92..6429e351 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: - node-version: '12.x' + node-version: '14.x' - run: npm install - run: npm run lint - run: npm run prettier @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [12] + node-version: [14] steps: - uses: actions/checkout@v1