Skip to content

Commit 615832c

Browse files
authored
Merge pull request #55 from mcode/staging-extractor
CSV Staging Extractor
2 parents 7fc4fbb + 7543251 commit 615832c

22 files changed

+1212
-16
lines changed

docs/staging.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
mrn,conditionId,stageGroup,t,n,m,type,effectiveDate
2+
mrn-1,example-condition-id,3C,cT3,cN3,cM0,Clinical,2020-01-01

src/extractors/CSVStagingExtractor.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
const path = require('path');
2+
const { Extractor } = require('./Extractor');
3+
const { CSVModule } = require('../modules');
4+
const { firstEntryInBundle } = require('../helpers/fhirUtils');
5+
const { generateMcodeResources } = require('../templates');
6+
const logger = require('../helpers/logger');
7+
const { formatDateTime } = require('../helpers/dateUtils');
8+
9+
function formatTNMCategoryData(stagingData) {
10+
logger.debug('Reformatting TNM Category data into template format');
11+
const formattedData = [];
12+
const {
13+
mrn, conditionId, t, n, m, type, effectiveDate,
14+
} = stagingData;
15+
16+
if (!mrn || !conditionId || !effectiveDate) {
17+
throw new Error('Staging data is missing an expected property: mrn, conditionId, effectiveDate are required.');
18+
}
19+
20+
// data needed for each TNM category
21+
const necessaryData = {
22+
conditionId,
23+
effectiveDateTime: formatDateTime(effectiveDate),
24+
stageType: type,
25+
subjectId: mrn,
26+
};
27+
28+
if (t) formattedData.push({ ...necessaryData, valueCode: t, categoryType: 'Tumor' });
29+
if (m) formattedData.push({ ...necessaryData, valueCode: m, categoryType: 'Metastases' });
30+
if (n) formattedData.push({ ...necessaryData, valueCode: n, categoryType: 'Nodes' });
31+
32+
return formattedData;
33+
}
34+
35+
function formatStagingData(stagingData, categoryIds) {
36+
const {
37+
mrn, conditionId, type, stageGroup, effectiveDate,
38+
} = stagingData;
39+
40+
return {
41+
subjectId: mrn,
42+
conditionId,
43+
type,
44+
stageGroup,
45+
effectiveDateTime: formatDateTime(effectiveDate),
46+
categoryIds,
47+
};
48+
}
49+
50+
class CSVStagingExtractor extends Extractor {
51+
constructor({ filePath }) {
52+
super();
53+
this.csvModule = new CSVModule(path.resolve(filePath));
54+
}
55+
56+
async getStagingData(mrn) {
57+
logger.debug('Getting Staging data');
58+
return this.csvModule.get('mrn', mrn);
59+
}
60+
61+
async get({ mrn }) {
62+
const stagingData = await this.getStagingData(mrn);
63+
const entryResources = [];
64+
stagingData.forEach((data) => {
65+
const formattedCategoryData = formatTNMCategoryData(data);
66+
67+
// Generate observation for each TNM category
68+
const mcodeCategoryResources = formattedCategoryData.map((d) => firstEntryInBundle(generateMcodeResources('TNMCategory', d)));
69+
70+
// Pass category resource ids to formatStagingData
71+
const formattedStagingData = formatStagingData(data, mcodeCategoryResources.map((r) => r.resource.id));
72+
const stagingResource = firstEntryInBundle(generateMcodeResources('Staging', formattedStagingData));
73+
74+
// Push all resources into entryResources
75+
mcodeCategoryResources.forEach((r) => entryResources.push(r));
76+
entryResources.push(stagingResource);
77+
});
78+
79+
return {
80+
resourceType: 'Bundle',
81+
type: 'collection',
82+
entry: entryResources,
83+
};
84+
}
85+
}
86+
87+
module.exports = {
88+
CSVStagingExtractor,
89+
};

