fhirsmith 0.18.1 → 1.0.0

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 (214) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +190 -9
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +331 -33
  15. package/ips/jurisdictions.js +65 -0
  16. package/ips/nationality.js +120 -0
  17. package/ips/pipeline.js +131 -17
  18. package/ips/presenter.js +1 -1
  19. package/ips/profile-editor.js +283 -4
  20. package/ips/readme.md +170 -20
  21. package/ips/render.js +769 -109
  22. package/ips/scripts.js +46 -0
  23. package/ips/store.js +123 -11
  24. package/ips/structure.js +247 -0
  25. package/ips/terminology.js +1 -1
  26. package/library/body-limit.js +22 -0
  27. package/library/cache-generation.js +15 -0
  28. package/library/cgroup-memory.js +133 -1
  29. package/library/cluster-counts.js +121 -0
  30. package/library/cluster-events.js +153 -0
  31. package/library/cluster-metrics.js +193 -0
  32. package/library/cluster-status.js +468 -0
  33. package/library/config-reader.js +96 -0
  34. package/library/daily-report.js +100 -2
  35. package/library/html-server.js +27 -1
  36. package/library/i18nsupport.js +23 -1
  37. package/library/keep-alive.js +18 -0
  38. package/library/languages.js +17 -3
  39. package/library/logger.js +31 -0
  40. package/library/memory-guard.js +428 -0
  41. package/library/native-memory.js +52 -0
  42. package/library/npm-audit.js +51 -25
  43. package/library/openapi-doc.js +7 -5
  44. package/library/package-manager.js +8 -0
  45. package/library/role.js +197 -0
  46. package/library/server-credentials.js +169 -0
  47. package/library/sqlite-utilities.js +59 -0
  48. package/library/ssrf-guard.js +100 -0
  49. package/library/static-response-stats.js +16 -0
  50. package/library/version-utilities.js +50 -0
  51. package/library/worker-count.js +192 -0
  52. package/nginx.md +30 -11
  53. package/npmprojector/indexer.js +18 -10
  54. package/package.json +11 -8
  55. package/packages/feed-check-page.js +117 -0
  56. package/packages/feed-check.js +279 -0
  57. package/packages/npm-check.js +57 -3
  58. package/packages/npm-signing.js +219 -0
  59. package/packages/openapi.yaml +207 -1
  60. package/packages/package-crawler.js +322 -41
  61. package/packages/packages-template.html +6 -8
  62. package/packages/packages.js +604 -57
  63. package/packages/readme.md +113 -1
  64. package/publisher/publisher-template.html +2 -5
  65. package/publisher/publisher.js +121 -110
  66. package/publisher/readme.md +75 -0
  67. package/registry/api.js +179 -1
  68. package/registry/crawler.js +86 -108
  69. package/registry/documentation/api.md +68 -2
  70. package/registry/documentation/configuration.md +34 -0
  71. package/registry/documentation/internals.md +4 -1
  72. package/registry/documentation/readme.md +3 -2
  73. package/registry/model.js +8 -0
  74. package/registry/openapi.yaml +84 -0
  75. package/registry/registry-template.html +5 -8
  76. package/registry/registry.js +58 -6
  77. package/root-bare-template.html +7 -4
  78. package/root-template.html +5 -8
  79. package/server.js +398 -198
  80. package/shl/shl.js +38 -37
  81. package/static/a11y/a11y-check.js +832 -0
  82. package/static/a11y/axe.min.js +12 -0
  83. package/static/assets/css/bootstrap-fhir.css +15 -15
  84. package/static/assets/images/flags/au.svg +18 -0
  85. package/static/assets/images/flags/unknown.svg +11 -0
  86. package/static/assets/images/flags/us.svg +63 -0
  87. package/static/assets/images/icon_audit.png +0 -0
  88. package/static/assets/images/icon_clinical.png +0 -0
  89. package/static/assets/images/icon_document.png +0 -0
  90. package/static/assets/images/icon_external.png +0 -0
  91. package/static/assets/images/icon_medication.png +0 -0
  92. package/static/assets/images/icon_organization.png +0 -0
  93. package/static/assets/images/icon_other.png +0 -0
  94. package/static/assets/images/icon_person.png +0 -0
  95. package/static/assets/images/icon_section.png +0 -0
  96. package/stats-db.js +45 -0
  97. package/stats.js +197 -10
  98. package/testing/testing-template.html +5 -8
  99. package/token/nginx_passport_setup.md +1 -1
  100. package/token/security_guide.md +1 -1
  101. package/token/token-template.html +2 -5
  102. package/token/token.js +40 -36
  103. package/translations/Messages.properties +19 -0
  104. package/tx/README.md +158 -4
  105. package/tx/closure/closure-store.js +126 -3
  106. package/tx/cm/cm-database.js +69 -57
  107. package/tx/cm/cm-package.js +50 -16
  108. package/tx/cs/cs-api.js +284 -19
  109. package/tx/cs/cs-api.md +41 -4
  110. package/tx/cs/cs-areacode.js +1 -1
  111. package/tx/cs/cs-country.js +17 -5
  112. package/tx/cs/cs-cpt.js +33 -16
  113. package/tx/cs/cs-cs.js +199 -24
  114. package/tx/cs/cs-currency.js +1 -1
  115. package/tx/cs/cs-icd11.js +21 -13
  116. package/tx/cs/cs-icf.js +22 -3
  117. package/tx/cs/cs-lang.js +2 -2
  118. package/tx/cs/cs-loinc.js +193 -67
  119. package/tx/cs/cs-mimetypes.js +12 -2
  120. package/tx/cs/cs-ndc.js +40 -25
  121. package/tx/cs/cs-omop.js +54 -29
  122. package/tx/cs/cs-rxnorm.js +300 -196
  123. package/tx/cs/cs-snomed.js +861 -95
  124. package/tx/cs/cs-ucum.js +14 -4
  125. package/tx/cs/cs-unii.js +18 -14
  126. package/tx/cs/cs-virtual.js +210 -0
  127. package/tx/ecosystem-resolver.js +413 -0
  128. package/tx/html/cluster-dash-metrics.liquid +26 -0
  129. package/tx/html/cluster-home-metrics.liquid +31 -0
  130. package/tx/html/cluster-metrics-script.liquid +251 -0
  131. package/tx/html/codesystem-operations.liquid +3 -3
  132. package/tx/html/ecl-panel.liquid +1 -1
  133. package/tx/html/external-metrics.liquid +17 -0
  134. package/tx/html/operations-form.liquid +17 -17
  135. package/tx/html/search-form.liquid +11 -11
  136. package/tx/html/status-line.liquid +58 -0
  137. package/tx/html/status-panel.liquid +140 -0
  138. package/tx/html/status-tasks.liquid +25 -0
  139. package/tx/html/tx-template.html +2 -5
  140. package/tx/importers/atc-to-fhir.js +17 -16
  141. package/tx/importers/export-icd11-codesystem.js +3 -2
  142. package/tx/importers/import-icd11.js +4 -3
  143. package/tx/importers/import-loinc.module.js +118 -56
  144. package/tx/importers/import-ndc.module.js +56 -44
  145. package/tx/importers/import-rxnorm.module.js +118 -79
  146. package/tx/importers/import-sct.module.js +125 -61
  147. package/tx/importers/import-unii.module.js +37 -36
  148. package/tx/importers/subset-loinc.module.js +49 -48
  149. package/tx/importers/subset-rxnorm.module.js +29 -28
  150. package/tx/importers/terminal.js +18 -0
  151. package/tx/importers/tx-import-base.js +13 -12
  152. package/tx/importers/tx-import-settings.js +5 -4
  153. package/tx/importers/tx-import.js +53 -52
  154. package/tx/library/canonical-resource.js +49 -0
  155. package/tx/library/codesystem.js +94 -5
  156. package/tx/library/designations.js +45 -2
  157. package/tx/library/extensions.js +13 -7
  158. package/tx/library/implicit-valuesets.js +66 -0
  159. package/tx/library/renderer.js +77 -21
  160. package/tx/library/sqlite-utilities.js +2 -0
  161. package/tx/library/tho-precedence.js +40 -0
  162. package/tx/library/ucum-parsers.js +11 -1
  163. package/tx/library/wait-for-cache.js +43 -0
  164. package/tx/library.js +321 -22
  165. package/tx/openapi-operations.js +6 -4
  166. package/tx/operation-context.js +460 -99
  167. package/tx/params.js +25 -0
  168. package/tx/problems.js +3 -5
  169. package/tx/provider.js +314 -67
  170. package/tx/resource-cache-sqlite.js +1050 -0
  171. package/tx/sct/documentation.md +45 -21
  172. package/tx/sct/ecl.js +81 -17
  173. package/tx/sct/editions.js +39 -0
  174. package/tx/sct/expressions.js +10 -1
  175. package/tx/sct/load-config.js +107 -0
  176. package/tx/sct/mmap.js +109 -0
  177. package/tx/sct/structures.js +175 -15
  178. package/tx/sct/text-index.js +5 -1
  179. package/tx/syndication/syndication-feed.js +2 -2
  180. package/tx/tx-html.js +44 -8
  181. package/tx/tx.fhir.org.yml +8 -8
  182. package/tx/tx.js +282 -80
  183. package/tx/usage-tracker.js +39 -11
  184. package/tx/vs/vs-api.js +19 -0
  185. package/tx/vs/vs-database.js +386 -62
  186. package/tx/vs/vs-package.js +44 -17
  187. package/tx/vs/vs-vsac.js +649 -101
  188. package/tx/workers/batch-validate.js +37 -10
  189. package/tx/workers/batch.js +6 -1
  190. package/tx/workers/cache-control.js +15 -4
  191. package/tx/workers/closure.js +156 -13
  192. package/tx/workers/compare.js +8 -5
  193. package/tx/workers/expand.js +1173 -289
  194. package/tx/workers/metadata.js +35 -0
  195. package/tx/workers/search.js +53 -1
  196. package/tx/workers/supplement-filters.js +145 -0
  197. package/tx/workers/translate.js +169 -4
  198. package/tx/workers/validate.js +223 -119
  199. package/tx/workers/worker.js +123 -31
  200. package/tx/xversion/xv-valueset.js +7 -4
  201. package/utilities/submit-test-report.js +3 -1
  202. package/utilities/tx-stress-test.js +482 -0
  203. package/vcl/readme.md +37 -0
  204. package/vcl/vcl-parser.js +15 -0
  205. package/xig/readme.md +52 -0
  206. package/xig/xig-template.html +5 -8
  207. package/xig/xig.js +238 -422
  208. package/changelog-template.md +0 -22
  209. package/registry/test-runner.js +0 -237
  210. package/release.sh +0 -80
  211. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  212. package/tsconfig.eslint.json +0 -9
  213. package/tx/html/dash-metrics.liquid +0 -147
  214. package/tx/html/home-metrics.liquid +0 -225
