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
9 changes: 8 additions & 1 deletion config/csv.config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,13 @@
"constructorArgs": {
"filePath": "./test/sample-client-data/adverse-event-information.csv"
}
},
{
"label": "ctcAdverseEvent",
"type": "CSVCTCAdverseEventExtractor",
"constructorArgs": {
"filePath": "./test/sample-client-data/ctc-adverse-event-information.csv"
}
}
]
}
}
Binary file modified docs/CSV_Templates.xlsx
Binary file not shown.
8 changes: 4 additions & 4 deletions docs/ctc-adverse-event.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate
mrn-full-example,example-id-1,event-code,code-system,code-display,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code,code-system,category-dislpay,mild,actual,id,1994-12-09,1994-12-09
mrn-two-category-example,example-id-2,event-code,code-system,code-display,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code|category-code,code-system|code-system,category-display|category-display,mild,actual,id,1994-12-09,1994-12-09
mrn-minimal-example,,code-from-default-system,,,,,,,,,,,,,,1994-12-09,
mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate,grade
mrn-full-example,example-id-1,event-code,code-system,code-display,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code,code-system,category-dislpay,mild,actual,id,1994-12-09,1994-12-09,1,
mrn-two-category-example,example-id-2,event-code,code-system,code-display,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code|category-code,code-system|code-system,category-display|category-display,mild,actual,id,1994-12-09,1994-12-09,3
mrn-minimal-example,,code-from-default-system,,,,,,,,,,,,,,1994-12-09,,1
7 changes: 5 additions & 2 deletions src/extractors/CSVCTCAdverseEventExtractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const { generateMcodeResources } = require('../templates');
const { getEmptyBundle } = require('../helpers/fhirUtils');
const { getPatientFromContext } = require('../helpers/contextUtils');
const { formatDateTime } = require('../helpers/dateUtils');
const { ctcAEGradeCodeToTextLookup } = require('../helpers/lookups/ctcAdverseEventLookup');
const logger = require('../helpers/logger');

// Formats data to be passed into template-friendly format
Expand All @@ -27,10 +28,11 @@ function formatData(adverseEventData, patientId) {
studyid: studyId,
effectivedate: effectiveDate,
recordeddate: recordedDate,
grade,
} = data;

if (!(adverseEventCode && effectiveDate)) {
throw new Error('The adverse event is missing an expected attribute. Adverse event code and effective date are all required.');
if (!(adverseEventCode && effectiveDate && grade)) {
throw new Error('The adverse event is missing an expected attribute. Adverse event code, effective date, and grade are all required.');
}

const categoryCodes = category.split('|');
Expand Down Expand Up @@ -64,6 +66,7 @@ function formatData(adverseEventData, patientId) {
studyId,
effectiveDateTime: formatDateTime(effectiveDate),
recordedDateTime: !recordedDate ? null : formatDateTime(recordedDate),
grade: { code: grade, display: ctcAEGradeCodeToTextLookup[grade] },
};
});
}
Expand Down
17 changes: 17 additions & 0 deletions src/helpers/lookups/ctcAdverseEventLookup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { createInvertedLookup, createLowercaseLookup } = require('../lookupUtils');

const ctcAEGradeTextToCodeLookup = {
'Absent Adverse Event': '0',
'Mild Adverse Event': '1',
'Moderate Adverse Event': '2',
'Severe Adverse Event': '3',
'Life Threatening or Disabling Adverse Event': '4',
'Death Related to Adverse Event': '5',
};

const ctcAEGradeCodeToTextLookup = createInvertedLookup(ctcAEGradeTextToCodeLookup);

module.exports = {
ctcAEGradeCodeToTextLookup: createLowercaseLookup(ctcAEGradeCodeToTextLookup),
ctcAEGradeTextToCodeLookup: createLowercaseLookup(ctcAEGradeTextToCodeLookup),
};
20 changes: 16 additions & 4 deletions src/templates/CTCAdverseEventTemplate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { coding, reference } = require('./snippets');
const { coding, reference, extensionArr } = require('./snippets');
const { ifAllArgsObj, ifSomeArgsObj, ifAllArgs, ifSomeArgsArr } = require('../helpers/templateUtils');

function eventTemplate(eventCoding) {
Expand Down Expand Up @@ -72,17 +72,29 @@ function recordedDateTemplate(recordedDateTime) {
};
}

function gradeTemplate(grade) {
return {
url: 'http://hl7.org/fhir/us/ctcae/StructureDefinition/ctcae-grade',
valueCodeableConcept: {
coding: [
coding({ ...grade, system: 'http://hl7.org/fhir/us/ctcae/CodeSystem/ctcae-grade-code-system' }),
],
},
};
}

