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
18 changes: 0 additions & 18 deletions src/application/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ const logger = require('../helpers/logger');
const { RunInstanceLogger } = require('./tools/RunInstanceLogger');
const { sendEmailNotification, zipErrors } = require('./tools/emailNotifications');
const { extractDataForPatients } = require('./tools/mcodeExtraction');
const { maskMRN } = require('../helpers/patientUtils');
const { parsePatientIds } = require('../helpers/appUtils');

function getConfig(pathToConfig) {
Expand Down Expand Up @@ -78,23 +77,6 @@ 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, i) => {
// NOTE: This may fail to mask MRN-related properties on non-patient resources
// Need to investigate further.
try {
maskMRN(bundle);
} catch (e) {
logger.error(`Bundle ${i + 1}: ${e.message}`);
}
});
}
}

return extractedData;
}

Expand Down
31 changes: 8 additions & 23 deletions src/helpers/patientUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ function maskPatientData(bundle, mask) {
delete patient._gender; // gender may have a dataAbsentReason on it for 'unknown' data, but we'll still want to mask it
patient._gender = masked;
} else if (field === 'mrn' && 'identifier' in patient) {
// id and fullURL still need valid values, so we use a hashed version of MRN instead of dataAbsentReason
const maskedMRN = shajs('sha256').update(patient.id).digest('hex');
patient.id = maskedMRN;
const patientEntry = fhirpath.evaluate(
bundle,
'Bundle.entry.where(resource.resourceType=\'Patient\')',
)[0];
patientEntry.fullUrl = `urn:uuid:${maskedMRN}`;
patient.identifier = [masked];
} else if (field === 'name' && 'name' in patient) {
patient.name = [masked];
Expand Down Expand Up @@ -151,33 +159,10 @@ 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: 0 additions & 30 deletions test/helpers/fixtures/bundle-with-mrn-id.json

This file was deleted.

4 changes: 2 additions & 2 deletions test/helpers/fixtures/masked-patient-bundle.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"type": "collection",
"entry": [
{
"fullUrl": "urn:uuid:119147111821125",
"fullUrl": "urn:uuid:be7e757c18743bf94cee7764b960eb69867b430232f759e706d080604fe416ad",
"resource": {
"resourceType": "Patient",
"id": "119147111821125",
"id": "be7e757c18743bf94cee7764b960eb69867b430232f759e706d080604fe416ad",
"identifier": [
{
"extension": [
Expand Down
18 changes: 1 addition & 17 deletions test/helpers/patientUtils.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
const _ = require('lodash');
const shajs = require('sha.js');
const {
getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData, maskMRN,
getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData,
} = 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 @@ -119,18 +117,4 @@ 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();
});
});
});