Skip to content

Commit cb629d2

Browse files
authored
Merge pull request #161 from mcode/mask-birthsex-variants
Creating new mask field 'genderAndSex' which masks all related fields
2 parents a60a067 + 6f3ccaf commit cb629d2

File tree

5 files changed

+83
-27
lines changed

5 files changed

+83
-27
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ npm start -- --entries-filter --from-date YYYY-MM-DD --to-date YYY-MM-DD --run-l
135135
### Masking Patient Data
136136

137137
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`.
138-
Patient properties that can be masked are: `gender`, `mrn`, `name`, `address`, `birthDate`, `language`, `ethnicity`, `birthsex`, `race`, `telecom`, `multipleBirth`, `photo`, `contact`, `generalPractitioner`, `managingOrganization`, and `link`.
138+
Patient properties that can be masked are: `genderAndSex`, `mrn`, `name`, `address`, `birthDate`, `language`, `ethnicity`, `race`, `telecom`, `multipleBirth`, `photo`, `contact`, `generalPractitioner`, `managingOrganization`, and `link`.
139139
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`:
140140

141141
```bash

src/helpers/patientUtils.js

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function getPatientName(name) {
7878
* dataAbsentReason extension with value 'masked'
7979
* @param {Object} bundle a FHIR bundle with a Patient resource
8080
* @param {Array} mask an array of fields to mask. Values can be:
81-
* 'gender','mrn','name','address','birthDate','language','ethnicity','birthsex',
81+
* 'genderAndSex','mrn','name','address','birthDate','language','ethnicity',
8282
* 'race', 'telecom', 'multipleBirth', 'photo', 'contact', 'generalPractitioner',
8383
* 'managingOrganization', and 'link'
8484
* @param {Boolean} maskAll indicates that all supported fields should be masked, defaults to false
@@ -91,14 +91,13 @@ function maskPatientData(bundle, mask, maskAll = false) {
9191
)[0];
9292

9393
const validFields = [
94-
'gender',
94+
'genderAndSex',
9595
'mrn',
9696
'name',
9797
'address',
9898
'birthDate',
9999
'language',
100100
'ethnicity',
101-
'birthsex',
102101
'race',
103102
'telecom',
104103
'multipleBirth',
@@ -117,13 +116,40 @@ function maskPatientData(bundle, mask, maskAll = false) {
117116
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(', ')}`);
118117
}
119118
// must check if the field exists in the patient resource, so we don't add unnecessary dataAbsent extensions
120-
if (field === 'gender' && 'gender' in patient) {
121-
delete patient.gender;
122-
// an underscore is added when a primitive type is being replaced by an object (extension)
123-
patient._gender = masked;
124-
} else if (field === 'gender' && '_gender' in patient) {
125-
delete patient._gender; // gender may have a dataAbsentReason on it for 'unknown' data, but we'll still want to mask it
126-
patient._gender = masked;
119+
if (field === 'genderAndSex') {
120+
if ('gender' in patient) {
121+
delete patient.gender;
122+
// an underscore is added when a primitive type is being replaced by an object (extension)
123+
patient._gender = masked;
124+
} else if ('_gender' in patient) {
125+
delete patient._gender; // gender may have a dataAbsentReason on it for 'unknown' data, but we'll still want to mask it
126+
patient._gender = masked;
127+
}
128+
// fields that are extensions need to be differentiated by URL using fhirpath
129+
const birthsex = fhirpath.evaluate(
130+
patient,
131+
'Patient.extension.where(url=\'http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex\')',
132+
);
133+
// fhirpath.evaluate will return [] if there is no extension with the given URL
134+
// so checking if the result is an array with anything in it checks if the field exists to be masked
135+
if (birthsex.length > 0) {
136+
delete birthsex[0].valueCode;
137+
birthsex[0]._valueCode = masked;
138+
}
139+
const legalsex = fhirpath.evaluate(
140+
patient,
141+
'Patient.extension.where(url=\'http://open.epic.com/FHIR/StructureDefinition/extension/legal-sex\')',
142+
);
143+
if (legalsex.length > 0) {
144+
legalsex[0].valueCodeableConcept = masked;
145+
}
146+
const clinicaluse = fhirpath.evaluate(
147+
patient,
148+
'Patient.extension.where(url=\'http://open.epic.com/FHIR/StructureDefinition/extension/sex-for-clinical-use\')',
149+
);
150+
if (clinicaluse.length > 0) {
151+
clinicaluse[0].valueCodeableConcept = masked;
152+
}
127153
} else if (field === 'mrn' && 'identifier' in patient) {
128154
// id and fullURL still need valid values, so we use a hashed version of MRN instead of dataAbsentReason
129155
const hash = crypto.createHash('sha256');
@@ -147,18 +173,6 @@ function maskPatientData(bundle, mask, maskAll = false) {
147173
if ('communication' in patient && 'language' in patient.communication[0]) {
148174
patient.communication[0].language = masked;
149175
}
150-
} else if (field === 'birthsex') {
151-
// fields that are extensions need to be differentiated by URL using fhirpath
152-
const birthsex = fhirpath.evaluate(
153-
patient,
154-
'Patient.extension.where(url=\'http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex\')',
155-
);
156-
// fhirpath.evaluate will return [] if there is no extension with the given URL
157-
// so checking if the result is an array with anything in it checks if the field exists to be masked
158-
if (birthsex.length > 0) {
159-
delete birthsex[0].valueCode;
160-
birthsex[0]._valueCode = masked;
161-
}
162176
} else if (field === 'race') {
163177
const race = fhirpath.evaluate(
164178
patient,

test/extractors/fixtures/extended-patient-bundle.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,28 @@
154154
],
155155
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"
156156
},
157+
{
158+
"valueCodeableConcept": {
159+
"coding": [
160+
{
161+
"system": "urn:oid:1.2.840.114350.1.13.0.1.7.10.698084.130.657370.19999000",
162+
"code": "female"
163+
}
164+
]
165+
},
166+
"url": "http://open.epic.com/FHIR/StructureDefinition/extension/legal-sex"
167+
},
168+
{
169+
"valueCodeableConcept": {
170+
"coding": [
171+
{
172+
"system": "urn:oid:1.2.840.114350.1.13.0.1.7.10.698084.130.657370.19999000",
173+
"code": "female"
174+
}
175+
]
176+
},
177+
"url": "http://open.epic.com/FHIR/StructureDefinition/extension/sex-for-clinical-use"
178+
},
157179
{
158180
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
159181
"valueCode": "male"

test/helpers/fixtures/masked-patient-bundle.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,28 @@
104104
],
105105
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity"
106106
},
107+
{
108+
"valueCodeableConcept": {
109+
"extension": [
110+
{
111+
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
112+
"valueCode": "masked"
113+
}
114+
]
115+
},
116+
"url": "http://open.epic.com/FHIR/StructureDefinition/extension/legal-sex"
117+
},
118+
{
119+
"valueCodeableConcept": {
120+
"extension": [
121+
{
122+
"url": "http://hl7.org/fhir/StructureDefinition/data-absent-reason",
123+
"valueCode": "masked"
124+
}
125+
]
126+
},
127+
"url": "http://open.epic.com/FHIR/StructureDefinition/extension/sex-for-clinical-use"
128+
},
107129
{
108130
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
109131
"_valueCode": {

test/helpers/patientUtils.test.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,14 +93,13 @@ describe('PatientUtils', () => {
9393
test('bundle should be modified to have dataAbsentReason for all fields specified in mask', () => {
9494
const bundle = _.cloneDeep(examplePatient);
9595
maskPatientData(bundle, [
96-
'gender',
96+
'genderAndSex',
9797
'mrn',
9898
'name',
9999
'address',
100100
'birthDate',
101101
'language',
102102
'ethnicity',
103-
'birthsex',
104103
'race',
105104
'telecom',
106105
'multipleBirth',
@@ -132,14 +131,13 @@ describe('PatientUtils', () => {
132131
],
133132
};
134133
maskPatientData(bundle, [
135-
'gender',
134+
'genderAndSex',
136135
'mrn',
137136
'name',
138137
'address',
139138
'birthDate',
140139
'language',
141140
'ethnicity',
142-
'birthsex',
143141
'race',
144142
'telecom',
145143
'multipleBirth',

0 commit comments

Comments
 (0)