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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/cli/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const logger = require('../helpers/logger');
const { RunInstanceLogger } = require('./RunInstanceLogger');
const { sendEmailNotification, zipErrors } = require('./emailNotifications');
const { extractDataForPatients } = require('./mcodeExtraction');
const { maskMRN } = require('../helpers/patientUtils');

function getConfig(pathToConfig) {
// Checks pathToConfig points to valid JSON file
Expand Down Expand Up @@ -109,6 +110,17 @@ async function mcodeApp(Client, fromDate, toDate, pathToConfig, pathToRunLogs, d
}
}

// check if config specifies that MRN needs to be masked
// if it does need to be masked, mask all references to MRN outside of the patient resource
const patientConfig = config.extractors.find((e) => e.type === 'CSVPatientExtractor');
if (patientConfig && ('constructorArgs' in patientConfig && 'mask' in patientConfig.constructorArgs)) {
if (patientConfig.constructorArgs.mask.includes('mrn')) {
extractedData.forEach((bundle) => {
maskMRN(bundle);
});
}
}

// Finally, save the data to disk
const outputPath = './output';
if (!fs.existsSync(outputPath)) {
Expand Down
24 changes: 24 additions & 0 deletions src/helpers/patientUtils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-underscore-dangle */
const fhirpath = require('fhirpath');
const shajs = require('sha.js');
const { extensionArr, dataAbsentReasonExtension } = require('../templates/snippets/extension.js');

// Based on the OMB Ethnicity table found here:http://hl7.org/fhir/us/core/STU3.1/ValueSet-omb-ethnicity-category.html
Expand Down Expand Up @@ -147,10 +148,33 @@ function maskPatientData(bundle, mask) {
});
}

/**
* Mask all references to the MRN used as an id
* Currently, the MRN appears as an id in 'subject' and 'individual' objects in other resources
* and in the 'id' and 'fullUrl' fields of the Patient resource.
* Replaces the MRN with a hash of the MRN
* @param {Object} bundle a FHIR bundle with a Patient resource and other resources
*/
function maskMRN(bundle) {
const patient = fhirpath.evaluate(bundle, 'Bundle.entry.where(resource.resourceType=\'Patient\')')[0];
if (patient === undefined) throw Error('No Patient resource in bundle. Could not mask MRN.');
const mrn = patient.resource.id;
const masked = shajs('sha256').update(mrn).digest('hex');
patient.fullUrl = `urn:uuid:${masked}`;
patient.resource.id = masked;
const subjects = fhirpath.evaluate(bundle, `Bundle.entry.resource.subject.where(reference='urn:uuid:${mrn}')`);
const individuals = fhirpath.evaluate(bundle, `Bundle.entry.resource.individual.where(reference='urn:uuid:${mrn}')`);
const mrnOccurrences = subjects.concat(individuals);
for (let i = 0; i < mrnOccurrences.length; i += 1) {
mrnOccurrences[i].reference = `urn:uuid:${masked}`;
}
}

module.exports = {
getEthnicityDisplay,
getRaceCodesystem,
getRaceDisplay,
getPatientName,
maskPatientData,
maskMRN,
};
30 changes: 30 additions & 0 deletions test/helpers/fixtures/bundle-with-mrn-id.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"fullUrl": "urn:uuid:123",
"resource": {
"resourceType": "Patient",
"id": "123"

}
},
{
"resource": {
"subject": {
"reference": "urn:uuid:123",
"type": "Patient"
}
}
},
{
"resource": {
"individual": {
"reference": "urn:uuid:123",
"type": "Patient"
}
}
}
]
}
18 changes: 17 additions & 1 deletion test/helpers/patientUtils.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const _ = require('lodash');
const shajs = require('sha.js');
const {
getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData,
getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData, maskMRN,
} = require('../../src/helpers/patientUtils');
const examplePatient = require('../extractors/fixtures/csv-patient-bundle.json');
const exampleMaskedPatient = require('./fixtures/masked-patient-bundle.json');
const exampleBundleWithMRN = require('./fixtures/bundle-with-mrn-id.json');

describe('PatientUtils', () => {
describe('getEthnicityDisplay', () => {
Expand Down Expand Up @@ -101,4 +103,18 @@ describe('PatientUtils', () => {
expect(() => maskPatientData(bundle, ['this is an invalid field', 'mrn'])).toThrowError();
});
});
describe('maskMRN', () => {
test('all occurances of the MRN as an id should be masked by a hashed version', () => {
const bundle = _.cloneDeep(exampleBundleWithMRN);
const hashedMRN = shajs('sha256').update(bundle.entry[0].resource.id).digest('hex');
maskMRN(bundle);
expect(bundle.entry[0].resource.id).toEqual(hashedMRN);
expect(bundle.entry[0].fullUrl).toEqual(`urn:uuid:${hashedMRN}`);
expect(bundle.entry[1].resource.subject.reference).toEqual(`urn:uuid:${hashedMRN}`);
expect(bundle.entry[2].resource.individual.reference).toEqual(`urn:uuid:${hashedMRN}`);
});
test('should throw error when there is no Patient resource in bundle', () => {
expect(() => maskMRN({})).toThrowError();
});
});
});