fhirsmith 0.17.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +80 -13
  3. package/config-template.json +106 -1
  4. package/ips/ai-client.js +263 -0
  5. package/ips/composer.js +805 -0
  6. package/ips/editor-schema.js +289 -0
  7. package/ips/editor.js +290 -0
  8. package/ips/fhirpath-ips.js +397 -0
  9. package/ips/ips-codeable-concepts.md +1742 -0
  10. package/ips/ips-template.html +125 -0
  11. package/ips/ips-valuesets.js +159 -0
  12. package/ips/ips.js +1736 -0
  13. package/ips/membership.js +179 -0
  14. package/ips/pipeline.js +1012 -0
  15. package/ips/presenter.js +974 -0
  16. package/ips/profile-editor.js +578 -0
  17. package/ips/profiles.js +506 -0
  18. package/ips/readme.md +460 -0
  19. package/ips/render.js +1482 -0
  20. package/ips/search-params.js +83 -0
  21. package/ips/search.js +667 -0
  22. package/ips/share.js +178 -0
  23. package/ips/store.js +650 -0
  24. package/ips/terminology.js +395 -0
  25. package/ips/tx-client.js +202 -0
  26. package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
  27. package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
  28. package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
  29. package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
  30. package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
  31. package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
  32. package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
  33. package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
  34. package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
  35. package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
  36. package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
  37. package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
  38. package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
  39. package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
  40. package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
  41. package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
  42. package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
  43. package/ips/valuesets/ValueSet-ips-medication.json +50 -0
  44. package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
  45. package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
  46. package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
  47. package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
  48. package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
  49. package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
  50. package/ips/valuesets/ValueSet-ips-route.json +46 -0
  51. package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
  52. package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
  53. package/ips/valuesets/elements.json +44 -0
  54. package/library/daily-report.js +432 -0
  55. package/library/fhir-search-values.js +138 -0
  56. package/library/html-server.js +32 -23
  57. package/library/mailer.js +77 -0
  58. package/library/narrative-check.js +21 -2
  59. package/library/npm-audit.js +25 -0
  60. package/library/package-manager.js +4 -2
  61. package/library/sqlite-session-store.js +29 -1
  62. package/package.json +8 -3
  63. package/packages/packages.js +34 -3
  64. package/publisher/publisher.js +29 -0
  65. package/registry/auth-store.js +366 -0
  66. package/registry/auth.js +872 -0
  67. package/registry/crawler.js +41 -1
  68. package/registry/documentation/api.md +162 -0
  69. package/registry/documentation/configuration.md +93 -0
  70. package/registry/documentation/documents.md +74 -0
  71. package/registry/documentation/internals.md +151 -0
  72. package/registry/documentation/readme.md +100 -0
  73. package/registry/documentation/security.md +199 -0
  74. package/registry/documents.js +309 -0
  75. package/registry/openapi.yaml +30 -0
  76. package/registry/registry-template.html +1 -1
  77. package/registry/registry.js +230 -44
  78. package/registry/uptime.js +154 -0
  79. package/server.js +105 -44
  80. package/shl/shl.js +64 -9
  81. package/stats-db.js +93 -0
  82. package/stats.js +158 -1
  83. package/testing/search.js +2 -108
  84. package/testing/store.js +5 -0
  85. package/testing/testing.js +20 -0
  86. package/token/token-template.html +5 -4
  87. package/token/token.js +17 -10
  88. package/translations/Messages.properties +10 -0
  89. package/tx/README.md +52 -0
  90. package/tx/closure/closure-store.js +14 -0
  91. package/tx/cs/cs-api.js +92 -14
  92. package/tx/cs/cs-cs.js +64 -3
  93. package/tx/cs/cs-icd11.js +366 -112
  94. package/tx/cs/cs-icf.js +431 -0
  95. package/tx/cs/cs-loinc.js +296 -27
  96. package/tx/cs/cs-omop.js +10 -3
  97. package/tx/cs/cs-snomed.js +1210 -80
  98. package/tx/cs/cs-ucum.js +6 -0
  99. package/tx/cs/icd11-expressions.js +191 -78
  100. package/tx/importers/csv-records.js +35 -0
  101. package/tx/importers/export-icd11-codesystem.js +58 -12
  102. package/tx/importers/icd11-schema.md +9 -5
  103. package/tx/importers/import-loinc.module.js +177 -41
  104. package/tx/importers/import-sct.module.js +66 -1
  105. package/tx/importers/subset-loinc.module.js +25 -6
  106. package/tx/library/codesystem.js +4 -1
  107. package/tx/library/designations.js +23 -30
  108. package/tx/library.js +98 -4
  109. package/tx/openapi-operations.js +6 -1
  110. package/tx/params.js +8 -1
  111. package/tx/provider.js +24 -6
  112. package/tx/sct/documentation.md +251 -0
  113. package/tx/sct/ecl.js +758 -46
  114. package/tx/sct/expressions.js +113 -52
  115. package/tx/sct/structures.js +8 -2
  116. package/tx/syndication/syndication-feed.js +218 -0
  117. package/tx/syndication/syndication-store.js +415 -0
  118. package/tx/tx-html.js +15 -1
  119. package/tx/tx.fhir.org.yml +3 -4
  120. package/tx/tx.js +144 -2
  121. package/tx/vs/vs-vsac.js +25 -6
  122. package/tx/workers/closure.js +20 -0
  123. package/tx/workers/expand.js +35 -8
  124. package/tx/workers/lookup.js +61 -6
  125. package/tx/workers/subsumes.js +1 -1
  126. package/tx/workers/translate.js +166 -15
  127. package/tx/workers/validate.js +138 -6
  128. package/utilities/make-sct-loinc-subset.js +250 -0
  129. package/utilities/make-sct-test-extension.js +264 -0
  130. package/xig/xig.js +32 -0
  131. package/registry/readme.md +0 -124
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() {
@@ -149,6 +187,12 @@ class LoincServices extends BaseCSServices {
149
187
  return 'LOINC';
150
188
  }
151
189
 
190
+ /** $lookup's display name: the code system's title. */
191
+ // eslint-disable-next-line no-unused-vars
192
+ lookupName(languages) {
193
+ return 'Logical Observation Identifier Names and Codes (LOINC)';
194
+ }
195
+
152
196
  description() {
153
197
  return 'LOINC';
154
198
  }
@@ -313,10 +357,114 @@ class LoincServices extends BaseCSServices {
313
357
  this.#addRelationshipProperties(ctxt, props, params),
314
358
  this.#addConceptProperties(ctxt, props,params),
315
359
  this.#addStatusProperty(ctxt, props,params),
316
- this.#addRelatedNames(ctxt, props,params)
360
+ this.#addRelatedNames(ctxt, props,params),
361
+ this.#addMappingProperties(ctxt, props, params)
317
362
  ]);
