fhirsmith 0.18.2 → 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 (203) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +149 -0
  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 +132 -59
  15. package/ips/nationality.js +120 -0
  16. package/ips/pipeline.js +74 -21
  17. package/ips/presenter.js +1 -1
  18. package/ips/readme.md +132 -25
  19. package/ips/render.js +466 -77
  20. package/ips/store.js +79 -22
  21. package/ips/structure.js +247 -0
  22. package/ips/terminology.js +1 -1
  23. package/library/body-limit.js +22 -0
  24. package/library/cache-generation.js +15 -0
  25. package/library/cgroup-memory.js +133 -1
  26. package/library/cluster-counts.js +121 -0
  27. package/library/cluster-events.js +153 -0
  28. package/library/cluster-metrics.js +193 -0
  29. package/library/cluster-status.js +468 -0
  30. package/library/config-reader.js +96 -0
  31. package/library/daily-report.js +100 -2
  32. package/library/html-server.js +27 -1
  33. package/library/i18nsupport.js +23 -1
  34. package/library/keep-alive.js +18 -0
  35. package/library/languages.js +17 -3
  36. package/library/logger.js +31 -0
  37. package/library/memory-guard.js +428 -0
  38. package/library/native-memory.js +52 -0
  39. package/library/npm-audit.js +51 -25
  40. package/library/openapi-doc.js +7 -5
  41. package/library/package-manager.js +8 -0
  42. package/library/role.js +197 -0
  43. package/library/server-credentials.js +169 -0
  44. package/library/sqlite-utilities.js +59 -0
  45. package/library/ssrf-guard.js +100 -0
  46. package/library/static-response-stats.js +16 -0
  47. package/library/version-utilities.js +50 -0
  48. package/library/worker-count.js +192 -0
  49. package/nginx.md +30 -11
  50. package/npmprojector/indexer.js +18 -10
  51. package/package.json +11 -8
  52. package/packages/feed-check-page.js +117 -0
  53. package/packages/feed-check.js +279 -0
  54. package/packages/npm-check.js +57 -3
  55. package/packages/npm-signing.js +219 -0
  56. package/packages/openapi.yaml +207 -1
  57. package/packages/package-crawler.js +322 -41
  58. package/packages/packages-template.html +6 -8
  59. package/packages/packages.js +604 -57
  60. package/packages/readme.md +113 -1
  61. package/publisher/publisher-template.html +2 -5
  62. package/publisher/publisher.js +121 -110
  63. package/publisher/readme.md +75 -0
  64. package/registry/crawler.js +84 -108
  65. package/registry/documentation/configuration.md +34 -0
  66. package/registry/registry-template.html +5 -8
  67. package/registry/registry.js +5 -4
  68. package/root-bare-template.html +7 -4
  69. package/root-template.html +5 -8
  70. package/server.js +398 -198
  71. package/shl/shl.js +38 -37
  72. package/static/a11y/a11y-check.js +832 -0
  73. package/static/a11y/axe.min.js +12 -0
  74. package/static/assets/css/bootstrap-fhir.css +15 -15
  75. package/static/assets/images/flags/au.svg +18 -0
  76. package/static/assets/images/flags/unknown.svg +11 -0
  77. package/static/assets/images/flags/us.svg +63 -0
  78. package/static/assets/images/icon_audit.png +0 -0
  79. package/static/assets/images/icon_clinical.png +0 -0
  80. package/static/assets/images/icon_document.png +0 -0
  81. package/static/assets/images/icon_external.png +0 -0
  82. package/static/assets/images/icon_medication.png +0 -0
  83. package/static/assets/images/icon_organization.png +0 -0
  84. package/static/assets/images/icon_other.png +0 -0
  85. package/static/assets/images/icon_person.png +0 -0
  86. package/static/assets/images/icon_section.png +0 -0
  87. package/stats-db.js +45 -0
  88. package/stats.js +197 -10
  89. package/testing/testing-template.html +5 -8
  90. package/token/nginx_passport_setup.md +1 -1
  91. package/token/security_guide.md +1 -1
  92. package/token/token-template.html +2 -5
  93. package/token/token.js +40 -36
  94. package/translations/Messages.properties +19 -0
  95. package/tx/README.md +154 -4
  96. package/tx/closure/closure-store.js +126 -3
  97. package/tx/cm/cm-database.js +69 -57
  98. package/tx/cm/cm-package.js +29 -1
  99. package/tx/cs/cs-api.js +284 -19
  100. package/tx/cs/cs-api.md +41 -4
  101. package/tx/cs/cs-areacode.js +1 -1
  102. package/tx/cs/cs-country.js +17 -5
  103. package/tx/cs/cs-cpt.js +33 -16
  104. package/tx/cs/cs-cs.js +148 -14
  105. package/tx/cs/cs-currency.js +1 -1
  106. package/tx/cs/cs-icd11.js +21 -13
  107. package/tx/cs/cs-icf.js +22 -3
  108. package/tx/cs/cs-lang.js +2 -2
  109. package/tx/cs/cs-loinc.js +185 -66
  110. package/tx/cs/cs-mimetypes.js +12 -2
  111. package/tx/cs/cs-ndc.js +40 -25
  112. package/tx/cs/cs-omop.js +54 -29
  113. package/tx/cs/cs-rxnorm.js +300 -196
  114. package/tx/cs/cs-snomed.js +608 -64
  115. package/tx/cs/cs-ucum.js +14 -4
  116. package/tx/cs/cs-unii.js +18 -14
  117. package/tx/cs/cs-virtual.js +210 -0
  118. package/tx/ecosystem-resolver.js +413 -0
  119. package/tx/html/cluster-dash-metrics.liquid +26 -0
  120. package/tx/html/cluster-home-metrics.liquid +31 -0
  121. package/tx/html/cluster-metrics-script.liquid +251 -0
  122. package/tx/html/codesystem-operations.liquid +3 -3
  123. package/tx/html/ecl-panel.liquid +1 -1
  124. package/tx/html/external-metrics.liquid +17 -0
  125. package/tx/html/operations-form.liquid +17 -17
  126. package/tx/html/search-form.liquid +11 -11
  127. package/tx/html/status-line.liquid +58 -0
  128. package/tx/html/status-panel.liquid +140 -0
  129. package/tx/html/status-tasks.liquid +25 -0
  130. package/tx/html/tx-template.html +2 -5
  131. package/tx/importers/atc-to-fhir.js +17 -16
  132. package/tx/importers/export-icd11-codesystem.js +3 -2
  133. package/tx/importers/import-icd11.js +4 -3
  134. package/tx/importers/import-loinc.module.js +118 -56
  135. package/tx/importers/import-ndc.module.js +56 -44
  136. package/tx/importers/import-rxnorm.module.js +118 -79
  137. package/tx/importers/import-sct.module.js +57 -58
  138. package/tx/importers/import-unii.module.js +37 -36
  139. package/tx/importers/subset-loinc.module.js +49 -48
  140. package/tx/importers/subset-rxnorm.module.js +29 -28
  141. package/tx/importers/terminal.js +18 -0
  142. package/tx/importers/tx-import-base.js +13 -12
  143. package/tx/importers/tx-import-settings.js +5 -4
  144. package/tx/importers/tx-import.js +53 -52
  145. package/tx/library/canonical-resource.js +49 -0
  146. package/tx/library/codesystem.js +94 -5
  147. package/tx/library/designations.js +45 -2
  148. package/tx/library/extensions.js +13 -7
  149. package/tx/library/implicit-valuesets.js +66 -0
  150. package/tx/library/renderer.js +77 -21
  151. package/tx/library/sqlite-utilities.js +2 -0
  152. package/tx/library/ucum-parsers.js +11 -1
  153. package/tx/library/wait-for-cache.js +43 -0
  154. package/tx/library.js +321 -22
  155. package/tx/openapi-operations.js +6 -4
  156. package/tx/operation-context.js +460 -99
  157. package/tx/params.js +25 -0
  158. package/tx/problems.js +3 -5
  159. package/tx/provider.js +278 -42
  160. package/tx/resource-cache-sqlite.js +1050 -0
  161. package/tx/sct/ecl.js +47 -6
  162. package/tx/sct/editions.js +39 -0
  163. package/tx/sct/expressions.js +10 -1
  164. package/tx/sct/load-config.js +107 -0
  165. package/tx/sct/mmap.js +109 -0
  166. package/tx/sct/structures.js +166 -13
  167. package/tx/sct/text-index.js +5 -1
  168. package/tx/syndication/syndication-feed.js +2 -2
  169. package/tx/tx-html.js +44 -8
  170. package/tx/tx.fhir.org.yml +8 -8
  171. package/tx/tx.js +282 -80
  172. package/tx/usage-tracker.js +39 -11
  173. package/tx/vs/vs-api.js +19 -0
  174. package/tx/vs/vs-database.js +386 -62
  175. package/tx/vs/vs-package.js +27 -4
  176. package/tx/vs/vs-vsac.js +648 -100
  177. package/tx/workers/batch-validate.js +37 -10
  178. package/tx/workers/batch.js +6 -1
  179. package/tx/workers/cache-control.js +15 -4
  180. package/tx/workers/closure.js +156 -13
  181. package/tx/workers/compare.js +8 -5
  182. package/tx/workers/expand.js +1173 -289
  183. package/tx/workers/metadata.js +35 -0
  184. package/tx/workers/search.js +53 -1
  185. package/tx/workers/supplement-filters.js +145 -0
  186. package/tx/workers/translate.js +169 -4
  187. package/tx/workers/validate.js +223 -119
  188. package/tx/workers/worker.js +123 -31
  189. package/tx/xversion/xv-valueset.js +7 -4
  190. package/utilities/submit-test-report.js +3 -1
  191. package/utilities/tx-stress-test.js +482 -0
  192. package/vcl/readme.md +37 -0
  193. package/vcl/vcl-parser.js +15 -0
  194. package/xig/readme.md +52 -0
  195. package/xig/xig-template.html +5 -8
  196. package/xig/xig.js +238 -422
  197. package/changelog-template.md +0 -22
  198. package/registry/test-runner.js +0 -237
  199. package/release.sh +0 -80
  200. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  201. package/tsconfig.eslint.json +0 -9
  202. package/tx/html/dash-metrics.liquid +0 -147
  203. 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,15 +491,18 @@ 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 {
@@ -504,7 +520,7 @@ class LoincServices extends BaseCSServices {
504
520
  }
505
521
  resolve();
506
522
  }
