├── .github ├── CODEOWNERS ├── pull_request_template.md └── workflows │ └── ci-workflow.yaml ├── test ├── application │ ├── fixtures │ │ ├── empty-run-logs.json │ │ ├── patient-mrns.csv │ │ ├── run-logs.json │ │ ├── example-clinical-trial-info.csv │ │ ├── example-patient.csv │ │ ├── example-disease-status.csv │ │ ├── example-condition.csv │ │ └── test-bundle.json │ ├── app.test.js │ └── runInstanceLogger.test.js ├── helpers │ ├── fixtures │ │ ├── valid-mrns.csv │ │ ├── invalid-mrns.csv │ │ ├── valid-mrns-bom.csv │ │ ├── emptyBundle.json │ │ ├── icd10.json │ │ ├── test-config.json │ │ ├── valid-resource.json │ │ ├── invalid-resource.json │ │ ├── count-bundle-5-same.json │ │ ├── count-bundle-5-unique.json │ │ ├── valueset-without-expansion.json │ │ ├── valueset-with-expansion.json │ │ ├── count-bundle-5-nested.json │ │ ├── condition-without-icd10.json │ │ ├── secondary-cancer-condition.json │ │ ├── condition-with-icd10.json │ │ ├── primary-cancer-condition.json │ │ └── searchsetBundleWithOneEntry.json │ ├── dateUtils.test.js │ ├── appUtils.test.js │ ├── dependencyUtils.test.js │ ├── csvParsingUtils.test.js │ ├── configUtils.test.js │ └── observationUtils.test.js ├── extractors │ ├── fixtures │ │ ├── example.csv │ │ ├── context-with-patient.json │ │ ├── csv-cancer-disease-status-module-response.json │ │ ├── csv-staging-module-response.json │ │ ├── csv-encounter-module-response.json │ │ ├── csv-treatment-plan-change-module-response.json │ │ ├── csv-clinical-trial-information-module-response.json │ │ ├── csv-observation-module-response.json │ │ ├── csv-patient-module-response.json │ │ ├── csv-appointment-module-response.json │ │ ├── csv-condition-module-response.json │ │ ├── csv-medication-administration-module-response.json │ │ ├── csv-procedure-module-response.json │ │ ├── csv-medication-request-module-response.json │ │ ├── csv-adverse-event-module-response.json │ │ ├── csv-encounter-bundle.json │ │ ├── csv-ctc-adverse-event-module-response.json │ │ ├── csv-medication-administration-bundle.json │ │ ├── csv-observation-bundle.json │ │ ├── csv-clinical-trial-information-bundle.json │ │ ├── csv-appointment-bundle.json │ │ ├── csv-adverse-event-bundle.json │ │ ├── csv-procedure-bundle.json │ │ └── patient-bundle.json │ ├── FHIREncounterExtractor.test.js │ ├── FHIRProcedureExtractor.test.js │ ├── FHIRMedicationOrderExtractor.test.js │ ├── Extractor.test.js │ ├── FHIRConditionExtractor.test.js │ ├── FHIRDocumentReferenceExtractor.test.js │ ├── FHIRMedicationStatementExtractor.test.js │ ├── FHIRAllergyIntoleranceExtractor.test.js │ ├── FHIRObservationExtractor.test.js │ ├── FHIRMedicationRequestExtractor.test.js │ ├── FHIRPatientExtractor.test.js │ ├── CSVPatientExtractor.test.js │ └── CSVConditionExtractor.test.js ├── sample-client-data │ ├── patient-mrns.csv │ ├── treatment-plan-change-information.csv │ ├── patient-information.csv │ ├── cancer-disease-status-information.csv │ ├── clinical-trial-information.csv │ ├── appointment-information.csv │ ├── observation-information.csv │ ├── encounter-information.csv │ ├── staging-information.csv │ ├── condition-information.csv │ ├── cancer-related-medication-administration-information.csv │ ├── procedure-information.csv │ ├── cancer-related-medication-request-information.csv │ ├── adverse-event-information.csv │ └── ctc-adverse-event-information.csv ├── templates │ ├── fixtures │ │ ├── minimal-reference-object.json │ │ ├── maximal-reference-object.json │ │ ├── maximal-coding-object.json │ │ ├── maximal-identifier.json │ │ ├── minimal-appointment-resource.json │ │ ├── minimal-encounter-resource.json │ │ ├── minimal-procedure-resource.json │ │ ├── identifier-array.json │ │ ├── minimal-adverse-event-resource.json │ │ ├── research-study-resource.json │ │ ├── minimal-condition-resource.json │ │ ├── research-subject-resource.json │ │ ├── maximal-encounter-resource.json │ │ ├── minimal-medication-request.json │ │ ├── minimal-medication-resource.json │ │ ├── minimal-ctc-adverse-event-resource.json │ │ ├── minimal-observation-resource.json │ │ ├── patient-resource.json │ │ ├── minimal-staging-clinical-resource.json │ │ ├── minimal-staging-pathologic-resource.json │ │ ├── nodes-category-clinical-resource.json │ │ ├── tumor-category-clinical-resource.json │ │ ├── nodes-category-pathologic-resource.json │ │ ├── tumor-category-pathologic-resource.json │ │ ├── metastases-category-clinical-resource.json │ │ ├── metastases-category-pathologic-resource.json │ │ ├── maximal-medication-resource.json │ │ ├── minimal-careplan-resource.json │ │ ├── minimal-disease-status-resource.json │ │ ├── maximal-observation-resource.json │ │ ├── maximal-appointment-resource.json │ │ ├── maximal-procedure-resource.json │ │ ├── maximal-adverse-event-resource.json │ │ ├── maximal-staging-resource.json │ │ ├── disease-status-resource.json │ │ ├── maximal-condition-resource.json │ │ └── maximal-patient-resource.json │ ├── researchStudy.test.js │ ├── snippets │ │ ├── reference.test.js │ │ ├── coding.test.js │ │ └── cancerStaging.test.js │ ├── researchSubject.test.js │ ├── encounter.test.js │ └── appointment.test.js ├── modules │ ├── fixtures │ │ ├── example-csv-empty-line.csv │ │ ├── example-csv-empty-values.csv │ │ ├── csv-response.json │ │ ├── example-csv.csv │ │ ├── example-csv-bom.csv │ │ └── patient-resource.json │ └── BaseFHIRModule.test.js └── utils.js ├── docs ├── patient-mrns.csv ├── CSV_Templates.xlsx ├── diagrams │ ├── steam-high-level-arch.png │ ├── steam-terminology-breakdown.png │ └── archive │ │ ├── 20_05-04_high-level-arch.png │ │ ├── 20_05_04_terminology-breakdown.png │ │ ├── 20_11_23_steam-high-level-arch.png │ │ ├── 21_06_11_steam-high-level-arch.png │ │ └── 20_11_23_steam-terminology-breakdown.png ├── staging.csv ├── treatment-plan-change.csv ├── patient.csv ├── clinical-trial-information.csv ├── observation.csv ├── appointment.csv ├── cancer-related-medication-administration.csv ├── encounter.csv ├── condition.csv ├── cancer-disease-status.csv ├── procedure.csv ├── cancer-related-medication-request.csv ├── adverse-event.csv ├── ctc-adverse-event.csv └── config.example.json ├── .gitignore ├── src ├── templates │ ├── index.js │ ├── snippets │ │ ├── subject.js │ │ ├── period.js │ │ ├── resource.js │ │ ├── medication.js │ │ ├── effectiveX.js │ │ ├── reference.js │ │ ├── treatmentReason.js │ │ ├── coding.js │ │ ├── bodySiteTemplate.js │ │ ├── extension.js │ │ ├── identifier.js │ │ ├── index.js │ │ └── cancerStaging.js │ ├── EncounterTemplate.js │ ├── ResearchStudyTemplate.js │ ├── ResearchSubjectTemplate.js │ ├── CancerRelatedMedicationAdministrationTemplate.js │ └── ProcedureTemplate.js ├── modules │ ├── index.js │ ├── BaseFHIRModule.js │ └── CSVFileModule.js ├── helpers │ ├── errors.js │ ├── cancerStagingUtils.js │ ├── lookups │ │ └── ctcAdverseEventLookup.js │ ├── logger.js │ ├── dateUtils.js │ ├── valueSets │ │ └── vs-expansion-explained.md │ ├── observationUtils.js │ ├── appUtils.js │ ├── csvValidator.js │ ├── lookupUtils.js │ ├── dependencyUtils.js │ └── configUtils.js ├── extractors │ ├── FHIRConditionExtractor.js │ ├── FHIREncounterExtractor.js │ ├── FHIRProcedureExtractor.js │ ├── FHIRObservationExtractor.js │ ├── FHIRMedicationOrderExtractor.js │ ├── FHIRDocumentReferenceExtractor.js │ ├── FHIRMedicationRequestExtractor.js │ ├── FHIRAllergyIntoleranceExtractor.js │ ├── FHIRMedicationStatementExtractor.js │ ├── Extractor.js │ ├── FHIRPatientExtractor.js │ ├── BaseCSVExtractor.js │ ├── MCODESurgicalProcedureExtractor.js │ ├── FHIRAdverseEventExtractor.js │ └── CSVEncounterExtractor.js ├── application │ ├── index.js │ └── tools │ │ └── mcodeExtraction.js ├── client │ └── MCODEClient.js └── cli │ └── cli.js ├── .eslintrc.json └── package.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dmendelowitz 2 | -------------------------------------------------------------------------------- /test/application/fixtures/empty-run-logs.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /test/application/fixtures/patient-mrns.csv: -------------------------------------------------------------------------------- 1 | mrn 2 | 123 -------------------------------------------------------------------------------- /docs/patient-mrns.csv: -------------------------------------------------------------------------------- 1 | mrn 2 | mrn-1 3 | mrn-2 4 | mrn-3 5 | -------------------------------------------------------------------------------- /test/helpers/fixtures/valid-mrns.csv: -------------------------------------------------------------------------------- 1 | mrn 2 | 123 3 | 456 4 | 789 5 | -------------------------------------------------------------------------------- /test/extractors/fixtures/example.csv: -------------------------------------------------------------------------------- 1 | example,columns 2 | example,values 3 | -------------------------------------------------------------------------------- /test/helpers/fixtures/invalid-mrns.csv: -------------------------------------------------------------------------------- 1 | not-mrn 2 | 123 3 | 456 4 | 789 5 | -------------------------------------------------------------------------------- /test/helpers/fixtures/valid-mrns-bom.csv: -------------------------------------------------------------------------------- 1 | mrn 2 | 123 3 | 456 4 | 789 5 | -------------------------------------------------------------------------------- /test/sample-client-data/patient-mrns.csv: -------------------------------------------------------------------------------- 1 | mrn 2 | 123 3 | 456 4 | 789 5 | 1011 6 | -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-reference-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "reference": "urn:uuid:example-id" 3 | } 4 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | ## New behavior 3 | ## Code changes 4 | # Testing guidance 5 | -------------------------------------------------------------------------------- /docs/CSV_Templates.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcode/mcode-extraction-framework/HEAD/docs/CSV_Templates.xlsx -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode/ 3 | .DS_Store 4 | output/ 5 | logs/ 6 | config/* 7 | !config/csv.config.example.json 8 | -------------------------------------------------------------------------------- /test/application/fixtures/run-logs.json: -------------------------------------------------------------------------------- 1 | [{"dateRun":"2021-06-17T15:10:49.601Z","toDate":"2020-06-16","fromDate":"2019-01-01"}] -------------------------------------------------------------------------------- /docs/diagrams/steam-high-level-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcode/mcode-extraction-framework/HEAD/docs/diagrams/steam-high-level-arch.png -------------------------------------------------------------------------------- /test/helpers/fixtures/emptyBundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "searchset", 4 | "total": 0, 5 | "entry": [ 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /docs/diagrams/steam-terminology-breakdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcode/mcode-extraction-framework/HEAD/docs/diagrams/steam-terminology-breakdown.png -------------------------------------------------------------------------------- /src/templates/index.js: -------------------------------------------------------------------------------- 1 | const { generateMcodeResources } = require('./ResourceGenerator'); 2 | 3 | module.exports = { 4 | generateMcodeResources, 5 | }; 6 | -------------------------------------------------------------------------------- /docs/diagrams/archive/20_05-04_high-level-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcode/mcode-extraction-framework/HEAD/docs/diagrams/archive/20_05-04_high-level-arch.png -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-reference-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "reference": "urn:uuid:example-id", 3 | "display": "example-name", 4 | "type": "ExampleType" 5 | } 6 | -------------------------------------------------------------------------------- /docs/diagrams/archive/20_05_04_terminology-breakdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcode/mcode-extraction-framework/HEAD/docs/diagrams/archive/20_05_04_terminology-breakdown.png -------------------------------------------------------------------------------- /docs/diagrams/archive/20_11_23_steam-high-level-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcode/mcode-extraction-framework/HEAD/docs/diagrams/archive/20_11_23_steam-high-level-arch.png -------------------------------------------------------------------------------- /docs/diagrams/archive/21_06_11_steam-high-level-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcode/mcode-extraction-framework/HEAD/docs/diagrams/archive/21_06_11_steam-high-level-arch.png -------------------------------------------------------------------------------- /docs/diagrams/archive/20_11_23_steam-terminology-breakdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcode/mcode-extraction-framework/HEAD/docs/diagrams/archive/20_11_23_steam-terminology-breakdown.png -------------------------------------------------------------------------------- /docs/staging.csv: -------------------------------------------------------------------------------- 1 | mrn,conditionId,stageGroup,t,n,m,type,stagingSystem,stagingCodeSystem,effectiveDate 2 | mrn-1,example-condition-id,3C,cT3,cN3,cM0,Clinical,443830009,http://snomed.info/sct,2020-01-01 3 | -------------------------------------------------------------------------------- /test/helpers/fixtures/icd10.json: -------------------------------------------------------------------------------- 1 | { 2 | "system": "http://hl7.org/fhir/sid/icd-10-cm", 3 | "code": "C50.211", 4 | "display": "Malignant neoplasm of upper-inner quadrant of right female breast" 5 | } 6 | -------------------------------------------------------------------------------- /docs/treatment-plan-change.csv: -------------------------------------------------------------------------------- 1 | mrn,reasonCode,reasonDisplayText,changed,dateOfCarePlan,dateRecorded 2 | mrn-1,281647001,Adverse reaction (disorder),true,2020-04-15,2020-05-01 3 | mrn-2,,,false,2020-03-30,2020-05-01 4 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-coding-object.json: -------------------------------------------------------------------------------- 1 | { 2 | "system": "example-sys", 3 | "version": "v3.1.4", 4 | "code": "example-code", 5 | "display": "A string of display text", 6 | "userSelected": true 7 | } 8 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-identifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": { 3 | "system": "http://system.com/codesystem", 4 | "value": "90210", 5 | "type": { 6 | "text": "Text explaining what this code system value is" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/templates/snippets/subject.js: -------------------------------------------------------------------------------- 1 | const { reference } = require('./reference'); 2 | 3 | function subjectTemplate({ id }) { 4 | return { 5 | subject: reference({ id, resourceType: 'Patient' }), 6 | }; 7 | } 8 | 9 | module.exports = { 10 | subjectTemplate, 11 | }; 12 | -------------------------------------------------------------------------------- /test/modules/fixtures/example-csv-empty-line.csv: -------------------------------------------------------------------------------- 1 | mrn,trialSubjectID,enrollmentStatus,trialResearchID,trialStatus,dateRecorded 2 | example-mrn-1,subjectId-1,status-1,researchId-1,trialStatus-1,2020-01-10 3 | 4 | example-mrn-2,subjectId-3,status-3,researchId-3,trialStatus-3,2020-06-10 -------------------------------------------------------------------------------- /test/extractors/fixtures/context-with-patient.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "entry": [ 4 | { 5 | "fullUrl": "context-url", 6 | "resource": { 7 | "resourceType": "Patient", 8 | "id": "mrn-1" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/modules/fixtures/example-csv-empty-values.csv: -------------------------------------------------------------------------------- 1 | mrn,trialSubjectID,enrollmentStatus,trialResearchID,trialStatus,dateRecorded 2 | example-mrn-1,subjectId-1,status-1,researchId-1,trialStatus-1,2020-01-10 3 | , , , , , 4 | , , , ,, 5 | ,,, , , 6 | example-mrn-not-ignored,,,,, -------------------------------------------------------------------------------- /src/modules/index.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRModule } = require('./BaseFHIRModule'); 2 | const { CSVFileModule } = require('./CSVFileModule'); 3 | const { CSVURLModule } = require('./CSVURLModule'); 4 | 5 | module.exports = { 6 | BaseFHIRModule, 7 | CSVFileModule, 8 | CSVURLModule, 9 | }; 10 | -------------------------------------------------------------------------------- /docs/patient.csv: -------------------------------------------------------------------------------- 1 | mrn,familyName,givenName,gender,birthsex,dateOfBirth,race,ethnicity,language,addressLine,city,state,zip 2 | pat-mrn-1,Doe,Jane,female,F,1991-06-21,ASKU,2186-5,no,2 West Side Rd,Malden,MA,02186 3 | pat-mrn-2,Doe,John,male,M,1986-11-08,2106-3,2186-5,en-US,1 Main street,Brooklyn,NY,11201 4 | -------------------------------------------------------------------------------- /src/templates/snippets/period.js: -------------------------------------------------------------------------------- 1 | function periodTemplate({ startDate, endDate }) { 2 | return { 3 | period: { 4 | ...(startDate && { start: startDate }), 5 | ...(endDate && { end: endDate }), 6 | }, 7 | }; 8 | } 9 | 10 | module.exports = { 11 | periodTemplate, 12 | }; 13 | -------------------------------------------------------------------------------- /test/modules/fixtures/csv-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "example-mrn-1", 4 | "trialsubjectid": "subjectId-1", 5 | "enrollmentstatus": "status-1", 6 | "trialresearchid": "researchId-1", 7 | "trialstatus": "trialStatus-1", 8 | "daterecorded": "2020-01-10" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /src/helpers/errors.js: -------------------------------------------------------------------------------- 1 | class NotImplementedError extends Error { 2 | constructor(...args) { 3 | super(...args); 4 | 5 | this.name = 'NotImplementedError'; 6 | Error.captureStackTrace(this, NotImplementedError); 7 | } 8 | } 9 | 10 | module.exports = { 11 | NotImplementedError, 12 | }; 13 | -------------------------------------------------------------------------------- /test/helpers/fixtures/test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "patientIdCsvPath": "test/application/fixtures/patient-mrns.csv", 3 | "commonExtractorArgs": {}, 4 | "extractors": [], 5 | "notificationInfo": { 6 | "host": "smtp.example.com", 7 | "to": [ 8 | "demo@example.com" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/helpers/fixtures/valid-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "id": "valid-patient", 4 | "name": [ 5 | { 6 | "family": "Frege", 7 | "given": [ 8 | "Gottlob" 9 | ] 10 | } 11 | ], 12 | "gender": "male", 13 | "birthDate": "1848-11-08" 14 | } 15 | -------------------------------------------------------------------------------- /test/sample-client-data/treatment-plan-change-information.csv: -------------------------------------------------------------------------------- 1 | mrn,reasonCode,reasonDisplayText,changed,dateOfCarePlan,dateRecorded 2 | 123,281647001,Adverse reaction (disorder),true,2020-04-15,2020-04-15 3 | 456,405613005,Planned Procedure (situation),true,2020-03-30,2020-04-15 4 | 789,,,false,2020-04-22,2020-06-17 5 | -------------------------------------------------------------------------------- /test/helpers/fixtures/invalid-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "id": "invalid-patient", 4 | "name": [ 5 | { 6 | "family": "Godel", 7 | "given": [ 8 | "Kurt" 9 | ] 10 | } 11 | ], 12 | "gender": "invalid-enum-value", 13 | "birthDate": "1906-04-28" 14 | } 15 | -------------------------------------------------------------------------------- /src/templates/snippets/resource.js: -------------------------------------------------------------------------------- 1 | function meta(profiles) { 2 | return { 3 | meta: { 4 | profile: profiles, 5 | }, 6 | }; 7 | } 8 | 9 | function narrative(status, div) { 10 | return { 11 | status, 12 | div, 13 | }; 14 | } 15 | 16 | module.exports = { 17 | meta, 18 | narrative, 19 | }; 20 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-cancer-disease-status-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "conditionid": "cond-1", 5 | "diseasestatuscode": "USCRS-352236", 6 | "dateofobservation": "2019-12-02", 7 | "evidence": "363679005|252416005", 8 | "observationstatus": "amended" 9 | } 10 | ] 11 | -------------------------------------------------------------------------------- /docs/clinical-trial-information.csv: -------------------------------------------------------------------------------- 1 | mrn,trialSubjectID,enrollmentStatus,trialResearchID,trialStatus,trialResearchSystem,startDate,endDate 2 | 123,subjectId-1,example-status,researchId-1,example-trialStatus,research-system,YYYY-MM-DD,YYYY-MM-DD 3 | 456,subjectId-2,example-status,researchId-1,example-trialStatus,research-system,YYYY-MM-DD,YYYY-MM-DD 4 | -------------------------------------------------------------------------------- /test/application/fixtures/example-clinical-trial-info.csv: -------------------------------------------------------------------------------- 1 | mrn,trialSubjectID,enrollmentStatus,trialResearchID,trialStatus,trialResearchSystem 2 | 123,subjectId-1,potential-candidate,researchId-1,approved,system-1 3 | 456,subjectId-2,on-study-intervention,researchId-1,completed,system-2 4 | 789,subjectId-3,on-study-observation,researchId-2,active, 5 | -------------------------------------------------------------------------------- /test/modules/fixtures/example-csv.csv: -------------------------------------------------------------------------------- 1 | mrn,trialSubjectID,enrollmentStatus,trialResearchID,trialStatus,dateRecorded 2 | example-mrn-1,subjectId-1,status-1,researchId-1,trialStatus-1,2020-01-10 3 | example-mrn-2,subjectId-2,status-2,researchId-2,trialStatus-2,2020-01-10 4 | example-mrn-2,subjectId-3,status-3,researchId-3,trialStatus-3,2020-06-10 5 | -------------------------------------------------------------------------------- /test/modules/fixtures/example-csv-bom.csv: -------------------------------------------------------------------------------- 1 | mrn,trialSubjectID,enrollmentStatus,trialResearchID,trialStatus,dateRecorded 2 | example-mrn-1,subjectId-1,status-1,researchId-1,trialStatus-1,2020-01-10 3 | example-mrn-2,subjectId-2,status-2,researchId-2,trialStatus-2,2020-01-10 4 | example-mrn-2,subjectId-3,status-3,researchId-3,trialStatus-3,2020-06-10 5 | -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-appointment-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Appointment", 3 | "id": "appointmentId-3", 4 | "status": "cancelled", 5 | "participant": [ 6 | { 7 | "actor": { 8 | "reference": "urn:uuid:789", 9 | "type": "Patient" 10 | }, 11 | "status": "tentative" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-encounter-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Encounter", 3 | "id": "encounterId-1", 4 | "status": "arrived", 5 | "class": { 6 | "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", 7 | "code": "AMB" 8 | }, 9 | "subject": { 10 | "reference": "urn:uuid:123", 11 | "type": "Patient" 12 | } 13 | } -------------------------------------------------------------------------------- /test/application/fixtures/example-patient.csv: -------------------------------------------------------------------------------- 1 | mrn,familyName,givenName,gender,birthsex,dateOfBirth,race,ethnicity,language,addressLine,city,state,zip 2 | 123,Doe,Jane,female,F,1980-01-01,2028-9,2186-5,en,2 West Side Rd,Malden,MA,02148 3 | 456,Doe,John,male,M,1970-01-01,2106-3,2186-5,en-US,3 East Side Rd,Brooklyn,NY,11201 4 | 789,Doe,Jimmy,other,UNK,1980-03-01,ASKU,2186-5,ar,,,,90001 5 | -------------------------------------------------------------------------------- /test/sample-client-data/patient-information.csv: -------------------------------------------------------------------------------- 1 | mrn,familyName,givenName,gender,birthsex,dateOfBirth,race,ethnicity,language,addressLine,city,state,zip 2 | 123,Doe,Jane,female,F,1980-01-01,2028-9,2186-5,en,2 West Side Rd,Malden,MA,02148 3 | 456,Doe,John,male,M,1970-01-01,2106-3,2186-5,en-US,3 East Side Rd,Brooklyn,NY,11201 4 | 789,Doe,Jimmy,other,UNK,1980-03-01,ASKU,2186-5,ar,,,,90001 5 | -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-procedure-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Procedure", 3 | "id": "example-id", 4 | "status": "completed", 5 | "code": { 6 | "coding": [{ "system": "http://snomed.info/sct", "code": "152198000" }] 7 | }, 8 | "subject": { "reference": "urn:uuid:example-mrn", "type": "Patient" }, 9 | "performedDateTime": "2020-01-01" 10 | } 11 | -------------------------------------------------------------------------------- /test/sample-client-data/cancer-disease-status-information.csv: -------------------------------------------------------------------------------- 1 | mrn,conditionId,diseaseStatusCode,diseaseStatusText,dateOfObservation,evidence,observationStatus,dateRecorded 2 | 123,conditionId-1,268910001,responding,2019-12-02,363679005|252416005,preliminary,2020-01-10 3 | 789,conditionId-2,709137006,not evaluated,2020-04-22,,final,2020-06-10 4 | 123,conditionId-1,not-asked,,2020-01-12,,, 5 | -------------------------------------------------------------------------------- /src/templates/snippets/medication.js: -------------------------------------------------------------------------------- 1 | const { coding } = require('./coding'); 2 | 3 | function medicationTemplate({ code, codeSystem, displayText }) { 4 | return { 5 | medicationCodeableConcept: { 6 | coding: [coding({ system: codeSystem, code, display: displayText }), 7 | ], 8 | }, 9 | }; 10 | } 11 | 12 | module.exports = { 13 | medicationTemplate, 14 | }; 15 | -------------------------------------------------------------------------------- /test/sample-client-data/clinical-trial-information.csv: -------------------------------------------------------------------------------- 1 | mrn,trialSubjectID,enrollmentStatus,trialResearchID,trialStatus,trialResearchSystem,startDate,endDate 2 | 123,subjectId-1,potential-candidate,researchId-1,approved,system-1,2020-01-01,2021-01-03 3 | 456,subjectId-2,on-study-intervention,researchId-1,completed,system-2,2023-05-03, 4 | 789,subjectId-3,on-study-observation,researchId-2,active,,, 5 | -------------------------------------------------------------------------------- /test/templates/fixtures/identifier-array.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": [ 3 | { 4 | "system": "http://system.com/codesystem", 5 | "value": "90210", 6 | "type": { 7 | "text": "Text explaining what this code system value is" 8 | } 9 | }, 10 | { 11 | "system": "http://system.com/testing", 12 | "value": "02135" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/templates/snippets/effectiveX.js: -------------------------------------------------------------------------------- 1 | function effectiveX(effective) { 2 | if (effective.high || effective.low) { 3 | return { 4 | effectivePeriod: { 5 | high: effective.high, 6 | low: effective.low, 7 | }, 8 | }; 9 | } 10 | return { 11 | effectiveDateTime: effective, 12 | }; 13 | } 14 | 15 | 16 | module.exports = { 17 | effectiveX, 18 | }; 19 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-staging-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "conditionid": "cond-1", 5 | "stagegroup": "3C", 6 | "t": "cT3", 7 | "n": "cN3", 8 | "m": "cM0", 9 | "type": "Clinical", 10 | "stagingsystem": "444256004", 11 | "stagingcodesystem": "http://snomed.info/sct", 12 | "effectivedate": "2020-01-01" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /test/application/fixtures/example-disease-status.csv: -------------------------------------------------------------------------------- 1 | mrn,conditionId,diseaseStatusCode,diseaseStatusText,dateOfObservation,evidence,observationStatus,dateRecorded 2 | 123,conditionId-1,268910001,responding,2019-12-02,363679005|252416005,preliminary,2020-01-10 3 | 456,conditionId-2,359746009,stable,2020-01-12,363679005,amended,2020-01-12 4 | 789,conditionId-2,709137006,not evaluated,2020-04-22,,final,2020-06-10 5 | -------------------------------------------------------------------------------- /src/templates/snippets/reference.js: -------------------------------------------------------------------------------- 1 | function reference({ id, name, resourceType }) { 2 | if (!id) throw Error('Trying to render a reference snippet, but the id argument is missing.'); 3 | 4 | return { 5 | reference: `urn:uuid:${id}`, 6 | ...(name && { display: name }), 7 | ...(resourceType && { type: resourceType }), 8 | }; 9 | } 10 | 11 | module.exports = { 12 | reference, 13 | }; 14 | -------------------------------------------------------------------------------- /docs/observation.csv: -------------------------------------------------------------------------------- 1 | mrn,observationId,status,code,codeSystem,displayName,value,valueCodeSystem,effectiveDate,bodySite,laterality 2 | mrn-1,example-id-1,example-status,example-code,example-system,example-name,example-value,example-system,YYYY-MM-DD,example-site,example-laterality 3 | mrn-2,example-id-2,example-status,example-code,example-system,example-name,example-value,example-system,YYYY-MM-DD,example-site,example-laterality -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-encounter-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "encounterid": "encounterId-1", 5 | "status": "arrived", 6 | "classcode": "AMB", 7 | "classsystem": "http://terminology.hl7.org/CodeSystem/v3-ActCode", 8 | "typecode": "11429006", 9 | "typesystem": "http://snomed.info/sct", 10 | "startdate": "2020-01-10", 11 | "enddate": "2020-01-10" 12 | } 13 | ] -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-adverse-event-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "AdverseEvent", 3 | "id": "adverseEventId-1", 4 | "subject": { 5 | "reference": "urn:uuid:mrn-1", 6 | "type": "Patient" 7 | }, 8 | "event": { 9 | "coding": [ 10 | { 11 | "system": "code-system", 12 | "code": "109006" 13 | } 14 | ] 15 | }, 16 | "actuality": "actual", 17 | "date": "1994-12-09" 18 | } -------------------------------------------------------------------------------- /src/helpers/cancerStagingUtils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { checkCodeInVs } = require('./valueSetUtils'); 3 | 4 | function isCancerStagingSystem(code, system) { 5 | const cancerStagingSystemVSPath = path.resolve(__dirname, 'valueSets', 'ValueSet-mcode-cancer-staging-system-vs.json'); 6 | return checkCodeInVs(code, system, cancerStagingSystemVSPath); 7 | } 8 | 9 | module.exports = { 10 | isCancerStagingSystem, 11 | }; 12 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-treatment-plan-change-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "subjectId": "mrn-1", 4 | "dateofcareplan": "2020-04-15", 5 | "reasoncode": "281647001", 6 | "reasondisplaytext": "Adverse reaction (disorder)", 7 | "changed": "true" 8 | }, 9 | { 10 | "subjectId": "mrn-1", 11 | "dateofcareplan": "2020-04-30", 12 | "reasoncode": "405613005", 13 | "changed": "true" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /docs/appointment.csv: -------------------------------------------------------------------------------- 1 | mrn,appointmentId,status,serviceCategory,serviceType,appointmentType,specialty,start,end,cancelationCode,description 2 | mrn-1,exampleId-1,status-code,service-cat,service-type,app-type,specialty-code,YYYY-MM-DD,YYYY-MM-DD,cancel-code,"Example description 1" 3 | mrn-2,exampleId-2,status-code,service-cat,service-type,app-type,specialty-code,YYYY-MM-DD,YYYY-MM-DD,cancel-code,"Example description 2" 4 | mrn-2,exampleId-3,status-code,,,,,,,, -------------------------------------------------------------------------------- /src/extractors/FHIRConditionExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | class FHIRConditionExtractor extends BaseFHIRExtractor { 4 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 5 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 6 | this.resourceType = 'Condition'; 7 | } 8 | } 9 | 10 | module.exports = { 11 | FHIRConditionExtractor, 12 | }; 13 | -------------------------------------------------------------------------------- /src/extractors/FHIREncounterExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | class FHIREncounterExtractor extends BaseFHIRExtractor { 4 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 5 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 6 | this.resourceType = 'Encounter'; 7 | } 8 | } 9 | 10 | module.exports = { 11 | FHIREncounterExtractor, 12 | }; 13 | -------------------------------------------------------------------------------- /src/extractors/FHIRProcedureExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | class FHIRProcedureExtractor extends BaseFHIRExtractor { 4 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 5 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 6 | this.resourceType = 'Procedure'; 7 | } 8 | } 9 | 10 | module.exports = { 11 | FHIRProcedureExtractor, 12 | }; 13 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-clinical-trial-information-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "patientId": "mrn-1", 4 | "trialsubjectid": "example-subjectId", 5 | "enrollmentstatus": "example-enrollment-status", 6 | "trialresearchid": "example-researchId", 7 | "trialstatus": "example-trialStatus", 8 | "trialresearchsystem":"example-system", 9 | "startdate": "2020-01-01", 10 | "enddate": "2021-01-01" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /src/application/index.js: -------------------------------------------------------------------------------- 1 | const { mcodeApp } = require('./app'); 2 | const { RunInstanceLogger } = require('./tools/RunInstanceLogger'); 3 | const { sendEmailNotification, zipErrors } = require('./tools/emailNotifications'); 4 | const { extractDataForPatients } = require('./tools/mcodeExtraction'); 5 | 6 | module.exports = { 7 | mcodeApp, 8 | RunInstanceLogger, 9 | extractDataForPatients, 10 | sendEmailNotification, 11 | zipErrors, 12 | }; 13 | -------------------------------------------------------------------------------- /src/extractors/FHIRObservationExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | class FHIRObservationExtractor extends BaseFHIRExtractor { 4 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 5 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 6 | this.resourceType = 'Observation'; 7 | } 8 | } 9 | 10 | module.exports = { 11 | FHIRObservationExtractor, 12 | }; 13 | -------------------------------------------------------------------------------- /test/application/fixtures/example-condition.csv: -------------------------------------------------------------------------------- 1 | mrn,conditionId,codeSystem,code,displayName,category,dateOfDiagnosis,clinicalStatus,verificationStatus,bodySite,laterality,histology 2 | 456,conditionId-2,http://snomed.info/sct,363346000,Prostate Cancer,problem-list-item,2019-11-11,remission,confirmed,251007,24028007,2735009 3 | 789,conditionId-2,http://snomed.info/sct,363346000,Lung Cancer,encounter-diagnosis,2020-02-14,resolved,confirmed,251007,66459002,2985005 -------------------------------------------------------------------------------- /test/sample-client-data/appointment-information.csv: -------------------------------------------------------------------------------- 1 | mrn,appointmentId,status,serviceCategory,serviceType,appointmentType,specialty,start,end,cancelationCode,description 2 | 123,appointmentId-1,arrived,35,175,CHECKUP,394592004,2019-12-10T09:00:00Z,2019-12-10T11:00:00Z,,"Example description 1" 3 | 456,appointmentId-2,pending,35,175,FOLLOWUP,394592004,2020-12-10T09:00:00Z,2020-12-10T11:00:00Z,,"Example description 2" 4 | 789,appointmentId-3,cancelled,,,,,,,pat-cpp, -------------------------------------------------------------------------------- /docs/cancer-related-medication-administration.csv: -------------------------------------------------------------------------------- 1 | mrn,medicationId,code,codeSystem,displayText,startDate,endDate,treatmentReasonCode,treatmentReasonCodeSystem,treatmentReasonDisplayText,treatmentIntent,status 2 | pat-mrn-1,medicationId-1,code,code-system,code-text,YYYY-MM-DD,YYYY-MM-DD,code,code-system,display-text,intent-code,example-status 3 | pat-mrn-2,medicationId-2,code,code-system,code-text,YYYY-MM-DD,YYYY-MM-DD,code,code-system,display-text,intent-code,example-status -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-observation-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "observationid": "observation-id", 5 | "status": "final", 6 | "code": "1695-6", 7 | "codesystem": "http://loinc.org", 8 | "displayname": "", 9 | "value": "10828004", 10 | "valuecodesystem": "http://snomed.info/sct", 11 | "effectivedate": "2020-01-02", 12 | "bodysite": "12345", 13 | "laterality": "678910" 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /src/extractors/FHIRMedicationOrderExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | class FHIRMedicationOrderExtractor extends BaseFHIRExtractor { 4 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 5 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 6 | this.resourceType = 'MedicationOrder'; 7 | } 8 | } 9 | 10 | module.exports = { 11 | FHIRMedicationOrderExtractor, 12 | }; 13 | -------------------------------------------------------------------------------- /src/extractors/FHIRDocumentReferenceExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | class FHIRDocumentReferenceExtractor extends BaseFHIRExtractor { 4 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 5 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 6 | this.resourceType = 'DocumentReference'; 7 | } 8 | } 9 | 10 | module.exports = { 11 | FHIRDocumentReferenceExtractor, 12 | }; 13 | -------------------------------------------------------------------------------- /src/extractors/FHIRMedicationRequestExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | class FHIRMedicationRequestExtractor extends BaseFHIRExtractor { 4 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 5 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 6 | this.resourceType = 'MedicationRequest'; 7 | } 8 | } 9 | 10 | module.exports = { 11 | FHIRMedicationRequestExtractor, 12 | }; 13 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-patient-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "119147111821125", 4 | "familyname": "Marshall", 5 | "givenname": "Archy", 6 | "gender": "male", 7 | "birthsex": "male", 8 | "dateofbirth":"1994-08-24", 9 | "language": "en", 10 | "addressline": "57 Adams St", 11 | "city": "New Rochelle", 12 | "state": "NY", 13 | "zip": "10801", 14 | "race": "1002-5", 15 | "ethnicity": "2186-5" 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /test/sample-client-data/observation-information.csv: -------------------------------------------------------------------------------- 1 | mrn,observationId,status,code,codeSystem,displayName,value,valueCodeSystem,effectiveDate,bodySite,laterality 2 | 123,observation-id-1,final,1695-6,http://loinc.org,,10828004,http://snomed.info/sct,2020-01-02,251007,66459002 3 | 456,observation-id-2,final,8302-2,http://loinc.org,Body height,66.89 [in_i],,2020-01-02,251007,66459002 4 | 789,observation-id-3,final,29463-7,http://loinc.org,Body Weight,185 [lb_av],,2020-01-02,251007,66459002 -------------------------------------------------------------------------------- /docs/encounter.csv: -------------------------------------------------------------------------------- 1 | mrn,encounterId,status,classCode,classSystem,typeCode,typeSystem,startDate,endDate 2 | mrn-1,exampleId-1,status-code,class-code,http://terminology.hl7.org/CodeSystem/v3-ActCode,type-code,http://snomed.info/sct,YYYY-MM-DD,YYYY-MM-DD 3 | mrn-2,exampleId-2,status-code,class-code,http://terminology.hl7.org/CodeSystem/v3-ActCode,type-code,http://snomed.info/sct,YYYY-MM-DD,YYYY-MM-DD 4 | mrn-3,exampleId-3,status-code,class-code,http://terminology.hl7.org/CodeSystem/v3-ActCode,,,, -------------------------------------------------------------------------------- /test/sample-client-data/encounter-information.csv: -------------------------------------------------------------------------------- 1 | mrn,encounterId,status,classCode,classSystem,typeCode,typeSystem,startDate,endDate 2 | 123,encounterId-1,arrived,AMB,http://terminology.hl7.org/CodeSystem/v3-ActCode,11429006,http://snomed.info/sct,2020-01-10,2020-01-10 3 | 456,encounterId-2,planned,AMB,http://terminology.hl7.org/CodeSystem/v3-ActCode,270427003,http://snomed.info/sct,2021-02-10,2021-05-12 4 | 789,encounterId-3,cancelled,IMP,http://terminology.hl7.org/CodeSystem/v3-ActCode,,,, -------------------------------------------------------------------------------- /src/extractors/FHIRAllergyIntoleranceExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | 4 | class FHIRAllergyIntoleranceExtractor extends BaseFHIRExtractor { 5 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 6 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 7 | this.resourceType = 'AllergyIntolerance'; 8 | } 9 | } 10 | 11 | module.exports = { 12 | FHIRAllergyIntoleranceExtractor, 13 | }; 14 | -------------------------------------------------------------------------------- /src/extractors/FHIRMedicationStatementExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | 3 | class FHIRMedicationStatementExtractor extends BaseFHIRExtractor { 4 | constructor({ baseFhirUrl, requestHeaders, version, searchParameters }) { 5 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 6 | this.resourceType = 'MedicationStatement'; 7 | } 8 | } 9 | 10 | module.exports = { 11 | FHIRMedicationStatementExtractor, 12 | }; 13 | -------------------------------------------------------------------------------- /docs/condition.csv: -------------------------------------------------------------------------------- 1 | mrn,conditionId,codeSystem,code,displayName,category,dateOfDiagnosis,clinicalStatus,verificationStatus,bodySite,laterality,histology 2 | mrn-1,conditionId-1,example-code-system,example-code,Example Name,example-category,YYYY-MM-DD,example-status,example-status,example-site,example-laterality,example-histology 3 | mrn-2,conditionId-2,example-code-system,example-code,Example Name,example-category,YYYY-MM-DD,example-status,example-status,example-site,example-laterality,example-histology -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-appointment-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "appointmentid": "appointmentId-1", 5 | "status": "arrived", 6 | "servicecategory": "35", 7 | "servicetype": "175", 8 | "appointmenttype": "CHECKUP", 9 | "specialty": "394592004", 10 | "start": "2019-12-10T09:00:00+00:00", 11 | "end": "2019-12-10T11:00:00+00:00", 12 | "cancelationcode": "pat-cpp", 13 | "description": "Example description 1" 14 | } 15 | ] -------------------------------------------------------------------------------- /docs/cancer-disease-status.csv: -------------------------------------------------------------------------------- 1 | mrn,conditionId,diseaseStatusCode,diseaseStatusText,dateOfObservation,evidence,observationStatus,dateRecorded 2 | pat-mrn-1,cond-1,268910001,Patient's Condition improved,2019-12-02,363679005|252416005,registered,2020-01-10 3 | pat-mrn-2,cond-2,268910001,Patient's Condition improved,2019-12-03,363679005,amended,2020-01-10 4 | pat-mrn-3,cond-3,260415000,Not detected,2019-12-04,363679005,,2020-01-10 5 | pat-mrn-4,cond-4,268910001,Patient's Condition improved,2019-12-05,,final,2020-05-10 -------------------------------------------------------------------------------- /src/templates/snippets/treatmentReason.js: -------------------------------------------------------------------------------- 1 | const { coding } = require('./coding'); 2 | 3 | function treatmentReasonTemplate({ treatmentReasonCode, treatmentReasonCodeSystem, treatmentReasonDisplayText }) { 4 | return { 5 | reasonCode: [ 6 | { 7 | coding: [coding({ system: treatmentReasonCodeSystem, code: treatmentReasonCode, display: treatmentReasonDisplayText }), 8 | ], 9 | }, 10 | ], 11 | }; 12 | } 13 | 14 | module.exports = { 15 | treatmentReasonTemplate, 16 | }; 17 | -------------------------------------------------------------------------------- /src/extractors/Extractor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | const { NotImplementedError } = require('../helpers/errors'); 3 | 4 | class Extractor { 5 | updateRequestHeaders() { 6 | throw new NotImplementedError('Extractor must implement the "updateRequestHeaders" function if WebService-enabled modules are used'); 7 | } 8 | 9 | get() { 10 | throw new NotImplementedError('Extractor must implement the "get" function'); 11 | } 12 | } 13 | 14 | module.exports = { 15 | Extractor, 16 | }; 17 | -------------------------------------------------------------------------------- /test/extractors/FHIREncounterExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIREncounterExtractor } = require('../../src/extractors/FHIREncounterExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | 6 | const extractor = new FHIREncounterExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 7 | describe('FHIREncounterExtractor', () => { 8 | test('Constructor sets resourceType as Encounter', () => { 9 | expect(extractor.resourceType).toEqual('Encounter'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/extractors/FHIRProcedureExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRProcedureExtractor } = require('../../src/extractors/FHIRProcedureExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | 6 | const extractor = new FHIRProcedureExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 7 | describe('FHIRProcedureExtractor', () => { 8 | test('Constructor sets resourceType as Procedure', () => { 9 | expect(extractor.resourceType).toEqual('Procedure'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/sample-client-data/staging-information.csv: -------------------------------------------------------------------------------- 1 | mrn,conditionId,stageGroup,t,n,m,type,stagingSystem,stagingCodeSystem,effectiveDate 2 | 123,conditionId-1,3C,cT3,cN3,cM0,Clinical,C146985,http://ncimeta.nci.nih.gov,2020-01-01 3 | 456,conditionId-2,3C,cT3,cN3,cM0,Clinical,443830009,http://snomed.info/sct,2020-01-01 4 | 789,conditionId-3,3C,,cN3,cM0,Clinical,444256004,http://snomed.info/sct,2020-01-02 5 | 789,conditionId-3,3C,cT3,,cM0,Clinical,,,2020-01-03 6 | 789,conditionId-3,3C,cT3,cN3,,Clinical,258235000,http://snomed.info/sct,2020-01-04 -------------------------------------------------------------------------------- /test/extractors/FHIRMedicationOrderExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRMedicationOrderExtractor } = require('../../src/extractors/FHIRMedicationOrderExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | 6 | const extractor = new FHIRMedicationOrderExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 7 | describe('FHIRMedicationOrderExtractor', () => { 8 | test('Constructor sets resourceType as MedicationOrder', () => { 9 | expect(extractor.resourceType).toEqual('MedicationOrder'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/extractors/Extractor.test.js: -------------------------------------------------------------------------------- 1 | const { NotImplementedError } = require('../../src/helpers/errors'); 2 | const { Extractor } = require('../../src/extractors'); 3 | 4 | const extractor = new Extractor(); 5 | 6 | describe('base extractor', () => { 7 | test('get should throw NotImplementedError', () => { 8 | expect(() => extractor.get()).toThrow(NotImplementedError); 9 | }); 10 | test('updateRequestHeaders should throw NotImplementedError', () => { 11 | expect(() => extractor.updateRequestHeaders()).toThrow(NotImplementedError); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/extractors/FHIRConditionExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRConditionExtractor } = require('../../src/extractors/FHIRConditionExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | 6 | 7 | const extractor = new FHIRConditionExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 8 | 9 | describe('FHIRConditionExtractor', () => { 10 | describe('Constructor', () => { 11 | test('sets resourceType to Condition', () => { 12 | expect(extractor.resourceType).toEqual('Condition'); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/extractors/FHIRDocumentReferenceExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRDocumentReferenceExtractor } = require('../../src/extractors/FHIRDocumentReferenceExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | 6 | const extractor = new FHIRDocumentReferenceExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 7 | describe('FHIRDocumentReferenceExtractor', () => { 8 | test('Constructor sets resourceType as DocumentReference', () => { 9 | expect(extractor.resourceType).toEqual('DocumentReference'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-condition-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "conditionid": "conditionId-1", 5 | "codesystem": "http://hl7.org/fhir/sid/icd-10-cm", 6 | "code": "C02.0", 7 | "displayname": "Some Cancer Condition", 8 | "category": "example-category", 9 | "dateofdiagnosis": "YYYY-MM-DD", 10 | "clinicalstatus": "example-status", 11 | "verificationstatus": "example-status", 12 | "bodysite": "example-site", 13 | "laterality": "example-laterality", 14 | "histology": "example-histology" 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /test/extractors/FHIRMedicationStatementExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRMedicationStatementExtractor } = require('../../src/extractors/FHIRMedicationStatementExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | 6 | const extractor = new FHIRMedicationStatementExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 7 | describe('FHIRMedicationStatementExtractor', () => { 8 | test('Constructor sets resourceType as MedicationStatement', () => { 9 | expect(extractor.resourceType).toEqual('MedicationStatement'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/sample-client-data/condition-information.csv: -------------------------------------------------------------------------------- 1 | mrn,conditionId,codeSystem,code,displayName,category,dateOfDiagnosis,clinicalStatus,verificationStatus,bodySite,laterality,histology 2 | 123,conditionId-1,http://snomed.info/sct,363346000,Breast Cancer,encounter-diagnosis,2020-07-12,relapse,confirmed,251007,7771000,2424003 3 | 456,conditionId-2,http://snomed.info/sct,363346000,Prostate Cancer,problem-list-item,2019-11-11,remission,confirmed,251007,24028007,2735009 4 | 789,conditionId-3,http://snomed.info/sct,363346000,Lung Cancer,encounter-diagnosis,2020-02-14,resolved,confirmed,251007,66459002,2985005 -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-medication-administration-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "medicationid": "medicationId-1", 5 | "code": "example-code", 6 | "codesystem": "example-code-system", 7 | "displaytext": "Example Text", 8 | "startdate": "YYYY-MM-DD", 9 | "enddate": "YYYY-MM-DD", 10 | "treatmentreasoncode": "example-reason", 11 | "treatmentreasoncodesystem": "example-code-system", 12 | "treatmentreasondisplaytext": "Example Text", 13 | "treatmentintent": "example-code", 14 | "status": "example-status" 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /test/templates/fixtures/research-study-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "ResearchStudy", 3 | "id": "id-for-research-study", 4 | "status": "active", 5 | "site": [ 6 | { 7 | "display": "ID associated with Clinical Trial", 8 | "identifier": { 9 | "system": "EXAMPLE_SITE_SYSTEM", 10 | "value": "EXAMPLE_SITE_ID" 11 | } 12 | } 13 | ], 14 | "identifier": [ 15 | { 16 | "value": "AFT1235", 17 | "system": "example-system", 18 | "type": { 19 | "text": "Clinical Trial Research ID" 20 | } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /docs/procedure.csv: -------------------------------------------------------------------------------- 1 | mrn,procedureId,conditionId,status,code,codeSystem,displayName,reasonCode,reasonCodeSystem,reasonDisplayName,effectiveDate,bodySite,laterality,treatmentIntent 2 | mrn-1,example-id-1,example-condition-id-1,example-status,example-code,example-system,example-name,example-reason-code,example-system,example-reason-display,YYYY-MM-DD,example-site,example-laterality,example-treatment-intent 3 | mrn-2,example-id-2,example-condition-id-2,example-status,example-code,example-system,example-name,example-reason-code,example-system,example-reason-display,YYYY-MM-DD,example-site,example-laterality,example-treatment-intent -------------------------------------------------------------------------------- /test/helpers/fixtures/count-bundle-5-same.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "entry": [ 4 | { 5 | "resource": { 6 | "resourceType": "Resource-1" 7 | } 8 | }, 9 | { 10 | "resource": { 11 | "resourceType": "Resource-1" 12 | } 13 | }, 14 | { 15 | "resource": { 16 | "resourceType": "Resource-1" 17 | } 18 | }, 19 | { 20 | "resource": { 21 | "resourceType": "Resource-1" 22 | } 23 | }, 24 | { 25 | "resource": { 26 | "resourceType": "Resource-1" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/helpers/fixtures/count-bundle-5-unique.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "entry": [ 4 | { 5 | "resource": { 6 | "resourceType": "Resource-1" 7 | } 8 | }, 9 | { 10 | "resource": { 11 | "resourceType": "Resource-2" 12 | } 13 | }, 14 | { 15 | "resource": { 16 | "resourceType": "Resource-3" 17 | } 18 | }, 19 | { 20 | "resource": { 21 | "resourceType": "Resource-4" 22 | } 23 | }, 24 | { 25 | "resource": { 26 | "resourceType": "Resource-5" 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-condition-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Condition", 3 | "id": "example-id", 4 | "category": [ 5 | { 6 | "coding": [ 7 | { 8 | "system": "http://terminology.hl7.org/CodeSystem/condition-category", 9 | "code": "example-code" 10 | } 11 | ] 12 | } 13 | ], 14 | "code": { 15 | "coding": [ 16 | { 17 | "system": "example-system", 18 | "code": "example-code" 19 | } 20 | ] 21 | }, 22 | "subject": { 23 | "reference": "urn:uuid:example-subject-id", 24 | "type": "Patient" 25 | } 26 | } -------------------------------------------------------------------------------- /test/extractors/FHIRAllergyIntoleranceExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRAllergyIntoleranceExtractor } = require('../../src/extractors/FHIRAllergyIntoleranceExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | 6 | const extractor = new FHIRAllergyIntoleranceExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 7 | 8 | describe('FHIRAllergyIntoleranceExtractor', () => { 9 | describe('Constructor', () => { 10 | test('sets resourceType as AllergyIntolerance', () => { 11 | expect(extractor.resourceType).toEqual('AllergyIntolerance'); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/templates/fixtures/research-subject-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "ResearchSubject", 3 | "id": "id-for-research-subject", 4 | "identifier": [ 5 | { 6 | "type": { 7 | "text": "Clinical Trial Subject ID" 8 | }, 9 | "system": "http://example.com/clinicaltrialsubjectids", 10 | "value": "123" 11 | } 12 | ], 13 | "status": "candidate", 14 | "study": { 15 | "reference": "ResearchStudy/rs1" 16 | }, 17 | "individual": { 18 | "reference": "urn:uuid:mCODEPatient1" 19 | }, 20 | "period": { 21 | "start": "2020-01-01", 22 | "end": "2021-01-01" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-procedure-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "procedureid": "procedure-1", 5 | "conditionid": "condition-id", 6 | "status": "completed", 7 | "codesystem": "http://snomed.info/sct", 8 | "code": "152198000", 9 | "displayname": "Brachytherapy (procedure)", 10 | "reasoncode": "example-code", 11 | "reasoncodesystem": "example-system", 12 | "reasondisplayname": "example-name", 13 | "bodysite": "example-site", 14 | "laterality": "example-laterality", 15 | "treatmentintent": "example-treatment-intent", 16 | "effectivedate": "2020-01-01" 17 | } 18 | ] -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-encounter-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Encounter", 3 | "id": "encounterId-1", 4 | "status": "arrived", 5 | "class": { 6 | "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", 7 | "code": "AMB" 8 | }, 9 | "subject": { 10 | "reference": "urn:uuid:123", 11 | "type": "Patient" 12 | }, 13 | "type": [ 14 | { 15 | "coding": [ 16 | { 17 | "system": "http://snomed.info/sct", 18 | "code": "11429006" 19 | } 20 | ] 21 | } 22 | ], 23 | "period": { 24 | "start": "2020-01-10", 25 | "end": "2020-01-10" 26 | } 27 | } -------------------------------------------------------------------------------- /test/extractors/FHIRObservationExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRObservationExtractor } = require('../../src/extractors/FHIRObservationExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | 6 | 7 | // Construct extractor and create spies for mocking responses 8 | const extractor = new FHIRObservationExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 9 | 10 | describe('FHIRObservationExtractor', () => { 11 | describe('Constructor', () => { 12 | test('sets resourceType as Observation', () => { 13 | expect(extractor.resourceType).toEqual('Observation'); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/helpers/fixtures/valueset-without-expansion.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "ValueSet", 3 | "id": "valueset-example", 4 | "compose": { 5 | "include": [ 6 | { 7 | "system": "http://hl7.org/fhir/sid/icd-10-cm", 8 | "concept": [ 9 | { 10 | "code": "C00.0", 11 | "display": "Malignant neoplasm of external upper lip" 12 | } 13 | ] 14 | }, 15 | { 16 | "concept": [ 17 | { 18 | "code": "C00.1", 19 | "display": "Malignant neoplasm of external upper lip" 20 | } 21 | ] 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/extractors/FHIRMedicationRequestExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRMedicationRequestExtractor } = require('../../src/extractors/FHIRMedicationRequestExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | // Construct extractor and create spies for mocking responses 6 | const extractor = new FHIRMedicationRequestExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 7 | 8 | describe('FHIRMedicationRequestExtractor', () => { 9 | describe('Constructor', () => { 10 | test('sets resourceType as MedicationRequest', () => { 11 | expect(extractor.resourceType).toEqual('MedicationRequest'); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-medication-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "MedicationRequest", 3 | "meta": { 4 | "profile": [ 5 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-request" 6 | ] 7 | }, 8 | "status": "example-status", 9 | "intent": "example-intent", 10 | "medicationCodeableConcept": { 11 | "coding": [ 12 | { 13 | "system": "example-code-system", 14 | "code": "example-code" 15 | } 16 | ] 17 | }, 18 | "subject": { 19 | "reference": "urn:uuid:mrn-1", 20 | "type": "Patient" 21 | }, 22 | "requester": { 23 | "reference": "urn:uuid:example-requester" 24 | } 25 | } -------------------------------------------------------------------------------- /src/helpers/lookups/ctcAdverseEventLookup.js: -------------------------------------------------------------------------------- 1 | const { createInvertedLookup, createLowercaseLookup } = require('../lookupUtils'); 2 | 3 | const ctcAEGradeTextToCodeLookup = { 4 | 'Absent Adverse Event': '0', 5 | 'Mild Adverse Event': '1', 6 | 'Moderate Adverse Event': '2', 7 | 'Severe Adverse Event': '3', 8 | 'Life Threatening or Disabling Adverse Event': '4', 9 | 'Death Related to Adverse Event': '5', 10 | }; 11 | 12 | const ctcAEGradeCodeToTextLookup = createInvertedLookup(ctcAEGradeTextToCodeLookup); 13 | 14 | module.exports = { 15 | ctcAEGradeCodeToTextLookup: createLowercaseLookup(ctcAEGradeCodeToTextLookup), 16 | ctcAEGradeTextToCodeLookup: createLowercaseLookup(ctcAEGradeTextToCodeLookup), 17 | }; 18 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "airbnb-base" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2018 17 | }, 18 | "rules": { 19 | "no-console": "off", 20 | "max-len": ["error", { "code": 200 }], 21 | "object-curly-newline": ["error", { 22 | "ObjectPattern": { "minProperties": 5 } 23 | }], 24 | "no-underscore-dangle": ["error", { "allow": ["__get__", "__set__","_include"] }] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/helpers/logger.js: -------------------------------------------------------------------------------- 1 | const { loggers, format, transports } = require('winston'); 2 | 3 | const LOGGER_NAME = 'cli'; 4 | const logFormat = format.printf(({ level, message, timestamp }) => (`${timestamp} [${level}]: ${message}`)); 5 | 6 | if (!loggers.has(LOGGER_NAME)) { 7 | loggers.add(LOGGER_NAME, { 8 | level: process.env.LOGGING || 'info', 9 | format: format.combine( 10 | format.colorize(), 11 | format.timestamp({ format: 'HH:mm:ss.SS' }), 12 | format.align(), 13 | logFormat, 14 | ), 15 | transports: [ 16 | new transports.Console({ 17 | silent: process.env.LOGGING === 'none', 18 | }), 19 | ], 20 | }); 21 | } 22 | 23 | module.exports = loggers.get(LOGGER_NAME); 24 | -------------------------------------------------------------------------------- /docs/cancer-related-medication-request.csv: -------------------------------------------------------------------------------- 1 | mrn,requestId,code,codeSystem,displayText,treatmentReasonCode,treatmentReasonCodeSystem,treatmentReasonDisplayText,procedureIntent,status,intent,authoredOn,requesterId,dosageRoute,asNeededCode,doseRateType,doseQuantityValue,doseQuantityUnit,timingCode,timingEvent 2 | mrn-1,requestId-1,code,code-system,code-text,code,code-system,display-text,procedure-intent-code,status-code,intent-code,YYY-MM-DD,requesterId-1,route-code,needed-code,rate-type,quantity-value,quantity-unit,timing-code,YYYY-MM-DD 3 | mrn-2,requestId-2,code,code-system,code-text,code,code-system,display-text,procedure-intent-code,status-code,intent-code,YYY-MM-DD,requesterId-2,route-code,needed-code,rate-type,quantity-value,quantity-unit,timing-code,YYYY-MM-DD -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-medication-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "MedicationAdministration", 3 | "meta": { 4 | "profile": [ 5 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-administration" 6 | ] 7 | }, 8 | "status": "example-status", 9 | "medicationCodeableConcept": { 10 | "coding": [ 11 | { 12 | "system": "example-code-system", 13 | "code": "example-code" 14 | } 15 | ] 16 | }, 17 | "subject": { 18 | "reference": "urn:uuid:mrn-1", 19 | "type": "Patient" 20 | }, 21 | "effectivePeriod": { 22 | "extension": [ 23 | { 24 | "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", 25 | "valueCode": "unknown" 26 | } 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /.github/workflows/ci-workflow.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Lint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '12.x' 14 | - run: npm ci 15 | - run: npm run lint 16 | env: 17 | CI: true 18 | test: 19 | name: Tests on ${{ matrix.os }} 20 | runs-on: ${{ matrix.os }} 21 | strategy: 22 | matrix: 23 | os: [ubuntu-latest, windows-latest, macos-latest] 24 | steps: 25 | - uses: actions/checkout@v1 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: '12.x' 29 | - run: npm ci 30 | - run: npm test 31 | env: 32 | CI: true -------------------------------------------------------------------------------- /docs/adverse-event.csv: -------------------------------------------------------------------------------- 1 | mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate 2 | mrn-full-example,example-id-1,event-code,code-system,code-display,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code,code-system,category-dislpay,mild,actual,id,1994-12-09,1994-12-09 3 | mrn-two-category-example,example-id-2,event-code,code-system,code-display,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code|category-code,code-system|code-system,category-display|category-display,mild,actual,id,1994-12-09,1994-12-09 4 | mrn-minimal-example,,code-from-default-system,,,,,,,,,,,,,,1994-12-09, 5 | -------------------------------------------------------------------------------- /test/sample-client-data/cancer-related-medication-administration-information.csv: -------------------------------------------------------------------------------- 1 | mrn,medicationId,code,codeSystem,displayText,startDate,endDate,treatmentReasonCode,treatmentReasonCodeSystem,treatmentReasonDisplayText,treatmentIntent,status 2 | 123,medicationId-1,10760,http://www.nlm.nih.gov/research/umls/rxnorm,Triamcinolone Oral Paste,2020-01-01,2020-07-05,999000,http://snomed.info/sct,Mixed islet cell and exocrine adenocarcinoma,373808002,on-hold 3 | 456,medicationId-2,91318,http://www.nlm.nih.gov/research/umls/rxnorm,Coal Tar Topical Solution,2020-02-17,2020-08-13,915007,http://snomed.info/sct,Malignant melanoma in junctional nevus,373808002,completed 4 | 789,medicationId-3,91833,http://www.nlm.nih.gov/research/umls/rxnorm,Vitamin K1 Injectable Solution [Aquamephyton],2020-01-12,2020-10-01,900006,http://snomed.info/sct,Mucin-producing adenocarcinoma,363676003,stopped -------------------------------------------------------------------------------- /test/helpers/fixtures/valueset-with-expansion.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "ValueSet", 3 | "id": "valueset-example", 4 | "compose": { 5 | "include": [ 6 | { 7 | "system": "http://hl7.org/fhir/sid/icd-10-cm", 8 | "concept": [ 9 | { 10 | "code": "C00.0", 11 | "display": "Malignant neoplasm of external upper lip" 12 | } 13 | ] 14 | } 15 | ] 16 | }, 17 | "expansion": { 18 | "timestamp": "2020-04-02T12:54:59-0400", 19 | "contains": [ 20 | { 21 | "code": "C00.1", 22 | "display": "Malignant neoplasm of external lower lip" 23 | }, 24 | { 25 | "code": "C00.2", 26 | "display": "Malignant neoplasm of external lower lip", 27 | "system": "http://hl7.org/fhir/sid/icd-10-cm" 28 | } 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/extractors/FHIRPatientExtractor.test.js: -------------------------------------------------------------------------------- 1 | const { FHIRPatientExtractor } = require('../../src/extractors/FHIRPatientExtractor.js'); 2 | 3 | const MOCK_URL = 'http://example.com'; 4 | const MOCK_HEADERS = {}; 5 | const MOCK_MRN = '123456789'; 6 | 7 | const extractor = new FHIRPatientExtractor({ baseFhirUrl: MOCK_URL, requestHeaders: MOCK_HEADERS }); 8 | describe('FHIRPatientExtractor', () => { 9 | test('Constructor sets resourceType as Patient', () => { 10 | expect(extractor.resourceType).toEqual('Patient'); 11 | }); 12 | 13 | test('parametrizeArgsForFHIRModule should translate the MRN value into the identifier param', async () => { 14 | const params = await extractor.parametrizeArgsForFHIRModule({ mrn: MOCK_MRN }); 15 | expect(params).toHaveProperty('identifier'); 16 | expect(params.identifier).toEqual(`MRN|${MOCK_MRN}`); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-ctc-adverse-event-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "AdverseEvent", 3 | "id": "adverseEventId-1", 4 | "subject": { 5 | "reference": "urn:uuid:mrn-1", 6 | "type": "Patient" 7 | }, 8 | "extension" : [ 9 | { 10 | "url" : "http://hl7.org/fhir/us/ctcae/StructureDefinition/ctcae-grade", 11 | "valueCodeableConcept" : { 12 | "coding" : [ 13 | { 14 | "system" : "http://hl7.org/fhir/us/ctcae/CodeSystem/ctcae-grade-code-system", 15 | "code" : "2", 16 | "display" : "Moderate Adverse Event" 17 | } 18 | ] 19 | } 20 | } 21 | ], 22 | "event": { 23 | "coding": [ 24 | { 25 | "system": "code-system", 26 | "code": "109006" 27 | } 28 | ] 29 | }, 30 | "actuality": "actual", 31 | "date": "1994-12-09" 32 | } -------------------------------------------------------------------------------- /src/templates/snippets/coding.js: -------------------------------------------------------------------------------- 1 | const { ifSomeArgsObj } = require('../../helpers/templateUtils'); 2 | 3 | // Template for FHIR Coding, based on https://www.hl7.org/fhir/datatypes.html#Coding 4 | function coding({ 5 | system, version, code, display, userSelected, 6 | }) { 7 | return ifSomeArgsObj( 8 | ({ 9 | system: system_, version: version_, code: code_, display: display_, userSelected: userSelected_, // using the _ to avoid duplicating vars across scopes 10 | }) => ({ 11 | ...(system_ && { system: system_ }), 12 | ...(version_ && { version: version_ }), 13 | ...(code_ && { code: code_ }), 14 | ...(display_ && { display: display_ }), 15 | ...(userSelected_ && { userSelected: userSelected_ }), 16 | }), 17 | )({ system, version, code, display, userSelected }); 18 | } 19 | 20 | module.exports = { 21 | coding, 22 | }; 23 | -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-observation-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "status": "final", 5 | "category": [ 6 | { 7 | "coding": [ 8 | { 9 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 10 | "code": "vital-signs", 11 | "display": "Vital Signs" 12 | } 13 | ] 14 | } 15 | ], 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "8302-2" 21 | 22 | } 23 | ] 24 | }, 25 | "subject" : { 26 | "reference": "urn:uuid:example-mrn", 27 | "type": "Patient" 28 | }, 29 | "effectiveDateTime" : "2020-01-01", 30 | "valueQuantity": { 31 | "value": 70, 32 | "unit": "in", 33 | "code": "[in_i]", 34 | "system": "http://unitsofmeasure.org" 35 | } 36 | } -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-medication-request-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "requestid": "requestId-1", 5 | "code": "example-code", 6 | "codesystem": "example-code-system", 7 | "displaytext": "Example Text", 8 | "authoredon": "YYYY-MM-DD", 9 | "treatmentreasoncode": "example-reason", 10 | "treatmentreasoncodesystem": "example-code-system", 11 | "treatmentreasondisplaytext": "Example Text", 12 | "procedureintent": "example-code", 13 | "status": "example-status", 14 | "requesterid": "example-requester", 15 | "intent": "example-intent", 16 | "dosageroute": "example-route", 17 | "asneededcode": "example-asneeded", 18 | "doseratetype": "example-type", 19 | "dosequantityvalue": "111", 20 | "dosequantityunit": "example-unit", 21 | "timingevent": "YYYY-MM-DD", 22 | "timingcode": "example-code" 23 | } 24 | ] -------------------------------------------------------------------------------- /src/templates/snippets/bodySiteTemplate.js: -------------------------------------------------------------------------------- 1 | const { valueX } = require('./valueX'); 2 | const { coding } = require('./coding'); 3 | const { ifAllArgsObj } = require('../../helpers/templateUtils'); 4 | 5 | function lateralityTemplate({ laterality }) { 6 | return { 7 | extension: [ 8 | { 9 | url: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-laterality', 10 | ...valueX({ code: laterality, system: 'http://snomed.info/sct' }, 'valueCodeableConcept'), 11 | }, 12 | ], 13 | }; 14 | } 15 | 16 | function bodySiteTemplate({ bodySite, laterality }) { 17 | if (!bodySite) return null; 18 | 19 | return { 20 | bodySite: { 21 | ...ifAllArgsObj(lateralityTemplate)({ laterality }), 22 | coding: [coding({ 23 | system: 'http://snomed.info/sct', 24 | code: bodySite, 25 | })], 26 | }, 27 | }; 28 | } 29 | 30 | module.exports = { 31 | bodySiteTemplate, 32 | }; 33 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-adverse-event-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "adverseeventid": "adverseEventId-1", 5 | "adverseeventcode": "109006", 6 | "adverseeventcodesystem": "code-system", 7 | "adverseeventdisplaytext": "Anxiety disorder of childhood OR adolescence", 8 | "suspectedcauseid": "procedure-id", 9 | "suspectedcausetype": "Procedure", 10 | "seriousness": "serious", 11 | "seriousnesscodesystem": "http://terminology.hl7.org/CodeSystem/adverse-event-seriousness", 12 | "seriousnessdisplaytext": "Serious", 13 | "category": "product-use-error", 14 | "categorycodesystem": "http://terminology.hl7.org/CodeSystem/adverse-event-category", 15 | "categorydisplaytext": "Product Use Error", 16 | "severity": "severe", 17 | "actuality": "actual", 18 | "studyid": "researchId-1", 19 | "effectivedate": "12-09-1994", 20 | "recordeddate": "12-09-1994" 21 | } 22 | ] -------------------------------------------------------------------------------- /src/client/MCODEClient.js: -------------------------------------------------------------------------------- 1 | const { BaseClient } = require('./BaseClient'); 2 | const { allExtractors, dependencyInfo } = require('../extractors'); 3 | const { sortExtractors } = require('../helpers/dependencyUtils.js'); 4 | 5 | class MCODEClient extends BaseClient { 6 | constructor({ extractors, commonExtractorArgs, webServiceAuthConfig }) { 7 | super(); 8 | this.registerExtractors(...allExtractors); 9 | // Store the extractors defined by the configuration file as local state 10 | this.extractorConfig = extractors; 11 | // Sort extractors based on order and dependencies 12 | this.extractorConfig = sortExtractors(this.extractorConfig, dependencyInfo); 13 | // Store webServiceAuthConfig if provided` 14 | this.authConfig = webServiceAuthConfig; 15 | this.commonExtractorArgs = { 16 | implementation: 'mcode', 17 | ...commonExtractorArgs, 18 | }; 19 | } 20 | } 21 | module.exports = { 22 | MCODEClient, 23 | }; 24 | -------------------------------------------------------------------------------- /test/sample-client-data/procedure-information.csv: -------------------------------------------------------------------------------- 1 | mrn,procedureId,conditionId,status,code,codeSystem,displayName,reasonCode,reasonCodeSystem,reasonDisplayName,effectiveDate,bodySite,laterality,treatmentIntent 2 | 123,procedure-id-1,condition-id-1,completed,152198000,http://snomed.info/sct,Brachytherapy (procedure),363346000,http://snomed.info/sct,Malignant tumor,2020-01-01,41224006,51440002,373808002 3 | 456,procedure-id-2,condition-id-2,in-progress,174337000,http://snomed.info/sct,Destruction of lesion,363346000,http://snomed.info/sct,Malignant tumor,2020-01-12,41224006,51440002,373808002 4 | 789,procedure-id-3,condition-id-3,completed,172043006,http://snomed.info/sct,Total mastectomy,363346000,http://snomed.info/sct,Malignant tumor,2020-06-30,41224006,51440002,373808002 5 | 789,procedure-id-4,condition-id-3,in-progress,10611004,http://snomed.info/sct,Teleradiotherapy protons (procedure),363346000,http://snomed.info/sct,Malignant tumor,2020-01-12,41224006,51440002,373808002 -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-encounter-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "urn:uuid:encounterId-1", 7 | "resource": { 8 | "resourceType": "Encounter", 9 | "id": "encounterId-1", 10 | "status": "arrived", 11 | "class": { 12 | "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", 13 | "code": "AMB" 14 | }, 15 | "subject": { 16 | "reference": "urn:uuid:mrn-1", 17 | "type": "Patient" 18 | }, 19 | "type": [ 20 | { 21 | "coding": [ 22 | { 23 | "system": "http://snomed.info/sct", 24 | "code": "11429006" 25 | } 26 | ] 27 | } 28 | ], 29 | "period": { 30 | "start": "2020-01-10", 31 | "end": "2020-01-10" 32 | } 33 | } 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /src/templates/snippets/extension.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { ifSomeArgs } = require('../../helpers/templateUtils'); 3 | 4 | function extensionArr(...extensions) { // 0. Spread since 1..n extensions 5 | // Extensions should always return if at least one is provided; use ifSomeArgs 6 | return ifSomeArgs( 7 | (...extArr) => ({ extension: _.compact(extArr) }), // 2. Spread since 1...n extensions; _.compact to drop the falsy ones 8 | )(...extensions); // 1. Spread to pass each extension individually 9 | } 10 | 11 | // See http://hl7.org/fhir/R4/extension-data-absent-reason.html 12 | // reasonCode is any code from Value Set http://hl7.org/fhir/R4/valueset-data-absent-reason.html 13 | function dataAbsentReasonExtension(reasonCode) { 14 | return { 15 | url: 'http://hl7.org/fhir/StructureDefinition/data-absent-reason', 16 | valueCode: reasonCode, 17 | }; 18 | } 19 | 20 | module.exports = { 21 | dataAbsentReasonExtension, 22 | extensionArr, 23 | }; 24 | -------------------------------------------------------------------------------- /test/templates/fixtures/patient-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "id": "SomeId", 4 | "identifier": [ 5 | { 6 | "type": { 7 | "coding": [ 8 | { 9 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203", 10 | "code": "MR", 11 | "display": "Medical Record Number" 12 | } 13 | ], 14 | "text": "Medical Record Number" 15 | }, 16 | "system": "http://example.com/system/mrn", 17 | "value": "1234" 18 | } 19 | ], 20 | "name": [ 21 | { 22 | "extension": [ 23 | { 24 | "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", 25 | "valueCode": "unknown" 26 | } 27 | ] 28 | } 29 | ], 30 | "_gender": { 31 | "extension": [ 32 | { 33 | "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", 34 | "valueCode": "unknown" 35 | } 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/modules/BaseFHIRModule.js: -------------------------------------------------------------------------------- 1 | const { FHIRClient } = require('fhir-crud-client'); 2 | const logger = require('../helpers/logger'); 3 | const { getBundleResourcesByType, logOperationOutcomeInfo } = require('../helpers/fhirUtils'); 4 | 5 | class BaseFHIRModule { 6 | constructor(baseUrl, requestHeaders) { 7 | this.baseUrl = baseUrl; 8 | this.client = new FHIRClient(this.baseUrl, requestHeaders); 9 | } 10 | 11 | updateRequestHeaders(newHeaders) { 12 | this.client.updateRequestHeaders(newHeaders); 13 | } 14 | 15 | async search(resourceType, params) { 16 | logger.debug(`GET ${this.baseUrl}/${resourceType}`); 17 | const result = await this.client.search({ resourceType, params }); 18 | const operationOutcomeEntry = getBundleResourcesByType(result, 'OperationOutcome', {}, true); 19 | if (operationOutcomeEntry) { 20 | logOperationOutcomeInfo(operationOutcomeEntry); 21 | } 22 | return result; 23 | } 24 | } 25 | 26 | module.exports = { 27 | BaseFHIRModule, 28 | }; 29 | -------------------------------------------------------------------------------- /test/helpers/fixtures/count-bundle-5-nested.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "entry": [ 4 | { 5 | "resource": { 6 | "resourceType": "Resource-1" 7 | }, 8 | "contained": [ 9 | { 10 | "resource": { 11 | "resourceType": "Resource-1" 12 | }, 13 | "contained": [ 14 | { 15 | "resource": { 16 | "resourceType": "Resource-1" 17 | }, 18 | "contained": [ 19 | { 20 | "resource": { 21 | "resourceType": "Resource-1" 22 | }, 23 | "contained": [ 24 | { 25 | "resource": { 26 | "resourceType": "Resource-1" 27 | } 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /test/sample-client-data/cancer-related-medication-request-information.csv: -------------------------------------------------------------------------------- 1 | mrn,requestId,code,codeSystem,displayText,treatmentReasonCode,treatmentReasonCodeSystem,treatmentReasonDisplayText,procedureIntent,status,intent,authoredOn,requesterId,dosageRoute,asNeededCode,doseRateType,doseQuantityValue,doseQuantityUnit,timingCode,timingEvent 2 | 123,requestId-1,10760,http://www.nlm.nih.gov/research/umls/rxnorm,Triamcinolone Oral Paste,999000,http://snomed.info/sct,Mixed islet cell and exocrine adenocarcinoma,373808002,active,order,2020-01-01,requester-1,26643006,1152001,calculated,100,mg,QD,2023-01-01 3 | 456,requestId-2,91318,http://www.nlm.nih.gov/research/umls/rxnorm,Coal Tar Topical Solution,915007,http://snomed.info/sct,Malignant melanoma in junctional nevus,373808002,on-hold,proposal,2019-02-02,requester-2,46713006,1140008,ordered,50,mg,AM,2023-04-04 4 | 789,requestId-3,91833,http://www.nlm.nih.gov/research/umls/rxnorm,Vitamin K1 Injectable Solution [Aquamephyton],900006,http://snomed.info/sct,Mucin-producing adenocarcinoma,363676003,cancelled,plan,,requester-3,,,,,,, 5 | -------------------------------------------------------------------------------- /src/templates/snippets/identifier.js: -------------------------------------------------------------------------------- 1 | const { ifSomeArgs, ifSomeArgsObj } = require('../../helpers/templateUtils'); 2 | 3 | function identifier({ system, type, value }) { 4 | return ifSomeArgsObj( 5 | ({ system: system_, type: type_, value: value_ }) => ({ 6 | identifier: { 7 | ...(system_ && { system: system_ }), 8 | ...(value_ && { value: value_ }), 9 | ...(type_ && { type: type_ }), 10 | }, 11 | }), 12 | )({ system, type, value }); 13 | } 14 | 15 | // Transform a list of identifier-objects into a list of identifier vales. Shape of return value below 16 | // identifier: [ 17 | // { system: "http://sys.com", value:"12345"}, 18 | // { system: "http://sys.com", value:"54321"}, 19 | // ] 20 | function identifierArr(...identifiers) { 21 | return ifSomeArgs( 22 | (...idenArr) => ({ identifier: idenArr.map((idenData) => identifier(idenData).identifier) }), 23 | )(...identifiers); // 1. Spread to pass each extension individually 24 | } 25 | 26 | module.exports = { 27 | identifier, 28 | identifierArr, 29 | }; 30 | -------------------------------------------------------------------------------- /docs/ctc-adverse-event.csv: -------------------------------------------------------------------------------- 1 | mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventCodeVersion,adverseEventDisplayText,adverseEventText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,studyId,effectiveDate,recordedDate,grade,expectation,resolvedDate,seriousnessOutcome,actor,functionCode 2 | mrn-full-example,example-id-1,event-code,code-system,code-version,code-display,event-text,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code,code-system,category-dislpay,id,1994-12-09,1994-12-09,1,expectation-code,YYYY-MM-DD,seriousness-outcome-code,practitioner-id,function-code 3 | mrn-two-category-example,example-id-2,event-code,code-system,code-version,code-display,event-text,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code,code-system,category-dislpay,id,1994-12-09,1994-12-09,1,expectation-code,YYYY-MM-DD,seriousness-outcome-code,practitioner-id, 4 | mrn-minimal-example,,code-from-default-system,,,,,,,,,,,,,1994-12-09,,1,,,,,, 5 | -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-staging-clinical-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-stage-group" 7 | ] 8 | }, 9 | "status": "final", 10 | "category": [ 11 | { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 15 | "code": "survey" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "21908-9" 25 | } 26 | ] 27 | }, 28 | "subject": { 29 | "reference": "urn:uuid:example-mrn", 30 | "type": "Patient" 31 | }, 32 | "effectiveDateTime": "2020-01-01", 33 | "valueCodeableConcept": { 34 | "coding": [ 35 | { 36 | "system": "http://cancerstaging.org", 37 | "code": "3C" 38 | } 39 | ] 40 | }, 41 | "focus": [ 42 | { 43 | "reference": "urn:uuid:example-condition-id", 44 | "type": "Condition" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-staging-pathologic-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-stage-group" 7 | ] 8 | }, 9 | "status": "final", 10 | "category": [ 11 | { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 15 | "code": "laboratory" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "21902-2" 25 | } 26 | ] 27 | }, 28 | "subject": { 29 | "reference": "urn:uuid:example-mrn", 30 | "type": "Patient" 31 | }, 32 | "effectiveDateTime": "2020-01-01", 33 | "valueCodeableConcept": { 34 | "coding": [ 35 | { 36 | "system": "http://cancerstaging.org", 37 | "code": "3C" 38 | } 39 | ] 40 | }, 41 | "focus": [ 42 | { 43 | "reference": "urn:uuid:example-condition-id", 44 | "type": "Condition" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /test/templates/fixtures/nodes-category-clinical-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-regional-nodes-category" 7 | ] 8 | }, 9 | "status": "final", 10 | "category": [ 11 | { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 15 | "code": "survey" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "21906-3" 25 | } 26 | ] 27 | }, 28 | "subject": { 29 | "reference": "urn:uuid:example-mrn", 30 | "type": "Patient" 31 | }, 32 | "effectiveDateTime": "2020-01-01", 33 | "valueCodeableConcept": { 34 | "coding": [ 35 | { 36 | "system": "http://cancerstaging.org", 37 | "code": "cN3" 38 | } 39 | ] 40 | }, 41 | "focus": [ 42 | { 43 | "reference": "urn:uuid:example-condition-id", 44 | "type": "Condition" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /test/templates/fixtures/tumor-category-clinical-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-primary-tumor-category" 7 | ] 8 | }, 9 | "status": "final", 10 | "category": [ 11 | { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 15 | "code": "survey" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "21905-5" 25 | } 26 | ] 27 | }, 28 | "subject": { 29 | "reference": "urn:uuid:example-mrn", 30 | "type": "Patient" 31 | }, 32 | "effectiveDateTime": "2020-01-01", 33 | "valueCodeableConcept": { 34 | "coding": [ 35 | { 36 | "system": "http://cancerstaging.org", 37 | "code": "cT3" 38 | } 39 | ] 40 | }, 41 | "focus": [ 42 | { 43 | "reference": "urn:uuid:example-condition-id", 44 | "type": "Condition" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /test/templates/fixtures/nodes-category-pathologic-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-regional-nodes-category" 7 | ] 8 | }, 9 | "status": "final", 10 | "category": [ 11 | { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 15 | "code": "laboratory" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "21900-6" 25 | } 26 | ] 27 | }, 28 | "subject": { 29 | "reference": "urn:uuid:example-mrn", 30 | "type": "Patient" 31 | }, 32 | "effectiveDateTime": "2020-01-01", 33 | "valueCodeableConcept": { 34 | "coding": [ 35 | { 36 | "system": "http://cancerstaging.org", 37 | "code": "pN3" 38 | } 39 | ] 40 | }, 41 | "focus": [ 42 | { 43 | "reference": "urn:uuid:example-condition-id", 44 | "type": "Condition" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /test/templates/fixtures/tumor-category-pathologic-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-primary-tumor-category" 7 | ] 8 | }, 9 | "status": "final", 10 | "category": [ 11 | { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 15 | "code": "laboratory" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "21899-0" 25 | } 26 | ] 27 | }, 28 | "subject": { 29 | "reference": "urn:uuid:example-mrn", 30 | "type": "Patient" 31 | }, 32 | "effectiveDateTime": "2020-01-01", 33 | "valueCodeableConcept": { 34 | "coding": [ 35 | { 36 | "system": "http://cancerstaging.org", 37 | "code": "pT3" 38 | } 39 | ] 40 | }, 41 | "focus": [ 42 | { 43 | "reference": "urn:uuid:example-condition-id", 44 | "type": "Condition" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /test/templates/fixtures/metastases-category-clinical-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-distant-metastases-category" 7 | ] 8 | }, 9 | "status": "final", 10 | "category": [ 11 | { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 15 | "code": "survey" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "21907-1" 25 | } 26 | ] 27 | }, 28 | "subject": { 29 | "reference": "urn:uuid:example-mrn", 30 | "type": "Patient" 31 | }, 32 | "effectiveDateTime": "2020-01-01", 33 | "valueCodeableConcept": { 34 | "coding": [ 35 | { 36 | "system": "http://cancerstaging.org", 37 | "code": "cM0" 38 | } 39 | ] 40 | }, 41 | "focus": [ 42 | { 43 | "reference": "urn:uuid:example-condition-id", 44 | "type": "Condition" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /test/templates/fixtures/metastases-category-pathologic-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-distant-metastases-category" 7 | ] 8 | }, 9 | "status": "final", 10 | "category": [ 11 | { 12 | "coding": [ 13 | { 14 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 15 | "code": "laboratory" 16 | } 17 | ] 18 | } 19 | ], 20 | "code": { 21 | "coding": [ 22 | { 23 | "system": "http://loinc.org", 24 | "code": "21901-4" 25 | } 26 | ] 27 | }, 28 | "subject": { 29 | "reference": "urn:uuid:example-mrn", 30 | "type": "Patient" 31 | }, 32 | "effectiveDateTime": "2020-01-01", 33 | "valueCodeableConcept": { 34 | "coding": [ 35 | { 36 | "system": "http://cancerstaging.org", 37 | "code": "pM0" 38 | } 39 | ] 40 | }, 41 | "focus": [ 42 | { 43 | "reference": "urn:uuid:example-condition-id", 44 | "type": "Condition" 45 | } 46 | ] 47 | } -------------------------------------------------------------------------------- /src/templates/snippets/index.js: -------------------------------------------------------------------------------- 1 | const { coding } = require('./coding'); 2 | const { valueX } = require('./valueX'); 3 | const { reference } = require('./reference'); 4 | const { meta, narrative } = require('./resource'); 5 | const { extensionArr, dataAbsentReasonExtension } = require('./extension'); 6 | const { effectiveX } = require('./effectiveX'); 7 | const { identifier, identifierArr } = require('./identifier'); 8 | const { bodySiteTemplate } = require('./bodySiteTemplate'); 9 | const { stagingMethodTemplate } = require('./cancerStaging'); 10 | const { medicationTemplate } = require('./medication'); 11 | const { subjectTemplate } = require('./subject'); 12 | const { treatmentReasonTemplate } = require('./treatmentReason'); 13 | const { periodTemplate } = require('./period'); 14 | 15 | module.exports = { 16 | bodySiteTemplate, 17 | coding, 18 | dataAbsentReasonExtension, 19 | effectiveX, 20 | extensionArr, 21 | identifier, 22 | identifierArr, 23 | medicationTemplate, 24 | meta, 25 | narrative, 26 | periodTemplate, 27 | reference, 28 | stagingMethodTemplate, 29 | subjectTemplate, 30 | treatmentReasonTemplate, 31 | valueX, 32 | }; 33 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-ctc-adverse-event-module-response.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mrn": "mrn-1", 4 | "adverseeventid": "adverseEventId-1", 5 | "adverseeventcode": "109006", 6 | "adverseeventcodesystem": "code-system", 7 | "adverseeventcodeversion": "code-version", 8 | "adverseeventdisplaytext": "Anxiety disorder of childhood OR adolescence", 9 | "adverseeventtext": "event-text", 10 | "suspectedcauseid": "procedure-id", 11 | "suspectedcausetype": "Procedure", 12 | "seriousness": "serious", 13 | "seriousnesscodesystem": "http://terminology.hl7.org/CodeSystem/adverse-event-seriousness", 14 | "seriousnessdisplaytext": "Serious", 15 | "category": "product-use-error", 16 | "categorycodesystem": "http://terminology.hl7.org/CodeSystem/adverse-event-category", 17 | "categorydisplaytext": "Product Use Error", 18 | "studyid": "researchId-1", 19 | "effectivedate": "12-09-1994", 20 | "recordeddate": "12-09-1994", 21 | "grade": "1", 22 | "resolveddate": "2021-12-01", 23 | "seriousnessoutcome": "C113380", 24 | "expectation": "C41333", 25 | "actor": "practitioner-id", 26 | "functioncode": "INF" 27 | } 28 | ] -------------------------------------------------------------------------------- /test/sample-client-data/adverse-event-information.csv: -------------------------------------------------------------------------------- 1 | mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate 2 | 123,adverseEventId-1,109006,code-system,Anxiety disorder of childhood OR adolescence,procedure-id,Procedure,serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Serious,product-use-error|product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|http://snomed.info/sct|http://terminology.hl7.org/CodeSystem/adverse-event-category,Product Use Error|Product Quality|Wrong Rate,severe,actual,researchId-1,12-09-1994,12-09-1994 3 | 456,adverseEventId-2,134006,http://snomed.info/sct,Decreased hair growth,medicationId-1,Medication,non-serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Non-serious,product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|,Product Quality|,mild,potential,researchId-2,12-10-1995,12-10-1995 4 | 789,adverseEventId-3,150003,,,,,,,,product-use-error,,,,,,12-09-1994, -------------------------------------------------------------------------------- /src/extractors/FHIRPatientExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | const { maskPatientData } = require('../helpers/patientUtils.js'); 3 | 4 | class FHIRPatientExtractor extends BaseFHIRExtractor { 5 | constructor({ 6 | baseFhirUrl, requestHeaders, version, mask = [], searchParameters, 7 | }) { 8 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 9 | this.resourceType = 'Patient'; 10 | this.mask = mask; 11 | } 12 | 13 | // Override default behavior for PatientExtractor; just use MRN directly 14 | // eslint-disable-next-line class-methods-use-this 15 | async parametrizeArgsForFHIRModule({ mrn }) { 16 | return { 17 | identifier: `MRN|${mrn}`, 18 | }; 19 | } 20 | 21 | async get(argumentObject) { 22 | const bundle = await super.get(argumentObject); 23 | // mask specified fields in the patient data 24 | if (typeof this.mask === 'string' && this.mask === 'all') { 25 | maskPatientData(bundle, [], true); 26 | } else if (this.mask.length > 0) maskPatientData(bundle, this.mask); 27 | return bundle; 28 | } 29 | } 30 | 31 | module.exports = { 32 | FHIRPatientExtractor, 33 | }; 34 | -------------------------------------------------------------------------------- /test/application/app.test.js: -------------------------------------------------------------------------------- 1 | const rewire = require('rewire'); 2 | 3 | const app = rewire('../../src/application/app.js'); 4 | const checkInputAndConfig = app.__get__('checkInputAndConfig'); 5 | 6 | describe('App Tests', () => { 7 | describe('checkInputAndConfig', () => { 8 | const config = { patientIdCsvPath: '', extractors: [] }; 9 | it('should throw error when fromDate is invalid.', () => { 10 | expect(() => checkInputAndConfig(config, '2020-06-31')).toThrowError('-f/--from-date is not a valid date.'); 11 | }); 12 | it('should throw error when toDate is invalid date.', () => { 13 | expect(() => checkInputAndConfig(config, '2020-06-30', '2020-06-31')).toThrowError('-t/--to-date is not a valid date.'); 14 | }); 15 | it('should throw error when config is not valid', () => { 16 | expect(() => checkInputAndConfig({})) 17 | .toThrowError('Error(s) found in config file: config should have required property \'patientIdCsvPath\', config should have required property \'extractors\''); 18 | }); 19 | it('should not throw error when all args are valid', () => { 20 | expect(() => checkInputAndConfig(config, '2020-06-01', '2020-06-30')).not.toThrowError(); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/modules/fixtures/patient-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "id": "mCODEPatientExample01", 4 | "meta": { 5 | "profile": ["http://hl7.org/fhir/us/mcode/StructureDefinition/obf-Patient"] 6 | }, 7 | "identifier": [ 8 | { 9 | "use": "usual", 10 | "system": "http://example.com/clinicaltrialids", 11 | "type": { 12 | "text": "Clinical Trial Research ID" 13 | }, 14 | "value": "AFT-05" 15 | }, 16 | { 17 | "use": "usual", 18 | "system": "http://example.com/clinicaltrialsubjectids", 19 | "type": { 20 | "text": "Clinical Trial Subject ID" 21 | }, 22 | "value": "AFT-05-patient-01" 23 | }, 24 | { 25 | "use": "usual", 26 | "type": { 27 | "coding": [ 28 | { 29 | "system": "http://hl7.org/fhir/v2/0203", 30 | "code": "MR", 31 | "display": "Medical Record Number" 32 | } 33 | ], 34 | "text": "Medical Record Number" 35 | }, 36 | "system": "http://hospital.example.org", 37 | "value": "m123" 38 | } 39 | ], 40 | "name": [ 41 | { 42 | "family": "Anyperson", 43 | "given": ["John", "B."] 44 | } 45 | ], 46 | "gender": "male" 47 | } 48 | -------------------------------------------------------------------------------- /test/templates/researchStudy.test.js: -------------------------------------------------------------------------------- 1 | const { isValidFHIR } = require('../../src/helpers/fhirUtils'); 2 | const validResearchStudy = require('./fixtures/research-study-resource.json'); 3 | const { researchStudyTemplate } = require('../../src/templates/ResearchStudyTemplate'); 4 | 5 | const VALID_DATA = { 6 | id: 'id-for-research-study', 7 | trialStatus: 'active', 8 | trialResearchID: 'AFT1235', 9 | clinicalSiteID: 'EXAMPLE_SITE_ID', 10 | clinicalSiteSystem: 'EXAMPLE_SITE_SYSTEM', 11 | trialResearchSystem: 'example-system', 12 | }; 13 | 14 | const INVALID_DATA = { 15 | // Omitting 'trialResearchID' field which is required 16 | trialStatus: 'completed', 17 | trialResearchID: null, 18 | }; 19 | 20 | describe('test ResearchStudy template', () => { 21 | test('valid data passed into template should generate valid FHIR resource', () => { 22 | const generatedResearchStudy = researchStudyTemplate(VALID_DATA); 23 | // Relevant fields should match the valid FHIR 24 | expect(generatedResearchStudy).toEqual(validResearchStudy); 25 | expect(isValidFHIR(generatedResearchStudy)).toBeTruthy(); 26 | }); 27 | 28 | test('invalid data should throw an error', () => { 29 | expect(() => researchStudyTemplate(INVALID_DATA)).toThrow(Error); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/helpers/dateUtils.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const logger = require('./logger'); 3 | 4 | moment.suppressDeprecationWarnings = true; // We handle invalid date formats 5 | const dateFormat = 'YYYY-MM-DD'; 6 | const dateTimeFormat = 'YYYY-MM-DDTHH:mm:ssZ'; 7 | 8 | function formatDate(date) { 9 | const parsedDate = moment.utc(date); 10 | if (!parsedDate.isValid()) { 11 | logger.warn(`Invalid date provided: ${date}. Provided value will be used.`); 12 | return date; // Use the provided date rather than 'Invalid date' 13 | } 14 | 15 | return parsedDate.format(dateFormat); 16 | } 17 | 18 | function formatDateTime(date) { 19 | const parsedDate = moment.utc(date); 20 | if (!parsedDate.isValid()) { 21 | logger.warn(`Invalid date provided: ${date}. Provided value will be used.`); 22 | return date; // Use the provided date rather than 'Invalid date' 23 | } 24 | 25 | // HACKY: If there is a minute, second, or hour, then we should treat this as a datetimestamp 26 | if (parsedDate.hour() || parsedDate.minute() || parsedDate.second()) { 27 | return parsedDate.format(dateTimeFormat); 28 | } 29 | return parsedDate.format(dateFormat); 30 | } 31 | 32 | module.exports = { 33 | formatDate, 34 | formatDateTime, 35 | dateFormat, 36 | dateTimeFormat, 37 | }; 38 | -------------------------------------------------------------------------------- /test/helpers/dateUtils.test.js: -------------------------------------------------------------------------------- 1 | const { formatDate, formatDateTime } = require('../../src/helpers/dateUtils'); 2 | 3 | test('formatDate reformats date', () => { 4 | expect(formatDate('04/12/19')).toEqual('2019-04-12'); 5 | expect(formatDate('2019-04-12T18:00:00')).toEqual('2019-04-12'); 6 | }); 7 | 8 | test('formatDate does not reformat invalid date', () => { 9 | expect(formatDate('102/30/19')).toEqual('102/30/19'); 10 | }); 11 | 12 | test('formatDateTime reformats date with a time if provided', () => { 13 | expect(formatDateTime('04/12/19')).toEqual('2019-04-12'); 14 | expect(formatDateTime('2019-04-12T08:00:00')).toEqual('2019-04-12T08:00:00+00:00'); 15 | }); 16 | 17 | test('formatDateTime respects timeZone information if provided', () => { 18 | const dateTimeWithZone = '2020-05-08T18:00:00+00:00'; 19 | expect(formatDateTime(dateTimeWithZone)).toEqual('2020-05-08T18:00:00+00:00'); 20 | const secondDate = '2020-05-08T18:00:00Z'; 21 | expect(formatDateTime(secondDate)).toEqual('2020-05-08T18:00:00+00:00'); 22 | const thirdDate = '2019-04-12T08:00:00+01:00'; 23 | expect(formatDateTime(thirdDate)).toEqual('2019-04-12T07:00:00+00:00'); 24 | }); 25 | 26 | test('formatDateTime does not reformat invalid date', () => { 27 | expect(formatDateTime('2019-104-12T18:00:00')).toEqual('2019-104-12T18:00:00'); 28 | }); 29 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-medication-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "MedicationAdministration", 3 | "id": "medicationId-1", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-administration" 7 | ] 8 | }, 9 | "extension": [ 10 | { 11 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-treatment-intent", 12 | "valueCodeableConcept": { 13 | "coding": [ 14 | { 15 | "system": "http://snomed.info/sct", 16 | "code": "example-code" 17 | } 18 | ] 19 | } 20 | } 21 | ], 22 | "status": "example-status", 23 | "medicationCodeableConcept": { 24 | "coding": [ 25 | { 26 | "system": "example-code-system", 27 | "code": "example-code", 28 | "display": "Example Text" 29 | } 30 | ] 31 | }, 32 | "subject": { 33 | "reference": "urn:uuid:mrn-1", 34 | "type": "Patient" 35 | }, 36 | "effectivePeriod": { 37 | "start": "2020-01-01", 38 | "end": "2020-02-01" 39 | }, 40 | "reasonCode": [ 41 | { 42 | "coding": [ 43 | { 44 | "system": "example-code-system", 45 | "code": "example-reason", 46 | "display": "Example Text" 47 | } 48 | ] 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-careplan-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "CarePlan", 3 | "id": "test-id", 4 | "meta": { 5 | "profile": [ 6 | "http://mcodeinitiative.org/codex/us/icare/StructureDefinition/icare-care-plan-with-review" 7 | ] 8 | }, 9 | "text": { 10 | "status": "additional", 11 | "div": "
This resource details the Treatment Plan Changes for a particular patient over a period of time, as modeled in the ICAREdata usecase of mCODE. It is based on the profile found here: http://standardhealthrecord.org/guides/icare/StructureDefinition-icare-CarePlanWithReview.html
" 12 | }, 13 | "extension": [ 14 | { 15 | "url": "http://mcodeinitiative.org/codex/us/icare/StructureDefinition/icare-care-plan-review", 16 | "extension": [ 17 | { 18 | "url": "ReviewDate", 19 | "valueDate": "2020-01-23" 20 | }, 21 | { 22 | "url": "ChangedFlag", 23 | "valueBoolean": false 24 | } 25 | ] 26 | } 27 | ], 28 | "subject": { 29 | "reference": "urn:uuid:abc-def", 30 | "type": "Patient" 31 | }, 32 | "status": "draft", 33 | "intent": "proposal", 34 | "category": [ 35 | { 36 | "coding": [ { "code": "assess-plan", "system": "http://argonaut.hl7.org"}] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /test/helpers/appUtils.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { parsePatientIds } = require('../../src/helpers/appUtils'); 3 | 4 | const MOCK_VALID_ID_CSV = path.join(__dirname, './fixtures/valid-mrns.csv'); 5 | const MOCK_VALID_ID_CSV_WITH_BOM = path.join(__dirname, './fixtures/valid-mrns-bom.csv'); 6 | 7 | // Has no MRN column 8 | const MOCK_INVALID_ID_CSV = path.join(__dirname, './fixtures/invalid-mrns.csv'); 9 | 10 | describe('appUtils', () => { 11 | describe('parsePatientIds', () => { 12 | test('valid path should parse content', () => { 13 | const expectedIds = ['123', '456', '789']; 14 | const ids = parsePatientIds(MOCK_VALID_ID_CSV); 15 | 16 | // Should get every MRN 17 | expect(ids).toHaveLength(expectedIds.length); 18 | expect(ids).toEqual(expectedIds); 19 | }); 20 | 21 | test('valid path to CSV with BOM should parse content', () => { 22 | const expectedIds = ['123', '456', '789']; 23 | const ids = parsePatientIds(MOCK_VALID_ID_CSV_WITH_BOM); 24 | 25 | // Should get every MRN and correctly parse with BOM 26 | expect(ids).toHaveLength(expectedIds.length); 27 | expect(ids).toEqual(expectedIds); 28 | }); 29 | 30 | test('invalid path should throw error', () => { 31 | expect(() => parsePatientIds(MOCK_INVALID_ID_CSV)).toThrowError(); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/sample-client-data/ctc-adverse-event-information.csv: -------------------------------------------------------------------------------- 1 | mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventCodeVersion,adverseEventDisplayText,adverseEventText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,studyId,effectiveDate,recordedDate,grade,expectation,resolvedDate,seriousnessOutcome,actor,functionCode 2 | 123,adverseEventId-1,10012174,http://terminology.hl7.org/CodeSystem/MDRAE,20.0,Dehydration,DHN IV given,procedure-id,Procedure,serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Serious,product-use-error|product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|http://snomed.info/sct|http://terminology.hl7.org/CodeSystem/adverse-event-category,Product Use Error|Product Quality|Wrong Rate,researchId-1,12-09-1994,12-09-1994,1,C41333,2021-12-01,C113380,practitioner-1,PART 3 | 456,adverseEventId-2,C143283,http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl,20.0,Anemia,AIHA NGTD,medicationId-1,Medication,non-serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Non-serious,product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|,Product Quality|,researchId-2,12-10-1995,12-10-1995,2,C41333,2020-10-02,C113380,practitioner-2, 4 | 789,adverseEventId-3,150003,,,,,,,,,,product-use-error,,,,12-09-1994,,3,,,,, -------------------------------------------------------------------------------- /test/templates/fixtures/minimal-disease-status-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "CancerDiseaseStatus-fixture", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-disease-status" 7 | ] 8 | }, 9 | "category": [ 10 | { 11 | "coding": [ 12 | { 13 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 14 | "code": "therapy", 15 | "display": "Therapy" 16 | } 17 | ] 18 | } 19 | ], 20 | "status": "final", 21 | "code": { 22 | "coding": [ 23 | { 24 | "system": "http://loinc.org", 25 | "code": "97509-4", 26 | "display": "Cancer Disease Progression" 27 | } 28 | ] 29 | }, 30 | "focus": [ 31 | { 32 | "reference": "urn:uuid:123-Walking-Corpse-Syndrome", 33 | "display": "Walking Corpse Syndrome", 34 | "type": "Condition" 35 | } 36 | ], 37 | "subject": { 38 | "reference": "urn:uuid:123-example-patient", 39 | "display": "Mr. Patient Example", 40 | "type": "Patient" 41 | }, 42 | "effectiveDateTime": "1994-12-09T09:07:00Z", 43 | "valueCodeableConcept": { 44 | "coding": [ 45 | { 46 | "code": "709137006", 47 | "display": "undetermined", 48 | "system": "http://snomed.info/sct" 49 | } 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/helpers/dependencyUtils.test.js: -------------------------------------------------------------------------------- 1 | const { sortExtractors } = require('../../src/helpers/dependencyUtils.js'); 2 | 3 | const WRONG_ORDER = [ 4 | { type: 'Extractor3' }, 5 | { type: 'Extractor1' }, 6 | { type: 'Extractor2' }, 7 | ]; 8 | 9 | const MISSING = [ 10 | { type: 'Extractor1' }, 11 | { type: 'Extractor3' }, 12 | ]; 13 | 14 | const NO_CHANGE = [ 15 | { type: 'Extractor1' }, 16 | { type: 'Extractor2' }, 17 | { type: 'Extractor3' }, 18 | ]; 19 | 20 | const DEPENDENCY_INFO = [ 21 | { type: 'Extractor1', dependencies: [] }, 22 | { type: 'Extractor2', dependencies: ['Extractor1'] }, 23 | { type: 'Extractor3', dependencies: ['Extractor1', 'Extractor2'] }, 24 | ]; 25 | 26 | describe('sortExtractors', () => { 27 | test('should put extractors in the correct order', () => { 28 | const sorted = sortExtractors(WRONG_ORDER, DEPENDENCY_INFO); 29 | expect(sorted[0].type).toEqual('Extractor1'); 30 | expect(sorted[1].type).toEqual('Extractor2'); 31 | expect(sorted[2].type).toEqual('Extractor3'); 32 | }); 33 | 34 | test('should change nothing if all extractors are in order with all dependencies', () => { 35 | const unchanged = sortExtractors(NO_CHANGE, DEPENDENCY_INFO); 36 | expect(unchanged).toEqual(NO_CHANGE); 37 | }); 38 | 39 | test('should fail when missing dependencies', () => { 40 | expect(() => sortExtractors(MISSING, DEPENDENCY_INFO)).toThrow(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/templates/EncounterTemplate.js: -------------------------------------------------------------------------------- 1 | const { ifAllArgsObj, ifSomeArgsObj } = require('../helpers/templateUtils'); 2 | const { coding, reference, periodTemplate } = require('./snippets'); 3 | 4 | function classTemplate({ classCode, classSystem }) { 5 | return { 6 | class: { ...coding({ code: classCode, system: classSystem }) }, 7 | }; 8 | } 9 | 10 | function typeTemplate({ typeCode, typeSystem }) { 11 | return { 12 | type: [{ coding: [coding({ code: typeCode, system: typeSystem })] }], 13 | }; 14 | } 15 | 16 | function subjectTemplate({ subject }) { 17 | return { 18 | subject: reference({ ...subject, resourceType: 'Patient' }), 19 | }; 20 | } 21 | 22 | function encounterTemplate({ 23 | subject, id, status, classCode, classSystem, typeCode, typeSystem, startDate, endDate, 24 | }) { 25 | if (!(id && subject && status && classCode && classSystem)) { 26 | throw Error('Trying to render a EncounterTemplate, but a required argument is missing; ensure that id, subject, status, classCode, and classSystem are all present'); 27 | } 28 | 29 | return { 30 | resourceType: 'Encounter', 31 | id, 32 | status, 33 | ...classTemplate({ classCode, classSystem }), 34 | ...subjectTemplate({ subject }), 35 | ...ifAllArgsObj(typeTemplate)({ typeCode, typeSystem }), 36 | ...ifSomeArgsObj(periodTemplate)({ startDate, endDate }), 37 | }; 38 | } 39 | 40 | module.exports = { 41 | encounterTemplate, 42 | }; 43 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-observation-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "status": "final", 5 | "category": [ 6 | { 7 | "coding": [ 8 | { 9 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 10 | "code": "vital-signs", 11 | "display": "Vital Signs" 12 | } 13 | ] 14 | } 15 | ], 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "8302-2", 21 | "display": "Body Height" 22 | 23 | } 24 | ] 25 | }, 26 | "subject" : { 27 | "reference": "urn:uuid:example-mrn", 28 | "type": "Patient" 29 | }, 30 | "effectiveDateTime" : "2020-01-01", 31 | "valueQuantity": { 32 | "value": 70, 33 | "unit": "in", 34 | "code": "[in_i]", 35 | "system": "http://unitsofmeasure.org" 36 | }, 37 | "bodySite": { 38 | "extension": [ 39 | { 40 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-laterality", 41 | "valueCodeableConcept": { 42 | "coding": [ 43 | { 44 | "system": "http://snomed.info/sct", 45 | "code": "51440002" 46 | } 47 | ] 48 | } 49 | } 50 | ], 51 | "coding": [ 52 | { 53 | "system": "http://snomed.info/sct", 54 | "code": "106004" 55 | } 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /src/helpers/valueSets/vs-expansion-explained.md: -------------------------------------------------------------------------------- 1 | # Generating New ValueSets 2 | 3 | Follow the steps below to generate a new set of valuesets based on the latest version of mCODE's IG. 4 | 5 | It's best to have a clean working directory before generating new ValueSets, as it will overwrite existing ValueSets. 6 | 7 | 1. Clone the fhir-mCODE-ig locally and _only run sushi to build FHIR resources_. This will build all mCODE Valuesets locally. You can find setup instructions for this repo on [HL7's GitHub](https://github.com/HL7/fhir-mCODE-ig). **Note**: you do not need to run the full `genonce` script; you only need to run `sushi`. 8 | 2. Move all ValueSets into a folder, for ease of access by the MEF'S `vs-expansion-script`. Below is a series of commands that will help with this (on OSX), but you could also do this manually. 9 | `cd some/path/to/local/fhir-mCODE-ig/fsh-generated/resources && mkdir valuesets && find * -name "ValueSet-*.json" | xargs -I '{}' cp {} ./valuesets` 10 | 3. Remove all irrelevant ValueSets from this folder, leaving behind only the ones to be expanded. 11 | 4. In the MEF, update the`PREEXPANSIONPATH` global variable in `vs-expansion-script.js` to point to the folder made to house the ValueSets in step 2. 12 | 5. Run `vs-expansion-script.js` using node. 13 | 6. For all failed expansions, determine if manual expansion is feasible (i.e. if there is just 1-2 codes that reference codeSystems GG's server do not support). If feasible, manually expand those VS. 14 | -------------------------------------------------------------------------------- /test/extractors/CSVPatientExtractor.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { CSVPatientExtractor } = require('../../src/extractors'); 3 | const examplePatientResponse = require('./fixtures/csv-patient-module-response.json'); 4 | const examplePatientBundle = require('./fixtures/csv-patient-bundle.json'); 5 | 6 | // Constants for mock tests 7 | const MOCK_PATIENT_MRN = 'EXAMPLE-MRN'; 8 | const MOCK_CSV_PATH = path.join(__dirname, 'fixtures/example.csv'); // need a valid path/csv here to avoid parse error 9 | 10 | // Instantiate module with mock parameters 11 | const csvPatientExtractor = new CSVPatientExtractor({ 12 | filePath: MOCK_CSV_PATH, 13 | }); 14 | 15 | // Destructure all modules 16 | const { csvModule } = csvPatientExtractor; 17 | // Spy on csvModule 18 | const csvModuleSpy = jest.spyOn(csvModule, 'get'); 19 | 20 | 21 | describe('CSV Patient Extractor', () => { 22 | describe('get', () => { 23 | test('should return a fhir bundle when MRN is known', async () => { 24 | csvModuleSpy.mockReset(); 25 | csvModuleSpy 26 | .mockReturnValue(examplePatientResponse); 27 | const data = await csvPatientExtractor.get({ mrn: MOCK_PATIENT_MRN }); 28 | 29 | expect(data.resourceType).toEqual('Bundle'); 30 | expect(data.type).toEqual('collection'); 31 | expect(data.entry).toBeDefined(); 32 | expect(data.entry.length).toEqual(1); 33 | expect(data.entry).toEqual(examplePatientBundle.entry); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/helpers/observationUtils.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { checkCodeInVs } = require('./valueSetUtils'); 3 | 4 | // Codes and display values for Vital Signs resources 5 | // Code mapping is based on http://hl7.org/fhir/R4/observation-vitalsigns.html 6 | const vitalSignsCodeToTextLookup = { 7 | '85353-1': 'Vital Signs Panel', 8 | '9279-1': 'Respiratory Rate', 9 | '8867-4': 'Heart rate', 10 | '2708-6': 'Oxygen saturation', 11 | '8310-5': 'Body temperature', 12 | '8302-2': 'Body height', 13 | '9843-4': 'Head circumference', 14 | '29463-7': 'Body weight', 15 | '39156-5': 'Body mass index', 16 | '85354-9': 'Blood pressure systolic and diastolic', 17 | '8480-6': 'Systolic blood pressure', 18 | '8462-4': 'Diastolic blood pressure', 19 | }; 20 | 21 | 22 | function isTumorMarker(code, system) { 23 | const tumorMarkerTestVSPath = path.resolve(__dirname, 'valueSets', 'ValueSet-mcode-tumor-marker-test-vs.json'); 24 | return checkCodeInVs(code, system, tumorMarkerTestVSPath); 25 | } 26 | 27 | function isVitalSign(code) { 28 | return Object.keys(vitalSignsCodeToTextLookup).includes(code); 29 | } 30 | 31 | function isKarnofskyPerformanceStatus(code) { 32 | return code === '89243-0'; 33 | } 34 | 35 | function isECOGPerformanceStatus(code) { 36 | return code === '89247-1'; 37 | } 38 | 39 | module.exports = { 40 | isTumorMarker, 41 | isVitalSign, 42 | isKarnofskyPerformanceStatus, 43 | isECOGPerformanceStatus, 44 | vitalSignsCodeToTextLookup, 45 | }; 46 | -------------------------------------------------------------------------------- /test/helpers/fixtures/condition-without-icd10.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Condition", 3 | "id": "condition-without-icd10", 4 | "patient": { 5 | "display": "Mitre-Mammogram Mitre-Test-Abnormal" 6 | }, 7 | "code": { 8 | "coding": [ 9 | { 10 | "system": "http://hl7.org/fhir/sid/icd-9-cm/diagnosis", 11 | "code": "174.2", 12 | "display": "Malignant neoplasm of upper-inner quadrant of right female breast" 13 | }, 14 | { 15 | "system": "http://snomed.info/sct", 16 | "code": "188152004", 17 | "display": "Malignant neoplasm of upper-inner quadrant of female breast (disorder)" 18 | } 19 | ], 20 | "text": "Malignant neoplasm of upper-inner quadrant of right female breast" 21 | }, 22 | "category": { 23 | "coding": [ 24 | { 25 | "system": "http://loinc.org", 26 | "code": "29308-4", 27 | "display": "Diagnosis" 28 | }, 29 | { 30 | "system": "http://snomed.info/sct", 31 | "code": "439401001", 32 | "display": "Diagnosis" 33 | }, 34 | { 35 | "system": "http://hl7.org/fhir/condition-category", 36 | "code": "diagnosis", 37 | "display": "Diagnosis" 38 | }, 39 | { 40 | "system": "http://argonautwiki.hl7.org/extension-codes", 41 | "code": "problem", 42 | "display": "Problem" 43 | } 44 | ], 45 | "text": "Diagnosis" 46 | }, 47 | "verificationStatus": "confirmed" 48 | } 49 | -------------------------------------------------------------------------------- /src/templates/ResearchStudyTemplate.js: -------------------------------------------------------------------------------- 1 | const { identifierArr, identifier } = require('./snippets'); 2 | 3 | function siteTemplate(clinicalSiteID, clinicalSiteSystem) { 4 | return { 5 | site: [ 6 | { 7 | display: 'ID associated with Clinical Trial', 8 | ...identifier({ 9 | system: clinicalSiteSystem, 10 | value: clinicalSiteID, 11 | }), 12 | }, 13 | ], 14 | }; 15 | } 16 | 17 | function researchStudyIdentifierTemplate(trialResearchID, trialResearchSystem) { 18 | return identifierArr({ 19 | system: trialResearchSystem, 20 | type: { 21 | text: 'Clinical Trial Research ID', 22 | }, 23 | value: trialResearchID, 24 | }); 25 | } 26 | 27 | 28 | // Based on https://www.hl7.org/fhir/researchstudy.html 29 | function researchStudyTemplate({ 30 | id, trialStatus, trialResearchID, clinicalSiteID, clinicalSiteSystem, trialResearchSystem, 31 | }) { 32 | if (!(id && trialStatus && trialResearchID && clinicalSiteID)) { 33 | throw Error('Trying to render a ResearchStudyTemplate, but a required argument is missing; ensure that id, trialStatus, trialResearchID, clinicalSiteID are all present'); 34 | } 35 | 36 | return { 37 | resourceType: 'ResearchStudy', 38 | id, 39 | status: trialStatus, 40 | ...siteTemplate(clinicalSiteID, clinicalSiteSystem), 41 | ...researchStudyIdentifierTemplate(trialResearchID, trialResearchSystem), 42 | }; 43 | } 44 | 45 | module.exports = { 46 | researchStudyTemplate, 47 | }; 48 | -------------------------------------------------------------------------------- /docs/config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "patientIdCsvPath": "/path/to/patient/mrn/file - can be absolute or relative to the location from which the client is being run", 3 | "commonExtractorArgs": { 4 | "argType": "ArgValue - unneeded arguments are ignored by extractors. This is helpful in defining commonly reused arguments. Examples below", 5 | "baseFHIRUrl": "http://www.example.com/fhir/api/", 6 | "baseEnvUrl": "http://www.example.com/service/api/", 7 | "requestHeaders": {}, 8 | "userId": { 9 | "id": "example_userid", 10 | "type": "example_userid_type" 11 | } 12 | }, 13 | "notificationInfo": { 14 | "host": "smtp.example.com", 15 | "port": 587, 16 | "from": "sender@example.com", 17 | "to": [ 18 | "demo@example.com", 19 | "test@example.com" 20 | ] 21 | }, 22 | "extractors": [ 23 | { 24 | "label": "Display Label for Extractor", 25 | "type": "ExtractorClassNameRegisteredInICAREClient", 26 | "constructorArgs": { 27 | "argType": "ArgValue - for example, Extractors that use CSV modules need to specify a file path, absolute or relative to the location from which the client is being run." 28 | } 29 | } 30 | ], 31 | "webServiceAuthConfig": { 32 | "url": "http://example.com/oauth2/token", 33 | "clientId": "client_id", 34 | "jwk": {} 35 | }, 36 | "awsConfig": { 37 | "baseURL": "http://example.com", 38 | "clientId": "client_id", 39 | "aud": "http://example.com/auth/realms/realm", 40 | "jwk": {} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-appointment-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Appointment", 3 | "id": "appointmentId-1", 4 | "status": "arrived", 5 | "serviceCategory": [ 6 | { 7 | "coding": [ 8 | { 9 | "system": "http://terminology.hl7.org/CodeSystem/service-category", 10 | "code": "35" 11 | } 12 | ] 13 | } 14 | ], 15 | "serviceType": [ 16 | { 17 | "coding": [ 18 | { 19 | "system": "http://terminology.hl7.org/CodeSystem/service-type", 20 | "code": "175" 21 | } 22 | ] 23 | } 24 | ], 25 | "appointmentType": { 26 | "coding": [ 27 | { 28 | "system": "http://terminology.hl7.org/CodeSystem/v2-0276", 29 | "code": "CHECKUP" 30 | } 31 | ] 32 | }, 33 | "specialty": [ 34 | { 35 | "coding": [ 36 | { 37 | "system": "http://snomed.info/sct", 38 | "code": "394592004" 39 | } 40 | ] 41 | } 42 | ], 43 | "participant": [ 44 | { 45 | "actor": { 46 | "reference": "urn:uuid:123", 47 | "type": "Patient" 48 | }, 49 | "status": "tentative" 50 | } 51 | ], 52 | "start": "2019-12-10T09:00:00+00:00", 53 | "end": "2019-12-10T11:00:00+00:00", 54 | "cancelationReason": { 55 | "coding": [ 56 | { 57 | "system": "http://terminology.hl7.org/CodeSystem/appointment-cancellation-reason", 58 | "code": "pat-cpp" 59 | } 60 | ] 61 | }, 62 | "description": "Example description 1" 63 | } -------------------------------------------------------------------------------- /src/templates/snippets/cancerStaging.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const logger = require('../../helpers/logger'); 3 | const { ifSomeArgsObj } = require('../../helpers/templateUtils'); 4 | const { isCancerStagingSystem } = require('../../helpers/cancerStagingUtils'); 5 | const { coding } = require('./coding'); 6 | 7 | function methodTemplate({ code, system }) { 8 | return ifSomeArgsObj( 9 | ({ code: code_, system: system_ }) => ({ 10 | method: { 11 | coding: [ 12 | coding({ 13 | ...(code_ && { code: code_ }), 14 | ...(system_ && { system: system_ }), 15 | }), 16 | ], 17 | }, 18 | }), 19 | )({ code, system }); 20 | } 21 | 22 | function stagingMethodTemplate({ code, system }) { 23 | if (isCancerStagingSystem(code, system)) { 24 | return methodTemplate({ code, system }); 25 | } if (code === 'C146985') { 26 | // TODO: fix this HARDCODED special case as delineated by this VS's description http://hl7.org/fhir/us/mcode/ValueSet-mcode-cancer-staging-system-vs.html 27 | // System based on http://hl7.org/fhir/us/mcode/Observation-mCODETNMClinicalPrimaryTumorCategoryExample01.json.html 28 | return methodTemplate({ code, system: 'http://ncimeta.nci.nih.gov' }); 29 | } 30 | if (!_.isNull(code)) { 31 | logger.debug(`stagingMethodTemplate received a code ${code} that is not recognized; code will be added without a codeSystem if possible`); 32 | } 33 | return methodTemplate({ code }); 34 | } 35 | 36 | module.exports = { 37 | stagingMethodTemplate, 38 | }; 39 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-procedure-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Procedure", 3 | "id": "example-id", 4 | "status": "completed", 5 | "code": { 6 | "coding": [ 7 | { 8 | "system": "http://snomed.info/sct", 9 | "code": "152198000", 10 | "display": "Brachytherapy (procedure)" 11 | } 12 | ] 13 | }, 14 | "subject": { "reference": "urn:uuid:example-mrn", "type": "Patient" }, 15 | "performedDateTime": "2020-01-01", 16 | "reasonCode": [ 17 | { 18 | "coding": [ 19 | { 20 | "system": "http://snomed.info/sct", 21 | "code": "363346000", 22 | "display": "Malignant tumor" 23 | } 24 | ] 25 | } 26 | ], 27 | "reasonReference": [{ "reference": "urn:uuid:example-condition-id", "type": "Condition" }], 28 | "extension": [ 29 | { 30 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-treatment-intent", 31 | "valueCodeableConcept": { 32 | "coding": [{ "system": "http://snomed.info/sct", "code": "373808002" }] 33 | } 34 | } 35 | ], 36 | "bodySite": [ 37 | { 38 | "extension": [ 39 | { 40 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-laterality", 41 | "valueCodeableConcept": { 42 | "coding": [ 43 | { "system": "http://snomed.info/sct", "code": "51440002" } 44 | ] 45 | } 46 | } 47 | ], 48 | "coding": [{ "system": "http://snomed.info/sct", "code": "41224006" }] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-adverse-event-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "AdverseEvent", 3 | "id": "adverseEventId-1", 4 | "subject": { 5 | "reference": "urn:uuid:mrn-1", 6 | "type": "Patient" 7 | }, 8 | "event": { 9 | "coding": [ 10 | { 11 | "system": "code-system", 12 | "code": "109006", 13 | "display": "Anxiety disorder of childhood OR adolescence" 14 | } 15 | ] 16 | }, 17 | "suspectEntity": [ 18 | { 19 | "instance": { 20 | "reference": "urn:uuid:procedure-id", 21 | "type": "Procedure" 22 | } 23 | } 24 | ], 25 | "seriousness": { 26 | "coding": [ 27 | { 28 | "system": "http://terminology.hl7.org/CodeSystem/adverse-event-seriousness", 29 | "code": "serious", 30 | "display": "Serious" 31 | } 32 | ] 33 | }, 34 | "category": [ 35 | { 36 | "coding": [ 37 | { 38 | "system": "http://terminology.hl7.org/CodeSystem/adverse-event-category", 39 | "code": "product-use-error", 40 | "display": "Product Use Error" 41 | } 42 | ] 43 | } 44 | ], 45 | "severity": { 46 | "coding": [ 47 | { 48 | "system": "http://terminology.hl7.org/CodeSystem/adverse-event-severity", 49 | "code": "severe" 50 | } 51 | ] 52 | }, 53 | "actuality": "actual", 54 | "study": [ 55 | { 56 | "reference": "urn:uuid:researchId-1", 57 | "type": "ResearchStudy" 58 | } 59 | ], 60 | "date": "1994-12-09", 61 | "recordedDate": "1994-12-09" 62 | } -------------------------------------------------------------------------------- /test/templates/snippets/reference.test.js: -------------------------------------------------------------------------------- 1 | const minimalReference = require('../fixtures/minimal-reference-object.json'); 2 | const maximalReference = require('../fixtures/maximal-reference-object.json'); 3 | const { reference } = require('../../../src/templates/snippets'); 4 | const { allOptionalKeyCombinationsNotThrow } = require('../../utils'); 5 | 6 | describe('Reference Snippet', () => { 7 | test('Throws an error when no argument is supplied', () => { 8 | expect(() => reference()).toThrowError(TypeError); 9 | }); 10 | 11 | test('Throws an error when empty object is supplied', () => { 12 | expect(() => reference({})).toThrowError(Error); 13 | }); 14 | 15 | test('Returns minimal reference object when minimal arguments are supplied.', () => { 16 | const MINIMAL_DATA = { 17 | id: 'example-id', 18 | }; 19 | 20 | expect(reference(MINIMAL_DATA)).toEqual(minimalReference); 21 | }); 22 | 23 | test('Returns minimal reference object when minimal arguments are supplied.', () => { 24 | const MAXIMAL_DATA = { 25 | id: 'example-id', 26 | name: 'example-name', 27 | resourceType: 'ExampleType', 28 | }; 29 | 30 | expect(reference(MAXIMAL_DATA)).toEqual(maximalReference); 31 | }); 32 | 33 | test('missing non-required data should not throw an error', () => { 34 | const NECESSARY_DATA = { 35 | id: 'example-id', 36 | }; 37 | 38 | const OPTIONAL_DATA = { 39 | name: 'example-name', 40 | }; 41 | 42 | allOptionalKeyCombinationsNotThrow(OPTIONAL_DATA, reference, NECESSARY_DATA); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcode-extraction-framework", 3 | "version": "2.2.2", 4 | "description": "", 5 | "contributors": [ 6 | "Julia Afeltra ", 7 | "Julian Carter ", 8 | "Matthew Gramigna ", 9 | "Daniel Lee ", 10 | "Dylan Mahalingam ", 11 | "Dylan Mendelowitz ", 12 | "Dylan Phelan " 13 | ], 14 | "main": "src/", 15 | "scripts": { 16 | "start": "node src/cli/cli.js", 17 | "lint": "eslint \"./**/*.js\"", 18 | "lint-fix": "eslint \"./**/*.js\" --fix", 19 | "test": "cross-env LOGGING=none jest", 20 | "test:watch": "cross-env LOGGING=none jest --watchAll" 21 | }, 22 | "license": "Apache-2.0", 23 | "dependencies": { 24 | "ajv": "^6.12.6", 25 | "antlr4": "4.8.0", 26 | "axios": "^0.21.4", 27 | "commander": "^6.2.0", 28 | "csv-parse": "^4.8.8", 29 | "fhir-crud-client": "^1.2.2", 30 | "fhir-mapper": "git+https://github.com/standardhealth/fhir-mapper.git#old-fhirpath", 31 | "fhirpath": "2.1.5", 32 | "lodash": "^4.17.21", 33 | "moment": "^2.29.4", 34 | "nodemailer": "^6.7.2", 35 | "winston": "^3.2.1" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^26.0.14", 39 | "cross-env": "^7.0.0", 40 | "eslint": "^6.6.0", 41 | "eslint-config-airbnb-base": "^14.0.0", 42 | "eslint-plugin-import": "^2.18.2", 43 | "jest": "^26.6.3", 44 | "jest-when": "^2.7.0", 45 | "nock": "^11.7.0", 46 | "rewire": "^5.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/helpers/csvParsingUtils.test.js: -------------------------------------------------------------------------------- 1 | const { normalizeEmptyValues } = require('../../src/helpers/csvParsingUtils.js'); 2 | 3 | describe('csvParsingUtils', () => { 4 | describe('normalizeEmptyValues', () => { 5 | it('Should turn "null" values into empty strings, regardless of case', () => { 6 | const data = [{ key: 'null' }, { key: 'NULL' }, { key: 'nuLL' }]; 7 | const normalizedData = normalizeEmptyValues(data); 8 | normalizedData.forEach((d) => { 9 | expect(d.key).toBe(''); 10 | }); 11 | }); 12 | 13 | it('Should turn "nil" values into empty strings, regardless of case', () => { 14 | const data = [{ key: 'nil' }, { key: 'NIL' }, { key: 'NIl' }]; 15 | const normalizedData = normalizeEmptyValues(data); 16 | normalizedData.forEach((d) => { 17 | expect(d.key).toBe(''); 18 | }); 19 | }); 20 | 21 | it('Should not modify unalterableColumns, regardless of their value', () => { 22 | const data = [{ key: 'null' }, { key: 'NULL' }, { key: 'nuLL' }, { key: 'nil' }, { key: 'NIL' }, { key: 'NIl' }]; 23 | const normalizedData = normalizeEmptyValues(data, ['key']); 24 | normalizedData.forEach((d) => { 25 | expect(d.key).not.toBe(''); 26 | }); 27 | }); 28 | 29 | it('Should leave all other values uneffected, regardless of case', () => { 30 | const data = [{ key: 'anything' }, { key: 'any' }, { key: 'thing' }]; 31 | const normalizedData = normalizeEmptyValues(data); 32 | normalizedData.forEach((d) => { 33 | expect(d.key).not.toBe(''); 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/modules/BaseFHIRModule.test.js: -------------------------------------------------------------------------------- 1 | const nock = require('nock'); 2 | const { BaseFHIRModule } = require('../../src/modules'); 3 | const operationOutcomeBundle = require('./fixtures/operation-outcome-bundle.json'); 4 | 5 | const MOCK_URL = 'http://localhost'; 6 | const MOCK_REQUEST_HEADERS = {}; 7 | 8 | // Instantiate module with mocks 9 | const baseFHIRModule = new BaseFHIRModule(MOCK_URL, MOCK_REQUEST_HEADERS); 10 | 11 | describe('BaseFHIRModule', () => { 12 | test('updateHeaders fn should update the headers variable and the clients headers', () => { 13 | // Ensure that the request headers are as expected initially 14 | const newHeaders = { 15 | ...MOCK_REQUEST_HEADERS, 16 | Authorization: 'Bearer tokenGoesHere', 17 | }; 18 | baseFHIRModule.updateRequestHeaders(newHeaders); 19 | 20 | Object.keys(newHeaders).forEach((key) => { 21 | const val = newHeaders[key]; 22 | expect(baseFHIRModule.client.httpClient.defaults.headers).toHaveProperty(key, val); 23 | }); 24 | }); 25 | 26 | test('search should make call for specified resourceType', async () => { 27 | const resourceType = 'Patient'; 28 | nock(baseFHIRModule.baseUrl) 29 | .get(`/${resourceType}`) 30 | .reply(200, operationOutcomeBundle); 31 | 32 | const searchResults = await baseFHIRModule.search('Patient', {}); 33 | expect(searchResults).toEqual(operationOutcomeBundle); 34 | // TODO: Check that the `logOperationOutcomeInfo` function in fhirUtils was called. 35 | // Having issues correctly spying on the function, so that test is not yet implemented. 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-staging-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "example-id", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-stage-group" 7 | ] 8 | }, 9 | "method": { 10 | "coding": [ 11 | { 12 | "code": "443830009", 13 | "system": "http://snomed.info/sct" 14 | } 15 | ] 16 | }, 17 | "status": "final", 18 | "category": [ 19 | { 20 | "coding": [ 21 | { 22 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 23 | "code": "survey" 24 | } 25 | ] 26 | } 27 | ], 28 | "code": { 29 | "coding": [ 30 | { 31 | "system": "http://loinc.org", 32 | "code": "21908-9" 33 | } 34 | ] 35 | }, 36 | "subject": { 37 | "reference": "urn:uuid:example-mrn", 38 | "type": "Patient" 39 | }, 40 | "effectiveDateTime": "2020-01-01", 41 | "valueCodeableConcept": { 42 | "coding": [ 43 | { 44 | "system": "http://cancerstaging.org", 45 | "code": "3C" 46 | } 47 | ] 48 | }, 49 | "focus": [ 50 | { 51 | "reference": "urn:uuid:example-condition-id", 52 | "type": "Condition" 53 | } 54 | ], 55 | "hasMember": [ 56 | { 57 | "reference": "urn:uuid:t-category-id", 58 | "type": "Observation" 59 | }, 60 | { 61 | "reference": "urn:uuid:n-category-id", 62 | "type": "Observation" 63 | }, 64 | { 65 | "reference": "urn:uuid:m-category-id", 66 | "type": "Observation" 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /src/helpers/appUtils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { csvParse } = require('./csvParsingUtils'); 4 | const logger = require('./logger'); 5 | 6 | /** 7 | * Loads the patientIdCSV data from disk, with some helpful hints logged in case of failure 8 | * 9 | * @returns file corresponding to the patient data 10 | */ 11 | function getPatientIdCSVData(patientIdCsvPath, dataDirectory) { 12 | try { 13 | const patientIdsCsvPath = path.resolve(patientIdCsvPath); 14 | return fs.readFileSync(patientIdsCsvPath, 'utf8'); 15 | } catch (e) { 16 | if (dataDirectory) { 17 | logger.error(`Could not resolve ${patientIdCsvPath}; even with a dataDirectory, the config.patientIdCsvPath variable needs to be a resolvable path to the patientID file on disk.`); 18 | } 19 | throw e; 20 | } 21 | } 22 | 23 | /** 24 | * Parses a provided CSV with MRN column into string array of IDs 25 | * 26 | * @param {string} patientIdCsvPath filePath to the CSV content to be parsed to get IDs 27 | * @param {string} dataDirectory optional argument for if a dataDirectory was specified by the config 28 | * @param {object} parserOptions options for the csv-parse module 29 | * @returns array of parsed IDs from the CSV 30 | */ 31 | function parsePatientIds(patientIdCsvPath, dataDirectory, parserOptions) { 32 | const csvData = getPatientIdCSVData(patientIdCsvPath, dataDirectory); 33 | return csvParse(csvData, parserOptions).map((row) => { 34 | if (!row.mrn) { 35 | throw new Error(`${patientIdCsvPath} has no "mrn" column`); 36 | } 37 | return row.mrn; 38 | }); 39 | } 40 | 41 | module.exports = { 42 | parsePatientIds, 43 | }; 44 | -------------------------------------------------------------------------------- /test/helpers/fixtures/secondary-cancer-condition.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Condition", 3 | "id": "TQ6-lmYpARvptztb1K4wGKgB", 4 | "patient": { 5 | "display": "Mitre-Mammogram Mitre-Test-Abnormal" 6 | }, 7 | "code": { 8 | "coding": [ 9 | { 10 | "system": "http://hl7.org/fhir/sid/icd-9-cm/diagnosis", 11 | "code": "174.2", 12 | "display": "Malignant neoplasm of upper-inner quadrant of right female breast" 13 | }, 14 | { 15 | "system": "http://hl7.org/fhir/sid/icd-10-cm", 16 | "code": "C79.81", 17 | "display": "Secondary malignant neoplasm of breast" 18 | }, 19 | { 20 | "system": "http://snomed.info/sct", 21 | "code": "188152004", 22 | "display": "Malignant neoplasm of upper-inner quadrant of female breast (disorder)" 23 | } 24 | ], 25 | "text": "Malignant neoplasm of upper-inner quadrant of right female breast" 26 | }, 27 | "category": { 28 | "coding": [ 29 | { 30 | "system": "http://loinc.org", 31 | "code": "29308-4", 32 | "display": "Diagnosis" 33 | }, 34 | { 35 | "system": "http://snomed.info/sct", 36 | "code": "439401001", 37 | "display": "Diagnosis" 38 | }, 39 | { 40 | "system": "http://hl7.org/fhir/condition-category", 41 | "code": "diagnosis", 42 | "display": "Diagnosis" 43 | }, 44 | { 45 | "system": "http://argonautwiki.hl7.org/extension-codes", 46 | "code": "problem", 47 | "display": "Problem" 48 | } 49 | ], 50 | "text": "Diagnosis" 51 | }, 52 | "verificationStatus": "confirmed" 53 | } 54 | -------------------------------------------------------------------------------- /src/helpers/csvValidator.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const logger = require('./logger'); 3 | 4 | // Validates csvData against the csvSchema 5 | // Uses the csvFileIdentifier in logs for readability 6 | function validateCSV(csvFileIdentifier, csvSchema, csvData, headers) { 7 | let isValid = true; 8 | 9 | // Check headers 10 | const schemaDiff = _.difference(csvSchema.headers.map((h) => h.name.toLowerCase()), headers); 11 | const fileDiff = _.difference(headers, csvSchema.headers.map((h) => h.name.toLowerCase())); 12 | 13 | if (fileDiff.length > 0) { 14 | logger.warn(`Found extra column(s) in CSV ${csvFileIdentifier}: "${fileDiff.join(',')}"`); 15 | } 16 | 17 | if (schemaDiff.length > 0) { 18 | schemaDiff.forEach((sd) => { 19 | const headerSchema = csvSchema.headers.find((h) => h.name.toLowerCase() === sd); 20 | if (headerSchema.required) { 21 | logger.error(`Column ${sd} is marked as required but is missing in CSV ${csvFileIdentifier}`); 22 | isValid = false; 23 | } else { 24 | logger.warn(`Column ${sd} is missing in CSV ${csvFileIdentifier}`); 25 | } 26 | }); 27 | } 28 | 29 | // Check values 30 | csvData.forEach((row, i) => { 31 | Object.entries(row).forEach(([key, value], j) => { 32 | const schema = csvSchema.headers.find((h) => h.name === key); 33 | 34 | if (schema && schema.required && !value) { 35 | logger.error(`Column ${key} marked as required but missing value in row ${i + 1} column ${j + 1} in CSV ${csvFileIdentifier}`); 36 | isValid = false; 37 | } 38 | }); 39 | }); 40 | 41 | return isValid; 42 | } 43 | 44 | module.exports = { 45 | validateCSV, 46 | }; 47 | -------------------------------------------------------------------------------- /test/helpers/fixtures/condition-with-icd10.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Condition", 3 | "id": "condition-with-icd10", 4 | "patient": { 5 | "display": "Mitre-Mammogram Mitre-Test-Abnormal" 6 | }, 7 | "code": { 8 | "coding": [ 9 | { 10 | "system": "http://hl7.org/fhir/sid/icd-9-cm/diagnosis", 11 | "code": "174.2", 12 | "display": "Malignant neoplasm of upper-inner quadrant of right female breast" 13 | }, 14 | { 15 | "system": "http://hl7.org/fhir/sid/icd-10-cm", 16 | "code": "C50.211", 17 | "display": "Malignant neoplasm of upper-inner quadrant of right female breast" 18 | }, 19 | { 20 | "system": "http://snomed.info/sct", 21 | "code": "188152004", 22 | "display": "Malignant neoplasm of upper-inner quadrant of female breast (disorder)" 23 | } 24 | ], 25 | "text": "Malignant neoplasm of upper-inner quadrant of right female breast" 26 | }, 27 | "category": { 28 | "coding": [ 29 | { 30 | "system": "http://loinc.org", 31 | "code": "29308-4", 32 | "display": "Diagnosis" 33 | }, 34 | { 35 | "system": "http://snomed.info/sct", 36 | "code": "439401001", 37 | "display": "Diagnosis" 38 | }, 39 | { 40 | "system": "http://hl7.org/fhir/condition-category", 41 | "code": "diagnosis", 42 | "display": "Diagnosis" 43 | }, 44 | { 45 | "system": "http://argonautwiki.hl7.org/extension-codes", 46 | "code": "problem", 47 | "display": "Problem" 48 | } 49 | ], 50 | "text": "Diagnosis" 51 | }, 52 | "verificationStatus": "confirmed" 53 | } 54 | -------------------------------------------------------------------------------- /test/helpers/fixtures/primary-cancer-condition.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Condition", 3 | "id": "TQ5-lmYpARvptztb1K4wGKgB", 4 | "patient": { 5 | "display": "Mitre-Mammogram Mitre-Test-Abnormal" 6 | }, 7 | "code": { 8 | "coding": [ 9 | { 10 | "system": "http://hl7.org/fhir/sid/icd-9-cm/diagnosis", 11 | "code": "174.2", 12 | "display": "Malignant neoplasm of upper-inner quadrant of right female breast" 13 | }, 14 | { 15 | "system": "http://hl7.org/fhir/sid/icd-10-cm", 16 | "code": "C50.211", 17 | "display": "Malignant neoplasm of upper-inner quadrant of right female breast" 18 | }, 19 | { 20 | "system": "http://snomed.info/sct", 21 | "code": "188152004", 22 | "display": "Malignant neoplasm of upper-inner quadrant of female breast (disorder)" 23 | } 24 | ], 25 | "text": "Malignant neoplasm of upper-inner quadrant of right female breast" 26 | }, 27 | "category": { 28 | "coding": [ 29 | { 30 | "system": "http://loinc.org", 31 | "code": "29308-4", 32 | "display": "Diagnosis" 33 | }, 34 | { 35 | "system": "http://snomed.info/sct", 36 | "code": "439401001", 37 | "display": "Diagnosis" 38 | }, 39 | { 40 | "system": "http://hl7.org/fhir/condition-category", 41 | "code": "diagnosis", 42 | "display": "Diagnosis" 43 | }, 44 | { 45 | "system": "http://argonautwiki.hl7.org/extension-codes", 46 | "code": "problem", 47 | "display": "Problem" 48 | } 49 | ], 50 | "text": "Diagnosis" 51 | }, 52 | "verificationStatus": "confirmed" 53 | } 54 | -------------------------------------------------------------------------------- /test/helpers/fixtures/searchsetBundleWithOneEntry.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "searchset", 4 | "total": 1, 5 | "entry": [ 6 | { "resource": 7 | { 8 | "resourceType": "Patient", 9 | "id": "mCODEPatientExample01", 10 | "meta": { 11 | "profile": ["http://hl7.org/fhir/us/mcode/StructureDefinition/obf-Patient"] 12 | }, 13 | "identifier": [ 14 | { 15 | "use": "usual", 16 | "system": "http://example.com/clinicaltrialids", 17 | "type": { 18 | "text": "Clinical Trial Research ID" 19 | }, 20 | "value": "AFT-05" 21 | }, 22 | { 23 | "use": "usual", 24 | "system": "http://example.com/clinicaltrialsubjectids", 25 | "type": { 26 | "text": "Clinical Trial Subject ID" 27 | }, 28 | "value": "AFT-05-patient-01" 29 | }, 30 | { 31 | "use": "usual", 32 | "type": { 33 | "coding": [ 34 | { 35 | "system": "http://hl7.org/fhir/v2/0203", 36 | "code": "MR", 37 | "display": "Medical Record Number" 38 | } 39 | ], 40 | "text": "Medical Record Number" 41 | }, 42 | "system": "http://hospital.example.org", 43 | "value": "m123" 44 | } 45 | ], 46 | "name": [ 47 | { 48 | "family": "Anyperson", 49 | "given": ["John", "B."] 50 | } 51 | ], 52 | "gender": "male" 53 | } 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /src/application/tools/mcodeExtraction.js: -------------------------------------------------------------------------------- 1 | const logger = require('../../helpers/logger'); 2 | const { getResourceCountInBundle } = require('../../helpers/fhirUtils'); 3 | 4 | async function extractDataForPatients(patientIds, mcodeClient, fromDate, toDate) { 5 | // Using an initialized mcodeClient, extract data for patient ids in the appropriate toDate-fromDate range 6 | const totalExtractionErrors = {}; 7 | const extractedData = []; 8 | // Track if these runs were successful; if not, don't log a new RunInstance 9 | let successfulExtraction = true; 10 | /* eslint-disable no-restricted-syntax */ 11 | /* eslint-disable no-await-in-loop */ 12 | for (const [index, mrn] of patientIds.entries()) { 13 | totalExtractionErrors[index] = []; 14 | try { 15 | logger.info(`Extracting information for patient at row ${index + 1} in .csv file`); 16 | const { bundle, extractionErrors } = await mcodeClient.get({ mrn, fromDate, toDate }); 17 | totalExtractionErrors[index].push(...extractionErrors); 18 | const resourceCount = getResourceCountInBundle(bundle); 19 | logger.info(`Resources extracted for patient ${index + 1} in .csv file`); 20 | Object.keys(resourceCount).forEach((resourceType) => logger.info(`${resourceType}: ${resourceCount[resourceType]} extracted`)); 21 | extractedData.push(bundle); 22 | } catch (fatalErr) { 23 | successfulExtraction = false; 24 | totalExtractionErrors[index].push(fatalErr); 25 | logger.error(`Fatal error extracting data: ${fatalErr.message}`); 26 | logger.debug(fatalErr.stack); 27 | } 28 | } 29 | return { extractedData, successfulExtraction, totalExtractionErrors }; 30 | } 31 | 32 | module.exports = { 33 | extractDataForPatients, 34 | }; 35 | -------------------------------------------------------------------------------- /src/helpers/lookupUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a lookup table that swaps keys and values s.t. (k->v) becomes (v->k) 3 | * @param {Object} lookup - a lookup table, defined by key-value pairs 4 | * @return {Object} the lookup table, with all keys and values inverted 5 | */ 6 | function createInvertedLookup(lookup) { 7 | // NOTE: This will produce collisions if values aren't unique 8 | // and unspecified behavior if values are non-strings 9 | return Object.entries(lookup).reduce((ret, entry) => { 10 | const [key, value] = entry; 11 | // eslint-disable-next-line no-param-reassign 12 | ret[value] = key; 13 | return ret; 14 | }, {}); 15 | } 16 | 17 | /** 18 | * Create a lookup table where all the keys are lowercased 19 | * @param {Object} lookup - a lookup table, defined by key-value pairs 20 | * @return {Object} the lookup table, with all keys lowercased 21 | */ 22 | function createLowercaseLookup(lookup) { 23 | // NOTE: This will produce collisions if keys aren't unique w/r/t case 24 | return Object.entries(lookup).reduce((ret, entry) => { 25 | const [k, v] = entry; 26 | // eslint-disable-next-line no-param-reassign 27 | ret[k.toLowerCase()] = v; 28 | return ret; 29 | }, {}); 30 | } 31 | 32 | /** 33 | * Performs a lookup using the original key and a lowercase key 34 | * @param {String} key - key being queried for in the lookup 35 | * @param {Object} lookup - lookup table 36 | * @return {any} the value associated with that key 37 | */ 38 | function lowercaseLookupQuery(key, lookup) { 39 | const lowerKey = key.toLowerCase(); 40 | return lookup[key] || lookup[lowerKey]; 41 | } 42 | 43 | module.exports = { 44 | lowercaseLookupQuery, 45 | createLowercaseLookup, 46 | createInvertedLookup, 47 | }; 48 | -------------------------------------------------------------------------------- /test/templates/snippets/coding.test.js: -------------------------------------------------------------------------------- 1 | const { coding } = require('../../../src/templates/snippets'); 2 | const { allOptionalKeyCombinationsNotThrow } = require('../../utils'); 3 | const maximalCoding = require('../fixtures/maximal-coding-object.json'); 4 | 5 | describe('Coding Snippet', () => { 6 | test('Throws an error when no argument is supplied', () => { 7 | // Throws a TypeError since it's expecting an object that it can destructure 8 | expect(() => coding()).toThrowError(TypeError); 9 | }); 10 | 11 | test('Returns null when non-`object` arguments are supplied', () => { 12 | // Throws a TypeError since it's expecting an object that it can destructure 13 | expect(coding(1)).toBeNull(); 14 | expect(coding([])).toBeNull(); 15 | expect(coding(true)).toBeNull(); 16 | expect(coding('code', 'system')).toBeNull(); 17 | }); 18 | 19 | test('Returns null when any empty object is supplied as an argument ', () => { 20 | expect(coding({})).toBeNull(); 21 | }); 22 | 23 | test('Returns a saturated coding object when all arguments are supplied', () => { 24 | const MAX_CODING_INPUT = { 25 | system: 'example-sys', 26 | version: 'v3.1.4', 27 | code: 'example-code', 28 | display: 'A string of display text', 29 | userSelected: true, 30 | }; 31 | 32 | expect(coding(MAX_CODING_INPUT)).toEqual(maximalCoding); 33 | }); 34 | 35 | test('Any combination of properties will not throw an error', () => { 36 | const MAX_CODING_INPUT = { 37 | system: 'example-sys', 38 | version: 'v3.1.4', 39 | code: 'example-code', 40 | display: 'A string of display text', 41 | userSelected: true, 42 | }; 43 | 44 | allOptionalKeyCombinationsNotThrow(MAX_CODING_INPUT, coding, {}); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/templates/researchSubject.test.js: -------------------------------------------------------------------------------- 1 | const { isValidFHIR } = require('../../src/helpers/fhirUtils'); 2 | const validResearchSubject = require('./fixtures/research-subject-resource.json'); 3 | const { researchSubjectTemplate } = require('../../src/templates/ResearchSubjectTemplate'); 4 | 5 | const VALID_DATA = { 6 | id: 'id-for-research-subject', 7 | enrollmentStatus: 'candidate', 8 | trialSubjectID: 'trial-123', 9 | trialResearchID: 'rs1', 10 | patientId: 'mCODEPatient1', 11 | startDate: '2020-01-01', 12 | endDate: '2021-01-01', 13 | }; 14 | 15 | const INVALID_DATA = { 16 | // Omitting 'trialSubjectID' field which is required 17 | enrollmentStatus: 'candidate', 18 | trialResearchID: 'rs1', 19 | patientId: 'mCODEPatient1', 20 | trialSubjectID: null, 21 | startDate: '2020-01-01', 22 | endDate: '2021-01-01', 23 | }; 24 | 25 | describe('test ResearchSubject template', () => { 26 | test('valid data passed into template should generate valid FHIR resource', () => { 27 | const generatedResearchSubject = researchSubjectTemplate(VALID_DATA); 28 | // Relevant fields should match the valid FHIR 29 | expect(generatedResearchSubject.id).toEqual(validResearchSubject.id); 30 | expect(generatedResearchSubject.trialStatus).toEqual(validResearchSubject.trialStatus); 31 | expect(generatedResearchSubject.trialResearchID).toEqual(validResearchSubject.trialResearchID); 32 | expect(generatedResearchSubject.period.start).toEqual(validResearchSubject.period.start); 33 | expect(generatedResearchSubject.period.end).toEqual(validResearchSubject.period.end); 34 | expect(isValidFHIR(generatedResearchSubject)).toBeTruthy(); 35 | }); 36 | 37 | test('invalid data should throw an error', () => { 38 | expect(() => researchSubjectTemplate(INVALID_DATA)).toThrow(Error); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-bitwise */ 2 | 3 | // Takes an object with optional arguments, a function, and an object with required arguments 4 | // Ensures that all permutations of optionalArguments along with the requiredObj's args 5 | // Never causes the testFn to fail 6 | function allOptionalKeyCombinationsNotThrow(optionalObj, testFn, requiredObj = null) { 7 | const n = Object.keys(optionalObj).length; 8 | // There are 2^n many combinations of n-many keys, corresponding to a powerset; 9 | for (let i = 1; i < 2 ** n; i += 1) { 10 | // Use the current index to identify each member of our powerset 11 | // Use the `i` of our loop as a bitwise-and mask on the 10's column associated with each index (via bitshift) 12 | // E.g. Assume 4 keys, powerset of size 16; for all i < 16 (0-15); looking at where i=13 13 | // Keys we want include: [k0, k1, k3] 14 | // Mask: 1101 (i=13 in bitwise representation, right to left) 15 | // Keys: [k0, k1, k2, k3] 16 | // Inds: [0, 1, 2, 3] 17 | // Ten's: [0001, 0010, 0100, 1000] (we get this with our 1 bit-shift) 18 | // Apply the mask with a bit-and to see which keys are included in this permutation 19 | // Incl?: [T, T, F, T] -> Matches the expected kets to include 20 | const permutationKeys = Object.keys(optionalObj).filter((val, ind) => i & (1 << ind)); 21 | const permutation = Object.keys(optionalObj) 22 | .filter((key) => permutationKeys.includes(key)) 23 | .reduce((accum, key) => { 24 | // eslint-disable-next-line no-param-reassign 25 | accum[key] = optionalObj[key]; 26 | return accum; 27 | }, {}); 28 | expect(() => testFn({ ...requiredObj, ...permutation })).not.toThrow(); 29 | } 30 | } 31 | 32 | module.exports = { 33 | allOptionalKeyCombinationsNotThrow, 34 | }; 35 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-medication-administration-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "urn:uuid:medicationId-1", 7 | "resource": { 8 | "resourceType": "MedicationAdministration", 9 | "id": "medicationId-1", 10 | "meta": { 11 | "profile": [ 12 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-administration" 13 | ] 14 | }, 15 | "extension": [ 16 | { 17 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-treatment-intent", 18 | "valueCodeableConcept": { 19 | "coding": [ 20 | { 21 | "system": "http://snomed.info/sct", 22 | "code": "example-code" 23 | } 24 | ] 25 | } 26 | } 27 | ], 28 | "status": "example-status", 29 | "medicationCodeableConcept": { 30 | "coding": [ 31 | { 32 | "system": "example-code-system", 33 | "code": "example-code", 34 | "display": "Example Text" 35 | } 36 | ] 37 | }, 38 | "subject": { 39 | "reference": "urn:uuid:mrn-1", 40 | "type": "Patient" 41 | }, 42 | "effectivePeriod": { 43 | "start": "YYYY-MM-DD", 44 | "end": "YYYY-MM-DD" 45 | }, 46 | "reasonCode": [ 47 | { 48 | "coding": [ 49 | { 50 | "system": "example-code-system", 51 | "code": "example-reason", 52 | "display": "Example Text" 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /src/templates/ResearchSubjectTemplate.js: -------------------------------------------------------------------------------- 1 | const { reference, identifier, identifierArr, periodTemplate } = require('./snippets'); 2 | const { ifSomeArgsObj } = require('../helpers/templateUtils'); 3 | 4 | function studyTemplate(trialResearchID, trialResearchSystem) { 5 | return { 6 | study: { 7 | ...identifier({ 8 | system: trialResearchSystem, 9 | value: trialResearchID, 10 | }), 11 | }, 12 | }; 13 | } 14 | 15 | function individualTemplate(patientId) { 16 | return { 17 | individual: { 18 | ...reference({ id: patientId, resourceType: 'Patient' }), 19 | }, 20 | }; 21 | } 22 | 23 | function researchSubjectIdentifiersTemplate(trialSubjectID) { 24 | return identifierArr( 25 | { 26 | system: 'http://example.com/clinicaltrialsubjectids', 27 | type: { 28 | text: 'Clinical Trial Subject ID', 29 | }, 30 | value: trialSubjectID, 31 | }, 32 | ); 33 | } 34 | 35 | function researchSubjectTemplate({ 36 | id, 37 | enrollmentStatus, 38 | trialSubjectID, 39 | trialResearchID, 40 | patientId, 41 | trialResearchSystem, 42 | startDate, 43 | endDate, 44 | }) { 45 | if (!(id && enrollmentStatus && trialSubjectID && trialResearchID && patientId)) { 46 | throw Error('Trying to render a ResearchStudyTemplate, but a required argument is missing; ensure that id, trialStatus, trialResearchID, clinicalSiteID are all present'); 47 | } 48 | 49 | return { 50 | resourceType: 'ResearchSubject', 51 | id, 52 | status: enrollmentStatus, 53 | ...studyTemplate(trialResearchID, trialResearchSystem), 54 | ...individualTemplate(patientId), 55 | ...researchSubjectIdentifiersTemplate(trialSubjectID), 56 | ...ifSomeArgsObj(periodTemplate)({ startDate, endDate }), 57 | }; 58 | } 59 | 60 | module.exports = { 61 | researchSubjectTemplate, 62 | }; 63 | -------------------------------------------------------------------------------- /test/helpers/configUtils.test.js: -------------------------------------------------------------------------------- 1 | const { validateConfig, getConfig } = require('../../src/helpers/configUtils.js'); 2 | const testConfig = require('./fixtures/test-config.json'); 3 | 4 | describe('getConfig', () => { 5 | const pathToConfig = 'test/helpers/fixtures/test-config.json'; 6 | 7 | it('should throw error when pathToConfig does not point to valid JSON file.', () => { 8 | expect(() => getConfig()).toThrowError(); 9 | }); 10 | 11 | it('should return test config', () => { 12 | const config = getConfig(pathToConfig); 13 | expect(config).toEqual(testConfig); 14 | }); 15 | }); 16 | 17 | describe('validateConfig', () => { 18 | const missingPropertyConfig = { patientIdCsvPath: '' }; 19 | const wrongTypeConfig = { patientIdCsvPath: '', extractors: 12 }; 20 | const wrongFormatConfig = { patientIdCsvPath: '', extractors: [], commonExtractorArgs: { baseFhirUrl: 'wrong' } }; 21 | const validConfig = { patientIdCsvPath: '', extractors: [] }; 22 | 23 | test('Should throw error when config file is missing required property', () => { 24 | expect(() => validateConfig(missingPropertyConfig)).toThrowError('Error(s) found in config file: config should have required property \'extractors\''); 25 | }); 26 | 27 | test('Should throw error when property is of incorrect type', () => { 28 | expect(() => validateConfig(wrongTypeConfig)).toThrowError('Error(s) found in config file: config.extractors should be array'); 29 | }); 30 | 31 | test('Should throw error when property has incorrect format', () => { 32 | expect(() => validateConfig(wrongFormatConfig)).toThrowError('Error(s) found in config file: config.commonExtractorArgs.baseFhirUrl should match format "uri"'); 33 | }); 34 | 35 | test('Should not throw error when config file is valid', () => { 36 | expect(() => validateConfig(validConfig)).not.toThrow(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-observation-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "urn:uuid:observation-id", 7 | "resource": { 8 | "resourceType": "Observation", 9 | "id": "observation-id", 10 | "status": "final", 11 | "category": [ 12 | { 13 | "coding": [ 14 | { 15 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 16 | "code": "laboratory" 17 | } 18 | ] 19 | } 20 | ], 21 | "code": { 22 | "coding": [ 23 | { 24 | "system": "http://loinc.org", 25 | "code": "1695-6" 26 | } 27 | ] 28 | }, 29 | "subject": { 30 | "reference": "urn:uuid:mrn-1", 31 | "type": "Patient" 32 | }, 33 | "effectiveDateTime": "2020-01-02", 34 | "valueCodeableConcept": { 35 | "coding": [ 36 | { 37 | "system": "http://snomed.info/sct", 38 | "code": "10828004" 39 | } 40 | ] 41 | }, 42 | "bodySite": { 43 | "extension": [ 44 | { 45 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-laterality", 46 | "valueCodeableConcept": { 47 | "coding": [ 48 | { 49 | "system": "http://snomed.info/sct", 50 | "code": "678910" 51 | } 52 | ] 53 | } 54 | } 55 | ], 56 | "coding": [ 57 | { 58 | "system": "http://snomed.info/sct", 59 | "code": "12345" 60 | } 61 | ] 62 | } 63 | } 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /test/application/runInstanceLogger.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { RunInstanceLogger } = require('../../src/application/tools/RunInstanceLogger.js'); 3 | 4 | describe('RunInstanceLogger', () => { 5 | describe('constructor', () => { 6 | const fsSpy = jest.spyOn(fs, 'readFileSync'); 7 | it('should throw error when not provided a path', () => { 8 | expect(() => new RunInstanceLogger()).toThrowError(); 9 | }); 10 | 11 | it('should throw error when path does not point to valid JSON', () => { 12 | expect(() => new RunInstanceLogger('./bad-path')).toThrowError(); 13 | }); 14 | 15 | it('should throw error when log file is not an array', () => { 16 | fsSpy.mockReturnValueOnce(Buffer.from('{}')); 17 | expect(() => new RunInstanceLogger('path')).toThrowError('Log file needs to be an array.'); 18 | expect(fsSpy).toHaveBeenCalled(); 19 | }); 20 | 21 | it('should not throw error when log file is an array', () => { 22 | expect(() => new RunInstanceLogger('./test/application/fixtures/run-logs.json')).not.toThrowError(); 23 | expect(fsSpy).toHaveBeenCalled(); 24 | }); 25 | }); 26 | 27 | describe('getEffectiveFromDate', () => { 28 | const testDate = '2020-06-16'; 29 | 30 | it('should return fromDate when valid', () => { 31 | const runLogger = new RunInstanceLogger('./test/application/fixtures/run-logs.json'); 32 | expect(runLogger.getEffectiveFromDate(testDate)).toEqual(testDate); 33 | }); 34 | 35 | it('should return most recent date from runLogger', () => { 36 | const runLogger = new RunInstanceLogger('./test/application/fixtures/run-logs.json'); 37 | expect(runLogger.getEffectiveFromDate(null)).toEqual(testDate); 38 | }); 39 | 40 | it('should throw error when no recent date from runlogger', () => { 41 | const runLogger = new RunInstanceLogger('./test/application/fixtures/empty-run-logs.json'); 42 | expect(() => runLogger.getEffectiveFromDate(null)).toThrowError(); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/extractors/BaseCSVExtractor.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { Extractor } = require('./Extractor'); 3 | const { CSVFileModule, CSVURLModule } = require('../modules'); 4 | const logger = require('../helpers/logger'); 5 | 6 | 7 | class BaseCSVExtractor extends Extractor { 8 | constructor({ 9 | filePath, url, fileName, dataDirectory, csvSchema, unalterableColumns, csvParse, 10 | }) { 11 | super(); 12 | this.unalterableColumns = unalterableColumns || []; 13 | this.csvSchema = csvSchema; 14 | this.parserOptions = csvParse && csvParse.options ? csvParse.options : {}; 15 | if (url) { 16 | logger.debug('Found url argument; creating a CSVURLModule with the provided url'); 17 | this.url = url; 18 | this.csvModule = new CSVURLModule(this.url, this.unalterableColumns, this.parserOptions); 19 | } else if (fileName && dataDirectory) { 20 | if (!path.isAbsolute(dataDirectory)) throw new Error('dataDirectory is not an absolutePath, it needs to be.'); 21 | this.filePath = path.join(dataDirectory, fileName); 22 | logger.debug( 23 | 'Found fileName and dataDirectory arguments; creating a CSVFileModule with the provided dataDirectory and fileName', 24 | ); 25 | this.csvModule = new CSVFileModule(this.filePath, this.unalterableColumns, this.parserOptions); 26 | } else if (filePath) { 27 | logger.debug('Found filePath argument; creating a CSVFileModule with the provided filePath'); 28 | this.filePath = filePath; 29 | this.csvModule = new CSVFileModule(this.filePath, this.unalterableColumns, this.parserOptions); 30 | } else { 31 | logger.debug( 32 | 'Could not instantiate a CSVExtractor with the provided constructor args', 33 | ); 34 | throw new Error('Trying to instantiate a CSVExtractor without a valid filePath, url, or fileName+dataDirectory combination'); 35 | } 36 | } 37 | 38 | async validate() { 39 | return this.csvModule.validate(this.csvSchema); 40 | } 41 | } 42 | 43 | module.exports = { 44 | BaseCSVExtractor, 45 | }; 46 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-clinical-trial-information-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "urn:uuid:aea78e13731d4b105b3a731a48ce77602f159d46299bb02aaaf80e49530115bd", 7 | "resource": { 8 | "resourceType": "ResearchSubject", 9 | "id": "aea78e13731d4b105b3a731a48ce77602f159d46299bb02aaaf80e49530115bd", 10 | "identifier": [ 11 | { 12 | "system": "http://example.com/clinicaltrialsubjectids", 13 | "type": { 14 | "text": "Clinical Trial Subject ID" 15 | }, 16 | "value": "example-subjectId" 17 | } 18 | ], 19 | "status": "example-enrollment-status", 20 | "study": { 21 | "identifier": { 22 | "system": "example-system", 23 | "value": "example-researchId" 24 | } 25 | }, 26 | "individual": { 27 | "reference": "urn:uuid:mrn-1", 28 | "type": "Patient" 29 | }, 30 | "period": { 31 | "start": "2020-01-01", 32 | "end": "2021-01-01" 33 | } 34 | } 35 | }, 36 | { 37 | "fullUrl": "urn:uuid:28bd7a4890539dc60998a1b2d423e631a6d4373eb05001c45bd9f2bab3166506", 38 | "resource": { 39 | "resourceType": "ResearchStudy", 40 | "id": "28bd7a4890539dc60998a1b2d423e631a6d4373eb05001c45bd9f2bab3166506", 41 | "status": "example-trialStatus", 42 | "site": [ 43 | { 44 | "display": "ID associated with Clinical Trial", 45 | "identifier": { 46 | "system": "EXAMPLE-CLINICAL-SITE-SYSTEM", 47 | "value": "EXAMPLE-CLINICAL-SITE-ID" 48 | } 49 | } 50 | ], 51 | "identifier": [ 52 | { 53 | "system": "example-system", 54 | "type": { 55 | "text": "Clinical Trial Research ID" 56 | }, 57 | "value": "example-researchId" 58 | } 59 | ] 60 | } 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-appointment-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "urn:uuid:appointmentId-1", 7 | "resource": { 8 | "resourceType": "Appointment", 9 | "id": "appointmentId-1", 10 | "status": "arrived", 11 | "serviceCategory": [ 12 | { 13 | "coding": [ 14 | { 15 | "system": "http://terminology.hl7.org/CodeSystem/service-category", 16 | "code": "35" 17 | } 18 | ] 19 | } 20 | ], 21 | "serviceType": [ 22 | { 23 | "coding": [ 24 | { 25 | "system": "http://terminology.hl7.org/CodeSystem/service-type", 26 | "code": "175" 27 | } 28 | ] 29 | } 30 | ], 31 | "appointmentType": { 32 | "coding": [ 33 | { 34 | "system": "http://terminology.hl7.org/CodeSystem/v2-0276", 35 | "code": "CHECKUP" 36 | } 37 | ] 38 | }, 39 | "specialty": [ 40 | { 41 | "coding": [ 42 | { 43 | "system": "http://snomed.info/sct", 44 | "code": "394592004" 45 | } 46 | ] 47 | } 48 | ], 49 | "participant": [ 50 | { 51 | "actor": { 52 | "reference": "urn:uuid:mrn-1", 53 | "type": "Patient" 54 | }, 55 | "status": "tentative" 56 | } 57 | ], 58 | "start": "2019-12-10T09:00:00+00:00", 59 | "end": "2019-12-10T11:00:00+00:00", 60 | "cancelationReason": { 61 | "coding": [ 62 | { 63 | "system": "http://terminology.hl7.org/CodeSystem/appointment-cancellation-reason", 64 | "code": "pat-cpp" 65 | } 66 | ] 67 | }, 68 | "description": "Example description 1" 69 | } 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /src/modules/CSVFileModule.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const moment = require('moment'); 3 | const logger = require('../helpers/logger'); 4 | const { validateCSV } = require('../helpers/csvValidator'); 5 | const { csvParse, stringNormalizer, normalizeEmptyValues, getCSVHeader } = require('../helpers/csvParsingUtils'); 6 | 7 | class CSVFileModule { 8 | constructor(csvFilePath, unalterableColumns, parserOptions) { 9 | // Parse then normalize the data 10 | const csvData = fs.readFileSync(csvFilePath); 11 | const parsedData = csvParse(csvData, parserOptions); 12 | this.filePath = csvFilePath; 13 | this.data = normalizeEmptyValues(parsedData, unalterableColumns); 14 | this.header = getCSVHeader(csvData); 15 | } 16 | 17 | async get(key, value, fromDate, toDate) { 18 | logger.debug(`Get csvFileModule info by key '${key}'`); 19 | // return all rows if key and value aren't provided 20 | if (!key && !value) return this.data; 21 | let result = this.data.filter((d) => d[stringNormalizer(key)] === value); 22 | if (result.length === 0) { 23 | logger.warn(`CSV Record with provided key '${key}' and value was not found`); 24 | return result; 25 | } 26 | 27 | // If fromDate and toDate is provided, filter out all results that fall outside that timespan 28 | if (fromDate && moment(fromDate).isValid()) result = result.filter((r) => !(r.daterecorded && moment(fromDate).isAfter(r.daterecorded))); 29 | if (toDate && moment(toDate).isValid()) result = result.filter((r) => !(r.daterecorded && moment(toDate).isBefore(r.daterecorded))); 30 | if (result.length === 0) logger.warn('No data for patient within specified time range'); 31 | return result; 32 | } 33 | 34 | async validate(csvSchema) { 35 | if (csvSchema) { 36 | logger.info(`Validating CSV file for ${this.filePath}`); 37 | return validateCSV(this.filePath, csvSchema, this.data, this.header); 38 | } 39 | logger.warn(`No CSV schema provided for ${this.filePath}`); 40 | return true; 41 | } 42 | } 43 | 44 | 45 | module.exports = { 46 | CSVFileModule, 47 | }; 48 | -------------------------------------------------------------------------------- /test/templates/fixtures/disease-status-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "CancerDiseaseStatus-fixture", 4 | "meta": { 5 | "profile": [ 6 | "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-disease-status" 7 | ] 8 | }, 9 | "category": [ 10 | { 11 | "coding": [ 12 | { 13 | "system": "http://terminology.hl7.org/CodeSystem/observation-category", 14 | "code": "therapy", 15 | "display": "Therapy" 16 | } 17 | ] 18 | } 19 | ], 20 | "status": "final", 21 | "code": { 22 | "coding": [ 23 | { 24 | "system": "http://loinc.org", 25 | "code": "97509-4", 26 | "display": "Cancer Disease Progression" 27 | } 28 | ] 29 | }, 30 | "focus" : [ 31 | { 32 | "reference": "urn:uuid:123-Walking-Corpse-Syndrome", 33 | "display": "Walking Corpse Syndrome", 34 | "type": "Condition" 35 | } 36 | ], 37 | "subject": { 38 | "reference": "urn:uuid:123-example-patient", 39 | "display": "Mr. Patient Example", 40 | "type": "Patient" 41 | }, 42 | "effectiveDateTime" : "1994-12-09T09:07:00Z", 43 | "valueCodeableConcept": { 44 | "coding": [ 45 | { 46 | "system": "http://snomed.info/sct", 47 | "code": "385633008", 48 | "display": "Improving" 49 | } 50 | ] 51 | }, 52 | "extension": [ 53 | { 54 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-evidence-type", 55 | "valueCodeableConcept": { 56 | "coding": [ 57 | { 58 | "system": "http://snomed.info/sct", 59 | "code": "09870987", 60 | "display": "Evidence display text" 61 | } 62 | ] 63 | } 64 | }, 65 | { 66 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-evidence-type", 67 | "valueCodeableConcept": { 68 | "coding": [ 69 | { 70 | "system": "http://snomed.info/sct", 71 | "code": "12341234" 72 | } 73 | ] 74 | } 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-adverse-event-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "urn:uuid:adverseEventId-1", 7 | "resource": { 8 | "resourceType": "AdverseEvent", 9 | "id": "adverseEventId-1", 10 | "subject": { 11 | "reference": "urn:uuid:mrn-1", 12 | "type": "Patient" 13 | }, 14 | "event": { 15 | "coding": [ 16 | { 17 | "system": "code-system", 18 | "code": "109006", 19 | "display": "Anxiety disorder of childhood OR adolescence" 20 | } 21 | ] 22 | }, 23 | "suspectEntity": [ 24 | { 25 | "instance": { 26 | "reference": "urn:uuid:procedure-id", 27 | "type": "Procedure" 28 | } 29 | } 30 | ], 31 | "seriousness": { 32 | "coding": [ 33 | { 34 | "system": "http://terminology.hl7.org/CodeSystem/adverse-event-seriousness", 35 | "code": "serious", 36 | "display": "Serious" 37 | } 38 | ] 39 | }, 40 | "category": [ 41 | { 42 | "coding": [ 43 | { 44 | "system": "http://terminology.hl7.org/CodeSystem/adverse-event-category", 45 | "code": "product-use-error", 46 | "display": "Product Use Error" 47 | } 48 | ] 49 | } 50 | ], 51 | "severity": { 52 | "coding": [ 53 | { 54 | "system": "http://terminology.hl7.org/CodeSystem/adverse-event-severity", 55 | "code": "severe" 56 | } 57 | ] 58 | }, 59 | "actuality": "actual", 60 | "study": [ 61 | { 62 | "reference": "urn:uuid:researchId-1", 63 | "type": "ResearchStudy" 64 | } 65 | ], 66 | "date": "1994-12-09", 67 | "recordedDate": "1994-12-09" 68 | } 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /src/helpers/dependencyUtils.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger'); 2 | 3 | /** 4 | * Checks if any dependencies of extractors are missing. 5 | * If no depedencies are missing, sorts extractors into the correct order. 6 | * @param {Array} extractors array of extractors from the config file 7 | * @param {Array} dependencyInfo array of objects dictacting order of extractors and their dependencies 8 | * @returns {Array} array of extractors sorted into the order defined in dependency info 9 | * Example of dependencyInfo: 10 | * [ 11 | * { type: 'CSVPatientExtractor', dependencies: [] }, 12 | * { type: 'CSVConditionExtractor', dependencies: ['CSVPatientExtractor'] }, 13 | * ... 14 | * ] 15 | */ 16 | function sortExtractors(extractors, dependencyInfo) { 17 | const missing = {}; 18 | // Filter dependency info to only extractors in the config 19 | dependencyInfo.filter((e) => extractors.map((x) => x.type).includes(e.type)).forEach((extractor) => { 20 | // For each extractor, check if its dependencies are present 21 | extractor.dependencies.forEach((dependency) => { 22 | if (extractors.filter((e) => e.type === dependency).length === 0) { 23 | if (missing[dependency] === undefined) { 24 | missing[dependency] = [extractor.type]; 25 | } else { 26 | missing[dependency].push(extractor.type); 27 | } 28 | } 29 | }); 30 | }); 31 | // If extractors are missing, alert user which are missing 32 | if (Object.keys(missing).length > 0) { 33 | Object.keys(missing).forEach((extractor) => { 34 | logger.error(`Missing dependency: ${extractor} (required by: ${missing[extractor].join(', ')})`); 35 | }); 36 | throw new Error('Some extractors are missing dependencies, see above for details.'); 37 | } 38 | // If no missing dependencies, sort extractors into correct order 39 | const sortedExtractors = [...extractors]; 40 | const order = dependencyInfo.map((x) => x.type); 41 | sortedExtractors.sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)); 42 | return sortedExtractors; 43 | } 44 | 45 | module.exports = { 46 | sortExtractors, 47 | }; 48 | -------------------------------------------------------------------------------- /src/extractors/MCODESurgicalProcedureExtractor.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { Extractor } = require('./Extractor'); 3 | const { FHIRProcedureExtractor } = require('./FHIRProcedureExtractor'); 4 | const { checkCodeInVs } = require('../helpers/valueSetUtils'); 5 | const logger = require('../helpers/logger'); 6 | 7 | function getMCODESurgicalProcedures(fhirProcedures) { 8 | const surgicalProcedureVSFilepath = path.resolve(__dirname, '..', 'helpers', 'valueSets', 'ValueSet-mcode-cancer-related-surgical-procedure-vs.json'); 9 | return fhirProcedures.filter((procedure) => { 10 | const coding = procedure.resource.code ? procedure.resource.code.coding : []; 11 | return coding.some((c) => checkCodeInVs(c.code, c.system, surgicalProcedureVSFilepath)); 12 | }); 13 | } 14 | 15 | class MCODESurgicalProcedureExtractor extends Extractor { 16 | constructor({ baseFhirUrl, requestHeaders }) { 17 | super({ baseFhirUrl, requestHeaders }); 18 | logger.debug('Note that MCODESurgicalProcedureExtractor uses FHIR to get FHIR Procedures.'); 19 | this.fhirProcedureExtractor = new FHIRProcedureExtractor({ baseFhirUrl, requestHeaders }); 20 | } 21 | 22 | updateRequestHeaders(newHeaders) { 23 | this.fhirProcedureExtractor.updateRequestHeaders(newHeaders); 24 | } 25 | 26 | async getFHIRProcedures(mrn, context) { 27 | logger.debug('Getting procedures available for patient'); 28 | const procedureBundle = await this.fhirProcedureExtractor.get({ mrn, context }); 29 | 30 | logger.debug(`Found ${procedureBundle.entry.length} result(s) in Procedure search`); 31 | return procedureBundle.entry; 32 | } 33 | 34 | async get({ mrn, context }) { 35 | const fhirProcedures = await this.getFHIRProcedures(mrn, context); 36 | 37 | // Filter to only include procedures that are from MCODE surgical procedure VS 38 | const surgicalProcedures = getMCODESurgicalProcedures(fhirProcedures); 39 | 40 | return { 41 | resourceType: 'Bundle', 42 | type: 'collection', 43 | entry: surgicalProcedures, 44 | }; 45 | } 46 | } 47 | 48 | module.exports = { 49 | MCODESurgicalProcedureExtractor, 50 | }; 51 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-condition-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Condition", 3 | "id": "example-id", 4 | "extension": [ 5 | { 6 | "url": "http://hl7.org/fhir/StructureDefinition/condition-assertedDate", 7 | "valueDateTime": "2020-01-01" 8 | }, 9 | { 10 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-histology-morphology-behavior", 11 | "valueCodeableConcept": { 12 | "coding": [ 13 | { 14 | "system": "http://snomed.info/sct", 15 | "code": "example-code" 16 | } 17 | ] 18 | } 19 | } 20 | ], 21 | "clinicalStatus": { 22 | "coding": [ 23 | { 24 | "system": "http://terminology.hl7.org/CodeSystem/condition-clinical", 25 | "code": "example-code" 26 | } 27 | ] 28 | }, 29 | "verificationStatus": { 30 | "coding": [ 31 | { 32 | "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status", 33 | "code": "example-code" 34 | } 35 | ] 36 | }, 37 | "category": [ 38 | { 39 | "coding": [ 40 | { 41 | "system": "http://terminology.hl7.org/CodeSystem/condition-category", 42 | "code": "example-code" 43 | } 44 | ] 45 | } 46 | ], 47 | "code": { 48 | "coding": [ 49 | { 50 | "system": "example-system", 51 | "code": "example-code", 52 | "display": "exampleDisplayName" 53 | } 54 | ] 55 | }, 56 | "bodySite": [ 57 | { 58 | "extension": [ 59 | { 60 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-laterality", 61 | "valueCodeableConcept": { 62 | "coding": [ 63 | { 64 | "system": "http://snomed.info/sct", 65 | "code": "example-code" 66 | } 67 | ] 68 | } 69 | } 70 | ], 71 | "coding": [ 72 | { 73 | "system": "http://snomed.info/sct", 74 | "code": "example-code" 75 | } 76 | ] 77 | } 78 | ], 79 | "subject": { 80 | "reference": "urn:uuid:example-subject-id", 81 | "type": "Patient" 82 | } 83 | } -------------------------------------------------------------------------------- /src/extractors/FHIRAdverseEventExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); 2 | const { getResearchStudiesFromContext } = require('../helpers/contextUtils'); 3 | const logger = require('../helpers/logger'); 4 | 5 | const BASE_STUDY = ''; // No base study specified 6 | 7 | class FHIRAdverseEventExtractor extends BaseFHIRExtractor { 8 | constructor({ 9 | baseFhirUrl, requestHeaders, version, study, searchParameters, 10 | }) { 11 | super({ baseFhirUrl, requestHeaders, version, searchParameters }); 12 | this.resourceType = 'AdverseEvent'; 13 | this.study = study || BASE_STUDY; 14 | } 15 | 16 | // In addition to default parametrization, add study if specified 17 | async parametrizeArgsForFHIRModule({ context }) { 18 | const paramsWithID = await super.parametrizeArgsForFHIRModule({ context }); 19 | let allResearchStudyResources = []; 20 | try { 21 | allResearchStudyResources = getResearchStudiesFromContext(context); 22 | } catch (e) { 23 | logger.debug(e.message); 24 | logger.debug(e.stack); 25 | if (!this.study) { 26 | logger.error('There is no ResearchStudy id to complete a request for Adverse Event resources; please include a ClinicalTrialInformationExtractor,' 27 | + ' ResearchStudyExtractor, or "study" constructorArg in your extraction configuration.'); 28 | } 29 | } 30 | 31 | // The patient is referenced in the 'subject' field of an AdverseEvent 32 | paramsWithID.subject = paramsWithID.patient; 33 | delete paramsWithID.patient; 34 | 35 | // If there are research study resources, create a parameters object for each call to be made 36 | const newStudyIds = allResearchStudyResources.map((rs) => rs.id).join(','); 37 | const studyIdsForCurrentPatient = `${this.study}${this.study && newStudyIds ? ',' : ''}${newStudyIds}`; 38 | 39 | // Only add study to parameters if it has been specified or was included from context 40 | const obj = { 41 | ...paramsWithID, 42 | ...(studyIdsForCurrentPatient && { study: studyIdsForCurrentPatient }), 43 | }; 44 | return obj; 45 | } 46 | } 47 | 48 | module.exports = { 49 | FHIRAdverseEventExtractor, 50 | }; 51 | -------------------------------------------------------------------------------- /test/extractors/fixtures/csv-procedure-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "collection", 4 | "entry": [ 5 | { 6 | "fullUrl": "urn:uuid:procedure-1", 7 | "resource": { 8 | "resourceType": "Procedure", 9 | "id": "procedure-1", 10 | "status": "completed", 11 | "code": { 12 | "coding": [ 13 | { 14 | "system": "http://snomed.info/sct", 15 | "code": "152198000", 16 | "display": "Brachytherapy (procedure)" 17 | } 18 | ] 19 | }, 20 | "subject": { "reference": "urn:uuid:mrn-1", "type": "Patient" }, 21 | "performedDateTime": "2020-01-01", 22 | "reasonCode": [ 23 | { 24 | "coding": [ 25 | { 26 | "system": "example-system", 27 | "code": "example-code", 28 | "display": "example-name" 29 | } 30 | ] 31 | } 32 | ], 33 | "reasonReference": [{ "reference": "urn:uuid:condition-id", "type": "Condition" }], 34 | "extension": [ 35 | { 36 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-treatment-intent", 37 | "valueCodeableConcept": { 38 | "coding": [ 39 | { 40 | "system": "http://snomed.info/sct", 41 | "code": "example-treatment-intent" 42 | } 43 | ] 44 | } 45 | } 46 | ], 47 | "bodySite": [ 48 | { 49 | "extension": [ 50 | { 51 | "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-laterality", 52 | "valueCodeableConcept": { 53 | "coding": [ 54 | { 55 | "system": "http://snomed.info/sct", 56 | "code": "example-laterality" 57 | } 58 | ] 59 | } 60 | } 61 | ], 62 | "coding": [ 63 | { "system": "http://snomed.info/sct", "code": "example-site" } 64 | ] 65 | } 66 | ] 67 | } 68 | } 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /test/templates/fixtures/maximal-patient-resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Patient", 3 | "id": "SomeId", 4 | "identifier": [ 5 | { 6 | "type": { 7 | "coding": [ 8 | { 9 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203", 10 | "code": "MR", 11 | "display": "Medical Record Number" 12 | } 13 | ], 14 | "text": "Medical Record Number" 15 | }, 16 | "system": "http://example.com/system/mrn", 17 | "value": "1234" 18 | } 19 | ], 20 | "name": [ 21 | { 22 | "text": "Test Patient", 23 | "family": "Patient", 24 | "given": [ 25 | "Test" 26 | ] 27 | } 28 | ], 29 | "gender": "female", 30 | "birthDate": "2001-02-06", 31 | "address": [ 32 | { 33 | "line": [ 34 | "57 Adams St" 35 | ], 36 | "city": "New Rochelle", 37 | "state": "NY", 38 | "postalCode": "10801", 39 | "country": "US" 40 | } 41 | ], 42 | "communication": [ 43 | { 44 | "language": { 45 | "coding": [ 46 | { 47 | "system": "urn:ietf:bcp:47", 48 | "code": "en" 49 | } 50 | ] 51 | } 52 | } 53 | ], 54 | "extension": [ 55 | { 56 | "extension": [ 57 | { 58 | "url": "ombCategory", 59 | "valueCoding": { 60 | "system": "http://some.codesystem.com", 61 | "code": "29178" 62 | } 63 | }, 64 | { 65 | "url": "text", 66 | "valueString": "Some Race" 67 | } 68 | ], 69 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race" 70 | }, 71 | { 72 | "extension": [ 73 | { 74 | "url": "ombCategory", 75 | "valueCoding": { 76 | "system": "urn:oid:2.16.840.1.113883.6.238", 77 | "code": "90210" 78 | } 79 | }, 80 | { 81 | "url": "text", 82 | "valueString": "Some Ethnicity" 83 | } 84 | ], 85 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" 86 | }, 87 | { 88 | "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", 89 | "valueCode": "female" 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /test/helpers/observationUtils.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | isVitalSign, isTumorMarker, isKarnofskyPerformanceStatus, isECOGPerformanceStatus, vitalSignsCodeToTextLookup, 3 | } = require('../../src/helpers/observationUtils.js'); 4 | 5 | describe('observationUtils', () => { 6 | test('isVitalSign should return true when passed a valid Vital Sign code', () => { 7 | Object.keys(vitalSignsCodeToTextLookup).forEach((code) => { 8 | expect(isVitalSign(code)).toEqual(true); 9 | }); 10 | }); 11 | test('isVitalSign should return false when passed a code that does not belong to a Vital Sign', () => { 12 | const code = '12345'; 13 | expect(isVitalSign(code)).toEqual(false); 14 | }); 15 | test('isTumorMarker should return true when passed a valid Tumor marker code', () => { 16 | const her2InTissue = '48676-1'; 17 | const loincSystem = 'http://loinc.org'; 18 | expect(isTumorMarker(her2InTissue, loincSystem)).toEqual(true); 19 | }); 20 | test('isTumorMarker should return false when passed a code that does not belong to a Tumor Marker', () => { 21 | const code = '12345'; 22 | const loincSystem = 'http://loinc.org'; 23 | expect(isTumorMarker(code, loincSystem)).toEqual(false); 24 | }); 25 | test('isTumorMarker should return false when passed a valid Tumor marker code but an invalid code system', () => { 26 | const her2InTissue = '48676-1'; 27 | const snomedSystem = 'http://snomed.info/sct'; 28 | expect(isTumorMarker(her2InTissue, snomedSystem)).toEqual(false); 29 | }); 30 | test('isKarnofskyPerformanceStatus should return true when passed the correct code', () => { 31 | const code = '89243-0'; 32 | expect(isKarnofskyPerformanceStatus(code)).toEqual(true); 33 | }); 34 | test('isKarnofskyPerformanceStatus should return false when passed an incorrect code', () => { 35 | const code = '12345'; 36 | expect(isKarnofskyPerformanceStatus(code)).toEqual(false); 37 | }); 38 | test('isECOGPerformanceStatus should return true when passed the correct code', () => { 39 | const code = '89247-1'; 40 | expect(isECOGPerformanceStatus(code)).toEqual(true); 41 | }); 42 | test('isECOGPerformanceStatus should return false when passed an incorrect code', () => { 43 | const code = '12345'; 44 | expect(isECOGPerformanceStatus(code)).toEqual(false); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/helpers/configUtils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Ajv = require('ajv'); 4 | const metaSchema = require('ajv/lib/refs/json-schema-draft-06.json'); 5 | const logger = require('./logger'); 6 | const configSchema = require('./schemas/config.schema.json'); 7 | 8 | function getConfig(pathToConfig) { 9 | // Checks pathToConfig points to valid JSON file 10 | const fullPath = path.resolve(pathToConfig); 11 | try { 12 | return JSON.parse(fs.readFileSync(fullPath)); 13 | } catch (err) { 14 | throw new Error(`The provided filepath to a configuration file ${pathToConfig}, full path ${fullPath} did not point to a valid JSON file.`); 15 | } 16 | } 17 | 18 | const ajv = new Ajv({ logger: false, allErrors: true }); 19 | ajv.addMetaSchema(metaSchema); 20 | 21 | ajv.addFormat('comma-separated-emails', { 22 | type: 'string', 23 | validate: (emails) => { 24 | // this is Ajv's regex for email format (https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts#L106) 25 | const emailRegex = new RegExp(/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i); 26 | return emails.split(',').every((email) => emailRegex.test(email.trim())); 27 | }, 28 | }); 29 | ajv.addFormat('email-with-name', { 30 | type: 'string', 31 | validate: (email) => { 32 | // this is Ajv's regex for email format (https://github.com/ajv-validator/ajv-formats/blob/master/src/formats.ts#L106) 33 | const emailRegex = new RegExp(/^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i); 34 | return emailRegex.test(email.trim().split(' ').pop()); 35 | }, 36 | }); 37 | ajv.addFormat('mask-all', { 38 | type: 'string', 39 | validate: (string) => string === 'all', 40 | }); 41 | 42 | const validator = ajv.addSchema(configSchema, 'config'); 43 | 44 | function validateConfig(config) { 45 | logger.debug('Validating config file'); 46 | const valid = validator.validate('config', config); 47 | const errors = ajv.errorsText(validator.errors, { dataVar: 'config' }); 48 | if (!valid) throw new Error(`Error(s) found in config file: ${errors}`); 49 | logger.debug('Config file validated successfully'); 50 | } 51 | 52 | module.exports = { 53 | getConfig, 54 | validateConfig, 55 | }; 56 | -------------------------------------------------------------------------------- /test/templates/encounter.test.js: -------------------------------------------------------------------------------- 1 | const { isValidFHIR } = require('../../src/helpers/fhirUtils'); 2 | const maximalEncounter = require('./fixtures/maximal-encounter-resource.json'); 3 | const minimalEncounter = require('./fixtures/minimal-encounter-resource.json'); 4 | const { encounterTemplate } = require('../../src/templates/EncounterTemplate'); 5 | 6 | describe('test Encounter template', () => { 7 | test('valid data passed into template should generate valid FHIR resource', () => { 8 | const MAX_DATA = { 9 | id: 'encounterId-1', 10 | status: 'arrived', 11 | subject: { 12 | id: '123', 13 | }, 14 | classCode: 'AMB', 15 | classSystem: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', 16 | typeCode: '11429006', 17 | typeSystem: 'http://snomed.info/sct', 18 | startDate: '2020-01-10', 19 | endDate: '2020-01-10', 20 | }; 21 | 22 | const generatedEncounter = encounterTemplate(MAX_DATA); 23 | 24 | // Relevant fields should match the valid FHIR 25 | expect(generatedEncounter).toEqual(maximalEncounter); 26 | expect(isValidFHIR(generatedEncounter)).toBeTruthy(); 27 | }); 28 | 29 | test('valid data with only required attributes passed into template should generate valid FHIR resource', () => { 30 | const MINIMAL_DATA = { 31 | id: 'encounterId-1', 32 | status: 'arrived', 33 | subject: { 34 | id: '123', 35 | }, 36 | classCode: 'AMB', 37 | classSystem: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', 38 | }; 39 | 40 | const generatedEncounter = encounterTemplate(MINIMAL_DATA); 41 | 42 | // Relevant fields should match the valid FHIR 43 | expect(generatedEncounter).toEqual(minimalEncounter); 44 | expect(isValidFHIR(generatedEncounter)).toBeTruthy(); 45 | }); 46 | 47 | test('missing required data should throw an error', () => { 48 | const INVALID_DATA = { 49 | id: 'encounterId-1', 50 | subject: { 51 | id: '123', 52 | }, 53 | classCode: 'AMB', 54 | classSystem: 'http://terminology.hl7.org/CodeSystem/v3-ActCode', 55 | typeCode: '11429006', 56 | typeSystem: 'http://snomed.info/sct', 57 | startDate: '2020-01-10', 58 | endDate: '2020-01-10', 59 | }; 60 | 61 | expect(() => encounterTemplate(INVALID_DATA)).toThrow(Error); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/extractors/fixtures/patient-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Bundle", 3 | "type": "searchset", 4 | "total": 1, 5 | "link": [ 6 | { 7 | "relation": "self" 8 | } 9 | ], 10 | "entry": [ 11 | { 12 | "link": [ 13 | { 14 | "relation": "self" 15 | } 16 | ], 17 | "resource": { 18 | "resourceType": "Patient", 19 | "id": "EXAMPLE-MRN", 20 | "extension": [ 21 | { 22 | "url": "http://hl7.org/fhir/StructureDefinition/us-core-race", 23 | "valueCodeableConcept": { 24 | "coding": [ 25 | { 26 | "system": "urn:oid:2.16.840.1.113883.5.104", 27 | "code": "UNK", 28 | "display": "Unknown" 29 | } 30 | ], 31 | "text": "Unknown" 32 | } 33 | }, 34 | { 35 | "url": "http://hl7.org/fhir/StructureDefinition/us-core-ethnicity", 36 | "valueCodeableConcept": { 37 | "coding": [ 38 | { 39 | "system": "urn:oid:2.16.840.1.113883.5.50", 40 | "code": "UNK", 41 | "display": "Unknown" 42 | } 43 | ], 44 | "text": "Unknown" 45 | } 46 | }, 47 | { 48 | "url": "http://hl7.org/fhir/StructureDefinition/us-core-birth-sex", 49 | "valueCodeableConcept": { 50 | "coding": [ 51 | { 52 | "system": "http://hl7.org/fhir/v3/AdministrativeGender", 53 | "code": "F", 54 | "display": "Female" 55 | } 56 | ], 57 | "text": "Female" 58 | } 59 | } 60 | ], 61 | "active": true, 62 | "name": [ 63 | { 64 | "use": "usual", 65 | "text": "Elizabeth-Test Smith-Mitre", 66 | "family": [ 67 | "Smith-Mitre" 68 | ], 69 | "given": [ 70 | "Elizabeth-Test" 71 | ] 72 | } 73 | ], 74 | "gender": "female", 75 | "birthDate": "1942-01-07", 76 | "deceasedBoolean": false 77 | }, 78 | "search": { 79 | "mode": "match" 80 | } 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /src/cli/cli.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const program = require('commander'); 4 | const { MCODEClient } = require('../client/MCODEClient'); 5 | const logger = require('../helpers/logger'); 6 | const { mcodeApp } = require('../application'); 7 | const { getConfig } = require('../helpers/configUtils'); 8 | 9 | const defaultPathToConfig = path.join('config', 'csv.config.json'); 10 | const defaultPathToRunLogs = path.join('logs', 'run-logs.json'); 11 | 12 | program 13 | .usage('[options]') 14 | .option('-f --from-date ', 'The earliest date and time to search') 15 | .option('-t --to-date ', 'The latest date and time to search') 16 | .option('-e, --entries-filter', 'Flag to indicate to filter data by date') 17 | .option('-c --config-filepath ', 'Specify relative path to config to use:', defaultPathToConfig) 18 | .option('-r --run-log-filepath ', 'Specify relative path to log file of previous runs:', defaultPathToRunLogs) 19 | .option('-d, --debug', 'output extra debugging information') 20 | .parse(process.argv); 21 | 22 | const { 23 | fromDate, toDate, configFilepath, runLogFilepath, debug, entriesFilter, 24 | } = program; 25 | 26 | // Flag to extract allEntries, or just to use to-from dates 27 | const allEntries = !entriesFilter; 28 | 29 | async function runApp() { 30 | try { 31 | const config = getConfig(configFilepath); 32 | const extractedData = await mcodeApp(MCODEClient, fromDate, toDate, config, runLogFilepath, debug, allEntries); 33 | 34 | // Finally, save the data to disk 35 | const outputPath = './output'; 36 | if (!fs.existsSync(outputPath)) { 37 | logger.info(`Creating directory ${outputPath}`); 38 | fs.mkdirSync(outputPath); 39 | } 40 | // For each bundle in our extractedData, write it to our output directory 41 | extractedData.forEach((bundle, i) => { 42 | const outputFile = path.join(outputPath, `mcode-extraction-patient-${i + 1}.json`); 43 | logger.debug(`Logging mCODE output to ${outputFile}`); 44 | fs.writeFileSync(outputFile, JSON.stringify(bundle), 'utf8'); 45 | }); 46 | logger.info(`Successfully logged ${extractedData.length} mCODE bundle(s) to ${outputPath}`); 47 | } catch (e) { 48 | if (debug) logger.level = 'debug'; 49 | logger.error(e.message); 50 | logger.debug(e.stack); 51 | process.exit(1); 52 | } 53 | } 54 | 55 | runApp(); 56 | -------------------------------------------------------------------------------- /src/templates/CancerRelatedMedicationAdministrationTemplate.js: -------------------------------------------------------------------------------- 1 | const { 2 | dataAbsentReasonExtension, 3 | extensionArr, 4 | valueX, 5 | medicationTemplate, 6 | subjectTemplate, 7 | treatmentReasonTemplate, 8 | } = require('./snippets'); 9 | const { ifAllArgsObj } = require('../helpers/templateUtils'); 10 | 11 | function treatmentIntentTemplate({ treatmentIntent }) { 12 | return { 13 | url: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-treatment-intent', 14 | ...valueX({ code: treatmentIntent, system: 'http://snomed.info/sct' }, 'valueCodeableConcept'), 15 | }; 16 | } 17 | 18 | function periodTemplate({ startDate, endDate }) { 19 | // If start and end date are not provided, indicate data absent with extension. 20 | if (!startDate && !endDate) { 21 | return { 22 | effectivePeriod: extensionArr(dataAbsentReasonExtension('unknown')), 23 | }; 24 | } 25 | 26 | return { 27 | effectivePeriod: { 28 | ...(startDate && { start: startDate }), 29 | ...(endDate && { end: endDate }), 30 | }, 31 | }; 32 | } 33 | 34 | function cancerRelatedMedicationAdministrationTemplate({ 35 | subjectId, 36 | id, 37 | code, 38 | codeSystem, 39 | displayText, 40 | startDate, 41 | endDate, 42 | treatmentReasonCode, 43 | treatmentReasonCodeSystem, 44 | treatmentReasonDisplayText, 45 | treatmentIntent, 46 | status, 47 | }) { 48 | if (!(subjectId && code && codeSystem && status)) { 49 | throw Error('Trying to render a CancerRelatedMedicationAdministrationTemplate, but a required argument is missing; ensure that subjectId, code, code system, and status are all present'); 50 | } 51 | 52 | return { 53 | resourceType: 'MedicationAdministration', 54 | id, 55 | meta: { 56 | profile: [ 57 | 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-administration', 58 | ], 59 | }, 60 | ...extensionArr(ifAllArgsObj(treatmentIntentTemplate)({ treatmentIntent })), 61 | status, 62 | ...medicationTemplate({ code, codeSystem, displayText }), 63 | ...ifAllArgsObj(subjectTemplate)({ id: subjectId }), 64 | ...periodTemplate({ startDate, endDate }), 65 | ...ifAllArgsObj(treatmentReasonTemplate)({ treatmentReasonCode, treatmentReasonCodeSystem, treatmentReasonDisplayText }), 66 | }; 67 | } 68 | 69 | module.exports = { 70 | cancerRelatedMedicationAdministrationTemplate, 71 | }; 72 | -------------------------------------------------------------------------------- /test/application/fixtures/test-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "fullUrl": "urn:uuid:638f2b81-0cf5-4f8f-90c1-bac3704ccfb6", 3 | "resource": { 4 | "resourceType": "Bundle", 5 | "id": "638f2b81-0cf5-4f8f-90c1-bac3704ccfb6", 6 | "type": "collection", 7 | "entry": [ 8 | { 9 | "fullUrl": "urn:uuid:any-unique-id", 10 | "resource": { 11 | "resourceType": "Patient", 12 | "id": "any-unique-id", 13 | "identifier": [ 14 | { 15 | "type": { 16 | "coding": [ 17 | { 18 | "system": "http://terminology.hl7.org/CodeSystem/v2-0203", 19 | "code": "MR", 20 | "display": "Medical Record Number" 21 | } 22 | ], 23 | "text": "Medical Record Number" 24 | }, 25 | "system": "http://example.com/system/mrn", 26 | "value": "119147111821125" 27 | } 28 | ], 29 | "name": [ 30 | { 31 | "text": "Archy Marshall", 32 | "family": "Marshall", 33 | "given": [ 34 | "Archy" 35 | ] 36 | } 37 | ], 38 | "gender": "male" 39 | } 40 | }, 41 | { 42 | "fullUrl": "urn:uuid:conditionId-1", 43 | "resource": { 44 | "resourceType": "Condition", 45 | "id": "conditionId-1", 46 | "subject": { 47 | "reference": "urn:uuid:mrn-1" 48 | }, 49 | "code": { 50 | "coding": [ 51 | { 52 | "system": "example-code-system", 53 | "code": "example-code" 54 | } 55 | ] 56 | }, 57 | "category": [ 58 | { 59 | "coding": [ 60 | { 61 | "system": "http://terminology.hl7.org/CodeSystem/condition-category", 62 | "code": "problem-list-item" 63 | } 64 | ] 65 | } 66 | ], 67 | "verificationStatus": { 68 | "coding": [ 69 | { 70 | "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status", 71 | "code": "confirmed" 72 | } 73 | ] 74 | } 75 | } 76 | } 77 | ] 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/extractors/CSVConditionExtractor.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const _ = require('lodash'); 3 | const { CSVConditionExtractor } = require('../../src/extractors'); 4 | const exampleConditionResponse = require('./fixtures/csv-condition-module-response.json'); 5 | const exampleConditionBundle = require('./fixtures/csv-condition-bundle.json'); 6 | const MOCK_CONTEXT = require('./fixtures/context-with-patient.json'); 7 | 8 | // Constants for mock tests 9 | const MOCK_PATIENT_MRN = 'mrn-1';// linked to values in example-module-response and context-with-patient above 10 | const MOCK_CSV_PATH = path.join(__dirname, 'fixtures/example.csv'); // need a valid path/csv here to avoid parse error 11 | const csvConditionExtractor = new CSVConditionExtractor({ 12 | filePath: MOCK_CSV_PATH, 13 | }); 14 | 15 | const { csvModule } = csvConditionExtractor; 16 | 17 | // Spy on csvModule 18 | const csvModuleSpy = jest.spyOn(csvModule, 'get'); 19 | 20 | // Creating an example bundle with two conditions 21 | const exampleEntry = exampleConditionResponse[0]; 22 | const expandedExampleBundle = _.cloneDeep(exampleConditionBundle); 23 | expandedExampleBundle.entry.push(exampleConditionBundle.entry[0]); 24 | 25 | 26 | describe('CSV Condition Extractor tests', () => { 27 | describe('get', () => { 28 | test('get should return a fhir bundle when MRN is known', async () => { 29 | csvModuleSpy.mockReturnValue(exampleConditionResponse); 30 | const data = await csvConditionExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); 31 | 32 | expect(data.resourceType).toEqual('Bundle'); 33 | expect(data.type).toEqual('collection'); 34 | expect(data.entry).toBeDefined(); 35 | expect(data.entry.length).toEqual(1); 36 | expect(data).toEqual(exampleConditionBundle); 37 | }); 38 | 39 | test('get() should return an array of 2 when two conditions are tied to a single patient', async () => { 40 | exampleConditionResponse.push(exampleEntry); 41 | csvModuleSpy.mockReturnValue(exampleConditionResponse); 42 | const data = await csvConditionExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); 43 | 44 | expect(data.resourceType).toEqual('Bundle'); 45 | expect(data.type).toEqual('collection'); 46 | expect(data.entry).toBeDefined(); 47 | expect(data.entry.length).toEqual(2); 48 | expect(data).toEqual(expandedExampleBundle); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/templates/appointment.test.js: -------------------------------------------------------------------------------- 1 | const { isValidFHIR } = require('../../src/helpers/fhirUtils'); 2 | const maximalAppointment = require('./fixtures/maximal-appointment-resource.json'); 3 | const minimalAppointment = require('./fixtures/minimal-appointment-resource.json'); 4 | const { appointmentTemplate } = require('../../src/templates/AppointmentTemplate'); 5 | 6 | describe('test Appointment template', () => { 7 | test('valid data passed into template should generate valid FHIR resource', () => { 8 | const MAX_DATA = { 9 | id: 'appointmentId-1', 10 | patientParticipant: { 11 | id: '123', 12 | }, 13 | status: 'arrived', 14 | serviceCategory: '35', 15 | serviceType: '175', 16 | appointmentType: 'CHECKUP', 17 | specialty: '394592004', 18 | start: '2019-12-10T09:00:00+00:00', 19 | end: '2019-12-10T11:00:00+00:00', 20 | cancelationCode: 'pat-cpp', 21 | description: 'Example description 1', 22 | }; 23 | 24 | const generatedAppointment = appointmentTemplate(MAX_DATA); 25 | 26 | // Relevant fields should match the valid FHIR 27 | expect(generatedAppointment).toEqual(maximalAppointment); 28 | expect(isValidFHIR(generatedAppointment)).toBeTruthy(); 29 | }); 30 | 31 | test('valid data with only required attributes passed into template should generate valid FHIR resource', () => { 32 | const MINIMAL_DATA = { 33 | id: 'appointmentId-3', 34 | patientParticipant: { 35 | id: '789', 36 | }, 37 | status: 'cancelled', 38 | }; 39 | 40 | const generatedAppointment = appointmentTemplate(MINIMAL_DATA); 41 | 42 | // Relevant fields should match the valid FHIR 43 | expect(generatedAppointment).toEqual(minimalAppointment); 44 | expect(isValidFHIR(generatedAppointment)).toBeTruthy(); 45 | }); 46 | 47 | test('missing required data should throw an error', () => { 48 | const INVALID_DATA = { 49 | id: 'appointmentId-1', 50 | patientParticipant: { 51 | id: '123', 52 | }, 53 | serviceCategory: '35', 54 | serviceType: '175', 55 | appointmentType: 'CHECKUP', 56 | specialty: '394592004', 57 | start: '2019-12-10T09:00:00+00:00', 58 | end: '2019-12-10T11:00:00+00:00', 59 | cancelationCode: 'pat-cpp', 60 | description: 'Example description 1', 61 | }; 62 | 63 | expect(() => appointmentTemplate(INVALID_DATA)).toThrow(Error); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/templates/snippets/cancerStaging.test.js: -------------------------------------------------------------------------------- 1 | const { stagingMethodTemplate } = require('../../../src/templates/snippets'); 2 | const cancerStagingSystemVS = require('../../../src/helpers/valueSets/ValueSet-mcode-cancer-staging-system-vs.json'); 3 | 4 | describe('cancerStaging snippets', () => { 5 | describe('stagingMethodTemplate', () => { 6 | const stagingCode = cancerStagingSystemVS.expansion.contains[0].code; 7 | const stagingCodeSystem = cancerStagingSystemVS.expansion.contains[0].system; 8 | const expectedStagingMethod = { 9 | method: { 10 | coding: [ 11 | { 12 | code: stagingCode, 13 | system: stagingCodeSystem, 14 | }, 15 | ], 16 | }, 17 | }; 18 | test('it returns null when provided an empty object as an argument', () => { 19 | expect(stagingMethodTemplate({})).toBe(null); 20 | }); 21 | test("it returns null when argument's code property is null", () => { 22 | expect(stagingMethodTemplate({ code: null })).toBe(null); 23 | }); 24 | test('it returns an object with a well defined method property when code is in the valueset', () => { 25 | const stagingMethod = stagingMethodTemplate({ code: stagingCode, system: stagingCodeSystem }); 26 | expect(stagingMethod).toEqual(expectedStagingMethod); 27 | }); 28 | test('Special case: it returns an object with a well defined method property when code is C146985', () => { 29 | const specialStagingCode = 'C146985'; 30 | const specialStagingCodeSystem = 'http://ncimeta.nci.nih.gov'; 31 | 32 | const expectedSpecialStagingMethod = { 33 | method: { 34 | coding: [ 35 | { 36 | code: specialStagingCode, 37 | system: specialStagingCodeSystem, 38 | }, 39 | ], 40 | }, 41 | }; 42 | const stagingMethod = stagingMethodTemplate({ code: specialStagingCode }); 43 | expect(stagingMethod).toEqual(expectedSpecialStagingMethod); 44 | }); 45 | test('it returns a method with a code and no system when provided an unknown code', () => { 46 | const unknownStagingCode = 'anything-goes'; 47 | const unknownStagingMethod = { 48 | method: { 49 | coding: [ 50 | { 51 | code: unknownStagingCode, 52 | }, 53 | ], 54 | }, 55 | }; 56 | expect(stagingMethodTemplate({ code: unknownStagingCode })).toEqual(unknownStagingMethod); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/templates/ProcedureTemplate.js: -------------------------------------------------------------------------------- 1 | const { 2 | bodySiteTemplate, 3 | coding, 4 | extensionArr, 5 | reference, 6 | valueX, 7 | } = require('./snippets'); 8 | const { ifAllArgsObj, ifSomeArgsObj } = require('../helpers/templateUtils'); 9 | 10 | function reasonTemplate({ reasonCode, reasonCodeSystem, reasonDisplayName }) { 11 | return { 12 | reasonCode: [ 13 | { 14 | coding: [ 15 | coding({ 16 | system: reasonCodeSystem, 17 | code: reasonCode, 18 | display: reasonDisplayName, 19 | }), 20 | ], 21 | }, 22 | ], 23 | }; 24 | } 25 | 26 | function reasonReference(conditionId) { 27 | return { 28 | reasonReference: [ 29 | reference({ id: conditionId, resourceType: 'Condition' }), 30 | ], 31 | }; 32 | } 33 | 34 | function treatmentIntentTemplate({ treatmentIntent }) { 35 | return { 36 | url: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-treatment-intent', 37 | ...valueX({ 38 | system: 'http://snomed.info/sct', 39 | code: treatmentIntent, 40 | }, 'valueCodeableConcept'), 41 | }; 42 | } 43 | 44 | function procedureBodySiteTemplate({ bodySite, laterality }) { 45 | if (!bodySite) return null; 46 | 47 | const bodySiteObj = bodySiteTemplate({ bodySite, laterality }); 48 | return { 49 | bodySite: [ 50 | bodySiteObj.bodySite, 51 | ], 52 | }; 53 | } 54 | 55 | function procedureTemplate({ 56 | id, subjectId, status, code, system, display, reasonCode, reasonCodeSystem, reasonDisplayName, conditionId, bodySite, laterality, effectiveDateTime, treatmentIntent, 57 | }) { 58 | if (!(id && subjectId && status && code && system && effectiveDateTime)) { 59 | throw Error('Trying to render a ProcedureTemplate, but a required argument is missing; ensure that id, subjectId, status, code, system, and effectiveDateTime are all present'); 60 | } 61 | 62 | return { 63 | resourceType: 'Procedure', 64 | id, 65 | status, 66 | code: { 67 | coding: [ 68 | coding({ code, system, display }), 69 | ], 70 | }, 71 | subject: reference({ id: subjectId, resourceType: 'Patient' }), 72 | performedDateTime: effectiveDateTime, 73 | ...ifSomeArgsObj(reasonTemplate)({ reasonCode, reasonCodeSystem, reasonDisplayName }), 74 | ...(conditionId && reasonReference(conditionId)), 75 | ...extensionArr(ifAllArgsObj(treatmentIntentTemplate)({ treatmentIntent })), 76 | ...ifSomeArgsObj(procedureBodySiteTemplate)({ bodySite, laterality }), 77 | }; 78 | } 79 | 80 | module.exports = { 81 | procedureTemplate, 82 | }; 83 | -------------------------------------------------------------------------------- /src/extractors/CSVEncounterExtractor.js: -------------------------------------------------------------------------------- 1 | const { BaseCSVExtractor } = require('./BaseCSVExtractor'); 2 | const { generateMcodeResources } = require('../templates'); 3 | const { getPatientFromContext } = require('../helpers/contextUtils'); 4 | const { getEmptyBundle } = require('../helpers/fhirUtils'); 5 | const { formatDateTime } = require('../helpers/dateUtils'); 6 | const { CSVEncounterSchema } = require('../helpers/schemas/csv'); 7 | const logger = require('../helpers/logger'); 8 | 9 | // Formats data to be passed into template-friendly format 10 | function formatData(encounterData, patientId) { 11 | logger.debug('Reformatting encounter data from CSV into template format'); 12 | return encounterData.map((data) => { 13 | const { 14 | encounterid: encounterId, 15 | status, 16 | classcode: classCode, 17 | classsystem: classSystem, 18 | typecode: typeCode, 19 | typesystem: typeSystem, 20 | startdate: startDate, 21 | enddate: endDate, 22 | } = data; 23 | 24 | if (!(encounterId && status && classCode && classSystem)) { 25 | throw Error('Missing required field for Encounter CSV Extraction: encounterId, status, classCode or classSystem'); 26 | } 27 | 28 | return { 29 | ...(encounterId && { id: encounterId }), 30 | subject: { 31 | id: patientId, 32 | }, 33 | status, 34 | classCode, 35 | classSystem, 36 | typeCode, 37 | typeSystem, 38 | startDate: !startDate ? null : formatDateTime(startDate), 39 | endDate: !endDate ? null : formatDateTime(endDate), 40 | }; 41 | }); 42 | } 43 | 44 | class CSVEncounterExtractor extends BaseCSVExtractor { 45 | constructor({ 46 | filePath, url, fileName, dataDirectory, csvParse, 47 | }) { 48 | super({ filePath, url, fileName, dataDirectory, csvSchema: CSVEncounterSchema, csvParse }); 49 | } 50 | 51 | async getEncounterData(mrn) { 52 | logger.debug('Getting Encounter Data'); 53 | return this.csvModule.get('mrn', mrn); 54 | } 55 | 56 | async get({ mrn, context }) { 57 | const encounterData = await this.getEncounterData(mrn); 58 | if (encounterData.length === 0) { 59 | logger.warn('No encounter data found for patient'); 60 | return getEmptyBundle(); 61 | } 62 | const patientId = getPatientFromContext(context).id; 63 | 64 | // Reformat data 65 | const formattedData = formatData(encounterData, patientId); 66 | 67 | // Fill templates 68 | return generateMcodeResources('Encounter', formattedData); 69 | } 70 | } 71 | 72 | module.exports = { 73 | CSVEncounterExtractor, 74 | }; 75 | --------------------------------------------------------------------------------