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.
Files changed (277) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/FHIRsmith.png +0 -0
  3. package/README.md +277 -0
  4. package/config-template.json +144 -0
  5. package/library/folder-setup.js +58 -0
  6. package/library/html-server.js +166 -0
  7. package/library/html.js +835 -0
  8. package/library/i18nsupport.js +259 -0
  9. package/library/languages.js +779 -0
  10. package/library/logger-telnet.js +205 -0
  11. package/library/logger.js +279 -0
  12. package/library/package-manager.js +876 -0
  13. package/library/utilities.js +196 -0
  14. package/library/version-utilities.js +1056 -0
  15. package/npmprojector/config-example.json +13 -0
  16. package/npmprojector/indexer.js +394 -0
  17. package/npmprojector/npmprojector.js +395 -0
  18. package/npmprojector/readme.md +174 -0
  19. package/npmprojector/watcher.js +335 -0
  20. package/package.json +119 -0
  21. package/packages/package-crawler.js +846 -0
  22. package/packages/packages-template.html +126 -0
  23. package/packages/packages.js +2838 -0
  24. package/passwords.ini +2 -0
  25. package/publisher/publisher-template.html +208 -0
  26. package/publisher/publisher.js +2167 -0
  27. package/publisher/task-draft.js +458 -0
  28. package/registry/api.js +735 -0
  29. package/registry/crawler.js +637 -0
  30. package/registry/model.js +513 -0
  31. package/registry/readme.md +243 -0
  32. package/registry/registry-data.json +121015 -0
  33. package/registry/registry-template.html +126 -0
  34. package/registry/registry.js +1395 -0
  35. package/registry/test-runner.js +237 -0
  36. package/root-template.html +124 -0
  37. package/server.js +524 -0
  38. package/shl/private-key.pem +5 -0
  39. package/shl/public-key.pem +18 -0
  40. package/shl/shl.js +1125 -0
  41. package/shl/vhl.js +69 -0
  42. package/static/FHIRsmith128.png +0 -0
  43. package/static/FHIRsmith16.png +0 -0
  44. package/static/FHIRsmith32.png +0 -0
  45. package/static/FHIRsmith64.png +0 -0
  46. package/static/assets/css/bootstrap-fhir.css +5302 -0
  47. package/static/assets/css/bootstrap-glyphicons.css +2 -0
  48. package/static/assets/css/bootstrap.css +4097 -0
  49. package/static/assets/css/jquery-ui.css +523 -0
  50. package/static/assets/css/jquery-ui.structure.css +863 -0
  51. package/static/assets/css/jquery-ui.structure.min.css +5 -0
  52. package/static/assets/css/jquery-ui.theme.css +439 -0
  53. package/static/assets/css/jquery-ui.theme.min.css +5 -0
  54. package/static/assets/css/jquery.ui.all.css +7 -0
  55. package/static/assets/css/modules.css +18 -0
  56. package/static/assets/css/project.css +367 -0
  57. package/static/assets/css/pygments-manni.css +66 -0
  58. package/static/assets/css/tags.css +74 -0
  59. package/static/assets/css/xml.css +2 -0
  60. package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
  61. package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
  62. package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
  63. package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
  64. package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
  65. package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
  66. package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
  67. package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
  68. package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
  69. package/static/assets/ico/favicon.ico +0 -0
  70. package/static/assets/ico/favicon.png +0 -0
  71. package/static/assets/images/fhir-logo-www.png +0 -0
  72. package/static/assets/images/fhir-logo.png +0 -0
  73. package/static/assets/images/hl7-logo.png +0 -0
  74. package/static/assets/images/logo_ansinew.jpg +0 -0
  75. package/static/assets/images/search.png +0 -0
  76. package/static/assets/images/stripe.png +0 -0
  77. package/static/assets/images/target.png +0 -0
  78. package/static/assets/images/tx-registry-root.gif +0 -0
  79. package/static/assets/images/tx-registry.png +0 -0
  80. package/static/assets/images/tx-server.png +0 -0
  81. package/static/assets/images/tx-version.png +0 -0
  82. package/static/assets/js/bootstrap.min.js +6 -0
  83. package/static/assets/js/fhir-gw.js +259 -0
  84. package/static/assets/js/fhir.js +2 -0
  85. package/static/assets/js/html5shiv.js +8 -0
  86. package/static/assets/js/jcookie.js +96 -0
  87. package/static/assets/js/jquery-ui.min.js +6 -0
  88. package/static/assets/js/jquery.js +10716 -0
  89. package/static/assets/js/jquery.min.js +2 -0
  90. package/static/assets/js/jquery.ui.core.js +314 -0
  91. package/static/assets/js/jquery.ui.draggable.js +825 -0
  92. package/static/assets/js/jquery.ui.mouse.js +162 -0
  93. package/static/assets/js/jquery.ui.resizable.js +842 -0
  94. package/static/assets/js/jquery.ui.widget.js +268 -0
  95. package/static/assets/js/json2.js +487 -0
  96. package/static/assets/js/jtip.js +97 -0
  97. package/static/assets/js/respond.min.js +6 -0
  98. package/static/assets/js/statuspage.js +70 -0
  99. package/static/assets/js/xml.js +2 -0
  100. package/static/dist/js/bootstrap.js +1964 -0
  101. package/static/favicon.png +0 -0
  102. package/static/fhir.css +626 -0
  103. package/static/icon-fhir-16.png +0 -0
  104. package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  105. package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  106. package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
  107. package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  108. package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  109. package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  110. package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  111. package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  112. package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  113. package/static/images/ui-icons_222222_256x240.png +0 -0
  114. package/static/images/ui-icons_228ef1_256x240.png +0 -0
  115. package/static/images/ui-icons_ef8c08_256x240.png +0 -0
  116. package/static/images/ui-icons_ffd27a_256x240.png +0 -0
  117. package/static/images/ui-icons_ffffff_256x240.png +0 -0
  118. package/static/js/jquery.effects.blind.js +49 -0
  119. package/static/js/jquery.effects.bounce.js +78 -0
  120. package/static/js/jquery.effects.clip.js +54 -0
  121. package/static/js/jquery.effects.core.js +763 -0
  122. package/static/js/jquery.effects.drop.js +50 -0
  123. package/static/js/jquery.effects.explode.js +79 -0
  124. package/static/js/jquery.effects.fade.js +32 -0
  125. package/static/js/jquery.effects.fold.js +56 -0
  126. package/static/js/jquery.effects.highlight.js +50 -0
  127. package/static/js/jquery.effects.pulsate.js +51 -0
  128. package/static/js/jquery.effects.scale.js +178 -0
  129. package/static/js/jquery.effects.shake.js +57 -0
  130. package/static/js/jquery.effects.slide.js +50 -0
  131. package/static/js/jquery.effects.transfer.js +45 -0
  132. package/static/js/jquery.ui.accordion.js +611 -0
  133. package/static/js/jquery.ui.autocomplete.js +612 -0
  134. package/static/js/jquery.ui.button.js +416 -0
  135. package/static/js/jquery.ui.datepicker.js +1823 -0
  136. package/static/js/jquery.ui.dialog.js +878 -0
  137. package/static/js/jquery.ui.droppable.js +296 -0
  138. package/static/js/jquery.ui.position.js +252 -0
  139. package/static/js/jquery.ui.progressbar.js +109 -0
  140. package/static/js/jquery.ui.selectable.js +266 -0
  141. package/static/js/jquery.ui.slider.js +666 -0
  142. package/static/js/jquery.ui.sortable.js +1077 -0
  143. package/static/js/jquery.ui.tabs.js +758 -0
  144. package/stats.js +80 -0
  145. package/test-cache/vsac/vsac-valuesets.db +0 -0
  146. package/token/nginx_passport_setup.md +383 -0
  147. package/token/security_guide.md +294 -0
  148. package/token/token-template.html +330 -0
  149. package/token/token.js +1300 -0
  150. package/translations/Messages.properties +1510 -0
  151. package/translations/Messages_ar.properties +1399 -0
  152. package/translations/Messages_de.properties +836 -0
  153. package/translations/Messages_es.properties +737 -0
  154. package/translations/Messages_fr.properties +1 -0
  155. package/translations/Messages_ja.properties +893 -0
  156. package/translations/Messages_nl.properties +1357 -0
  157. package/translations/Messages_pt.properties +1302 -0
  158. package/translations/Messages_ru.properties +1 -0
  159. package/translations/Messages_uz.properties +1 -0
  160. package/translations/Messages_zh.properties +1 -0
  161. package/translations/rendering-phrases.properties +1128 -0
  162. package/translations/rendering-phrases_ar.properties +1091 -0
  163. package/translations/rendering-phrases_de.properties +6 -0
  164. package/translations/rendering-phrases_es.properties +6 -0
  165. package/translations/rendering-phrases_fr.properties +624 -0
  166. package/translations/rendering-phrases_ja.properties +21 -0
  167. package/translations/rendering-phrases_nl.properties +970 -0
  168. package/translations/rendering-phrases_pt.properties +1020 -0
  169. package/translations/rendering-phrases_ru.properties +1094 -0
  170. package/translations/rendering-phrases_uz.properties +1 -0
  171. package/translations/rendering-phrases_zh.properties +1 -0
  172. package/tx/README.md +418 -0
  173. package/tx/cm/cm-api.js +110 -0
  174. package/tx/cm/cm-database.js +735 -0
  175. package/tx/cm/cm-package.js +325 -0
  176. package/tx/cs/cs-api.js +789 -0
  177. package/tx/cs/cs-areacode.js +615 -0
  178. package/tx/cs/cs-country.js +1110 -0
  179. package/tx/cs/cs-cpt.js +785 -0
  180. package/tx/cs/cs-cs.js +1579 -0
  181. package/tx/cs/cs-currency.js +539 -0
  182. package/tx/cs/cs-db.js +1321 -0
  183. package/tx/cs/cs-hgvs.js +329 -0
  184. package/tx/cs/cs-lang.js +465 -0
  185. package/tx/cs/cs-loinc.js +1485 -0
  186. package/tx/cs/cs-mimetypes.js +238 -0
  187. package/tx/cs/cs-ndc.js +704 -0
  188. package/tx/cs/cs-omop.js +1025 -0
  189. package/tx/cs/cs-provider-api.js +43 -0
  190. package/tx/cs/cs-provider-list.js +37 -0
  191. package/tx/cs/cs-rxnorm.js +808 -0
  192. package/tx/cs/cs-snomed.js +1102 -0
  193. package/tx/cs/cs-ucum.js +514 -0
  194. package/tx/cs/cs-unii.js +271 -0
  195. package/tx/cs/cs-uri.js +218 -0
  196. package/tx/cs/cs-usstates.js +305 -0
  197. package/tx/dev.fhir.org.yml +14 -0
  198. package/tx/fixtures/test-cases-setup.json +18 -0
  199. package/tx/fixtures/test-cases.yml +16 -0
  200. package/tx/html/codesystem-operations.liquid +25 -0
  201. package/tx/html/home-metrics.liquid +247 -0
  202. package/tx/html/operations-form.liquid +148 -0
  203. package/tx/html/search-form.liquid +62 -0
  204. package/tx/html/tx-template.html +133 -0
  205. package/tx/html/valueset-operations.liquid +54 -0
  206. package/tx/importers/atc-to-fhir.js +316 -0
  207. package/tx/importers/import-loinc.module.js +1536 -0
  208. package/tx/importers/import-ndc.module.js +1088 -0
  209. package/tx/importers/import-rxnorm.module.js +898 -0
  210. package/tx/importers/import-sct.module.js +2457 -0
  211. package/tx/importers/import-unii.module.js +601 -0
  212. package/tx/importers/readme.md +453 -0
  213. package/tx/importers/subset-loinc.module.js +1081 -0
  214. package/tx/importers/subset-rxnorm.module.js +938 -0
  215. package/tx/importers/tx-import-base.js +351 -0
  216. package/tx/importers/tx-import-settings.js +310 -0
  217. package/tx/importers/tx-import.js +357 -0
  218. package/tx/library/canonical-resource.js +88 -0
  219. package/tx/library/capabilitystatement.js +292 -0
  220. package/tx/library/codesystem.js +774 -0
  221. package/tx/library/conceptmap.js +568 -0
  222. package/tx/library/designations.js +932 -0
  223. package/tx/library/errors.js +77 -0
  224. package/tx/library/extensions.js +117 -0
  225. package/tx/library/namingsystem.js +322 -0
  226. package/tx/library/operation-outcome.js +127 -0
  227. package/tx/library/parameters.js +105 -0
  228. package/tx/library/renderer.js +1559 -0
  229. package/tx/library/terminologycapabilities.js +418 -0
  230. package/tx/library/ucum-parsers.js +1029 -0
  231. package/tx/library/ucum-service.js +370 -0
  232. package/tx/library/ucum-types.js +1099 -0
  233. package/tx/library/valueset.js +543 -0
  234. package/tx/library.js +676 -0
  235. package/tx/ocl/cm-ocl.js +106 -0
  236. package/tx/ocl/cs-ocl.js +39 -0
  237. package/tx/ocl/vs-ocl.js +105 -0
  238. package/tx/operation-context.js +568 -0
  239. package/tx/params.js +613 -0
  240. package/tx/provider.js +403 -0
  241. package/tx/sct/ecl.js +1560 -0
  242. package/tx/sct/expressions.js +2077 -0
  243. package/tx/sct/structures.js +1396 -0
  244. package/tx/tx-html.js +1063 -0
  245. package/tx/tx.fhir.org.yml +39 -0
  246. package/tx/tx.js +927 -0
  247. package/tx/vs/vs-api.js +112 -0
  248. package/tx/vs/vs-database.js +786 -0
  249. package/tx/vs/vs-package.js +358 -0
  250. package/tx/vs/vs-vsac.js +366 -0
  251. package/tx/workers/batch-validate.js +129 -0
  252. package/tx/workers/batch.js +361 -0
  253. package/tx/workers/closure.js +32 -0
  254. package/tx/workers/expand.js +1845 -0
  255. package/tx/workers/lookup.js +407 -0
  256. package/tx/workers/metadata.js +467 -0
  257. package/tx/workers/operations.js +34 -0
  258. package/tx/workers/read.js +164 -0
  259. package/tx/workers/search.js +384 -0
  260. package/tx/workers/subsumes.js +334 -0
  261. package/tx/workers/translate.js +492 -0
  262. package/tx/workers/validate.js +2504 -0
  263. package/tx/workers/worker.js +904 -0
  264. package/tx/xml/capabilitystatement-xml.js +63 -0
  265. package/tx/xml/codesystem-xml.js +62 -0
  266. package/tx/xml/conceptmap-xml.js +65 -0
  267. package/tx/xml/namingsystem-xml.js +65 -0
  268. package/tx/xml/operationoutcome-xml.js +127 -0
  269. package/tx/xml/parameters-xml.js +312 -0
  270. package/tx/xml/terminologycapabilities-xml.js +64 -0
  271. package/tx/xml/valueset-xml.js +64 -0
  272. package/tx/xml/xml-base.js +603 -0
  273. package/vcl/vcl-parser.js +1098 -0
  274. package/vcl/vcl.js +253 -0
  275. package/windows-install.js +19 -0
  276. package/xig/xig-template.html +124 -0
  277. 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;