507
- });
523
+ }));
508
524
  });
509
525
  }
510
526
 
@@ -518,14 +534,15 @@ class LoincServices extends BaseCSServices {
518
534
  AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
519
535
  `;
520
536
 
521
- this.db.all(sql, [ctxt.key], (err, rows) => {
537
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
522
538
  if (err) {
523
539
  reject(err);
524
540
  } else {
525
541
  for (const row of rows) {
526
542
  if (this._hasProp(props, row.Description, true)) {
527
543
  if (row.Description == 'CLASSTYPE') {
528
- 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)
529
546
  .part.push({name: 'description', valueString: row.Value});
530
547
  } else {
531
548
  this._addStringProperty(params, 'property', row.Description, row.Value);
@@ -534,7 +551,7 @@ class LoincServices extends BaseCSServices {
534
551
  }
535
552
  resolve();
536
553
  }
537
- });
554
+ }));
538
555
  });
539
556
  }
540
557
 
@@ -542,7 +559,7 @@ class LoincServices extends BaseCSServices {
542
559
  return new Promise((resolve, reject) => {
543
560
  const sql = 'SELECT StatusKey FROM Codes WHERE CodeKey = ? AND StatusKey != 0';
544
561
 
545
- this.db.get(sql, [ctxt.key], (err, row) => {
562
+ this.db.get(sql, [ctxt.key], guarded(reject, (err, row) => {
546
563
  if (err) {
547
564
  reject(err);
548
565
  } else if (row) {
@@ -556,7 +573,7 @@ class LoincServices extends BaseCSServices {
556
573
  } else {
557
574
  resolve();
558
575
  }
559
- });
576
+ }));
560
577
  });
561
578
  }
562
579
 
@@ -584,7 +601,7 @@ class LoincServices extends BaseCSServices {
584
601
  AND Descriptions.LanguageKey = Languages.LanguageKey
585
602
  `;
586
603
 
587
- this.db.all(sql, [ctxt.key], (err, rows) => {
604
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
588
605
  if (err) {
589
606
  reject(err);
590
607
  } else {
@@ -593,7 +610,7 @@ class LoincServices extends BaseCSServices {
593
610
  }
594
611
  resolve();
595
612
  }
596
- });
613
+ }));
597
614
  });
