fhirsmith 0.18.0 → 0.18.2
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/CHANGELOG.md +72 -10
- package/ips/ips.js +251 -26
- package/ips/jurisdictions.js +65 -0
- package/ips/pipeline.js +67 -6
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +48 -5
- package/ips/render.js +317 -46
- package/ips/scripts.js +46 -0
- package/ips/store.js +55 -0
- package/library/daily-report.js +25 -15
- package/package.json +3 -3
- package/registry/api.js +179 -1
- package/registry/crawler.js +2 -0
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry.js +53 -2
- package/translations/Messages.properties +1 -0
- package/tx/README.md +4 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cm/cm-package.js +21 -15
- package/tx/cs/cs-api.js +3 -1
- package/tx/cs/cs-cs.js +59 -12
- package/tx/cs/cs-icf.js +47 -34
- package/tx/cs/cs-loinc.js +257 -3
- package/tx/cs/cs-snomed.js +544 -55
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +131 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/library/tho-precedence.js +40 -0
- package/tx/openapi-operations.js +2 -1
- package/tx/provider.js +46 -29
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +34 -11
- package/tx/sct/structures.js +15 -2
- package/tx/tx.fhir.org.yml +2 -2
- package/tx/vs/vs-package.js +17 -13
- package/tx/vs/vs-vsac.js +1 -1
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +2 -2
- package/tx/workers/validate.js +10 -0
- package/utilities/make-sct-loinc-subset.js +250 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Yields the records of a CSV file one at a time, as text.
|
|
6
|
+
*
|
|
7
|
+
* A quoted field can contain a line break (LOINC 2.83's ComponentHierarchyBySystem.csv has one
|
|
8
|
+
* in a CODE_TEXT), so a record that ends inside quotes is joined to the next line until the
|
|
9
|
+
* quotes balance. An escaped quote ("") counts twice, so it doesn't upset the count.
|
|
10
|
+
*/
|
|
11
|
+
async function* csvRecords(filePath) {
|
|
12
|
+
const rl = readline.createInterface({
|
|
13
|
+
input: fs.createReadStream(filePath),
|
|
14
|
+
crlfDelay: Infinity
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let pending = null;
|
|
18
|
+
let quotes = 0;
|
|
19
|
+
for await (const line of rl) {
|
|
20
|
+
pending = pending === null ? line : pending + '\n' + line;
|
|
21
|
+
for (let i = 0; i < line.length; i++) {
|
|
22
|
+
if (line.charCodeAt(i) === 34) quotes++;
|
|
23
|
+
}
|
|
24
|
+
if (quotes % 2 === 0) {
|
|
25
|
+
yield pending;
|
|
26
|
+
pending = null;
|
|
27
|
+
quotes = 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (pending !== null) {
|
|
31
|
+
yield pending;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { csvRecords };
|
|
@@ -3,6 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const sqlite3 = require('sqlite3').verbose();
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const readline = require('readline');
|
|
6
|
+
const { csvRecords } = require('./csv-records');
|
|
6
7
|
const chalk = require('chalk');
|
|
7
8
|
|
|
8
9
|
class LoincModule extends BaseTerminologyModule {
|
|
@@ -291,7 +292,10 @@ class LoincModule extends BaseTerminologyModule {
|
|
|
291
292
|
// Translations are a different case: a main-only import is a legitimate thing to want,
|
|
292
293
|
// and their absence costs designations, not correctness
|
|
293
294
|
const optionalFiles = [
|
|
294
|
-
'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv'
|
|
295
|
+
'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv',
|
|
296
|
+
// the external code mappings: their absence costs maps to other code systems, nothing else
|
|
297
|
+
'AccessoryFiles/PartFile/PartRelatedCodeMapping.csv',
|
|
298
|
+
'AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable/LoincIeeeMedicalDeviceCodeMappingTable.csv'
|
|
295
299
|
];
|
|
296
300
|
|
|
297
301
|
const warnings = [];
|
|
@@ -428,6 +432,11 @@ class LoincModule extends BaseTerminologyModule {
|
|
|
428
432
|
'CREATE INDEX IF NOT EXISTS idx_descriptions_type ON Descriptions(DescriptionTypeKey)',
|
|
429
433
|
'CREATE INDEX IF NOT EXISTS idx_descriptions_code_type ON Descriptions(CodeKey, DescriptionTypeKey)',
|
|
430
434
|
|
|
435
|
+
// External mapping indexes
|
|
436
|
+
'CREATE INDEX IF NOT EXISTS idx_externalcodes_code ON ExternalCodes(System, Code)',
|
|
437
|
+
'CREATE INDEX IF NOT EXISTS idx_externalmappings_code ON ExternalMappings(CodeKey)',
|
|
438
|
+
'CREATE INDEX IF NOT EXISTS idx_externalmappings_target ON ExternalMappings(ExternalCodeKey)',
|
|
439
|
+
|
|
431
440
|
// Closure table indexes
|
|
432
441
|
'CREATE INDEX IF NOT EXISTS idx_closure_ancestor ON Closure(AncestorKey)',
|
|
433
442
|
'CREATE INDEX IF NOT EXISTS idx_closure_descendent ON Closure(DescendentKey)'
|
|
@@ -506,7 +515,9 @@ class LoincDataMigratorWithProgress {
|
|
|
506
515
|
'AccessoryFiles/AnswerFile/AnswerList.csv',
|
|
507
516
|
'AccessoryFiles/PartFile/LoincPartLink_Primary.csv',
|
|
508
517
|
'AccessoryFiles/AnswerFile/LoincAnswerListLink.csv',
|
|
509
|
-
'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv'
|
|
518
|
+
'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
|
|
519
|
+
'AccessoryFiles/PartFile/PartRelatedCodeMapping.csv',
|
|
520
|
+
'AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable/LoincIeeeMedicalDeviceCodeMappingTable.csv'
|
|
510
521
|
];
|
|
511
522
|
|
|
512
523
|
// Count main files
|
|
@@ -599,6 +610,10 @@ class LoincDataMigrator {
|
|
|
599
610
|
this.props = new Map();
|
|
600
611
|
this.propValues = new Map();
|
|
601
612
|
this.partNames = new Map();
|
|
613
|
+
this.extCodeKey = 0;
|
|
614
|
+
this.extCodes = new Map(); // system|version|code -> ExternalCodeKey
|
|
615
|
+
this.extMapKey = 0;
|
|
616
|
+
this.extMaps = new Set(); // codeKey|ExternalCodeKey
|
|
602
617
|
|
|
603
618
|
// Create tables and initial data
|
|
604
619
|
await this.createTables(db, version, options.verbose);
|
|
@@ -631,6 +646,10 @@ class LoincDataMigrator {
|
|
|
631
646
|
this.updateProgress(0, 'List Links');
|
|
632
647
|
await this.processListLinks(db, sourceDir, 8, options);
|
|
633
648
|
|
|
649
|
+
this.updateProgress(0, 'External Mappings');
|
|
650
|
+
await this.processPartMappings(db, sourceDir, 9, options);
|
|
651
|
+
await this.processIeeeMappings(db, sourceDir, 9, options);
|
|
652
|
+
|
|
634
653
|
this.updateProgress(0, 'Hierarchy');
|
|
635
654
|
await this.processHierarchy(db, sourceDir, 9, options);
|
|
636
655
|
|
|
@@ -719,6 +738,21 @@ class LoincDataMigrator {
|
|
|
719
738
|
DescriptionTypeKey INTEGER NOT NULL,
|
|
720
739
|
Value TEXT NOT NULL
|
|
721
740
|
)`,
|
|
741
|
+
// Codes in other code systems that LOINC codes map to (one row per system|version|code)
|
|
742
|
+
`CREATE TABLE ExternalCodes (
|
|
743
|
+
ExternalCodeKey INTEGER NOT NULL PRIMARY KEY,
|
|
744
|
+
System TEXT NOT NULL,
|
|
745
|
+
Version TEXT NULL,
|
|
746
|
+
Code TEXT NOT NULL,
|
|
747
|
+
Display TEXT NULL
|
|
748
|
+
)`,
|
|
749
|
+
// LOINC code (Codes.CodeKey) -> ExternalCodes, with a ConceptMap equivalence (R4 codes)
|
|
750
|
+
`CREATE TABLE ExternalMappings (
|
|
751
|
+
ExternalMappingKey INTEGER NOT NULL PRIMARY KEY,
|
|
752
|
+
CodeKey INTEGER NOT NULL,
|
|
753
|
+
ExternalCodeKey INTEGER NOT NULL,
|
|
754
|
+
Equivalence TEXT NOT NULL
|
|
755
|
+
)`,
|
|
722
756
|
`CREATE TABLE Closure (
|
|
723
757
|
AncestorKey INTEGER NOT NULL,
|
|
724
758
|
DescendentKey INTEGER NOT NULL,
|
|
@@ -843,10 +877,7 @@ class LoincDataMigrator {
|
|
|
843
877
|
return;
|
|
844
878
|
}
|
|
845
879
|
|
|
846
|
-
const rl =
|
|
847
|
-
input: fs.createReadStream(filePath),
|
|
848
|
-
crlfDelay: Infinity
|
|
849
|
-
});
|
|
880
|
+
const rl = csvRecords(filePath);
|
|
850
881
|
|
|
851
882
|
let lineCount = 0;
|
|
852
883
|
for await (const line of rl) {
|
|
@@ -872,10 +903,7 @@ class LoincDataMigrator {
|
|
|
872
903
|
if (options.verbose) console.log('Processing Parts...');
|
|
873
904
|
|
|
874
905
|
const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/Part.csv');
|
|
875
|
-
const rl =
|
|
876
|
-
input: fs.createReadStream(filePath),
|
|
877
|
-
crlfDelay: Infinity
|
|
878
|
-
});
|
|
906
|
+
const rl = csvRecords(filePath);
|
|
879
907
|
|
|
880
908
|
let lineCount = 0;
|
|
881
909
|
let processedCount = 0;
|
|
@@ -927,10 +955,7 @@ class LoincDataMigrator {
|
|
|
927
955
|
if (options.verbose) console.log('Processing Main Codes...');
|
|
928
956
|
|
|
929
957
|
const filePath = path.join(sourceDir, 'LoincTable/Loinc.csv');
|
|
930
|
-
const rl =
|
|
931
|
-
input: fs.createReadStream(filePath),
|
|
932
|
-
crlfDelay: Infinity
|
|
933
|
-
});
|
|
958
|
+
const rl = csvRecords(filePath);
|
|
934
959
|
|
|
935
960
|
let lineCount = 0;
|
|
936
961
|
let processedCount = 0;
|
|
@@ -1007,10 +1032,7 @@ class LoincDataMigrator {
|
|
|
1007
1032
|
|
|
1008
1033
|
if (options.verbose) console.log('Processing Consumer Names...');
|
|
1009
1034
|
|
|
1010
|
-
const rl =
|
|
1011
|
-
input: fs.createReadStream(filePath),
|
|
1012
|
-
crlfDelay: Infinity
|
|
1013
|
-
});
|
|
1035
|
+
const rl = csvRecords(filePath);
|
|
1014
1036
|
|
|
1015
1037
|
let lineCount = 0;
|
|
1016
1038
|
let processedCount = 0;
|
|
@@ -1045,10 +1067,7 @@ class LoincDataMigrator {
|
|
|
1045
1067
|
|
|
1046
1068
|
if (options.verbose) console.log('Processing Answer Lists...');
|
|
1047
1069
|
|
|
1048
|
-
const rl =
|
|
1049
|
-
input: fs.createReadStream(filePath),
|
|
1050
|
-
crlfDelay: Infinity
|
|
1051
|
-
});
|
|
1070
|
+
const rl = csvRecords(filePath);
|
|
1052
1071
|
|
|
1053
1072
|
let lineCount = 0;
|
|
1054
1073
|
let processedCount = 0;
|
|
@@ -1058,7 +1077,7 @@ class LoincDataMigrator {
|
|
|
1058
1077
|
lineCount++;
|
|
1059
1078
|
if (lineCount === 1) continue;
|
|
1060
1079
|
|
|
1061
|
-
const items = csvSplit(line,
|
|
1080
|
+
const items = csvSplit(line, 15);
|
|
1062
1081
|
if (items.length < 11) continue;
|
|
1063
1082
|
|
|
1064
1083
|
await this.processListItem(db, items, currentList);
|
|
@@ -1111,6 +1130,111 @@ class LoincDataMigrator {
|
|
|
1111
1130
|
|
|
1112
1131
|
this.addRelationship(db, listCodeKey, answerCodeKey, this.rels.get('Answer'));
|
|
1113
1132
|
this.addRelationship(db, answerCodeKey, listCodeKey, this.rels.get('AnswerList'));
|
|
1133
|
+
|
|
1134
|
+
// ExtCodeId (11), ExtCodeDisplayName (12), ExtCodeSystem (13), ExtCodeSystemVersion (14).
|
|
1135
|
+
// The file carries no equivalence for these; an answer and its external code are the same thing
|
|
1136
|
+
if (items[11] && items[13]) {
|
|
1137
|
+
this.addExternalMapping(db, answerCodeKey, items[13], items[14], items[11], items[12], 'equivalent');
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async processPartMappings(db, sourceDir, step, options) {
|
|
1142
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/PartFile/PartRelatedCodeMapping.csv');
|
|
1143
|
+
if (!fs.existsSync(filePath)) {
|
|
1144
|
+
if (options.verbose) console.warn(`Part mapping file not found: ${filePath}`);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (options.verbose) console.log('Processing Part Mappings...');
|
|
1149
|
+
|
|
1150
|
+
const rl = csvRecords(filePath);
|
|
1151
|
+
|
|
1152
|
+
// PartNumber, PartName, PartTypeName, ExtCodeId, ExtCodeDisplayName, ExtCodeSystem,
|
|
1153
|
+
// Equivalence, ContentOrigin, ExtCodeSystemVersion, ExtCodeSystemCopyrightNotice
|
|
1154
|
+
let lineCount = 0;
|
|
1155
|
+
let processedCount = 0;
|
|
1156
|
+
for await (const line of rl) {
|
|
1157
|
+
lineCount++;
|
|
1158
|
+
if (lineCount === 1) continue;
|
|
1159
|
+
|
|
1160
|
+
const items = csvSplit(line, 10);
|
|
1161
|
+
const partCode = items[0];
|
|
1162
|
+
const system = items[5];
|
|
1163
|
+
// RadLex mappings are not simple code-to-code maps; not handled yet
|
|
1164
|
+
if (!items[3] || !system || system === 'http://www.radlex.org' || !this.codes.has(partCode)) continue;
|
|
1165
|
+
|
|
1166
|
+
this.addExternalMapping(db, this.codes.get(partCode).key, system, items[8], items[3], items[4], items[6] || 'equivalent');
|
|
1167
|
+
processedCount++;
|
|
1168
|
+
|
|
1169
|
+
if (processedCount % 100 === 0) {
|
|
1170
|
+
this.updateProgress(100);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const remaining = processedCount % 100;
|
|
1175
|
+
if (remaining > 0) {
|
|
1176
|
+
this.updateProgress(remaining);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
async processIeeeMappings(db, sourceDir, step, options) {
|
|
1181
|
+
const filePath = path.join(sourceDir, 'AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable/LoincIeeeMedicalDeviceCodeMappingTable.csv');
|
|
1182
|
+
if (!fs.existsSync(filePath)) {
|
|
1183
|
+
if (options.verbose) console.warn(`IEEE mapping file not found: ${filePath}`);
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (options.verbose) console.log('Processing IEEE Mappings...');
|
|
1188
|
+
|
|
1189
|
+
const rl = csvRecords(filePath);
|
|
1190
|
+
|
|
1191
|
+
// LOINC_NUM, LOINC_LONG_COMMON_NAME, IEEE_CF_CODE10, IEEE_REFID, EQUIVALENCE
|
|
1192
|
+
// The code is the CF_CODE10 (as used in FHIR, e.g. 150456); the REFID is its display.
|
|
1193
|
+
let lineCount = 0;
|
|
1194
|
+
let processedCount = 0;
|
|
1195
|
+
for await (const line of rl) {
|
|
1196
|
+
lineCount++;
|
|
1197
|
+
if (lineCount === 1) continue;
|
|
1198
|
+
|
|
1199
|
+
const items = csvSplit(line, 5);
|
|
1200
|
+
const loincCode = items[0];
|
|
1201
|
+
if (!items[2] || !this.codes.has(loincCode)) continue;
|
|
1202
|
+
|
|
1203
|
+
this.addExternalMapping(db, this.codes.get(loincCode).key, 'urn:iso:std:iso:11073:10101', null,
|
|
1204
|
+
items[2], items[3], ieeeEquivalence(items[4]));
|
|
1205
|
+
processedCount++;
|
|
1206
|
+
|
|
1207
|
+
if (processedCount % 100 === 0) {
|
|
1208
|
+
this.updateProgress(100);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const remaining = processedCount % 100;
|
|
1213
|
+
if (remaining > 0) {
|
|
1214
|
+
this.updateProgress(remaining);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
addExternalMapping(db, codeKey, system, version, code, display, equivalence) {
|
|
1219
|
+
system = normaliseExternalSystem(system);
|
|
1220
|
+
version = version || null;
|
|
1221
|
+
const targetId = system + '|' + (version || '') + '|' + code;
|
|
1222
|
+
let extKey = this.extCodes.get(targetId);
|
|
1223
|
+
if (!extKey) {
|
|
1224
|
+
this.extCodeKey++;
|
|
1225
|
+
extKey = this.extCodeKey;
|
|
1226
|
+
this.extCodes.set(targetId, extKey);
|
|
1227
|
+
db.run('INSERT INTO ExternalCodes (ExternalCodeKey, System, Version, Code, Display) VALUES (?, ?, ?, ?, ?)',
|
|
1228
|
+
[extKey, system, version, code, display || null]);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// the same answer appears once per answer list it's in
|
|
1232
|
+
const mapId = codeKey + '|' + extKey;
|
|
1233
|
+
if (this.extMaps.has(mapId)) return;
|
|
1234
|
+
this.extMaps.add(mapId);
|
|
1235
|
+
this.extMapKey++;
|
|
1236
|
+
db.run('INSERT INTO ExternalMappings (ExternalMappingKey, CodeKey, ExternalCodeKey, Equivalence) VALUES (?, ?, ?, ?)',
|
|
1237
|
+
[this.extMapKey, codeKey, extKey, equivalence]);
|
|
1114
1238
|
}
|
|
1115
1239
|
|
|
1116
1240
|
async processPartLinks(db, sourceDir, step, options) {
|
|
@@ -1122,10 +1246,7 @@ class LoincDataMigrator {
|
|
|
1122
1246
|
|
|
1123
1247
|
if (options.verbose) console.log('Processing Part Links...');
|
|
1124
1248
|
|
|
1125
|
-
const rl =
|
|
1126
|
-
input: fs.createReadStream(filePath),
|
|
1127
|
-
crlfDelay: Infinity
|
|
1128
|
-
});
|
|
1249
|
+
const rl = csvRecords(filePath);
|
|
1129
1250
|
|
|
1130
1251
|
let lineCount = 0;
|
|
1131
1252
|
let processedCount = 0;
|
|
@@ -1171,10 +1292,7 @@ class LoincDataMigrator {
|
|
|
1171
1292
|
|
|
1172
1293
|
if (options.verbose) console.log('Processing List Links...');
|
|
1173
1294
|
|
|
1174
|
-
const rl =
|
|
1175
|
-
input: fs.createReadStream(filePath),
|
|
1176
|
-
crlfDelay: Infinity
|
|
1177
|
-
});
|
|
1295
|
+
const rl = csvRecords(filePath);
|
|
1178
1296
|
|
|
1179
1297
|
let lineCount = 0;
|
|
1180
1298
|
let processedCount = 0;
|
|
@@ -1227,10 +1345,7 @@ class LoincDataMigrator {
|
|
|
1227
1345
|
|
|
1228
1346
|
if (options.verbose) console.log('Processing Hierarchy...');
|
|
1229
1347
|
|
|
1230
|
-
const rl =
|
|
1231
|
-
input: fs.createReadStream(filePath),
|
|
1232
|
-
crlfDelay: Infinity
|
|
1233
|
-
});
|
|
1348
|
+
const rl = csvRecords(filePath);
|
|
1234
1349
|
|
|
1235
1350
|
let lineCount = 0;
|
|
1236
1351
|
let processedCount = 0;
|
|
@@ -1258,7 +1373,7 @@ class LoincDataMigrator {
|
|
|
1258
1373
|
async processHierarchyItem(db, items) {
|
|
1259
1374
|
const pathCode = items[3];
|
|
1260
1375
|
const parentPath = items[2];
|
|
1261
|
-
const description = items[4];
|
|
1376
|
+
const description = items[4].trim();
|
|
1262
1377
|
|
|
1263
1378
|
if (!this.codes.has(pathCode)) {
|
|
1264
1379
|
this.codeKey++;
|
|
@@ -1339,10 +1454,7 @@ class LoincDataMigrator {
|
|
|
1339
1454
|
return;
|
|
1340
1455
|
}
|
|
1341
1456
|
|
|
1342
|
-
const rl =
|
|
1343
|
-
input: fs.createReadStream(filePath),
|
|
1344
|
-
crlfDelay: Infinity
|
|
1345
|
-
});
|
|
1457
|
+
const rl = csvRecords(filePath);
|
|
1346
1458
|
|
|
1347
1459
|
let lineCount = 0;
|
|
1348
1460
|
let processedCount = 0;
|
|
@@ -1488,6 +1600,29 @@ function adjustPropName(s) {
|
|
|
1488
1600
|
throw new Error(`Unknown Property Name: ${s}`);
|
|
1489
1601
|
}
|
|
1490
1602
|
|
|
1603
|
+
// LOINC names some code systems with something other than their FHIR URI. The others not yet
|
|
1604
|
+
// known ('CMS LCDS', 'Unicode') are left as LOINC has them
|
|
1605
|
+
const EXTERNAL_SYSTEM_URIS = {
|
|
1606
|
+
'CMS/HCPCS': 'http://www.cms.gov/Medicare/Coding/HCPCSReleaseCodeSets',
|
|
1607
|
+
'https://www.cdc.gov/phin': 'urn:oid:2.16.840.1.113883.6.238' // CDC Race & Ethnicity
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
function normaliseExternalSystem(system) {
|
|
1611
|
+
return EXTERNAL_SYSTEM_URIS[system] || system;
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// The IEEE table states equivalence from the LOINC side ('broader' = the LOINC code is broader,
|
|
1615
|
+
// e.g. Glucose in Serum or Plasma -> MDC_CONC_GLU_VENOUS_PLASMA). ConceptMap equivalence is stated
|
|
1616
|
+
// from the target side, so broader/narrower swap to narrower/wider.
|
|
1617
|
+
function ieeeEquivalence(s) {
|
|
1618
|
+
switch (s) {
|
|
1619
|
+
case 'broader': return 'narrower';
|
|
1620
|
+
case 'narrower': return 'wider';
|
|
1621
|
+
case '': return 'equivalent';
|
|
1622
|
+
default: return s;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1491
1626
|
function descClassType(s) {
|
|
1492
1627
|
const types = {
|
|
1493
1628
|
'1': 'Laboratory class',
|
|
@@ -1539,5 +1674,6 @@ function removeQuotes(str) {
|
|
|
1539
1674
|
|
|
1540
1675
|
module.exports = {
|
|
1541
1676
|
LoincModule,
|
|
1542
|
-
LoincDataMigrator
|
|
1677
|
+
LoincDataMigrator,
|
|
1678
|
+
normaliseExternalSystem
|
|
1543
1679
|
};
|
|
@@ -1136,6 +1136,12 @@ class SnomedImporter {
|
|
|
1136
1136
|
// Step 10: Read relationships
|
|
1137
1137
|
await this.readRelationships();
|
|
1138
1138
|
|
|
1139
|
+
// Step 10a: Read alternate identifiers (e.g. the LOINC codes of the LOINC Extension)
|
|
1140
|
+
await this.readIdentifiers();
|
|
1141
|
+
|
|
1142
|
+
// Step 10b: Read concrete value relationships (sct2_RelationshipConcreteValues)
|
|
1143
|
+
await this.readConcreteValues();
|
|
1144
|
+
|
|
1139
1145
|
// Step 11: Link concepts
|
|
1140
1146
|
this.linkConcepts();
|
|
1141
1147
|
|
|
@@ -1165,6 +1171,8 @@ class SnomedImporter {
|
|
|
1165
1171
|
concepts: [],
|
|
1166
1172
|
descriptions: [],
|
|
1167
1173
|
relationships: [],
|
|
1174
|
+
identifiers: [],
|
|
1175
|
+
concreteValues: [],
|
|
1168
1176
|
refsetDirectories: []
|
|
1169
1177
|
};
|
|
1170
1178
|
|
|
@@ -1218,6 +1226,10 @@ class SnomedImporter {
|
|
|
1218
1226
|
} else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tsourceId\tdestinationId\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId') &&
|
|
1219
1227
|
!filePath.includes('StatedRelationship')) {
|
|
1220
1228
|
files.relationships.push(filePath);
|
|
1229
|
+
} else if (files.identifiers && firstLine.startsWith('alternateIdentifier\teffectiveTime\tactive\tmoduleId\tidentifierSchemeId\treferencedComponentId')) {
|
|
1230
|
+
files.identifiers.push(filePath);
|
|
1231
|
+
} else if (files.concreteValues && firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tsourceId\tvalue\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId')) {
|
|
1232
|
+
files.concreteValues.push(filePath);
|
|
1221
1233
|
}
|
|
1222
1234
|
} catch (error) {
|
|
1223
1235
|
console.log(`DEBUG: Error reading file ${filePath}: ${error.message}`);
|
|
@@ -1726,6 +1738,108 @@ class SnomedImporter {
|
|
|
1726
1738
|
return /^\d+$/.test(str);
|
|
1727
1739
|
}
|
|
1728
1740
|
|
|
1741
|
+
/**
|
|
1742
|
+
* Read the alternate identifier files (sct2_Identifier). Each active row gives a
|
|
1743
|
+
* concept a code in another scheme - the LOINC Extension, for instance, gives each
|
|
1744
|
+
* of its concepts its LOINC code (scheme 30051010000102 |LOINC code identifier|).
|
|
1745
|
+
* Kept as [scheme, module, code, conceptId] for the concepts in this edition; where
|
|
1746
|
+
* a row appears more than once (Full files, or base + extension) the latest wins.
|
|
1747
|
+
*/
|
|
1748
|
+
async readIdentifiers() {
|
|
1749
|
+
const rows = new Map(); // scheme|code|concept -> {effectiveTime, active, row}
|
|
1750
|
+
for (const file of this.files.identifiers || []) {
|
|
1751
|
+
const rl = readline.createInterface({
|
|
1752
|
+
input: fs.createReadStream(file),
|
|
1753
|
+
crlfDelay: Infinity
|
|
1754
|
+
});
|
|
1755
|
+
let lineCount = 0;
|
|
1756
|
+
for await (const line of rl) {
|
|
1757
|
+
lineCount++;
|
|
1758
|
+
if (lineCount === 1 || !line) continue;
|
|
1759
|
+
const parts = line.split('\t');
|
|
1760
|
+
if (parts.length < 6) continue;
|
|
1761
|
+
const [code, effectiveTime, active, moduleId, schemeId, conceptId] = parts.map(p => p.trim());
|
|
1762
|
+
const key = schemeId + '|' + code + '|' + conceptId;
|
|
1763
|
+
const prior = rows.get(key);
|
|
1764
|
+
if (!prior || prior.effectiveTime <= effectiveTime) {
|
|
1765
|
+
rows.set(key, { effectiveTime, active: active === '1', row: [schemeId, moduleId, code, conceptId] });
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
this.identifiers = [];
|
|
1770
|
+
let missing = 0;
|
|
1771
|
+
for (const r of rows.values()) {
|
|
1772
|
+
if (!r.active) continue;
|
|
1773
|
+
let known = false;
|
|
1774
|
+
try {
|
|
1775
|
+
known = this.conceptMap.has(BigInt(r.row[3]));
|
|
1776
|
+
} catch {
|
|
1777
|
+
known = false;
|
|
1778
|
+
}
|
|
1779
|
+
if (known) {
|
|
1780
|
+
this.identifiers.push(r.row);
|
|
1781
|
+
} else {
|
|
1782
|
+
missing++;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
this.identifiers.sort((a, b) => (a.join('|') < b.join('|') ? -1 : 1));
|
|
1786
|
+
if (this.identifiers.length > 0 || missing > 0) {
|
|
1787
|
+
console.log(`Alternate identifiers: ${this.identifiers.length}${missing ? ` (${missing} for concepts not in this edition, ignored)` : ''}`);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
/**
|
|
1792
|
+
* Read the concrete value relationship files (sct2_RelationshipConcreteValues_*):
|
|
1793
|
+
* the numeric and string attribute values of concepts (strength, count of base of
|
|
1794
|
+
* active ingredient, ...). Kept as rows [sourceId, typeId, group, value] - the value
|
|
1795
|
+
* as written in RF2 ("#500", "#5.5", "\"text\"", "true") - for the ECL evaluator;
|
|
1796
|
+
* they are not relationships to concepts, so they do not join the relationship
|
|
1797
|
+
* table. Only active, inferred rows; where a row appears more than once the latest
|
|
1798
|
+
* effectiveTime wins.
|
|
1799
|
+
*/
|
|
1800
|
+
async readConcreteValues() {
|
|
1801
|
+
const rows = new Map(); // id -> {effectiveTime, active, row}
|
|
1802
|
+
for (const file of this.files.concreteValues || []) {
|
|
1803
|
+
const rl = readline.createInterface({
|
|
1804
|
+
input: fs.createReadStream(file),
|
|
1805
|
+
crlfDelay: Infinity
|
|
1806
|
+
});
|
|
1807
|
+
let lineCount = 0;
|
|
1808
|
+
for await (const line of rl) {
|
|
1809
|
+
lineCount++;
|
|
1810
|
+
if (lineCount === 1 || !line) continue;
|
|
1811
|
+
const parts = line.split('\t');
|
|
1812
|
+
if (parts.length < 10) continue;
|
|
1813
|
+
const [id, effectiveTime, active, , sourceId, value, group, typeId, characteristicTypeId] = parts.map(p => p.trim());
|
|
1814
|
+
if (characteristicTypeId !== '900000000000011006') continue; // inferred only
|
|
1815
|
+
const prior = rows.get(id);
|
|
1816
|
+
if (!prior || prior.effectiveTime <= effectiveTime) {
|
|
1817
|
+
rows.set(id, { effectiveTime, active: active === '1', row: [sourceId, typeId, parseInt(group, 10) || 0, value] });
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
this.concreteValues = [];
|
|
1822
|
+
let missing = 0;
|
|
1823
|
+
for (const r of rows.values()) {
|
|
1824
|
+
if (!r.active) continue;
|
|
1825
|
+
let known = false;
|
|
1826
|
+
try {
|
|
1827
|
+
known = this.conceptMap.has(BigInt(r.row[0])) && this.conceptMap.has(BigInt(r.row[1]));
|
|
1828
|
+
} catch {
|
|
1829
|
+
known = false;
|
|
1830
|
+
}
|
|
1831
|
+
if (known) {
|
|
1832
|
+
this.concreteValues.push(r.row);
|
|
1833
|
+
} else {
|
|
1834
|
+
missing++;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
this.concreteValues.sort((a, b) => (a.join('|') < b.join('|') ? -1 : 1));
|
|
1838
|
+
if (this.concreteValues.length > 0 || missing > 0) {
|
|
1839
|
+
console.log(`Concrete values: ${this.concreteValues.length}${missing ? ` (${missing} for concepts not in this edition, ignored)` : ''}`);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1729
1843
|
async readRelationships() {
|
|
1730
1844
|
// Estimate total lines
|
|
1731
1845
|
let totalLines = 0;
|
|
@@ -2766,8 +2880,15 @@ class SnomedImporter {
|
|
|
2766
2880
|
|
|
2767
2881
|
progressBar?.update(50);
|
|
2768
2882
|
|
|
2883
|
+
const identifiers = this.identifiers || [];
|
|
2884
|
+
const concreteValues = this.concreteValues || [];
|
|
2769
2885
|
await writer.writeCache({
|
|
2770
|
-
|
|
2886
|
+
// 17 = the base format; 18 = 17 plus the alternate identifiers; 19 = 18 plus
|
|
2887
|
+
// the concrete value relationships. Each is only used when it has something
|
|
2888
|
+
// to carry, so that caches without them stay readable by older servers.
|
|
2889
|
+
version: concreteValues.length > 0 ? '19' : identifiers.length > 0 ? '18' : '17',
|
|
2890
|
+
identifiers: identifiers,
|
|
2891
|
+
concreteValues: concreteValues,
|
|
2771
2892
|
versionUri: this.isTesting ? this.config.uri.replace("/sct/", "/xsct/") : this.config.uri,
|
|
2772
2893
|
versionDate: this.extractDateFromUri(this.config.uri),
|
|
2773
2894
|
|
|
@@ -2854,6 +2975,15 @@ class SnomedCacheWriter {
|
|
|
2854
2975
|
}
|
|
2855
2976
|
buffers.push(this.writeInteger(data.defaultLanguage));
|
|
2856
2977
|
|
|
2978
|
+
if (data.version === '18' || data.version === '19') {
|
|
2979
|
+
// alternate identifiers, as JSON: [[scheme, module, code, conceptId], ...]
|
|
2980
|
+
buffers.push(this.writeBytes(Buffer.from(JSON.stringify(data.identifiers || []), 'utf8')));
|
|
2981
|
+
}
|
|
2982
|
+
if (data.version === '19') {
|
|
2983
|
+
// concrete value relationships, as JSON: [[sourceId, typeId, group, value], ...]
|
|
2984
|
+
buffers.push(this.writeBytes(Buffer.from(JSON.stringify(data.concreteValues || []), 'utf8')));
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2857
2987
|
// Write to file
|
|
2858
2988
|
const finalBuffer = Buffer.concat(buffers);
|
|
2859
2989
|
await fs.promises.writeFile(this.filePath, finalBuffer);
|
|
@@ -2,6 +2,7 @@ const { BaseTerminologyModule } = require('./tx-import-base');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const readline = require('readline');
|
|
5
|
+
const { csvRecords } = require('./csv-records');
|
|
5
6
|
const chalk = require('chalk');
|
|
6
7
|
|
|
7
8
|
class LoincSubsetModule extends BaseTerminologyModule {
|
|
@@ -716,6 +717,16 @@ class LoincSubsetProcessor {
|
|
|
716
717
|
dest: 'AccessoryFiles/ComponentHierarchyBySystem/ComponentHierarchyBySystem.csv',
|
|
717
718
|
handler: 'processHierarchy'
|
|
718
719
|
},
|
|
720
|
+
{
|
|
721
|
+
source: 'AccessoryFiles/PartFile/PartRelatedCodeMapping.csv',
|
|
722
|
+
dest: 'AccessoryFiles/PartFile/PartRelatedCodeMapping.csv',
|
|
723
|
+
handler: 'processFirstColumnCodes'
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
source: 'AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable/LoincIeeeMedicalDeviceCodeMappingTable.csv',
|
|
727
|
+
dest: 'AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable/LoincIeeeMedicalDeviceCodeMappingTable.csv',
|
|
728
|
+
handler: 'processFirstColumnCodes'
|
|
729
|
+
},
|
|
719
730
|
{
|
|
720
731
|
source: 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv',
|
|
721
732
|
dest: 'AccessoryFiles/LinguisticVariants/LinguisticVariants.csv',
|
|
@@ -787,7 +798,8 @@ class LoincSubsetProcessor {
|
|
|
787
798
|
path.join(destDir, 'AccessoryFiles/ConsumerName'),
|
|
788
799
|
path.join(destDir, 'AccessoryFiles/AnswerFile'),
|
|
789
800
|
path.join(destDir, 'AccessoryFiles/ComponentHierarchyBySystem'),
|
|
790
|
-
path.join(destDir, 'AccessoryFiles/LinguisticVariants')
|
|
801
|
+
path.join(destDir, 'AccessoryFiles/LinguisticVariants'),
|
|
802
|
+
path.join(destDir, 'AccessoryFiles/LoincIeeeMedicalDeviceCodeMappingTable')
|
|
791
803
|
];
|
|
792
804
|
|
|
793
805
|
for (const dir of dirs) {
|
|
@@ -879,6 +891,16 @@ class LoincSubsetProcessor {
|
|
|
879
891
|
});
|
|
880
892
|
}
|
|
881
893
|
|
|
894
|
+
// keep the rows whose first column (the LOINC code or part) is in the subset
|
|
895
|
+
async processFirstColumnCodes(sourcePath, destPath) {
|
|
896
|
+
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
897
|
+
if (lineNum === 1) return true; // Keep header
|
|
898
|
+
|
|
899
|
+
const items = this.csvSplit(line, 1);
|
|
900
|
+
return this.targetCodes.has(this.removeQuotes(items[0]));
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
|
|
882
904
|
async processAnswerLists(sourcePath, destPath) {
|
|
883
905
|
await this.processFileWithFilter(sourcePath, destPath, (line, lineNum) => {
|
|
884
906
|
if (lineNum === 1) return true; // Keep header
|
|
@@ -1002,13 +1024,10 @@ class LoincSubsetProcessor {
|
|
|
1002
1024
|
return;
|
|
1003
1025
|
}
|
|
1004
1026
|
|
|
1005
|
-
const readStream = fs.createReadStream(sourcePath);
|
|
1006
1027
|
const writeStream = fs.createWriteStream(destPath);
|
|
1007
1028
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
crlfDelay: Infinity
|
|
1011
|
-
});
|
|
1029
|
+
// records, not lines: a quoted field can hold a line break
|
|
1030
|
+
const rl = csvRecords(sourcePath);
|
|
1012
1031
|
|
|
1013
1032
|
let lineNum = 0;
|
|
1014
1033
|
let includedLines = 0;
|
package/tx/library/codesystem.js
CHANGED
|
@@ -649,7 +649,10 @@ class CodeSystem extends CanonicalResource {
|
|
|
649
649
|
if (this.jsonObj.url === 'https://terminology.dhp.uz/fhir/CodeSystem/loinc-supplement-uz') {
|
|
650
650
|
return true;
|
|
651
651
|
}
|
|
652
|
-
|
|
652
|
+
// a global language pack is used whether or not it is referenced. The code was 'lang-pack'
|
|
653
|
+
// until Oct 2026; it is still accepted, so that supplements marked the old way keep working
|
|
654
|
+
return (this.jsonObj.extension || []).find(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type'
|
|
655
|
+
&& ['global-lang-pack', 'lang-pack'].includes(getValuePrimitive(x)));
|
|
653
656
|
}
|
|
654
657
|
|
|
655
658
|
isPropUri(code, uri) {
|