From cd912c3f364cdd292374c52ddb991c72da7fb9f3 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 09:39:00 -0400 Subject: [PATCH 01/27] Upgrading to mongoose 7 and making slight adjustments based on that update to deal with failing tests --- env.json | 4 +-- package.json | 5 ++-- src/config.ts | 2 +- src/fhir/utilities.ts | 24 +++++++--------- src/lib/MongoDatabase.ts | 2 +- src/lib/__tests__/vsac_cache.test.ts | 43 ++++++++++++---------------- 6 files changed, 35 insertions(+), 45 deletions(-) diff --git a/env.json b/env.json index a9a3fd6e..b36b4dd9 100644 --- a/env.json +++ b/env.json @@ -1,7 +1,7 @@ { - "MONGO_HOSTNAME": { + "MONGO_URL": { "type": "string", - "default": "mongodb://rems-user:pass@127.0.0.1:27017", + "default": "mongodb://127.0.0.1:27017", "required": true }, "MONGO_DB_NAME": { diff --git a/package.json b/package.json index f1896fd5..ce527370 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "develop": "node dist/scripts/develop.js", "start": "ts-node-dev --inspect=8091 src/scripts/serve.ts", - "test": "jest --maxWorkers=4 --coverage --detectOpenHandles", + "test": "jest --runInBand --coverage --detectOpenHandles", "lint": "eslint \"**/*.{js,ts}\"", "lint:fix": "eslint \"**/*.{js,ts}\" --quiet --fix", "prettier": "prettier --check \"**/*.{js,ts}\"", @@ -54,7 +54,6 @@ "dependencies": { "@projecttacoma/node-fhir-server-core": "^2.2.8", "@types/fhir": "^0.0.35", - "@types/mongoose": "^5.11.97", "axios": "^1.2.1", "body-parser": "^1.19.0", "conventional-changelog-cli": "^2.0.34", @@ -65,7 +64,7 @@ "moment": "^2.24.0", "moment-timezone": "^0.5.40", "mongodb": "^4.12.1", - "mongoose": "^6.9.2", + "mongoose": "^7.0.3", "morgan": "^1.9.1", "tingodb": "^0.6.1", "uid": "^2.0.1", diff --git a/src/config.ts b/src/config.ts index 8fbdf8f0..270025d8 100644 --- a/src/config.ts +++ b/src/config.ts @@ -32,7 +32,7 @@ export default { options: '' }, mongoConfig: { - location: env.MONGO_HOSTNAME, + location: env.MONGO_URL, db_name: env.MONGO_DB_NAME, options: { //auto_reconnect: true, diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 606e0201..62c1f333 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -64,20 +64,24 @@ export class FhirUtilities { id = resource.id; } console.log(' FhirUtilities::store: ' + resource.resourceType + ' -- ' + id); - - const fhirResource = new model(resource); + const fhirResource = new model(resource); // Create the resource's metadata const Meta = FhirUtilities.getMeta(baseVersion); fhirResource.meta = new Meta({ versionId: '1', lastUpdated: moment.utc().format('YYYY-MM-DDTHH:mm:ssZ') }); + + + model.exists({ id: fhirResource.id }).then(doesExist => { if (!doesExist) { try { resolve(fhirResource.save()); + ("resolve") } catch { reject(); + } } else { reject(); @@ -269,7 +273,7 @@ export class FhirUtilities { static async populateDB() { // prepopulateDB - medicationCollection.insertMany( + await medicationCollection.insertMany( [ { name: 'Turalio', @@ -376,14 +380,10 @@ export class FhirUtilities { } ] } - ], - (err: any, result: any) => { - if (err) console.log(err); - console.log('Inserted Drug Information'); - } + ] ); - metRequirementsCollection.insertMany( + await metRequirementsCollection.insertMany( [ { stakeholderId: 'Organization/pharm0111', @@ -417,11 +417,7 @@ export class FhirUtilities { completedQuestionnaire: null, case_numbers: [] } - ], - (err: any, result: any) => { - if (err) console.log(err); - console.log('Inserted Pharmacist Met Requirements'); - } + ] ); } } diff --git a/src/lib/MongoDatabase.ts b/src/lib/MongoDatabase.ts index 448f58c1..27661738 100644 --- a/src/lib/MongoDatabase.ts +++ b/src/lib/MongoDatabase.ts @@ -16,7 +16,7 @@ export class MongoDatabase extends Database { new Promise(resolve => { // Connect to mongo console.log('MongoDatabase connect: ' + this.location); - const dbString = `${this.location}/${this.db_name}`; + const dbString = `${this.location}${this.db_name}`; this.client = new mongoose.mongo.MongoClient(dbString); this.database = this.client.db(this.db_name); return resolve(mongoose.connect(dbString)); diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index 2d011ccc..be03b5cc 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -18,11 +18,11 @@ describe('VsacCache', () => { afterAll(async () => { await mongoose.connection.close(); }); + beforeEach(async () => { // client.clearCache(); const baseVersion = '4_0_0'; - const collectionString = `${constants.COLLECTION.VALUESET}_${baseVersion}`; - + await ValueSetModel.deleteMany({}); client.onlyVsac = false; jest.resetModules(); @@ -48,7 +48,7 @@ describe('VsacCache', () => { ); }); - test.skip('should be able to cache valuesets in Library Resources', async () => { + test('should be able to cache valuesets in Library Resources', async function() { const mockRequest = nock('http://cts.nlm.nih.gov/fhir'); mockRequest @@ -59,7 +59,7 @@ describe('VsacCache', () => { .reply(200, JSON.stringify(valueSet)); const valueSets = client.collectLibraryValuesets(library); - valueSets.forEach(async vs => { + valueSets.forEach(async function(vs) { expect(await client.isCached(vs)).toBeFalsy(); }); @@ -73,7 +73,7 @@ describe('VsacCache', () => { } }); - test.skip('should be able to cache valuesets in Questionnaire Resources', async () => { + 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)); @@ -92,22 +92,20 @@ describe('VsacCache', () => { }); test.skip('should be not load valuesets already cached unless forced', async () => { - const mockRequest = nock('http://terminology.hl7.org/'); + const mockRequest = nock('http://terminology.hl7.org'); + const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked' + mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); try { const valueSets = client.collectQuestionnaireValuesets(questionnaire); - valueSets.forEach(async vs => { - expect(await client.isCached(vs)).toBeFalsy(); - }); - - const cached = await client.cacheQuestionnaireItems(questionnaire); + expect(await client.isCached(vs)).toBeFalsy(); - valueSets.forEach(async vs => { - expect(await client.isCached(vs)).toBeTruthy(); - }); + const cached = await client.downloadAndCacheValueset(vs); + expect(await client.isCached(vs)).toBeTruthy(); - const vs = valueSets.values().next().value; + mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); let update = await client.downloadAndCacheValueset(vs); + expect(update.get('cached')).toBeFalsy(); mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); @@ -119,19 +117,16 @@ describe('VsacCache', () => { } }); - test.skip('should be able to handle errors downloading valuesests', async () => { + test('should be able to handle errors downloading valuesests', async () => { const mockRequest = nock('http://terminology.hl7.org/'); + const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked' mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(404, ''); - - const valueSets = client.collectQuestionnaireValuesets(questionnaire); - valueSets.forEach(async vs => { - expect(await client.isCached(vs)).toBeFalsy(); - }); + expect(await client.isCached(vs)).toBeFalsy(); try { - const err = await client.downloadAndCacheValueset( - 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked' - ); + const err = await client.downloadAndCacheValueset(vs); + console.log(err); + expect(err.get('error')).toBeDefined(); } finally { mockRequest.done(); From 0bc9a86d6d01a74f6c2b3d9e7b9a2394e4c74e0b Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 09:40:39 -0400 Subject: [PATCH 02/27] fixing issue with droping collection --- src/lib/vsac_cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index 3cbf3ce9..3f4defe9 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -237,7 +237,7 @@ class VsacCache { clearCache() { // drop the collection try { - ValueSetModel.collection.drop(console.log); + ValueSetModel.collection.drop(); } catch (e) { console.error(e); } From 293f7faecb267d49f884a168b0755c2bacbe2a97 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 09:44:29 -0400 Subject: [PATCH 03/27] linting and prettier fixes --- src/fhir/utilities.ts | 293 +++++++++++++-------------- src/lib/__tests__/vsac_cache.test.ts | 14 +- src/lib/vsac_cache.ts | 8 +- 3 files changed, 152 insertions(+), 163 deletions(-) diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 62c1f333..16fea019 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -64,7 +64,7 @@ export class FhirUtilities { id = resource.id; } console.log(' FhirUtilities::store: ' + resource.resourceType + ' -- ' + id); - const fhirResource = new model(resource); + const fhirResource = new model(resource); // Create the resource's metadata const Meta = FhirUtilities.getMeta(baseVersion); fhirResource.meta = new Meta({ @@ -72,16 +72,13 @@ export class FhirUtilities { lastUpdated: moment.utc().format('YYYY-MM-DDTHH:mm:ssZ') }); - - model.exists({ id: fhirResource.id }).then(doesExist => { if (!doesExist) { try { resolve(fhirResource.save()); - ("resolve") + ('resolve'); } catch { reject(); - } } else { reject(); @@ -273,151 +270,147 @@ export class FhirUtilities { static async populateDB() { // prepopulateDB - await medicationCollection.insertMany( - [ - { - name: 'Turalio', - codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', - code: '2183126', - requirements: [ - { - name: 'Patient Enrollment', - description: 'Submit Patient Enrollment form to the REMS Administrator', - stakeholderType: 'patient', - createNewCase: true, - resourceId: 'TuralioRemsPatientEnrollment' - }, - { - name: 'Prescriber Enrollment', - description: 'Submit Prescriber Enrollment form to the REMS Administrator', - stakeholderType: 'prescriber', - createNewCase: false, - resourceId: 'TuralioPrescriberEnrollmentForm' - }, - { - name: 'Prescriber Knowledge Assessment', - description: 'Submit Prescriber Knowledge Assessment form to the REMS Administrator', - stakeholderType: 'prescriber', - createNewCase: false, - resourceId: 'TuralioPrescriberKnowledgeAssessment' - }, - { - name: 'Pharmacist Enrollment', - description: 'Submit Pharmacist Enrollment form to the REMS Administrator', - stakeholderType: 'pharmacist', - createNewCase: false, - resourceId: 'TuralioPharmacistEnrollment' - } - ] - }, - { - name: 'TIRF', - codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', - code: '1237051', - requirements: [ - { - name: 'Patient Enrollment', - description: 'Submit Patient Enrollment form to the REMS Administrator', - stakeholderType: 'patient', - createNewCase: true, - resourceId: 'TIRFRemsPatientEnrollment' - }, - { - name: 'Prescriber Enrollment', - description: 'Submit Prescriber Enrollment form to the REMS Administrator', - stakeholderType: 'prescriber', - createNewCase: false, - resourceId: 'TIRFPrescriberEnrollmentForm' - }, - { - name: 'Prescriber Knowledge Assessment', - description: 'Submit Prescriber Knowledge Assessment form to the REMS Administrator', - stakeholderType: 'prescriber', - createNewCase: false, - resourceId: 'TIRFPrescriberKnowledgeAssessment' - }, - { - name: 'Pharmacist Enrollment', - description: 'Submit Pharmacist Enrollment form to the REMS Administrator', - stakeholderType: 'pharmacist', - createNewCase: false, - resourceId: 'TIRFPharmacistEnrollmentForm' - }, - { - name: 'Pharmacist Knowledge Assessment', - description: 'Submit Pharmacist Knowledge Assessment form to the REMS Administrator', - stakeholderType: 'pharmacist', - createNewCase: false, - resourceId: 'TIRFPharmacistKnowledgeAssessment' - } - ] - }, - { - name: 'Isotretinoin', - codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', - code: '6064', - requirements: [ - { - name: 'Patient Enrollment', - description: 'Submit Patient Enrollment form to the REMS Administrator', - stakeholderType: 'patient', - createNewCase: true, - resourceId: 'IPledgeRemsPatientEnrollment' - }, - { - name: 'Prescriber Enrollment', - description: 'Submit Prescriber Enrollment form to the REMS Administrator', - stakeholderType: 'prescriber', - createNewCase: false, - resourceId: 'IPledgeRemsPrescriberEnrollmentForm' - }, - { - name: 'Pharmacist Enrollment', - description: 'Submit Pharmacist Enrollment form to the REMS Administrator', - stakeholderType: 'pharmacist', - createNewCase: false, - resourceId: 'IPledgeRemsPharmacistEnrollmentForm' - } - ] - } - ] - ); + await medicationCollection.insertMany([ + { + name: 'Turalio', + codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: '2183126', + requirements: [ + { + name: 'Patient Enrollment', + description: 'Submit Patient Enrollment form to the REMS Administrator', + stakeholderType: 'patient', + createNewCase: true, + resourceId: 'TuralioRemsPatientEnrollment' + }, + { + name: 'Prescriber Enrollment', + description: 'Submit Prescriber Enrollment form to the REMS Administrator', + stakeholderType: 'prescriber', + createNewCase: false, + resourceId: 'TuralioPrescriberEnrollmentForm' + }, + { + name: 'Prescriber Knowledge Assessment', + description: 'Submit Prescriber Knowledge Assessment form to the REMS Administrator', + stakeholderType: 'prescriber', + createNewCase: false, + resourceId: 'TuralioPrescriberKnowledgeAssessment' + }, + { + name: 'Pharmacist Enrollment', + description: 'Submit Pharmacist Enrollment form to the REMS Administrator', + stakeholderType: 'pharmacist', + createNewCase: false, + resourceId: 'TuralioPharmacistEnrollment' + } + ] + }, + { + name: 'TIRF', + codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: '1237051', + requirements: [ + { + name: 'Patient Enrollment', + description: 'Submit Patient Enrollment form to the REMS Administrator', + stakeholderType: 'patient', + createNewCase: true, + resourceId: 'TIRFRemsPatientEnrollment' + }, + { + name: 'Prescriber Enrollment', + description: 'Submit Prescriber Enrollment form to the REMS Administrator', + stakeholderType: 'prescriber', + createNewCase: false, + resourceId: 'TIRFPrescriberEnrollmentForm' + }, + { + name: 'Prescriber Knowledge Assessment', + description: 'Submit Prescriber Knowledge Assessment form to the REMS Administrator', + stakeholderType: 'prescriber', + createNewCase: false, + resourceId: 'TIRFPrescriberKnowledgeAssessment' + }, + { + name: 'Pharmacist Enrollment', + description: 'Submit Pharmacist Enrollment form to the REMS Administrator', + stakeholderType: 'pharmacist', + createNewCase: false, + resourceId: 'TIRFPharmacistEnrollmentForm' + }, + { + name: 'Pharmacist Knowledge Assessment', + description: 'Submit Pharmacist Knowledge Assessment form to the REMS Administrator', + stakeholderType: 'pharmacist', + createNewCase: false, + resourceId: 'TIRFPharmacistKnowledgeAssessment' + } + ] + }, + { + name: 'Isotretinoin', + codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: '6064', + requirements: [ + { + name: 'Patient Enrollment', + description: 'Submit Patient Enrollment form to the REMS Administrator', + stakeholderType: 'patient', + createNewCase: true, + resourceId: 'IPledgeRemsPatientEnrollment' + }, + { + name: 'Prescriber Enrollment', + description: 'Submit Prescriber Enrollment form to the REMS Administrator', + stakeholderType: 'prescriber', + createNewCase: false, + resourceId: 'IPledgeRemsPrescriberEnrollmentForm' + }, + { + name: 'Pharmacist Enrollment', + description: 'Submit Pharmacist Enrollment form to the REMS Administrator', + stakeholderType: 'pharmacist', + createNewCase: false, + resourceId: 'IPledgeRemsPharmacistEnrollmentForm' + } + ] + } + ]); - await metRequirementsCollection.insertMany( - [ - { - stakeholderId: 'Organization/pharm0111', - completed: true, - requirementName: 'Pharmacist Enrollment', - drugName: 'Turalio', - completedQuestionnaire: null, - case_numbers: [] - }, - { - stakeholderId: 'Organization/pharm0111', - completed: true, - requirementName: 'Pharmacist Enrollment', - drugName: 'TIRF', - completedQuestionnaire: null, - case_numbers: [] - }, - { - stakeholderId: 'Organization/pharm0111', - completed: true, - requirementName: 'Pharmacist Knowledge Assessment', - drugName: 'TIRF', - completedQuestionnaire: null, - case_numbers: [] - }, - { - stakeholderId: 'Organization/pharm0111', - completed: true, - requirementName: 'Pharmacist Enrollment', - drugName: 'Isotretinoin', - completedQuestionnaire: null, - case_numbers: [] - } - ] - ); + await metRequirementsCollection.insertMany([ + { + stakeholderId: 'Organization/pharm0111', + completed: true, + requirementName: 'Pharmacist Enrollment', + drugName: 'Turalio', + completedQuestionnaire: null, + case_numbers: [] + }, + { + stakeholderId: 'Organization/pharm0111', + completed: true, + requirementName: 'Pharmacist Enrollment', + drugName: 'TIRF', + completedQuestionnaire: null, + case_numbers: [] + }, + { + stakeholderId: 'Organization/pharm0111', + completed: true, + requirementName: 'Pharmacist Knowledge Assessment', + drugName: 'TIRF', + completedQuestionnaire: null, + case_numbers: [] + }, + { + stakeholderId: 'Organization/pharm0111', + completed: true, + requirementName: 'Pharmacist Enrollment', + drugName: 'Isotretinoin', + completedQuestionnaire: null, + case_numbers: [] + } + ]); } } diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index be03b5cc..7a139c91 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -22,7 +22,7 @@ describe('VsacCache', () => { beforeEach(async () => { // client.clearCache(); const baseVersion = '4_0_0'; - + await ValueSetModel.deleteMany({}); client.onlyVsac = false; jest.resetModules(); @@ -48,7 +48,7 @@ describe('VsacCache', () => { ); }); - test('should be able to cache valuesets in Library Resources', async function() { + test('should be able to cache valuesets in Library Resources', async function () { const mockRequest = nock('http://cts.nlm.nih.gov/fhir'); mockRequest @@ -59,7 +59,7 @@ describe('VsacCache', () => { .reply(200, JSON.stringify(valueSet)); const valueSets = client.collectLibraryValuesets(library); - valueSets.forEach(async function(vs) { + valueSets.forEach(async function (vs) { expect(await client.isCached(vs)).toBeFalsy(); }); @@ -93,7 +93,7 @@ describe('VsacCache', () => { test.skip('should be not load valuesets already cached unless forced', async () => { const mockRequest = nock('http://terminology.hl7.org'); - const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked' + const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'; mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); try { @@ -105,7 +105,7 @@ describe('VsacCache', () => { mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); let update = await client.downloadAndCacheValueset(vs); - + expect(update.get('cached')).toBeFalsy(); mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); @@ -119,14 +119,14 @@ describe('VsacCache', () => { test('should be able to handle errors downloading valuesests', async () => { const mockRequest = nock('http://terminology.hl7.org/'); - const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked' + const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'; mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(404, ''); expect(await client.isCached(vs)).toBeFalsy(); try { const err = await client.downloadAndCacheValueset(vs); console.log(err); - + expect(err.get('error')).toBeDefined(); } finally { mockRequest.done(); diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index 3f4defe9..98139014 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -1,10 +1,6 @@ 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'; import ValueSetModel from './schemas/resources/ValueSet'; import { ValueSet } from 'fhir/r4'; class VsacCache { @@ -36,7 +32,7 @@ class VsacCache { */ async cacheLibrary(library: any, forceReload = false) { const valueSets = this.collectLibraryValuesets(library); - return await this.cacheValuesets(valueSets); + return await this.cacheValuesets(valueSets,forceReload); } /** @@ -48,7 +44,7 @@ class VsacCache { async cacheQuestionnaireItems(obj: any, forceReload = false) { const valueSets = this.collectQuestionnaireValuesets(obj); - return await this.cacheValuesets(valueSets); + return await this.cacheValuesets(valueSets,forceReload); } /** From f1ce72071ed6567d7351ec900c32658cf4f9d915 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 10:07:45 -0400 Subject: [PATCH 04/27] fixing prettier errors after linting --- src/lib/vsac_cache.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index 98139014..8b0b88e2 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -32,7 +32,7 @@ class VsacCache { */ async cacheLibrary(library: any, forceReload = false) { const valueSets = this.collectLibraryValuesets(library); - return await this.cacheValuesets(valueSets,forceReload); + return await this.cacheValuesets(valueSets, forceReload); } /** @@ -44,7 +44,7 @@ class VsacCache { async cacheQuestionnaireItems(obj: any, forceReload = false) { const valueSets = this.collectQuestionnaireValuesets(obj); - return await this.cacheValuesets(valueSets,forceReload); + return await this.cacheValuesets(valueSets, forceReload); } /** From 00a5d82786ea42d67659f405de1562c4cd949ded Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 10:09:30 -0400 Subject: [PATCH 05/27] removing node 16 tests --- .github/workflows/ci-workflow.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 27900387..6429e351 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -33,22 +33,4 @@ jobs: - run: npm install - run: npm test env: - CI: true - test_mac_os: - name: Test on node ${{ matrix.node-version }} and ${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [macos-latest] - node-version: [16] - - steps: - - uses: actions/checkout@v1 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm test - env: - CI: true + CI: true \ No newline at end of file From 7e9dedfbf2c4309e05755e949b9e9c04a1a23361 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 10:29:17 -0400 Subject: [PATCH 06/27] removing log statements --- src/lib/__tests__/vsac_cache.test.ts | 7 +++++-- src/lib/vsac_cache.ts | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index 7a139c91..1bcfc1cc 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -122,12 +122,15 @@ describe('VsacCache', () => { const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'; mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(404, ''); expect(await client.isCached(vs)).toBeFalsy(); - + let err; try { - const err = await client.downloadAndCacheValueset(vs); + err = await client.downloadAndCacheValueset(vs); console.log(err); expect(err.get('error')).toBeDefined(); + } catch (e) { + console.log('Expected Error to be defined', err); + throw e; } finally { mockRequest.done(); } diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index 8b0b88e2..e54b4ee3 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -103,8 +103,7 @@ class VsacCache { 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); + console.log('Error Downloading ', idOrUrl, vs.get('error')); } else if (vs.get('valueSet')) { await this.storeValueSet(this.getValuesetId(idOrUrl), vs.get('valueSet')); vs.set('cached', true); @@ -145,7 +144,6 @@ class VsacCache { }); if ((this.onlyVsac && isVsac) || !this.onlyVsac) { try { - console.log('Downloading vs ' + url); const vs = await axios.get(url, { headers: headers }); From c2113db13461f52e75e49ab5aaf3994e3a273dc3 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 10:39:57 -0400 Subject: [PATCH 07/27] Attempting to update ci test to node 18 to see if that has effect on outcomes --- .github/workflows/ci-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 6429e351..cb680df3 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [14] + node-version: [18] steps: - uses: actions/checkout@v1 From c977e23734610ee08042a39ce3aefb644d33ca06 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 11:06:56 -0400 Subject: [PATCH 08/27] aligning jest package versions --- package.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ce527370..915d4801 100644 --- a/package.json +++ b/package.json @@ -76,8 +76,9 @@ "devDependencies": { "@shelf/jest-mongodb": "^4.1.6", "@types/cors": "^2.8.12", + "@types/mongoose": "^5.11.97", "@types/express": "^4.17.14", - "@types/jest": "^27.5.2", + "@types/jest": "^27.5.1", "@types/lodash": "^4.14.188", "@types/morgan": "^1.9.3", "@types/node": "^18.11.9", @@ -88,7 +89,9 @@ "axios-mock-adapter": "^1.21.2", "eslint": "^8.28.0", "eslint-config-prettier": "^6.10.1", - "jest": "^27.4.5", + "jest": "^27.5.1", + "jest-environment-node":"^27.5.1", + "jest-config": "^27.5.1", "jest-extended": "^1.2.0", "json-diff": "^0.9.0", "nock": "^13.2.9", From eca269267a381332fe0311e7778880128b968124 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 11:16:41 -0400 Subject: [PATCH 09/27] removing logging statements --- src/lib/__tests__/vsac_cache.test.ts | 2 +- src/lib/vsac_cache.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index 1bcfc1cc..ebb73610 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -125,7 +125,7 @@ describe('VsacCache', () => { let err; try { err = await client.downloadAndCacheValueset(vs); - console.log(err); + // console.log(err); expect(err.get('error')).toBeDefined(); } catch (e) { diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index e54b4ee3..12eeaead 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -103,7 +103,7 @@ class VsacCache { if (forceReload || !(await this.isCached(idOrUrl))) { const vs = await this.downloadValueset(idOrUrl); if (vs.get('error')) { - console.log('Error Downloading ', idOrUrl, vs.get('error')); + console.log('Error Downloading ', idOrUrl, typeof vs.get('error')); } else if (vs.get('valueSet')) { await this.storeValueSet(this.getValuesetId(idOrUrl), vs.get('valueSet')); vs.set('cached', true); From 98a421a2daf690254c6b19a7adb93fea3d29602b Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Fri, 31 Mar 2023 11:18:32 -0400 Subject: [PATCH 10/27] Prettier fixes --- src/lib/__tests__/vsac_cache.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index ebb73610..241884c0 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -125,7 +125,7 @@ describe('VsacCache', () => { let err; try { err = await client.downloadAndCacheValueset(vs); - // console.log(err); + // console.log(err); expect(err.get('error')).toBeDefined(); } catch (e) { From f346b0901712565956ad069cdfbd45c258cbc3ff Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Mon, 3 Apr 2023 13:50:52 -0400 Subject: [PATCH 11/27] Migrating tests to mocha and sinon to deal with github CI runner issues --- package.json | 44 ++-------- src/hooks/rems.hook.test.ts | 11 +-- src/lib/__tests__/vsac_cache.test.ts | 64 ++++++++------ src/lib/vsac_cache.ts | 4 +- src/lib/winston.test.ts | 33 +++---- src/server.test.ts | 126 +++++++++++++++------------ 6 files changed, 140 insertions(+), 142 deletions(-) diff --git a/package.json b/package.json index 915d4801..47ca37cf 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "develop": "node dist/scripts/develop.js", "start": "ts-node-dev --inspect=8091 src/scripts/serve.ts", - "test": "jest --runInBand --coverage --detectOpenHandles", + "test": "mocha -r ts-node/register src/*.test.ts src/**/*.test.ts src/**/**/*.test.ts", "lint": "eslint \"**/*.{js,ts}\"", "lint:fix": "eslint \"**/*.{js,ts}\" --quiet --fix", "prettier": "prettier --check \"**/*.{js,ts}\"", @@ -24,33 +24,6 @@ "type": "git", "url": "git+ssh://git@bitbucket.org/asymmetrik/carejourney-cds.git" }, - "jest": { - "moduleNameMapper": { - "axios": "axios/dist/node/axios.cjs" - }, - "preset": "@shelf/jest-mongodb", - "testEnvironment": "node", - "transform": { - "^.+\\.ts?$": "ts-jest" - }, - "transformIgnorePatterns": [ - "/node_modules/" - ], - "testPathIgnorePatterns": [ - "/src/config/env/test.js" - ], - "verbose": true, - "collectCoverage": true, - "coverageReporters": [ - "text", - "lcov", - "json" - ], - "coveragePathIgnorePatterns": [ - "/node_modules", - "/src/profiles" - ] - }, "dependencies": { "@projecttacoma/node-fhir-server-core": "^2.2.8", "@types/fhir": "^0.0.35", @@ -74,29 +47,30 @@ "winston-daily-rotate-file": "^4.2.1" }, "devDependencies": { - "@shelf/jest-mongodb": "^4.1.6", + "@types/chai": "^4.3.4", "@types/cors": "^2.8.12", - "@types/mongoose": "^5.11.97", "@types/express": "^4.17.14", - "@types/jest": "^27.5.1", "@types/lodash": "^4.14.188", + "@types/mocha": "^10.0.1", + "@types/mongoose": "^5.11.97", "@types/morgan": "^1.9.3", "@types/node": "^18.11.9", "@types/nodemon": "^1.19.2", + "@types/sinon" : "10.0.13", "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "axios-mock-adapter": "^1.21.2", + "chai": "^4.3.7", "eslint": "^8.28.0", "eslint-config-prettier": "^6.10.1", - "jest": "^27.5.1", - "jest-environment-node":"^27.5.1", - "jest-config": "^27.5.1", - "jest-extended": "^1.2.0", "json-diff": "^0.9.0", + "mocha": "^10.2.0", + "mongodb-memory-server": "^8.12.1", "nock": "^13.2.9", "nodemon": "^2.0.20", "prettier": "^2.0.5", + "sinon": "^15.0.3", "ts-jest": "^27.1.2", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", diff --git a/src/hooks/rems.hook.test.ts b/src/hooks/rems.hook.test.ts index 488f41be..4e96e339 100644 --- a/src/hooks/rems.hook.test.ts +++ b/src/hooks/rems.hook.test.ts @@ -1,8 +1,9 @@ import OrderSign from './OrderSign'; import getREMSHook from './rems.hook'; +import {expect} from 'chai'; describe('hook: test rems', () => { - test('should have definition and handler', () => { + it('should have definition and handler', () => { const prefetch = { patient: 'Patient/{{context.patientId}}', practitioner: 'Practitioner/{{context.userId}}' @@ -15,10 +16,10 @@ describe('hook: test rems', () => { prefetch ); - expect(getREMSHook).toHaveProperty('definition'); - expect(getREMSHook).toHaveProperty('handler'); + expect(getREMSHook).to.haveOwnProperty('definition'); + expect(getREMSHook).to.haveOwnProperty('handler'); - expect(getREMSHook.definition).toStrictEqual(expectedDefinition); - expect(getREMSHook.handler).toBeInstanceOf(Function); + expect(getREMSHook.definition).to.deep.equal(expectedDefinition); + expect(getREMSHook.handler).to.instanceOf(Function); }); }); diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index 241884c0..9d116f81 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -5,18 +5,29 @@ import valueSet from './fixtures/valueSet.json'; import nock from 'nock'; import constants from '../../constants'; import ValueSetModel from '../schemas/resources/ValueSet'; -import mongoose from 'mongoose'; +import mongoose, { ConnectOptions } from 'mongoose'; +const { MongoMemoryServer } = require("mongodb-memory-server"); +import { assert, expect } from "chai" describe('VsacCache', () => { const client = new VsacCache('./tmp', '2c1d55c3-3484-4902-b645-25f3a4974ce6'); - - beforeAll(async () => { - if (process.env.MONGO_URL) { - await mongoose.connect(process.env.MONGO_URL); + let mongo: typeof MongoMemoryServer; + before(async () => { + + mongo = await MongoMemoryServer.create(); + const uri = mongo.getUri(); + let options :ConnectOptions = { + } + await mongoose.connect(uri, options); }); - afterAll(async () => { + after(async () => { + console.log("Closing connection?"); + + await mongoose.connection.dropDatabase(); await mongoose.connection.close(); + await mongo.stop(); + }); beforeEach(async () => { @@ -25,15 +36,14 @@ describe('VsacCache', () => { await ValueSetModel.deleteMany({}); client.onlyVsac = false; - 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 () => { + it('should be able to collect valueset references from Library Resources', async () => { const valueSets = client.collectLibraryValuesets(library); - expect(valueSets).toEqual( + expect(valueSets).to.deep.equal( 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' @@ -41,14 +51,14 @@ describe('VsacCache', () => { ); }); - test('should be able to collect valueset references from Questionnaire Resources', async () => { + it('should be able to collect valueset references from Questionnaire Resources', async () => { const valueSets = client.collectQuestionnaireValuesets(questionnaire); - expect(valueSets).toEqual( + expect(valueSets).to.deep.equal( new Set(['http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked']) ); }); - test('should be able to cache valuesets in Library Resources', async function () { + it('should be able to cache valuesets in Library Resources', async function () { const mockRequest = nock('http://cts.nlm.nih.gov/fhir'); mockRequest @@ -60,74 +70,74 @@ describe('VsacCache', () => { const valueSets = client.collectLibraryValuesets(library); valueSets.forEach(async function (vs) { - expect(await client.isCached(vs)).toBeFalsy(); + expect(await client.isCached(vs)).to.be.false; }); try { await client.cacheLibrary(library); valueSets.forEach(async vs => { - expect(await client.isCached(vs)).toBeTruthy(); + expect(await client.isCached(vs)).to.be.true; }); } finally { mockRequest.done(); } }); - test('should be able to cache valuesets in Questionnaire Resources', async () => { + it('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)); const valueSets = client.collectQuestionnaireValuesets(questionnaire); valueSets.forEach(async vs => { - expect(await client.isCached(vs)).toBeFalsy(); + expect(await client.isCached(vs)).to.be.false; }); try { await client.cacheQuestionnaireItems(questionnaire); valueSets.forEach(async vs => { - expect(await client.isCached(vs)).toBeTruthy(); + expect(await client.isCached(vs)).to.be.true; }); } finally { mockRequest.done(); } }); - test.skip('should be not load valuesets already cached unless forced', async () => { + it.skip('should be not load valuesets already cached unless forced', async () => { const mockRequest = nock('http://terminology.hl7.org'); const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'; mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); try { const valueSets = client.collectQuestionnaireValuesets(questionnaire); - expect(await client.isCached(vs)).toBeFalsy(); + expect(await client.isCached(vs)).to.be.false; const cached = await client.downloadAndCacheValueset(vs); - expect(await client.isCached(vs)).toBeTruthy(); + expect(await client.isCached(vs)).to.be.true; mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); let update = await client.downloadAndCacheValueset(vs); - expect(update.get('cached')).toBeFalsy(); + expect(update.get('cached')).to.be.false; 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')).to.be.true; } finally { mockRequest.done(); } }); - test('should be able to handle errors downloading valuesests', async () => { + it('should be able to handle errors downloading valuesests', async () => { const mockRequest = nock('http://terminology.hl7.org/'); const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'; mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(404, ''); - expect(await client.isCached(vs)).toBeFalsy(); + expect(await client.isCached(vs)).to.be.false; let err; try { err = await client.downloadAndCacheValueset(vs); // console.log(err); - expect(err.get('error')).toBeDefined(); + expect(err.get('error')).to.not.be.undefined; } catch (e) { console.log('Expected Error to be defined', err); throw e; @@ -136,10 +146,10 @@ describe('VsacCache', () => { } }); - test('Should not attempt tp download non-vsac valuesets if configured to do so', async () => { + it('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( + expect(err.get('error')).to.equal( '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 12eeaead..a19a6ed0 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -174,10 +174,10 @@ class VsacCache { if (valueSet) { resolve(valueSet); } - resolve(null); + resolve(false); }); } else { - resolve(null); + resolve(false); } }); } diff --git a/src/lib/winston.test.ts b/src/lib/winston.test.ts index 508847bd..4141a32a 100644 --- a/src/lib/winston.test.ts +++ b/src/lib/winston.test.ts @@ -6,37 +6,38 @@ */ import container from './winston'; import config from '../config'; +import {assert, expect} from "chai"; describe('Logger Class', () => { beforeEach(() => { - jest.resetModules(); + }); - test('setup without daily log file', () => { + it('setup without daily log file', () => { const logger = container.get('application'); - expect(logger).toBeDefined(); - expect(logger.transports).toHaveLength(1); - expect(logger.transports[0].level).toBe(config.logging.level); + expect(logger).to.not.equal(undefined); + expect(logger.transports.length).to.equal(1); + expect(logger.transports[0].level).to.equal(config.logging.level); }); - test('setup with daily log file generation', () => { + it('setup with daily log file generation', () => { // Mock the config to test other branch of if statement - jest.mock('../config', () => ({ - logging: { - level: 'debug', - directory: 'logs' - } - })); + // jest.mock('../config', () => ({ + // logging: { + // level: 'debug', + // directory: 'logs' + // } + // })); const containerPromise = import('./winston'); const configPromise = import('../config'); containerPromise.then(container => { configPromise.then(config => { const logger = container.default.get('application'); - expect(logger).toBeDefined(); - expect(logger.transports).toHaveLength(2); - expect(logger.transports[0].level).toBe(config.default.logging.level); - expect(logger.transports[1].level).toBe(config.default.logging.level); + expect(logger).to.not.equal(undefined); + expect(logger.transports.length).to.equal(2); + expect(logger.transports[0].level).to.equal(config.default.logging.level); + expect(logger.transports[1].level).to.equal(config.default.logging.level); }); }); }); diff --git a/src/server.test.ts b/src/server.test.ts index bdf4fa88..5e1a5b58 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -2,83 +2,95 @@ import { initialize, REMSServer } from './server'; import config from './config'; import { Globals } from './globals'; import { Db, MongoClient } from 'mongodb'; +import sinon from "sinon"; +import {assert, expect} from "chai"; +const { MongoMemoryServer } = require("mongodb-memory-server"); +import mongoose, { ConnectOptions } from 'mongoose'; + describe('REMSServer class', () => { let server: REMSServer; let connection: MongoClient; let db: Db; - - beforeAll(async () => { - if (process.env.MONGO_URL) { - connection = await MongoClient.connect(process.env.MONGO_URL, {}); - db = await connection.db(process.env.MONGO_DB_NAME); - Globals.database = db; + let mongo: typeof MongoMemoryServer; + before(async () => { + + mongo = await MongoMemoryServer.create(); + const uri = mongo.getUri(); + let options :ConnectOptions = { + } + await mongoose.connect(uri, options); }); - afterAll(async () => { - await connection.close(); + after(async () => { + console.log("Closing connection?"); + + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongo.stop(); + }); beforeEach(() => { - jest.mock('morgan', () => jest.fn()); - - // Mock express and body parser - jest.mock('body-parser', () => ({ - urlencoded: jest.fn(), - json: jest.fn() - })); - - jest.mock('express', () => { - const mock = jest.fn(() => ({ - use: jest.fn(), - set: jest.fn(), - get: jest.fn(), - listen: jest.fn(), - options: jest.fn(), - post: jest.fn(), - static: jest.fn() - })); - return mock; - }); + // jest.mock('morgan', () => jest.fn()); + + // // Mock express and body parser + // jest.mock('body-parser', () => ({ + // urlencoded: jest.fn(), + // json: jest.fn() + // })); + + // jest.mock('express', () => { + // const mock = jest.fn(() => ({ + // use: jest.fn(), + // set: jest.fn(), + // get: jest.fn(), + // listen: jest.fn(), + // options: jest.fn(), + // post: jest.fn(), + // static: jest.fn() + // })); + // return mock; + // }); server = new REMSServer(config.fhirServerConfig); }); afterEach(() => { - jest.clearAllMocks(); + // jest.clearAllMocks(); }); - test('method: constructor', () => { - expect(server).toBeInstanceOf(REMSServer); - expect(server).toHaveProperty('app'); - expect(server).toHaveProperty('listen'); + it('method: constructor', () => { + expect(server).to.be.instanceOf(REMSServer); + expect(server).to.have.property('app'); + expect(server).to.have.property('listen'); }); - test('method: configureMiddleware', () => { - const set = jest.spyOn(server.app, 'set'); - const use = jest.spyOn(server.app, 'use'); + it('method: configureMiddleware', () => { + const set = sinon.spy(server.app, 'set'); + const use = sinon.spy(server.app, 'use'); server.configureMiddleware(); - expect(set).toHaveBeenCalledTimes(6); - expect(set.mock.calls[0][0]).toBe('showStackError'); - expect(set.mock.calls[0][1]).toBe(true); - expect(set.mock.calls[5][0]).toBe('jsonp callback'); - expect(set.mock.calls[5][1]).toBe(true); + expect(set.callCount).to.equal(6); + // expect(set.mock.calls[0][0]).toBe('showStackError'); + // expect(set.mock.calls[0][1]).toBe(true); + // expect(set.mock.calls[5][0]).toBe('jsonp callback'); + // expect(set.mock.calls[5][1]).toBe(true); - expect(use).toHaveBeenCalledTimes(8); + expect(use.callCount).to.equal(8); }); - test('method: configureLogstream', () => { - const use = jest.spyOn(server.app, 'use'); + it('method: configureLogstream', () => { + const use = sinon.spy(server.app, 'use'); server.configureLogstream(); - expect(use).toHaveBeenCalledTimes(1); + expect(use.calledOnce).to.have.true; }); - test('method: registerService', () => { + it('method: registerService', () => { const mockService = { definition: { hook: 'patient-view', @@ -93,7 +105,7 @@ describe('REMSServer class', () => { server.registerService(mockService); - expect(server.services).toStrictEqual([ + expect(server.services).to.deep.equal([ { hook: 'patient-view', name: 'foo', @@ -103,21 +115,21 @@ describe('REMSServer class', () => { ]); }); - test('Method: listen', () => { - const listen = jest.spyOn(server.app, 'listen'); - const callback = jest.fn(); + it('Method: listen', () => { + const listen = sinon.spy(server.app, 'listen'); + const callback = sinon.fake(); // Start listening on a port and pass the callback through const serverListen = server.listen({ port: 3000 }, callback); - expect(listen).toHaveBeenCalledTimes(1); - expect(listen.mock.calls[0][0]).toBe(3000); - expect(listen.mock.calls[0][1]).toBe(callback); + expect(listen.calledOnce).to.be.true; + // expect(listen.mock.calls[0][0]).toBe(3000); + // expect(listen.mock.calls[0][1]).toBe(callback); serverListen.close(); }); - test('should be able to initilize a server', () => { + it('should be able to initilize a server', () => { const newServer = initialize(config); - expect(newServer).toBeInstanceOf(REMSServer); - expect(newServer).toHaveProperty('app'); - expect(newServer).toHaveProperty('listen'); + expect(newServer).to.be.instanceOf(REMSServer); + expect(newServer).to.have.property('app'); + expect(newServer).to.have.property('listen'); }); }); From 41244928262403669f2c2f6575eedab6527015e4 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Mon, 3 Apr 2023 14:17:09 -0400 Subject: [PATCH 12/27] Fixing liniting/prettier issues --- package.json | 1 + src/hooks/rems.hook.test.ts | 2 +- src/lib/__tests__/vsac_cache.test.ts | 16 ++++++---------- src/lib/winston.test.ts | 5 +---- src/server.test.ts | 19 +++++++------------ 5 files changed, 16 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 47ca37cf..d7b3585b 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@types/lodash": "^4.14.188", "@types/mocha": "^10.0.1", "@types/mongoose": "^5.11.97", + "@types/mongodb-memory-server": "2.3.0", "@types/morgan": "^1.9.3", "@types/node": "^18.11.9", "@types/nodemon": "^1.19.2", diff --git a/src/hooks/rems.hook.test.ts b/src/hooks/rems.hook.test.ts index 4e96e339..60dd15dd 100644 --- a/src/hooks/rems.hook.test.ts +++ b/src/hooks/rems.hook.test.ts @@ -1,6 +1,6 @@ import OrderSign from './OrderSign'; import getREMSHook from './rems.hook'; -import {expect} from 'chai'; +import { expect } from 'chai'; describe('hook: test rems', () => { it('should have definition and handler', () => { diff --git a/src/lib/__tests__/vsac_cache.test.ts b/src/lib/__tests__/vsac_cache.test.ts index 9d116f81..b74bd3eb 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/src/lib/__tests__/vsac_cache.test.ts @@ -6,28 +6,24 @@ import nock from 'nock'; import constants from '../../constants'; import ValueSetModel from '../schemas/resources/ValueSet'; import mongoose, { ConnectOptions } from 'mongoose'; -const { MongoMemoryServer } = require("mongodb-memory-server"); -import { assert, expect } from "chai" +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { assert, expect } from 'chai'; describe('VsacCache', () => { const client = new VsacCache('./tmp', '2c1d55c3-3484-4902-b645-25f3a4974ce6'); - let mongo: typeof MongoMemoryServer; + let mongo: any; before(async () => { - mongo = await MongoMemoryServer.create(); const uri = mongo.getUri(); - let options :ConnectOptions = { - - } + const options: ConnectOptions = {}; await mongoose.connect(uri, options); }); after(async () => { - console.log("Closing connection?"); - + console.log('Closing connection?'); + await mongoose.connection.dropDatabase(); await mongoose.connection.close(); await mongo.stop(); - }); beforeEach(async () => { diff --git a/src/lib/winston.test.ts b/src/lib/winston.test.ts index 4141a32a..0d589fbe 100644 --- a/src/lib/winston.test.ts +++ b/src/lib/winston.test.ts @@ -6,12 +6,9 @@ */ import container from './winston'; import config from '../config'; -import {assert, expect} from "chai"; +import { expect } from 'chai'; describe('Logger Class', () => { - beforeEach(() => { - - }); it('setup without daily log file', () => { const logger = container.get('application'); diff --git a/src/server.test.ts b/src/server.test.ts index 5e1a5b58..644f1044 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -1,10 +1,9 @@ import { initialize, REMSServer } from './server'; import config from './config'; -import { Globals } from './globals'; import { Db, MongoClient } from 'mongodb'; -import sinon from "sinon"; -import {assert, expect} from "chai"; -const { MongoMemoryServer } = require("mongodb-memory-server"); +import sinon from 'sinon'; +import {expect} from 'chai'; +import { MongoMemoryServer } from 'mongodb-memory-server'; import mongoose, { ConnectOptions } from 'mongoose'; describe('REMSServer class', () => { @@ -12,19 +11,18 @@ describe('REMSServer class', () => { let connection: MongoClient; let db: Db; - let mongo: typeof MongoMemoryServer; + let mongo: any; before(async () => { - mongo = await MongoMemoryServer.create(); const uri = mongo.getUri(); - let options :ConnectOptions = { + const options :ConnectOptions = { - } + }; await mongoose.connect(uri, options); }); after(async () => { - console.log("Closing connection?"); + console.log('Closing connection?'); await mongoose.connection.dropDatabase(); await mongoose.connection.close(); @@ -57,9 +55,6 @@ describe('REMSServer class', () => { server = new REMSServer(config.fhirServerConfig); }); - afterEach(() => { - // jest.clearAllMocks(); - }); it('method: constructor', () => { expect(server).to.be.instanceOf(REMSServer); From cff60ccfee855e4bed719f2ebcb9400946a2a786 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Mon, 3 Apr 2023 14:43:02 -0400 Subject: [PATCH 13/27] more prettier issues --- src/lib/winston.test.ts | 1 - src/server.test.ts | 12 ++++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/lib/winston.test.ts b/src/lib/winston.test.ts index 0d589fbe..87600480 100644 --- a/src/lib/winston.test.ts +++ b/src/lib/winston.test.ts @@ -9,7 +9,6 @@ import config from '../config'; import { expect } from 'chai'; describe('Logger Class', () => { - it('setup without daily log file', () => { const logger = container.get('application'); diff --git a/src/server.test.ts b/src/server.test.ts index 644f1044..1b58074e 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -2,8 +2,8 @@ import { initialize, REMSServer } from './server'; import config from './config'; import { Db, MongoClient } from 'mongodb'; import sinon from 'sinon'; -import {expect} from 'chai'; -import { MongoMemoryServer } from 'mongodb-memory-server'; +import { expect } from 'chai'; +import { MongoMemoryServer } from 'mongodb-memory-server'; import mongoose, { ConnectOptions } from 'mongoose'; describe('REMSServer class', () => { @@ -15,19 +15,16 @@ describe('REMSServer class', () => { before(async () => { mongo = await MongoMemoryServer.create(); const uri = mongo.getUri(); - const options :ConnectOptions = { - - }; + const options: ConnectOptions = {}; await mongoose.connect(uri, options); }); after(async () => { console.log('Closing connection?'); - + await mongoose.connection.dropDatabase(); await mongoose.connection.close(); await mongo.stop(); - }); beforeEach(() => { @@ -55,7 +52,6 @@ describe('REMSServer class', () => { server = new REMSServer(config.fhirServerConfig); }); - it('method: constructor', () => { expect(server).to.be.instanceOf(REMSServer); expect(server).to.have.property('app'); From 2f7b3074bbac2a037c0da9cc1770320117dfc4b3 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Mon, 3 Apr 2023 14:47:52 -0400 Subject: [PATCH 14/27] Increasing timeout from 2 seconds to 10 seconds --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d7b3585b..3461cb83 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "develop": "node dist/scripts/develop.js", "start": "ts-node-dev --inspect=8091 src/scripts/serve.ts", - "test": "mocha -r ts-node/register src/*.test.ts src/**/*.test.ts src/**/**/*.test.ts", + "test": "mocha --timeout 10000 -r ts-node/register src/*.test.ts src/**/*.test.ts src/**/**/*.test.ts", "lint": "eslint \"**/*.{js,ts}\"", "lint:fix": "eslint \"**/*.{js,ts}\" --quiet --fix", "prettier": "prettier --check \"**/*.{js,ts}\"", From 9a745dbc6bba02f4fff1f8f99286796ada81a971 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Tue, 4 Apr 2023 10:37:56 -0400 Subject: [PATCH 15/27] Moving test to thier own directory --- package.json | 2 +- {src/lib/__tests__ => test}/fixtures/library.json | 0 {src/lib/__tests__ => test}/fixtures/questionnaire.json | 0 {src/lib/__tests__ => test}/fixtures/valueSet.json | 0 {src/hooks => test}/rems.hook.test.ts | 4 ++-- {src => test}/server.test.ts | 4 ++-- {src/lib/__tests__ => test}/vsac_cache.test.ts | 5 ++--- {src/lib => test}/winston.test.ts | 8 ++++---- 8 files changed, 11 insertions(+), 12 deletions(-) rename {src/lib/__tests__ => test}/fixtures/library.json (100%) rename {src/lib/__tests__ => test}/fixtures/questionnaire.json (100%) rename {src/lib/__tests__ => test}/fixtures/valueSet.json (100%) rename {src/hooks => test}/rems.hook.test.ts (87%) rename {src => test}/server.test.ts (97%) rename {src/lib/__tests__ => test}/vsac_cache.test.ts (97%) rename {src/lib => test}/winston.test.ts (86%) diff --git a/package.json b/package.json index 3461cb83..0aaeaa2a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "develop": "node dist/scripts/develop.js", "start": "ts-node-dev --inspect=8091 src/scripts/serve.ts", - "test": "mocha --timeout 10000 -r ts-node/register src/*.test.ts src/**/*.test.ts src/**/**/*.test.ts", + "test": "mocha --timeout 10000 -r ts-node/register test/*.test.ts", "lint": "eslint \"**/*.{js,ts}\"", "lint:fix": "eslint \"**/*.{js,ts}\" --quiet --fix", "prettier": "prettier --check \"**/*.{js,ts}\"", diff --git a/src/lib/__tests__/fixtures/library.json b/test/fixtures/library.json similarity index 100% rename from src/lib/__tests__/fixtures/library.json rename to test/fixtures/library.json diff --git a/src/lib/__tests__/fixtures/questionnaire.json b/test/fixtures/questionnaire.json similarity index 100% rename from src/lib/__tests__/fixtures/questionnaire.json rename to test/fixtures/questionnaire.json diff --git a/src/lib/__tests__/fixtures/valueSet.json b/test/fixtures/valueSet.json similarity index 100% rename from src/lib/__tests__/fixtures/valueSet.json rename to test/fixtures/valueSet.json diff --git a/src/hooks/rems.hook.test.ts b/test/rems.hook.test.ts similarity index 87% rename from src/hooks/rems.hook.test.ts rename to test/rems.hook.test.ts index 60dd15dd..55d3fe07 100644 --- a/src/hooks/rems.hook.test.ts +++ b/test/rems.hook.test.ts @@ -1,5 +1,5 @@ -import OrderSign from './OrderSign'; -import getREMSHook from './rems.hook'; +import OrderSign from '../src/hooks/OrderSign'; +import getREMSHook from '../src/hooks/rems.hook'; import { expect } from 'chai'; describe('hook: test rems', () => { diff --git a/src/server.test.ts b/test/server.test.ts similarity index 97% rename from src/server.test.ts rename to test/server.test.ts index 1b58074e..e31e7e30 100644 --- a/src/server.test.ts +++ b/test/server.test.ts @@ -1,5 +1,5 @@ -import { initialize, REMSServer } from './server'; -import config from './config'; +import { initialize, REMSServer } from '../src/server'; +import config from '../src/config'; import { Db, MongoClient } from 'mongodb'; import sinon from 'sinon'; import { expect } from 'chai'; diff --git a/src/lib/__tests__/vsac_cache.test.ts b/test/vsac_cache.test.ts similarity index 97% rename from src/lib/__tests__/vsac_cache.test.ts rename to test/vsac_cache.test.ts index b74bd3eb..99418d6a 100644 --- a/src/lib/__tests__/vsac_cache.test.ts +++ b/test/vsac_cache.test.ts @@ -1,10 +1,9 @@ -import VsacCache from '../vsac_cache'; +import VsacCache from '../src/lib/vsac_cache'; import library from './fixtures/library.json'; import questionnaire from './fixtures/questionnaire.json'; import valueSet from './fixtures/valueSet.json'; import nock from 'nock'; -import constants from '../../constants'; -import ValueSetModel from '../schemas/resources/ValueSet'; +import ValueSetModel from '../src/lib/schemas/resources/ValueSet'; import mongoose, { ConnectOptions } from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; import { assert, expect } from 'chai'; diff --git a/src/lib/winston.test.ts b/test/winston.test.ts similarity index 86% rename from src/lib/winston.test.ts rename to test/winston.test.ts index 87600480..c770d4c1 100644 --- a/src/lib/winston.test.ts +++ b/test/winston.test.ts @@ -4,8 +4,8 @@ * environment based configuration that changes between runs. We need this * for full coverage */ -import container from './winston'; -import config from '../config'; +import container from '../src/lib/winston'; +import config from '../src/config'; import { expect } from 'chai'; describe('Logger Class', () => { @@ -25,8 +25,8 @@ describe('Logger Class', () => { // directory: 'logs' // } // })); - const containerPromise = import('./winston'); - const configPromise = import('../config'); + const containerPromise = import('../src/lib/winston'); + const configPromise = import('../src/config'); containerPromise.then(container => { configPromise.then(config => { const logger = container.default.get('application'); From 93820b6c99ba7906b64194adff5c859e4997de35 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Tue, 4 Apr 2023 11:12:22 -0400 Subject: [PATCH 16/27] adding inspection of spied calls --- .eslintrc | 4 +++- package.json | 7 ++++--- test/server.test.ts | 34 ++++++---------------------------- 3 files changed, 13 insertions(+), 32 deletions(-) diff --git a/.eslintrc b/.eslintrc index bbb67807..dccaa743 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,8 @@ "plugin:@typescript-eslint/recommended" ], "plugins": [ - "@typescript-eslint" + "@typescript-eslint", + "prettier" ], "parser": "@typescript-eslint/parser", "parserOptions": { @@ -13,6 +14,7 @@ "sourceType": "module" }, "rules": { + "prettier/prettier": ["error"], "semi": ["off"], "@typescript-eslint/semi": ["error", "always"], "quotes": ["error", "single", { "avoidEscape": true }], diff --git a/package.json b/package.json index 0aaeaa2a..036d2f5d 100644 --- a/package.json +++ b/package.json @@ -52,19 +52,20 @@ "@types/express": "^4.17.14", "@types/lodash": "^4.14.188", "@types/mocha": "^10.0.1", - "@types/mongoose": "^5.11.97", "@types/mongodb-memory-server": "2.3.0", + "@types/mongoose": "^5.11.97", "@types/morgan": "^1.9.3", "@types/node": "^18.11.9", "@types/nodemon": "^1.19.2", - "@types/sinon" : "10.0.13", + "@types/sinon": "10.0.13", "@types/uuid": "^9.0.0", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "axios-mock-adapter": "^1.21.2", "chai": "^4.3.7", "eslint": "^8.28.0", - "eslint-config-prettier": "^6.10.1", + "eslint-config-prettier": "^6.15.0", + "eslint-plugin-prettier": "^4.2.1", "json-diff": "^0.9.0", "mocha": "^10.2.0", "mongodb-memory-server": "^8.12.1", diff --git a/test/server.test.ts b/test/server.test.ts index e31e7e30..ef2df2c8 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -28,27 +28,6 @@ describe('REMSServer class', () => { }); beforeEach(() => { - // jest.mock('morgan', () => jest.fn()); - - // // Mock express and body parser - // jest.mock('body-parser', () => ({ - // urlencoded: jest.fn(), - // json: jest.fn() - // })); - - // jest.mock('express', () => { - // const mock = jest.fn(() => ({ - // use: jest.fn(), - // set: jest.fn(), - // get: jest.fn(), - // listen: jest.fn(), - // options: jest.fn(), - // post: jest.fn(), - // static: jest.fn() - // })); - // return mock; - // }); - server = new REMSServer(config.fhirServerConfig); }); @@ -63,12 +42,11 @@ describe('REMSServer class', () => { const use = sinon.spy(server.app, 'use'); server.configureMiddleware(); - expect(set.callCount).to.equal(6); - // expect(set.mock.calls[0][0]).toBe('showStackError'); - // expect(set.mock.calls[0][1]).toBe(true); - // expect(set.mock.calls[5][0]).toBe('jsonp callback'); - // expect(set.mock.calls[5][1]).toBe(true); + expect(set.getCall(4).args[0]).to.equal('showStackError'); + expect(set.getCall(4).args[1]).to.be.true; + expect(set.getCall(5).args[0]).to.equal('jsonp callback'); + expect(set.getCall(5).args[1]).to.be.true; expect(use.callCount).to.equal(8); }); @@ -112,8 +90,8 @@ describe('REMSServer class', () => { // Start listening on a port and pass the callback through const serverListen = server.listen({ port: 3000 }, callback); expect(listen.calledOnce).to.be.true; - // expect(listen.mock.calls[0][0]).toBe(3000); - // expect(listen.mock.calls[0][1]).toBe(callback); + expect(listen.getCall(0).args[0]).to.equal(3000); + expect(listen.getCall(0).args[1]).to.equal(callback); serverListen.close(); }); From a7ed9e5ff15fa7dd28b8ee66800dde488e388c9a Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Wed, 5 Apr 2023 14:13:42 -0400 Subject: [PATCH 17/27] moving etasu endpoints into their own middleware component --- src/lib/etasu.ts | 358 +++++++++++++++++++++++++++++++++++++++++++++++ src/server.ts | 353 +--------------------------------------------- 2 files changed, 360 insertions(+), 351 deletions(-) create mode 100644 src/lib/etasu.ts diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts new file mode 100644 index 00000000..5ff3adb5 --- /dev/null +++ b/src/lib/etasu.ts @@ -0,0 +1,358 @@ +import { Router, Response, Request } from 'express'; +import { Globals } from '../globals'; +import { FhirUtilities } from '../fhir/utilities'; +import { + medicationCollection, + metRequirementsCollection, + remsCaseCollection +} from '../fhir/models'; +import { uid } from 'uid'; +const router = Router(); + +const db = Globals.database; + +// const medicationCollection = db.collection('medication-requirements'); +// const metRequirementsCollection = db.collection('met-requirements'); +// const remsCaseCollection = db.collection('rems-case'); + +// etasu endpoints +router.get('/:drug', (req: Request, res: Response) => { + medicationCollection.findOne({ name: req.params.drug }, (err: any, drug: any) => { + if (err) throw err; + res.send(drug); + }); +}); + +router.get('/met/:caseId', (req: Request, res: Response) => { + remsCaseCollection.findOne({ case_number: req.params.caseId }, (err: any, remsCase: any) => { + if (err) throw err; + res.send(remsCase); + }); +}); + +router.get( + '/met/patient/:patientFirstName/:patientLastName/:patientDOB/drug/:drugName', + (req: Request, res: Response) => { + const searchDict = { + patientFirstName: req.params.patientFirstName, + patientLastName: req.params.patientLastName, + patientDOB: req.params.patientDOB, + drugName: req.params.drugName + }; + + remsCaseCollection.findOne(searchDict, (err: any, remsCase: any) => { + if (err) throw err; + res.send(remsCase); + }); + } +); + +router.post('/reset', async (req: Request, res: Response) => { + console.log('Dropping collections'); + await medicationCollection.deleteMany({}); + await remsCaseCollection.deleteMany({}); + await metRequirementsCollection.deleteMany({}); + console.log('Resetting the database'); + await FhirUtilities.populateDB(); + res.send('reset etasu database collections'); +}); + +router.post('/met', async (req: Request, res: Response) => { + try { + let returnedRemsRequestDoc: any; + let returnedMetReqDoc: any; + let returnRemsRequest = false; + const requestBody = req.body; + + // extract params and questionnaire response identifier + const params = getResource( + requestBody, + requestBody.entry[0].resource.focus.parameters.reference + ); + const questionnaireResponse = getQuestionnaireResponse(requestBody); + const questionnaireStringArray = questionnaireResponse.questionnaire.split('/'); + const requirementId = questionnaireStringArray[questionnaireStringArray.length - 1]; + + // stakeholder and medication references + let prescriptionReference = ''; + let practitionerReference = ''; + let pharmacistReference = ''; + let patientReference = ''; + for (const param of params.parameter) { + if (param.name === 'prescription') { + prescriptionReference = param.reference; + } else if (param.name === 'prescriber') { + practitionerReference = param.reference; + } else if (param.name === 'pharmacy') { + pharmacistReference = param.reference; + } else if (param.name === 'source-patient') { + patientReference = param.reference; + } + } + + // obtain drug information from database + const presciption = getResource(requestBody, prescriptionReference); + const prescriptionSystem = presciption.medicationCodeableConcept.coding[0].system; + const prescriptionCode = presciption.medicationCodeableConcept.coding[0].code; + const patient = getResource(requestBody, patientReference); + const patientFirstName = patient.name[0].given[0]; + const patientLastName = patient.name[0].family; + const patientDOB = patient.birthDate; + + const drug = await medicationCollection + .findOne({ + code: prescriptionCode, + codeSystem: prescriptionSystem + }) + .exec(); + // iterate through each requirement of the drug + if (drug) { + for (const requirement of drug.requirements) { + // figure out which stakeholder the req corresponds to + const reqStakeholder = requirement.stakeholderType; + const reqStakeholderReference = + reqStakeholder === 'prescriber' + ? practitionerReference + : reqStakeholder === 'pharmacist' + ? pharmacistReference + : patientReference; + + // if the requirement is the one submitted continue + if (requirement.resourceId === requirementId) { + // if the req submitted is a patient enrollment form and requires creating a new case + if (requirement.createNewCase) { + returnRemsRequest = true; + const case_number = uid(); + + // create new rems request and add the created metReq to it + let remsRequestCompletedStatus = 'Approved'; + const remsRequest: any = { + case_number: case_number, + status: remsRequestCompletedStatus, + drugName: drug?.name, + drugCode: prescriptionCode, + patientFirstName: patientFirstName, + patientLastName: patientLastName, + patientDOB: patientDOB, + metRequirements: [] + }; + returnRemsRequest = true; + + // create the metReq that was submitted + const metReq = { + completed: true, + completedQuestionnaire: questionnaireResponse, + requirementName: requirement.name, + requirementDescription: requirement.description, + drugName: drug?.name, + stakeholderId: reqStakeholderReference, + case_numbers: [case_number] + }; + + try { + const matchedMetReq = await metRequirementsCollection.create(metReq); + remsRequest.metRequirements.push({ + stakeholderId: matchedMetReq?.stakeholderId, + completed: matchedMetReq?.completed, + metRequirementId: matchedMetReq?._id, + requirementName: matchedMetReq?.requirementName, + requirementDescription: matchedMetReq?.requirementDescription + }); + } catch { + console.log('create error'); + } + + // iterate through all other reqs again to create corresponding false metReqs / assign to existing + if (drug) { + for (const requirement2 of drug.requirements) { + // skip if the req found is the same as in the outer loop and has already been processed + if (!(requirement2.resourceId === requirementId)) { + // figure out which stakeholder the req corresponds to + const reqStakeholder2 = requirement2.stakeholderType; + const reqStakeholder2Reference = + reqStakeholder2 === 'prescriber' + ? practitionerReference + : reqStakeholder2 === 'pharmacist' + ? pharmacistReference + : patientReference; + + const matchedMetReq2 = await metRequirementsCollection + .findOne({ + stakeholderId: reqStakeholder2Reference, + requirementName: requirement2.name, + drugName: drug?.name + }) + .exec(); + if (matchedMetReq2) { + remsRequest.metRequirements.push({ + stakeholderId: matchedMetReq2.stakeholderId, + completed: matchedMetReq2.completed, + metRequirementId: matchedMetReq2._id, + requirementName: matchedMetReq2.requirementName, + requirementDescription: matchedMetReq2.requirementDescription + }); + if (!matchedMetReq2.completed) { + remsRequestCompletedStatus = 'Pending'; + } + matchedMetReq2.case_numbers.push(case_number); + await matchedMetReq2.save(); + // await metRequirementsCollection.findByIdAndUpdate(matchedMetReq2, { + // $addToSet: { case_numbers: case_number } + // }); + } else { + // create the metReq that was submitted + const newMetReq = { + completed: false, + completedQuestionnaire: null, + requirementName: requirement2.name, + requirementDescription: requirement2.description, + drugName: drug?.name, + stakeholderId: reqStakeholder2Reference, + case_numbers: [case_number] + }; + + remsRequestCompletedStatus = 'Pending'; + + // TODO: make this check more robust + try { + const newMetReqDoc = await metRequirementsCollection.create(newMetReq); + remsRequest.metRequirements.push({ + stakeholderId: newMetReqDoc?.stakeholderId, + completed: newMetReqDoc?.completed, + metRequirementId: newMetReqDoc?._id, + requirementName: newMetReqDoc?.requirementName, + requirementDescription: newMetReqDoc?.requirementDescription + }); + } catch { + console.log('create error'); + } + } + } + } + } + + remsRequest.status = remsRequestCompletedStatus; + returnedRemsRequestDoc = await remsCaseCollection.create(remsRequest); + } else { + const matchedMetReq3 = await metRequirementsCollection + .findOne({ + stakeholderId: reqStakeholderReference, + requirementName: requirement.name, + drugName: drug?.name + }) + .exec(); + if (matchedMetReq3) { + if (!matchedMetReq3.completed) { + matchedMetReq3.completed = true; + matchedMetReq3.completedQuestionnaire = questionnaireResponse; + await matchedMetReq3.save(); + // await metRequirementsCollection.findByIdAndUpdate(matchedMetReq3, { + // $set: { completed: true, completedQuestionnaire: questionnaireResponse } + // }); + + returnedMetReqDoc = await metRequirementsCollection + .findOne({ + _id: matchedMetReq3._id + }) + .exec(); + + // this should be an array returned via .find() - tried using $in but could not get it to work - using the first element for now as a work around since we only have one patient + const remsRequestToUpdate = await remsCaseCollection + .findOne({ + case_number: returnedMetReqDoc.case_numbers[0] + }) + .exec(); + + // ToDO: iterate over multiple remsRequests - right now there will only be one that matches, but with multiple patients in the system there could be more + + // for (let remsRequestToUpdate of remsRequestsToUpdate) { + let foundUncompleted = false; + const metReqArray = remsRequestToUpdate?.metRequirements; + for (let i = 0; i < remsRequestToUpdate?.metRequirements.length; i++) { + const req4 = remsRequestToUpdate?.metRequirements[i]; + // _id comparison would not work for some reason + if (req4.requirementName === matchedMetReq3.requirementName) { + metReqArray[i].completed = true; + req4.completed = true; + const update = await remsCaseCollection.updateOne( + { _id: remsRequestToUpdate?._id }, + { $set: { metRequirements: metReqArray } } + ); + } + if (!req4.completed) { + foundUncompleted = true; + } + } + + if (!foundUncompleted && remsRequestToUpdate?.status === 'Pending') { + remsRequestToUpdate.status = 'Approved'; + await remsRequestToUpdate.save(); + // await remsCaseCollection.findByIdAndUpdate(remsRequestToUpdate, { + // $set: { status: 'Approved' } + // }); + } + + // } + } + } else { + // create the metReq that was submitted + const newMetReq3 = { + completed: true, + completedQuestionnaire: questionnaireResponse, + requirementName: requirement.name, + requirementDescription: requirement.requirementDescription, + drugName: drug?.name, + stakeholderId: reqStakeholderReference, + case_numbers: [] + }; + + returnedMetReqDoc = await metRequirementsCollection.create(newMetReq3); + } + } + break; + } + } + } + + // return MetReq unless a new case is created in which case return the Rems request + if (returnRemsRequest) { + res.status(201); + res.send(returnedRemsRequestDoc); + } else { + res.status(201); + res.send(returnedMetReqDoc); + } + } catch (error) { + console.log(error); + throw error; + } +}); + +let getResource = (bundle: { entry: any[] }, resourceReference: string) => { + const temp = resourceReference.split('/'); + const _resourceType = temp[0]; + const _id = temp[1]; + + for (let i = 0; i < bundle.entry.length; i++) { + if ( + bundle.entry[i].resource.resourceType === _resourceType && + bundle.entry[i].resource.id === _id + ) { + return bundle.entry[i].resource; + } + } + return null; +}; + +let getQuestionnaireResponse = (bundle: { entry: any[] }) => { + const _resourceType = 'QuestionnaireResponse'; + + for (let i = 0; i < bundle.entry.length; i++) { + if (bundle.entry[i].resource.resourceType === _resourceType) { + return bundle.entry[i].resource; + } + } + return null; +}; + +export default router; diff --git a/src/server.ts b/src/server.ts index 950d1159..8cbf2417 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,11 +5,7 @@ import morgan from 'morgan'; import Hook from './hooks/Hook'; import remsService from './hooks/rems.hook'; import { Server } from '@projecttacoma/node-fhir-server-core'; -import { Globals } from './globals'; -import { uid } from 'uid'; -import { FhirUtilities } from './fhir/utilities'; -import { medicationCollection, metRequirementsCollection, remsCaseCollection } from './fhir/models'; -import { Request, Response, NextFunction } from 'express'; +import Etasu from "./lib/etasu"; const logger = container.get('application'); @@ -105,355 +101,10 @@ class REMSServer extends Server { } configureEtasuEndpoints() { - const db = Globals.database; - - // const medicationCollection = db.collection('medication-requirements'); - // const metRequirementsCollection = db.collection('met-requirements'); - // const remsCaseCollection = db.collection('rems-case'); - - // etasu endpoints - this.app.get('/etasu/:drug', (req: Request, res: Response) => { - medicationCollection.findOne({ name: req.params.drug }, (err: any, drug: any) => { - if (err) throw err; - res.send(drug); - }); - }); - - this.app.get('/etasu/met/:caseId', (req: Request, res: Response) => { - remsCaseCollection.findOne({ case_number: req.params.caseId }, (err: any, remsCase: any) => { - if (err) throw err; - res.send(remsCase); - }); - }); - - this.app.get( - '/etasu/met/patient/:patientFirstName/:patientLastName/:patientDOB/drug/:drugName', - (req: Request, res: Response) => { - const searchDict = { - patientFirstName: req.params.patientFirstName, - patientLastName: req.params.patientLastName, - patientDOB: req.params.patientDOB, - drugName: req.params.drugName - }; - - remsCaseCollection.findOne(searchDict, (err: any, remsCase: any) => { - if (err) throw err; - res.send(remsCase); - }); - } - ); - - this.app.post('/etasu/reset', async (req: Request, res: Response) => { - console.log('Dropping collections'); - await medicationCollection.deleteMany({}); - await remsCaseCollection.deleteMany({}); - await metRequirementsCollection.deleteMany({}); - console.log('Resetting the database'); - await FhirUtilities.populateDB(); - res.send('reset etasu database collections'); - }); - - this.app.post('/etasu/met', async (req: Request, res: Response) => { - try { - let returnedRemsRequestDoc: any; - let returnedMetReqDoc: any; - let returnRemsRequest = false; - const requestBody = req.body; - - // extract params and questionnaire response identifier - const params = this.getResource( - requestBody, - requestBody.entry[0].resource.focus.parameters.reference - ); - const questionnaireResponse = this.getQuestionnaireResponse(requestBody); - const questionnaireStringArray = questionnaireResponse.questionnaire.split('/'); - const requirementId = questionnaireStringArray[questionnaireStringArray.length - 1]; - - // stakeholder and medication references - let prescriptionReference = ''; - let practitionerReference = ''; - let pharmacistReference = ''; - let patientReference = ''; - for (const param of params.parameter) { - if (param.name === 'prescription') { - prescriptionReference = param.reference; - } else if (param.name === 'prescriber') { - practitionerReference = param.reference; - } else if (param.name === 'pharmacy') { - pharmacistReference = param.reference; - } else if (param.name === 'source-patient') { - patientReference = param.reference; - } - } - - // obtain drug information from database - const presciption = this.getResource(requestBody, prescriptionReference); - const prescriptionSystem = presciption.medicationCodeableConcept.coding[0].system; - const prescriptionCode = presciption.medicationCodeableConcept.coding[0].code; - const patient = this.getResource(requestBody, patientReference); - const patientFirstName = patient.name[0].given[0]; - const patientLastName = patient.name[0].family; - const patientDOB = patient.birthDate; - - const drug = await medicationCollection - .findOne({ - code: prescriptionCode, - codeSystem: prescriptionSystem - }) - .exec(); - // iterate through each requirement of the drug - if (drug) { - for (const requirement of drug.requirements) { - // figure out which stakeholder the req corresponds to - const reqStakeholder = requirement.stakeholderType; - const reqStakeholderReference = - reqStakeholder === 'prescriber' - ? practitionerReference - : reqStakeholder === 'pharmacist' - ? pharmacistReference - : patientReference; - - // if the requirement is the one submitted continue - if (requirement.resourceId === requirementId) { - // if the req submitted is a patient enrollment form and requires creating a new case - if (requirement.createNewCase) { - returnRemsRequest = true; - const case_number = uid(); - - // create new rems request and add the created metReq to it - let remsRequestCompletedStatus = 'Approved'; - const remsRequest: any = { - case_number: case_number, - status: remsRequestCompletedStatus, - drugName: drug?.name, - drugCode: prescriptionCode, - patientFirstName: patientFirstName, - patientLastName: patientLastName, - patientDOB: patientDOB, - metRequirements: [] - }; - returnRemsRequest = true; - - // create the metReq that was submitted - const metReq = { - completed: true, - completedQuestionnaire: questionnaireResponse, - requirementName: requirement.name, - requirementDescription: requirement.description, - drugName: drug?.name, - stakeholderId: reqStakeholderReference, - case_numbers: [case_number] - }; - - try { - const matchedMetReq = await metRequirementsCollection.create(metReq); - remsRequest.metRequirements.push({ - stakeholderId: matchedMetReq?.stakeholderId, - completed: matchedMetReq?.completed, - metRequirementId: matchedMetReq?._id, - requirementName: matchedMetReq?.requirementName, - requirementDescription: matchedMetReq?.requirementDescription - }); - } catch { - console.log('create error'); - } - - // iterate through all other reqs again to create corresponding false metReqs / assign to existing - if (drug) { - for (const requirement2 of drug.requirements) { - // skip if the req found is the same as in the outer loop and has already been processed - if (!(requirement2.resourceId === requirementId)) { - // figure out which stakeholder the req corresponds to - const reqStakeholder2 = requirement2.stakeholderType; - const reqStakeholder2Reference = - reqStakeholder2 === 'prescriber' - ? practitionerReference - : reqStakeholder2 === 'pharmacist' - ? pharmacistReference - : patientReference; - - const matchedMetReq2 = await metRequirementsCollection - .findOne({ - stakeholderId: reqStakeholder2Reference, - requirementName: requirement2.name, - drugName: drug?.name - }) - .exec(); - if (matchedMetReq2) { - remsRequest.metRequirements.push({ - stakeholderId: matchedMetReq2.stakeholderId, - completed: matchedMetReq2.completed, - metRequirementId: matchedMetReq2._id, - requirementName: matchedMetReq2.requirementName, - requirementDescription: matchedMetReq2.requirementDescription - }); - if (!matchedMetReq2.completed) { - remsRequestCompletedStatus = 'Pending'; - } - matchedMetReq2.case_numbers.push(case_number); - await matchedMetReq2.save(); - // await metRequirementsCollection.findByIdAndUpdate(matchedMetReq2, { - // $addToSet: { case_numbers: case_number } - // }); - } else { - // create the metReq that was submitted - const newMetReq = { - completed: false, - completedQuestionnaire: null, - requirementName: requirement2.name, - requirementDescription: requirement2.description, - drugName: drug?.name, - stakeholderId: reqStakeholder2Reference, - case_numbers: [case_number] - }; - - remsRequestCompletedStatus = 'Pending'; - - // TODO: make this check more robust - try { - const newMetReqDoc = await metRequirementsCollection.create(newMetReq); - remsRequest.metRequirements.push({ - stakeholderId: newMetReqDoc?.stakeholderId, - completed: newMetReqDoc?.completed, - metRequirementId: newMetReqDoc?._id, - requirementName: newMetReqDoc?.requirementName, - requirementDescription: newMetReqDoc?.requirementDescription - }); - } catch { - console.log('create error'); - } - } - } - } - } - - remsRequest.status = remsRequestCompletedStatus; - returnedRemsRequestDoc = await remsCaseCollection.create(remsRequest); - } else { - const matchedMetReq3 = await metRequirementsCollection - .findOne({ - stakeholderId: reqStakeholderReference, - requirementName: requirement.name, - drugName: drug?.name - }) - .exec(); - if (matchedMetReq3) { - if (!matchedMetReq3.completed) { - matchedMetReq3.completed = true; - matchedMetReq3.completedQuestionnaire = questionnaireResponse; - await matchedMetReq3.save(); - // await metRequirementsCollection.findByIdAndUpdate(matchedMetReq3, { - // $set: { completed: true, completedQuestionnaire: questionnaireResponse } - // }); - - returnedMetReqDoc = await metRequirementsCollection - .findOne({ - _id: matchedMetReq3._id - }) - .exec(); - - // this should be an array returned via .find() - tried using $in but could not get it to work - using the first element for now as a work around since we only have one patient - const remsRequestToUpdate = await remsCaseCollection - .findOne({ - case_number: returnedMetReqDoc.case_numbers[0] - }) - .exec(); - - // ToDO: iterate over multiple remsRequests - right now there will only be one that matches, but with multiple patients in the system there could be more - - // for (let remsRequestToUpdate of remsRequestsToUpdate) { - let foundUncompleted = false; - const metReqArray = remsRequestToUpdate?.metRequirements; - for (let i = 0; i < remsRequestToUpdate?.metRequirements.length; i++) { - const req4 = remsRequestToUpdate?.metRequirements[i]; - // _id comparison would not work for some reason - if (req4.requirementName === matchedMetReq3.requirementName) { - metReqArray[i].completed = true; - req4.completed = true; - const update = await remsCaseCollection.updateOne( - { _id: remsRequestToUpdate?._id }, - { $set: { metRequirements: metReqArray } } - ); - } - if (!req4.completed) { - foundUncompleted = true; - } - } - - if (!foundUncompleted && remsRequestToUpdate?.status === 'Pending') { - remsRequestToUpdate.status = 'Approved'; - await remsRequestToUpdate.save(); - // await remsCaseCollection.findByIdAndUpdate(remsRequestToUpdate, { - // $set: { status: 'Approved' } - // }); - } - - // } - } - } else { - // create the metReq that was submitted - const newMetReq3 = { - completed: true, - completedQuestionnaire: questionnaireResponse, - requirementName: requirement.name, - requirementDescription: requirement.requirementDescription, - drugName: drug?.name, - stakeholderId: reqStakeholderReference, - case_numbers: [] - }; - - returnedMetReqDoc = await metRequirementsCollection.create(newMetReq3); - } - } - break; - } - } - } - - // return MetReq unless a new case is created in which case return the Rems request - if (returnRemsRequest) { - res.status(201); - res.send(returnedRemsRequestDoc); - } else { - res.status(201); - res.send(returnedMetReqDoc); - } - } catch (error) { - console.log(error); - throw error; - } - }); - + this.app.use("/etasu",Etasu); return this; } - getResource(bundle: { entry: any[] }, resourceReference: string) { - const temp = resourceReference.split('/'); - const _resourceType = temp[0]; - const _id = temp[1]; - - for (let i = 0; i < bundle.entry.length; i++) { - if ( - bundle.entry[i].resource.resourceType === _resourceType && - bundle.entry[i].resource.id === _id - ) { - return bundle.entry[i].resource; - } - } - return null; - } - - getQuestionnaireResponse(bundle: { entry: any[] }) { - const _resourceType = 'QuestionnaireResponse'; - - for (let i = 0; i < bundle.entry.length; i++) { - if (bundle.entry[i].resource.resourceType === _resourceType) { - return bundle.entry[i].resource; - } - } - return null; - } - /** * @method listen * @description Start listening on the configured port From 8e3ad684d98cac78107125281826e7ee365f34d7 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 6 Apr 2023 10:05:46 -0400 Subject: [PATCH 18/27] fixing linting issues --- src/lib/etasu.ts | 4 ++-- src/server.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 5ff3adb5..b23a70ea 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -328,7 +328,7 @@ router.post('/met', async (req: Request, res: Response) => { } }); -let getResource = (bundle: { entry: any[] }, resourceReference: string) => { +const getResource = (bundle: { entry: any[] }, resourceReference: string) => { const temp = resourceReference.split('/'); const _resourceType = temp[0]; const _id = temp[1]; @@ -344,7 +344,7 @@ let getResource = (bundle: { entry: any[] }, resourceReference: string) => { return null; }; -let getQuestionnaireResponse = (bundle: { entry: any[] }) => { +const getQuestionnaireResponse = (bundle: { entry: any[] }) => { const _resourceType = 'QuestionnaireResponse'; for (let i = 0; i < bundle.entry.length; i++) { diff --git a/src/server.ts b/src/server.ts index 8cbf2417..132dc2a7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,7 +5,7 @@ import morgan from 'morgan'; import Hook from './hooks/Hook'; import remsService from './hooks/rems.hook'; import { Server } from '@projecttacoma/node-fhir-server-core'; -import Etasu from "./lib/etasu"; +import Etasu from './lib/etasu'; const logger = container.get('application'); @@ -101,7 +101,7 @@ class REMSServer extends Server { } configureEtasuEndpoints() { - this.app.use("/etasu",Etasu); + this.app.use('/etasu', Etasu); return this; } From b337de7a9d4181a17daad8a447a4b81dde735d96 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 6 Apr 2023 11:13:37 -0400 Subject: [PATCH 19/27] addressing linting errors, removed unused variables, added lint rule for use of any, updated lint rules to allow for unused variables prepended with _ --- .eslintrc | 3 ++- src/fhir/questionnaireUtilities.ts | 2 -- src/fhir/utilities.ts | 8 +----- src/lib/MongoDatabase.ts | 1 - src/lib/etasu.ts | 5 +--- src/lib/schemas/models/DataRequirement.ts | 1 - .../models/DataRequirement_CodeFilter.ts | 1 - src/lib/schemas/models/ParameterDefinition.ts | 1 - src/lib/schemas/models/Questionnaire_Item.ts | 2 -- src/lib/schemas/models/Reference.ts | 2 +- src/lib/schemas/models/RelatedArtifact.ts | 1 - src/lib/schemas/models/ValueSet_Include.ts | 1 - src/lib/schemas/resources/Questionnaire.ts | 1 - .../resources/QuestionnaireResponse.ts | 1 - src/lib/vsac_cache.ts | 3 ++- src/services/library.service.ts | 2 -- src/services/patient.service.ts | 2 -- src/services/questionnaire.service.ts | 2 -- src/services/questionnaireresponse.service.ts | 26 +++++++++---------- test/server.test.ts | 4 --- test/vsac_cache.test.ts | 9 +++---- 21 files changed, 22 insertions(+), 56 deletions(-) diff --git a/.eslintrc b/.eslintrc index dccaa743..a9245319 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,6 +18,7 @@ "semi": ["off"], "@typescript-eslint/semi": ["error", "always"], "quotes": ["error", "single", { "avoidEscape": true }], - + "@typescript-eslint/no-explicit-any": ["off"], + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] } } \ No newline at end of file diff --git a/src/fhir/questionnaireUtilities.ts b/src/fhir/questionnaireUtilities.ts index 15d23d2d..3484469a 100644 --- a/src/fhir/questionnaireUtilities.ts +++ b/src/fhir/questionnaireUtilities.ts @@ -10,8 +10,6 @@ import { QuestionnaireItem, ValueSet } from 'fhir/r4'; -import constants from '../constants'; -import { Globals } from '../globals'; import { FhirUtilities } from './utilities'; import container from '../lib/winston'; import config from '../config'; diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 16fea019..5e5f2fe1 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -1,16 +1,12 @@ import { resolveSchema } from '@projecttacoma/node-fhir-server-core'; import * as moment from 'moment'; import 'moment-timezone'; -import { v1 as uuidv1 } from 'uuid'; - -import constants from '../constants'; -import { Globals } from '../globals'; import * as path from 'path'; import * as fs from 'fs'; import * as process from 'process'; import crypto from 'crypto'; import { QuestionnaireUtilities } from './questionnaireUtilities'; -import { FhirResource, Library, Patient, Questionnaire, Resource } from 'fhir/r4'; +import { FhirResource } from 'fhir/r4'; import LibraryModel from '../lib/schemas/resources/Library'; import PatientModel from '../lib/schemas/resources/Patient'; import QuestionnaireModel from '../lib/schemas/resources/Questionnaire'; @@ -53,8 +49,6 @@ export class FhirUtilities { reject: any, baseVersion = '4_0_0' ) { - const db = Globals.database; - // If no resource ID was provided, generate one. let id = ''; if (!resource.id) { diff --git a/src/lib/MongoDatabase.ts b/src/lib/MongoDatabase.ts index 27661738..0d1b1658 100644 --- a/src/lib/MongoDatabase.ts +++ b/src/lib/MongoDatabase.ts @@ -1,5 +1,4 @@ import { Database } from './Database'; -import * as mongoDB from 'mongodb'; import mongoose from 'mongoose'; export class MongoDatabase extends Database { options: any; diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index b23a70ea..d1321279 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -1,5 +1,4 @@ import { Router, Response, Request } from 'express'; -import { Globals } from '../globals'; import { FhirUtilities } from '../fhir/utilities'; import { medicationCollection, @@ -9,8 +8,6 @@ import { import { uid } from 'uid'; const router = Router(); -const db = Globals.database; - // const medicationCollection = db.collection('medication-requirements'); // const metRequirementsCollection = db.collection('met-requirements'); // const remsCaseCollection = db.collection('rems-case'); @@ -274,7 +271,7 @@ router.post('/met', async (req: Request, res: Response) => { if (req4.requirementName === matchedMetReq3.requirementName) { metReqArray[i].completed = true; req4.completed = true; - const update = await remsCaseCollection.updateOne( + await remsCaseCollection.updateOne( { _id: remsRequestToUpdate?._id }, { $set: { metRequirements: metReqArray } } ); diff --git a/src/lib/schemas/models/DataRequirement.ts b/src/lib/schemas/models/DataRequirement.ts index 6c3d49ac..497e6b15 100644 --- a/src/lib/schemas/models/DataRequirement.ts +++ b/src/lib/schemas/models/DataRequirement.ts @@ -1,6 +1,5 @@ import mongoose from 'mongoose'; import { DataRequirement } from 'fhir/r4'; -import canonical from './canonical'; import CodeableConcept from './CodeableConcept'; import Reference from './Reference'; import DataRequirement_CodeFilter from './DataRequirement_CodeFilter'; diff --git a/src/lib/schemas/models/DataRequirement_CodeFilter.ts b/src/lib/schemas/models/DataRequirement_CodeFilter.ts index cc905eb1..dedac5b8 100644 --- a/src/lib/schemas/models/DataRequirement_CodeFilter.ts +++ b/src/lib/schemas/models/DataRequirement_CodeFilter.ts @@ -1,6 +1,5 @@ import mongoose from 'mongoose'; import { DataRequirementCodeFilter } from 'fhir/r4'; -import canonical from './canonical'; import Coding from './Coding'; export default new mongoose.Schema( { diff --git a/src/lib/schemas/models/ParameterDefinition.ts b/src/lib/schemas/models/ParameterDefinition.ts index 21e2cf2a..8917060a 100644 --- a/src/lib/schemas/models/ParameterDefinition.ts +++ b/src/lib/schemas/models/ParameterDefinition.ts @@ -1,7 +1,6 @@ import mongoose from 'mongoose'; import { ParameterDefinition } from 'fhir/r4'; import integer from './integer'; -import canonical from './canonical'; export default new mongoose.Schema( { name: { diff --git a/src/lib/schemas/models/Questionnaire_Item.ts b/src/lib/schemas/models/Questionnaire_Item.ts index fffb262e..88d850fe 100644 --- a/src/lib/schemas/models/Questionnaire_Item.ts +++ b/src/lib/schemas/models/Questionnaire_Item.ts @@ -2,8 +2,6 @@ import mongoose from 'mongoose'; import { QuestionnaireItem } from 'fhir/r4'; import Coding from './Coding'; import Questionnaire_EnableWhen from './Questionnaire_EnableWhen'; -import integer from './integer'; -import canonical from './canonical'; import Questionnaire_AnswerOption from './Questionnaire_AnswerOption'; import Questionnaire_Initial from './Questionnaire_Initial'; import Extension from './Extension'; diff --git a/src/lib/schemas/models/Reference.ts b/src/lib/schemas/models/Reference.ts index ab4e488a..4d5b6e97 100644 --- a/src/lib/schemas/models/Reference.ts +++ b/src/lib/schemas/models/Reference.ts @@ -1,4 +1,4 @@ -import mongoose, { Schema, Model } from 'mongoose'; +import mongoose, { Schema } from 'mongoose'; import { Reference } from 'fhir/r4'; const idF = new mongoose.Schema( { diff --git a/src/lib/schemas/models/RelatedArtifact.ts b/src/lib/schemas/models/RelatedArtifact.ts index da1d4d10..e748b764 100644 --- a/src/lib/schemas/models/RelatedArtifact.ts +++ b/src/lib/schemas/models/RelatedArtifact.ts @@ -1,7 +1,6 @@ import mongoose from 'mongoose'; import { RelatedArtifact } from 'fhir/r4'; import Attachment from './Attachment'; -import canonical from './canonical'; export default new mongoose.Schema( { type: { diff --git a/src/lib/schemas/models/ValueSet_Include.ts b/src/lib/schemas/models/ValueSet_Include.ts index 93a0ff70..7bedb63e 100644 --- a/src/lib/schemas/models/ValueSet_Include.ts +++ b/src/lib/schemas/models/ValueSet_Include.ts @@ -2,7 +2,6 @@ import mongoose from 'mongoose'; import { ValueSetComposeInclude } from 'fhir/r4'; import ValueSet_Concept from './ValueSet_Concept'; import ValueSet_Filter from './ValueSet_Filter'; -import canonical from './canonical'; export default new mongoose.Schema( { system: { diff --git a/src/lib/schemas/resources/Questionnaire.ts b/src/lib/schemas/resources/Questionnaire.ts index 1e45c078..21b1b16b 100644 --- a/src/lib/schemas/resources/Questionnaire.ts +++ b/src/lib/schemas/resources/Questionnaire.ts @@ -1,7 +1,6 @@ import mongoose, { model } from 'mongoose'; import { Questionnaire } from 'fhir/r4'; import Identifier from '../models/Identifier'; -import canonical from '../models/canonical'; import ContactDetail from '../models/ContactDetail'; import UsageContext from '../models/UsageContext'; import CodeableConcept from '../models/CodeableConcept'; diff --git a/src/lib/schemas/resources/QuestionnaireResponse.ts b/src/lib/schemas/resources/QuestionnaireResponse.ts index 2c5f2092..05c5c97c 100644 --- a/src/lib/schemas/resources/QuestionnaireResponse.ts +++ b/src/lib/schemas/resources/QuestionnaireResponse.ts @@ -2,7 +2,6 @@ import mongoose, { model } from 'mongoose'; import { QuestionnaireResponse } from 'fhir/r4'; import Identifier from '../models/Identifier'; import Reference from '../models/Reference'; -import canonical from '../models/canonical'; import QuestionnaireResponse_Item from '../models/QuestionnaireResponse_Item'; function QuestionnaireResponseSchema() { diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index a19a6ed0..e654dbab 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -141,6 +141,7 @@ class VsacCache { // axios cleanup await process.nextTick(() => { const v = 1; + return v; }); if ((this.onlyVsac && isVsac) || !this.onlyVsac) { try { @@ -166,7 +167,7 @@ class VsacCache { async isCached(idOrUrl: string) { const id = this.getValuesetId(idOrUrl); // Query our collection for this observation - return await new Promise((resolve, reject) => { + return await new Promise((resolve, _reject) => { if (id) { ValueSetModel.findOne({ id: id.toString() }) .exec() diff --git a/src/services/library.service.ts b/src/services/library.service.ts index a739726a..fa1e110f 100644 --- a/src/services/library.service.ts +++ b/src/services/library.service.ts @@ -1,5 +1,3 @@ -import constants from '../constants'; -import { Globals } from '../globals'; import { FhirUtilities } from '../fhir/utilities'; import LibraryModel from '../lib/schemas/resources/Library'; diff --git a/src/services/patient.service.ts b/src/services/patient.service.ts index de5f53ce..4a70de57 100644 --- a/src/services/patient.service.ts +++ b/src/services/patient.service.ts @@ -1,5 +1,3 @@ -import constants from '../constants'; -import { Globals } from '../globals'; import { FhirUtilities } from '../fhir/utilities'; import PatientModel from '../lib/schemas/resources/Patient'; diff --git a/src/services/questionnaire.service.ts b/src/services/questionnaire.service.ts index 27926e9f..93f7f1c7 100644 --- a/src/services/questionnaire.service.ts +++ b/src/services/questionnaire.service.ts @@ -1,5 +1,3 @@ -import constants from '../constants'; -import { Globals } from '../globals'; import { FhirUtilities } from '../fhir/utilities'; import { Questionnaire } from 'fhir/r4'; import { QuestionnaireUtilities } from '../fhir/questionnaireUtilities'; diff --git a/src/services/questionnaireresponse.service.ts b/src/services/questionnaireresponse.service.ts index 758ef969..cc44291e 100644 --- a/src/services/questionnaireresponse.service.ts +++ b/src/services/questionnaireresponse.service.ts @@ -1,23 +1,21 @@ import { FhirUtilities } from '../fhir/utilities'; import QuestionnaireResponseModel from '../lib/schemas/resources/QuestionnaireResponse'; -module.exports.searchById = (args: any) => - new Promise((resolve, reject) => { +module.exports.searchById = (args: any) => { + console.log('QuestionnaireResponse >>> searchById: -- ' + id); + return new Promise((resolve, reject) => { const { id } = args; - console.log('QuestionnaireResponse >>> searchById: -- ' + id); - new Promise((resolve, reject) => { - const { id } = args; - console.log('Patient >>> searchById -- ' + id); - const doc = QuestionnaireResponseModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); - doc.then(result => { - if (result) { - resolve(result); - } else { - reject(result); - } - }); + console.log('Patient >>> searchById -- ' + id); + const doc = QuestionnaireResponseModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); + doc.then(result => { + if (result) { + resolve(result); + } else { + reject(result); + } }); }); +}; module.exports.create = (args: any, req: any) => new Promise((resolve, reject) => { diff --git a/test/server.test.ts b/test/server.test.ts index ef2df2c8..7667cfd2 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -1,6 +1,5 @@ import { initialize, REMSServer } from '../src/server'; import config from '../src/config'; -import { Db, MongoClient } from 'mongodb'; import sinon from 'sinon'; import { expect } from 'chai'; import { MongoMemoryServer } from 'mongodb-memory-server'; @@ -8,9 +7,6 @@ import mongoose, { ConnectOptions } from 'mongoose'; describe('REMSServer class', () => { let server: REMSServer; - - let connection: MongoClient; - let db: Db; let mongo: any; before(async () => { mongo = await MongoMemoryServer.create(); diff --git a/test/vsac_cache.test.ts b/test/vsac_cache.test.ts index 99418d6a..0d0cc7fe 100644 --- a/test/vsac_cache.test.ts +++ b/test/vsac_cache.test.ts @@ -6,7 +6,7 @@ import nock from 'nock'; import ValueSetModel from '../src/lib/schemas/resources/ValueSet'; import mongoose, { ConnectOptions } from 'mongoose'; import { MongoMemoryServer } from 'mongodb-memory-server'; -import { assert, expect } from 'chai'; +import { expect } from 'chai'; describe('VsacCache', () => { const client = new VsacCache('./tmp', '2c1d55c3-3484-4902-b645-25f3a4974ce6'); let mongo: any; @@ -26,9 +26,6 @@ describe('VsacCache', () => { }); beforeEach(async () => { - // client.clearCache(); - const baseVersion = '4_0_0'; - await ValueSetModel.deleteMany({}); client.onlyVsac = false; }); @@ -102,10 +99,10 @@ describe('VsacCache', () => { mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); try { - const valueSets = client.collectQuestionnaireValuesets(questionnaire); + client.collectQuestionnaireValuesets(questionnaire); expect(await client.isCached(vs)).to.be.false; - const cached = await client.downloadAndCacheValueset(vs); + await client.downloadAndCacheValueset(vs); expect(await client.isCached(vs)).to.be.true; mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); From bf2ff07f0cb3ec064cf20f8e58ddc570500aa2db Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 6 Apr 2023 11:15:11 -0400 Subject: [PATCH 20/27] fixing issue introduced with removing erroneous wrapped Promise --- src/services/questionnaireresponse.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/questionnaireresponse.service.ts b/src/services/questionnaireresponse.service.ts index cc44291e..5169d12d 100644 --- a/src/services/questionnaireresponse.service.ts +++ b/src/services/questionnaireresponse.service.ts @@ -2,7 +2,6 @@ import { FhirUtilities } from '../fhir/utilities'; import QuestionnaireResponseModel from '../lib/schemas/resources/QuestionnaireResponse'; module.exports.searchById = (args: any) => { - console.log('QuestionnaireResponse >>> searchById: -- ' + id); return new Promise((resolve, reject) => { const { id } = args; console.log('Patient >>> searchById -- ' + id); From d9a18ac25bda402e63def454472d4e3614aa2916 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 6 Apr 2023 11:41:48 -0400 Subject: [PATCH 21/27] removing Promises and replacing with async/await to bring the app inline with a single approach to asynchronous activities --- src/fhir/questionnaireUtilities.ts | 11 +-- src/fhir/utilities.ts | 39 +++------- src/lib/vsac_cache.ts | 2 +- src/services/library.service.ts | 31 +++----- src/services/patient.service.ts | 31 +++----- src/services/questionnaire.service.ts | 76 +++++++++---------- src/services/questionnaireresponse.service.ts | 30 +++----- src/services/valueset.service.ts | 33 +++----- 8 files changed, 94 insertions(+), 159 deletions(-) diff --git a/src/fhir/questionnaireUtilities.ts b/src/fhir/questionnaireUtilities.ts index 3484469a..6d230a0b 100644 --- a/src/fhir/questionnaireUtilities.ts +++ b/src/fhir/questionnaireUtilities.ts @@ -141,16 +141,7 @@ export class QuestionnaireUtilities { if (!valueSet) { valueSet = await this.fetchValueSetFromVSAC(valueSetUrl); if (valueSet) { - await FhirUtilities.store( - valueSet, - ValueSetModel, - function () { - return; - }, - function () { - return; - } - ); + await FhirUtilities.store(valueSet, ValueSetModel); } else { this.logger.warn(`Library Processor >> Failed to find ValueSet: ${valueSetUrl}`); } diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 5e5f2fe1..4c064198 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -42,13 +42,7 @@ export class FhirUtilities { return resolveSchema(baseVersion, 'Meta'); }; - static async store( - resource: FhirResource, - model: Model, - resolve: any, - reject: any, - baseVersion = '4_0_0' - ) { + static async store(resource: FhirResource, model: Model, baseVersion = '4_0_0') { // If no resource ID was provided, generate one. let id = ''; if (!resource.id) { @@ -66,18 +60,14 @@ export class FhirUtilities { lastUpdated: moment.utc().format('YYYY-MM-DDTHH:mm:ssZ') }); - model.exists({ id: fhirResource.id }).then(doesExist => { - if (!doesExist) { - try { - resolve(fhirResource.save()); - ('resolve'); - } catch { - reject(); - } - } else { - reject(); - } - }); + const doesExist = await model.exists({ id: fhirResource.id }); + if (!doesExist) { + return await fhirResource.save(); + } else { + throw new Error( + `Resource ${fhirResource.resourceType} with id ${fhirResource.id} already exists` + ); + } } static async loadFile(filePath: string, file: any) { @@ -114,16 +104,7 @@ export class FhirUtilities { break; } if (model) { - await FhirUtilities.store( - resource, - model, - function () { - return; - }, - function () { - return; - } - ); + await FhirUtilities.store(resource, model); } else { console.log(' Unsupported FHIR Resource Type'); } diff --git a/src/lib/vsac_cache.ts b/src/lib/vsac_cache.ts index e654dbab..6c9007ed 100644 --- a/src/lib/vsac_cache.ts +++ b/src/lib/vsac_cache.ts @@ -192,7 +192,7 @@ class VsacCache { if (!vs.id) { vs.id = id; } - await new Promise((resolve, reject) => FhirUtilities.store(vs, ValueSetModel, resolve, reject)); + return await FhirUtilities.store(vs, ValueSetModel); } /** diff --git a/src/services/library.service.ts b/src/services/library.service.ts index fa1e110f..b83c00db 100644 --- a/src/services/library.service.ts +++ b/src/services/library.service.ts @@ -1,24 +1,15 @@ import { FhirUtilities } from '../fhir/utilities'; import LibraryModel from '../lib/schemas/resources/Library'; -module.exports.searchById = (args: any) => - new Promise((resolve, reject) => { - const { id } = args; - console.log('Library >>> searchById: -- ' + id); - const doc = LibraryModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); - doc.then(result => { - if (result) { - resolve(result); - } else { - reject(result); - } - }); - }); +module.exports.searchById = async (args: any) => { + const { id } = args; + console.log('Library >>> searchById: -- ' + id); + return await LibraryModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); +}; -module.exports.create = (args: any, req: any) => - new Promise((resolve, reject) => { - console.log('Library >>> create'); - const resource = req.req.body; - const { base_version } = args; - FhirUtilities.store(resource, LibraryModel, resolve, reject, base_version); - }); +module.exports.create = async (args: any, req: any) => { + console.log('Library >>> create'); + const resource = req.req.body; + const { base_version } = args; + return await FhirUtilities.store(resource, LibraryModel, base_version); +}; diff --git a/src/services/patient.service.ts b/src/services/patient.service.ts index 4a70de57..c3128a02 100644 --- a/src/services/patient.service.ts +++ b/src/services/patient.service.ts @@ -1,24 +1,15 @@ import { FhirUtilities } from '../fhir/utilities'; import PatientModel from '../lib/schemas/resources/Patient'; -module.exports.searchById = (args: any) => - new Promise((resolve, reject) => { - const { id } = args; - console.log('Patient >>> searchById -- ' + id); - const doc = PatientModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); - doc.then(result => { - if (result) { - resolve(result); - } else { - reject(result); - } - }); - }); +module.exports.searchById = async (args: any) => { + const { id } = args; + console.log('Patient >>> searchById -- ' + id); + return await PatientModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); +}; -module.exports.create = (args: any, req: any) => - new Promise((resolve, reject) => { - console.log('Patient >>> create'); - const resource = req.req.body; - const { base_version } = args; - FhirUtilities.store(resource, PatientModel, resolve, reject, base_version); - }); +module.exports.create = async (args: any, req: any) => { + console.log('Patient >>> create'); + const resource = req.req.body; + const { base_version } = args; + return await FhirUtilities.store(resource, PatientModel, base_version); +}; diff --git a/src/services/questionnaire.service.ts b/src/services/questionnaire.service.ts index 93f7f1c7..d183570f 100644 --- a/src/services/questionnaire.service.ts +++ b/src/services/questionnaire.service.ts @@ -1,47 +1,47 @@ import { FhirUtilities } from '../fhir/utilities'; -import { Questionnaire } from 'fhir/r4'; import { QuestionnaireUtilities } from '../fhir/questionnaireUtilities'; import QuestionnaireModel from '../lib/schemas/resources/Questionnaire'; -module.exports.searchById = (args: any) => { - return new Promise((resolve, reject) => { - const { id } = args; - console.log('Questionnaire >>> searchById: -- ' + id); - const doc = QuestionnaireModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); - doc.then(result => { - console.log(result); - if (result) { - resolve(result); - } else { - reject(result); - } - }); - }); +module.exports.searchById = async (args: any) => { + const { id } = args; + console.log('Questionnaire >>> searchById: -- ' + id); + return await QuestionnaireModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); }; -module.exports.create = (args: any, req: any) => - new Promise((resolve, reject) => { - console.log('Questionnaire >>> create'); - const resource = req.req.body; - const { base_version } = args; - FhirUtilities.store(resource, QuestionnaireModel, resolve, reject, base_version); - }); +module.exports.create = async (args: any, req: any) => { + console.log('Questionnaire >>> create'); + const resource = req.req.body; + const { base_version } = args; + return await FhirUtilities.store(resource, QuestionnaireModel, base_version); +}; -module.exports.questionnairePackage = (args: any, context: any, logger: any) => { +module.exports.questionnairePackage = async (args: any, context: any, logger: any) => { logger.info('Running Questionnaire Package /:id/$questionnaire-package'); - return new Promise((resolve, reject) => { - const { id } = args; - const doc = QuestionnaireModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); - doc.then(async result => { - if (result) { - const unprocessedQ: Questionnaire = result.toObject(); - const parameters = await QuestionnaireUtilities.createPackageFromQuestionnaire( - unprocessedQ - ); - resolve(parameters); - } else { - reject(result); - } - }); - }); + const { id } = args; + const result = await QuestionnaireModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); + if (result) { + const parameters = await QuestionnaireUtilities.createPackageFromQuestionnaire(result); + return parameters; + } else { + throw result; + } }; + +// module.exports.questionnairePackage = (args: any, context: any, logger: any) => { +// logger.info('Running Questionnaire Package /:id/$questionnaire-package'); +// return new Promise((resolve, reject) => { +// const { id } = args; +// const doc = QuestionnaireModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); +// doc.then(async result => { +// if (result) { +// const unprocessedQ: Questionnaire = result.toObject(); +// const parameters = await QuestionnaireUtilities.createPackageFromQuestionnaire( +// unprocessedQ +// ); +// resolve(parameters); +// } else { +// reject(result); +// } +// }); +// }); +// }; diff --git a/src/services/questionnaireresponse.service.ts b/src/services/questionnaireresponse.service.ts index 5169d12d..9d20d461 100644 --- a/src/services/questionnaireresponse.service.ts +++ b/src/services/questionnaireresponse.service.ts @@ -1,25 +1,15 @@ import { FhirUtilities } from '../fhir/utilities'; import QuestionnaireResponseModel from '../lib/schemas/resources/QuestionnaireResponse'; -module.exports.searchById = (args: any) => { - return new Promise((resolve, reject) => { - const { id } = args; - console.log('Patient >>> searchById -- ' + id); - const doc = QuestionnaireResponseModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); - doc.then(result => { - if (result) { - resolve(result); - } else { - reject(result); - } - }); - }); +module.exports.searchById = async (args: any) => { + const { id } = args; + console.log('Patient >>> searchById -- ' + id); + return await QuestionnaireResponseModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); }; -module.exports.create = (args: any, req: any) => - new Promise((resolve, reject) => { - console.log('QuestionnaireResponse >>> create'); - const resource = req.req.body; - const { base_version } = args; - FhirUtilities.store(resource, QuestionnaireResponseModel, resolve, reject, base_version); - }); +module.exports.create = async (args: any, req: any) => { + console.log('QuestionnaireResponse >>> create'); + const resource = req.req.body; + const { base_version } = args; + return await FhirUtilities.store(resource, QuestionnaireResponseModel, base_version); +}; diff --git a/src/services/valueset.service.ts b/src/services/valueset.service.ts index 5917f0db..6ffad714 100644 --- a/src/services/valueset.service.ts +++ b/src/services/valueset.service.ts @@ -1,25 +1,16 @@ import { FhirUtilities } from '../fhir/utilities'; import ValueSetModel from '../lib/schemas/resources/ValueSet'; -module.exports.searchById = (args: any) => - new Promise((resolve, reject) => { - const { id } = args; - console.log('ValueSet >>> searchById: -- ' + id); - const doc = ValueSetModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); - doc.then(result => { - if (result) { - resolve(result); - } else { - reject(result); - } - }); - }); +module.exports.searchById = async (args: any) => { + const { id } = args; + console.log('ValueSet >>> searchById: -- ' + id); + return await ValueSetModel.findOne({ id: id.toString() }, { _id: 0 }).exec(); +}; -module.exports.create = (args: any, req: any) => - new Promise((resolve, reject) => { - console.log('ValueSet >>> create'); - const resource = req.req.body; - const { base_version } = args; - console.log(resource); - FhirUtilities.store(resource, ValueSetModel, resolve, reject, base_version); - }); +module.exports.create = async (args: any, req: any) => { + console.log('ValueSet >>> create'); + const resource = req.req.body; + const { base_version } = args; + console.log(resource); + return await FhirUtilities.store(resource, ValueSetModel, base_version); +}; From d68797865959318e0bbb6e265fab2ebdeedfb660 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Mon, 10 Apr 2023 16:17:24 -0400 Subject: [PATCH 22/27] adding bare minimum test cases for services --- .mocharc.json | 7 + package.json | 3 +- .../models/QuestionnaireResponse_Answer.ts | 8 - .../models/QuestionnaireResponse_Item.ts | 21 ++- src/server.ts | 3 + test/fixtures/patient.json | 164 ++++++++++++++++++ test/fixtures/questionnaireResponse.json | 137 +++++++++++++++ test/fixtures/valueSet.json | 4 +- test/helpers.ts | 44 +++++ test/server.test.ts | 17 -- test/services/library.service.test.ts | 6 + test/services/patient.service.test.ts | 6 + test/services/questionnaire.service.test.ts | 6 + .../questionnarieResponse.service.test.ts | 10 ++ test/services/valueset.service.test.ts | 6 + test/setup.ts | 17 ++ test/vsac_cache.test.ts | 42 ++--- 17 files changed, 445 insertions(+), 56 deletions(-) create mode 100644 .mocharc.json create mode 100644 test/fixtures/patient.json create mode 100644 test/fixtures/questionnaireResponse.json create mode 100644 test/helpers.ts create mode 100644 test/services/library.service.test.ts create mode 100644 test/services/patient.service.test.ts create mode 100644 test/services/questionnaire.service.test.ts create mode 100644 test/services/questionnarieResponse.service.test.ts create mode 100644 test/services/valueset.service.test.ts create mode 100644 test/setup.ts diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 00000000..281531fc --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,7 @@ +{ + "timeout" : 20000, + "extension": ["ts"], + "spec": "test/**/*.test.ts", + "require": "ts-node/register", + "file" : ["test/setup.ts"] +} \ No newline at end of file diff --git a/package.json b/package.json index d511d25f..8f884b54 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "develop": "node dist/scripts/develop.js", "start": "ts-node-dev --inspect=8091 src/scripts/serve.ts", - "test": "mocha --timeout 10000 -r ts-node/register test/*.test.ts", + "test": "mocha ", "lint": "eslint \"**/*.{js,ts}\"", "lint:fix": "eslint \"**/*.{js,ts}\" --quiet --fix", "prettier": "prettier --check \"**/*.{js,ts}\"", @@ -62,6 +62,7 @@ "@typescript-eslint/parser": "^5.45.0", "axios-mock-adapter": "^1.21.2", "chai": "^4.3.7", + "chai-http": "^4.3.0", "eslint": "^8.28.0", "eslint-config-prettier": "^6.15.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/src/lib/schemas/models/QuestionnaireResponse_Answer.ts b/src/lib/schemas/models/QuestionnaireResponse_Answer.ts index 5e01ee43..48daeb95 100644 --- a/src/lib/schemas/models/QuestionnaireResponse_Answer.ts +++ b/src/lib/schemas/models/QuestionnaireResponse_Answer.ts @@ -4,7 +4,6 @@ import Attachment from './Attachment'; import Coding from './Coding'; import Quantity from './Quantity'; import Reference from './Reference'; -import QuestionnaireResponse_Item from './QuestionnaireResponse_Item'; const QAnswer = new mongoose.Schema( { valueBoolean: { @@ -59,11 +58,4 @@ const QAnswer = new mongoose.Schema( { _id: false } ); -QAnswer.add({ - item: { - type: [QuestionnaireResponse_Item], - default: void 0 - } -}); - export default QAnswer; diff --git a/src/lib/schemas/models/QuestionnaireResponse_Item.ts b/src/lib/schemas/models/QuestionnaireResponse_Item.ts index 2f8995af..f24778af 100644 --- a/src/lib/schemas/models/QuestionnaireResponse_Item.ts +++ b/src/lib/schemas/models/QuestionnaireResponse_Item.ts @@ -3,6 +3,11 @@ import { QuestionnaireResponseItem } from 'fhir/r4'; import QuestionnaireResponse_Answer from './QuestionnaireResponse_Answer'; const qrItem = new mongoose.Schema( { + text: { + type: String, + required: false + }, + linkId: { type: String, default: void 0 @@ -10,16 +15,24 @@ const qrItem = new mongoose.Schema( definition: { type: String, default: void 0 - }, - answer: { - type: [QuestionnaireResponse_Answer], - default: void 0 } }, { _id: false } ); qrItem.add({ + item: { + type: [qrItem], + default: void 0 + }, + + answer: { + type: [QuestionnaireResponse_Answer], + default: void 0 + } +}); + +QuestionnaireResponse_Answer.add({ item: { type: [qrItem], default: void 0 diff --git a/src/server.ts b/src/server.ts index 132dc2a7..1bd78ac1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -42,6 +42,9 @@ class REMSServer extends Server { return this; } + _app() { + return this.app; + } /** * @method configureMiddleware * @description Enable all the standard middleware diff --git a/test/fixtures/patient.json b/test/fixtures/patient.json new file mode 100644 index 00000000..ddd20dde --- /dev/null +++ b/test/fixtures/patient.json @@ -0,0 +1,164 @@ +{ + "resourceType": "Patient", + "id": "example", + "text": { + "status": "generated" + }, + "identifier": [ + { + "use": "usual", + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0203", + "code": "MR" + } + ] + }, + "system": "urn:oid:1.2.36.146.595.217.0.1", + "value": "12345", + "period": { + "start": "2001-05-06" + }, + "assigner": { + "display": "Acme Healthcare" + } + } + ], + "active": true, + "name": [ + { + "use": "official", + "family": "Chalmers", + "given": [ + "Peter", + "James" + ] + }, + { + "use": "usual", + "given": [ + "Jim" + ] + }, + { + "use": "maiden", + "family": "Windsor", + "given": [ + "Peter", + "James" + ], + "period": { + "end": "2002" + } + } + ], + "telecom": [ + { + "use": "home" + }, + { + "system": "phone", + "value": "(03) 5555 6473", + "use": "work", + "rank": 1 + }, + { + "system": "phone", + "value": "(03) 3410 5613", + "use": "mobile", + "rank": 2 + }, + { + "system": "phone", + "value": "(03) 5555 8834", + "use": "old", + "period": { + "end": "2014" + } + } + ], + "gender": "male", + "birthDate": "1974-12-25", + "_birthDate": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime", + "valueDateTime": "1974-12-25T14:35:45-05:00" + } + ] + }, + "deceasedBoolean": false, + "address": [ + { + "use": "home", + "type": "both", + "text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999", + "line": [ + "534 Erewhon St" + ], + "city": "PleasantVille", + "district": "Rainbow", + "state": "Vic", + "postalCode": "3999", + "period": { + "start": "1974-12-25" + } + } + ], + "contact": [ + { + "relationship": [ + { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0131", + "code": "N" + } + ] + } + ], + "name": { + "family": "du Marché", + "_family": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix", + "valueString": "VV" + } + ] + }, + "given": [ + "Bénédicte" + ] + }, + "telecom": [ + { + "system": "phone", + "value": "+33 (237) 998327" + } + ], + "address": { + "use": "home", + "type": "both", + "line": [ + "534 Erewhon St" + ], + "city": "PleasantVille", + "district": "Rainbow", + "state": "Vic", + "postalCode": "3999", + "period": { + "start": "1974-12-25" + } + }, + "gender": "female", + "period": { + "start": "2012" + } + } + ], + "managingOrganization": { + "reference": "Organization/1" + } +} \ No newline at end of file diff --git a/test/fixtures/questionnaireResponse.json b/test/fixtures/questionnaireResponse.json new file mode 100644 index 00000000..3357eb2e --- /dev/null +++ b/test/fixtures/questionnaireResponse.json @@ -0,0 +1,137 @@ +{ + "resourceType": "QuestionnaireResponse", + "id": "example", + "text": { + "status": "generated" + }, + "contained": [ + { + "resourceType": "Patient", + "id": "patsub", + "identifier": [ + { + "system": "http://cancer.questionnaire.org/systems/id/patientnr", + "value": "A34442332" + }, + { + "type": { + "text": "Dutch BSN" + }, + "system": "urn:oid:2.16.840.1.113883.2.4.6.3", + "value": "188912345" + } + ], + "gender": "male", + "birthDate": "1972-11-30" + }, + { + "resourceType": "ServiceRequest", + "id": "order", + "status": "unknown", + "intent": "order", + "subject": { + "reference": "#patsub" + }, + "requester": { + "reference": "Practitioner/example" + } + }, + { + "resourceType": "Practitioner", + "id": "questauth", + "identifier": [ + { + "type": { + "text": "AUMC, Den Helder" + }, + "system": "http://cancer.questionnaire.org/systems/id/org", + "value": "AUMC" + } + ] + } + ], + "identifier": { + "system": "http://example.org/fhir/NamingSystem/questionnaire-ids", + "value": "Q12349876" + }, + "basedOn": [ + { + "reference": "#order" + } + ], + "partOf": [ + { + "reference": "Procedure/f201" + } + ], + "status": "completed", + "subject": { + "reference": "#patsub" + }, + "encounter": { + "reference": "Encounter/example" + }, + "authored": "2013-02-19T14:15:00-05:00", + "author": { + "reference": "#questauth" + }, + "item": [ + { + "linkId": "1", + "item": [ + { + "linkId": "1.1", + "answer": [ + { + "valueCoding": { + "system": "http://cancer.questionnaire.org/system/code/yesno", + "code": "1", + "display": "Yes" + }, + "item": [ + { + "linkId": "1.1.1", + "item": [ + { + "linkId": "1.1.1.1", + "answer": [ + { + "valueCoding": { + "system": "http://cancer.questionnaire.org/system/code/yesno", + "code": "1" + } + } + ] + }, + { + "linkId": "1.1.1.2", + "answer": [ + { + "valueCoding": { + "system": "http://cancer.questionnaire.org/system/code/yesno", + "code": "1" + } + } + ] + }, + { + "linkId": "1.1.1.3", + "answer": [ + { + "valueCoding": { + "system": "http://cancer.questionnaire.org/system/code/yesno", + "code": "0" + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/fixtures/valueSet.json b/test/fixtures/valueSet.json index d9fd1185..055377be 100644 --- a/test/fixtures/valueSet.json +++ b/test/fixtures/valueSet.json @@ -1,4 +1,4 @@ { - "resourceType": "ValueSet" - + "resourceType": "ValueSet", + "id" : "example" } \ No newline at end of file diff --git a/test/helpers.ts b/test/helpers.ts new file mode 100644 index 00000000..1b4b1127 --- /dev/null +++ b/test/helpers.ts @@ -0,0 +1,44 @@ +import config from '../src/config'; +import chai from 'chai'; +import { expect } from 'chai'; +import chaiHttp from 'chai-http'; +import { initialize } from '../src/server'; +import { Model } from 'mongoose'; +chai.use(chaiHttp); + +const actLikeAService = async (resourceType: string, fixture: any, model: Model) => { + let remsServer: any; + let app: any; + before(async () => { + remsServer = initialize(config); + // Start listening on a port and pass the callback through + app = remsServer._app(); + }); + + it(`should be able to store ${resourceType}`, async () => { + let found = await model.findOne({ id: fixture.id }); + expect(found).to.be.null; + + const res = await chai.request(app).post(`/4_0_0/${resourceType}`).send(fixture); + expect(res).to.have.status(201); + expect(res.body).to.be.a('object'); + + found = await model.findOne({ id: fixture.id }); + expect(found.id).to.not.be.null; + }); + + it(`should be able to retrieve ${resourceType}`, async () => { + const resource = { resourceType: resourceType, id: 'findMe' }; + + await model.create(resource); + const res = await chai.request(app).get(`/4_0_0/${resourceType}/${resource.id}`); + + expect(res).to.have.status(200); + expect(res.body).to.be.a('object'); + expect(res.body.id).to.be.equal(resource.id); + }); +}; + +export default { + actLikeAService +}; diff --git a/test/server.test.ts b/test/server.test.ts index 7667cfd2..187296df 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -2,26 +2,9 @@ import { initialize, REMSServer } from '../src/server'; import config from '../src/config'; import sinon from 'sinon'; import { expect } from 'chai'; -import { MongoMemoryServer } from 'mongodb-memory-server'; -import mongoose, { ConnectOptions } from 'mongoose'; describe('REMSServer class', () => { let server: REMSServer; - let mongo: any; - before(async () => { - mongo = await MongoMemoryServer.create(); - const uri = mongo.getUri(); - const options: ConnectOptions = {}; - await mongoose.connect(uri, options); - }); - - after(async () => { - console.log('Closing connection?'); - - await mongoose.connection.dropDatabase(); - await mongoose.connection.close(); - await mongo.stop(); - }); beforeEach(() => { server = new REMSServer(config.fhirServerConfig); diff --git a/test/services/library.service.test.ts b/test/services/library.service.test.ts new file mode 100644 index 00000000..2f02325d --- /dev/null +++ b/test/services/library.service.test.ts @@ -0,0 +1,6 @@ +import helpers from '../helpers'; +import library from '../fixtures/library.json'; +import LibraryModel from '../../src/lib/schemas/resources/Library'; +describe('Library service', () => { + helpers.actLikeAService('Library', library, LibraryModel); +}); diff --git a/test/services/patient.service.test.ts b/test/services/patient.service.test.ts new file mode 100644 index 00000000..eed88a64 --- /dev/null +++ b/test/services/patient.service.test.ts @@ -0,0 +1,6 @@ +import helpers from '../helpers'; +import patient from '../fixtures/patient.json'; +import PatientModel from '../../src/lib/schemas/resources/Patient'; +describe('Patient service', () => { + helpers.actLikeAService('Patient', patient, PatientModel); +}); diff --git a/test/services/questionnaire.service.test.ts b/test/services/questionnaire.service.test.ts new file mode 100644 index 00000000..63476839 --- /dev/null +++ b/test/services/questionnaire.service.test.ts @@ -0,0 +1,6 @@ +import helpers from '../helpers'; +import questionnaire from '../fixtures/questionnaire.json'; +import QuestionnaireModel from '../../src/lib/schemas/resources/Questionnaire'; +describe('questionnaire service', () => { + helpers.actLikeAService('Questionnaire', questionnaire, QuestionnaireModel); +}); diff --git a/test/services/questionnarieResponse.service.test.ts b/test/services/questionnarieResponse.service.test.ts new file mode 100644 index 00000000..7b94cdd2 --- /dev/null +++ b/test/services/questionnarieResponse.service.test.ts @@ -0,0 +1,10 @@ +import helpers from '../helpers'; +import questionnaireResponse from '../fixtures/questionnaireResponse.json'; +import QuestionnaireResponseModel from '../../src/lib/schemas/resources/QuestionnaireResponse'; +describe('questionnaireResponse service', () => { + helpers.actLikeAService( + 'QuestionnaireResponse', + questionnaireResponse, + QuestionnaireResponseModel + ); +}); diff --git a/test/services/valueset.service.test.ts b/test/services/valueset.service.test.ts new file mode 100644 index 00000000..7f31623f --- /dev/null +++ b/test/services/valueset.service.test.ts @@ -0,0 +1,6 @@ +import helpers from '../helpers'; +import valueset from '../fixtures/valueSet.json'; +import ValueSetModel from '../../src/lib/schemas/resources/ValueSet'; +describe('valueset service', () => { + helpers.actLikeAService('Valueset', valueset, ValueSetModel); +}); diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 00000000..ab7c19e6 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,17 @@ +import { MongoMemoryServer } from 'mongodb-memory-server'; +import mongoose, { ConnectOptions } from 'mongoose'; + +let mongo: any; +before(async () => { + console.log('Setting up MONGO'); + mongo = await MongoMemoryServer.create(); + const uri = mongo.getUri(); + const options: ConnectOptions = {}; + await mongoose.connect(uri, options); +}); +after(async () => { + console.log('Tearing down Mongo'); + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongo.stop(); +}); diff --git a/test/vsac_cache.test.ts b/test/vsac_cache.test.ts index 0d0cc7fe..9a85bde9 100644 --- a/test/vsac_cache.test.ts +++ b/test/vsac_cache.test.ts @@ -1,29 +1,15 @@ import VsacCache from '../src/lib/vsac_cache'; import library from './fixtures/library.json'; import questionnaire from './fixtures/questionnaire.json'; -import valueSet from './fixtures/valueSet.json'; import nock from 'nock'; import ValueSetModel from '../src/lib/schemas/resources/ValueSet'; -import mongoose, { ConnectOptions } from 'mongoose'; -import { MongoMemoryServer } from 'mongodb-memory-server'; import { expect } from 'chai'; + +const generateValueset = (idOrUrl: string) => { + return { resourceType: 'ValueSet', id: idOrUrl }; +}; describe('VsacCache', () => { const client = new VsacCache('./tmp', '2c1d55c3-3484-4902-b645-25f3a4974ce6'); - let mongo: any; - before(async () => { - mongo = await MongoMemoryServer.create(); - const uri = mongo.getUri(); - const options: ConnectOptions = {}; - await mongoose.connect(uri, options); - }); - - after(async () => { - console.log('Closing connection?'); - - await mongoose.connection.dropDatabase(); - await mongoose.connection.close(); - await mongo.stop(); - }); beforeEach(async () => { await ValueSetModel.deleteMany({}); @@ -55,10 +41,10 @@ describe('VsacCache', () => { mockRequest .get('/ValueSet/2.16.840.1.113762.1.4.1219.85/$expand') - .reply(200, JSON.stringify(valueSet)); + .reply(200, generateValueset('2.16.840.1.113762.1.4.1219.85')); mockRequest .get('/ValueSet/2.16.840.1.113762.1.4.1219.35/$expand') - .reply(200, JSON.stringify(valueSet)); + .reply(200, generateValueset('2.16.840.1.113762.1.4.1219.35')); const valueSets = client.collectLibraryValuesets(library); valueSets.forEach(async function (vs) { @@ -77,7 +63,9 @@ describe('VsacCache', () => { it('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, generateValueset('yes-no-unknown-not-asked')); const valueSets = client.collectQuestionnaireValuesets(questionnaire); valueSets.forEach(async vs => { @@ -97,7 +85,9 @@ describe('VsacCache', () => { const mockRequest = nock('http://terminology.hl7.org'); const vs = 'http://terminology.hl7.org/ValueSet/yes-no-unknown-not-asked'; - mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); + mockRequest + .get('/ValueSet/yes-no-unknown-not-asked') + .reply(200, generateValueset('yes-no-unknown-not-asked')); try { client.collectQuestionnaireValuesets(questionnaire); expect(await client.isCached(vs)).to.be.false; @@ -105,12 +95,16 @@ describe('VsacCache', () => { await client.downloadAndCacheValueset(vs); expect(await client.isCached(vs)).to.be.true; - mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); + mockRequest + .get('/ValueSet/yes-no-unknown-not-asked') + .reply(200, generateValueset('yes-no-unknown-not-asked')); let update = await client.downloadAndCacheValueset(vs); expect(update.get('cached')).to.be.false; - mockRequest.get('/ValueSet/yes-no-unknown-not-asked').reply(200, JSON.stringify(valueSet)); + mockRequest + .get('/ValueSet/yes-no-unknown-not-asked') + .reply(200, generateValueset('yes-no-unknown-not-asked')); update = await client.downloadAndCacheValueset(vs, true); expect(update.get('cached')).to.be.true; From 4d61da68eb67bf4d6f82a4c2cbfa3cbfb0e84d04 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 20 Apr 2023 10:25:52 -0400 Subject: [PATCH 23/27] removing addtional callback based async calls and replacing with synchronous versions. Adding npm glob package to streamline processing of resources to load from filesystem. Adding tests for populateDB and loadResource functions --- package.json | 2 + src/fhir/utilities.ts | 241 +- .../lib1/R4/resources/library.json | 59 + .../lib2/R4/resources/questionnaire.json | 3527 +++++++++++++++++ test/server.test.ts | 23 + 5 files changed, 3676 insertions(+), 176 deletions(-) create mode 100644 test/fixtures/cds-library/lib1/R4/resources/library.json create mode 100644 test/fixtures/cds-library/lib2/R4/resources/questionnaire.json diff --git a/package.json b/package.json index 8f884b54..9534bf68 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,14 @@ "dependencies": { "@projecttacoma/node-fhir-server-core": "^2.2.8", "@types/fhir": "^0.0.35", + "@types/glob": "^8.1.0", "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", + "glob": "^10.2.1", "lodash": "^4.17.19", "moment": "^2.24.0", "moment-timezone": "^0.5.40", diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 4c064198..b7652b10 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -1,9 +1,7 @@ import { resolveSchema } from '@projecttacoma/node-fhir-server-core'; import * as moment from 'moment'; import 'moment-timezone'; -import * as path from 'path'; import * as fs from 'fs'; -import * as process from 'process'; import crypto from 'crypto'; import { QuestionnaireUtilities } from './questionnaireUtilities'; import { FhirResource } from 'fhir/r4'; @@ -14,8 +12,7 @@ import QuestionnaireResponseModel from '../lib/schemas/resources/QuestionnaireRe import ValueSetModel from '../lib/schemas/resources/ValueSet'; import { Model } from 'mongoose'; import { medicationCollection, metRequirementsCollection } from './models'; - -const re = /(?:\.([^.]+))?$/; +import { glob } from 'glob'; export class FhirUtilities { static getLibrary(baseVersion: string) { @@ -70,182 +67,51 @@ export class FhirUtilities { } } - static async loadFile(filePath: string, file: any) { - const extension = re.exec(filePath); - if (extension) { - if (extension[1].toLowerCase() === 'json') { - if (file !== 'TopicMetadata.json') { - console.log("'%s' is a JSON Resource file.", filePath); - fs.readFile(filePath, 'utf8', async (err: any, jsonString: string) => { - if (err) { - console.error('Failed to read file:', err); - return; - } - try { - const resource = JSON.parse(jsonString); - // Build the strings to connect to the collections - let model; - switch (resource.resourceType) { - case 'Library': - model = LibraryModel; - await QuestionnaireUtilities.processLibraryCodeFilters(resource, {}); - break; - case 'Patient': - model = PatientModel; - break; - case 'Questionnaire': - model = QuestionnaireModel; - break; - case 'QuestionnaireResponse': - model = QuestionnaireResponseModel; - break; - case 'ValueSet': - model = ValueSetModel; - break; - } - if (model) { - await FhirUtilities.store(resource, model); - } else { - console.log(' Unsupported FHIR Resource Type'); - } - } catch (parseError: any) { - console.warn('Failed to parse json file: ' + filePath); - console.warn(parseError); - } - }); - } - } - } - } - static async loadResources(resourcePath: string) { console.log('Loading FHIR Resources from: ' + resourcePath); - - // Loop through all the files in the directory looking for folders - fs.readdir(resourcePath, function (rootError: any, rootFiles: any) { - if (rootError) { - console.error('Could not list the directory.', rootError); - process.exit(1); + // this assumes the cds-library directory structure of + // libraries -> -> R4 -> resources -> * json resources + const files = glob.sync(`${resourcePath}/*/R4/resources/*.json`); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const jsonString = fs.readFileSync(file, 'utf8'); + try { + const resource = JSON.parse(jsonString); + // Build the strings to connect to the collections + let model; + switch (resource.resourceType) { + case 'Library': + model = LibraryModel; + await QuestionnaireUtilities.processLibraryCodeFilters(resource, {}); + break; + case 'Patient': + model = PatientModel; + break; + case 'Questionnaire': + model = QuestionnaireModel; + break; + case 'QuestionnaireResponse': + model = QuestionnaireResponseModel; + break; + case 'ValueSet': + model = ValueSetModel; + break; + } + if (model) { + await FhirUtilities.store(resource, model); + } else { + console.log(' Unsupported FHIR Resource Type'); + } + } catch (parseError: any) { + console.warn('Failed to parse json file: ' + file); + console.warn(parseError); } - - rootFiles.forEach(function (rootFile: any) { - const rootFilePath = path.join(resourcePath, rootFile); - - fs.stat(rootFilePath, function (rootFileError: any, rootFileStat: any) { - if (rootFileError) { - console.error('Error getting root folder file statistics.', rootFileError); - return; - } - - if (rootFileStat.isDirectory()) { - // loop through all the fhir versions looking for R4 - fs.readdir(rootFilePath, function (rulesErr: any, rulesFiles: any) { - if (rulesErr) { - console.error('Error getting rule folder file list.', rulesErr); - } - - rulesFiles.forEach(function (rulesFile: any) { - const rulesFilePath = path.join(rootFilePath, rulesFile); - - fs.stat(rulesFilePath, function (rulesFileError: any, rulesFileStat: any) { - if (rulesFileError) { - console.error('Error getting rules folder file statistics.', rulesFileError); - return; - } - - if (rulesFileStat.isDirectory()) { - // only process the R4 directory - if (rulesFile === 'R4') { - fs.readdir( - rulesFilePath, - function (fhirVersionErr: any, fhirVersionFiles: any) { - if (fhirVersionErr) { - console.error('Error getting fhir folder file list.', fhirVersionErr); - return; - } - - // loop through all the folders in the R4 folder - fhirVersionFiles.forEach(function (fhirVersionFile: any) { - const fhirVersionFilePath = path.join(rulesFilePath, fhirVersionFile); - - fs.stat( - fhirVersionFilePath, - function (fhirVersionFileError: any, fhirVersionFileStat: any) { - if (fhirVersionFileError) { - console.error( - 'Error getting fhir version file statistics.', - fhirVersionFileError - ); - return; - } - - if (fhirVersionFileStat.isDirectory()) { - if (fhirVersionFile === 'resources') { - console.log(fhirVersionFilePath); - - fs.readdir( - fhirVersionFilePath, - function (resourceFilesErr: any, resourceFiles: any) { - if (resourceFilesErr) { - console.error( - 'Error getting resource folder file list.', - resourceFilesErr - ); - return; - } - - resourceFiles.forEach(function (resourceFile: any) { - const resourceFilePath = path.join( - fhirVersionFilePath, - resourceFile - ); - - fs.stat( - resourceFilePath, - function ( - resourceFileError: any, - resourceFileStat: any - ) { - if (resourceFileError) { - console.error( - 'Error getting resource file statistics.', - resourceFileError - ); - return; - } - - if (resourceFileStat.isFile()) { - FhirUtilities.loadFile( - resourceFilePath, - resourceFile - ); - } - } - ); - }); - } - ); - } - } - } - ); - }); - } - ); - } - } - }); - }); - }); - } - }); - }); - }); + } } static async populateDB() { // prepopulateDB - await medicationCollection.insertMany([ + const medications = [ { name: 'Turalio', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', @@ -351,9 +217,9 @@ export class FhirUtilities { } ] } - ]); + ]; - await metRequirementsCollection.insertMany([ + const medicationRequirements = [ { stakeholderId: 'Organization/pharm0111', completed: true, @@ -386,6 +252,29 @@ export class FhirUtilities { completedQuestionnaire: null, case_numbers: [] } - ]); + ]; + await medicationCollection.bulkWrite( + medications.map(med => ({ + updateOne: { + filter: { name: med.name }, + update: { $set: med }, + upsert: true + } + })) + ); + + await metRequirementsCollection.bulkWrite( + medicationRequirements.map(mr => ({ + updateOne: { + filter: { + drugName: mr.drugName, + requirementName: mr.requirementName, + stakeholderId: mr.stakeholderId + }, + update: { $set: mr }, + upsert: true + } + })) + ); } } diff --git a/test/fixtures/cds-library/lib1/R4/resources/library.json b/test/fixtures/cds-library/lib1/R4/resources/library.json new file mode 100644 index 00000000..0c71cc5a --- /dev/null +++ b/test/fixtures/cds-library/lib1/R4/resources/library.json @@ -0,0 +1,59 @@ +{ + "resourceType": "Library", + "id": "lib1-library-1", + "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/test/fixtures/cds-library/lib2/R4/resources/questionnaire.json b/test/fixtures/cds-library/lib2/R4/resources/questionnaire.json new file mode 100644 index 00000000..6b4428d8 --- /dev/null +++ b/test/fixtures/cds-library/lib2/R4/resources/questionnaire.json @@ -0,0 +1,3527 @@ +{ + "resourceType": "Questionnaire", + "id": "lib2-questionnaire-1", + "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/test/server.test.ts b/test/server.test.ts index 187296df..fa527904 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -2,6 +2,11 @@ import { initialize, REMSServer } from '../src/server'; import config from '../src/config'; import sinon from 'sinon'; import { expect } from 'chai'; +import { metRequirementsCollection, medicationCollection } from '../src/fhir/models'; + +import { FhirUtilities } from '../src/fhir/utilities'; +import LibraryModel from '../src/lib/schemas/resources/Library'; +import QuestionnaireModel from '../src/lib/schemas/resources/Questionnaire'; describe('REMSServer class', () => { let server: REMSServer; @@ -80,4 +85,22 @@ describe('REMSServer class', () => { expect(newServer).to.have.property('app'); expect(newServer).to.have.property('listen'); }); + + it('should be able to prepopulate data without error', async () => { + expect(await metRequirementsCollection.count({})).to.equal(0); + expect(await medicationCollection.count({})).to.equal(0); + await FhirUtilities.populateDB(); + expect(await metRequirementsCollection.count({})).to.not.equal(0); + expect(await medicationCollection.count({})).to.not.equal(0); + await FhirUtilities.populateDB(); + }); + + it('should be able to load artifacts from filesystem', async () => { + expect(await LibraryModel.count({})).to.equal(0); + expect(await QuestionnaireModel.count({})).to.equal(0); + await FhirUtilities.loadResources('./test/fixtures/cds-library'); + expect(await LibraryModel.count({})).to.not.equal(0); + expect(await QuestionnaireModel.count({})).to.not.equal(0); + await FhirUtilities.loadResources('./test/fixtures/cds-library'); + }); }); From 61f836e438fc24d3f8ef1222468ad93aff7a8072 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 20 Apr 2023 11:12:26 -0400 Subject: [PATCH 24/27] adding default usr/pass back to env --- env.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/env.json b/env.json index b36b4dd9..338c0110 100644 --- a/env.json +++ b/env.json @@ -1,7 +1,7 @@ { "MONGO_URL": { "type": "string", - "default": "mongodb://127.0.0.1:27017", + "default": "mongodb://rems-user:pass@127.0.0.1:27017", "required": true }, "MONGO_DB_NAME": { From a25c68e8b97cbe90053e63a055021a9a8ae03150 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 20 Apr 2023 11:14:25 -0400 Subject: [PATCH 25/27] removing mocha options from cmd line call in script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 854790ec..1ab53c4e 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "develop": "node dist/scripts/develop.js", "start": "ts-node-dev --inspect=8091 src/scripts/serve.ts", - "test": "mocha --timeout 20000 -r ts-node/register test/*.test.ts", + "test": "mocha", "lint": "eslint \"**/*.{js,ts}\"", "lint:fix": "eslint \"**/*.{js,ts}\" --quiet --fix", "prettier": "prettier --check \"**/*.{js,ts}\"", From 8e5e931213c3367390f485bb0ed1d7fbe7016038 Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Wed, 26 Apr 2023 09:16:01 -0400 Subject: [PATCH 26/27] Trapping resource exists errors on startup. --- src/fhir/utilities.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index b7652b10..d3ec363c 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -14,6 +14,8 @@ import { Model } from 'mongoose'; import { medicationCollection, metRequirementsCollection } from './models'; import { glob } from 'glob'; +class ResourceExistsException extends Error {} + export class FhirUtilities { static getLibrary(baseVersion: string) { return resolveSchema(baseVersion, 'Library'); @@ -61,7 +63,7 @@ export class FhirUtilities { if (!doesExist) { return await fhirResource.save(); } else { - throw new Error( + throw new ResourceExistsException( `Resource ${fhirResource.resourceType} with id ${fhirResource.id} already exists` ); } @@ -98,7 +100,16 @@ export class FhirUtilities { break; } if (model) { - await FhirUtilities.store(resource, model); + try { + await FhirUtilities.store(resource, model); + } catch (e: any) { + //catch the resource exist exception and keep going. + // there are better ways to deal with this but involves + // a larger rework effort which can happen later + if (!(e instanceof ResourceExistsException)) { + throw e; + } + } } else { console.log(' Unsupported FHIR Resource Type'); } From 4399cb9a5bfc0388e4fe0badaa626a82d00df5da Mon Sep 17 00:00:00 2001 From: Robert A Dingwell Date: Thu, 27 Apr 2023 14:27:07 -0400 Subject: [PATCH 27/27] removing addtional promises --- src/fhir/questionnaireUtilities.ts | 59 ++++-------------------------- src/lib/etasu.ts | 21 +++-------- 2 files changed, 14 insertions(+), 66 deletions(-) diff --git a/src/fhir/questionnaireUtilities.ts b/src/fhir/questionnaireUtilities.ts index 6d230a0b..90ff6d00 100644 --- a/src/fhir/questionnaireUtilities.ts +++ b/src/fhir/questionnaireUtilities.ts @@ -161,60 +161,17 @@ export class QuestionnaireUtilities { } return returnValue; } - static async findQuestionnaire(id: string): Promise { - return new Promise((resolve, reject) => { - QuestionnaireModel.findOne( - { id: id.toString() }, - (err: any, questionnaire: Questionnaire) => { - if (err) { - return reject(err); - } - if (questionnaire) { - resolve(new QuestionnaireModel(questionnaire)); - } - resolve(undefined); - } - ); - }); + static async findQuestionnaire(id: string): Promise { + return await QuestionnaireModel.findOne({ id: id.toString() }); } - static async findLibraryByUrl(url: string): Promise { - return new Promise((resolve, reject) => { - LibraryModel.findOne({ url: url.toString() }, (err: any, library: Library) => { - if (err) { - return reject(err); - } - if (library) { - resolve(new LibraryModel(library)); - } - resolve(undefined); - }); - }); + static async findLibraryByUrl(url: string): Promise { + return await LibraryModel.findOne({ url: url.toString() }); } - static async findLibraryById(id: string): Promise { - return new Promise((resolve, reject) => { - LibraryModel.findOne({ id: id.toString() }, (err: any, library: Library) => { - if (err) { - return reject(err); - } - if (library) { - resolve(new LibraryModel(library)); - } - resolve(undefined); - }); - }); + static async findLibraryById(id: string): Promise { + return await LibraryModel.findOne({ id: id.toString() }); } - static async findValueSetByUrl(url: string): Promise { - return new Promise((resolve, reject) => { - ValueSetModel.findOne({ url: url.toString() }, (err: any, valueset: ValueSet) => { - if (err) { - return reject(err); - } - if (valueset) { - resolve(new ValueSetModel(valueset)); - } - resolve(undefined); - }); - }); + static async findValueSetByUrl(url: string): Promise { + return await ValueSetModel.findOne({ url: url.toString() }); } static async processSubQuestionnaires(questionnaire: Questionnaire) { const extensions = questionnaire.extension || []; diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index d1321279..6f18b807 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -13,23 +13,17 @@ const router = Router(); // const remsCaseCollection = db.collection('rems-case'); // etasu endpoints -router.get('/:drug', (req: Request, res: Response) => { - medicationCollection.findOne({ name: req.params.drug }, (err: any, drug: any) => { - if (err) throw err; - res.send(drug); - }); +router.get('/:drug', async (req: Request, res: Response) => { + res.send(await medicationCollection.findOne({ name: req.params.drug })); }); -router.get('/met/:caseId', (req: Request, res: Response) => { - remsCaseCollection.findOne({ case_number: req.params.caseId }, (err: any, remsCase: any) => { - if (err) throw err; - res.send(remsCase); - }); +router.get('/met/:caseId', async (req: Request, res: Response) => { + res.send(await remsCaseCollection.findOne({ case_number: req.params.caseId })); }); router.get( '/met/patient/:patientFirstName/:patientLastName/:patientDOB/drug/:drugName', - (req: Request, res: Response) => { + async (req: Request, res: Response) => { const searchDict = { patientFirstName: req.params.patientFirstName, patientLastName: req.params.patientLastName, @@ -37,10 +31,7 @@ router.get( drugName: req.params.drugName }; - remsCaseCollection.findOne(searchDict, (err: any, remsCase: any) => { - if (err) throw err; - res.send(remsCase); - }); + res.send(await remsCaseCollection.findOne(searchDict)); } );