package/tx/cs/cs-loinc.js CHANGED
@@ -2,12 +2,13 @@ const sqlite3 = require('sqlite3').verbose();
2
2
  const assert = require('assert');
3
3
  const { CodeSystem } = require('../library/codesystem');
4
4
  const { Language, Languages} = require('../../library/languages');
5
- const { CodeSystemFactoryProvider} = require('./cs-api');
5
+ const { CodeSystemFactoryProvider, CodeSystemProvider } = require('./cs-api');
6
6
  const { validateOptionalParameter, validateArrayParameter} = require("../../library/utilities");
7
7
  const {BaseCSServices} = require("./cs-base");
8
- const {sqlEscapeString} = require("../../xig/xig");
8
+ const {sqlEscapeString} = require("../../library/sqlite-utilities");
9
9
  const regexUtilities = require('../../library/regex-utilities');
10
10
  const {ConceptMap} = require("../library/conceptmap");
11
+ const { guarded } = require('../library/sqlite-utilities');
11
12
 
12
13
  // Context kinds matching Pascal enum
13
14
  const LoincProviderContextKind = {
@@ -53,13 +54,16 @@ function mapRelationship(equivalence, fromLoinc) {
53
54
  }
54
55
  }
55
56
 
57
+ // CLASSTYPE: the code and the description the importer stores (see descClassType in
58
+ // import-loinc.module.js - 'Claims attachment', singular), and back
56
59
  const classTypes = {
57
60
  '1': 'Laboratory class',
58
61
  '2': 'Clinical class',
59
- '3': 'Claims attachments',
62
+ '3': 'Claims attachment',
60
63
  '4': 'Surveys',
61
64
  'Laboratory class' : '1',
62
65
  'Clinical class' : '2',
66
+ 'Claims attachment' : '3',
63
67
  'Claims attachments' : '3',
64
68
  'Surveys' : '4'
65
69
  };