598
615
  }
599
616
 
@@ -611,7 +628,7 @@ class LoincServices extends BaseCSServices {
611
628
  ORDER BY DescriptionTypeKey
612
629
  `;
613
630
 
614
- this.db.all(sql, [ctxt.key], (err, rows) => {
631
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
615
632
  if (err) {
616
633
  reject(err);
617
634
  } else {
@@ -624,7 +641,7 @@ class LoincServices extends BaseCSServices {
624
641
 
625
642
  resolve(displays);
626
643
  }
627
- });
644
+ }));
628
645
  });
629
646
  }
630
647
 
@@ -658,7 +675,7 @@ class LoincServices extends BaseCSServices {
658
675
  AND Descriptions.LanguageKey = Languages.LanguageKey
659
676
  `;
660
677
 
661
- this.db.all(sql, [ctxt.key], (err, rows) => {
678
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
662
679
  if (err) {
663
680
  reject(err);
664
681
  } else {
@@ -669,7 +686,7 @@ class LoincServices extends BaseCSServices {
669
686
  }
670
687
  resolve(results);
671
688
  }
672
- });
689
+ }));
673
690
  });
674
691
  }
675
692
 
@@ -683,7 +700,7 @@ class LoincServices extends BaseCSServices {
683
700
  AND Descriptions.LanguageKey = Languages.LanguageKey
684
701
  `;
685
702
 
686
- this.db.all(sql, [ctxt.key], (err, rows) => {
703
+ this.db.all(sql, [ctxt.key], guarded(reject, (err, rows) => {
687
704
  if (err) {
688
705
  reject(err);
689
706
  } else {
@@ -693,7 +710,7 @@ class LoincServices extends BaseCSServices {
693
710
  }
694
711
  resolve(results);
695
712
  }
696
- });
713
+ }));
697
714
  });
698
715
  }
699
716
 
@@ -782,11 +799,17 @@ class LoincServices extends BaseCSServices {
782
799
  return true;
783
800
  }
784
801
 
785
- // 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)
786
804
  if (prop === 'LIST' && op === '=' && this.codes.has(value)) {
787
805
  return true;
788
806
  }
789
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
+
790
813
  // CLASSSTYPE filter
791
814
  if (prop === 'CLASSTYPE' && op === '=' && ["1", "2", "3", "4"].includes(value)) {
792
815
  return true;
@@ -802,6 +825,13 @@ class LoincServices extends BaseCSServices {
802
825
  return true;
803
826
  }
804
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
+
805
835
  // code filters (VSAC workaround)
806
836
  if (prop === 'code' && ['is-a', 'descendent-of', '='].includes(op)) {
807
837
  return true;
@@ -815,8 +845,8 @@ class LoincServices extends BaseCSServices {
815
845
  return false;
816
846
  }
817
847
 
818
- async getPrepContext(iterate) {
819
- return new LoincPrep(iterate);
848
+ async getPrepContext(iterate, languages = null) {
849
+ return new LoincPrep(languages);
820
850
  }
821
851
 
822
852
  async filter(filterContext, forIteration, prop, op, value) {
@@ -825,10 +855,49 @@ class LoincServices extends BaseCSServices {
825
855
  filterContext.filters.push(filter);
826
856
  }
827
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
+ */
828
867
  async searchFilter(filterContext, filterText, sort) { // eslint-disable-line no-unused-vars
829
868
  const filter = new LoincFilterHolder();
830
- // LOINC does not rate its matches, so sorted or not they come in description order
831
- 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
832
901
  filterContext.filters.push(filter);
833
902
  }
834
903
 
@@ -847,6 +916,17 @@ class LoincServices extends BaseCSServices {
847
916
  AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
848
917
  AND TargetKey = `;
849
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
+ }
850
930
  // answers-for filter
