fhirsmith 0.3.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/CHANGELOG.md +42 -0
- package/FHIRsmith.png +0 -0
- package/README.md +277 -0
- package/config-template.json +144 -0
- package/library/folder-setup.js +58 -0
- package/library/html-server.js +166 -0
- package/library/html.js +835 -0
- package/library/i18nsupport.js +259 -0
- package/library/languages.js +779 -0
- package/library/logger-telnet.js +205 -0
- package/library/logger.js +279 -0
- package/library/package-manager.js +876 -0
- package/library/utilities.js +196 -0
- package/library/version-utilities.js +1056 -0
- package/npmprojector/config-example.json +13 -0
- package/npmprojector/indexer.js +394 -0
- package/npmprojector/npmprojector.js +395 -0
- package/npmprojector/readme.md +174 -0
- package/npmprojector/watcher.js +335 -0
- package/package.json +119 -0
- package/packages/package-crawler.js +846 -0
- package/packages/packages-template.html +126 -0
- package/packages/packages.js +2838 -0
- package/passwords.ini +2 -0
- package/publisher/publisher-template.html +208 -0
- package/publisher/publisher.js +2167 -0
- package/publisher/task-draft.js +458 -0
- package/registry/api.js +735 -0
- package/registry/crawler.js +637 -0
- package/registry/model.js +513 -0
- package/registry/readme.md +243 -0
- package/registry/registry-data.json +121015 -0
- package/registry/registry-template.html +126 -0
- package/registry/registry.js +1395 -0
- package/registry/test-runner.js +237 -0
- package/root-template.html +124 -0
- package/server.js +524 -0
- package/shl/private-key.pem +5 -0
- package/shl/public-key.pem +18 -0
- package/shl/shl.js +1125 -0
- package/shl/vhl.js +69 -0
- package/static/FHIRsmith128.png +0 -0
- package/static/FHIRsmith16.png +0 -0
- package/static/FHIRsmith32.png +0 -0
- package/static/FHIRsmith64.png +0 -0
- package/static/assets/css/bootstrap-fhir.css +5302 -0
- package/static/assets/css/bootstrap-glyphicons.css +2 -0
- package/static/assets/css/bootstrap.css +4097 -0
- package/static/assets/css/jquery-ui.css +523 -0
- package/static/assets/css/jquery-ui.structure.css +863 -0
- package/static/assets/css/jquery-ui.structure.min.css +5 -0
- package/static/assets/css/jquery-ui.theme.css +439 -0
- package/static/assets/css/jquery-ui.theme.min.css +5 -0
- package/static/assets/css/jquery.ui.all.css +7 -0
- package/static/assets/css/modules.css +18 -0
- package/static/assets/css/project.css +367 -0
- package/static/assets/css/pygments-manni.css +66 -0
- package/static/assets/css/tags.css +74 -0
- package/static/assets/css/xml.css +2 -0
- package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
- package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
- package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
- package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
- package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
- package/static/assets/ico/favicon.ico +0 -0
- package/static/assets/ico/favicon.png +0 -0
- package/static/assets/images/fhir-logo-www.png +0 -0
- package/static/assets/images/fhir-logo.png +0 -0
- package/static/assets/images/hl7-logo.png +0 -0
- package/static/assets/images/logo_ansinew.jpg +0 -0
- package/static/assets/images/search.png +0 -0
- package/static/assets/images/stripe.png +0 -0
- package/static/assets/images/target.png +0 -0
- package/static/assets/images/tx-registry-root.gif +0 -0
- package/static/assets/images/tx-registry.png +0 -0
- package/static/assets/images/tx-server.png +0 -0
- package/static/assets/images/tx-version.png +0 -0
- package/static/assets/js/bootstrap.min.js +6 -0
- package/static/assets/js/fhir-gw.js +259 -0
- package/static/assets/js/fhir.js +2 -0
- package/static/assets/js/html5shiv.js +8 -0
- package/static/assets/js/jcookie.js +96 -0
- package/static/assets/js/jquery-ui.min.js +6 -0
- package/static/assets/js/jquery.js +10716 -0
- package/static/assets/js/jquery.min.js +2 -0
- package/static/assets/js/jquery.ui.core.js +314 -0
- package/static/assets/js/jquery.ui.draggable.js +825 -0
- package/static/assets/js/jquery.ui.mouse.js +162 -0
- package/static/assets/js/jquery.ui.resizable.js +842 -0
- package/static/assets/js/jquery.ui.widget.js +268 -0
- package/static/assets/js/json2.js +487 -0
- package/static/assets/js/jtip.js +97 -0
- package/static/assets/js/respond.min.js +6 -0
- package/static/assets/js/statuspage.js +70 -0
- package/static/assets/js/xml.js +2 -0
- package/static/dist/js/bootstrap.js +1964 -0
- package/static/favicon.png +0 -0
- package/static/fhir.css +626 -0
- package/static/icon-fhir-16.png +0 -0
- package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
- package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- package/static/images/ui-icons_222222_256x240.png +0 -0
- package/static/images/ui-icons_228ef1_256x240.png +0 -0
- package/static/images/ui-icons_ef8c08_256x240.png +0 -0
- package/static/images/ui-icons_ffd27a_256x240.png +0 -0
- package/static/images/ui-icons_ffffff_256x240.png +0 -0
- package/static/js/jquery.effects.blind.js +49 -0
- package/static/js/jquery.effects.bounce.js +78 -0
- package/static/js/jquery.effects.clip.js +54 -0
- package/static/js/jquery.effects.core.js +763 -0
- package/static/js/jquery.effects.drop.js +50 -0
- package/static/js/jquery.effects.explode.js +79 -0
- package/static/js/jquery.effects.fade.js +32 -0
- package/static/js/jquery.effects.fold.js +56 -0
- package/static/js/jquery.effects.highlight.js +50 -0
- package/static/js/jquery.effects.pulsate.js +51 -0
- package/static/js/jquery.effects.scale.js +178 -0
- package/static/js/jquery.effects.shake.js +57 -0
- package/static/js/jquery.effects.slide.js +50 -0
- package/static/js/jquery.effects.transfer.js +45 -0
- package/static/js/jquery.ui.accordion.js +611 -0
- package/static/js/jquery.ui.autocomplete.js +612 -0
- package/static/js/jquery.ui.button.js +416 -0
- package/static/js/jquery.ui.datepicker.js +1823 -0
- package/static/js/jquery.ui.dialog.js +878 -0
- package/static/js/jquery.ui.droppable.js +296 -0
- package/static/js/jquery.ui.position.js +252 -0
- package/static/js/jquery.ui.progressbar.js +109 -0
- package/static/js/jquery.ui.selectable.js +266 -0
- package/static/js/jquery.ui.slider.js +666 -0
- package/static/js/jquery.ui.sortable.js +1077 -0
- package/static/js/jquery.ui.tabs.js +758 -0
- package/stats.js +80 -0
- package/test-cache/vsac/vsac-valuesets.db +0 -0
- package/token/nginx_passport_setup.md +383 -0
- package/token/security_guide.md +294 -0
- package/token/token-template.html +330 -0
- package/token/token.js +1300 -0
- package/translations/Messages.properties +1510 -0
- package/translations/Messages_ar.properties +1399 -0
- package/translations/Messages_de.properties +836 -0
- package/translations/Messages_es.properties +737 -0
- package/translations/Messages_fr.properties +1 -0
- package/translations/Messages_ja.properties +893 -0
- package/translations/Messages_nl.properties +1357 -0
- package/translations/Messages_pt.properties +1302 -0
- package/translations/Messages_ru.properties +1 -0
- package/translations/Messages_uz.properties +1 -0
- package/translations/Messages_zh.properties +1 -0
- package/translations/rendering-phrases.properties +1128 -0
- package/translations/rendering-phrases_ar.properties +1091 -0
- package/translations/rendering-phrases_de.properties +6 -0
- package/translations/rendering-phrases_es.properties +6 -0
- package/translations/rendering-phrases_fr.properties +624 -0
- package/translations/rendering-phrases_ja.properties +21 -0
- package/translations/rendering-phrases_nl.properties +970 -0
- package/translations/rendering-phrases_pt.properties +1020 -0
- package/translations/rendering-phrases_ru.properties +1094 -0
- package/translations/rendering-phrases_uz.properties +1 -0
- package/translations/rendering-phrases_zh.properties +1 -0
- package/tx/README.md +418 -0
- package/tx/cm/cm-api.js +110 -0
- package/tx/cm/cm-database.js +735 -0
- package/tx/cm/cm-package.js +325 -0
- package/tx/cs/cs-api.js +789 -0
- package/tx/cs/cs-areacode.js +615 -0
- package/tx/cs/cs-country.js +1110 -0
- package/tx/cs/cs-cpt.js +785 -0
- package/tx/cs/cs-cs.js +1579 -0
- package/tx/cs/cs-currency.js +539 -0
- package/tx/cs/cs-db.js +1321 -0
- package/tx/cs/cs-hgvs.js +329 -0
- package/tx/cs/cs-lang.js +465 -0
- package/tx/cs/cs-loinc.js +1485 -0
- package/tx/cs/cs-mimetypes.js +238 -0
- package/tx/cs/cs-ndc.js +704 -0
- package/tx/cs/cs-omop.js +1025 -0
- package/tx/cs/cs-provider-api.js +43 -0
- package/tx/cs/cs-provider-list.js +37 -0
- package/tx/cs/cs-rxnorm.js +808 -0
- package/tx/cs/cs-snomed.js +1102 -0
- package/tx/cs/cs-ucum.js +514 -0
- package/tx/cs/cs-unii.js +271 -0
- package/tx/cs/cs-uri.js +218 -0
- package/tx/cs/cs-usstates.js +305 -0
- package/tx/dev.fhir.org.yml +14 -0
- package/tx/fixtures/test-cases-setup.json +18 -0
- package/tx/fixtures/test-cases.yml +16 -0
- package/tx/html/codesystem-operations.liquid +25 -0
- package/tx/html/home-metrics.liquid +247 -0
- package/tx/html/operations-form.liquid +148 -0
- package/tx/html/search-form.liquid +62 -0
- package/tx/html/tx-template.html +133 -0
- package/tx/html/valueset-operations.liquid +54 -0
- package/tx/importers/atc-to-fhir.js +316 -0
- package/tx/importers/import-loinc.module.js +1536 -0
- package/tx/importers/import-ndc.module.js +1088 -0
- package/tx/importers/import-rxnorm.module.js +898 -0
- package/tx/importers/import-sct.module.js +2457 -0
- package/tx/importers/import-unii.module.js +601 -0
- package/tx/importers/readme.md +453 -0
- package/tx/importers/subset-loinc.module.js +1081 -0
- package/tx/importers/subset-rxnorm.module.js +938 -0
- package/tx/importers/tx-import-base.js +351 -0
- package/tx/importers/tx-import-settings.js +310 -0
- package/tx/importers/tx-import.js +357 -0
- package/tx/library/canonical-resource.js +88 -0
- package/tx/library/capabilitystatement.js +292 -0
- package/tx/library/codesystem.js +774 -0
- package/tx/library/conceptmap.js +568 -0
- package/tx/library/designations.js +932 -0
- package/tx/library/errors.js +77 -0
- package/tx/library/extensions.js +117 -0
- package/tx/library/namingsystem.js +322 -0
- package/tx/library/operation-outcome.js +127 -0
- package/tx/library/parameters.js +105 -0
- package/tx/library/renderer.js +1559 -0
- package/tx/library/terminologycapabilities.js +418 -0
- package/tx/library/ucum-parsers.js +1029 -0
- package/tx/library/ucum-service.js +370 -0
- package/tx/library/ucum-types.js +1099 -0
- package/tx/library/valueset.js +543 -0
- package/tx/library.js +676 -0
- package/tx/ocl/cm-ocl.js +106 -0
- package/tx/ocl/cs-ocl.js +39 -0
- package/tx/ocl/vs-ocl.js +105 -0
- package/tx/operation-context.js +568 -0
- package/tx/params.js +613 -0
- package/tx/provider.js +403 -0
- package/tx/sct/ecl.js +1560 -0
- package/tx/sct/expressions.js +2077 -0
- package/tx/sct/structures.js +1396 -0
- package/tx/tx-html.js +1063 -0
- package/tx/tx.fhir.org.yml +39 -0
- package/tx/tx.js +927 -0
- package/tx/vs/vs-api.js +112 -0
- package/tx/vs/vs-database.js +786 -0
- package/tx/vs/vs-package.js +358 -0
- package/tx/vs/vs-vsac.js +366 -0
- package/tx/workers/batch-validate.js +129 -0
- package/tx/workers/batch.js +361 -0
- package/tx/workers/closure.js +32 -0
- package/tx/workers/expand.js +1845 -0
- package/tx/workers/lookup.js +407 -0
- package/tx/workers/metadata.js +467 -0
- package/tx/workers/operations.js +34 -0
- package/tx/workers/read.js +164 -0
- package/tx/workers/search.js +384 -0
- package/tx/workers/subsumes.js +334 -0
- package/tx/workers/translate.js +492 -0
- package/tx/workers/validate.js +2504 -0
- package/tx/workers/worker.js +904 -0
- package/tx/xml/capabilitystatement-xml.js +63 -0
- package/tx/xml/codesystem-xml.js +62 -0
- package/tx/xml/conceptmap-xml.js +65 -0
- package/tx/xml/namingsystem-xml.js +65 -0
- package/tx/xml/operationoutcome-xml.js +127 -0
- package/tx/xml/parameters-xml.js +312 -0
- package/tx/xml/terminologycapabilities-xml.js +64 -0
- package/tx/xml/valueset-xml.js +64 -0
- package/tx/xml/xml-base.js +603 -0
- package/vcl/vcl-parser.js +1098 -0
- package/vcl/vcl.js +253 -0
- package/windows-install.js +19 -0
- package/xig/xig-template.html +124 -0
- package/xig/xig.js +3049 -0
package/tx/tx.js
ADDED
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
//
|
|
2
|
+
// TX Module - FHIR Terminology Server
|
|
3
|
+
//
|
|
4
|
+
// This module provides FHIR terminology services (CodeSystem, ValueSet, ConceptMap)
|
|
5
|
+
// with support for multiple endpoints at different FHIR versions.
|
|
6
|
+
//
|
|
7
|
+
|
|
8
|
+
const express = require('express');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const Logger = require('../library/logger');
|
|
11
|
+
const { Library } = require('./library');
|
|
12
|
+
const { OperationContext, ResourceCache, ExpansionCache } = require('./operation-context');
|
|
13
|
+
const { LanguageDefinitions } = require('../library/languages');
|
|
14
|
+
const { I18nSupport } = require('../library/i18nsupport');
|
|
15
|
+
const { CodeSystemXML } = require('./xml/codesystem-xml');
|
|
16
|
+
const txHtml = require('./tx-html');
|
|
17
|
+
const { Liquid } = require('liquidjs');
|
|
18
|
+
const packageJson = require("../package.json");
|
|
19
|
+
|
|
20
|
+
// Import workers
|
|
21
|
+
const ReadWorker = require('./workers/read');
|
|
22
|
+
const SearchWorker = require('./workers/search');
|
|
23
|
+
const { ExpandWorker } = require('./workers/expand');
|
|
24
|
+
const { ValidateWorker } = require('./workers/validate');
|
|
25
|
+
const TranslateWorker = require('./workers/translate');
|
|
26
|
+
const LookupWorker = require('./workers/lookup');
|
|
27
|
+
const SubsumesWorker = require('./workers/subsumes');
|
|
28
|
+
const { MetadataHandler } = require('./workers/metadata');
|
|
29
|
+
const { BatchValidateWorker } = require('./workers/batch-validate');
|
|
30
|
+
const {CapabilityStatementXML} = require("./xml/capabilitystatement-xml");
|
|
31
|
+
const {TerminologyCapabilitiesXML} = require("./xml/terminologycapabilities-xml");
|
|
32
|
+
const {ParametersXML} = require("./xml/parameters-xml");
|
|
33
|
+
const {OperationOutcomeXML} = require("./xml/operationoutcome-xml");
|
|
34
|
+
const {ValueSetXML} = require("./xml/valueset-xml");
|
|
35
|
+
const {ConceptMapXML} = require("./xml/conceptmap-xml");
|
|
36
|
+
const {TxHtmlRenderer} = require("./tx-html");
|
|
37
|
+
const {Renderer} = require("./library/renderer");
|
|
38
|
+
const {OperationsWorker} = require("./workers/operations");
|
|
39
|
+
|
|
40
|
+
class TXModule {
|
|
41
|
+
timers = [];
|
|
42
|
+
|
|
43
|
+
constructor(stats) {
|
|
44
|
+
this.config = null;
|
|
45
|
+
this.library = null;
|
|
46
|
+
this.endpoints = [];
|
|
47
|
+
this.routers = new Map(); // path -> router
|
|
48
|
+
this.requestIdCounter = 0; // Thread-safe request ID counter
|
|
49
|
+
this.languages = null; // LanguageDefinitions
|
|
50
|
+
this.i18n = null; // I18nSupport
|
|
51
|
+
this.metadataHandler = null; // MetadataHandler
|
|
52
|
+
this.liquid = new Liquid({
|
|
53
|
+
root: path.join(__dirname, 'html'), // optional: where to look for templates
|
|
54
|
+
extname: '.liquid' // optional: default extension
|
|
55
|
+
});
|
|
56
|
+
this.stats = stats;
|
|
57
|
+
if (stats) {
|
|
58
|
+
stats.cachingModules.push(this);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate a unique request ID
|
|
64
|
+
* @returns {string} Unique request ID
|
|
65
|
+
*/
|
|
66
|
+
generateRequestId() {
|
|
67
|
+
this.requestIdCounter++;
|
|
68
|
+
return `tx-${this.requestIdCounter}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
acceptsXml(req) {
|
|
72
|
+
const accept = req.headers.accept || '';
|
|
73
|
+
return accept.includes('application/fhir+xml') || accept.includes('application/xml+fhir');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initialize the TX module
|
|
79
|
+
* @param {Object} config - Module configuration
|
|
80
|
+
* @param {express.Application} app - Express application for registering endpoints
|
|
81
|
+
*/
|
|
82
|
+
async initialize(config, app) {
|
|
83
|
+
this.config = config;
|
|
84
|
+
// Initialize logger with config settings
|
|
85
|
+
this.log = Logger.getInstance().child({
|
|
86
|
+
module: 'tx',
|
|
87
|
+
consoleErrors: config.consoleErrors,
|
|
88
|
+
telnetErrors: config.telnetErrors
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this.log.info('Initializing TX module');
|
|
92
|
+
|
|
93
|
+
// Load HTML template
|
|
94
|
+
txHtml.loadTemplate();
|
|
95
|
+
|
|
96
|
+
// Validate config
|
|
97
|
+
if (!config.librarySource) {
|
|
98
|
+
throw new Error('TX module requires librarySource configuration');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!config.endpoints || !Array.isArray(config.endpoints) || config.endpoints.length === 0) {
|
|
102
|
+
throw new Error('TX module requires at least one endpoint configuration');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Load language definitions
|
|
106
|
+
const langPath = path.join(__dirname, 'data', 'lang.dat');
|
|
107
|
+
this.log.info(`Loading language definitions from: ${langPath}`);
|
|
108
|
+
this.languages = await LanguageDefinitions.fromFile(langPath);
|
|
109
|
+
this.log.info('Language definitions loaded');
|
|
110
|
+
|
|
111
|
+
// Initialize i18n support
|
|
112
|
+
const translationsPath = path.join(__dirname, '..', 'translations');
|
|
113
|
+
this.log.info(`Loading translations from: ${translationsPath}`);
|
|
114
|
+
this.i18n = new I18nSupport(translationsPath, this.languages);
|
|
115
|
+
await this.i18n.load();
|
|
116
|
+
this.log.info('I18n support initialized');
|
|
117
|
+
|
|
118
|
+
// Initialize metadata handler with config
|
|
119
|
+
this.metadataHandler = new MetadataHandler({
|
|
120
|
+
baseUrl: config.baseUrl,
|
|
121
|
+
serverVersion: packageJson.version,
|
|
122
|
+
softwareName: config.softwareName || 'FHIRsmith',
|
|
123
|
+
name: config.name || 'FHIRTerminologyServer',
|
|
124
|
+
title: config.title || 'FHIR Terminology Server',
|
|
125
|
+
description: config.description || 'FHIR Terminology Server',
|
|
126
|
+
contactUrl: config.contactUrl,
|
|
127
|
+
contact: config.contact,
|
|
128
|
+
releaseDate: config.releaseDate,
|
|
129
|
+
host: config.host ? config.host : "localhost"
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Load the library from YAML
|
|
133
|
+
this.log.info(`Loading library from: ${config.librarySource}`);
|
|
134
|
+
this.library = new Library(config.librarySource, this.log);
|
|
135
|
+
this.log.info(`Load...`);
|
|
136
|
+
await this.library.load();
|
|
137
|
+
this.log.info('Library loaded successfully');
|
|
138
|
+
|
|
139
|
+
// Set up each endpoint
|
|
140
|
+
for (const endpoint of config.endpoints) {
|
|
141
|
+
await this.setupEndpoint(endpoint, app);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.log.info(`TX module initialized with ${config.endpoints.length} endpoint(s)`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Set up a single endpoint
|
|
149
|
+
* @param {Object} endpoint - Endpoint configuration {path, fhirVersion, context}
|
|
150
|
+
* @param {express.Application} app - Express application
|
|
151
|
+
*/
|
|
152
|
+
async setupEndpoint(endpoint, app) {
|
|
153
|
+
const { path: endpointPath, context } = endpoint;
|
|
154
|
+
const fhirVersion = String(endpoint.fhirVersion);
|
|
155
|
+
|
|
156
|
+
if (!endpointPath) {
|
|
157
|
+
throw new Error('Endpoint requires a path');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!fhirVersion) {
|
|
161
|
+
throw new Error(`Endpoint ${endpointPath} requires a fhirVersion`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check for path conflicts
|
|
165
|
+
if (this.routers.has(endpointPath)) {
|
|
166
|
+
throw new Error(`Duplicate endpoint path: ${endpointPath}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.log.info(`Setting up endpoint: ${endpointPath} (FHIR v${fhirVersion}, context: ${context || 'none'})`);
|
|
170
|
+
|
|
171
|
+
const router = express.Router();
|
|
172
|
+
|
|
173
|
+
// Get cache configuration
|
|
174
|
+
const cacheTimeoutMinutes = this.config.cacheTimeout || 30;
|
|
175
|
+
const expansionCacheSize = this.config.expansionCacheSize || 1000;
|
|
176
|
+
const expansionCacheMemoryThreshold = this.config.expansionCacheMemoryThreshold || 0;
|
|
177
|
+
|
|
178
|
+
// Store endpoint info for provider creation
|
|
179
|
+
const endpointInfo = {
|
|
180
|
+
path: endpointPath,
|
|
181
|
+
fhirVersion,
|
|
182
|
+
context: context || null,
|
|
183
|
+
resourceCache: new ResourceCache(),
|
|
184
|
+
expansionCache: new ExpansionCache(expansionCacheSize, expansionCacheMemoryThreshold)
|
|
185
|
+
};
|
|
186
|
+
// Create the provider once for this endpoint
|
|
187
|
+
endpointInfo.provider = await this.library.cloneWithFhirVersion(fhirVersion, context, endpointPath);
|
|
188
|
+
|
|
189
|
+
// Set up periodic pruning of the resource cache
|
|
190
|
+
// cacheTimeout is in minutes, default to 30 minutes
|
|
191
|
+
const cacheTimeoutMs = cacheTimeoutMinutes * 60 * 1000;
|
|
192
|
+
const pruneIntervalMs = 5 * 60 * 1000; // Run every 5 minutes
|
|
193
|
+
this.timers.push(setInterval(() => {
|
|
194
|
+
endpointInfo.resourceCache.prune(cacheTimeoutMs);
|
|
195
|
+
}, pruneIntervalMs));
|
|
196
|
+
this.log.info(`Resource cache pruning enabled for ${endpointPath}: timeout ${cacheTimeoutMinutes} minutes, check interval 5 minutes`);
|
|
197
|
+
|
|
198
|
+
// Set up periodic memory pressure check for expansion cache (if threshold configured)
|
|
199
|
+
if (expansionCacheMemoryThreshold > 0) {
|
|
200
|
+
this.timers.push(setInterval(() => {
|
|
201
|
+
if (endpointInfo.expansionCache.checkMemoryPressure()) {
|
|
202
|
+
this.log.info(`Expansion cache memory pressure detected for ${endpointPath}, evicted oldest half`);
|
|
203
|
+
}
|
|
204
|
+
}, pruneIntervalMs));
|
|
205
|
+
this.log.info(`Expansion cache for ${endpointPath}: max ${expansionCacheSize} entries, memory threshold ${expansionCacheMemoryThreshold}MB`);
|
|
206
|
+
} else {
|
|
207
|
+
this.log.info(`Expansion cache for ${endpointPath}: max ${expansionCacheSize} entries, no memory threshold`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Middleware to attach provider, context, and timing to request, and wrap res.json for HTML
|
|
211
|
+
router.use((req, res, next) => {
|
|
212
|
+
// Increment request count
|
|
213
|
+
endpointInfo.provider.requestCount++;
|
|
214
|
+
|
|
215
|
+
// Generate unique request ID
|
|
216
|
+
const requestId = this.generateRequestId();
|
|
217
|
+
|
|
218
|
+
// Get Accept-Language header for language preferences
|
|
219
|
+
const acceptLanguage = req.get('Accept-Language') || 'en';
|
|
220
|
+
|
|
221
|
+
// Create operation context with language, ID, time limit, and caches
|
|
222
|
+
const opContext = new OperationContext(
|
|
223
|
+
acceptLanguage, this.i18n, requestId, 30,
|
|
224
|
+
endpointInfo.resourceCache, endpointInfo.expansionCache
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Attach everything to request
|
|
228
|
+
req.txProvider = endpointInfo.provider;
|
|
229
|
+
req.txEndpoint = endpointInfo;
|
|
230
|
+
req.txStartTime = Date.now();
|
|
231
|
+
req.txOpContext = opContext;
|
|
232
|
+
req.txLanguages = this.languages;
|
|
233
|
+
req.txI18n = this.i18n;
|
|
234
|
+
req.txLog = this.log;
|
|
235
|
+
|
|
236
|
+
// Add X-Request-Id header to response
|
|
237
|
+
res.setHeader('X-Request-Id', requestId);
|
|
238
|
+
|
|
239
|
+
// Wrap res.json to intercept and convert to HTML if browser requests it, and log the request
|
|
240
|
+
const originalJson = res.json.bind(res);
|
|
241
|
+
|
|
242
|
+
let txhtml = new TxHtmlRenderer(new Renderer(opContext, endpointInfo.provider), this.liquid);
|
|
243
|
+
res.json = async (data) => {
|
|
244
|
+
try {
|
|
245
|
+
const duration = Date.now() - req.txStartTime;
|
|
246
|
+
const isHtml = txhtml.acceptsHtml(req);
|
|
247
|
+
const isXml = this.acceptsXml(req);
|
|
248
|
+
|
|
249
|
+
let responseSize;
|
|
250
|
+
let result;
|
|
251
|
+
|
|
252
|
+
if (isHtml) {
|
|
253
|
+
const title = txhtml.buildTitle(data, req);
|
|
254
|
+
const content = await txhtml.render(data, req);
|
|
255
|
+
const html = await txhtml.renderPage(title, content, req.txEndpoint, req.txStartTime);
|
|
256
|
+
responseSize = Buffer.byteLength(html, 'utf8');
|
|
257
|
+
res.setHeader('Content-Type', 'text/html');
|
|
258
|
+
result = res.send(html);
|
|
259
|
+
} else if (isXml) {
|
|
260
|
+
try {
|
|
261
|
+
const xml = this.convertResourceToXml(data);
|
|
262
|
+
responseSize = Buffer.byteLength(xml, 'utf8');
|
|
263
|
+
res.setHeader('Content-Type', 'application/fhir+xml');
|
|
264
|
+
result = res.send(xml);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(err);
|
|
267
|
+
// Fall back to JSON if XML conversion not supported
|
|
268
|
+
this.log.warn(`XML conversion failed for ${data.resourceType}: ${err.message}, falling back to JSON`);
|
|
269
|
+
const jsonStr = JSON.stringify(data);
|
|
270
|
+
responseSize = Buffer.byteLength(jsonStr, 'utf8');
|
|
271
|
+
result = originalJson(data);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
const jsonStr = JSON.stringify(data);
|
|
275
|
+
responseSize = Buffer.byteLength(jsonStr, 'utf8');
|
|
276
|
+
result = originalJson(data);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Log the request with request ID
|
|
280
|
+
const format = isHtml ? 'html' : (isXml ? 'xml' : 'json');
|
|
281
|
+
let li = req.logInfo ? "(" + req.logInfo + ")" : "";
|
|
282
|
+
this.log.info(`[${requestId}] ${req.method} ${format} ${res.statusCode} ${duration}ms ${responseSize}: ${req.originalUrl} ${li})`);
|
|
283
|
+
|
|
284
|
+
return result;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
this.log.error(`Error rendering response: ${err.message}`);
|
|
287
|
+
console.error(err);
|
|
288
|
+
res.status(500).send('Internal Server Error');
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
next();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// CORS headers
|
|
296
|
+
router.use((req, res, next) => {
|
|
297
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
298
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
|
299
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
300
|
+
if (req.method === 'OPTIONS') {
|
|
301
|
+
return res.sendStatus(200);
|
|
302
|
+
}
|
|
303
|
+
next();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// JSON body parsing - accept both application/json and application/fhir+json
|
|
307
|
+
// Handle body that may already be read as a Buffer by app-level middleware
|
|
308
|
+
router.use((req, res, next) => {
|
|
309
|
+
const contentType = req.get('Content-Type') || '';
|
|
310
|
+
|
|
311
|
+
// Only process POST/PUT
|
|
312
|
+
if (req.method !== 'POST' && req.method !== 'PUT') {
|
|
313
|
+
return next();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (contentType.includes('application/json') ||
|
|
317
|
+
contentType.includes('application/fhir+json') ||
|
|
318
|
+
contentType.includes('application/json+fhir')) {
|
|
319
|
+
|
|
320
|
+
// If body is a Buffer, parse it
|
|
321
|
+
if (Buffer.isBuffer(req.body)) {
|
|
322
|
+
try {
|
|
323
|
+
const bodyStr = req.body.toString('utf8');
|
|
324
|
+
if (bodyStr) {
|
|
325
|
+
req.body = JSON.parse(bodyStr);
|
|
326
|
+
}
|
|
327
|
+
} catch (e) {
|
|
328
|
+
this.log.error(`JSON parse error: ${e.message}`);
|
|
329
|
+
return res.status(400).json({
|
|
330
|
+
resourceType: 'OperationOutcome',
|
|
331
|
+
issue: [{
|
|
332
|
+
severity: 'error',
|
|
333
|
+
code: 'invalid',
|
|
334
|
+
diagnostics: `Invalid JSON: ${e.message}`
|
|
335
|
+
}]
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
} else if (contentType.includes('application/xml') ||
|
|
341
|
+
// Handle XML
|
|
342
|
+
contentType.includes('application/fhir+xml') ||
|
|
343
|
+
contentType.includes('application/xml+fhir')) {
|
|
344
|
+
|
|
345
|
+
let xmlStr;
|
|
346
|
+
if (Buffer.isBuffer(req.body)) {
|
|
347
|
+
xmlStr = req.body.toString('utf8');
|
|
348
|
+
} else if (typeof req.body === 'string') {
|
|
349
|
+
xmlStr = req.body;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (xmlStr) {
|
|
353
|
+
try {
|
|
354
|
+
req.body = this.convertXmlToResource(xmlStr);
|
|
355
|
+
} catch (e) {
|
|
356
|
+
this.log.error(`XML parse error: ${e.message}`);
|
|
357
|
+
return res.status(400).json({
|
|
358
|
+
resourceType: 'OperationOutcome',
|
|
359
|
+
issue: [{
|
|
360
|
+
severity: 'error',
|
|
361
|
+
code: 'invalid',
|
|
362
|
+
diagnostics: `Invalid XML: ${e.message}`
|
|
363
|
+
}]
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
next();
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
// Set up routes
|
|
374
|
+
this.setupRoutes(router);
|
|
375
|
+
|
|
376
|
+
// Register the router with the app
|
|
377
|
+
app.use(endpointPath, router);
|
|
378
|
+
this.routers.set(endpointPath, router);
|
|
379
|
+
this.endpoints.push(endpointInfo);
|
|
380
|
+
|
|
381
|
+
this.log.info(`Endpoint ${endpointPath} registered`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Set up routes for an endpoint
|
|
386
|
+
* @param {express.Router} router - Express router
|
|
387
|
+
*/
|
|
388
|
+
setupRoutes(router) {
|
|
389
|
+
const resourceTypes = ['CodeSystem', 'ValueSet', 'ConceptMap'];
|
|
390
|
+
|
|
391
|
+
// ===== Operations =====
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
// CodeSystem/$lookup (GET and POST)
|
|
395
|
+
router.get('/CodeSystem/\\$lookup', async (req, res) => {
|
|
396
|
+
const start = Date.now();
|
|
397
|
+
try {
|
|
398
|
+
let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
399
|
+
await worker.handle(req, res);
|
|
400
|
+
} finally {
|
|
401
|
+
this.countRequest('$lookup', Date.now() - start);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
router.post('/CodeSystem/\\$lookup', async (req, res) => {
|
|
405
|
+
const start = Date.now();
|
|
406
|
+
try {
|
|
407
|
+
let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
408
|
+
await worker.handle(req, res);
|
|
409
|
+
} finally {
|
|
410
|
+
this.countRequest('$lookup', Date.now() - start);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// CodeSystem/$subsumes (GET and POST)
|
|
415
|
+
router.get('/CodeSystem/\\$subsumes', async (req, res) => {
|
|
416
|
+
const start = Date.now();
|
|
417
|
+
try {
|
|
418
|
+
let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
419
|
+
await worker.handle(req, res);
|
|
420
|
+
} finally {
|
|
421
|
+
this.countRequest('$subsumes', Date.now() - start);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
router.post('/CodeSystem/\\$subsumes', async (req, res) => {
|
|
425
|
+
const start = Date.now();
|
|
426
|
+
try {
|
|
427
|
+
let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
428
|
+
await worker.handle(req, res);
|
|
429
|
+
} finally {
|
|
430
|
+
this.countRequest('$subsumes', Date.now() - start);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// CodeSystem/$validate-code (GET and POST)
|
|
435
|
+
router.get('/CodeSystem/\\$validate-code', async (req, res) => {
|
|
436
|
+
const start = Date.now();
|
|
437
|
+
try {
|
|
438
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
439
|
+
await worker.handleCodeSystem(req, res);
|
|
440
|
+
} finally {
|
|
441
|
+
this.countRequest('$validate', Date.now() - start);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
router.post('/CodeSystem/\\$validate-code', async (req, res) => {
|
|
445
|
+
const start = Date.now();
|
|
446
|
+
try {
|
|
447
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
448
|
+
await worker.handleCodeSystem(req, res);
|
|
449
|
+
} finally {
|
|
450
|
+
this.countRequest('$validate', Date.now() - start);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// CodeSystem/$batch-validate-code (GET and POST)
|
|
455
|
+
router.get('/CodeSystem/\\$batch-validate-code', async (req, res) => {
|
|
456
|
+
const start = Date.now();
|
|
457
|
+
try {
|
|
458
|
+
let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
459
|
+
await worker.handleCodeSystem(req, res);
|
|
460
|
+
} finally {
|
|
461
|
+
this.countRequest('$batch', Date.now() - start);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
router.post('/CodeSystem/\\$batch-validate-code', async (req, res) => {
|
|
465
|
+
const start = Date.now();
|
|
466
|
+
try {
|
|
467
|
+
let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
468
|
+
await worker.handleCodeSystem(req, res);
|
|
469
|
+
} finally {
|
|
470
|
+
this.countRequest('$batch', Date.now() - start);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
// ValueSet/$validate-code (GET and POST)
|
|
474
|
+
router.get('/ValueSet/\\$validate-code', async (req, res) => {
|
|
475
|
+
const start = Date.now();
|
|
476
|
+
try {
|
|
477
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
478
|
+
await worker.handleValueSet(req, res);
|
|
479
|
+
} finally {
|
|
480
|
+
this.countRequest('$validate', Date.now() - start);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
router.post('/ValueSet/\\$validate-code', async (req, res) => {
|
|
484
|
+
const start = Date.now();
|
|
485
|
+
try {
|
|
486
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
487
|
+
await worker.handleValueSet(req, res);
|
|
488
|
+
} finally {
|
|
489
|
+
this.countRequest('$validate', Date.now() - start);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
// ValueSet/$batch-validate-code (GET and POST)
|
|
494
|
+
router.get('/ValueSet/\\$batch-validate-code', async (req, res) => {
|
|
495
|
+
const start = Date.now();
|
|
496
|
+
try {
|
|
497
|
+
let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
498
|
+
await worker.handleValueSet(req, res);
|
|
499
|
+
} finally {
|
|
500
|
+
this.countRequest('$batch', Date.now() - start);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
router.post('/ValueSet/\\$batch-validate-code', async (req, res) => {
|
|
504
|
+
const start = Date.now();
|
|
505
|
+
try {
|
|
506
|
+
let worker = new BatchValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
507
|
+
await worker.handleValueSet(req, res);
|
|
508
|
+
} finally {
|
|
509
|
+
this.countRequest('validate', Date.now() - start);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// ValueSet/$expand (GET and POST)
|
|
514
|
+
router.get('/ValueSet/\\$expand', async (req, res) => {
|
|
515
|
+
const start = Date.now();
|
|
516
|
+
try {
|
|
517
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
518
|
+
await worker.handle(req, res, this.log);
|
|
519
|
+
} finally {
|
|
520
|
+
this.countRequest('$expand', Date.now() - start);
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
router.post('/ValueSet/\\$expand', async (req, res) => {
|
|
524
|
+
const start = Date.now();
|
|
525
|
+
try {
|
|
526
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
527
|
+
await worker.handle(req, res, this.log);
|
|
528
|
+
} finally {
|
|
529
|
+
this.countRequest('$expand', Date.now() - start);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ConceptMap/$translate (GET and POST)
|
|
534
|
+
router.get('/ConceptMap/\\$translate', async (req, res) => {
|
|
535
|
+
const start = Date.now();
|
|
536
|
+
try {
|
|
537
|
+
let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
538
|
+
await worker.handle(req, res, this.log);
|
|
539
|
+
} finally {
|
|
540
|
+
this.countRequest('$translate', Date.now() - start);
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
router.post('/ConceptMap/\\$translate', async (req, res) => {
|
|
544
|
+
const start = Date.now();
|
|
545
|
+
try {
|
|
546
|
+
let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
547
|
+
await worker.handle(req, res, this.log);
|
|
548
|
+
} finally {
|
|
549
|
+
this.countRequest('$translate', Date.now() - start);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// ConceptMap/$closure (GET and POST)
|
|
554
|
+
router.get('/ConceptMap/\\$closure', async (req, res) => {
|
|
555
|
+
const start = Date.now();
|
|
556
|
+
try {
|
|
557
|
+
let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
558
|
+
await worker.handle(req, res, this.log);
|
|
559
|
+
} finally {
|
|
560
|
+
this.countRequest('$closure', Date.now() - start);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
router.post('/ConceptMap/\\$closure', async (req, res) => {
|
|
564
|
+
const start = Date.now();
|
|
565
|
+
try {
|
|
566
|
+
let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
567
|
+
await worker.handle(req, res, this.log);
|
|
568
|
+
} finally {
|
|
569
|
+
this.countRequest('$closure', Date.now() - start);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// ===== Instance operations =====
|
|
574
|
+
|
|
575
|
+
// CodeSystem/[id]/$lookup
|
|
576
|
+
router.get('/CodeSystem/:id/\\$lookup', async (req, res) => {
|
|
577
|
+
const start = Date.now();
|
|
578
|
+
try {
|
|
579
|
+
let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
580
|
+
await worker.handleInstance(req, res);
|
|
581
|
+
} finally {
|
|
582
|
+
this.countRequest('$lookup', Date.now() - start);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
router.post('/CodeSystem/:id/\\$lookup', async (req, res) => {
|
|
586
|
+
const start = Date.now();
|
|
587
|
+
try {
|
|
588
|
+
let worker = new LookupWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
589
|
+
await worker.handleInstance(req, res);
|
|
590
|
+
} finally {
|
|
591
|
+
this.countRequest('$lookup', Date.now() - start);
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// CodeSystem/[id]/$subsumes
|
|
596
|
+
router.get('/CodeSystem/:id/\\$subsumes', async (req, res) => {
|
|
597
|
+
const start = Date.now();
|
|
598
|
+
try {
|
|
599
|
+
let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
600
|
+
await worker.handleInstance(req, res);
|
|
601
|
+
} finally {
|
|
602
|
+
this.countRequest('$subsumes', Date.now() - start);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
router.post('/CodeSystem/:id/\\$subsumes', async (req, res) => {
|
|
606
|
+
const start = Date.now();
|
|
607
|
+
try {
|
|
608
|
+
let worker = new SubsumesWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
609
|
+
await worker.handleInstance(req, res);
|
|
610
|
+
} finally {
|
|
611
|
+
this.countRequest('$subsumes', Date.now() - start);
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// CodeSystem/[id]/$validate-code
|
|
616
|
+
router.get('/CodeSystem/:id/\\$validate-code', async (req, res) => {
|
|
617
|
+
const start = Date.now();
|
|
618
|
+
try {
|
|
619
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
620
|
+
await worker.handleCodeSystemInstance(req, res, this.log);
|
|
621
|
+
} finally {
|
|
622
|
+
this.countRequest('$validate', Date.now() - start);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
router.post('/CodeSystem/:id/\\$validate-code', async (req, res) => {
|
|
626
|
+
const start = Date.now();
|
|
627
|
+
try {
|
|
628
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
629
|
+
await worker.handleCodeSystemInstance(req, res, this.log);
|
|
630
|
+
} finally {
|
|
631
|
+
this.countRequest('$validate', Date.now() - start);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ValueSet/[id]/$validate-code
|
|
637
|
+
router.get('/ValueSet/:id/\\$validate-code', async (req, res) => {
|
|
638
|
+
const start = Date.now();
|
|
639
|
+
try {
|
|
640
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
641
|
+
await worker.handleValueSetInstance(req, res, this.log);
|
|
642
|
+
} finally {
|
|
643
|
+
this.countRequest('$validate', Date.now() - start);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
router.post('/ValueSet/:id/\\$validate-code', async (req, res) => {
|
|
647
|
+
const start = Date.now();
|
|
648
|
+
try {
|
|
649
|
+
let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
650
|
+
await worker.handleValueSetInstance(req, res, this.log);
|
|
651
|
+
} finally {
|
|
652
|
+
this.countRequest('$validate', Date.now() - start);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// ValueSet/[id]/$expand
|
|
657
|
+
router.get('/ValueSet/:id/\\$expand', async (req, res) => {
|
|
658
|
+
const start = Date.now();
|
|
659
|
+
try {
|
|
660
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
661
|
+
await worker.handleInstance(req, res, this.log);
|
|
662
|
+
} finally {
|
|
663
|
+
this.countRequest('$expand', Date.now() - start);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
router.post('/ValueSet/:id/\\$expand', async (req, res) => {
|
|
667
|
+
const start = Date.now();
|
|
668
|
+
try {
|
|
669
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
670
|
+
await worker.handleInstance(req, res, this.log);
|
|
671
|
+
} finally {
|
|
672
|
+
this.countRequest('$expand', Date.now() - start);
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
// ConceptMap/[id]/$translate
|
|
677
|
+
router.get('/ConceptMap/:id/\\$translate', async (req, res) => {
|
|
678
|
+
const start = Date.now();
|
|
679
|
+
try {
|
|
680
|
+
let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
681
|
+
await worker.handleInstance(req, res, this.log);
|
|
682
|
+
} finally {
|
|
683
|
+
this.countRequest('$translate', Date.now() - start);
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
router.post('/ConceptMap/:id/\\$translate', async (req, res) => {
|
|
687
|
+
const start = Date.now();
|
|
688
|
+
try {
|
|
689
|
+
let worker = new TranslateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
690
|
+
await worker.handleInstance(req, res, this.log);
|
|
691
|
+
} finally {
|
|
692
|
+
this.countRequest('$translate', Date.now() - start);
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// ===== Read and Search =====
|
|
697
|
+
|
|
698
|
+
// Read: GET /[type]/[id]
|
|
699
|
+
for (const resourceType of resourceTypes) {
|
|
700
|
+
router.get(`/${resourceType}/:id`, async (req, res) => {
|
|
701
|
+
const start = Date.now();
|
|
702
|
+
try {
|
|
703
|
+
// Skip if id starts with $ (it's an operation)
|
|
704
|
+
if (req.params.id.startsWith('$')) {
|
|
705
|
+
return res.status(404).json(this.operationOutcome(
|
|
706
|
+
'error',
|
|
707
|
+
'not-found',
|
|
708
|
+
`Unknown operation: ${req.params.id}`
|
|
709
|
+
));
|
|
710
|
+
}
|
|
711
|
+
let worker = new ReadWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
712
|
+
await worker.handle(req, res, resourceType);
|
|
713
|
+
} finally {
|
|
714
|
+
this.countRequest('read', Date.now() - start);
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Search: GET /[type]
|
|
720
|
+
for (const resourceType of resourceTypes) {
|
|
721
|
+
router.get(`/${resourceType}`, async (req, res) => {
|
|
722
|
+
const start = Date.now();
|
|
723
|
+
try {
|
|
724
|
+
let worker = new SearchWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
725
|
+
await worker.handle(req, res, resourceType);
|
|
726
|
+
} finally {
|
|
727
|
+
this.countRequest('search', Date.now() - start);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
router.post(`/${resourceType}/_search`, async (req, res) => {
|
|
731
|
+
const start = Date.now();
|
|
732
|
+
try {
|
|
733
|
+
let worker = new SearchWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
734
|
+
await worker.handle(req, res, resourceType);
|
|
735
|
+
} finally {
|
|
736
|
+
this.countRequest('search', Date.now() - start);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Unsupported methods
|
|
742
|
+
for (const resourceType of resourceTypes) {
|
|
743
|
+
router.all(`/${resourceType}/:id`, (req, res) => {
|
|
744
|
+
const start = Date.now();
|
|
745
|
+
try {
|
|
746
|
+
if (['PUT', 'POST', 'DELETE', 'PATCH'].includes(req.method)) {
|
|
747
|
+
return res.status(405).json(this.operationOutcome(
|
|
748
|
+
'error',
|
|
749
|
+
'not-supported',
|
|
750
|
+
`Method ${req.method} is not supported`
|
|
751
|
+
));
|
|
752
|
+
}
|
|
753
|
+
} finally {
|
|
754
|
+
this.countRequest('$read', Date.now() - start);
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
router.get('/op.html', async(req, res) => {
|
|
760
|
+
const start = Date.now();
|
|
761
|
+
try {
|
|
762
|
+
let worker = new OperationsWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
763
|
+
await worker.handle(req, res);
|
|
764
|
+
} finally {
|
|
765
|
+
this.countRequest('$op', Date.now() - start);
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// Metadata / CapabilityStatement
|
|
770
|
+
router.get('/metadata', async (req, res) => {
|
|
771
|
+
const start = Date.now();
|
|
772
|
+
try {
|
|
773
|
+
try {
|
|
774
|
+
await this.metadataHandler.handle(req, res);
|
|
775
|
+
} catch (error) {
|
|
776
|
+
this.log.error(`Error in /metadata: ${error.message}`);
|
|
777
|
+
res.status(500).json(this.operationOutcome('error', 'exception', error.message));
|
|
778
|
+
}
|
|
779
|
+
} finally {
|
|
780
|
+
this.countRequest('metadata', Date.now() - start);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// $versions operation
|
|
785
|
+
router.get('/\\$versions', async (req, res) => {
|
|
786
|
+
const start = Date.now();
|
|
787
|
+
try {
|
|
788
|
+
try {
|
|
789
|
+
await this.metadataHandler.handleVersions(req, res);
|
|
790
|
+
} catch (error) {
|
|
791
|
+
this.log.error(`Error in $versions: ${error.message}`);
|
|
792
|
+
res.status(500).json(this.operationOutcome('error', 'exception', error.message));
|
|
793
|
+
}
|
|
794
|
+
} finally {
|
|
795
|
+
this.countRequest('$versions', Date.now() - start);
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Root endpoint info
|
|
800
|
+
router.get('/', async (req, res) => {
|
|
801
|
+
const start = Date.now();
|
|
802
|
+
try {
|
|
803
|
+
await res.json({
|
|
804
|
+
resourceType: 'OperationOutcome',
|
|
805
|
+
issue: [{
|
|
806
|
+
severity: 'information',
|
|
807
|
+
code: 'informational',
|
|
808
|
+
diagnostics: `FHIR Terminology Server - FHIR v${req.txEndpoint.fhirVersion}`
|
|
809
|
+
}]
|
|
810
|
+
});
|
|
811
|
+
} finally {
|
|
812
|
+
this.countRequest('home', Date.now() - start);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Build an OperationOutcome for errors
|
|
819
|
+
*/
|
|
820
|
+
operationOutcome(severity, code, message) {
|
|
821
|
+
return {
|
|
822
|
+
resourceType: 'OperationOutcome',
|
|
823
|
+
issue: [{
|
|
824
|
+
severity,
|
|
825
|
+
code,
|
|
826
|
+
diagnostics: message
|
|
827
|
+
}]
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Get module status for health check
|
|
833
|
+
*/
|
|
834
|
+
getStatus() {
|
|
835
|
+
return {
|
|
836
|
+
enabled: true,
|
|
837
|
+
status: this.library ? 'Running' : 'Not initialized',
|
|
838
|
+
endpoints: this.endpoints.map(e => ({
|
|
839
|
+
path: e.path,
|
|
840
|
+
fhirVersion: e.fhirVersion,
|
|
841
|
+
context: e.context
|
|
842
|
+
}))
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Shutdown the module
|
|
848
|
+
*/
|
|
849
|
+
async shutdown() {
|
|
850
|
+
this.log.info('Shutting down TX module');
|
|
851
|
+
for (const timer of this.timers) {
|
|
852
|
+
clearInterval(timer);
|
|
853
|
+
}
|
|
854
|
+
this.timers = [];
|
|
855
|
+
// Clean up any resources if needed
|
|
856
|
+
await this.library.close();
|
|
857
|
+
this.log.info('TX module shut down');
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
trimParameters(params) {
|
|
861
|
+
if (!params || !params.parameter) {
|
|
862
|
+
return params;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
params.parameter = params.parameter.filter(p => p.name !== 'tx-resource');
|
|
866
|
+
|
|
867
|
+
return params;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
convertResourceToXml(res) {
|
|
871
|
+
switch (res.resourceType) {
|
|
872
|
+
case "CodeSystem" : return CodeSystemXML._jsonToXml(res);
|
|
873
|
+
case "ValueSet" : return ValueSetXML.toXml(res);
|
|
874
|
+
case "CapabilityStatement" : return CapabilityStatementXML.toXml(res, "R5");
|
|
875
|
+
case "TerminologyCapabilities" : return TerminologyCapabilitiesXML.toXml(res, "R5");
|
|
876
|
+
case "Parameters": return ParametersXML.toXml(res, this.fhirVersion);
|
|
877
|
+
case "OperationOutcome": return OperationOutcomeXML.toXml(res, this.fhirVersion);
|
|
878
|
+
}
|
|
879
|
+
throw new Error(`Resource type ${res.resourceType} not supported in XML`);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
convertXmlToResource(xml) {
|
|
883
|
+
// Detect resource type from root element
|
|
884
|
+
const rootMatch = xml.match(/<([A-Za-z]+)\s/);
|
|
885
|
+
if (!rootMatch) {
|
|
886
|
+
throw new Error('Could not detect resource type from XML');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const resourceType = rootMatch[1];
|
|
890
|
+
|
|
891
|
+
let data;
|
|
892
|
+
switch (resourceType) {
|
|
893
|
+
case "Parameters":
|
|
894
|
+
data = ParametersXML.fromXml(xml);
|
|
895
|
+
break;
|
|
896
|
+
case "CodeSystem":
|
|
897
|
+
data = CodeSystemXML.fromXml(xml);
|
|
898
|
+
break;
|
|
899
|
+
case "ValueSet":
|
|
900
|
+
data = ValueSetXML.fromXml(xml);
|
|
901
|
+
break;
|
|
902
|
+
case "ConceptMap":
|
|
903
|
+
data = ConceptMapXML.fromXml(xml);
|
|
904
|
+
break;
|
|
905
|
+
default:
|
|
906
|
+
throw new Error(`Resource type ${resourceType} not supported for XML input`);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
return data;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
countRequest(name, tat) {
|
|
913
|
+
if (this.stats) {
|
|
914
|
+
this.stats.countRequest(name, tat);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
cacheCount() {
|
|
919
|
+
let count = 0;
|
|
920
|
+
for (let ep of this.endpoints) {
|
|
921
|
+
count = count + ep.resourceCache.size() + ep.expansionCache.size();
|
|
922
|
+
}
|
|
923
|
+
return count;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
module.exports = TXModule;
|