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
package/tx/cs/cs-loinc.js
CHANGED
|
@@ -7,6 +7,7 @@ const { validateOptionalParameter, validateArrayParameter} = require("../../libr
|
|
|
7
7
|
const {BaseCSServices} = require("./cs-base");
|
|
8
8
|
const {sqlEscapeString} = require("../../xig/xig");
|
|
9
9
|
const regexUtilities = require('../../library/regex-utilities');
|
|
10
|
+
const {ConceptMap} = require("../library/conceptmap");
|
|
10
11
|
|
|
11
12
|
// Context kinds matching Pascal enum
|
|
12
13
|
const LoincProviderContextKind = {
|
|
@@ -16,6 +17,42 @@ const LoincProviderContextKind = {
|
|
|
16
17
|
ANSWER: 3 // lpckAnswer
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
// LOINC's mappings to other code systems (the ExternalCodes / ExternalMappings tables, from
|
|
21
|
+
// PartRelatedCodeMapping, the IEEE mapping table and the answer lists). Each target system
|
|
22
|
+
// has an implicit ConceptMap http://loinc.org/cm/<key>, and its mappings are $lookup
|
|
23
|
+
// properties named <key>. Targets in systems not listed here are not used yet
|
|
24
|
+
const LOINC_MAPS = [
|
|
25
|
+
{ system: 'http://loinc.org', key: 'loinc' },
|
|
26
|
+
{ system: 'http://pubchem.ncbi.nlm.nih.gov', key: 'ncbi' },
|
|
27
|
+
{ system: 'http://snomed.info/sct', key: 'sct' },
|
|
28
|
+
{ system: 'http://www.cms.gov/Medicare/Coding/HCPCSReleaseCodeSets', key: 'hcpcs' },
|
|
29
|
+
{ system: 'http://www.genenames.org', key: 'gene-names' },
|
|
30
|
+
{ system: 'http://www.nlm.nih.gov/research/umls/rxnorm', key: 'rxnorm' },
|
|
31
|
+
{ system: 'https://www.ebi.ac.uk/chebi', key: 'chebi' },
|
|
32
|
+
{ system: 'https://www.ncbi.nlm.nih.gov/clinvar', key: 'ncbi-clinvar' },
|
|
33
|
+
{ system: 'https://www.ncbi.nlm.nih.gov/gene', key: 'ncbi-gene' },
|
|
34
|
+
{ system: 'https://www.ncbi.nlm.nih.gov/taxonomy', key: 'ncbi-taxo' },
|
|
35
|
+
{ system: 'urn:iso:std:iso:11073:10101', key: 'mdc' },
|
|
36
|
+
{ system: 'urn:oid:2.16.840.1.113883.6.238', key: 'race' }
|
|
37
|
+
];
|
|
38
|
+
const LOINC_MAP_BASE = 'http://loinc.org/cm/';
|
|
39
|
+
|
|
40
|
+
// The mapping tables hold R4 equivalences, stated from the LOINC code to the external code.
|
|
41
|
+
// Translations report R5 relationships, from the code being translated to the one found
|
|
42
|
+
function mapRelationship(equivalence, fromLoinc) {
|
|
43
|
+
switch (equivalence) {
|
|
44
|
+
case 'equivalent':
|
|
45
|
+
case 'equal':
|
|
46
|
+
return 'equivalent';
|
|
47
|
+
case 'narrower': // the external code is narrower than the LOINC code
|
|
48
|
+
return fromLoinc ? 'source-is-broader-than-target' : 'source-is-narrower-than-target';
|
|
49
|
+
case 'wider':
|
|
50
|
+
return fromLoinc ? 'source-is-narrower-than-target' : 'source-is-broader-than-target';
|
|
51
|
+
default:
|
|
52
|
+
return 'related-to';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
19
56
|
const classTypes = {
|
|
20
57
|
'1': 'Laboratory class',
|
|
21
58
|
'2': 'Clinical class',
|
|
@@ -127,6 +164,7 @@ class LoincServices extends BaseCSServices {
|
|
|
127
164
|
this.propertyList = sharedData.propertyList;
|
|
128
165
|
this.statusKeys = sharedData.statusKeys;
|
|
129
166
|
this.statusCodes = sharedData.statusCodes;
|
|
167
|
+
this.hasMappings = sharedData.hasMappings;
|
|
130
168
|
}
|
|
131
169
|
|
|
132
170
|
close() {
|
|
@@ -319,10 +357,114 @@ class LoincServices extends BaseCSServices {
|
|
|
319
357
|
this.#addRelationshipProperties(ctxt, props, params),
|
|
320
358
|
this.#addConceptProperties(ctxt, props,params),
|
|
321
359
|
this.#addStatusProperty(ctxt, props,params),
|
|
322
|
-
this.#addRelatedNames(ctxt, props,params)
|
|
360
|
+
this.#addRelatedNames(ctxt, props,params),
|
|
361
|
+
this.#addMappingProperties(ctxt, props, params)
|
|
323
362
|
]);
|
|
324
363
|
}
|
|
325
364
|
|
|
365
|
+
/**
|
|
366
|
+
* The code's mappings to other code systems, as Coding-valued properties named for the
|
|
367
|
+
* map (sct, chebi, mdc...), each with its equivalence as a subproperty. Returned by
|
|
368
|
+
* default, and when asked for by name
|
|
369
|
+
*/
|
|
370
|
+
async #addMappingProperties(ctxt, props, params) {
|
|
371
|
+
if (!this.hasMappings) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const rows = await this.#dbAll(`
|
|
375
|
+
SELECT ExternalCodes.System, ExternalCodes.Version, ExternalCodes.Code, ExternalCodes.Display, ExternalMappings.Equivalence
|
|
376
|
+
FROM ExternalMappings, ExternalCodes
|
|
377
|
+
WHERE ExternalMappings.CodeKey = ?
|
|
378
|
+
AND ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
379
|
+
ORDER BY ExternalCodes.System, ExternalCodes.Code`, [ctxt.key]);
|
|
380
|
+
for (const row of rows) {
|
|
381
|
+
const map = LOINC_MAPS.find(m => m.system === row.System);
|
|
382
|
+
if (map && this._hasProp(props, map.key, true)) {
|
|
383
|
+
const coding = { system: row.System };
|
|
384
|
+
if (row.Version) {
|
|
385
|
+
coding.version = row.Version;
|
|
386
|
+
}
|
|
387
|
+
coding.code = row.Code;
|
|
388
|
+
if (row.Display) {
|
|
389
|
+
coding.display = row.Display;
|
|
390
|
+
}
|
|
391
|
+
params.push({
|
|
392
|
+
name: 'property',
|
|
393
|
+
part: [
|
|
394
|
+
{ name: 'code', valueCode: map.key },
|
|
395
|
+
{ name: 'value', valueCoding: coding },
|
|
396
|
+
{ name: 'subproperty', part: [{ name: 'code', valueCode: 'equivalence' }, { name: 'value', valueCode: row.Equivalence }] }
|
|
397
|
+
]
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
#dbAll(sql, args) {
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
this.db.all(sql, args, (err, rows) => err ? reject(err) : resolve(rows));
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Translate using one of LOINC's implicit maps (http://loinc.org/cm/<key>). The map works
|
|
411
|
+
* in both directions, decided by the system of the code being translated: a LOINC code to
|
|
412
|
+
* the codes it maps to in the map's system, or a code in that system to the LOINC codes
|
|
413
|
+
* that map to it (for the LOINC to LOINC map, always the first). The version of a non-LOINC
|
|
414
|
+
* code is not used: the mappings name one version of each external system
|
|
415
|
+
*/
|
|
416
|
+
async getTranslations(map, coding, target, reverse) { // eslint-disable-line no-unused-vars
|
|
417
|
+
const lm = map && map.jsonObj.loincMap;
|
|
418
|
+
if (!lm || !this.hasMappings) {
|
|
419
|
+
return [];
|
|
420
|
+
}
|
|
421
|
+
const targetSystem = target ? target.split('|')[0] : null;
|
|
422
|
+
const result = [];
|
|
423
|
+
if (coding.system === this.system() && (!targetSystem || targetSystem === lm.system)) {
|
|
424
|
+
const ctxt = this.codes.get(coding.code);
|
|
425
|
+
if (!ctxt) {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
const rows = await this.#dbAll(`
|
|
429
|
+
SELECT ExternalCodes.Version, ExternalCodes.Code, ExternalCodes.Display, ExternalMappings.Equivalence
|
|
430
|
+
FROM ExternalMappings, ExternalCodes
|
|
431
|
+
WHERE ExternalMappings.CodeKey = ?
|
|
432
|
+
AND ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
433
|
+
AND ExternalCodes.System = ?
|
|
434
|
+
ORDER BY ExternalCodes.Code`, [ctxt.key, lm.system]);
|
|
435
|
+
for (const row of rows) {
|
|
436
|
+
const t = { map: map.vurl, system: lm.system, code: row.Code, relationship: mapRelationship(row.Equivalence, true) };
|
|
437
|
+
if (row.Version) {
|
|
438
|
+
t.version = row.Version;
|
|
439
|
+
}
|
|
440
|
+
if (row.Display) {
|
|
441
|
+
t.display = row.Display;
|
|
442
|
+
}
|
|
443
|
+
result.push(t);
|
|
444
|
+
}
|
|
445
|
+
} else if (coding.system === lm.system && (!targetSystem || targetSystem === this.system())) {
|
|
446
|
+
const rows = await this.#dbAll(`
|
|
447
|
+
SELECT Codes.Code, Codes.Description, ExternalMappings.Equivalence
|
|
448
|
+
FROM ExternalCodes, ExternalMappings, Codes
|
|
449
|
+
WHERE ExternalCodes.System = ?
|
|
450
|
+
AND ExternalCodes.Code = ?
|
|
451
|
+
AND ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
452
|
+
AND Codes.CodeKey = ExternalMappings.CodeKey
|
|
453
|
+
ORDER BY Codes.Code`, [lm.system, coding.code]);
|
|
454
|
+
for (const row of rows) {
|
|
455
|
+
result.push({
|
|
456
|
+
map: map.vurl,
|
|
457
|
+
system: this.system(),
|
|
458
|
+
version: this.version(),
|
|
459
|
+
code: row.Code,
|
|
460
|
+
display: row.Description,
|
|
461
|
+
relationship: mapRelationship(row.Equivalence, false)
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
|
|
326
468
|
#getDesignationUse(kind) {
|
|
327
469
|
switch (kind) {
|
|
328
470
|
case LoincProviderContextKind.CODE:
|
|
@@ -350,7 +492,14 @@ class LoincServices extends BaseCSServices {
|
|
|
350
492
|
} else {
|
|
351
493
|
for (const row of rows) {
|
|
352
494
|
if (this._hasProp(props, row.Relationship, true)) {
|
|
353
|
-
|
|
495
|
+
if (row.Relationship === 'CLASS') {
|
|
496
|
+
// CLASS is a string property - the class name (e.g. PULM). The Part code for the
|
|
497
|
+
// class is also returned, as partCode (an informal extension to the property)
|
|
498
|
+
this._addStringProperty(params, 'property', row.Relationship, row.Value || row.Code)
|
|
499
|
+
.part.push({name: 'partCode', valueCode: row.Code});
|
|
500
|
+
} else {
|
|
501
|
+
this._addCodeProperty(params, 'property', row.Relationship, row.Code);
|
|
502
|
+
}
|
|
354
503
|
}
|
|
355
504
|
}
|
|
356
505
|
resolve();
|
|
@@ -1178,6 +1327,9 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1178
1327
|
propertyList: new Map(),
|
|
1179
1328
|
statusKeys: new Map(),
|
|
1180
1329
|
statusCodes: new Map(),
|
|
1330
|
+
hasMappings: false,
|
|
1331
|
+
mappedCodes: new Map(), // LOINC code -> Set of the systems it maps to
|
|
1332
|
+
mappedTargets: new Set(), // system|code of every external code something maps to
|
|
1181
1333
|
_version: '',
|
|
1182
1334
|
root: '',
|
|
1183
1335
|
firstCodeKey: 0
|
|
@@ -1190,11 +1342,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1190
1342
|
this.#loadStatusCodes(db),
|
|
1191
1343
|
this.#loadRelationshipTypes(db),
|
|
1192
1344
|
this.#loadPropertyTypes(db),
|
|
1193
|
-
this.#loadConfig(db)
|
|
1345
|
+
this.#loadConfig(db),
|
|
1346
|
+
this.#loadHasMappings(db)
|
|
1194
1347
|
]);
|
|
1195
1348
|
|
|
1196
1349
|
// Load codes (largest operation)
|
|
1197
1350
|
await this.#loadCodes(db);
|
|
1351
|
+
await this.#loadMappingIndex(db);
|
|
1198
1352
|
|
|
1199
1353
|
// Load dependent data in parallel
|
|
1200
1354
|
await Promise.all([
|
|
@@ -1420,6 +1574,106 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
|
|
|
1420
1574
|
return this._sharedData?._version || 'unknown';
|
|
1421
1575
|
}
|
|
1422
1576
|
|
|
1577
|
+
// the mapping tables were added with the LOINC 2.83 importer; older databases don't have them
|
|
1578
|
+
async #loadHasMappings(db) {
|
|
1579
|
+
return new Promise((resolve, reject) => {
|
|
1580
|
+
db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ExternalMappings'", (err, row) => {
|
|
1581
|
+
if (err) {
|
|
1582
|
+
reject(err);
|
|
1583
|
+
} else {
|
|
1584
|
+
this._sharedData.hasMappings = !!row;
|
|
1585
|
+
resolve();
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// which codes have mappings, so that a map is only offered for a code it says something about
|
|
1592
|
+
async #loadMappingIndex(db) {
|
|
1593
|
+
if (!this._sharedData.hasMappings) {
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
return new Promise((resolve, reject) => {
|
|
1597
|
+
db.all(`SELECT Codes.Code as LoincCode, ExternalCodes.System, ExternalCodes.Code
|
|
1598
|
+
FROM ExternalMappings, ExternalCodes, Codes
|
|
1599
|
+
WHERE ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
|
|
1600
|
+
AND ExternalMappings.CodeKey = Codes.CodeKey`, (err, rows) => {
|
|
1601
|
+
if (err) {
|
|
1602
|
+
reject(err);
|
|
1603
|
+
} else {
|
|
1604
|
+
for (const row of rows) {
|
|
1605
|
+
let systems = this._sharedData.mappedCodes.get(row.LoincCode);
|
|
1606
|
+
if (!systems) {
|
|
1607
|
+
systems = new Set();
|
|
1608
|
+
this._sharedData.mappedCodes.set(row.LoincCode, systems);
|
|
1609
|
+
}
|
|
1610
|
+
systems.add(row.System);
|
|
1611
|
+
this._sharedData.mappedTargets.add(row.System + '|' + row.Code);
|
|
1612
|
+
}
|
|
1613
|
+
resolve();
|
|
1614
|
+
}
|
|
1615
|
+
});
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* LOINC's implicit maps (see LOINC_MAPS): from LOINC to a system it maps to, or from that
|
|
1621
|
+
* system to LOINC. When the code being translated is known, the map is only offered if it
|
|
1622
|
+
* has a mapping for it - so a translation between LOINC and SNOMED CT that the SNOMED CT
|
|
1623
|
+
* edition answers (the LOINC Extension) does not also report a LOINC map that says nothing.
|
|
1624
|
+
* The code may be on either side (under reverse it is the target concept)
|
|
1625
|
+
*/
|
|
1626
|
+
async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest, code = null) { // eslint-disable-line no-unused-vars
|
|
1627
|
+
await this.#ensureLoaded();
|
|
1628
|
+
if (!dest || !this._sharedData.hasMappings) {
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const destSystem = dest.split('|')[0];
|
|
1632
|
+
let other = null;
|
|
1633
|
+
if (source === this.system()) {
|
|
1634
|
+
other = destSystem;
|
|
1635
|
+
} else if (destSystem === this.system()) {
|
|
1636
|
+
other = source;
|
|
1637
|
+
}
|
|
1638
|
+
const lm = LOINC_MAPS.find(m => m.system === other);
|
|
1639
|
+
if (!lm || conceptMaps.some(m => m.url === LOINC_MAP_BASE + lm.key)) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (code && !(this._sharedData.mappedCodes.get(code)?.has(lm.system)) && !this._sharedData.mappedTargets.has(lm.system + '|' + code)) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
conceptMaps.push(this.#loincMap(lm));
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
async findImplicitConceptMap(url, version) {
|
|
1649
|
+
if (!url || !url.startsWith(LOINC_MAP_BASE)) {
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
await this.#ensureLoaded();
|
|
1653
|
+
if ((version && version !== this.version()) || !this._sharedData.hasMappings) {
|
|
1654
|
+
return null;
|
|
1655
|
+
}
|
|
1656
|
+
const lm = LOINC_MAPS.find(m => LOINC_MAP_BASE + m.key === url);
|
|
1657
|
+
return lm ? this.#loincMap(lm) : null;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
#loincMap(lm) {
|
|
1661
|
+
const groups = [{ source: this.system(), target: lm.system }];
|
|
1662
|
+
if (lm.system !== this.system()) {
|
|
1663
|
+
groups.push({ source: lm.system, target: this.system() });
|
|
1664
|
+
}
|
|
1665
|
+
return new ConceptMap({
|
|
1666
|
+
resourceType: 'ConceptMap',
|
|
1667
|
+
internalSource: this,
|
|
1668
|
+
loincMap: lm,
|
|
1669
|
+
url: LOINC_MAP_BASE + lm.key,
|
|
1670
|
+
name: `LOINC ${lm.key} Concept Map`,
|
|
1671
|
+
description: `The mappings between LOINC ${this.version()} and ${lm.system}, as published by LOINC`,
|
|
1672
|
+
status: 'active',
|
|
1673
|
+
group: groups
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1423
1677
|
async build(opContext, supplements) {
|
|
1424
1678
|
await this.#ensureLoaded();
|
|
1425
1679
|
this.recordUse();
|