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
@@ -128,8 +128,6 @@
128
128
  <div id="stripe"> </div>
129
129
  <div class="container"> <!-- container -->
130
130
  <div style="background-color: #ad1f2f; padding: 6px; color: white;"> <!-- container -->
131
- <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org &nbsp;|&nbsp;
132
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &nbsp;|&nbsp;
133
131
  <a href="/" style="color: gold"> Server Home</a> &nbsp;|&nbsp;
134
132
  <a href="/token" style="color: gold">Token Home</a>
135
133
  </div> <!-- /container -->
@@ -144,7 +142,7 @@
144
142
  <div class="inner-wrapper">
145
143
  <div class="col-9">
146
144
 
147
- <h2><img border="0" src="../FHIRsmith32.png" style="vertical-align: text-bottom"/> Token Server: [%title%] </h2>
145
+ <h2><a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" title="FHIRsmith [%ver%]"><img border="0" src="/FHIRsmith32.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/></a> Token Server: [%title%] </h2>
148
146
 
149
147
  [%content%]
150
148
 
@@ -183,8 +181,7 @@
183
181
  <div class="container"> <!-- container -->
184
182
  <div class="inner-wrapper">
185
183
  <p>
186
- <a href="http://www.hl7.org/fhir" style="color: gold" title="Fast Healthcare Interoperability Resources - Home Page"><img border="0" src="/icon-fhir-16.png" style="vertical-align: text-bottom"/> <b>FHIR</b></a> &copy; HL7.org 2011+. &nbsp;|&nbsp;
187
- <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
184
+ <a href="https://github.com/HealthIntersections/FHIRsmith/blob/main/README.md" style="color: gold"><img border="0" src="/FHIRsmith16.png" alt="FHIRsmith Logo" style="vertical-align: text-bottom"/> FHIRsmith</a> [%ver%] &copy; HealthIntersections.com.au 2023+ &nbsp;|&nbsp;
188
185
  ([%ms%] ms)
189
186
 
190
187
  </p>
package/token/token.js CHANGED
@@ -20,6 +20,7 @@ const lusca = require('lusca');
20
20
  const folders = require('../library/folder-setup');
21
21
  const { SqliteSessionStore } = require('../library/sqlite-session-store');
22
22
  const escape = require('escape-html');
23
+ const { guarded } = require('../library/sqlite-utilities');
23
24
 
24
25
 
25
26
  const Logger = require('../library/logger');
@@ -74,7 +75,7 @@ class TokenModule {
74
75
  }
75
76
 
76
77
  // Create tables
