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
@@ -5,6 +5,7 @@ const chalk = require('chalk');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { getConfigManager } = require('./tx-import-settings');
8
+ const terminal = require('./terminal');
8
9
 
9
10
  class TerminologyImportCLI {
10
11
  constructor() {
@@ -71,7 +72,7 @@ class TerminologyImportCLI {
71
72
  }
72
73
 
73
74
  discoverAndLoadModules() {
74
- console.log("Looking in " + __dirname);
75
+ terminal.log("Looking in " + __dirname);
75
76
 
76
77
  const moduleFiles = fs.readdirSync(__dirname)
77
78
  .filter(file => file.endsWith('.module.js') && !file.startsWith('_'))
@@ -107,10 +108,10 @@ class TerminologyImportCLI {
107
108
  if (this.isValidModule(moduleInstance)) {
108
109
  this.registerModule(moduleInstance);
109
110
  } else {
110
- console.warn(chalk.yellow(`Warning: Invalid module format in ${moduleFile}`));
111
+ terminal.warn(chalk.yellow(`Warning: Invalid module format in ${moduleFile}`));
111
112
  }
112
113
  } catch (error) {
113
- console.warn(chalk.yellow(`Warning: Failed to load module ${moduleFile}: ${error.message}`));
114
+ terminal.warn(chalk.yellow(`Warning: Failed to load module ${moduleFile}: ${error.message}`));
114
115
  }
115
116
  }
116
117
  }
@@ -127,7 +128,7 @@ class TerminologyImportCLI {
127
128
  const name = module.getName();
128
129
 
129
130
  if (this.modules.has(name)) {
130
- console.warn(chalk.yellow(`Warning: Module ${name} already registered, skipping`));
131
+ terminal.warn(chalk.yellow(`Warning: Module ${name} already registered, skipping`));
131
132
  return;
132
133
  }
133
134
 
@@ -141,35 +142,35 @@ class TerminologyImportCLI {
141
142
  // Let the module register its subcommands
142
143
  module.registerCommands(terminologyCommand, this.program.opts());
143
144
 
144
- console.log(chalk.green(`✓ Loaded ${name} module`));
145
+ terminal.log(chalk.green(`✓ Loaded ${name} module`));
145
146
  }
146
147
 
147
148
  listTerminologies() {
148
- console.log(chalk.blue.bold('🏥 Available Terminology Importers\\n'));
149
+ terminal.log(chalk.blue.bold('🏥 Available Terminology Importers\\n'));
149
150
 
150
151
  if (this.modules.size === 0) {
151
- console.log(chalk.yellow('No terminology modules found.'));
152
- console.log(chalk.gray('Add modules to the ./modules/ directory.'));
152
+ terminal.log(chalk.yellow('No terminology modules found.'));
153
+ terminal.log(chalk.gray('Add modules to the ./modules/ directory.'));
153
154
  return;
154
155
  }
155
156
 
156
157
  for (const [name, module] of this.modules) {
157
- console.log(chalk.cyan(`${name}:`));
158
- console.log(` ${module.getDescription()}`);
158
+ terminal.log(chalk.cyan(`${name}:`));
159
+ terminal.log(` ${module.getDescription()}`);
159
160
 
160
161
  if (module.getSupportedFormats) {
161
162
  const formats = module.getSupportedFormats();
162
- console.log(chalk.gray(` Formats: ${formats.join(', ')}`));
163
+ terminal.log(chalk.gray(` Formats: ${formats.join(', ')}`));
163
164
  }
164
165
 
165
166
  if (module.getEstimatedDuration) {
166
- console.log(chalk.gray(` Typical duration: ${module.getEstimatedDuration()}`));
167
+ terminal.log(chalk.gray(` Typical duration: ${module.getEstimatedDuration()}`));
167
168
  }
168
169
 
169
- console.log('');
170
+ terminal.log('');
170
171
  }
171
172
 
172
- console.log(chalk.gray('Use "tx-import <terminology> --help" for specific options'));
173
+ terminal.log(chalk.gray('Use "tx-import <terminology> --help" for specific options'));
173
174
  }
174
175
 
175
176
  showHelp(commandName) {
@@ -179,27 +180,27 @@ class TerminologyImportCLI {
179
180
  }
180
181
 
181
182
  if (this.modules.has(commandName)) {
182
- console.log(chalk.blue.bold(`Help for ${commandName}:\\n`));
183
+ terminal.log(chalk.blue.bold(`Help for ${commandName}:\\n`));
183
184
  this.program.commands
184
185
  .find(cmd => cmd.name() === commandName)
185
186
  ?.help();
186
187
  } else {
187
- console.log(chalk.red(`Unknown command: ${commandName}`));
188
+ terminal.log(chalk.red(`Unknown command: ${commandName}`));
188
189
  this.listTerminologies();
189
190
  }
190
191
  }
191
192
 
192
193
  showConfigHelp() {
193
- console.log(chalk.blue.bold('📁 Configuration Management\\n'));
194
- console.log('Available configuration commands:');
195
- console.log(' tx-import config:show Show saved configuration history');
196
- console.log(' tx-import config:clear Clear saved configuration history');
197
- console.log(' tx-import config:export Export configuration to file');
198
- console.log(' tx-import config:import Import configuration from file');
199
- console.log('\\nExamples:');
200
- console.log(' tx-import config:show --terminology unii');
201
- console.log(' tx-import config:clear --terminology loinc');
202
- console.log(' tx-import config:export --output my-config.json');
194
+ terminal.log(chalk.blue.bold('📁 Configuration Management\\n'));
195
+ terminal.log('Available configuration commands:');
196
+ terminal.log(' tx-import config:show Show saved configuration history');
197
+ terminal.log(' tx-import config:clear Clear saved configuration history');
198
+ terminal.log(' tx-import config:export Export configuration to file');
199
+ terminal.log(' tx-import config:import Import configuration from file');
200
+ terminal.log('\\nExamples:');
201
+ terminal.log(' tx-import config:show --terminology unii');
202
+ terminal.log(' tx-import config:clear --terminology loinc');
203
+ terminal.log(' tx-import config:export --output my-config.json');
203
204
  }
204
205
 
205
206
  showConfig(options) {
@@ -210,30 +211,30 @@ class TerminologyImportCLI {
210
211
  const config = configManager.getPreviousConfig(options.terminology);
211
212
 
212
213
  if (Object.keys(config).length === 0) {
213
- console.log(chalk.yellow(`No saved configuration found for ${options.terminology}`));
214
+ terminal.log(chalk.yellow(`No saved configuration found for ${options.terminology}`));
214
215
  return;
215
216
  }
216
217
 
217
- console.log(chalk.blue.bold(`📋 Saved Configuration for ${options.terminology}:\\n`));
218
+ terminal.log(chalk.blue.bold(`📋 Saved Configuration for ${options.terminology}:\\n`));
218
219
 
219
220
  if (config.lastUsed) {
220
- console.log(`Last used: ${chalk.white(new Date(config.lastUsed).toLocaleString())}`);
221
+ terminal.log(`Last used: ${chalk.white(new Date(config.lastUsed).toLocaleString())}`);
221
222
  }
222
223
 
223
224
  Object.entries(config).forEach(([key, value]) => {
224
225
  if (key !== 'lastUsed' && key !== 'recentSources') {
225
226
  if (Array.isArray(value)) {
226
- console.log(` ${key}: ${chalk.white(value.join(', '))}`);
227
+ terminal.log(` ${key}: ${chalk.white(value.join(', '))}`);
227
228
  } else {
228
- console.log(` ${key}: ${chalk.white(value)}`);
229
+ terminal.log(` ${key}: ${chalk.white(value)}`);
229
230
  }
230
231
  }
231
232
  });
232
233
 
233
234
  if (config.recentSources && config.recentSources.length > 0) {
234
- console.log(`\\n Recent sources:`);
235
+ terminal.log(`\\n Recent sources:`);
235
236
  config.recentSources.forEach((src, index) => {
236
- console.log(` ${index + 1}. ${chalk.gray(src)}`);
237
+ terminal.log(` ${index + 1}. ${chalk.gray(src)}`);
237
238
  });
238
239
  }
239
240
 
@@ -242,24 +243,24 @@ class TerminologyImportCLI {
242
243
  const allHistory = configManager.history;
243
244
 
244
245
  if (Object.keys(allHistory).length === 0) {
245
- console.log(chalk.yellow('No saved configurations found'));
246
+ terminal.log(chalk.yellow('No saved configurations found'));
246
247
  return;
247
248
  }
248
249
 
249
- console.log(chalk.blue.bold('📋 All Saved Configurations:\\n'));
250
+ terminal.log(chalk.blue.bold('📋 All Saved Configurations:\\n'));
250
251
 
251
252
  Object.entries(allHistory).forEach(([terminology, config]) => {
252
- console.log(chalk.cyan(`${terminology}:`));
253
+ terminal.log(chalk.cyan(`${terminology}:`));
253
254
  if (config.lastUsed) {
254
- console.log(` Last used: ${chalk.white(new Date(config.lastUsed).toLocaleString())}`);
255
+ terminal.log(` Last used: ${chalk.white(new Date(config.lastUsed).toLocaleString())}`);
255
256
  }
256
257
  if (config.source) {
257
- console.log(` Source: ${chalk.gray(config.source)}`);
258
+ terminal.log(` Source: ${chalk.gray(config.source)}`);
258
259
  }
259
260
  if (config.version) {
260
- console.log(` Version: ${chalk.gray(config.version)}`);
261
+ terminal.log(` Version: ${chalk.gray(config.version)}`);
261
262
  }
262
- console.log('');
263
+ terminal.log('');
263
264
  });
264
265
  }
265
266
  }
@@ -278,9 +279,9 @@ class TerminologyImportCLI {
278
279
 
279
280
  if (confirmed) {
280
281
  configManager.clearAllHistory();
281
- console.log(chalk.green('✓ All configuration history cleared'));
282
+ terminal.log(chalk.green('✓ All configuration history cleared'));
282
283
  } else {
283
- console.log(chalk.yellow('Configuration clear cancelled'));
284
+ terminal.log(chalk.yellow('Configuration clear cancelled'));
284
285
  }
285
286
 
286
287
  } else if (options.terminology) {
@@ -293,14 +294,14 @@ class TerminologyImportCLI {
293
294
 
294
295
  if (confirmed) {
295
296
  configManager.clearHistory(options.terminology);
296
- console.log(chalk.green(`✓ Configuration history cleared for ${options.terminology}`));
297
+ terminal.log(chalk.green(`✓ Configuration history cleared for ${options.terminology}`));
297
298
  } else {
298
- console.log(chalk.yellow('Configuration clear cancelled'));
299
+ terminal.log(chalk.yellow('Configuration clear cancelled'));
299
300
  }
300
301
 
301
302
  } else {
302
- console.log(chalk.red('Please specify --terminology <name> or --all'));
303
- console.log('Example: tx-import config:clear --terminology unii');
303
+ terminal.log(chalk.red('Please specify --terminology <name> or --all'));
304
+ terminal.log('Example: tx-import config:clear --terminology unii');
304
305
  }
305
306
  }
306
307
 
@@ -310,9 +311,9 @@ class TerminologyImportCLI {
310
311
 
311
312
  try {
312
313
  fs.writeFileSync(options.output, JSON.stringify(data, null, 2));
313
- console.log(chalk.green(`✓ Configuration exported to ${options.output}`));
314
+ terminal.log(chalk.green(`✓ Configuration exported to ${options.output}`));
314
315
  } catch (error) {
315
- console.log(chalk.red(`✗ Export failed: ${error.message}`));
316
+ terminal.log(chalk.red(`✗ Export failed: ${error.message}`));
316
317
  }
317
318
  }
318
319
 
@@ -333,16 +334,16 @@ class TerminologyImportCLI {
333
334
  try {
334
335
  const data = JSON.parse(fs.readFileSync(options.input, 'utf8'));
335
336
  configManager.importConfig(data);
336
- console.log(chalk.green(`✓ Configuration imported from ${options.input}`));
337
+ terminal.log(chalk.green(`✓ Configuration imported from ${options.input}`));
337
338
  } catch (error) {
338
- console.log(chalk.red(`✗ Import failed: ${error.message}`));
339
+ terminal.log(chalk.red(`✗ Import failed: ${error.message}`));
339
340
  }
340
341
  }
341
342
 
342
343
  run() {
343
344
  // Show logo and loaded modules
344
- console.log(chalk.blue.bold('🏥 TX-Import - Medical Terminology Import Tool'));
345
- console.log(chalk.gray(`Loaded ${this.modules.size} terminology module(s)\\n`));
345
+ terminal.log(chalk.blue.bold('🏥 TX-Import - Medical Terminology Import Tool'));
346
+ terminal.log(chalk.gray(`Loaded ${this.modules.size} terminology module(s)\\n`));
346
347
 
347
348
  this.program.parse();
348
349
  }
@@ -1,3 +1,4 @@
1
+ const crypto = require("crypto");
1
2
  const {VersionUtilities, VersionPrecision} = require("../../library/version-utilities");
2
3
  const {Issue} = require("./operation-outcome");
3
4
 
@@ -80,6 +81,54 @@ class CanonicalResource {
80
81
  checkContained(this.jsonObj);
81
82
  }
82
83
 
84
+ /**
85
+ * The hash ExpansionCache.computeKey puts in an expansion's cache key for this
86
+ * resource: the first 16 hex characters of the sha256 of JSON.stringify(json). Computing
87
+ * it means serialising the whole resource, and a $cache-control cache hands every
88
+ * resource it holds to every $expand that carries its cache-id, so it is worked out
89
+ * once and remembered.
90
+ *
91
+ * @param {Object} json - the resource (R5 form)
92
+ * @returns {string}
93
+ */
94
+ static computeExpansionKeyHash(json) {
95
+ return crypto.createHash('sha256')
96
+ .update(JSON.stringify(json))
97
+ .digest('hex')
98
+ .substring(0, 16);
99
+ }
100
+
101
+ /**
102
+ * This resource's expansion-key hash (see computeExpansionKeyHash), computed on first
103
+ * use and then remembered. Nothing writes to jsonObj once a resource is in use (the r4/r5 conversion
104
+ * happens in the constructor, before anything can ask), and the memo is tied to the
105
+ * jsonObj it was computed from, so replacing jsonObj gets a new hash.
106
+ * @returns {string}
107
+ */
108
+ expansionKeyHash() {
109
+ const memo = this._expansionKeyHashMemo;
110
+ if (memo && memo.json === this.jsonObj) {
111
+ return memo.hash;
112
+ }
113
+ return this.setExpansionKeyHash(CanonicalResource.computeExpansionKeyHash(this.jsonObj));
114
+ }
115
+
116
+ /**
117
+ * Record the expansion-key hash for a store that already knows it, so it is never
118
+ * recomputed. It must be computeExpansionKeyHash(this.jsonObj). Non-enumerable, so it never serialises
119
+ * into a response or log, and structuredClone/spread don't copy it onto a different
120
+ * resource.
121
+ * @param {string} hash
122
+ * @returns {string} the hash
123
+ */
124
+ setExpansionKeyHash(hash) {
125
+ Object.defineProperty(this, '_expansionKeyHashMemo', {
126
+ value: { json: this.jsonObj, hash },
127
+ enumerable: false, configurable: true, writable: true
128
+ });
129
+ return hash;
130
+ }
131
+
83
132
  get resourceType() {
84
133
  return this.jsonObj.resourceType;
85
134
  }
@@ -4,6 +4,7 @@ const {codeSystemFromR5, codeSystemToR5} = require("../xversion/xv-codesystem");
4
4
  const {getValuePrimitive} = require("../../library/utilities");
5
5
  const {VersionUtilities} = require("../../library/version-utilities");
6
6
  const {countNested} = require("./nesting-limit");
7
+ const regexUtilities = require("../../library/regex-utilities");
7
8
 
8
9
  const CodeSystemContentMode = Object.freeze({
9
10
  Complete: 'complete',
@@ -644,21 +645,109 @@ class CodeSystem extends CanonicalResource {
644
645
  this.jsonObj.id = value;
645
646
  }
646
647
 
648
+ /**
649
+ * @returns {string|null} the supplement's type, from the codesystem-supplement-type extension
650
+ * (lang-pack, properties, extension-codes...), or null if it doesn't say
651
+ */
652
+ supplementType() {
653
+ const ext = (this.jsonObj.extension || []).find(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type');
654
+ return ext ? getValuePrimitive(ext) : null;
655
+ }
656
+
657
+ /**
658
+ * @returns {boolean} true if this is a language pack (codesystem-supplement-type = lang-pack). A
659
+ * language pack is only used when it is referenced, unless it is also global (isGlobalSupplement)
660
+ */
647
661
  isLangPack() {
648
- // todo: this is a temporary work around until fhir.tx.support.r4#0.37.0 is released that properly marks this as a language pack
662
+ return this.supplementType() === 'lang-pack';
663
+ }
664
+
665
+ /**
666
+ * A global supplement is used for the code system it supplements whether or not it is
667
+ * referenced: a lang-pack or extension-codes supplement marked with codesystem-supplement-global
668
+ * = true. Any other supplement is only used when it is referenced.
669
+ *
670
+ * @returns {boolean} true if this is a global supplement
671
+ */
672
+ isGlobalSupplement() {
673
+ // todo: a work around until fhir.tx.support.r4 is re-released with this language pack marked global
649
674
  if (this.jsonObj.url === 'https://terminology.dhp.uz/fhir/CodeSystem/loinc-supplement-uz') {
650
675
  return true;
651
676
  }
652
- // a global language pack is used whether or not it is referenced. The code was 'lang-pack'
653
- // until Oct 2026; it is still accepted, so that supplements marked the old way keep working
654
- return (this.jsonObj.extension || []).find(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-type'
655
- && ['global-lang-pack', 'lang-pack'].includes(getValuePrimitive(x)));
677
+ if (!['lang-pack', 'extension-codes'].includes(this.supplementType())) {
678
+ return false;
679
+ }
680
+ const ext = (this.jsonObj.extension || []).find(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-supplement-global');
681
+ return !!ext && getValuePrimitive(ext) === true;
682
+ }
683
+
684
+ /**
685
+ * @returns {boolean} true if this is an extension-codes supplement (codesystem-supplement-type) - one
686
+ * that defines codes in the reserved code space of the code system it supplements
687
+ */
688
+ isExtensionCodes() {
689
+ return this.supplementType() === 'extension-codes';
690
+ }
691
+
692
+ /**
693
+ * The code spaces the code system's steward has reserved for users to define their own codes in (the
694
+ * codesystem-reservedCodeSpace extension: reservedCodeSpace is a regular expression that a code must
695
+ * match in full, and default-display is the display of a code in the space that nothing defines -
696
+ * 'User-assigned' if it doesn't say). Compiled with re2, since the expressions can come from a request
697
+ *
698
+ * @returns {{regex: Object, defaultDisplay: string}[]} the code spaces (empty if there are none)
699
+ */
700
+ reservedCodeSpaces() {
701
+ if (this._reservedCodeSpaces === undefined) {
702
+ this._reservedCodeSpaces = [];
703
+ for (const ext of (this.jsonObj.extension || []).filter(x => x.url == 'http://hl7.org/fhir/StructureDefinition/codesystem-reservedCodeSpace')) {
704
+ const rcs = (ext.extension || []).find(x => x.url == 'reservedCodeSpace');
705
+ const pattern = rcs ? getValuePrimitive(rcs) : null;
706
+ if (pattern) {
707
+ const dd = (ext.extension || []).find(x => x.url == 'default-display');
708
+ try {
709
+ this._reservedCodeSpaces.push({
710
+ regex: regexUtilities.compile('^(?:' + pattern + ')$', this.caseInsensitive() ? 'i' : ''),
711
+ defaultDisplay: (dd && getValuePrimitive(dd)) || 'User-assigned'
712
+ });
713
+ } catch (e) {
714
+ // an expression that doesn't compile reserves nothing
715
+ }
716
+ }
717
+ }
718
+ }
719
+ return this._reservedCodeSpaces;
720
+ }
721
+
722
+ /**
723
+ * @param {string} code - the code
724
+ * @returns {{regex: Object, defaultDisplay: string}|null} the reserved code space the code is in, if it is in one
725
+ */
726
+ reservedCodeSpaceFor(code) {
727
+ return code ? (this.reservedCodeSpaces().find(rcs => rcs.regex.test(code)) || null) : null;
728
+ }
729
+
730
+ /**
731
+ * @param {string} code - the code
732
+ * @returns {boolean} true if the code is in a code space reserved for users to define their own codes
733
+ */
734
+ isInReservedCodeSpace(code) {
735
+ return this.reservedCodeSpaceFor(code) != null;
656
736
  }
657
737
 
658
738
  isPropUri(code, uri) {
659
739
  return (this.jsonObj.property || []).find(x => x.code == code && x.uri == uri);
660
740
  }
661
741
 
742
+ /**
743
+ * The canonical of the code system this one supplements (CodeSystem.supplements),
744
+ * or undefined if it isn't a supplement.
745
+ * @returns {string|undefined}
746
+ */
747
+ get supplementsUrl() {
748
+ return this.jsonObj.supplements;
749
+ }
750
+
662
751
  isSupplementFor(url, version) {
663
752
  if (this.jsonObj.content !== 'supplement') {
664
753
  return false;
@@ -82,10 +82,43 @@ class SearchFilterText {
82
82
  return returnRating ? {passes: false, rating: 0} : false;
83
83
  }
84
84
 
85
+ /**
86
+ * Whether every word of the filter is in the value, a word matching as in passes() (its
87
+ * stem starts with the filter word's stem). passes() needs only one word
88
+ */
89
+ passesAll(value) {
90
+ validateParameter(value, 'value', String);
91
+
92
+ if (this.isNull) {
93
+ return true;
94
+ }
95
+
96
+ const stems = [];
97
+ let i = 0;
98
+ while (i < value.length) {
99
+ if (this._isAlphaNumeric(value[i])) {
100
+ const j = i;
101
+ while (i < value.length && this._isAlphaNumeric(value[i])) {
102
+ i++;
103
+ }
104
+ stems.push(this.stemmer.stem(value.substring(j, i).toLowerCase()));
105
+ } else {
106
+ i++;
107
+ }
108
+ }
109
+ return this.stems.every(filterStem => stems.some(stem => stem.startsWith(filterStem)));
110
+ }
111
+
85
112
  /**
86
113
  * Check if designations pass the filter
87
114
  */
88
- passesDesignations(cds) {
115
+ /**
116
+ * Whether any of the designations passes the filter. With a search language (see
117
+ * CodeSystemProvider.searchLanguage), only designations in that language take part,
118
+ * compared at the language subtag level; one that doesn't say is in `fallback` (the
119
+ * code system's language)
120
+ */
121
+ passesDesignations(cds, searchLang = null, fallback = 'en') {
89
122
  validateOptionalParameter(cds, 'cds', Designations);
90
123
 
91
124
  if (this.isNull) {
@@ -95,13 +128,23 @@ class SearchFilterText {
95
128
  if (!cds) return false;
96
129
 
97
130
  for (const cd of cds.designations) {
98
- if (cd.value && this.passes(cd.value)) {
131
+ if (cd.value && (!searchLang || SearchFilterText.#languageOf(cd.language, fallback) === searchLang.language) && this.passes(cd.value)) {
99
132
  return true;
100
133
  }
101
134
  }
102
135
  return false;
103
136
  }
104
137
 
138
+ static #languageOf(lang, fallback) {
139
+ if (lang && lang.language) {
140
+ return lang.language;
141
+ }
142
+ if (typeof lang === 'string' && lang) {
143
+ return lang.split('-')[0].toLowerCase();
144
+ }
145
+ return (fallback || 'en').split('-')[0].toLowerCase();
146
+ }
147
+
105
148
  /**
106
149
  * Calculate match score against stems
107
150
  */
@@ -25,10 +25,17 @@ const Extensions = {
25
25
  resource = resource.jsonObj
26
26
  }
27
27
  if (resource.implicitRules && 'https://hl7.org/fhir/implicit-rules/omitted-mandatory-interversion-code' != resource.implicitRules) {
28
- throw new Issue("error", "business-rule", null, null, 'Cannot process resource "'+name+'" due to the presence of implicit rules \''+resource.implicitRules+'\' @'+place);
28
+ throw new Issue("error", "business-rule", null, null, 'Cannot process resource "'+name+'" due to the presence of implicit rules \''+resource.implicitRules+'\' @'+place, null, 422);
29
29
  }
30
30
  },
31
31
 
32
+ /**
33
+ * @param {Object} element - what may carry modifier extensions
34
+ * @param {string} place - the code doing the check (unused, for reading)
35
+ * @param {string} name - the element's path (FHIRPath, e.g. ValueSet.compose.include[0].filter[1]),
36
+ * reported as the issue's expression
37
+ * @param {string} resource - the value set's url|version
38
+ */
32
39
  checkNoModifiers(element, place, name, resource) {
33
40
  if (!element) {
34
41
  return;
@@ -41,13 +48,12 @@ const Extensions = {
41
48
  for (const extension of element.modifierExtension) {
42
49
  urls.add(extension.url);
43
50
  }
51
+ // a modifier extension changes the meaning of what it's on, so an unknown one means the value
52
+ // set can't be processed: a problem with the request's content, not the server (422)
44
53
  const resId = resource ? resource : "";
45
- const urlList = [...urls].join('\', \'');
46
- if (urls.size > 1) {
47
- throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of modifier extensions '+urlList);
48
- } else {
49
- throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of the modifier extension '+urlList);
50
- }
54
+ throw new Issue("error", "business-rule", name, 'VALUESET_MODIFIER_EXTENSION_UNKNOWN',
55
+ "The value set " + resId + " can't be processed: " + name + " has a modifier extension that isn't understood (" + [...urls].join(', ') + ")",
56
+ 'vs-invalid', 422).handleAsOO(422);
51
57
  }
52
58
  return true;
53
59
  },
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Choosing which version of a code system an implicit value set URL is drawn from, when
3
+ * several versions are loaded (SNOMED CT editions, typically). A factory that can say what
4
+ * form an implicit value set URL takes implements implicitValueSetForm(url), returning
5
+ * {form: 'generic'|'edition'|'version'} or null:
6
+ *
7
+ * generic (http://snomed.info/sct?fhir_vs...) - the version the request asks for
8
+ * (system-version and the like, through systemVersionFor) where a loaded version
9
+ * matches it, else the default version: the one registered under the bare system
10
+ * edition (http://snomed.info/sct/<edition>?fhir_vs...) - the latest version of that
11
+ * edition the server has
12
+ * version (.../version/<date>?fhir_vs...) - that version; only it matches
13
+ *
14
+ * @param {Iterable<object>} factories - the registered code system factories (duplicates are ignored)
15
+ * @param {function(string): object} defaultFor - the default factory for a system url
16
+ * @param {string} url - the value set url
17
+ * @param {function(string): (string|null)} [systemVersionFor] - the version the request asks for, for a system
18
+ * @returns {object|null} the factory to build the value set, or null if none recognises the url
19
+ */
20
+ function chooseImplicitValueSetFactory(factories, defaultFor, url, systemVersionFor = null) {
21
+ const matches = [];
22
+ const seen = new Set();
23
+ for (const csp of factories) {
24
+ if (seen.has(csp) || typeof csp.implicitValueSetForm !== 'function') {
25
+ continue;
26
+ }
27
+ seen.add(csp);
28
+ const form = csp.implicitValueSetForm(url);
29
+ if (form) {
30
+ matches.push({ csp, form: form.form });
31
+ }
32
+ }
33
+ if (matches.length === 0) {
34
+ return null;
35
+ }
36
+ if (matches.every(m => m.form === 'generic')) {
37
+ const requested = systemVersionFor ? systemVersionFor(matches[0].csp.system()) : null;
38
+ if (requested) {
39
+ // an edition (with or without its version) or a full version uri; the latest that matches
40
+ const asked = matches.filter(m => versionMatches(m.csp, requested));
41
+ if (asked.length > 0) {
42
+ asked.sort((a, b) => (b.csp.version() || '').localeCompare(a.csp.version() || ''));
43
+ return asked[0].csp;
44
+ }
45
+ }
46
+ const preferred = defaultFor(matches[0].csp.system());
47
+ return matches.some(m => m.csp === preferred) ? preferred : matches[0].csp;
48
+ }
49
+ // edition or version form: the latest version that matches. Version URIs of one edition
50
+ // differ only in their date, so they order as strings.
51
+ const specific = matches.filter(m => m.form !== 'generic');
52
+ specific.sort((a, b) => (b.csp.version() || '').localeCompare(a.csp.version() || ''));
53
+ return specific[0].csp;
54
+ }
55
+
56
+ // Whether a factory's version is the one asked for: the same version, or a version of the
57
+ // edition asked for (SNOMED CT: http://snomed.info/sct/<edition> covers .../version/<date>)
58
+ function versionMatches(csp, requested) {
59
+ if (typeof csp.isMyEdition === 'function') {
60
+ return csp.isMyEdition(requested);
61
+ }
62
+ const mine = csp.version() || '';
63
+ return mine === requested || mine.startsWith(requested + '/version/');
64
+ }
65
+
66
+ module.exports = { chooseImplicitValueSetFactory };