318
363
  }
319
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
+
320
468
  #getDesignationUse(kind) {
321
469
  switch (kind) {
322
470
  case LoincProviderContextKind.CODE:
@@ -607,12 +755,18 @@ class LoincServices extends BaseCSServices {
607
755
  // Filter support
608
756
  async doesFilter(prop, op, value) {
609
757
  // Relationship filters
610
- if (this.relationships.has(prop) && ['=', 'in', 'exists', 'regex'].includes(op)) {
758
+ if (this.relationships.has(prop) && ['=', 'in', 'regex'].includes(op)) {
759
+ return true;
760
+ }
761
+ if (this.relationships.has(prop) && op === 'exists' && ['true', 'false'].includes(value)) {
611
762
  return true;
612
763
  }
613
764
 
614
765
  // Property filters
615
- if (this.propertyList.has(prop) && ['=', 'in', 'exists', 'regex'].includes(op)) {
766
+ if (this.propertyList.has(prop) && ['=', 'in', 'regex'].includes(op)) {
767
+ return true;
768
+ }
769
+ if (this.propertyList.has(prop) && op === 'exists' && ['true', 'false'].includes(value)) {
616
770
  return true;
617
771
  }
618
772
 
@@ -748,26 +902,26 @@ class LoincServices extends BaseCSServices {
748
902
  AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))
749
903
  AND SourceKey = `;
750
904
  }
751
- // Relationship 'exists' filter
905
+ // Relationship 'exists' filter: value is true or false (doesFilter rejects anything else).
906
+ // true = codes that have at least one relationship of this type; false = every other code
907
+ // in the code system (Parts, AnswerLists and Answers included - combine with e.g.
908
+ // CLASSTYPE exists true to restrict to LOINC terms)
752
909
  else if (this.relationships.has(prop) && op === 'exists') {
753
- if (this.codes.has(value)) {
754
- sql = `SELECT DISTINCT SourceKey as Key FROM Relationships
755
- WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
756
- AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
757
- ORDER BY SourceKey ASC`;
758
- lsql = `SELECT COUNT(DISTINCT SourceKey) FROM Relationships
759
- WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
760
- AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
761
- AND SourceKey = `;
762
- } else {
910
+ const rk = this.relationships.get(prop);
911
+ if (value === 'true') {
763
912
  sql = `SELECT DISTINCT SourceKey as Key FROM Relationships
764
- WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
765
- AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
913
+ WHERE RelationshipTypeKey = ${rk}
766
914
  ORDER BY SourceKey ASC`;
767
- lsql = `SELECT COUNT(DISTINCT SourceKey) FROM Relationships
768
- WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
769
- AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
915
+ lsql = `SELECT COUNT(SourceKey) FROM Relationships
916
+ WHERE RelationshipTypeKey = ${rk}
770
917
  AND SourceKey = `;
918
+ } else if (value === 'false') {
919
+ sql = `SELECT CodeKey as Key FROM Codes
920
+ WHERE NOT EXISTS (SELECT 1 FROM Relationships r WHERE r.RelationshipTypeKey = ${rk} AND r.SourceKey = Codes.CodeKey)
921
+ ORDER BY CodeKey ASC`;
922
+ lsql = `SELECT COUNT(CodeKey) FROM Codes
923
+ WHERE NOT EXISTS (SELECT 1 FROM Relationships r WHERE r.RelationshipTypeKey = ${rk} AND r.SourceKey = Codes.CodeKey)
924
+ AND CodeKey = `;
771
925
  }
772
926
  }
773
927
  // Relationship regex filter
@@ -820,14 +974,24 @@ class LoincServices extends BaseCSServices {
820
974
  AND PropertyValues.Value IN (${codes}) COLLATE NOCASE
821
975
  AND CodeKey = `;
822
976
  }
823
- // Property 'exists' filter
977
+ // Property 'exists' filter: value is true or false, as for relationships above
824
978
  else if (this.propertyList.has(prop) && op === 'exists') {
825
- sql = `SELECT DISTINCT CodeKey as Key FROM Properties
826
- WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
827
- ORDER BY CodeKey ASC`;
828
- lsql = `SELECT COUNT(CodeKey) FROM Properties
829
- WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
830
- AND CodeKey = `;
979
+ const pk = this.propertyList.get(prop);
980
+ if (value === 'true') {
981
+ sql = `SELECT DISTINCT CodeKey as Key FROM Properties
982
+ WHERE Properties.PropertyTypeKey = ${pk}
983
+ ORDER BY CodeKey ASC`;
984
+ lsql = `SELECT COUNT(CodeKey) FROM Properties
985
+ WHERE Properties.PropertyTypeKey = ${pk}
986
+ AND CodeKey = `;
987
+ } else if (value === 'false') {
988
+ sql = `SELECT CodeKey as Key FROM Codes
989
+ WHERE NOT EXISTS (SELECT 1 FROM Properties p WHERE p.PropertyTypeKey = ${pk} AND p.CodeKey = Codes.CodeKey)
990
+ ORDER BY CodeKey ASC`;
991
+ lsql = `SELECT COUNT(CodeKey) FROM Codes
992
+ WHERE NOT EXISTS (SELECT 1 FROM Properties p WHERE p.PropertyTypeKey = ${pk} AND p.CodeKey = Codes.CodeKey)
993
+ AND CodeKey = `;
994
+ }
831
995
  }
832
996
  // Property regex filter
833
997
  else if (this.propertyList.has(prop) && op === 'regex') {
@@ -1156,6 +1320,9 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1156
1320
  propertyList: new Map(),
1157
1321
  statusKeys: new Map(),
1158
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
1159
1326
  _version: '',
1160
1327
  root: '',
1161
1328
  firstCodeKey: 0
@@ -1168,11 +1335,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1168
1335
  this.#loadStatusCodes(db),
1169
1336
  this.#loadRelationshipTypes(db),
1170
1337
  this.#loadPropertyTypes(db),
1171
- this.#loadConfig(db)
1338
+ this.#loadConfig(db),
1339
+ this.#loadHasMappings(db)
1172
1340
  ]);
1173
1341
 
1174
1342
  // Load codes (largest operation)
1175
1343
  await this.#loadCodes(db);
1344
+ await this.#loadMappingIndex(db);
1176
1345
 
1177
1346
  // Load dependent data in parallel
1178
1347
  await Promise.all([
@@ -1398,6 +1567,106 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1398
1567
  return this._sharedData?._version || 'unknown';
1399
1568
  }
1400
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
+
1401
1670
  async build(opContext, supplements) {
1402
1671
  await this.#ensureLoaded();
1403
1672
  this.recordUse();
package/tx/cs/cs-omop.js CHANGED
@@ -151,7 +151,13 @@ class OMOPServices extends BaseCSServices {
151
151
  }
152
152
 
153
153
  name() {
154
- return `OMOP Concepts`;
154
+ return 'OMOP_Concepts';
155
+ }
156
+
157
+ /** $lookup's display name: the code system's title. */
158
+ // eslint-disable-next-line no-unused-vars
159
+ lookupName(languages) {
160
+ return 'OMOP Unified Vocabularies';
155
161
  }
156
162
 
157
163
  description() {
@@ -952,7 +958,8 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider {
952
958
  * @param version
953
959
  * @returns {ConceptMap}
954
960
  */
955
- async findImplicitConceptMaps(conceptMaps, source, dest) {
961
+ // eslint-disable-next-line no-unused-vars
962
+ async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest) {
956
963
  if (source == 'https://fhir-terminology.ohdsi.org') {
957
964
  let key = this.#getVocabId(dest);
958
965
  if (key) {
@@ -1007,7 +1014,7 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider {
1007
1014
 
1008
1015
 
1009
1016
  name() {
1010
- return `OMOP Concepts`;
1017
+ return 'OMOP_Concepts';
1011
1018
  }
1012
1019
 
1013
1020
  id() {