@@ -124,6 +128,9 @@ class LoincFilterHolder {
124
128
  }
125
129
 
126
130
  hasKey(key) {
131
+ if (this.keySet) {
132
+ return this.keySet.has(key);
133
+ }
127
134
  // Binary search since keys should be sorted
128
135
  let l = 0;
129
136
  let r = this.keys.length - 1;
@@ -142,8 +149,9 @@ class LoincFilterHolder {
142
149
  }
143
150
 
144
151
  class LoincPrep {
145
- constructor() {
152
+ constructor(languages = null) {
146
153
  this.filters = [];
154
+ this.languages = languages; // what the request asked for (a text filter searches one language)
147
155
  }
148
156
  }
149
157
 
@@ -168,10 +176,7 @@ class LoincServices extends BaseCSServices {
168
176
  }
169
177
 
170
178
  close() {
171
- if (this.db) {
172
- this.db.close();
173
- this.db = null;
174
- }
179
+ this.closeDb();
175
180
  }
176
181
 
177
182
  // Metadata methods
@@ -336,6 +341,14 @@ class LoincServices extends BaseCSServices {
336
341
 
337
342
  }
338
343
 
344
+ /**
345
+ * The concept's properties for $expand: built by extendLookup, so that an expansion reports
346
+ * the same properties $lookup does (see CodeSystemProvider._propertiesFromLookup)
347
+ */
348
+ async properties(context, names) {
349
+ return this._propertiesFromLookup(context, names);
350
+ }
351
+
339
352
  async extendLookup(ctxt, props, params) {
340
353
  validateArrayParameter(props, 'props', String);
341
354
  validateArrayParameter(params, 'params', Object);
@@ -478,26 +491,36 @@ class LoincServices extends BaseCSServices {
478
491
 
479
492
  async #addRelationshipProperties(ctxt, props, params) {
480
493
  return new Promise((resolve, reject) => {
494
+ // DISTINCT: databases imported before the importer de-duplicated relationships have the
495
+ // same row more than once - a part listed twice under the same parent in
496
+ // ComponentHierarchyBySystem (on two paths) has two parent rows
481
497
  const sql = `
482
- SELECT RelationshipTypes.Description as Relationship, Codes.Code, Codes.Description as Value
498
+ SELECT DISTINCT RelationshipTypes.Description as Relationship, Codes.Code, Codes.Description as Value
483
499
  FROM Relationships, RelationshipTypes, Codes
484
500
  WHERE Relationships.SourceKey = ?
485
501
  AND Relationships.RelationshipTypeKey = RelationshipTypes.RelationshipTypeKey
486
502
  AND Relationships.TargetKey = Codes.CodeKey
487
503
  `;
488
504
 
489
- this.db.all(sql, [ctxt.key], (err, rows) => {
505
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
490
506
  if (err) {
491
507
  reject(err);
492
508
  } else {
493
509
  for (const row of rows) {
494
510
  if (this._hasProp(props, row.Relationship, true)) {
495
- this._addCodeProperty(params, 'property', row.Relationship, row.Code);
511
+ if (row.Relationship === 'CLASS') {
512
+ // CLASS is a string property - the class name (e.g. PULM). The Part code for the
513
+ // class is also returned, as partCode (an informal extension to the property)
514
+ this._addStringProperty(params, 'property', row.Relationship, row.Value || row.Code)
515
+ .part.push({name: 'partCode', valueCode: row.Code});
516
+ } else {
517
+ this._addCodeProperty(params, 'property', row.Relationship, row.Code);
518
+ }
496
519
  }
497
520
  }
498
521
  resolve();
499
522
  }
500
- });
523
+ }));
501
524
  });
502
525
  }