77
- this.db.serialize(() => {
78
+ this.db.serialize(guarded(reject, () => {
78
79
  // Users table - enhanced with OAuth data
79
80
  this.db.run(`
80
81
  CREATE TABLE IF NOT EXISTS users (
@@ -146,7 +147,7 @@ class TokenModule {
146
147
  this.db.run(`CREATE INDEX IF NOT EXISTS idx_usage_date ON usage_stats(date)`);
147
148
  this.db.run(`CREATE INDEX IF NOT EXISTS idx_security_log_user ON security_log(user_id)`);
148
149
  this.db.run(`CREATE INDEX IF NOT EXISTS idx_security_log_event ON security_log(event_type)`);
149
- });
150
+ }));
150
151
 
151
152
  this.log.info(`Token database initialized: ${dbPath}`);
152
153
  resolve();
@@ -166,7 +167,10 @@ class TokenModule {
166
167
  saveUninitialized: false,
167
168
  rolling: true, // Reset expiration on each request
168
169
  cookie: {
169
- secure: process.env.NODE_ENV === 'production', // HTTPS only in production
170
+ // Secure when the request came over https: 'auto' asks req.secure, which (with
171
+ // server.trustProxy) follows nginx's X-Forwarded-Proto. It used to depend on NODE_ENV,
172
+ // which nothing sets, so the cookie was never Secure. config.cookieSecure overrides
173
+ secure: this.config.cookieSecure ?? 'auto',
170
174
  httpOnly: true, // Prevent XSS
171
175
  maxAge: 24 * 60 * 60 * 1000, // 24 hours
172
176
  sameSite: 'lax' // CSRF protection
@@ -780,7 +784,7 @@ class TokenModule {
780
784
  this.db.get(
781
785
  'SELECT id FROM users WHERE (email = ?) OR (provider = ? AND provider_id = ?)',
782
786
  [userData.email, userData.provider, userData.provider_id],
783
- (err, row) => {
787
+ guarded(reject, (err, row) => {
784
788
  if (err) {
785
789
  reject(err);
786
790
  return;
@@ -791,29 +795,29 @@ class TokenModule {
791
795
  this.db.run(
792
796
  'UPDATE users SET email = ?, name = ?, profile_data = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
793
797
  [userData.email, userData.name, userData.profile_data, row.id],
794
- (err) => {
798
+ guarded(reject, (err) => {
795
799
  if (err) {
796
800
  reject(err);
797
801
  } else {
798
802
  resolve(row.id);
799
803
  }
800
- }
804
+ })
801
805
  );
802
806
  } else {
803
807
  // Create new user
804
808
  this.db.run(
805
809
  'INSERT INTO users (email, name, provider, provider_id, profile_data) VALUES (?, ?, ?, ?, ?)',
806
810
  [userData.email, userData.name, userData.provider, userData.provider_id, userData.profile_data],
807
- function(err) {
811
+ guarded(reject, function(err) {
808
812
  if (err) {
809
813
  reject(err);
810
814
  } else {
811
815
  resolve(this.lastID);
812
816
  }
813
- }
817
+ })
814
818
  );
815
819
  }
816
- }
820
+ })
817
821
  );
818
822
  });
819
823
  }
@@ -823,10 +827,10 @@ class TokenModule {
823
827
  this.db.get(
824
828
  'SELECT * FROM users WHERE id = ? AND is_active = 1',
825
829
  [userId],
826
- (err, row) => {
830
+ guarded(reject, (err, row) => {
827
831
  if (err) reject(err);
828
832
  else resolve(row);
829
- }
833
+ })
830
834
  );
831
835
  });
832
836
  }
@@ -836,10 +840,10 @@ class TokenModule {
836
840
  this.db.run(
837
841
  'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?',
838
842
  [userId],
839
- (err) => {
843
+ guarded(reject, (err) => {
840
844
  if (err) reject(err);
841
845
  else resolve();
842
- }
846
+ })
843
847
  );
844
848
  });
845
849
  }
@@ -852,10 +856,10 @@ class TokenModule {
852
856
  WHERE user_id = ?
853
857
  ORDER BY created_at DESC`,
854
858
  [userId],
855
- (err, rows) => {
859
+ guarded(reject, (err, rows) => {
856
860
  if (err) reject(err);
857
861
  else resolve(rows || []);
858
- }
862
+ })
859
863
  );
860
864
  });
861
865
  }
@@ -870,10 +874,10 @@ class TokenModule {
870
874
  `INSERT INTO api_keys (user_id, key_hash, key_prefix, name, scopes, expires_at, created_ip)
871
875
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
872
876
  [userId, keyHash, keyPrefix, name, scopes, expiresAt, ip],
873
- function(err) {
877
+ guarded(reject, function(err) {
874
878
  if (err) reject(err);
875
879
  else resolve(this.lastID);
876
- }
880
+ })
877
881
  );
878
882
  });
879
883
  }
@@ -887,7 +891,7 @@ class TokenModule {
887
891
  FROM api_keys ak
888
892
  JOIN users u ON ak.user_id = u.id
889
893
  WHERE ak.key_prefix = ? AND ak.is_active = 1 AND u.is_active = 1
890
- `, [keyPrefix], async (err, rows) => {
894
+ `, [keyPrefix], guarded(reject, async (err, rows) => {
891
895
  if (err) {
892
896
  reject(err);
893
897
  return;
@@ -911,7 +915,7 @@ class TokenModule {
911
915
  } catch (error) {
912
916
  reject(error);
913
917
  }
914
- });
918
+ }));
915
919
  });
916
920
  }
917
921
 
@@ -920,13 +924,13 @@ class TokenModule {
920
924
  this.db.run(
921
925
  'UPDATE api_keys SET is_active = 0 WHERE id = ? AND user_id = ?',
922
926
  [keyId, userId],
923
- function(err) {
927
+ guarded(reject, function(err) {
924
928
  if (err) {
925
929
  reject(err);
926
930
  } else {
927
931
  resolve(this.changes > 0);
928
932
  }
929
- }
933
+ })
930
934
  );
931
935
  });
932
936
  }
@@ -937,10 +941,10 @@ class TokenModule {
937
941
  this.db.run(
938
942
  'UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?',
939
943
  [keyId],
940
- (err) => {
944
+ guarded(reject, (err) => {
941
945
  if (err) reject(err);
942
946
  else resolve();
943
- }
947
+ })
944
948
  );
945
949
  });
946
950
  }
@@ -956,10 +960,10 @@ class TokenModule {
956
960
  request_count = request_count + ?,
957
961
  last_request_ip = ?,
958
962
  updated_at = CURRENT_TIMESTAMP
959
- `, [keyId, today, count, ip, count, ip], function(err) {
963
+ `, [keyId, today, count, ip, count, ip], guarded(reject, function(err) {
960
964
  if (err) reject(err);
961
965
  else resolve();
962
- });
966
+ }));
963
967
  });
964
968
  }
965
969
 
@@ -970,10 +974,10 @@ class TokenModule {
970
974
  this.db.get(
971
975
  'SELECT request_count FROM usage_stats WHERE api_key_id = ? AND date = ?',
972
976
  [keyId, today],
973
- (err, row) => {
977
+ guarded(reject, (err, row) => {
974
978
  if (err) reject(err);
975
979
  else resolve(row ? row.request_count : 0);
976
- }
980
+ })
977
981
  );
978
982
  });
979
983
  }
@@ -986,10 +990,10 @@ class TokenModule {
986
990
  JOIN api_keys ak ON us.api_key_id = ak.id
987
991
  WHERE ak.user_id = ? AND us.date >= date('now', '-30 days')
988
992
  ORDER BY us.date DESC
989
- `, [userId], (err, rows) => {
993
+ `, [userId], guarded(reject, (err, rows) => {
990
994
  if (err) reject(err);
991
995
  else resolve(rows || []);
992
- });
996
+ }));
993
997
  });
994
998
  }
995
999
 
@@ -1000,10 +1004,10 @@ class TokenModule {
1000
1004
  FROM usage_stats
1001
1005
  WHERE api_key_id = ? AND date >= date('now', '-${days} days')
1002
1006
  ORDER BY date DESC
1003
- `, [keyId], (err, rows) => {
1007
+ `, [keyId], guarded(reject, (err, rows) => {
1004
1008
  if (err) reject(err);
1005
1009
  else resolve(rows || []);
1006
- });
1010
+ }));
1007
1011
  });
1008
1012
  }
1009
1013
 
@@ -1013,13 +1017,13 @@ class TokenModule {
1013
1017
  this.db.run(
1014
1018
  'INSERT INTO security_log (user_id, event_type, ip_address, user_agent, details) VALUES (?, ?, ?, ?, ?)',
1015
1019
  [userId, eventType, ip, userAgent, JSON.stringify(details)],
1016
- function(err) {
1020
+ guarded(reject, function(err) {
1017
1021
  if (err) {
1018
1022
  // Don't fail the main operation if logging fails
1019
1023
  console.error('Failed to log security event:', err);
1020
1024
  }
1021
1025
  resolve();
1022
- }
1026
+ })
1023
1027
  );
1024
1028
  });
1025
1029
  }
@@ -1292,15 +1296,15 @@ class TokenModule {
1292
1296
  this.sessionStore.close();
1293
1297
  }
1294
1298
  if (this.db) {
1295
- await new Promise((resolve) => {
1296
- this.db.close((err) => {
1299
+ await new Promise((resolve, reject) => {
1300
+ this.db.close(guarded(reject, (err) => {
1297
1301
  if (err) {
1298
1302
  this.log.error('Error closing database:', err);
1299
1303
  } else {
1300
1304
  this.log.info('Database connection closed');
1301
1305
  }
1302
1306
  resolve();
1303
- });
1307
+ }));
1304
1308
  });
1305
1309
  }
1306
1310
  }
@@ -159,6 +159,7 @@ CODESYSTEM_SHAREABLE_MISSING_HL7 = Code systems published by HL7 SHALL conform t
159
159
  CODESYSTEM_SUPP_NO_DISPLAY = This display (''{0}'') differs from that defined by the base code system (''{1}''). Both displays claim to be the ''primary designation'' for the same language (''{2}''), and the correct interpretation of this is undefined
160
160
  CODESYSTEM_THO_CHECK = Most code systems defined in HL7 IGs will need to move to THO later during the process. Consider giving this code system a THO URL now (See https://confluence.hl7.org/display/TSMG/Terminology+Play+Book, and/or talk to TSMG)
161
161
  CODESYSTEM_TOO_COSTLY_TIME = The operation ''{1}'' took too long to process (>{0}sec) (at {2})
162
+ CODESYSTEM_VERSION_NEEDED = The code system ''{0}'' says that a version must be specified when it is referenced (versionNeeded = true), but this coding has no version
162
163
  CODESYSTEM_NOT_ENUMERABLE = The code system ''{0}'' cannot be expanded because its codes cannot be iterated or enumerated in any meaningful sense
163
164
  Coding_has_no_system__cannot_validate = Coding has no system. A code with no system has no defined meaning, and it cannot be validated. A system should be provided
164
165
  Coding_has_no_system_or_code__cannot_validate = Coding has neither a system nor a code, so it cannot be validated. A system and a code should be provided
@@ -1094,6 +1095,8 @@ Unable_to_handle_system__filter_with_property__ = Unable to handle system {0} fi
1094
1095
  UNABLE_TO_HANDLE_SYSTEM__PROPERTY_FILTER_WITH_OP__ = Unable to handle system {0} property filter with op = {1}
1095
1096
  UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE = The system {0} filter with property = {1}, op = {2} has no value
1096
1097
  UNABLE_TO_INFER_CODESYSTEM = The System URI could not be determined for the code ''{0}'' in the ValueSet ''{1}''
1098
+ UNABLE_TO_INFER_CODESYSTEM_UNKNOWN_SYSTEM = The System URI could not be determined for the code ''{0}'' in the ValueSet ''{1}'': a definition for the CodeSystem ''{2}'' it includes could not be found
1099
+ VALUESET_MODIFIER_EXTENSION_UNKNOWN = The value set {0} can''t be processed: {1} has a modifier extension that isn''t understood ({2})
1097
1100
  Unable_to_locate_the_profile__in_order_to_validate_against_it = Unable to locate the profile ''{0}'' in order to validate against it
1098
1101
  Unable_to_process_request_for_resource_for___ = Unable to process request for resource for {0} / {1}
1099
1102
  Unable_to_resolve_ = Unable to resolve the reference {0}
@@ -1116,9 +1119,20 @@ Unable_to_resolve_system__value_set_has_no_includes_or_expansion = The System UR
1116
1119
  UNABLE_TO_RESOLVE_SYSTEM_SYSTEM_IS_INDETERMINATE = The code system {1} referred to from value set {0} has a grammar, and the code might be valid in it
1117
1120
  Unable_to_resolve_value_Set_ = A definition for the value Set ''{0}'' could not be found
1118
1121
  CACHE_ID_UNKNOWN = The cache ''{0}'' was never issued by this server, and this server has no record of it ever existing. Caches are created with $cache-control?mode=start, and a cache-id is only valid on the server instance and endpoint that issued it - either it was issued somewhere else, or this server has restarted since it was issued
1122
+ CACHE_ID_UNKNOWN_SHARED = The cache ''{0}'' was never issued by this server for this endpoint: this server has no record of it. Caches are created with $cache-control?mode=start, and a cache-id is only valid on the server and endpoint that issued it
1123
+ CACHE_ID_NEVER_ISSUED = The cache ''{0}'' was never issued by this server: it carries this server''s instance code and the start of this run, which began {1} ago, but this server has no record of it (it remembers the fate of the last {2} caches that ended). Caches are created with $cache-control?mode=start
1124
+ CACHE_ID_GONE_DURING_REQUEST = The cache ''{0}'' ended ({1}) while this request was using it, so {2} is no longer available. This operation has not been performed. Start a new cache with $cache-control?mode=start, and send the request again
1125
+ CACHE_ID_CHANGED_DURING_REQUEST = The cache ''{0}'' changed while this request was using it: another request replaced {1} after this one had read the list of what the cache holds. This operation has not been performed. Send the request again, to read the new contents together
1126
+ CACHE_ID_LOST_TO_RESTART = The cache ''{0}'' was lost to a server restart {1} ago. Caches are not carried over a restart, because neither the client nor the server can be sure a cache is intact across operations that were tried while the server was down. Start a new cache with $cache-control?mode=start and send the resources again
1119
1127
  CACHE_ID_CLOSED = The cache ''{0}'' was closed {1} ago by the client, using $cache-control?mode=end (it had been open for {2}). It did not expire and it was not idle: something in the client released this cache while this request was still using it
1120
1128
  CACHE_ID_EXPIRED = The cache ''{0}'' expired: it went {1} without being used, and this server closes caches that have been idle for more than {2} (this one was dropped {3} ago). Every request that uses a cache resets its idle clock, so a client that keeps using a cache will not see this
1121
1129
  CACHE_ID_CLEARED = The cache ''{0}'' was dropped {1} ago, when this server cleared all of its caches
1130
+ CACHE_ID_EVICTED = The cache ''{0}'' was dropped {1} ago to make room: the caches held more concepts than this server allows, and this was the one used least recently
1131
+ CACHE_ID_DISCARDED = The cache ''{0}'' was dropped {1} ago because it grew too large: a request would have taken it over {2} concepts, the most this server allows in one cache (cacheMaxConcepts). This operation has not been performed. Start a new cache, and send the request again
1132
+ CACHE_ID_DISCARDED_TOO_BIG = The cache ''{0}'' grew too large: this request would have taken it over {1} concepts, the most this server allows in one cache (cacheMaxConcepts), so the server has dropped the cache. This operation has not been performed. Start a new cache, and send the request again
1133
+ CACHE_ID_EVICTED_SIZE = The cache ''{0}'' was dropped {1} ago to make room: this server holds at most {2} of client resources across all caches, and when it is full it drops the caches that have gone longest without being used (this one had been idle for {3}). Start a new cache and send the resources again
1134
+ CACHE_REQUEST_TOO_BIG = This request''s resources hold {0} concepts, more than this server allows in one cache ({1}, cacheMaxConcepts), so no cache can hold them
1135
+ CACHE_START_TOO_BIG = The cache ''{0}'' would hold {1} concepts, more than this server allows in one cache ({2}, cacheMaxConcepts)
1122
1136
  CACHE_ID_OTHER_INSTANCE = The cache ''{0}'' was issued by the server instance ''{1}'', but this is instance ''{2}''. A cache only exists on the instance that issued it, so this request reached the wrong server: either the proxy in front of these servers is not routing on the cache-id, or instance ''{1}'' is unavailable
1123
1137
  CACHE_ID_OTHER_INSTANCE_UNNAMED = The cache ''{0}'' was issued by the server instance ''{1}'', but this server has no instance code, so it cannot have issued it. A cache only exists on the instance that issued it, so this request reached the wrong server: either the proxy in front of these servers is not routing on the cache-id, or instance ''{1}'' is unavailable
1124
1138
  CLOSURE_NOT_SUPPORTED = This server does not support $closure (it has to be turned on by the administrator)
@@ -1141,6 +1155,7 @@ CLOSURE_TOO_MANY_REQUEST_CONCEPTS = A single $closure request can add at most {0
1141
1155
  CLOSURE_TOO_MANY_ENTRIES = Adding these concepts would put more entries into the closure table ''{0}'' than this server allows, in one request or in total
1142
1156
  CLOSURE_CONFIG_TOO_LARGE = The configuration supplied for this closure table (mostly the tx-resource parameters) is larger than this server will keep, which is {0} bytes
1143
1157
  CLOSURE_TABLE_BUSY = The closure table ''{0}'' is busy with other requests. Try again shortly
1158
+ CLOSURE_TABLE_CHANGED = The closure table ''{0}'' was reset while this request was reading it. Retry the request against the new table
1144
1159
  SUBSUMPTION_CS_HIERARCHY_MEANING = The CodeSystem ''{0}'' has hierarchyMeaning = {1}, so its hierarchy does not describe subsumption, and subsumption cannot be tested
1145
1160
  SUBSUMPTION_CS_NOT_COMPLETE = The CodeSystem ''{0}'' has content = {1}, so subsumption cannot be tested (subsumption can only be tested against a code system with content = complete)
1146
1161
  SUBSUMPTION_CS_NOT_FOUND = A definition for the CodeSystem ''{0}'' could not be found, so subsumption cannot be tested
@@ -1283,6 +1298,7 @@ VALUESET_CONCEPT_DISPLAY_PRESENCE_MIXED = This include has some concepts with di
1283
1298
  VALUESET_CONCEPT_DISPLAY_SCT_TAG_MIXED = This SNOMED-CT based include has some concepts with semantic tags (FSN terms) and some without (preferred terms) - check that this is what is intended (examples for FSN: {0} and examples for no FSN: {1})
1284
1299
  VALUESET_EXAMPLE_SYSTEM_ERROR = Example System ''{0}'' specified, which is illegal. Concepts and Filters can''t be checked
1285
1300
  VALUESET_EXAMPLE_SYSTEM_HINT = Example System ''{0}'' specified, so Concepts and Filters can''t be checked
1301
+ VALUESET_EXPANSION_CODESYSTEM_LIMITED = Only {1} codes from the code system ''{0}'' can be listed in an expansion (by agreement with its publisher), so this expansion doesn''t list all the codes asked for
1286
1302
  VALUESET_IMPORT_UNION_INTERSECTION = This value set has a single include with multiple imported value sets. Per issue https://jira.hl7.org/browse/FHIR-25179, there has been confusion in the past whether these value sets are unioned or intersectioned. If this value set is contained in a package published prior to March 31 2022, it will be treated as a union, otherwise it will be treated as an intersection. If want a union, split the value set imports across multiple includes
1287
1303
  VALUESET_INC_TOO_MANY_CODES = The value set include has too many concepts to validate ({0}), so each individual code has not been checked
1288
1304
  VALUESET_INC_NOT_VALIDATING = The concepts in the value set include have not been validated because the validator is configured not to check them
@@ -1327,6 +1343,9 @@ VALUESET_SHAREABLE_MISSING_HL7 = Value sets published by HL7 SHALL conform to th
1327
1343
  VALUESET_SUPPLEMENT_MISSING_one = Required supplement not found: {1}
1328
1344
  VALUESET_SUPPLEMENT_MISSING_other = Required supplements not found: {1}
1329
1345
  VALUESET_TOO_COSTLY = The value set ''{0}'' expansion has too many codes to produce ({1})
1346
+ VALUESET_TOO_COSTLY_REQUEST = The expansion of the value set in the request has too many codes to produce ({0})
1347
+ VALUESET_TOO_COSTLY_PAGE = The value set ''{0}'' can''t be paged to offset {1} with count {2}: the server walks at most {3} codes to reach a page
1348
+ VALUESET_TOO_COSTLY_PAGE_REQUEST = The value set in the request can''t be paged to offset {0} with count {1}: the server walks at most {2} codes to reach a page
1330
1349
  VALUESET_TOO_COSTLY_COUNT = The value set ''{0}'' expansion has {2} codes, which is too many to display ({1})
1331
1350
  VALUESET_TOO_COSTLY_TIME = The value set ''{0}'' {2} took too long to process (>{1}sec)
1332
1351
  VALUESET_UNC_SYSTEM_WARNING = Unknown System ''{0}'' specified, so Concepts and Filters can''t be checked (Details: {1})
package/tx/README.md CHANGED
@@ -27,8 +27,8 @@ Add the `tx` section to your `config.json`:
27
27
  "librarySource": "/path/to/library.yml",
28
28
  "publishLibrarySource": false,
29
29
  "cacheTimeout": 30,
30
- "internalLimit" : 10000,
31
- "externalLimit" : 1000,
30
+ "internalLimit" : 100000,
31
+ "externalLimit" : 10000,
32
32
  "expansionCacheSize": 1000,
33
33
  "expansionCacheMemoryThreshold": 0,
34
34
  "endpoints": [
@@ -64,13 +64,96 @@ Add the `tx` section to your `config.json`:
64
64
  |---------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------|
65
65
  | `enabled` | boolean | Yes | Whether the module is enabled |
66
66
  | `cacheTimeout` | integer | No | How many minutes to keep client side caches (for cache-id parameter). Default: 30 |
67
+ | `cacheMaxConcepts` | integer | No | Concepts one client side cache (cache-id) may hold. Default: 250000, about 10x the largest IG Publisher session measured on build.fhir.org (24,048). A request that would take a cache over it is not performed: the server drops the cache and answers 404, an OperationOutcome with one issue coded `tx-issue-type#cache-id-unknown` (message id `CACHE_ID_DISCARDED_TOO_BIG`, saying the cache grew too large), as for any cache that is gone. The client starts a new cache and sends the request again; later requests with the old id get the same 404 `cache-id-unknown`, saying why it was dropped (`CACHE_ID_DISCARDED`). A request whose own resources hold more than this, so that no cache could take them, is refused instead (422 too-costly) and the cache is kept, as is a new cache too big to start. 0 = no limit |
68
+ | `cacheMaxTotalConcepts` | integer | No | Concepts all the client side caches of an endpoint may hold between them (about 250 bytes of heap each). Default: 1000000, 4 full caches, about 40x the largest session measured (24,048). The least recently used caches are dropped to stay under it; a request with a dropped cache-id gets a 404 `cache-id-unknown`, saying it was dropped to make room. 0 = no limit |
69
+ | `usageTrackerMaxCodes` | integer | No | Distinct codes the usage tracker (`usage-count` expansion property) keeps. Default: 1000000, 2x the about 500,000 codes the value sets of a full library list. Codes beyond it aren't counted (no error); codes already tracked still are. 0 = no limit |
70
+ | `usageTrackerMaxCodeLength` | integer | No | Longest code, in characters, the usage tracker keeps. Default: 100, 2.5x the longest code those value sets list (40). Longer codes aren't counted (no error). 0 = no limit |
71
+ | `languageTagCacheSize` | integer | No | Parsed language tags kept (least recently used dropped). Default: 1000; an 8 KB private use tag takes about 46 KB parsed, so at most about 46 MB. Clients see nothing but a re-parse. 0 = no limit (unique tags in requests then grow the heap forever) |
67
72
  | `expansionCacheSize` | integer | No | Maximum number of expanded ValueSets to cache. Default: 1000 |
68
73
  | `expansionCacheMemoryThreshold` | integer | No | Heap memory usage in MB that triggers evicting oldest half of expansion cache. 0 = disabled. Default: 0 |
74
+ | `expansionCacheMaxCodes` | integer | No | Most codes the expansion caches of all the endpoints hold between them; the least recently used expansions are dropped to stay under it. Default: a quarter of the heap limit at 1 KB a code (about 1 million codes with a 4 GB heap) |
69
75
  | `librarySource` | string | Yes | Path to the YAML file that defines the terminology sources to load |
70
76
  | `publishLibrarySource` | boolean | No | Publish that YAML file at `/{path}/library`. Default: false |
71
- | `internalLimit` | integer | No | Largest number of codes in internal expansions |
72
- | `externalLimit` | integer | No | Largest number of codes the server will return in an expansion |
77
+ | `pruneTerminologyCache` | boolean | No | Once the library and every endpoint's core packages have loaded, delete what in `terminology-cache` wasn't used: the `.db` and `.cache` files (the downloaded databases and SNOMED CT caches of sources no longer in the library, and the fallback of an `a\|b` source whose first file is there), with their SQLite side files; and the package folders (`id#version`: packages no longer in the library, older versions of ones that are, the core packages of FHIR versions no endpoint has, and what failed downloads left), with the value set and concept map caches in them. Other files and folders (`vsac`, `ocl`, `syndication`, `folder-source-*`) are left alone, and so is anything changed in the last day (it may be being copied in for a library that doesn't name it yet). In a cluster, only the master deletes. Default: false: everything run from the same data directory (a development server and the tests, say) shares `terminology-cache`, and what one doesn't use another may |
78
+ | `internalLimit` | integer | No | Most codes in the expansion of a value set that an `$expand` imports (`compose.include.valueSet`). Default: 100000. Over it, the `$expand` fails with 422 too-costly. Also the most the `x-too-costly-threshold` header can ask for. Other internal expansions (in `$validate-code`, for one) use 10000 and don't read the setting |
79
+ | `externalLimit` | integer | No | Most codes an `$expand` returns. Default: 10000 (or `internalLimit`, if that is less). Over it, an expansion asked for without `count` fails with 422 too-costly (`VALUESET_TOO_COSTLY`, "... has too many codes to produce (>10000)"); a `count` bigger than it is cut down to it. A request can change it with the `x-too-costly-threshold` header (see below): the tx-ecosystem tests that depend on a limit set it |
80
+ | `walkLimit` | integer | No | How far a paged `$expand` (`count` and `offset`) may walk to reach its page: the most `offset + count`. Default: 10 x `internalLimit` (1000000). Only the page is built - the codes before it are passed keeping just their keys - so a page can lie well past `externalLimit`, which still caps `count`. Over it, the `$expand` fails with 422 too-costly (`VALUESET_TOO_COSTLY_PAGE`, "... can't be paged to offset ... with count ...: the server walks at most 1000000 codes to reach a page"); a value set that ends before the offset is an empty page, not a refusal. The walk is also bounded by `operationTimeLimit`. Any page, the first too, builds only its own codes; the codes after it are counted by key for the total, which is reported only when the whole expansion is within `externalLimit`, as for an unpaged one. Not for a value set with an exclude, or a request with a text filter: there, `offset + count` must still be within `externalLimit` |
81
+ | `operationTimeLimit` | integer | No | Seconds of compute one operation may use (sub-operations share it). Default: 20. Time spent waiting - for the event loop, or in a queue before the operation starts - doesn't count. Over it, the operation fails with 422 too-costly ("Operation exceeded time limit of 20 seconds at ..."). The default sits under common client timeouts (okhttp gives up after 10-30 s): with a limit at or over the client's, the client hangs up first and never sees the OperationOutcome |
82
+ | `ecosystem` | object | No | Answering for code systems that aren't held here by asking other servers in the terminology ecosystem - see [Ecosystem Resolution](#ecosystem-resolution) below. Default: not used |
73
83
  | `endpoints` | array | Yes | List of endpoint configurations (at least one required) |
84
+ | `vsacCfg` | object | No | Settings for the VSAC value set provider; required if the library loads VSAC (see below) |
85
+
86
+ ### Ecosystem Resolution
87
+
88
+ When a code system (or a version of one) isn't held here, the server can ask the terminology
89
+ ecosystem registry which server has it, and ask that server (`cs/cs-virtual.js`). Only
90
+ `$validate-code` against a value set is supported this way, and slowly: a `$lookup` per code, and a
91
+ `$validate-code` per filter.
92
+
93
+ ```json
94
+ "ecosystem" : {
95
+ "use": true,
96
+ "excludedServers": [ "tx.fhir.org" ],
97
+ "excludedPatterns": [ "http://hl7.org/*", "http://terminology.hl7.org/*" ],
98
+ "apiKeys": { "hl7-ca" : "xx" },
99
+ "oauth" : {
100
+ "nzhts": {
101
+ "tokenEndpoint" : "https://authenticate.nzhts.digital.health.nz/auth/realms/nzhts/protocol/openid-connect/token",
102
+ "clientId" : "hl7",
103
+ "clientSecret" : "xx"
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ | Setting | Type | Description |
110
+ |---------|------|-------------|
111
+ | `use` | boolean | Turns it on. Default: false |
112
+ | `registry` | string | The ecosystem registry. Default: `https://ecosystem.hl7.org/tx-reg` |
113
+ | `excludedServers` | array | Servers never used, each a host (`tx.fhir.org`) or an address that the server's address starts with. tx.fhir.org (and tx-dev.fhir.org, which serves the same content) sets this to `["tx.fhir.org"]`: the registry names tx.fhir.org for what `tx.fhir.org.yml` loads, so that's this server, logically, whatever its address is, and asking it again can only give the answer this server already gave |
114
+ | `excludedPatterns` | array | Code systems never looked for in the ecosystem, as the registry writes them: `*` matches anything, and a pattern is matched against the url, and the url\|version. These aren't even asked about, so they cost nothing: a server that knows the registry will name itself (or a server in `excludedServers`) for them should list them |
115
+ | `apiKeys` | object | Server code (as in the registry) -> API key, sent as `Api-Key`. As for the registry module |
116
+ | `oauth` | object | Server code -> `{ tokenEndpoint, clientId, clientSecret, scope }`: an OAuth2 client_credentials grant, the token sent as `Authorization: Bearer`. As for the registry module |
117
+ | `allowPrivateAddresses` | boolean | Lets requests go to private, loopback and other non-public addresses. Default: false - the registry, the servers it names, and the OAuth token endpoints must all be at public addresses, because the addresses come from the registry, which may not be the public one (as for the registry module's setting of the same name). For a local registry or test servers |
118
+
119
+ A server is used if it's open, or if there are credentials for it. The server must also say, in its
120
+ TerminologyCapabilities, that it has the code system and version asked for (or, for a version that
121
+ is a url, a release of it - a SNOMED CT edition asked for without a release date). With no version
122
+ asked for, it's the server's only version, or the one it marks `isDefault`. Requests sent on to
123
+ another server carry the header `X-FHIRsmith-Ecosystem-Forwarded`, and a request that arrives with
124
+ it is never resolved through the ecosystem again.
125
+
126
+ What the registry says, and what each server says it has, is kept for an hour, and then refreshed
127
+ in the background: a request never waits for a refresh, only the first request for something does
128
+ (if a refresh fails, the old answer is used, and the refresh tried again in five minutes). The
129
+ other servers' answers to `$lookup` and `$validate-code` are kept for an hour too (the most recent
130
+ 10,000), so the same question is only asked once. Requests that had to ask another server are
131
+ counted, and graphed, separately on the home page (External requests).
132
+
133
+ ### VSAC Configuration
134
+
135
+ `vsacCfg` configures the VSAC provider (a `vsac` source in the library). The value sets are
136
+ synced from VSAC into a SQLite database in the terminology cache folder, and read from it when
137
+ they are asked for; only the recently used ones are kept parsed in memory.
138
+
139
+ | Option | Type | Required | Description |
140
+ |------------------------|---------|----------|---------------------------------------------------------------------------------------------------|
141
+ | `apiKey` | string | Yes | UMLS API key for VSAC |
142
+ | `baseUrl` | string | No | VSAC FHIR server. Default: `https://cts.nlm.nih.gov/fhir` |
143
+ | `refreshIntervalHours` | number | No | Hours between syncs. A failed sync is retried after 1, 5, then every 30 minutes. Default: 24 |
144
+ | `timeoutMs` | integer | No | Timeout for requests to VSAC, in milliseconds. Default: 120000 |
145
+ | `resyncPassword` | string | No | If set, the /info page offers a form (gated by this password) to resync one value set now |
146
+ | `valueSetCacheMB` | number | No | Size of the recently used value sets kept parsed, measured by their stored JSON; 0 keeps none. Default: 64 |
147
+
148
+ `valueSetCacheMB` is measured in stored JSON, not memory: parsed value sets take several
149
+ times as much heap as their JSON (so expect a few hundred MB of heap at the default), and in
150
+ cluster mode every worker keeps its own.
151
+
152
+ `externalLimit`, `internalLimit` and `walkLimit` are checked at startup: each, when set, must be a whole number above 0, and `externalLimit` <= `internalLimit` <= `walkLimit`; anything else stops the server with an error saying which. The limits in use are logged at startup. Every client gets the same limits: the tx-ecosystem test runner (User-Agent `Tools/Java`) used to be held to the old defaults (3000 and 10000) whatever the configuration; now the tests that depend on a limit ask for it with the header.
153
+
154
+ The `x-too-costly-threshold` request header sets the `externalLimit` for that request, for any client. It is there so test cases can ask for a lower limit: it can't be more than the `internalLimit` (a bigger value is cut down to it), and a value that isn't a positive whole number is ignored, so there is always a limit.
155
+
156
+ The largest request body the server accepts is a server setting, `server.maxBodyMB` (see the main README); a tx endpoint answers a bigger one with a 413 OperationOutcome (too-costly).
74
157
 
75
158
  ### Endpoint Configuration
76
159
 
@@ -84,6 +167,24 @@ Each endpoint in the `endpoints` array has:
84
167
 
85
168
  **Note:** Endpoint paths must be unique. The module will throw an error if duplicate paths are configured or if a path conflicts with another module.
86
169
 
170
+ ### Fixed limits
171
+
172
+ These are constants in the code, not settings; they are listed so their effects can be recognised.
173
+
174
+ | Limit | Value | What it bounds |
175
+ |---|---|---|
176
+ | Memory per code, for the default of `expansionCacheMaxCodes` | 1 KB | only the derived default (`operation-context.js`) |
177
+ | Share of the heap, for the default of `expansionCacheMaxCodes` | 0.25 | a quarter of the V8 heap limit, per process |
178
+ | Shortest expansion that is cached | 250 ms | expansions that took less are not kept in the expansion cache |
179
+ | Longest compute before yielding the event loop | 10 ms | how long one operation can hold up the others (`operation-context.js`, `YIELD_INTERVAL`) |
180
+ | ECL nesting depth | 100 | bracketed expressions and refinements; deeper is a parse error |
181
+ | ECL sub-expressions | 500 | focus concepts, refinement values, ... (a long `A OR B OR ...` reaches it first); more is a parse error |
182
+ | Resource self-nesting depth | 100 | `CodeSystem.concept` and `ValueSet.expansion.contains` (SNOMED CT is about 30 deep); deeper is a 400 |
183
+ | XML element depth | 1000 | a deeper XML body is an error |
184
+ | LOINC SQLite `cache_size` | 10000 pages (about 40 MB) | page cache, per database connection |
185
+ | LOINC SQLite `mmap_size` | 256 MB | memory-mapped reads, per database connection; file-backed pages, shared between connections |
186
+ | Forced GC back-off | 60 s at most | while the memory guard is refusing requests, the wait between forced GCs doubles up to this (server wide, `library/memory-guard.js`) |
187
+
87
188
  ## Supported Operations
88
189
 
89
190
  Each endpoint provides the following FHIR terminology operations:
@@ -258,6 +359,52 @@ actually returns (metadata, read and search, `$expand`, `$validate-code`, `$look
258
359
  `$subsumes`, `$batch-validate-code`, `$versions`, `$cache-control`, an error) against the
259
360
  schemas, using ajv. So a new parameter needs adding to openapi-operations.js.
260
361
 
362
+ ## Loading SNOMED CT
363
+
364
+ By default each SNOMED CT cache file is read into the server process, at about its file size
365
+ (0.6–1.2 GB an edition). A file can instead be memory-mapped read-only:
366
+
367
+ - the edition loads in milliseconds;
368
+ - only the pages that requests touch become resident, as page cache that the kernel can drop
369
+ and read again;
370
+ - every server process on the machine that maps the same file shares those pages.
371
+
372
+ The first request to touch a part of a mapped edition waits for the disk, unless that edition
373
+ is *prewarmed*: mapped, then read through once in the background at startup, so its pages are
374
+ already in the page cache when requests arrive. Startup doesn't wait for it.
375
+
376
+ This is set per server, in the tx module's `snomed` block in `config.json`:
377
+
378
+ ```json
379
+ "tx": {
380
+ "snomed": {
381
+ "load": "mmap",
382
+ "prewarm": ["http://snomed.info/sct/900000000000207008/version/20250201",
383
+ "sct_us_20250901b.cache"],
384
+ "read": []
385
+ }
386
+ }
387
+ ```
388
+
389
+ - **`load`** is `"read"` (the default) or `"mmap"`, for every edition not named below.
390
+ - **`prewarm`** lists editions to prewarm (map, then read through once in the background at
391
+ startup), even when `load` is `"read"`.
392
+ - **`read`** lists editions to read into the process even when `load` is `"mmap"`.
393
+ - An entry is any of:
394
+ - a cache file name (`sct_us_20250901b.cache`)
395
+ - an edition version URI (`http://snomed.info/sct/731000124108/version/20250901`)
396
+ - an edition URI (`http://snomed.info/sct/731000124108`), which covers every loaded version
397
+ of that edition
398
+ - `read` wins over `prewarm`.
399
+ - Entries that match no loaded edition are warned about at startup.
400
+ - With no `snomed` block, everything is read, as before.
401
+
402
+ Mapping uses the optional `koffi` dependency. Where it is not available (Windows, 32-bit
403
+ platforms, or koffi not installed), a mapped edition is read instead, with a warning in the
404
+ server log. The log says, for each edition that was to be mapped, whether it really was. When
405
+ any edition is mapped, the memory guard leaves file-backed pages out of the memory it measures
406
+ (Linux).
407
+
261
408
  ## Library Configuration
262
409
 
263
410
  The library - the source to load - is configured using a YAML file.
@@ -312,6 +459,10 @@ THO's v3-ActCode code system is `CodeSystem/tho-v3-ActCode`. No resource is serv
312
459
  - `core` is the FHIR core package each endpoint loads for its FHIR version (`CodeSystem/core-administrative-gender`), and `x` is the code systems the server implements natively (`CodeSystem/x-...`); neither can be given as a prefix
313
460
  - Sources that only provide code systems natively (`internal`, `snomed`, `loinc` etc.) don't serve resources by id, so a prefix does nothing for them
314
461
 
462
+ #### hl7.terminology and the FHIR core packages
463
+
464
+ Much of the content of the FHIR core packages has moved to terminology.hl7.org (`hl7.terminology`), and its versions were reset when it moved (`coverage-selfpay` is 4.0.1 in `hl7.fhir.r4.core`, and 1.0.1 in `hl7.terminology`), so version order can't decide between the two. Where `hl7.terminology` provides a code system, value set or concept map with the same url as the endpoint's core package, THO's is always the one a reference with no version gets. The core package's versions are still available to a reference that names them (`http://hl7.org/fhir/concept-property-type|5.0.0`), unless THO provides that same version itself. Core value sets and concept maps that THO also provides are not read by id, found by search, or used for `$translate` unless asked for by version.
465
+
315
466
  ### Source Types
316
467
 
317
468
  #### `internal` - Built-in Code Systems
@@ -404,6 +555,9 @@ Common edition identifiers:
404
555
 
405
556
  The file is built by importing SNOMED CT (see [documentation](importers/readme.md))
406
557
 
558
+ How the cache files are loaded is a deployment choice, so it is set in `config.json` (the
559
+ tx module's `snomed` block), not here - see [Loading SNOMED CT](#loading-snomed-ct).
560
+
407
561
 
408
562
  #### `cpt` - CPT (Current Procedural Terminology)
409
563