851
931
  else if (prop === 'answers-for' && op === '=') {
852
932
  if (value.startsWith('LL')) {
@@ -1033,6 +1113,14 @@ class LoincServices extends BaseCSServices {
1033
1113
  else if (prop === 'concept' && ['is-a', 'descendent-of'].includes(op)) {
1034
1114
  ({ sql, lsql } = this.#hierarchyFilterSql(op, value));
1035
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
+ }
1036
1124
  // Concept equal filter (workaround for VSAC misuse)
1037
1125
  else if (prop === 'concept' && op === '=') {
1038
1126
  sql = `SELECT CodeKey as Key FROM Codes
@@ -1109,14 +1197,15 @@ class LoincServices extends BaseCSServices {
1109
1197
  * the file as a graph would, for example, put Glucose in Serum or Plasma under Urinalysis,
1110
1198
  * because Protein appears under both.
1111
1199
  */
1112
- #hierarchyFilterSql(op, value) {
1113
- const code = this.#sqlWrapString(value);
1114
- const descendants = `SELECT DescendentKey as Key FROM Closure
1115
- 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}))`;
1116
1205
  const set = op === 'is-a'
1117
1206
  ? `${descendants}
1118
1207
  UNION
1119
- SELECT CodeKey as Key FROM Codes WHERE Code = '${code}'`
1208
+ SELECT CodeKey as Key FROM Codes WHERE Code IN (${codes})`
1120
1209
  : descendants;
1121
1210
  return {
1122
1211
  sql: `${set}
@@ -1128,10 +1217,11 @@ class LoincServices extends BaseCSServices {
1128
1217
  // Helper method for regex matching
1129
1218
  async #findRegexMatches(sql, pattern, valueColumn, keyColumn = 'Key') {
1130
1219
  return new Promise((resolve, reject) => {
1131
- 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 + ')$');
1132
1222
  const matchingKeys = [];
1133
1223
 
1134
- this.db.all(sql, (err, rows) => {
1224
+ this.db.all(sql, guarded(reject, (err, rows) => {
1135
1225
  if (err) {
1136
1226
  reject(err);
1137
1227
  } else {
@@ -1143,7 +1233,7 @@ class LoincServices extends BaseCSServices {
1143
1233
  }
1144
1234
  resolve(matchingKeys);
1145
1235
  }
1146
- });
1236
+ }));
1147
1237
  });
1148
1238
  }
1149
1239
 
@@ -1157,14 +1247,14 @@ class LoincServices extends BaseCSServices {
1157
1247
 
1158
1248
  async #executeSQL(sql, filter) {
1159
1249
  return new Promise((resolve, reject) => {
1160
- this.db.all(sql, (err, rows) => {
1250
+ this.db.all(sql, guarded(reject, (err, rows) => {
1161
1251
  if (err) {
1162
1252
  reject(err);
1163
1253
  } else {
1164
1254
  filter.keys = rows.map(row => row.Key).filter(key => key !== 0);
1165
1255
  resolve();
1166
1256
  }
1167
- });
1257
+ }));
1168
1258
  });
1169
1259
  }
1170
1260
 
@@ -1173,8 +1263,19 @@ class LoincServices extends BaseCSServices {
1173
1263
  }
1174
1264
 
1175
1265
  async executeFilters(filterContext) {
1176
-
1177
- 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];
1178
1279
  }
1179
1280
 
1180
1281
  async filterSize(filterContext, set) {
@@ -1285,9 +1386,14 @@ class LoincServices extends BaseCSServices {
1285
1386
  }
1286
1387
 
1287
1388
  class LoincServicesFactory extends CodeSystemFactoryProvider {
1288
- 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 = {}) {
1289
1394
  super(i18n);
1290
1395
  this.dbPath = dbPath;
1396
+ this.readOnly = !!options.readOnly;
1291
1397
  this.uses = 0;
1292
1398
  this._loaded = false;
1293
1399
  this._sharedData = null;
@@ -1312,7 +1418,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1312
1418
  }
1313
1419
 
1314
1420
  async load() {
1315
- 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);
1316
1422
 
1317
1423
  // Enable performance optimizations
1318
1424
  await this.#optimizeDatabase(db);
@@ -1365,6 +1471,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1365
1471
  async #optimizeDatabase(db) {
1366
1472
  return new Promise((resolve, reject) => {
1367
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
+ }
1368
1481
  db.run('PRAGMA journal_mode = WAL');
1369
1482
  db.run('PRAGMA synchronous = NORMAL');
1370
1483
  db.run('PRAGMA cache_size = 10000');
@@ -1384,7 +1497,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1384
1497
 
1385
1498
  async #loadLanguages(db) {
1386
1499
  return new Promise((resolve, reject) => {
1387
- db.all('SELECT LanguageKey, Code FROM Languages', (err, rows) => {
1500
+ db.all('SELECT LanguageKey, Code FROM Languages', guarded(reject, (err, rows) => {
1388
1501
  if (err) {
1389
1502
  reject(err);
1390
1503
  } else {
@@ -1393,13 +1506,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1393
1506
  }
1394
1507
  resolve();
1395
1508
  }
1396
- });
1509
+ }));
1397
1510
  });
1398
1511
  }
1399
1512
 
1400
1513
  async #loadStatusCodes(db) {
1401
1514
  return new Promise((resolve, reject) => {
1402
- db.all('SELECT StatusKey, Description FROM StatusCodes', (err, rows) => {
1515
+ db.all('SELECT StatusKey, Description FROM StatusCodes', guarded(reject, (err, rows) => {
1403
1516
  if (err) {
1404
1517
  reject(err);
1405
1518
  } else {
@@ -1409,13 +1522,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1409
1522
  }
1410
1523
  resolve();
1411
1524
  }
1412
- });
1525
+ }));
1413
1526
  });
1414
1527
  }
1415
1528
 
1416
1529
  async #loadRelationshipTypes(db) {
1417
1530
  return new Promise((resolve, reject) => {
1418
- db.all('SELECT RelationshipTypeKey, Description FROM RelationshipTypes', (err, rows) => {
1531
+ db.all('SELECT RelationshipTypeKey, Description FROM RelationshipTypes', guarded(reject, (err, rows) => {
1419
1532
  if (err) {
1420
1533
  reject(err);
1421
1534
  } else {
@@ -1424,13 +1537,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1424
1537
  }
1425
1538
  resolve();
1426
1539
  }
1427
- });
1540
+ }));
1428
1541
  });
1429
1542
  }
1430
1543
 
1431
1544
  async #loadPropertyTypes(db) {
1432
1545
  return new Promise((resolve, reject) => {
1433
- db.all('SELECT PropertyTypeKey, Description FROM PropertyTypes', (err, rows) => {
1546
+ db.all('SELECT PropertyTypeKey, Description FROM PropertyTypes', guarded(reject, (err, rows) => {
1434
1547
  if (err) {
1435
1548
  reject(err);
1436
1549
  } else {
@@ -1439,14 +1552,14 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1439
1552
  }
1440
1553
  resolve();
1441
1554
  }
1442
- });
1555
+ }));
1443
1556
  });
1444
1557
  }
1445
1558
 
1446
1559
  async #loadCodes(db) {
1447
1560
  return new Promise((resolve, reject) => {
1448
1561
  // First get the count to pre-allocate array
1449
- 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) => {
1450
1563
  if (err) return reject(err);
1451
1564
 
1452
1565
  // Pre-allocate the array to avoid repeated resizing
@@ -1454,7 +1567,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1454
1567
  this._sharedData.codeList = new Array(maxKey + 1).fill(null);
1455
1568
 
1456
1569
  // Now load all codes
1457
- 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) => {
1458
1571
  if (err) return reject(err);
1459
1572
 
1460
1573
  // Batch process rows
@@ -1476,8 +1589,8 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1476
1589
  }
1477
1590
  }
1478
1591
  resolve();
1479
- });
1480
- });
1592
+ }));
1593
+ }));
1481
1594
  });
1482
1595
  }
1483
1596
 
@@ -1497,7 +1610,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1497
1610
  ORDER BY d.CodeKey
1498
1611
  `;
1499
1612
 
1500
- db.all(sql, (err, rows) => {
1613
+ db.all(sql, guarded(reject, (err, rows) => {
1501
1614
  if (err) return reject(err);
1502
1615
 
1503
1616
  // Batch process by CodeKey to reduce lookups
@@ -1517,7 +1630,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1517
1630
  }
1518
1631
  }
1519
1632
  resolve();
1520
- });
1633
+ }));
1521
1634
  });