src/extractors/index.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
const { BaseFHIRExtractor } = require('./BaseFHIRExtractor');
2+
const { CSVCancerDiseaseStatusExtractor } = require('./CSVCancerDiseaseStatusExtractor');
3+
const { CSVCancerRelatedMedicationExtractor } = require('./CSVCancerRelatedMedicationExtractor');
4+
const { CSVClinicalTrialInformationExtractor } = require('./CSVClinicalTrialInformationExtractor');
5+
const { CSVConditionExtractor } = require('./CSVConditionExtractor');
6+
const { CSVObservationExtractor } = require('./CSVObservationExtractor');
7+
const { CSVPatientExtractor } = require('./CSVPatientExtractor');
8+
const { CSVProcedureExtractor } = require('./CSVProcedureExtractor');
9+
const { CSVStagingExtractor } = require('./CSVStagingExtractor');
10+
const { CSVTreatmentPlanChangeExtractor } = require('./CSVTreatmentPlanChangeExtractor');
11+
const { Extractor } = require('./Extractor');
212
const { FHIRAllergyIntoleranceExtractor } = require('./FHIRAllergyIntoleranceExtractor');
313
const { FHIRConditionExtractor } = require('./FHIRConditionExtractor');
414
const { FHIRDocumentReferenceExtractor } = require('./FHIRDocumentReferenceExtractor');
@@ -8,26 +18,18 @@ const { FHIRMedicationStatementExtractor } = require('./FHIRMedicationStatementE
818
const { FHIRObservationExtractor } = require('./FHIRObservationExtractor');
919
const { FHIRPatientExtractor } = require('./FHIRPatientExtractor');
1020
const { FHIRProcedureExtractor } = require('./FHIRProcedureExtractor');
11-
const { CSVCancerDiseaseStatusExtractor } = require('./CSVCancerDiseaseStatusExtractor');
12-
const { CSVClinicalTrialInformationExtractor } = require('./CSVClinicalTrialInformationExtractor');
13-
const { CSVCancerRelatedMedicationExtractor } = require('./CSVCancerRelatedMedicationExtractor');
14-
const { CSVConditionExtractor } = require('./CSVConditionExtractor');
15-
const { CSVPatientExtractor } = require('./CSVPatientExtractor');
16-
const { CSVTreatmentPlanChangeExtractor } = require('./CSVTreatmentPlanChangeExtractor');
17-
const { CSVObservationExtractor } = require('./CSVObservationExtractor');
18-
const { CSVProcedureExtractor } = require('./CSVProcedureExtractor');
19-
const { Extractor } = require('./Extractor');
2021

2122
module.exports = {
2223
BaseFHIRExtractor,
2324
CSVCancerDiseaseStatusExtractor,
25+
CSVCancerRelatedMedicationExtractor,
2426
CSVClinicalTrialInformationExtractor,
2527
CSVConditionExtractor,
28+
CSVObservationExtractor,
2629
CSVPatientExtractor,
2730
CSVProcedureExtractor,
28-
CSVObservationExtractor,
31+
CSVStagingExtractor,
2932
CSVTreatmentPlanChangeExtractor,
30-
CSVCancerRelatedMedicationExtractor,
3133
Extractor,
3234
FHIRAllergyIntoleranceExtractor,
3335
FHIRConditionExtractor,

src/index.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ const { BaseClient } = require('./client/BaseClient');
33
const {
44
BaseFHIRExtractor,
55
CSVCancerDiseaseStatusExtractor,
6+
CSVCancerRelatedMedicationExtractor,
67
CSVClinicalTrialInformationExtractor,
78
CSVConditionExtractor,
9+
CSVObservationExtractor,
810
CSVPatientExtractor,
911
CSVProcedureExtractor,
12+
CSVStagingExtractor,
1013
CSVTreatmentPlanChangeExtractor,
11-
CSVObservationExtractor,
12-
CSVCancerRelatedMedicationExtractor,
1314
Extractor,
1415
FHIRAllergyIntoleranceExtractor,
1516
FHIRConditionExtractor,
@@ -54,14 +55,15 @@ module.exports = {
5455
BaseFHIRExtractor,
5556
BaseFHIRModule,
5657
CSVCancerDiseaseStatusExtractor,
58+
CSVCancerRelatedMedicationExtractor,
5759
CSVClinicalTrialInformationExtractor,
5860
CSVConditionExtractor,
5961
CSVModule,
6062
CSVPatientExtractor,
63+
CSVObservationExtractor,
6164
CSVProcedureExtractor,
65+
CSVStagingExtractor,
6266
CSVTreatmentPlanChangeExtractor,
63-
CSVCancerRelatedMedicationExtractor,
64-
CSVObservationExtractor,
6567
Extractor,
6668
FHIRAllergyIntoleranceExtractor,
6769
FHIRConditionExtractor,

src/templates/ResourceGenerator.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const { patientTemplate } = require('./PatientTemplate');
1111
const { procedureTemplate } = require('./ProcedureTemplate');
1212
const { researchStudyTemplate } = require('./ResearchStudyTemplate');
1313
const { researchSubjectTemplate } = require('./ResearchSubjectTemplate');
14+
const { stagingTemplate } = require('./StagingTemplate');
15+
const { tnmCategoryTemplate } = require('./TNMCategoryTemplate');
1416

1517
const fhirTemplateLookup = {
1618
CancerDiseaseStatus: cancerDiseaseStatusTemplate,
@@ -22,6 +24,8 @@ const fhirTemplateLookup = {
2224
Procedure: procedureTemplate,
2325
ResearchStudy: researchStudyTemplate,
2426
ResearchSubject: researchSubjectTemplate,
27+
Staging: stagingTemplate,
28+
TNMCategory: tnmCategoryTemplate,
2529
};
2630

2731
function loadFhirTemplate(mcodeProfileID) {

src/templates/StagingTemplate.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const { coding, reference, valueX } = require('./snippets');
2+
3+
// Returns staging specific data based whether it is clinical or pathologic
4+
function getTypeSpecificData(type) {
5+
if (type === 'Clinical') {
6+
return {
7+
categoryCode: 'survey',
8+
code: '21908-9',
9+
profileUrl: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-stage-group',
10+
};
11+
}
12+
13+
if (type === 'Pathologic') {
14+
return {
15+
categoryCode: 'laboratory',
16+
code: '21902-2',
17+
profileUrl: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-stage-group',
18+
};
19+
}
20+
21+
throw new Error('Provided staging type must be "Clinical" or "Pathologic"');
22+
}
23+
24+
function hasMemberTemplate(categoryIds) {
25+
if (!categoryIds || categoryIds.length === 0) return {};
26+
27+
return {
28+
hasMember: categoryIds.map((id) => reference({ id })),
29+
};
30+
}
31+
32+
function stagingTemplate({
33+
id,
34+
subjectId,
35+
conditionId,
36+
type,
37+
stageGroup,
38+
effectiveDateTime,
39+
categoryIds,
40+
}) {
41+
if (!(id && subjectId && conditionId && effectiveDateTime && stageGroup && type)) {
42+
throw Error('Trying to render a StagingTemplate, but a required argument is missing;'
43+
+ ' ensure that id, subjectId, conditionId, effectiveDateTime, stageGroup, type, are all present');
44+
}
45+
46+
const typeSpecificData = getTypeSpecificData(type);
47+
const { categoryCode, code, profileUrl } = typeSpecificData;
48+
49+
return {
50+
resourceType: 'Observation',
51+
id,
52+
meta: {
53+
profile: [profileUrl],
54+
},
55+
status: 'final',
56+
category: [
57+
{
58+
coding: [
59+
coding({
60+
system:
61+
'http://terminology.hl7.org/CodeSystem/observation-category',
62+
code: categoryCode,
63+
}),
64+
],
65+
},
66+
],
67+
code: {
68+
coding: [
69+
coding({
70+
system: 'http://loinc.org',
71+
code,
72+
}),
73+
],
74+
},
75+
subject: reference({ id: subjectId }),
76+
effectiveDateTime,
77+
...valueX({ code: stageGroup, system: 'http://cancerstaging.org' }, 'valueCodeableConcept'),
78+
focus: [reference({ id: conditionId })],
79+
...hasMemberTemplate(categoryIds),
80+
};
81+
}
82+
83+
module.exports = {
84+
stagingTemplate,
85+
};

src/templates/TNMCategoryTemplate.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const { coding, reference, valueX } = require('./snippets');
2+
3+
// Returns category specific data based on stage and category type
4+
function getCategorySpecificData(stageType, categoryType) {
5+
if (stageType === 'Clinical') {
6+
const categoryCode = 'survey';
7+
if (categoryType === 'Tumor') {
8+
return {
9+
categoryCode,
10+
profileUrl: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-primary-tumor-category',
11+
code: '21905-5',
12+
};
13+
}
14+
if (categoryType === 'Metastases') {
15+
return {
16+
categoryCode,
17+
profileUrl: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-distant-metastases-category',
18+
code: '21907-1',
19+
};
20+
}
21+
if (categoryType === 'Nodes') {
22+
return {
23+
categoryCode,
24+
profileUrl: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-clinical-regional-nodes-category',
25+
code: '21906-3',
26+
};
27+
}
28+
} else if (stageType === 'Pathologic') {
29+
const categoryCode = 'laboratory';
30+
if (categoryType === 'Tumor') {
31+
return {
32+
categoryCode,
33+
profileUrl: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-primary-tumor-category',
34+
code: '21899-0',
35+
};
36+
}
37+
if (categoryType === 'Metastases') {
38+
return {
39+
categoryCode,
40+
profileUrl: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-distant-metastases-category',
41+
code: '21901-4',
42+
};
43+
}
44+
if (categoryType === 'Nodes') {
45+
return {
46+
categoryCode,
47+
profileUrl: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-tnm-pathological-regional-nodes-category',
48+
code: '21900-6',
49+
};
50+
}
51+
}
52+
53+
throw new Error('Provided stage and category types are not valid.');
54+
}
55+
56+
function tnmCategoryTemplate({
57+
id, subjectId, conditionId, valueCode, effectiveDateTime, categoryType, stageType,
58+
}) {
59+
if (!(id && subjectId && conditionId && valueCode && effectiveDateTime && categoryType && stageType)) {
60+
throw Error('Trying to render a TNMCategoryTemplate, but a required argument is missing;'
61+
+ ' ensure that id, subjectId, conditionId, valueCode, effectiveDateTime, categoryType, and stageType, are all present');
62+
}
63+
64+
const categorySpecificData = getCategorySpecificData(stageType, categoryType);
65+
const { categoryCode, code, profileUrl } = categorySpecificData;
66+
67+
return {
68+
resourceType: 'Observation',
69+
id,
70+
meta: {
71+
profile: [profileUrl],
72+
},
73+
status: 'final',
74+
category: [
75+
{
76+
coding: [
77+
coding({
78+
system:
79+
'http://terminology.hl7.org/CodeSystem/observation-category',
80+
code: categoryCode,
81+
}),
82+
],
83+
},
84+
],
85+
code: {
86+
coding: [
87+
coding({
88+
system: 'http://loinc.org',
89+
code,
90+
}),
91+
],
92+
},
93+
subject: reference({ id: subjectId }),
94+
effectiveDateTime,
95+
...valueX({ code: valueCode, system: 'http://cancerstaging.org' }, 'valueCodeableConcept'),
96+
focus: [reference({ id: conditionId })],
97+
};
98+
}
99+
100+
module.exports = {
101+
tnmCategoryTemplate,
102+
};

0 commit comments

Comments
 (0)