503
526
 
@@ -511,14 +534,15 @@ class LoincServices extends BaseCSServices {
511
534
  AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
512
535
  `;
513
536
 
514
- this.db.all(sql, [ctxt.key], (err, rows) => {
537
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
515
538
  if (err) {
516
539
  reject(err);
517
540
  } else {
518
541
  for (const row of rows) {
519
542
  if (this._hasProp(props, row.Description, true)) {
520
543
  if (row.Description == 'CLASSTYPE') {
521
- this._addStringProperty(params, 'property', row.Description, classTypes[row.Value])
544
+ // the code (1-4), with its description; a description with no known code is the value
545
+ this._addStringProperty(params, 'property', row.Description, classTypes[row.Value] || row.Value)
522
546
  .part.push({name: 'description', valueString: row.Value});
523
547
  } else {
524
548
  this._addStringProperty(params, 'property', row.Description, row.Value);
@@ -527,7 +551,7 @@ class LoincServices extends BaseCSServices {
527
551
  }
528
552
  resolve();
529
553
  }
530
- });
554
+ }));
531
555
  });
532
556
  }
533
557
 
@@ -535,7 +559,7 @@ class LoincServices extends BaseCSServices {
535
559
  return new Promise((resolve, reject) => {
536
560
  const sql = 'SELECT StatusKey FROM Codes WHERE CodeKey = ? AND StatusKey != 0';
537
561
 
538
- this.db.get(sql, [ctxt.key], (err, row) => {
562
+ this.db.get(sql, [ctxt.key], guarded(reject, (err, row) => {
539
563
  if (err) {
540
564
  reject(err);
541
565
  } else if (row) {
@@ -549,7 +573,7 @@ class LoincServices extends BaseCSServices {
549
573
  } else {
550
574
  resolve();
551
575
  }
552
- });
576
+ }));
553
577
  });
554
578
  }
555
579
 
@@ -577,7 +601,7 @@ class LoincServices extends BaseCSServices {
577
601
  AND Descriptions.LanguageKey = Languages.LanguageKey
578
602
  `;
579
603
 
580
- this.db.all(sql, [ctxt.key], (err, rows) => {
604
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
581
605
  if (err) {
582
606
  reject(err);
583
607
  } else {
@@ -586,7 +610,7 @@ class LoincServices extends BaseCSServices {
586
610
  }
587
611
  resolve();
588
612
  }
589
- });
613
+ }));
590
614
  });
591
615
  }
592
616
 
@@ -604,7 +628,7 @@ class LoincServices extends BaseCSServices {
604
628
  ORDER BY DescriptionTypeKey
605
629
  `;
606
630
 
607
- this.db.all(sql, [ctxt.key], (err, rows) => {
631
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
608
632
  if (err) {
609
633
  reject(err);
610
634
  } else {
@@ -617,7 +641,7 @@ class LoincServices extends BaseCSServices {
617
641
 
618
642
  resolve(displays);
619
643
  }
620
- });
644
+ }));
621
645
  });
622
646
  }
623
647
 
@@ -651,7 +675,7 @@ class LoincServices extends BaseCSServices {
651
675
  AND Descriptions.LanguageKey = Languages.LanguageKey
652
676
  `;
653
677
 
654
- this.db.all(sql, [ctxt.key], (err, rows) => {
678
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
655
679
  if (err) {
656
680
  reject(err);
657
681
  } else {
@@ -662,7 +686,7 @@ class LoincServices extends BaseCSServices {
662
686
  }
663
687
  resolve(results);
664
688
  }
665
- });
689
+ }));
666
690
  });
667
691
  }
668
692
 
