Skip to content

Commit 45af086

Browse files
Merge pull request #98 from mcode/mask-mrn-references
Mask MRN when used as the ID of a Patient resource
2 parents 6d081f0 + 71eb803 commit 45af086

File tree

4 files changed

+83
-1
lines changed

4 files changed

+83
-1
lines changed

src/cli/app.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const logger = require('../helpers/logger');
66
const { RunInstanceLogger } = require('./RunInstanceLogger');
77
const { sendEmailNotification, zipErrors } = require('./emailNotifications');
88
const { extractDataForPatients } = require('./mcodeExtraction');
9+
const { maskMRN } = require('../helpers/patientUtils');
910

1011
function getConfig(pathToConfig) {
1112
// Checks pathToConfig points to valid JSON file
@@ -109,6 +110,17 @@ async function mcodeApp(Client, fromDate, toDate, pathToConfig, pathToRunLogs, d
109110
}
110111
}
111112

113+
// check if config specifies that MRN needs to be masked
114+
// if it does need to be masked, mask all references to MRN outside of the patient resource
115+
const patientConfig = config.extractors.find((e) => e.type === 'CSVPatientExtractor');
116+
if (patientConfig && ('constructorArgs' in patientConfig && 'mask' in patientConfig.constructorArgs)) {
117+
if (patientConfig.constructorArgs.mask.includes('mrn')) {
118+
extractedData.forEach((bundle) => {
119+
maskMRN(bundle);
120+
});
121+
}
122+
}
123+
112124
// Finally, save the data to disk
113125
const outputPath = './output';
114126
if (!fs.existsSync(outputPath)) {

src/helpers/patientUtils.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable no-underscore-dangle */
22
const fhirpath = require('fhirpath');
3+
const shajs = require('sha.js');
34
const { extensionArr, dataAbsentReasonExtension } = require('../templates/snippets/extension.js');
45

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

151+
/**
152+
* Mask all references to the MRN used as an id
153+
* Currently, the MRN appears as an id in 'subject' and 'individual' objects in other resources
154+
* and in the 'id' and 'fullUrl' fields of the Patient resource.
155+
* Replaces the MRN with a hash of the MRN
156+
* @param {Object} bundle a FHIR bundle with a Patient resource and other resources
157+
*/
158+
function maskMRN(bundle) {
159+
const patient = fhirpath.evaluate(bundle, 'Bundle.entry.where(resource.resourceType=\'Patient\')')[0];
160+
if (patient === undefined) throw Error('No Patient resource in bundle. Could not mask MRN.');
161+
const mrn = patient.resource.id;
162+
const masked = shajs('sha256').update(mrn).digest('hex');
163+
patient.fullUrl = `urn:uuid:${masked}`;
164+
patient.resource.id = masked;
165+
const subjects = fhirpath.evaluate(bundle, `Bundle.entry.resource.subject.where(reference='urn:uuid:${mrn}')`);
166+
const individuals = fhirpath.evaluate(bundle, `Bundle.entry.resource.individual.where(reference='urn:uuid:${mrn}')`);
167+
const mrnOccurrences = subjects.concat(individuals);
168+
for (let i = 0; i < mrnOccurrences.length; i += 1) {
169+
mrnOccurrences[i].reference = `urn:uuid:${masked}`;
170+
}
171+
}
172+
150173
module.exports = {
151174
getEthnicityDisplay,
152175
getRaceCodesystem,
153176
getRaceDisplay,
154177
getPatientName,
155178
maskPatientData,
179+
maskMRN,
156180
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"resourceType": "Bundle",
3+
"type": "collection",
4+
"entry": [
5+
{
6+
"fullUrl": "urn:uuid:123",
7+
"resource": {
8+
"resourceType": "Patient",
9+
"id": "123"
10+
11+
}
12+
},
13+
{
14+
"resource": {
15+
"subject": {
16+
"reference": "urn:uuid:123",
17+
"type": "Patient"
18+
}
19+
}
20+
},
21+
{
22+
"resource": {
23+
"individual": {
24+
"reference": "urn:uuid:123",
25+
"type": "Patient"
26+
}
27+
}
28+
}
29+
]
30+
}

test/helpers/patientUtils.test.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
const _ = require('lodash');
2+
const shajs = require('sha.js');
23
const {
3-
getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData,
4+
getEthnicityDisplay, getRaceCodesystem, getRaceDisplay, getPatientName, maskPatientData, maskMRN,
45
} = require('../../src/helpers/patientUtils');
56
const examplePatient = require('../extractors/fixtures/csv-patient-bundle.json');
67
const exampleMaskedPatient = require('./fixtures/masked-patient-bundle.json');
8+
const exampleBundleWithMRN = require('./fixtures/bundle-with-mrn-id.json');
79

810
describe('PatientUtils', () => {
911
describe('getEthnicityDisplay', () => {
@@ -101,4 +103,18 @@ describe('PatientUtils', () => {
101103
expect(() => maskPatientData(bundle, ['this is an invalid field', 'mrn'])).toThrowError();
102104
});
103105
});
106+
describe('maskMRN', () => {
107+
test('all occurances of the MRN as an id should be masked by a hashed version', () => {
108+
const bundle = _.cloneDeep(exampleBundleWithMRN);
109+
const hashedMRN = shajs('sha256').update(bundle.entry[0].resource.id).digest('hex');
110+
maskMRN(bundle);
111+
expect(bundle.entry[0].resource.id).toEqual(hashedMRN);
112+
expect(bundle.entry[0].fullUrl).toEqual(`urn:uuid:${hashedMRN}`);
113+
expect(bundle.entry[1].resource.subject.reference).toEqual(`urn:uuid:${hashedMRN}`);
114+
expect(bundle.entry[2].resource.individual.reference).toEqual(`urn:uuid:${hashedMRN}`);
115+
});
116+
test('should throw error when there is no Patient resource in bundle', () => {
117+
expect(() => maskMRN({})).toThrowError();
118+
});
119+
});
104120
});

0 commit comments

Comments
 (0)