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.
Files changed (47) hide show
  1. package/CHANGELOG.md +72 -10
  2. package/ips/ips.js +251 -26
  3. package/ips/jurisdictions.js +65 -0
  4. package/ips/pipeline.js +67 -6
  5. package/ips/profile-editor.js +283 -4
  6. package/ips/readme.md +48 -5
  7. package/ips/render.js +317 -46
  8. package/ips/scripts.js +46 -0
  9. package/ips/store.js +55 -0
  10. package/library/daily-report.js +25 -15
  11. package/package.json +3 -3
  12. package/registry/api.js +179 -1
  13. package/registry/crawler.js +2 -0
  14. package/registry/documentation/api.md +68 -2
  15. package/registry/documentation/internals.md +4 -1
  16. package/registry/documentation/readme.md +3 -2
  17. package/registry/model.js +8 -0
  18. package/registry/openapi.yaml +84 -0
  19. package/registry/registry.js +53 -2
  20. package/translations/Messages.properties +1 -0
  21. package/tx/README.md +4 -0
  22. package/tx/closure/closure-store.js +14 -0
  23. package/tx/cm/cm-package.js +21 -15
  24. package/tx/cs/cs-api.js +3 -1
  25. package/tx/cs/cs-cs.js +59 -12
  26. package/tx/cs/cs-icf.js +47 -34
  27. package/tx/cs/cs-loinc.js +257 -3
  28. package/tx/cs/cs-snomed.js +544 -55
  29. package/tx/importers/csv-records.js +35 -0
  30. package/tx/importers/import-loinc.module.js +177 -41
  31. package/tx/importers/import-sct.module.js +131 -1
  32. package/tx/importers/subset-loinc.module.js +25 -6
  33. package/tx/library/codesystem.js +4 -1
  34. package/tx/library/designations.js +23 -30
  35. package/tx/library/tho-precedence.js +40 -0
  36. package/tx/openapi-operations.js +2 -1
  37. package/tx/provider.js +46 -29
  38. package/tx/sct/documentation.md +45 -21
  39. package/tx/sct/ecl.js +34 -11
  40. package/tx/sct/structures.js +15 -2
  41. package/tx/tx.fhir.org.yml +2 -2
  42. package/tx/vs/vs-package.js +17 -13
  43. package/tx/vs/vs-vsac.js +1 -1
  44. package/tx/workers/closure.js +20 -0
  45. package/tx/workers/expand.js +2 -2
  46. package/tx/workers/validate.js +10 -0
  47. 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
- this._addCodeProperty(params, 'property', row.Relationship, row.Code);
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();