@openhealth/oht-custom-parser-lib 0.2.81 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/service/ohtAgnosticMeasurementsExtractor.service.d.ts +59 -0
- package/dist/service/ohtAgnosticMeasurementsExtractor.service.js +603 -0
- package/dist/service/ohtAgnosticMeasurementsExtractor.service.js.map +1 -0
- package/dist/service/ohtReportMeasurementsExtractor.service.d.ts +157 -0
- package/dist/service/ohtReportMeasurementsExtractor.service.js +693 -0
- package/dist/service/ohtReportMeasurementsExtractor.service.js.map +1 -0
- package/dist/types/custom-parser.types.d.ts +1 -1
- package/dist/types/oht.types.d.ts +5 -3
- package/dist/types/oht.types.js.map +1 -1
- package/dist/util-ts/extractionUtils.d.ts +69 -0
- package/dist/util-ts/extractionUtils.js +666 -0
- package/dist/util-ts/extractionUtils.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.ohtReportMeasurementsExtractor = ohtReportMeasurementsExtractor;
|
|
40
|
+
const dataUtils_1 = require("../util-ts/dataUtils");
|
|
41
|
+
const extractionUtils_1 = require("../util-ts/extractionUtils");
|
|
42
|
+
const pinoLogger_1 = __importDefault(require("../util-ts/pinoLogger"));
|
|
43
|
+
const apiUtils_1 = require("../util-ts/apiUtils");
|
|
44
|
+
const oht_types_1 = require("../types/oht.types");
|
|
45
|
+
const transformationRules_service_1 = require("./transformationRules.service");
|
|
46
|
+
const auxiliaryFunctions_service_1 = require("./auxiliaryFunctions.service");
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
/**
|
|
50
|
+
* Logger instance for the application
|
|
51
|
+
*/
|
|
52
|
+
const logger = (0, pinoLogger_1.default)();
|
|
53
|
+
// Import version from package.json file
|
|
54
|
+
// Handle multiple possible paths: compiled (dist/service/), development (service/), or when used as dependency
|
|
55
|
+
function getPackageJsonVersion() {
|
|
56
|
+
const possiblePaths = [
|
|
57
|
+
path.resolve(__dirname, '../../package.json'), // Compiled: dist/service/ -> dist/ -> root
|
|
58
|
+
path.resolve(__dirname, '../package.json'), // Development: service/ -> root
|
|
59
|
+
path.resolve(__dirname, '../../../package.json'), // When used as dependency from node_modules
|
|
60
|
+
path.resolve(process.cwd(), 'package.json'), // From current working directory
|
|
61
|
+
];
|
|
62
|
+
for (const packagePath of possiblePaths) {
|
|
63
|
+
if (fs.existsSync(packagePath)) {
|
|
64
|
+
try {
|
|
65
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
66
|
+
if (packageJson.name === '@openhealth/oht-custom-parser-lib' && packageJson.version) {
|
|
67
|
+
return packageJson.version;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
// Continue to next path if this one fails
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Fallback to a default version if package.json cannot be found
|
|
77
|
+
logger.warn('Could not find package.json, using default version');
|
|
78
|
+
return '0.0.0';
|
|
79
|
+
}
|
|
80
|
+
const ohtCustomParserLibVersion = getPackageJsonVersion();
|
|
81
|
+
/**
|
|
82
|
+
* Environment variable for OHT Core
|
|
83
|
+
*/
|
|
84
|
+
const OHT_CORE_URL = process.env.OHT_CORE_URL;
|
|
85
|
+
/**
|
|
86
|
+
* Current filename being processed (global state for error reporting)
|
|
87
|
+
*/
|
|
88
|
+
let filename = '';
|
|
89
|
+
/**
|
|
90
|
+
* Error messages for parser operations
|
|
91
|
+
*/
|
|
92
|
+
const ERROR_MESSAGES = {
|
|
93
|
+
NO_LAB_KEYS_FOUND: 'No lab keys found',
|
|
94
|
+
NO_MAPPING_FOUND: 'No Mapping found for labKey',
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Apply transformation rules to exams
|
|
98
|
+
* This function applies transformation rules chain to each exam, passing all exams as context
|
|
99
|
+
*
|
|
100
|
+
* @param exams - Array of original exam objects
|
|
101
|
+
* @param transformationRules - Mapping of labKey to transformation rule chains
|
|
102
|
+
* @param patientInfo - Patient information
|
|
103
|
+
* @param documentDate - Document date
|
|
104
|
+
* @returns Promise<ApplyTransformationRulesResult> - Array of transformed exams and error messages
|
|
105
|
+
*/
|
|
106
|
+
async function applyTransformationRulesToExams(exams, transformationRules, patientInfo, documentDate) {
|
|
107
|
+
if (transformationRules.size === 0) {
|
|
108
|
+
return {
|
|
109
|
+
transformedExams: exams,
|
|
110
|
+
applicationErrorMessages: []
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const transformedExams = [];
|
|
114
|
+
// Prepare context for transformation rules
|
|
115
|
+
const reportContext = {
|
|
116
|
+
patient: {
|
|
117
|
+
sex: patientInfo.sex,
|
|
118
|
+
birthdate: patientInfo.birthdate,
|
|
119
|
+
age: (0, auxiliaryFunctions_service_1.getPatientAge)(documentDate, patientInfo.birthdate),
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
// Prepare the OHT exam data
|
|
123
|
+
const ohtReport = {
|
|
124
|
+
exams: exams.map(e => e.ohtExam),
|
|
125
|
+
};
|
|
126
|
+
logger.info({ examsCount: exams.length, chainsCount: transformationRules.size, filename }, 'Applying transformation rules to exams...');
|
|
127
|
+
let labKeysWithRulesCount = 0;
|
|
128
|
+
let transformedLabKeysCount = 0;
|
|
129
|
+
const applicationErrorMessages = [];
|
|
130
|
+
for (const exam of exams) {
|
|
131
|
+
const labKey = exam.ohtExam.labKey;
|
|
132
|
+
const rulesChain = transformationRules.get(labKey);
|
|
133
|
+
if (!rulesChain || rulesChain.length === 0) {
|
|
134
|
+
transformedExams.push(exam);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
labKeysWithRulesCount++;
|
|
138
|
+
try {
|
|
139
|
+
const result = await (0, transformationRules_service_1.applyRules)(rulesChain, exam.ohtExam, {
|
|
140
|
+
ohtReportJson: ohtReport,
|
|
141
|
+
contextJson: reportContext,
|
|
142
|
+
});
|
|
143
|
+
if (result.errors.length > 0) {
|
|
144
|
+
logger.warn({ labKey, errors: result.errors, filename }, 'Errors applying transformation rule(s) for labKey.');
|
|
145
|
+
applicationErrorMessages.push(`${labKey}: ${result.errors.map(e => `(rule ID ${e.ruleId}) ${e.error}`).join('; ')}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
if (result.appliedRules.length > 0) {
|
|
149
|
+
transformedLabKeysCount++;
|
|
150
|
+
}
|
|
151
|
+
logger.info({ labKey, appliedRules: result.appliedRules.length, filename }, 'Done applying transformation rule(s) for labKey.');
|
|
152
|
+
// Add the transformed exam to the result array only if there are no errors in the chain
|
|
153
|
+
transformedExams.push({
|
|
154
|
+
ohtExam: result.data,
|
|
155
|
+
originalExam: exam.originalExam
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
logger.error({ labKey, error, filename }, 'Failed to apply transformation rules for labKey.');
|
|
161
|
+
applicationErrorMessages.push(`${labKey}: Unhandled error occurred.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
logger.info({
|
|
165
|
+
filename,
|
|
166
|
+
examsCount: exams.length,
|
|
167
|
+
chainsCount: transformationRules.size,
|
|
168
|
+
transformedLabKeysCount,
|
|
169
|
+
labKeysWithRulesCount,
|
|
170
|
+
}, 'Done applying the transformation rules to exams.');
|
|
171
|
+
return {
|
|
172
|
+
transformedExams,
|
|
173
|
+
applicationErrorMessages
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Extracts measurements from OHT report exams by processing each exam through biomarker mapping,
|
|
178
|
+
* transformation rules, and value extraction.
|
|
179
|
+
*
|
|
180
|
+
* This function is the main entry point for converting raw exam data into standardized OHT measurements.
|
|
181
|
+
* It handles biomarker mapping, unit extraction, value parsing, range extraction, and error reporting.
|
|
182
|
+
*
|
|
183
|
+
* ## Processing Flow:
|
|
184
|
+
*
|
|
185
|
+
* ### Step 1: Initialization and Validation
|
|
186
|
+
* - Validates that exams array is not empty
|
|
187
|
+
* - Initializes result arrays (measurements, unknownMeasurements, unknownUnits, unmappedLabKeys)
|
|
188
|
+
* - Sets up error tracking and transformation rules error tracking
|
|
189
|
+
*
|
|
190
|
+
* ### Step 2: Fetch Biomarker Mappings
|
|
191
|
+
* - Calls `getMappedBiomarkers()` to retrieve biomarker mappings from OHT Core backend
|
|
192
|
+
* - Maps labKeys to OHT biomarker IDs (ohtBmId) for the given partner
|
|
193
|
+
* - Logs the number of mappings retrieved
|
|
194
|
+
*
|
|
195
|
+
* ### Step 3: Apply Transformation Rules
|
|
196
|
+
* - Applies transformation rules to all exams using `applyTransformationRulesToExams()`
|
|
197
|
+
* - Transformation rules can modify exam data before processing (e.g., normalize units, fix values)
|
|
198
|
+
* - Creates a map of transformed exams for quick lookup during processing
|
|
199
|
+
* - Collects and reports any transformation rule errors
|
|
200
|
+
*
|
|
201
|
+
* ### Step 4: Collect Biomarker IDs and Fetch Biomarker Details
|
|
202
|
+
* - Iterates through all exams to collect unique OHT biomarker IDs from mappings
|
|
203
|
+
* - Filters out ignored biomarkers and unmapped biomarkers (ohtBmId === -1)
|
|
204
|
+
* - Makes API call to OHT Core to fetch full biomarker details (standard units, ranges, etc.)
|
|
205
|
+
* - Fetches synonym units from API if partnerId is provided (for unit resolution)
|
|
206
|
+
*
|
|
207
|
+
* ### Step 5: Process Each Exam
|
|
208
|
+
* For each exam in the input array:
|
|
209
|
+
*
|
|
210
|
+
* #### 5.1: Create UnmappedLabKey (Always)
|
|
211
|
+
* - **Always** creates an `unmappedLabKey` entry for every exam, regardless of mapping status
|
|
212
|
+
* - This ensures all labKeys are tracked for unmapped biomarker analysis
|
|
213
|
+
* - Includes exam metadata: labKey, biomarkerName, value, unit, annotation, originalName, dataOrigin
|
|
214
|
+
*
|
|
215
|
+
* #### 5.2: Check Biomarker Mapping Status
|
|
216
|
+
* - Retrieves backend mapping for the exam's labKey
|
|
217
|
+
* - Skips processing if biomarker is marked as `isIgnored === true`
|
|
218
|
+
* - Uses `handleBiomarkerId()` to determine final `ohtBmId` and `annotationConfig`
|
|
219
|
+
* - Handles unmapped biomarkers (ohtBmId === -1)
|
|
220
|
+
* - Handles `keepAsUnmapped` flag override
|
|
221
|
+
*
|
|
222
|
+
* #### 5.3: Process Mapped Biomarkers (ohtBmId !== -1 and !== undefined)
|
|
223
|
+
*
|
|
224
|
+
* ##### 5.3.1: Duplicate Check
|
|
225
|
+
* - Checks if biomarker ID already exists in measurements array
|
|
226
|
+
* - If duplicate, logs warning and adds error message, then continues to next exam
|
|
227
|
+
*
|
|
228
|
+
* ##### 5.3.2: Fetch Biomarker Details
|
|
229
|
+
* - Finds biomarker object from API response using `ohtBmId`
|
|
230
|
+
* - If not found, logs warning, adds error, and continues to next exam
|
|
231
|
+
*
|
|
232
|
+
* ##### 5.3.3: Extract Unit
|
|
233
|
+
* - Calls `ohtReportExtractUnit()` to extract and validate unit
|
|
234
|
+
* - Handles arbitrary units, standard units, alternative units, and synonym units
|
|
235
|
+
* - If unit cannot be resolved, adds to `unknownUnits` array
|
|
236
|
+
* - Logs warning and marks measurement as incorrectly parsed if unit extraction fails
|
|
237
|
+
*
|
|
238
|
+
* ##### 5.3.4: Parse Exam Value
|
|
239
|
+
* - Calls `ohtReportParseExamValue()` to extract value from exam
|
|
240
|
+
* - Determines value type (NUMERICAL or ALPHANUMERICAL) based on biomarker configuration
|
|
241
|
+
* - Handles greater than/less than comparators (e.g., ">10", "<5")
|
|
242
|
+
* - Logs warning and marks measurement as incorrectly parsed if value extraction fails
|
|
243
|
+
*
|
|
244
|
+
* ##### 5.3.5: Handle Numerical Without Valid Result
|
|
245
|
+
* - Checks if value is numerical but cannot be parsed as a valid number
|
|
246
|
+
* - If true, creates `unknownMeasurement` and continues to next exam
|
|
247
|
+
*
|
|
248
|
+
* ##### 5.3.6: Validate Plausible Values (for NUMERICAL biomarkers)
|
|
249
|
+
* - Calls `ohtReportCheckValueForPlausibleValues()` to validate value is within expected range
|
|
250
|
+
* - Converts value to standard unit if necessary using conversion factors
|
|
251
|
+
* - Logs warning and marks measurement as incorrectly parsed if value is not plausible
|
|
252
|
+
*
|
|
253
|
+
* ##### 5.3.7: Extract Reference Ranges (for NUMERICAL biomarkers)
|
|
254
|
+
* - Calls `ohtReportExtractReferenceRanges()` which uses `parseRange()` internally
|
|
255
|
+
* - Extracts min/max normal values based on patient age, sex, and range data
|
|
256
|
+
* - Filters ranges by patient demographics and selects most appropriate range
|
|
257
|
+
* - Logs warning and marks measurement as incorrectly parsed if range extraction fails
|
|
258
|
+
*
|
|
259
|
+
* ##### 5.3.8: Extract Reference Annotation
|
|
260
|
+
* - Calls `ohtReportExtractReferenceAnnotation()` to extract annotation text
|
|
261
|
+
* - Handles appended mappings (combines values from multiple related biomarkers)
|
|
262
|
+
* - Merges annotation from exam data with appended mapping results
|
|
263
|
+
*
|
|
264
|
+
* ##### 5.3.9: Build Measurement Object
|
|
265
|
+
* - Constructs complete `Measurement` object with all extracted data:
|
|
266
|
+
* - Biomarker ID, value type, value, alphanumeric value
|
|
267
|
+
* - Unit, original unit, arbitrary unit flag
|
|
268
|
+
* - Reference range (min/max normal values)
|
|
269
|
+
* - Range annotation
|
|
270
|
+
* - Material, method
|
|
271
|
+
* - Collection dates (collectionDate, effectiveDate, examCollectionDate, examEffectiveDate)
|
|
272
|
+
* - Original name for document search
|
|
273
|
+
* - Digitalization approval flags
|
|
274
|
+
* - Adds measurement to `measurements` array
|
|
275
|
+
*
|
|
276
|
+
* #### 5.4: Process Unmapped Biomarkers (no mapping or ohtBmId === -1)
|
|
277
|
+
* - Creates `unknownMeasurement` object with:
|
|
278
|
+
* - LabKey, biomarker name (mnemonic:examName), value, unit, annotation
|
|
279
|
+
* - Data origin information
|
|
280
|
+
* - Original exam data for reference
|
|
281
|
+
* - Adds to `unknownMeasurements` array
|
|
282
|
+
*
|
|
283
|
+
* ### Step 6: Summary and Return
|
|
284
|
+
* - Logs processing summary (total labKeys, mapped biomarkers, unmapped biomarkers, errors)
|
|
285
|
+
* - Returns result object containing:
|
|
286
|
+
* - `measurements`: Array of successfully parsed and mapped measurements
|
|
287
|
+
* - `unknownMeasurements`: Array of measurements that could not be mapped or parsed
|
|
288
|
+
* - `unknownUnits`: Array of units that could not be resolved
|
|
289
|
+
* - `unmappedLabKeys`: Array of all labKeys (always created for every exam)
|
|
290
|
+
* - `isReportCorrectlyParsed`: Boolean indicating if all measurements were parsed correctly
|
|
291
|
+
* - `transformationRulesErrors`: Array of errors from transformation rules application
|
|
292
|
+
*
|
|
293
|
+
* ## Error Handling:
|
|
294
|
+
* - All errors are added to the `errorMessages` array passed as parameter
|
|
295
|
+
* - Errors are categorized by destination (slack, notes, bi)
|
|
296
|
+
* - Processing continues even if individual exams fail
|
|
297
|
+
* - Errors are logged with context (filename, labKey, ohtBmId, etc.)
|
|
298
|
+
*
|
|
299
|
+
* ## Key Behaviors:
|
|
300
|
+
* - **UnmappedLabKeys**: Always created for ALL exams, regardless of mapping status
|
|
301
|
+
* - **UnknownMeasurements**: Created when:
|
|
302
|
+
* - No backend mapping exists for labKey
|
|
303
|
+
* - ohtBmId === -1 (explicitly unmapped)
|
|
304
|
+
* - Value is numerical but cannot be parsed as valid number
|
|
305
|
+
* - **Measurements**: Created only when biomarker is successfully mapped and all extraction steps succeed
|
|
306
|
+
*
|
|
307
|
+
* @param exams - Array of Exam objects containing ohtExam and originalExam data
|
|
308
|
+
* @param sourceFilename - Name of the source file being processed (for error reporting)
|
|
309
|
+
* @param patientInfo - Patient information (sex, birthdate) for range extraction
|
|
310
|
+
* @param documentDate - Document date for age calculation and range extraction
|
|
311
|
+
* @param transformationRules - Map of labKey to transformation rule chains to apply
|
|
312
|
+
* @param ohtCoreApiKey - API key for OHT Core API calls
|
|
313
|
+
* @param partnerId - Partner ID for fetching biomarker mappings and synonym units
|
|
314
|
+
* @param errorMessages - Array to accumulate error messages (modified in-place)
|
|
315
|
+
* @returns Promise resolving to extraction result with measurements, unknownMeasurements, unknownUnits, unmappedLabKeys, parsing status, and transformation errors
|
|
316
|
+
*/
|
|
317
|
+
async function ohtReportMeasurementsExtractor(exams, sourceFilename, patientInfo, documentDate, transformationRules, ohtCoreApiKey, partnerId, errorMessages) {
|
|
318
|
+
filename = sourceFilename;
|
|
319
|
+
const measurements = [];
|
|
320
|
+
const unknownMeasurements = [];
|
|
321
|
+
const unknownUnits = [];
|
|
322
|
+
const unmappedLabKeys = [];
|
|
323
|
+
let isReportCorrectlyParsed = true;
|
|
324
|
+
let transformationRulesErrors = [];
|
|
325
|
+
// Check if we have exams
|
|
326
|
+
const labKeys = exams.map(e => e.ohtExam.labKey);
|
|
327
|
+
if (labKeys.length === 0) {
|
|
328
|
+
const errorMsg = `No lab keys found, filename: ${filename}`;
|
|
329
|
+
logger.error(errorMsg);
|
|
330
|
+
errorMessages.push({
|
|
331
|
+
destination: { slack: true, notes: false, bi: false },
|
|
332
|
+
message: errorMsg,
|
|
333
|
+
});
|
|
334
|
+
return {
|
|
335
|
+
measurements,
|
|
336
|
+
unknownMeasurements,
|
|
337
|
+
unknownUnits,
|
|
338
|
+
unmappedLabKeys,
|
|
339
|
+
isReportCorrectlyParsed: false,
|
|
340
|
+
transformationRulesErrors,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
logger.info(`Processing lab file with ${labKeys.length} keys, filename: ${filename}`);
|
|
344
|
+
// Call getMappedBiomarkers to get the biomarker mapping for the partner
|
|
345
|
+
const mappedBiomarkers = await (0, dataUtils_1.getMappedBiomarkers)(partnerId, ohtCoreApiKey);
|
|
346
|
+
// add "GROUPED_" bms for appended mappings
|
|
347
|
+
if (mappedBiomarkers) {
|
|
348
|
+
const groupedLabKeys = Object.keys(mappedBiomarkers).filter(labKey => labKey.startsWith('GROUPED_'));
|
|
349
|
+
for (const groupedLabKey of groupedLabKeys) {
|
|
350
|
+
const emptyOhtExam = {
|
|
351
|
+
labKey: groupedLabKey,
|
|
352
|
+
value: mappedBiomarkers[groupedLabKey]?.syntheticValue ?? '',
|
|
353
|
+
unit: '',
|
|
354
|
+
documentDate: documentDate ? documentDate.toISOString() : '',
|
|
355
|
+
description: '',
|
|
356
|
+
mnemonic: '',
|
|
357
|
+
examName: '',
|
|
358
|
+
range: [],
|
|
359
|
+
annotation: '',
|
|
360
|
+
};
|
|
361
|
+
const emptyExam = {
|
|
362
|
+
ohtExam: emptyOhtExam,
|
|
363
|
+
originalExam: {
|
|
364
|
+
contentType: 'application/json',
|
|
365
|
+
exam: JSON.stringify(emptyOhtExam),
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
exams.push(emptyExam);
|
|
369
|
+
}
|
|
370
|
+
if (groupedLabKeys.length > 0) {
|
|
371
|
+
logger.info(`Added ${groupedLabKeys.length} empty OhtExam objects for GROUPED_ biomarkers to check for appended mappings`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (mappedBiomarkers) {
|
|
375
|
+
logger.info(`Retrieved ${Object.keys(mappedBiomarkers).length} biomarker mappings from backend for partner: ${partnerId}`);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
logger.info(`No biomarker mappings found in backend for partner: ${partnerId}`);
|
|
379
|
+
}
|
|
380
|
+
// Apply transformation rules to all exams
|
|
381
|
+
let transformedExamsResult = { transformedExams: [], applicationErrorMessages: [] };
|
|
382
|
+
try {
|
|
383
|
+
transformedExamsResult = await applyTransformationRulesToExams(exams, transformationRules, patientInfo, documentDate);
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
logger.error({ error, filename }, 'Unhandled error while applying transformation rules to exams.');
|
|
387
|
+
errorMessages.push({
|
|
388
|
+
destination: { slack: true, notes: true, bi: false },
|
|
389
|
+
message: `Failed applying the transformation rules for filename ${filename}. ${error instanceof Error ? error.message : error}`,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
if (transformedExamsResult.applicationErrorMessages.length > 0) {
|
|
393
|
+
transformationRulesErrors = transformedExamsResult.applicationErrorMessages;
|
|
394
|
+
errorMessages.push({
|
|
395
|
+
destination: { slack: false, notes: true, bi: false },
|
|
396
|
+
message: `Failed applying some transformation rules:\n${transformedExamsResult.applicationErrorMessages.map((e) => ` - ${e}`).join("\n\n")}`,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const transformedExams = transformedExamsResult.transformedExams;
|
|
400
|
+
const transformedExamsMap = transformedExams.reduce((map, exam) => map.set(exam.ohtExam.labKey, exam), new Map());
|
|
401
|
+
// Collect all possible OHT biomarker IDs from mapped biomarkers
|
|
402
|
+
const allPossibleOHTBmIds = [];
|
|
403
|
+
for (const exam of exams) {
|
|
404
|
+
const labKey = exam.ohtExam.labKey;
|
|
405
|
+
const backendMapping = mappedBiomarkers?.[labKey];
|
|
406
|
+
if (backendMapping && !backendMapping.isIgnored) {
|
|
407
|
+
const { ohtBmId } = (0, extractionUtils_1.handleBiomarkerId)(backendMapping, labKey);
|
|
408
|
+
if (ohtBmId && ohtBmId !== -1) {
|
|
409
|
+
allPossibleOHTBmIds.push(ohtBmId);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
// data to request api
|
|
414
|
+
const requestData = {
|
|
415
|
+
"biomarkerIds": allPossibleOHTBmIds,
|
|
416
|
+
"page": 1,
|
|
417
|
+
"pageSize": allPossibleOHTBmIds.length
|
|
418
|
+
};
|
|
419
|
+
// api call to ohtCore to find biomarkers.
|
|
420
|
+
const biomarkers = await (0, apiUtils_1.makeApiCallWithRetry)('post', `${OHT_CORE_URL}/v1/biomarker/biomarkers`, requestData, ohtCoreApiKey);
|
|
421
|
+
// Fetch synonym units from API if enabled
|
|
422
|
+
let synonymUnits = null;
|
|
423
|
+
if (partnerId) {
|
|
424
|
+
synonymUnits = await (0, dataUtils_1.fetchSynonymUnits)(partnerId, ohtCoreApiKey);
|
|
425
|
+
// If API fetch failed or returned empty, set to null to use hardcoded fallback
|
|
426
|
+
if (synonymUnits.length === 0) {
|
|
427
|
+
synonymUnits = null;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Process each exam directly
|
|
431
|
+
for (const exam of exams) {
|
|
432
|
+
const labKey = exam.ohtExam.labKey;
|
|
433
|
+
const examObj = exam.ohtExam;
|
|
434
|
+
const originalExam = exam.originalExam;
|
|
435
|
+
// Get the transformed exam for this labKey, or use the original ohtExam
|
|
436
|
+
const transformedExam = transformedExamsMap.get(labKey);
|
|
437
|
+
const transformedExamObj = transformedExam ? transformedExam.ohtExam : examObj;
|
|
438
|
+
// ALWAYS create unmappedLabKey for all exams
|
|
439
|
+
const unmappedLabKey = {
|
|
440
|
+
labKey: labKey,
|
|
441
|
+
biomarkerName: `${examObj.mnemonic}`,
|
|
442
|
+
value: examObj.value || '',
|
|
443
|
+
unit: examObj.unit || '',
|
|
444
|
+
annotation: examObj.annotation || '',
|
|
445
|
+
originalName: `${examObj.examName}`,
|
|
446
|
+
dataOrigin: [
|
|
447
|
+
{
|
|
448
|
+
itemExtracted: 'measurement',
|
|
449
|
+
method: 'custom-parser',
|
|
450
|
+
methodVersion: `oht-custom-parser-lib-v${ohtCustomParserLibVersion}`,
|
|
451
|
+
path: 'exams',
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
originalExam: originalExam ? originalExam : undefined,
|
|
455
|
+
ohtExam: examObj,
|
|
456
|
+
};
|
|
457
|
+
unmappedLabKeys.push(unmappedLabKey);
|
|
458
|
+
// Get backend mapping for this labKey
|
|
459
|
+
const backendMapping = mappedBiomarkers?.[labKey];
|
|
460
|
+
// Skip processing if biomarker is marked as ignored
|
|
461
|
+
if (backendMapping?.isIgnored === true) {
|
|
462
|
+
logger.info(`Skipping ignored biomarker: ${labKey}`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
// Handle biomarker id
|
|
466
|
+
let finalOhtBmId;
|
|
467
|
+
let annotationConfig;
|
|
468
|
+
if (backendMapping) {
|
|
469
|
+
const { ohtBmId, annotationConfig: config } = (0, extractionUtils_1.handleBiomarkerId)(backendMapping, labKey);
|
|
470
|
+
finalOhtBmId = ohtBmId;
|
|
471
|
+
annotationConfig = {
|
|
472
|
+
enabled: config.enabled,
|
|
473
|
+
labKeys: backendMapping.annotationConfig?.labKeys || [],
|
|
474
|
+
maxLength: backendMapping.annotationConfig?.maxLength,
|
|
475
|
+
fallbackValue: backendMapping.annotationConfig?.fallbackValue,
|
|
476
|
+
separator: backendMapping.annotationConfig?.separator,
|
|
477
|
+
};
|
|
478
|
+
logger.info(`Using mappings from custom parser studio (oht-core backend) for labKey: ${labKey} (ohtBmId: ${backendMapping.ohtBmId})`);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
logger.debug(`No backend mapping found for labKey: ${labKey}, will process as unmapped`);
|
|
482
|
+
}
|
|
483
|
+
// Process as mapped biomarker if ohtBmId is valid
|
|
484
|
+
if (finalOhtBmId !== undefined && finalOhtBmId !== -1) {
|
|
485
|
+
let isMeasurementCorrectlyParsed = true;
|
|
486
|
+
// if BM is already in measurements, continue to the next one
|
|
487
|
+
if (measurements.some(measurement => measurement.biomarkerId === finalOhtBmId)) {
|
|
488
|
+
logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Biomarker already in measurements array, should investigate, filename: ${filename}`);
|
|
489
|
+
errorMessages.push({
|
|
490
|
+
destination: { slack: false, notes: false, bi: true },
|
|
491
|
+
message: `Biomarkers Duplicated keys:[PLACEHOLDER]`,
|
|
492
|
+
item: `bmId:${finalOhtBmId}, labKey:${transformedExamObj.labKey}`
|
|
493
|
+
});
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
const ohtBiomarker = biomarkers?.data?.items.find((bm) => bm.id === finalOhtBmId);
|
|
497
|
+
if (!ohtBiomarker) {
|
|
498
|
+
logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Biomarker not found in API response for ohtBmId: ${finalOhtBmId}, labKey: ${transformedExamObj.labKey}, filename: ${filename}`);
|
|
499
|
+
errorMessages.push({
|
|
500
|
+
destination: { slack: true, notes: true, bi: true },
|
|
501
|
+
message: `Biomarker not found for:[PLACEHOLDER]`,
|
|
502
|
+
item: `bmId:${finalOhtBmId}, labKey:${transformedExamObj.labKey}`
|
|
503
|
+
});
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
// Used for error messages
|
|
507
|
+
const bmInfo = ohtBiomarker?.acf?.shortName ?
|
|
508
|
+
`name:${ohtBiomarker.acf.shortName}, bmId:${finalOhtBmId}, labKey:${transformedExamObj.labKey}` :
|
|
509
|
+
`bmId:${finalOhtBmId}, labKey:${transformedExamObj.labKey}`;
|
|
510
|
+
const { unit, isUnitParsedCorrectly, originalUnit, isArbitraryUnit, unknownUnit } = (0, extractionUtils_1.ohtReportExtractUnit)(transformedExamObj, ohtBiomarker, synonymUnits, partnerId);
|
|
511
|
+
if (unknownUnit.length > 0) {
|
|
512
|
+
unknownUnits.push({
|
|
513
|
+
biomarkerId: ohtBiomarker.id,
|
|
514
|
+
unit: unknownUnit,
|
|
515
|
+
partnerId: partnerId || '',
|
|
516
|
+
labKey: transformedExamObj.labKey,
|
|
517
|
+
ohtExam: examObj,
|
|
518
|
+
originalExam: originalExam,
|
|
519
|
+
biomarkerName: transformedExamObj.mnemonic || '',
|
|
520
|
+
dataImportFileUploadId: null, // is null it will be set later in the structParser.service.ts
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
if (!isUnitParsedCorrectly) {
|
|
524
|
+
logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Unit not extracted correctly for "${finalOhtBmId}|${transformedExamObj.labKey}", filename: ${filename}`);
|
|
525
|
+
errorMessages.push({
|
|
526
|
+
destination: { slack: true, notes: true, bi: true },
|
|
527
|
+
message: `Unit not extracted correctly for:[PLACEHOLDER]`,
|
|
528
|
+
item: `${bmInfo}`
|
|
529
|
+
});
|
|
530
|
+
isMeasurementCorrectlyParsed = false;
|
|
531
|
+
}
|
|
532
|
+
let { valueType, alphanumericalValue, numberValue, valueComparator, isValueParsedCorrectly } = (0, extractionUtils_1.ohtReportParseExamValue)(transformedExamObj, ohtBiomarker);
|
|
533
|
+
if (!isValueParsedCorrectly) {
|
|
534
|
+
logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Value not extracted correctly for "${finalOhtBmId}|${transformedExamObj.labKey}", filename: ${filename}`);
|
|
535
|
+
errorMessages.push({
|
|
536
|
+
destination: { slack: true, notes: true, bi: true },
|
|
537
|
+
message: `Value not extracted correctly for:[PLACEHOLDER]`,
|
|
538
|
+
item: `${bmInfo}`
|
|
539
|
+
});
|
|
540
|
+
isMeasurementCorrectlyParsed = false;
|
|
541
|
+
}
|
|
542
|
+
if ((0, dataUtils_1.isNumericalWithoutValidResult)(valueType, numberValue, alphanumericalValue)) {
|
|
543
|
+
const alphanumericalValueTrimmed = alphanumericalValue?.trim() ?? '';
|
|
544
|
+
const unknownMeasurement = {
|
|
545
|
+
biomarkerName: transformedExamObj.description ?? '',
|
|
546
|
+
value: alphanumericalValueTrimmed,
|
|
547
|
+
unit: unit ?? '',
|
|
548
|
+
annotation: transformedExamObj.annotation ?? '',
|
|
549
|
+
dataOrigin: [{
|
|
550
|
+
itemExtracted: 'measurement',
|
|
551
|
+
method: 'custom-parser',
|
|
552
|
+
methodVersion: 'oht-custom-parser-lib',
|
|
553
|
+
path: 'exams',
|
|
554
|
+
}],
|
|
555
|
+
originalName: transformedExamObj.examName ?? '',
|
|
556
|
+
originalExam: {
|
|
557
|
+
contentType: 'application/json',
|
|
558
|
+
exam: JSON.stringify(transformedExamObj)
|
|
559
|
+
},
|
|
560
|
+
ohtExam: transformedExamObj
|
|
561
|
+
};
|
|
562
|
+
unknownMeasurements.push(unknownMeasurement);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
// Check if the value is plausible
|
|
566
|
+
if (isValueParsedCorrectly && valueType === 'NUMERICAL') {
|
|
567
|
+
const isPlausibleValue = numberValue !== null && (0, extractionUtils_1.ohtReportCheckValueForPlausibleValues)(finalOhtBmId, transformedExamObj.labKey, ohtBiomarker, unit, numberValue, isUnitParsedCorrectly, filename);
|
|
568
|
+
if (!isPlausibleValue) {
|
|
569
|
+
logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Value [${numberValue} ${unit}] is not plausible for "${finalOhtBmId}|${transformedExamObj.labKey}", filename: ${filename}`);
|
|
570
|
+
errorMessages.push({
|
|
571
|
+
destination: { slack: true, notes: true, bi: true },
|
|
572
|
+
message: `Value is not plausible for:[PLACEHOLDER]`,
|
|
573
|
+
item: `${bmInfo}, value:${numberValue}${unit}`
|
|
574
|
+
});
|
|
575
|
+
isMeasurementCorrectlyParsed = false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
let from = null, to = null;
|
|
579
|
+
let isRangeParsedCorrectly = true;
|
|
580
|
+
// If the biomarker is numerical, extract the reference range
|
|
581
|
+
if ((0, dataUtils_1.isBiomarkerValueNumerical)(ohtBiomarker)) {
|
|
582
|
+
const extractedRange = (0, extractionUtils_1.ohtReportExtractReferenceRanges)(transformedExamObj, patientInfo);
|
|
583
|
+
from = extractedRange.from ?? null;
|
|
584
|
+
to = extractedRange.to ?? null;
|
|
585
|
+
isRangeParsedCorrectly = extractedRange.isRangeParsedCorrectly;
|
|
586
|
+
if (!isRangeParsedCorrectly) {
|
|
587
|
+
logger.warn({ ohtBmId: finalOhtBmId, labKey }, `Range not extracted correctly for "${finalOhtBmId}|${transformedExamObj.labKey}", filename: ${filename}`);
|
|
588
|
+
errorMessages.push({
|
|
589
|
+
destination: { slack: true, notes: true, bi: true },
|
|
590
|
+
message: `Range not extracted correctly for:[PLACEHOLDER]`,
|
|
591
|
+
item: `${bmInfo}`
|
|
592
|
+
});
|
|
593
|
+
isMeasurementCorrectlyParsed = false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
let extractedAnnotation;
|
|
597
|
+
// TODO: always extract annotation? or only if annotationConfig.enabled is true?
|
|
598
|
+
extractedAnnotation = (0, extractionUtils_1.ohtReportExtractReferenceAnnotation)(annotationConfig, transformedExamObj, patientInfo, documentDate, transformedExams, filename);
|
|
599
|
+
// Use material and method directly from ohtExam
|
|
600
|
+
const material = transformedExamObj?.material ?? '';
|
|
601
|
+
const method = transformedExamObj?.method ?? '';
|
|
602
|
+
let alphaValue = !(0, dataUtils_1.isBiomarkerValueNumerical)(ohtBiomarker) && alphanumericalValue
|
|
603
|
+
? alphanumericalValue.trim()
|
|
604
|
+
: '';
|
|
605
|
+
// adding unit for alphanumerical values as number.
|
|
606
|
+
if (!Number.isNaN(parseFloat(alphaValue))) {
|
|
607
|
+
// Use unit directly from ohtExam
|
|
608
|
+
let originalUnitFromExam = transformedExamObj?.unit ?? '';
|
|
609
|
+
originalUnitFromExam = originalUnitFromExam?.replaceAll(/ /g, '') ?? '';
|
|
610
|
+
alphaValue = alphaValue && unit?.length > 0 ? `${alphaValue} ${originalUnitFromExam}`.trim() : alphaValue;
|
|
611
|
+
}
|
|
612
|
+
// adding dates for measurements in ISO 8601 format
|
|
613
|
+
const collectionDate = transformedExamObj.collectionDate ? new Date(transformedExamObj.collectionDate).toISOString() : null;
|
|
614
|
+
const effectiveDate = transformedExamObj.effectiveDate ? new Date(transformedExamObj.effectiveDate).toISOString() : null;
|
|
615
|
+
const examCollectionDate = transformedExamObj.examCollectionDate ? new Date(transformedExamObj.examCollectionDate).toISOString() : null;
|
|
616
|
+
const examEffectiveDate = transformedExamObj.examEffectiveDate ? new Date(transformedExamObj.examEffectiveDate).toISOString() : null;
|
|
617
|
+
// add original name for document search
|
|
618
|
+
const originalName = transformedExamObj.examName ?? '';
|
|
619
|
+
// if the biomarker is a grouped biomarker and the annotation is empty, ignore it and continue to the next one
|
|
620
|
+
if (labKey.startsWith('GROUPED_') && !extractedAnnotation) {
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
let measurement = {
|
|
624
|
+
biomarkerId: ohtBiomarker.id,
|
|
625
|
+
valueType,
|
|
626
|
+
value: (0, dataUtils_1.isBiomarkerValueNumerical)(ohtBiomarker) ? numberValue : null,
|
|
627
|
+
alphanumericValue: alphaValue,
|
|
628
|
+
rangeAnnotation: extractedAnnotation ? extractedAnnotation : null,
|
|
629
|
+
showVisualisationIfAnnotated: backendMapping?.referenceAsAnnotation?.enabled ?? false,
|
|
630
|
+
comparator: valueComparator ?? oht_types_1.MeasurementValueComparator.EQUAL,
|
|
631
|
+
minNormalValue: from,
|
|
632
|
+
maxNormalValue: to,
|
|
633
|
+
unit,
|
|
634
|
+
originalUnit,
|
|
635
|
+
isArbitraryUnit,
|
|
636
|
+
material,
|
|
637
|
+
method,
|
|
638
|
+
isDigitalizationApproved: isMeasurementCorrectlyParsed,
|
|
639
|
+
isManualChecked: isMeasurementCorrectlyParsed,
|
|
640
|
+
collectionDate: collectionDate,
|
|
641
|
+
effectiveDate: effectiveDate,
|
|
642
|
+
examCollectionDate: examCollectionDate,
|
|
643
|
+
examEffectiveDate: examEffectiveDate,
|
|
644
|
+
originalName: originalName,
|
|
645
|
+
};
|
|
646
|
+
measurements.push(measurement);
|
|
647
|
+
isReportCorrectlyParsed = isReportCorrectlyParsed && isMeasurementCorrectlyParsed;
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
// No mapping or ohtBmId === -1: create unknownMeasurement
|
|
651
|
+
const labkey = transformedExamObj?.labKey?.trim() || labKey.split(':')[0];
|
|
652
|
+
const unknownMeasurementName = `${transformedExamObj.mnemonic}:${transformedExamObj.examName}`;
|
|
653
|
+
const originalName = labKey.split(':')[1] || transformedExamObj.examName;
|
|
654
|
+
const annotation = `${transformedExamObj.annotation || ''}`;
|
|
655
|
+
const dataOrigin = {
|
|
656
|
+
itemExtracted: 'measurement',
|
|
657
|
+
method: 'custom-parser',
|
|
658
|
+
methodVersion: `oht-custom-parser-lib-v${ohtCustomParserLibVersion}`,
|
|
659
|
+
path: 'exams',
|
|
660
|
+
};
|
|
661
|
+
const unknownMeasurement = {
|
|
662
|
+
biomarkerName: unknownMeasurementName,
|
|
663
|
+
value: transformedExamObj.value || '',
|
|
664
|
+
unit: transformedExamObj.unit || '',
|
|
665
|
+
annotation: annotation,
|
|
666
|
+
dataOrigin: [dataOrigin],
|
|
667
|
+
originalName: originalName,
|
|
668
|
+
originalExam: originalExam ? originalExam : undefined,
|
|
669
|
+
ohtExam: transformedExamObj,
|
|
670
|
+
};
|
|
671
|
+
unknownMeasurements.push(unknownMeasurement);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Log summary of processing
|
|
675
|
+
const backendMappedCount = measurements.length;
|
|
676
|
+
const unmappedCount = unknownMeasurements.length;
|
|
677
|
+
logger.info(`Processing completed for ${filename}:`, {
|
|
678
|
+
totalLabKeys: labKeys.length,
|
|
679
|
+
backendMappedBiomarkers: backendMappedCount,
|
|
680
|
+
unmappedBiomarkers: unmappedCount,
|
|
681
|
+
totalProcessed: exams.length,
|
|
682
|
+
errors: errorMessages.length,
|
|
683
|
+
});
|
|
684
|
+
return {
|
|
685
|
+
measurements,
|
|
686
|
+
unknownMeasurements,
|
|
687
|
+
unknownUnits,
|
|
688
|
+
unmappedLabKeys,
|
|
689
|
+
isReportCorrectlyParsed,
|
|
690
|
+
transformationRulesErrors,
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
//# sourceMappingURL=ohtReportMeasurementsExtractor.service.js.map
|