1522
1635
  }
1523
1636
 
@@ -1528,12 +1641,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1528
1641
  }
1529
1642
 
1530
1643
  return new Promise((resolve, reject) => {
1644
+ // DISTINCT, for databases imported with duplicate rows (see #addRelationshipProperties)
1531
1645
  const sql = `
1532
- SELECT SourceKey, TargetKey FROM Relationships
1646
+ SELECT DISTINCT SourceKey, TargetKey FROM Relationships
1533
1647
  WHERE RelationshipTypeKey = ${childRelKey}
1534
1648
  `;
1535
1649
 
1536
- db.all(sql, (err, rows) => {
1650
+ db.all(sql, guarded(reject, (err, rows) => {
1537
1651
  if (err) {
1538
1652
  reject(err);
1539
1653
  } else {
@@ -1547,13 +1661,13 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1547
1661
  }
1548
1662
  resolve();
1549
1663
  }
1550
- });
1664
+ }));
1551
1665
  });
1552
1666
  }
1553
1667
 
1554
1668
  async #loadConfig(db) {
1555
1669
  return new Promise((resolve, reject) => {
1556
- 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) => {
1557
1671
  if (err) {
1558
1672
  reject(err);
1559
1673
  } else {
@@ -1566,7 +1680,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1566
1680
  }
1567
1681
  resolve();
1568
1682
  }
1569
- });
1683
+ }));
1570
1684
  });
