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.
- package/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- 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
|
-
|
|
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
|
-
|
|
111
|
+
terminal.warn(chalk.yellow(`Warning: Invalid module format in ${moduleFile}`));
|
|
111
112
|
}
|
|
112
113
|
} catch (error) {
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
145
|
+
terminal.log(chalk.green(`✓ Loaded ${name} module`));
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
listTerminologies() {
|
|
148
|
-
|
|
149
|
+
terminal.log(chalk.blue.bold('🏥 Available Terminology Importers\\n'));
|
|
149
150
|
|
|
150
151
|
if (this.modules.size === 0) {
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
163
|
+
terminal.log(chalk.gray(` Formats: ${formats.join(', ')}`));
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
if (module.getEstimatedDuration) {
|
|
166
|
-
|
|
167
|
+
terminal.log(chalk.gray(` Typical duration: ${module.getEstimatedDuration()}`));
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
|
|
170
|
+
terminal.log('');
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
+
terminal.log(chalk.red(`Unknown command: ${commandName}`));
|
|
188
189
|
this.listTerminologies();
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
192
193
|
showConfigHelp() {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
214
|
+
terminal.log(chalk.yellow(`No saved configuration found for ${options.terminology}`));
|
|
214
215
|
return;
|
|
215
216
|
}
|
|
216
217
|
|
|
217
|
-
|
|
218
|
+
terminal.log(chalk.blue.bold(`📋 Saved Configuration for ${options.terminology}:\\n`));
|
|
218
219
|
|
|
219
220
|
if (config.lastUsed) {
|
|
220
|
-
|
|
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
|
-
|
|
227
|
+
terminal.log(` ${key}: ${chalk.white(value.join(', '))}`);
|
|
227
228
|
} else {
|
|
228
|
-
|
|
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
|
-
|
|
235
|
+
terminal.log(`\\n Recent sources:`);
|
|
235
236
|
config.recentSources.forEach((src, index) => {
|
|
236
|
-
|
|
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
|
-
|
|
246
|
+
terminal.log(chalk.yellow('No saved configurations found'));
|
|
246
247
|
return;
|
|
247
248
|
}
|
|
248
249
|
|
|
249
|
-
|
|
250
|
+
terminal.log(chalk.blue.bold('📋 All Saved Configurations:\\n'));
|
|
250
251
|
|
|
251
252
|
Object.entries(allHistory).forEach(([terminology, config]) => {
|
|
252
|
-
|
|
253
|
+
terminal.log(chalk.cyan(`${terminology}:`));
|
|
253
254
|
if (config.lastUsed) {
|
|
254
|
-
|
|
255
|
+
terminal.log(` Last used: ${chalk.white(new Date(config.lastUsed).toLocaleString())}`);
|
|
255
256
|
}
|
|
256
257
|
if (config.source) {
|
|
257
|
-
|
|
258
|
+
terminal.log(` Source: ${chalk.gray(config.source)}`);
|
|
258
259
|
}
|
|
259
260
|
if (config.version) {
|
|
260
|
-
|
|
261
|
+
terminal.log(` Version: ${chalk.gray(config.version)}`);
|
|
261
262
|
}
|
|
262
|
-
|
|
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
|
-
|
|
282
|
+
terminal.log(chalk.green('✓ All configuration history cleared'));
|
|
282
283
|
} else {
|
|
283
|
-
|
|
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
|
-
|
|
297
|
+
terminal.log(chalk.green(`✓ Configuration history cleared for ${options.terminology}`));
|
|
297
298
|
} else {
|
|
298
|
-
|
|
299
|
+
terminal.log(chalk.yellow('Configuration clear cancelled'));
|
|
299
300
|
}
|
|
300
301
|
|
|
301
302
|
} else {
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
314
|
+
terminal.log(chalk.green(`✓ Configuration exported to ${options.output}`));
|
|
314
315
|
} catch (error) {
|
|
315
|
-
|
|
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
|
-
|
|
337
|
+
terminal.log(chalk.green(`✓ Configuration imported from ${options.input}`));
|
|
337
338
|
} catch (error) {
|
|
338
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
}
|
package/tx/library/codesystem.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
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
|
*/
|
package/tx/library/extensions.js
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 };
|