fhirsmith 0.18.0 → 0.18.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/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:
@@ -1178,6 +1320,9 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1178
1320
  propertyList: new Map(),
1179
1321
  statusKeys: new Map(),
1180
1322
  statusCodes: new Map(),
1323
+ hasMappings: false,
1324
+ mappedCodes: new Map(), // LOINC code -> Set of the systems it maps to
1325
+ mappedTargets: new Set(), // system|code of every external code something maps to
1181
1326
  _version: '',
1182
1327
  root: '',
1183
1328
  firstCodeKey: 0
@@ -1190,11 +1335,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1190
1335
  this.#loadStatusCodes(db),
1191
1336
  this.#loadRelationshipTypes(db),
1192
1337
  this.#loadPropertyTypes(db),
1193
- this.#loadConfig(db)
1338
+ this.#loadConfig(db),
1339
+ this.#loadHasMappings(db)
1194
1340
  ]);
1195
1341
 
1196
1342
  // Load codes (largest operation)
1197
1343
  await this.#loadCodes(db);
1344
+ await this.#loadMappingIndex(db);
1198
1345
 
1199
1346
  // Load dependent data in parallel
1200
1347
  await Promise.all([
@@ -1420,6 +1567,106 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1420
1567
  return this._sharedData?._version || 'unknown';
1421
1568
  }
1422
1569
 
1570
+ // the mapping tables were added with the LOINC 2.83 importer; older databases don't have them
1571
+ async #loadHasMappings(db) {
1572
+ return new Promise((resolve, reject) => {
1573
+ db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ExternalMappings'", (err, row) => {
1574
+ if (err) {
1575
+ reject(err);
1576
+ } else {
1577
+ this._sharedData.hasMappings = !!row;
1578
+ resolve();
1579
+ }
1580
+ });
1581
+ });
1582
+ }
1583
+
1584
+ // which codes have mappings, so that a map is only offered for a code it says something about
1585
+ async #loadMappingIndex(db) {
1586
+ if (!this._sharedData.hasMappings) {
1587
+ return;
1588
+ }
1589
+ return new Promise((resolve, reject) => {
1590
+ db.all(`SELECT Codes.Code as LoincCode, ExternalCodes.System, ExternalCodes.Code
1591
+ FROM ExternalMappings, ExternalCodes, Codes
1592
+ WHERE ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
1593
+ AND ExternalMappings.CodeKey = Codes.CodeKey`, (err, rows) => {
1594
+ if (err) {
1595
+ reject(err);
1596
+ } else {
1597
+ for (const row of rows) {
1598
+ let systems = this._sharedData.mappedCodes.get(row.LoincCode);
1599
+ if (!systems) {
1600
+ systems = new Set();
1601
+ this._sharedData.mappedCodes.set(row.LoincCode, systems);
1602
+ }
1603
+ systems.add(row.System);
1604
+ this._sharedData.mappedTargets.add(row.System + '|' + row.Code);
1605
+ }
1606
+ resolve();
1607
+ }
1608
+ });
1609
+ });
1610
+ }
1611
+
1612
+ /**
1613
+ * LOINC's implicit maps (see LOINC_MAPS): from LOINC to a system it maps to, or from that
1614
+ * system to LOINC. When the code being translated is known, the map is only offered if it
1615
+ * has a mapping for it - so a translation between LOINC and SNOMED CT that the SNOMED CT
1616
+ * edition answers (the LOINC Extension) does not also report a LOINC map that says nothing.
1617
+ * The code may be on either side (under reverse it is the target concept)
1618
+ */
1619
+ async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest, code = null) { // eslint-disable-line no-unused-vars
1620
+ await this.#ensureLoaded();
1621
+ if (!dest || !this._sharedData.hasMappings) {
1622
+ return;
1623
+ }
1624
+ const destSystem = dest.split('|')[0];
1625
+ let other = null;
1626
+ if (source === this.system()) {
1627
+ other = destSystem;
1628
+ } else if (destSystem === this.system()) {
1629
+ other = source;
1630
+ }
1631
+ const lm = LOINC_MAPS.find(m => m.system === other);
1632
+ if (!lm || conceptMaps.some(m => m.url === LOINC_MAP_BASE + lm.key)) {
1633
+ return;
1634
+ }
1635
+ if (code && !(this._sharedData.mappedCodes.get(code)?.has(lm.system)) && !this._sharedData.mappedTargets.has(lm.system + '|' + code)) {
1636
+ return;
1637
+ }
1638
+ conceptMaps.push(this.#loincMap(lm));
1639
+ }
1640
+
1641
+ async findImplicitConceptMap(url, version) {
1642
+ if (!url || !url.startsWith(LOINC_MAP_BASE)) {
1643
+ return null;
1644
+ }
1645
+ await this.#ensureLoaded();
1646
+ if ((version && version !== this.version()) || !this._sharedData.hasMappings) {
1647
+ return null;
1648
+ }
1649
+ const lm = LOINC_MAPS.find(m => LOINC_MAP_BASE + m.key === url);
1650
+ return lm ? this.#loincMap(lm) : null;
1651
+ }
1652
+
1653
+ #loincMap(lm) {
1654
+ const groups = [{ source: this.system(), target: lm.system }];
1655
+ if (lm.system !== this.system()) {
1656
+ groups.push({ source: lm.system, target: this.system() });
1657
+ }
1658
+ return new ConceptMap({
1659
+ resourceType: 'ConceptMap',
1660
+ internalSource: this,
1661
+ loincMap: lm,
1662
+ url: LOINC_MAP_BASE + lm.key,
1663
+ name: `LOINC ${lm.key} Concept Map`,
1664
+ description: `The mappings between LOINC ${this.version()} and ${lm.system}, as published by LOINC`,
1665
+ status: 'active',
1666
+ group: groups
1667
+ });
1668
+ }
1669
+
1423
1670
  async build(opContext, supplements) {
1424
1671
  await this.#ensureLoaded();
1425
1672
  this.recordUse();