1571
1685
  }
1572
1686
 
@@ -1577,14 +1691,14 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1577
1691
  // the mapping tables were added with the LOINC 2.83 importer; older databases don't have them
1578
1692
  async #loadHasMappings(db) {
1579
1693
  return new Promise((resolve, reject) => {
1580
- 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) => {
1581
1695
  if (err) {
1582
1696
  reject(err);
1583
1697
  } else {
1584
1698
  this._sharedData.hasMappings = !!row;
1585
1699
  resolve();
1586
1700
  }
1587
- });
1701
+ }));
1588
1702
  });
1589
1703
  }
1590
1704
 
@@ -1597,7 +1711,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1597
1711
  db.all(`SELECT Codes.Code as LoincCode, ExternalCodes.System, ExternalCodes.Code
1598
1712
  FROM ExternalMappings, ExternalCodes, Codes
1599
1713
  WHERE ExternalMappings.ExternalCodeKey = ExternalCodes.ExternalCodeKey
1600
- AND ExternalMappings.CodeKey = Codes.CodeKey`, (err, rows) => {
1714
+ AND ExternalMappings.CodeKey = Codes.CodeKey`, guarded(reject, (err, rows) => {
1601
1715
  if (err) {
1602
1716
  reject(err);
1603
1717
  } else {
@@ -1612,7 +1726,7 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1612
1726
  }
1613
1727
  resolve();
1614
1728
  }
1615
- });
1729
+ }));
1616
1730
  });
1617
1731
  }
1618
1732
 
@@ -1791,6 +1905,11 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1791
1905
  id() {
1792
1906
  return "loinc"+this.version();
1793
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
+ }
1794
1913
  }
1795
1914
 
1796
1915
  module.exports = {