@@ -676,7 +700,7 @@ class LoincServices extends BaseCSServices {
676
700
  AND Descriptions.LanguageKey = Languages.LanguageKey
677
701
  `;
678
702
 
679
- this.db.all(sql, [ctxt.key], (err, rows) => {
703
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
680
704
  if (err) {
681
705
  reject(err);
682
706
  } else {
@@ -686,7 +710,7 @@ class LoincServices extends BaseCSServices {
686
710
  }
687
711
  resolve(results);
688
712
  }
689
- });
713
+ }));
690
714
  });
691
715
  }
692
716
 
@@ -775,11 +799,17 @@ class LoincServices extends BaseCSServices {
775
799
  return true;
776
800
  }
777
801
 
778
- // LIST filter
802
+ // LIST filter: tx.fhir.org's own name for answers-for = [answer list], supported for
803
+ // backwards compatibility only (the defined filters are answers-for and answer-list, UP-645)
779
804
  if (prop === 'LIST' && op === '=' && this.codes.has(value)) {
780
805
  return true;
781
806
  }
782
807
 
808
+ // answer-list filter: the answer lists (LL) that an answer (LA) is in (UP-645)
809
+ if (prop === 'answer-list' && op === '=') {
810
+ return true;
811
+ }
812
+
783
813
  // CLASSSTYPE filter
784
814
  if (prop === 'CLASSTYPE' && op === '=' && ["1", "2", "3", "4"].includes(value)) {
785
815
  return true;
@@ -795,6 +825,13 @@ class LoincServices extends BaseCSServices {
795
825
  return true;
796
826
  }
797
827
 
828
+ // LOINC's own hierarchy filter: ancestor = part (or in parts), the codes the part subsumes
829
+ // in the multi-axial hierarchy. LOINC's implicit value sets (http://loinc.org/vs/[partcode])
830
+ // are defined with it
831
+ if (prop === 'ancestor' && ['=', 'in'].includes(op)) {
832
+ return true;
833
+ }
834
+
798
835
  // code filters (VSAC workaround)
799
836
  if (prop === 'code' && ['is-a', 'descendent-of', '='].includes(op)) {
800
837
  return true;
@@ -808,8 +845,8 @@ class LoincServices extends BaseCSServices {
808
845
  return false;
809
846
  }
810
847
 
811
- async getPrepContext(iterate) {
812
- return new LoincPrep(iterate);
848
+ async getPrepContext(iterate, languages = null) {
849
+ return new LoincPrep(languages);
813
850
  }
814
851
 
815
852
  async filter(filterContext, forIteration, prop, op, value) {
@@ -818,10 +855,49 @@ class LoincServices extends BaseCSServices {
818
855
  filterContext.filters.push(filter);
819
856
  }
820
857
 
858
+ /**
859
+ * A text filter matches the codes with a name in the search language (see searchLanguage:
860
+ * the request's language if LOINC has it, else English) that contains the text, and the
861
+ * codes that a supplement in use gives a matching designation in that language. In English
862
+ * the name is the display (scanning every English description - short names, consumer
863
+ * names - takes 2+ seconds, against 0.2 for the displays); in another language it is any
864
+ * description in it but RELATEDNAMES2, which is a list of keywords (a property), not a name.
865
+ * LOINC does not rate its matches, so sorted or not they come in display order
866
+ */
821
867
  async searchFilter(filterContext, filterText, sort) { // eslint-disable-line no-unused-vars
822
868
  const filter = new LoincFilterHolder();
823
- // LOINC does not rate its matches, so sorted or not they come in description order
824
- await this.#executeFilterQuery('$text', '<', filterText.filter, filter);
869
+ const lang = this.searchLanguage(filterContext.languages);
870
+ const english = CodeSystemProvider.inSearchLanguage(null, lang);
871
+ const langKeys = [];
872
+ for (const [code, key] of this.langs) {
873
+ if (!english && CodeSystemProvider.inSearchLanguage(code, lang)) {
874
+ langKeys.push(key);
875
+ }
876
+ }
877
+ const text = sqlEscapeString(filterText.filter || '');
878
+ const conds = [];
879
+ if (english) {
880
+ conds.push(`Codes.Description like '%${text}%'`); // the display, which is English
881
+ }
882
+ if (langKeys.length > 0) {
883
+ conds.push(`Codes.CodeKey IN (SELECT CodeKey FROM Descriptions WHERE LanguageKey IN (${langKeys.join(',')})
884
+ AND DescriptionTypeKey != 4 AND Value like '%${text}%')`);
885
+ }
886
+ if (conds.length > 0) {
887
+ await this.#executeSQL(`SELECT Codes.CodeKey as Key FROM Codes WHERE ${conds.join(' OR ')} ORDER BY Codes.Description ASC`, filter);
888
+ } else {
889
+ filter.keys = [];
890
+ }
891
+ const have = new Set(filter.keys);
892
+ for (const code of this._supplementSearchCodes(filterText, lang)) {
893
+ const ctxt = this.codes.get(code);
894
+ if (ctxt && !have.has(ctxt.key)) {
895
+ filter.keys.push(ctxt.key);
896
+ have.add(ctxt.key);
897
+ }
898
+ }
899
+ filter.lsql = 'text'; // checked by key (filterLocate needs it set)
900
+ filter.keySet = new Set(filter.keys); // in display order, not key order, so hasKey can't binary search
825
901
  filterContext.filters.push(filter);
826
902
  }
827
903
 
@@ -840,6 +916,17 @@ class LoincServices extends BaseCSServices {
840
916
  AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
841
917
  AND TargetKey = `;
842
918
  }
919
+ // answer-list filter: the answer lists (LL) the answer (LA) is in - the inverse of answers-for
920
+ else if (prop === 'answer-list' && op === '=') {
921
+ sql = `SELECT DISTINCT SourceKey as Key FROM Relationships
922
+ WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
923
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
924
+ ORDER BY SourceKey ASC`;
925
+ lsql = `SELECT COUNT(DISTINCT SourceKey) FROM Relationships
926
+ WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
927
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
928
+ AND SourceKey = `;
929
+ }
843
930
  // answers-for filter