function CTCAdverseEventTemplate({
id, subjectId, code, system, display, suspectedCauseId, suspectedCauseType, seriousnessCode, seriousnessCodeSystem, seriousnessDisplayText, category,
severity, actuality, studyId, effectiveDateTime, recordedDateTime,
severity, actuality, studyId, effectiveDateTime, recordedDateTime, grade,
}) {
if (!(subjectId && code && system && effectiveDateTime && actuality)) {
throw Error('Trying to render an AdverseEventTemplate, but a required argument is messing; ensure that subjectId, code, system, actuality, and effectiveDateTime are all present');
if (!(subjectId && code && system && effectiveDateTime && actuality && grade)) {
throw Error('Trying to render an AdverseEventTemplate, but a required argument is messing; ensure that subjectId, code, system, actuality, grade, and effectiveDateTime are all present');
}

return {
resourceType: 'AdverseEvent',
id,
...extensionArr(gradeTemplate(grade)),
subject: reference({ id: subjectId, resourceType: 'Patient' }),
...ifSomeArgsObj(eventTemplate)({ code, system, display }),
...ifAllArgsObj(suspectedCauseTemplate)({ suspectedCauseId, suspectedCauseType }),
Expand Down
101 changes: 101 additions & 0 deletions test/extractors/CSVCTCAdverseEventextractor.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const path = require('path');
const rewire = require('rewire');
const _ = require('lodash');
const { CSVCTCAdverseEventExtractor } = require('../../src/extractors');
const exampleCTCCSVAdverseEventModuleResponse = require('./fixtures/csv-ctc-adverse-event-module-response.json');
const exampleCTCCSVAdverseEventBundle = require('./fixtures/csv-ctc-adverse-event-bundle.json');
const { getPatientFromContext } = require('../../src/helpers/contextUtils');
const MOCK_CONTEXT = require('./fixtures/context-with-patient.json');

// Constants for tests
const MOCK_PATIENT_MRN = 'mrn-1'; // linked to values in example-module-response and context-with-patient above
const MOCK_CSV_PATH = path.join(__dirname, 'fixtures/example.csv'); // need a valid path/csv here to avoid parse error

// Instantiate module with parameters
const csvCTCAdverseEventExtractor = new CSVCTCAdverseEventExtractor({
filePath: MOCK_CSV_PATH,
});

// Destructure all modules
const { csvModule } = csvCTCAdverseEventExtractor;

// Spy on csvModule
const csvModuleSpy = jest.spyOn(csvModule, 'get');

// Creating an example bundle with two medication statements
const exampleEntry = exampleCTCCSVAdverseEventModuleResponse[0];
const expandedExampleBundle = _.cloneDeep(exampleCTCCSVAdverseEventBundle);
expandedExampleBundle.entry.push(exampleCTCCSVAdverseEventBundle.entry[0]);

// Rewired extractor for helper tests
const CSVCTCAdverseEventExtractorRewired = rewire('../../src/extractors/CSVCTCAdverseEventExtractor.js');

describe('CSVCTCAdverseEventExtractor', () => {
describe('formatData', () => {
const formatData = CSVCTCAdverseEventExtractorRewired.__get__('formatData');
test('should join data appropriately and throw errors when missing required properties', () => {
const expectedErrorString = 'The adverse event is missing an expected attribute. Adverse event code, effective date, and grade are all required.';
const expectedCategoryErrorString = 'A category attribute on the adverse event is missing a corresponding categoryCodeSystem or categoryDisplayText value.';
const localData = _.cloneDeep(exampleCTCCSVAdverseEventModuleResponse);
const patientId = getPatientFromContext(MOCK_CONTEXT).id;

// Test that valid maximal data works fine
expect(formatData(exampleCTCCSVAdverseEventModuleResponse, patientId)).toEqual(expect.anything());

// Test that deleting an optional value works fine
delete localData[0].actuality;
expect(formatData(exampleCTCCSVAdverseEventModuleResponse, patientId)).toEqual(expect.anything());

// Test that adding another category but not adding a corresponding categoryCodeSystem throws an error
localData[0].category = 'product-use-error|product-problem';
expect(() => formatData(localData, patientId)).toThrow(new Error(expectedCategoryErrorString));

// Test that adding another category but adding a corresponding categoryCodeSystem and categoryDisplayText works fine
localData[0].categorycodesystem = 'http://terminology.hl7.org/CodeSystem/adverse-event-category|http://snomed.info/sct';
localData[0].categorydisplaytext = 'Product Use Error|Product Problem';
expect(formatData(localData, patientId)).toEqual(expect.anything());

// Test that adding another category but including syntax for default categoryCodeSystem and categoryDisplayText values works fine
localData[0].categorycodesystem = 'http://terminology.hl7.org/CodeSystem/adverse-event-category|';
localData[0].categorydisplaytext = 'Product Use Error|';
expect(formatData(localData, patientId)).toEqual(expect.anything());

// Test that deleting a mandatory value throws an error
delete localData[0].grade;
expect(() => formatData(localData, patientId)).toThrow(new Error(expectedErrorString));
});
});

describe('get', () => {
test('should return bundle with a CTCAdverseEvent resource', async () => {
csvModuleSpy.mockReturnValue(exampleCTCCSVAdverseEventModuleResponse);
const data = await csvCTCAdverseEventExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT });
expect(data.resourceType).toEqual('Bundle');
expect(data.type).toEqual('collection');
expect(data.entry).toBeDefined();
expect(data.entry.length).toEqual(1);
expect(data.entry).toEqual(exampleCTCCSVAdverseEventBundle.entry);
});

test('should return empty bundle when no data available from module', async () => {
csvModuleSpy.mockReturnValue([]);
const data = await csvCTCAdverseEventExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT });
expect(data.resourceType).toEqual('Bundle');
expect(data.type).toEqual('collection');
expect(data.entry).toBeDefined();
expect(data.entry.length).toEqual(0);
});

test('get() should return an array of 2 when two adverse event resources are tied to a single patient', async () => {
exampleCTCCSVAdverseEventModuleResponse.push(exampleEntry);
csvModuleSpy.mockReturnValue(exampleCTCCSVAdverseEventModuleResponse);
const data = await csvCTCAdverseEventExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT });

expect(data.resourceType).toEqual('Bundle');
expect(data.type).toEqual('collection');
expect(data.entry).toBeDefined();
expect(data.entry.length).toEqual(2);
expect(data).toEqual(expandedExampleBundle);
});
});
});
85 changes: 85 additions & 0 deletions test/extractors/fixtures/csv-ctc-adverse-event-bundle.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"resourceType": "Bundle",
"type": "collection",
"entry": [
{
"fullUrl": "urn:uuid:adverseEventId-1",
"resource": {
"resourceType": "AdverseEvent",
"id": "adverseEventId-1",
"extension" : [
{
"url" : "http://hl7.org/fhir/us/ctcae/StructureDefinition/ctcae-grade",
"valueCodeableConcept" : {
"coding" : [
{
"system" : "http://hl7.org/fhir/us/ctcae/CodeSystem/ctcae-grade-code-system",
"code" : "1",
"display" : "Mild Adverse Event"
}
]
}
}
],
"subject": {
"reference": "urn:uuid:mrn-1",
"type": "Patient"
},
"event": {
"coding": [
{
"system": "code-system",
"code": "109006",
"display": "Anxiety disorder of childhood OR adolescence"
}
]
},
"suspectEntity": [
{
"instance": {
"reference": "urn:uuid:procedure-id",
"type": "Procedure"
}
}
],
"seriousness": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/adverse-event-seriousness",
"code": "serious",
"display": "Serious"
}
]
},
"category": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/adverse-event-category",
"code": "product-use-error",
"display": "Product Use Error"
}
]
}
],
"severity": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/adverse-event-severity",
"code": "severe"
}
]
},
"actuality": "actual",
"study": [
{
"reference": "urn:uuid:researchId-1",
"type": "ResearchStudy"
}
],
"date": "1994-12-09",
"recordedDate": "1994-12-09"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"mrn": "mrn-1",
"adverseeventid": "adverseEventId-1",
"adverseeventcode": "109006",
"adverseeventcodesystem": "code-system",
"adverseeventdisplaytext": "Anxiety disorder of childhood OR adolescence",
"suspectedcauseid": "procedure-id",
"suspectedcausetype": "Procedure",
"seriousness": "serious",
"seriousnesscodesystem": "http://terminology.hl7.org/CodeSystem/adverse-event-seriousness",
"seriousnessdisplaytext": "Serious",
"category": "product-use-error",
"categorycodesystem": "http://terminology.hl7.org/CodeSystem/adverse-event-category",
"categorydisplaytext": "Product Use Error",
"severity": "severe",
"actuality": "actual",
"studyid": "researchId-1",
"effectivedate": "12-09-1994",
"recordeddate": "12-09-1994",
"grade": "1"
}
]
8 changes: 4 additions & 4 deletions test/sample-client-data/ctc-adverse-event-information.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate
123,adverseEventId-1,109006,code-system,Anxiety disorder of childhood OR adolescence,procedure-id,Procedure,serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Serious,product-use-error|product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|http://snomed.info/sct|http://terminology.hl7.org/CodeSystem/adverse-event-category,Product Use Error|Product Quality|Wrong Rate,severe,actual,researchId-1,12-09-1994,12-09-1994
456,adverseEventId-2,134006,http://snomed.info/sct,Decreased hair growth,medicationId-1,Medication,non-serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Non-serious,product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|,Product Quality|,mild,potential,researchId-2,12-10-1995,12-10-1995
789,adverseEventId-3,150003,,,,,,,,product-use-error,,,,,,12-09-1994,
mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate,grade
123,adverseEventId-1,109006,code-system,Anxiety disorder of childhood OR adolescence,procedure-id,Procedure,serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Serious,product-use-error|product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|http://snomed.info/sct|http://terminology.hl7.org/CodeSystem/adverse-event-category,Product Use Error|Product Quality|Wrong Rate,severe,actual,researchId-1,12-09-1994,12-09-1994,1
456,adverseEventId-2,134006,http://snomed.info/sct,Decreased hair growth,medicationId-1,Medication,non-serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Non-serious,product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|,Product Quality|,mild,potential,researchId-2,12-10-1995,12-10-1995,2
789,adverseEventId-3,150003,,,,,,,,product-use-error,,,,,,12-09-1994,,3
Loading