From aede88d2a508752939d3add86f9236cd5b3efe6c Mon Sep 17 00:00:00 2001 From: Dylan Mendelowitz Date: Mon, 25 Oct 2021 15:23:55 -0400 Subject: [PATCH 1/3] Creating new mask field 'genderAndSex' which masks all related fields --- src/helpers/patientUtils.js | 58 +++++++++++++++++++------------ test/helpers/patientUtils.test.js | 6 ++-- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/src/helpers/patientUtils.js b/src/helpers/patientUtils.js index 7224c5c2..0631347e 100644 --- a/src/helpers/patientUtils.js +++ b/src/helpers/patientUtils.js @@ -78,7 +78,7 @@ function getPatientName(name) { * dataAbsentReason extension with value 'masked' * @param {Object} bundle a FHIR bundle with a Patient resource * @param {Array} mask an array of fields to mask. Values can be: - * 'gender','mrn','name','address','birthDate','language','ethnicity','birthsex', + * 'genderAndSex','mrn','name','address','birthDate','language','ethnicity', * 'race', 'telecom', 'multipleBirth', 'photo', 'contact', 'generalPractitioner', * 'managingOrganization', and 'link' * @param {Boolean} maskAll indicates that all supported fields should be masked, defaults to false @@ -91,14 +91,13 @@ function maskPatientData(bundle, mask, maskAll = false) { )[0]; const validFields = [ - 'gender', + 'genderAndSex', 'mrn', 'name', 'address', 'birthDate', 'language', 'ethnicity', - 'birthsex', 'race', 'telecom', 'multipleBirth', @@ -117,13 +116,40 @@ function maskPatientData(bundle, mask, maskAll = false) { throw Error(`'${field}' is not a field that can be masked. Patient will only be extracted if all mask fields are valid. Valid fields include: Valid fields include: ${validFields.join(', ')}`); } // must check if the field exists in the patient resource, so we don't add unnecessary dataAbsent extensions - if (field === 'gender' && 'gender' in patient) { - delete patient.gender; - // an underscore is added when a primitive type is being replaced by an object (extension) - patient._gender = masked; - } else if (field === 'gender' && '_gender' in patient) { - delete patient._gender; // gender may have a dataAbsentReason on it for 'unknown' data, but we'll still want to mask it - patient._gender = masked; + if (field === 'genderAndSex') { + if ('gender' in patient) { + delete patient.gender; + // an underscore is added when a primitive type is being replaced by an object (extension) + patient._gender = masked; + } else if ('_gender' in patient) { + delete patient._gender; // gender may have a dataAbsentReason on it for 'unknown' data, but we'll still want to mask it + patient._gender = masked; + } + // fields that are extensions need to be differentiated by URL using fhirpath + const birthsex = fhirpath.evaluate( + patient, + 'Patient.extension.where(url=\'http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex\')', + ); + // fhirpath.evaluate will return [] if there is no extension with the given URL + // so checking if the result is an array with anything in it checks if the field exists to be masked + if (birthsex.length > 0) { + delete birthsex[0].valueCode; + birthsex[0]._valueCode = masked; + } + const legalsex = fhirpath.evaluate( + patient, + 'Patient.extension.where(url=\'http://open.epic.com/FHIR/StructureDefinition/extension/legal-sex\')', + ); + if (legalsex.length > 0) { + legalsex[0].valueCodeableConcept = masked; + } + const clinicaluse = fhirpath.evaluate( + patient, + 'Patient.extension.where(url=\'http://open.epic.com/FHIR/StructureDefinition/extension/sex-for-clinical-use\')', + ); + if (clinicaluse.length > 0) { + clinicaluse[0].valueCodeableConcept = 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 hash = crypto.createHash('sha256'); @@ -147,18 +173,6 @@ function maskPatientData(bundle, mask, maskAll = false) { if ('communication' in patient && 'language' in patient.communication[0]) { patient.communication[0].language = masked; } - } else if (field === 'birthsex') { - // fields that are extensions need to be differentiated by URL using fhirpath - const birthsex = fhirpath.evaluate( - patient, - 'Patient.extension.where(url=\'http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex\')', - ); - // fhirpath.evaluate will return [] if there is no extension with the given URL - // so checking if the result is an array with anything in it checks if the field exists to be masked - if (birthsex.length > 0) { - delete birthsex[0].valueCode; - birthsex[0]._valueCode = masked; - } } else if (field === 'race') { const race = fhirpath.evaluate( patient, diff --git a/test/helpers/patientUtils.test.js b/test/helpers/patientUtils.test.js index ecd5174e..5dc07257 100644 --- a/test/helpers/patientUtils.test.js +++ b/test/helpers/patientUtils.test.js @@ -93,14 +93,13 @@ describe('PatientUtils', () => { test('bundle should be modified to have dataAbsentReason for all fields specified in mask', () => { const bundle = _.cloneDeep(examplePatient); maskPatientData(bundle, [ - 'gender', + 'genderAndSex', 'mrn', 'name', 'address', 'birthDate', 'language', 'ethnicity', - 'birthsex', 'race', 'telecom', 'multipleBirth', @@ -132,14 +131,13 @@ describe('PatientUtils', () => { ], }; maskPatientData(bundle, [ - 'gender', + 'genderAndSex', 'mrn', 'name', 'address', 'birthDate', 'language', 'ethnicity', - 'birthsex', 'race', 'telecom', 'multipleBirth', From e11d065256126f6ce90541ca28ec3167ac401bf3 Mon Sep 17 00:00:00 2001 From: Dylan Mendelowitz Date: Tue, 26 Oct 2021 14:54:38 -0400 Subject: [PATCH 2/3] README updates --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50b640dd..57b398be 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ npm start -- --entries-filter --from-date YYYY-MM-DD --to-date YYY-MM-DD --run-l ### Masking Patient Data Patient data can be masked within the extracted `Patient` resource. When masked, the value of the field will be replaced with a [Data Absent Reason extension](https://www.hl7.org/fhir/extension-data-absent-reason.html) with the code `masked`. -Patient properties that can be masked are: `gender`, `mrn`, `name`, `address`, `birthDate`, `language`, `ethnicity`, `birthsex`, `race`, `telecom`, `multipleBirth`, `photo`, `contact`, `generalPractitioner`, `managingOrganization`, and `link`. +Patient properties that can be masked are: `genderAndSex`, `mrn`, `name`, `address`, `birthDate`, `language`, `ethnicity`, `race`, `telecom`, `multipleBirth`, `photo`, `contact`, `generalPractitioner`, `managingOrganization`, and `link`. To mask a property, provide an array of the properties to mask in the `constructorArgs` of the Patient extractor. For example, the following configuration can be used to mask `address` and `birthDate`: ```bash From 6f3ccaff69d01bbcfea9ecefd59a7d3a85cd2bef Mon Sep 17 00:00:00 2001 From: Dylan Mendelowitz Date: Tue, 26 Oct 2021 15:39:17 -0400 Subject: [PATCH 3/3] updating patient fixtures to have new genderAndSex maskable fields --- .../fixtures/extended-patient-bundle.json | 22 +++++++++++++++++++ .../fixtures/masked-patient-bundle.json | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/test/extractors/fixtures/extended-patient-bundle.json b/test/extractors/fixtures/extended-patient-bundle.json index 872ee155..6e8ec70c 100644 --- a/test/extractors/fixtures/extended-patient-bundle.json +++ b/test/extractors/fixtures/extended-patient-bundle.json @@ -154,6 +154,28 @@ ], "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" }, + { + "valueCodeableConcept": { + "coding": [ + { + "system": "urn:oid:1.2.840.114350.1.13.0.1.7.10.698084.130.657370.19999000", + "code": "female" + } + ] + }, + "url": "http://open.epic.com/FHIR/StructureDefinition/extension/legal-sex" + }, + { + "valueCodeableConcept": { + "coding": [ + { + "system": "urn:oid:1.2.840.114350.1.13.0.1.7.10.698084.130.657370.19999000", + "code": "female" + } + ] + }, + "url": "http://open.epic.com/FHIR/StructureDefinition/extension/sex-for-clinical-use" + }, { "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", "valueCode": "male" diff --git a/test/helpers/fixtures/masked-patient-bundle.json b/test/helpers/fixtures/masked-patient-bundle.json index beb49fe1..3a37ae8a 100644 --- a/test/helpers/fixtures/masked-patient-bundle.json +++ b/test/helpers/fixtures/masked-patient-bundle.json @@ -104,6 +104,28 @@ ], "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity" }, + { + "valueCodeableConcept": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "masked" + } + ] + }, + "url": "http://open.epic.com/FHIR/StructureDefinition/extension/legal-sex" + }, + { + "valueCodeableConcept": { + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason", + "valueCode": "masked" + } + ] + }, + "url": "http://open.epic.com/FHIR/StructureDefinition/extension/sex-for-clinical-use" + }, { "url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", "_valueCode": {