844
931
  else if (prop === 'answers-for' && op === '=') {
845
932
  if (value.startsWith('LL')) {
@@ -1026,6 +1113,14 @@ class LoincServices extends BaseCSServices {
1026
1113
  else if (prop === 'concept' && ['is-a', 'descendent-of'].includes(op)) {
1027
1114
  ({ sql, lsql } = this.#hierarchyFilterSql(op, value));
1028
1115
  }
1116
+ // ancestor = X is LOINC's name for descendent-of X: "Ancestor includes parents transitively",
1117
+ // and the part itself is not its own ancestor. in is the union over the parts
1118
+ else if (prop === 'ancestor' && op === '=') {
1119
+ ({ sql, lsql } = this.#hierarchyFilterSql('descendent-of', value));
1120
+ }
1121
+ else if (prop === 'ancestor' && op === 'in') {
1122
+ ({ sql, lsql } = this.#hierarchyFilterSql('descendent-of', null, this.#commaListOfCodes(value)));
1123
+ }
1029
1124
  // Concept equal filter (workaround for VSAC misuse)
1030
1125
  else if (prop === 'concept' && op === '=') {
1031
1126
  sql = `SELECT CodeKey as Key FROM Codes
@@ -1102,14 +1197,15 @@ class LoincServices extends BaseCSServices {
1102
1197
  * the file as a graph would, for example, put Glucose in Serum or Plasma under Urinalysis,
1103
1198
  * because Protein appears under both.
1104
1199
  */
1105
- #hierarchyFilterSql(op, value) {
1106
- const code = this.#sqlWrapString(value);
1107
- const descendants = `SELECT DescendentKey as Key FROM Closure
1108
- WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${code}')`;
1200
+ #hierarchyFilterSql(op, value, codeList = null) {
1201
+ // one code, or (codeList) an already quoted, comma separated list of them
1202
+ const codes = codeList || `'${this.#sqlWrapString(value)}'`;
1203
+ const descendants = `SELECT DISTINCT DescendentKey as Key FROM Closure
1204
+ WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))`;
1109
1205
  const set = op === 'is-a'
1110
1206
  ? `${descendants}
1111
1207
  UNION
1112
- SELECT CodeKey as Key FROM Codes WHERE Code = '${code}'`
1208
+ SELECT CodeKey as Key FROM Codes WHERE Code IN (${codes})`
1113
1209
  : descendants;
1114
1210
  return {
1115
1211
  sql: `${set}
@@ -1121,10 +1217,11 @@ class LoincServices extends BaseCSServices {
1121
1217
  // Helper method for regex matching
1122
1218
  async #findRegexMatches(sql, pattern, valueColumn, keyColumn = 'Key') {
1123
1219
  return new Promise((resolve, reject) => {
1124
- const regex = regexUtilities.compile(pattern);
1220
+ // the whole value, as for every regex filter: a|b is ^(?:a|b)$
1221
+ const regex = regexUtilities.compile('^(?:' + pattern + ')$');
1125
1222
  const matchingKeys = [];
1126
1223
 
1127
- this.db.all(sql, (err, rows) => {
1224
+ this.db.all(sql, guarded(reject, (err, rows) => {
1128
1225
  if (err) {
1129
1226
  reject(err);
1130
1227
  } else {
@@ -1136,7 +1233,7 @@ class LoincServices extends BaseCSServices {
1136
1233
  }
1137
1234
  resolve(matchingKeys);
1138
1235
  }
1139
- });
1236
+ }));
1140
1237
  });
1141
1238
  }
1142
1239
 
@@ -1150,14 +1247,14 @@ class LoincServices extends BaseCSServices {
1150
1247
 
1151
1248
  async #executeSQL(sql, filter) {
1152
1249
  return new Promise((resolve, reject) => {
1153
- this.db.all(sql, (err, rows) => {
1250
+ this.db.all(sql, guarded(reject, (err, rows) => {
1154
1251
  if (err) {
1155
1252
  reject(err);
1156
1253
  } else {
1157
1254
  filter.keys = rows.map(row => row.Key).filter(key => key !== 0);
1158
1255
  resolve();
1159
1256
  }
1160
- });
1257
+ }));
1161
1258
  });
1162
1259
  }
1163
1260
 
@@ -1166,8 +1263,19 @@ class LoincServices extends BaseCSServices {
1166
1263
  }
1167
1264
 
1168
1265
  async executeFilters(filterContext) {
1169
-
1170
- return filterContext.filters;
1266
+ const filters = filterContext.filters;
1267
+ if (filters.length <= 1) {
1268
+ return filters;
1269
+ }
1270
+ // Every filter is already a list of keys in memory, so the include is their intersection,
1271
+ // as one set: its size is the include's size (count=0 needs no walk, and so no limit), and
1272
+ // the expander walks only the codes that are in it. In the first filter's order
1273
+ const others = filters.slice(1).map(f => new Set(f.keys));
1274
+ const set = new LoincFilterHolder();
1275
+ set.keys = filters[0].keys.filter(k => others.every(o => o.has(k)));
1276
+ set.keySet = new Set(set.keys);
1277
+ set.lsql = filters.map(f => f.lsql).find(l => l) || 'intersection';
1278
+ return [set];
1171
1279
  }
1172
1280
 
1173
1281
  async filterSize(filterContext, set) {
@@ -1278,9 +1386,14 @@ class LoincServices extends BaseCSServices {
1278
1386
  }
1279
1387
 
1280
1388
  class LoincServicesFactory extends CodeSystemFactoryProvider {
1281
- constructor(i18n, dbPath) {
1389
+ /**
1390
+ * @param {Object} [options] - {readOnly: true} in a tx-worker (cluster.md): the master
1391
+ * sets the journal mode and makes the indexes; this process opens the file read-only
1392
+ */
1393
+ constructor(i18n, dbPath, options = {}) {
1282
1394
  super(i18n);
1283
1395
  this.dbPath = dbPath;
1396
+ this.readOnly = !!options.readOnly;
1284
1397
  this.uses = 0;
1285
1398
  this._loaded = false;
1286
1399
  this._sharedData = null;
@@ -1305,7 +1418,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1305
1418
  }
1306
1419
 
1307
1420
  async load() {
1308
- const db = new sqlite3.Database(this.dbPath);
1421
+ const db = this.readOnly ? new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY) : new sqlite3.Database(this.dbPath);
1309
1422
 
1310
1423
  // Enable performance optimizations
1311
1424
  await this.#optimizeDatabase(db);
@@ -1358,6 +1471,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1358
1471
  async #optimizeDatabase(db) {
1359
1472
  return new Promise((resolve, reject) => {
1360
1473
  db.serialize(() => {
1474
+ if (this.readOnly) {
1475
+ // this connection's settings only; the file is the master's
1476
+ db.run('PRAGMA cache_size = 10000');
1477
+ db.run('PRAGMA temp_store = MEMORY');
1478
+ db.run('PRAGMA mmap_size = 268435456', (err) => err ? reject(err) : resolve());
1479
+ return;
1480
+ }
1361
1481
  db.run('PRAGMA journal_mode = WAL');
1362
1482
  db.run('PRAGMA synchronous = NORMAL');
1363
1483
  db.run('PRAGMA cache_size = 10000');
@@ -1377,7 +1497,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1377
1497
 
1378
1498
  async #loadLanguages(db) {
1379
1499
  return new Promise((resolve, reject) => {
1380
- db.all('SELECT LanguageKey, Code FROM Languages', (err, rows) => {
1500
+ db.all('SELECT LanguageKey, Code FROM Languages', guarded(reject, (err, rows) => {
1381
1501
  if (err) {
1382
1502
  reject(err);
1383
1503
  } else {
@@ -1386,13 +1506,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1386
1506
  }
1387
1507
  resolve();
1388
1508
  }
1389
- });
1509
+ }));
1390
1510
  });
1391
1511
  }
1392
1512
 
1393
1513
  async #loadStatusCodes(db) {
1394
1514
  return new Promise((resolve, reject) => {
1395
- db.all('SELECT StatusKey, Description FROM StatusCodes', (err, rows) => {
1515
+ db.all('SELECT StatusKey, Description FROM StatusCodes', guarded(reject, (err, rows) => {
1396
1516
  if (err) {
1397
1517
  reject(err);
1398
1518
  } else {
@@ -1402,13 +1522,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1402
1522
  }
1403
1523
  resolve();
1404
1524
  }
1405
- });
1525
+ }));
1406
1526
  });
1407
1527
  }
1408
1528
 
1409
1529
  async #loadRelationshipTypes(db) {
1410
1530
  return new Promise((resolve, reject) => {
1411
- db.all('SELECT RelationshipTypeKey, Description FROM RelationshipTypes', (err, rows) => {
1531
+ db.all('SELECT RelationshipTypeKey, Description FROM RelationshipTypes', guarded(reject, (err, rows) => {
1412
1532
  if (err) {
1413
1533
  reject(err);
1414
1534
  } else {
@@ -1417,13 +1537,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1417
1537
  }
1418
1538
  resolve();
1419
1539
  }
1420
- });
1540
+ }));
1421
1541
  });
1422
1542
  }
1423
1543
 
1424
1544
  async #loadPropertyTypes(db) {
1425
1545
  return new Promise((resolve, reject) => {
1426
- db.all('SELECT PropertyTypeKey, Description FROM PropertyTypes', (err, rows) => {
1546
+ db.all('SELECT PropertyTypeKey, Description FROM PropertyTypes', guarded(reject, (err, rows) => {
1427
1547
  if (err) {
1428
1548
  reject(err);
1429
1549
  } else {
@@ -1432,14 +1552,14 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1432
1552
  }
1433
1553
  resolve();
1434
1554
  }
1435
- });
1555
+ }));
1436
1556
  });
1437
1557
  }
1438
1558
 
1439
1559
  async #loadCodes(db) {
1440
1560
  return new Promise((resolve, reject) => {
1441
1561
  // First get the count to pre-allocate array
1442
- db.get('SELECT MAX(CodeKey) as maxKey FROM Codes', (err, row) => {
1562
+ db.get('SELECT MAX(CodeKey) as maxKey FROM Codes', guarded(reject, (err, row) => {
1443
1563
  if (err) return reject(err);
1444
1564
 
1445
1565
  // Pre-allocate the array to avoid repeated resizing
@@ -1447,7 +1567,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1447
1567
  this._sharedData.codeList = new Array(maxKey + 1).fill(null);
1448
1568
 
1449
1569
  // Now load all codes
1450
- db.all('SELECT CodeKey, Code, Type, Codes.Description, StatusCodes.Description as Status FROM Codes, StatusCodes where StatusCodes.StatusKey = Codes.StatusKey order by Type Asc, CodeKey Asc', (err, rows) => {
1570
+ db.all('SELECT CodeKey, Code, Type, Codes.Description, StatusCodes.Description as Status FROM Codes, StatusCodes where StatusCodes.StatusKey = Codes.StatusKey order by Type Asc, CodeKey Asc', guarded(reject, (err, rows) => {
1451
1571
  if (err) return reject(err);
1452
1572
 
1453
1573
  // Batch process rows
@@ -1469,8 +1589,8 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1469
1589
  }
1470
1590
  }
1471
1591
  resolve();
1472
- });
1473
- });
1592
+ }));
1593
+ }));
1474
1594
  });
1475
1595
  }
1476
1596
 
@@ -1490,7 +1610,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1490
1610
  ORDER BY d.CodeKey
1491
1611
  `;
1492
1612
 
1493
- db.all(sql, (err, rows) => {
1613
+ db.all(sql, guarded(reject, (err, rows) => {
1494
1614
  if (err) return reject(err);
1495
1615
 
1496
1616
  // Batch process by CodeKey to reduce lookups
@@ -1510,7 +1630,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1510
1630
  }
1511
1631
  }
1512
1632
  resolve();
1513
- });
1633
+ }));
1514
1634
  });
1515
1635
  }
1516
1636
 
@@ -1521,12 +1641,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1521
1641
  }
1522
1642
 
1523
1643
  return new Promise((resolve, reject) => {
1644
+ // DISTINCT, for databases imported with duplicate rows (see #addRelationshipProperties)
1524
1645
  const sql = `
1525
- SELECT SourceKey, TargetKey FROM Relationships
1646
+ SELECT DISTINCT SourceKey, TargetKey FROM Relationships
1526
1647
  WHERE RelationshipTypeKey = ${childRelKey}
1527
1648
  `;
1528
1649
 
1529
- db.all(sql, (err, rows) => {
1650
+ db.all(sql, guarded(reject, (err, rows) => {
1530
1651
  if (err) {
1531
1652
  reject(err);
1532
1653
  } else {
@@ -1540,13 +1661,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1540
1661
  }
1541
1662
  resolve();
1542
1663
  }
1543
- });
1664
+ }));
1544
1665
  });
1545
1666
  }
1546
1667
 
1547
1668
  async #loadConfig(db) {
1548
1669
  return new Promise((resolve, reject) => {
1549
- db.all('SELECT ConfigKey, Value FROM Config WHERE ConfigKey IN (2, 3)', (err, rows) => {
1670
+ db.all('SELECT ConfigKey, Value FROM Config WHERE ConfigKey IN (2, 3)', guarded(reject, (err, rows) => {
1550
1671
  if (err) {
1551
1672
  reject(err);
1552
1673
  } else {
@@ -1559,7 +1680,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1559
1680
  }
1560
1681
  resolve();
1561
1682
  }
1562
- });
1683
+ }));
1563
1684
  });
1564
1685
  }
1565
1686
 
@@ -1570,14 +1691,14 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1570
1691
  // the mapping tables were added with the LOINC 2.83 importer; older databases don't have them
1571
1692
  async #loadHasMappings(db) {
1572
1693
  return new Promise((resolve, reject) => {
1573
- db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ExternalMappings'", (err, row) => {
1694
+ db.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'ExternalMappings'", guarded(reject, (err, row) => {
1574
1695
  if (err) {
1575
1696
  reject(err);
1576
1697
  } else {
1577
1698
  this._sharedData.hasMappings = !!row;
1578
1699
  resolve();
1579
1700
  }
1580
- });
1701
+ }));
1581
1702
  });
1582
1703
  }
1583
1704
 
@@ -1590,7 +1711,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1590
1711
  db.all(`SELECT Codes.Code as LoincCode, ExternalCodes.System, ExternalCodes.Code
1591
1712
  FROM ExternalMappings, ExternalCodes, Codes
1592
1713
  WHERE ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
1593
- AND ExternalMappings.CodeKey = Codes.CodeKey`, (err, rows) => {
1714
+ AND ExternalMappings.CodeKey = Codes.CodeKey`, guarded(reject, (err, rows) => {
1594
1715
  if (err) {
1595
1716
  reject(err);
1596
1717
  } else {
@@ -1605,7 +1726,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1605
1726
  }
1606
1727
  resolve();
1607
1728
  }
1608
- });
1729
+ }));
1609
1730
  });
1610
1731
  }
1611
1732
 
@@ -1784,6 +1905,11 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1784
1905
  id() {
1785
1906
  return "loinc"+this.version();
1786
1907
  }
1908
+
1909
+ /** Codes, parts, answers and groups all have a page at loinc.org (as LoincLinker in org.hl7.fhir.core) */
1910
+ codeLink(context) {
1911
+ return context && context.code ? `https://loinc.org/${encodeURIComponent(context.code)}/` : undefined;
1912
+ }
1787
1913
  }
1788
1914
 
1789
1915
  module.exports = {