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
@@ -0,0 +1,395 @@
1
+ //
2
+ // NpmProjector Module
3
+ // Watches an npm package directory and serves FHIR resources with search indexes
4
+ //
5
+
6
+ const express = require('express');
7
+ const Logger = require('../library/logger');
8
+ const FHIRIndexer = require('./indexer');
9
+ const PackageWatcher = require('./watcher');
10
+
11
+ // Load FHIRPath models for different FHIR versions
12
+ const fhirModels = {
13
+ 'r4': () => require('fhirpath/fhir-context/r4'),
14
+ 'r5': () => require('fhirpath/fhir-context/r5'),
15
+ 'stu3': () => require('fhirpath/fhir-context/stu3'),
16
+ 'dstu2': () => require('fhirpath/fhir-context/dstu2')
17
+ };
18
+ class NpmProjectorModule {
19
+ constructor(stats) {
20
+ this.router = express.Router();
21
+ this.log = Logger.getInstance().child({ module: 'npmprojector' });
22
+ this.config = null;
23
+ this.watcher = null;
24
+ this.currentIndexer = null;
25
+ this.lastReloadTime = null;
26
+ this.lastReloadStats = null;
27
+ this.reloadCount = 0;
28
+ this.stats = stats;
29
+ }
30
+
31
+ /**
32
+ * Get the configured base path for this module (for server.js to use)
33
+ */
34
+ static getBasePath(config) {
35
+ return config.basePath || '/npmprojector';
36
+ }
37
+ async initialize(config) {
38
+ this.config = config;
39
+ this.log.info('Initializing NpmProjector module');
40
+
41
+ // Validate config
42
+ if (!config.npmPath) {
43
+ throw new Error('NpmProjector module requires npmPath configuration');
44
+ }
45
+
46
+ // Load the appropriate FHIRPath model
47
+ const fhirVersion = config.fhirVersion || 'r4';
48
+ if (!fhirModels[fhirVersion]) {
49
+ throw new Error(`Unsupported FHIR version: ${fhirVersion}. Supported: ${Object.keys(fhirModels).join(', ')}`);
50
+ }
51
+
52
+ try {
53
+ this.fhirModel = fhirModels[fhirVersion]();
54
+ this.log.info(`Loaded FHIRPath model for ${fhirVersion.toUpperCase()}`);
55
+ } catch (err) {
56
+ throw new Error(`Failed to load FHIRPath model for ${fhirVersion}: ${err.message}`);
57
+ }
58
+
59
+ // Initialize indexer with the model
60
+ this.currentIndexer = new FHIRIndexer(this.fhirModel);
61
+ // Set up routes
62
+ this.setupRoutes();
63
+
64
+ // Set up file watcher
65
+ this.watcher = new PackageWatcher(config.npmPath, {
66
+ debounceMs: config.debounceMs || 500,
67
+ onReload: (data) => this.handleReload(data),
68
+ log: this.log
69
+ });
70
+
71
+ // Start watching (includes initial load)
72
+ this.watcher.start();
73
+
74
+ this.log.info(`NpmProjector module initialized, watching: ${config.npmPath}`);
75
+ }
76
+
77
+ /**
78
+ * Handle package reload - builds new index then swaps atomically
79
+ */
80
+ handleReload({ resources, searchParameters }) {
81
+ const startTime = Date.now();
82
+
83
+ try {
84
+ // Filter resources by configured types if specified
85
+ let filteredResources = resources;
86
+ if (this.config.resourceTypes && this.config.resourceTypes.length > 0) {
87
+ filteredResources = resources.filter(r =>
88
+ this.config.resourceTypes.includes(r.resourceType)
89
+ );
90
+ this.log.info(`Filtered to ${filteredResources.length} resources of types: ${this.config.resourceTypes.join(', ')}`);
91
+ }
92
+
93
+ // Load additional search parameters from configured path if specified
94
+ let allSearchParameters = [...searchParameters];
95
+ if (this.config.searchParametersPath) {
96
+ const additionalParams = this.watcher.loadSearchParametersFrom(this.config.searchParametersPath);
97
+ allSearchParameters = [...allSearchParameters, ...additionalParams];
98
+ this.log.info(`Loaded ${additionalParams.length} additional search parameters from ${this.config.searchParametersPath}`);
99
+ }
100
+
101
+ // Build new indexer with the FHIR model
102
+ const newIndexer = new FHIRIndexer(this.fhirModel);
103
+ const stats = newIndexer.build(filteredResources, allSearchParameters);
104
+
105
+ // Atomic swap
106
+ this.currentIndexer = newIndexer;
107
+
108
+ const elapsed = Date.now() - startTime;
109
+ this.lastReloadTime = new Date().toISOString();
110
+ this.lastReloadStats = stats;
111
+ this.reloadCount++;
112
+
113
+ this.log.info(`Index rebuilt in ${elapsed}ms: ${JSON.stringify(stats)}`);
114
+ } catch (error) {
115
+ this.log.error('Error during reload:', error);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get the current indexer for request handling
121
+ */
122
+ getIndexer() {
123
+ return this.currentIndexer;
124
+ }
125
+
126
+ /**
127
+ * Set up Express routes
128
+ */
129
+ setupRoutes() {
130
+ // CORS for browser access
131
+ this.router.use((req, res, next) => {
132
+ res.header('Access-Control-Allow-Origin', '*');
133
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
134
+ next();
135
+ });
136
+
137
+ // Root - module info
138
+ this.router.get('/', (req, res) => {
139
+ const start = Date.now();
140
+ try {
141
+
142
+ const indexer = this.getIndexer();
143
+ const types = indexer.getResourceTypes();
144
+ const stats = indexer.getStats();
145
+
146
+ res.json({
147
+ message: 'NpmProjector FHIR Server',
148
+ npmPath: this.config.npmPath,
149
+ fhirVersion: this.config.fhirVersion || 'r4',
150
+ resourceTypes: types,
151
+ configuredTypes: this.config.resourceTypes || 'all',
152
+ stats: stats,
153
+ lastReload: this.lastReloadTime,
154
+ reloadCount: this.reloadCount,
155
+ endpoints: {
156
+ metadata: 'metadata',
157
+ stats: '_stats',
158
+ search: '[ResourceType]',
159
+ read: '[ResourceType]/[id]'
160
+ }
161
+ });
162
+ } finally {
163
+ this.stats.countRequest('home', Date.now() - start);
164
+ }
165
+ });
166
+
167
+ // Capability Statement (metadata)
168
+ this.router.get('/metadata', (req, res) => {
169
+ const start = Date.now();
170
+ try {
171
+ const indexer = this.getIndexer();
172
+ res.json(this.buildCapabilityStatement(indexer));
173
+ } finally {
174
+ this.stats.countRequest('metadata', Date.now() - start);
175
+ }
176
+ });
177
+
178
+ // Stats endpoint
179
+ this.router.get('/_stats', (req, res) => {
180
+ const start = Date.now();
181
+ try {
182
+ const indexer = this.getIndexer();
183
+ res.json({
184
+ ...indexer.getStats(),
185
+ lastReload: this.lastReloadTime,
186
+ reloadCount: this.reloadCount
187
+ });
188
+ } finally {
189
+ this.stats.countRequest('stats', Date.now() - start);
190
+ }
191
+ });
192
+
193
+ // Trigger manual reload
194
+ this.router.post('/_reload', (req, res) => {
195
+ const start = Date.now();
196
+ try {
197
+
198
+ this.log.info('Manual reload triggered');
199
+ this.watcher.triggerReload();
200
+ res.json({message: 'Reload triggered', reloadCount: this.reloadCount});
201
+ } finally {
202
+ this.stats.countRequest('reload', Date.now() - start);
203
+ }
204
+ });
205
+
206
+ // Read: GET /[type]/[id]
207
+ this.router.get('/:resourceType/:id', (req, res) => {
208
+ const start = Date.now();
209
+ try {
210
+
211
+ const {resourceType, id} = req.params;
212
+ const indexer = this.getIndexer();
213
+
214
+ const resource = indexer.read(resourceType, id);
215
+
216
+ if (!resource) {
217
+ return res.status(404).json(this.operationOutcome(
218
+ 'error',
219
+ 'not-found',
220
+ `${resourceType}/${id} not found`
221
+ ));
222
+ }
223
+
224
+ res.json(resource);
225
+ } finally {
226
+ this.stats.countRequest('*', Date.now() - start);
227
+ }
228
+ });
229
+
230
+ // Search: GET /[type]?params...
231
+ this.router.get('/:resourceType', (req, res) => {
232
+ const start = Date.now();
233
+ try {
234
+
235
+ const {resourceType} = req.params;
236
+ const indexer = this.getIndexer();
237
+
238
+ // Check if resource type exists
239
+ if (!indexer.getResourceTypes().includes(resourceType)) {
240
+ return res.status(404).json(this.operationOutcome(
241
+ 'error',
242
+ 'not-found',
243
+ `Resource type ${resourceType} not found`
244
+ ));
245
+ }
246
+
247
+ // Extract search parameters
248
+ const searchParams = {};
249
+ for (const [key, value] of Object.entries(req.query || {})) {
250
+ if (!key.startsWith('_') || key === '_id') {
251
+ searchParams[key] = value;
252
+ }
253
+ }
254
+
255
+ // Handle _id specially
256
+ if (searchParams._id) {
257
+ searchParams.id = searchParams._id;
258
+ delete searchParams._id;
259
+ }
260
+
261
+ const results = indexer.search(resourceType, searchParams);
262
+
263
+ // Handle _count
264
+ let count = parseInt(req.query._count) || 100;
265
+ count = Math.min(count, 1000);
266
+
267
+ const paginatedResults = results.slice(0, count);
268
+
269
+ res.json(this.buildSearchBundle(paginatedResults, req, results.length));
270
+ } finally {
271
+ this.stats.countRequest(':resourceType', Date.now() - start);
272
+ }
273
+ });
274
+ }
275
+
276
+ /**
277
+ * Build a FHIR Bundle for search results
278
+ */
279
+ buildSearchBundle(resources, req, totalCount) {
280
+ const protocol = req.protocol;
281
+ const host = req.get('host');
282
+ const baseUrl = `${protocol}://${host}${req.baseUrl}`;
283
+ const fullUrl = `${protocol}://${host}${req.originalUrl}`;
284
+
285
+ return {
286
+ resourceType: 'Bundle',
287
+ type: 'searchset',
288
+ total: totalCount,
289
+ link: [
290
+ {
291
+ relation: 'self',
292
+ url: fullUrl
293
+ }
294
+ ],
295
+ entry: resources.map(resource => ({
296
+ fullUrl: `${baseUrl}/${resource.resourceType}/${resource.id}`,
297
+ resource: resource,
298
+ search: {
299
+ mode: 'match'
300
+ }
301
+ }))
302
+ };
303
+ }
304
+
305
+ /**
306
+ * Build a CapabilityStatement
307
+ */
308
+ buildCapabilityStatement(indexer) {
309
+ const resourceTypes = indexer.getResourceTypes();
310
+
311
+ const restResources = resourceTypes.map(type => {
312
+ const searchParams = indexer.getSearchParams(type);
313
+
314
+ return {
315
+ type: type,
316
+ interaction: [
317
+ { code: 'read' },
318
+ { code: 'search-type' }
319
+ ],
320
+ searchParam: searchParams.map(sp => ({
321
+ name: sp.code,
322
+ type: sp.type,
323
+ documentation: sp.description || sp.name
324
+ }))
325
+ };
326
+ });
327
+
328
+ return {
329
+ resourceType: 'CapabilityStatement',
330
+ status: 'active',
331
+ date: new Date().toISOString(),
332
+ kind: 'instance',
333
+ software: {
334
+ name: 'NpmProjector FHIR Server',
335
+ version: '1.0.0'
336
+ },
337
+ fhirVersion: '4.0.1',
338
+ format: ['json'],
339
+ rest: [
340
+ {
341
+ mode: 'server',
342
+ resource: restResources
343
+ }
344
+ ]
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Build an OperationOutcome for errors
350
+ */
351
+ operationOutcome(severity, code, message) {
352
+ return {
353
+ resourceType: 'OperationOutcome',
354
+ issue: [
355
+ {
356
+ severity: severity,
357
+ code: code,
358
+ diagnostics: message
359
+ }
360
+ ]
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Get module status for health check
366
+ */
367
+ getStatus() {
368
+ const stats = this.currentIndexer.getStats();
369
+ return {
370
+ enabled: true,
371
+ status: stats.totalResources > 0 ? 'Running' : 'Empty',
372
+ npmPath: this.config?.npmPath,
373
+ basePath: this.config?.basePath || '/npmprojector',
374
+ fhirVersion: this.config?.fhirVersion || 'r4',
375
+ totalResources: stats.totalResources,
376
+ resourceTypes: Object.keys(stats.resourceTypes),
377
+ lastReload: this.lastReloadTime,
378
+ reloadCount: this.reloadCount
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Shutdown the module
384
+ */
385
+ async shutdown() {
386
+ this.log.info('Shutting down NpmProjector module');
387
+ if (this.watcher) {
388
+ this.watcher.stop();
389
+ }
390
+ this.log.info('NpmProjector module shut down');
391
+ }
392
+
393
+ }
394
+
395
+ module.exports = NpmProjectorModule;
@@ -0,0 +1,174 @@
1
+ # NpmProjector Module
2
+
3
+ Watches a local npm package directory and serves FHIR resources with FHIRPath-based search indexes. Part of the FHIR Development Server.
4
+
5
+ ## Features
6
+
7
+ - **Hot reload**: Automatically rebuilds indexes when files in the watched directory change
8
+ - **Atomic swap**: In-flight requests complete against consistent data during reloads
9
+ - **FHIRPath-based indexing**: Uses `fhirpath` library to evaluate SearchParameter expressions
10
+ - **Standard FHIR search**: Supports string, token, reference, date, quantity, number, and uri parameter types
11
+ - **Bundle support**: Automatically extracts resources from FHIR Bundles
12
+
13
+ ## Configuration
14
+
15
+ Add to your `config.json`:
16
+
17
+ ```json
18
+ {
19
+ "modules": {
20
+ "npmprojector": {
21
+ "enabled": true,
22
+ "basePath": "/fhir",
23
+ "npmPath": "/path/to/fhir/package.tgz",
24
+ "fhirVersion": "r4",
25
+ "resourceTypes": ["Medication"],
26
+ "resourceFolders": ["data/medications"],
27
+ "searchParametersFolder": "data/search",
28
+ "searchParametersPath": "/path/to/external/search-params",
29
+ "debounceMs": 500
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ### Configuration Options
36
+
37
+ | Option | Required | Default | Description |
38
+ |--------|----------|---------|-------------|
39
+ | `enabled` | Yes | - | Whether the module is enabled |
40
+ | `basePath` | No | `/npmprojector` | URL path to mount the module |
41
+ | `npmPath` | Yes | - | Path to directory or .tgz file containing FHIR resources |
42
+ | `fhirVersion` | No | `r4` | FHIR version: `r4`, `r5`, `stu3`, `dstu2` |
43
+ | `resourceTypes` | No | all | Array of resource types to serve (omit/null for all) |
44
+ | `resourceFolders` | No | all | Array of subfolders within package to load resources from |
45
+ | `searchParametersFolder` | No | - | Subfolder within the package containing SearchParameters |
46
+ | `searchParametersPath` | No | - | External path to load additional SearchParameters from |
47
+ | `debounceMs` | No | 500 | Debounce time for file change detection |
48
+
49
+ ### Folder Options Explained
50
+
51
+ **resourceFolders**: Only load resources from specific subfolders within the package. Paths are relative to the package root. If not specified, all folders are scanned.
52
+
53
+ ```json
54
+ "resourceFolders": ["data/medications", "data/patients"]
55
+ ```
56
+
57
+ **searchParametersFolder**: Load SearchParameters from a specific subfolder within the package (instead of finding them mixed in with resources).
58
+
59
+ ```json
60
+ "searchParametersFolder": "data/search"
61
+ ```
62
+
63
+ **searchParametersPath**: Load SearchParameters from an external location (outside the package). This is useful for loading standard FHIR search parameters.
64
+
65
+ ```json
66
+ "searchParametersPath": "/Users/you/.fhir/packages/hl7.fhir.r4.core#4.0.1/package"
67
+ ```
68
+
69
+ Both `searchParametersFolder` and `searchParametersPath` can be used together - they will be merged.
70
+
71
+ ## Server Integration
72
+
73
+ Add to `server.js`:
74
+
75
+ ```javascript
76
+ const NpmProjectorModule = require('./npmprojector/npmprojector.js');
77
+
78
+ // In initializeModules():
79
+ if (config.modules.npmprojector && config.modules.npmprojector.enabled) {
80
+ try {
81
+ modules.npmprojector = new NpmProjectorModule();
82
+ await modules.npmprojector.initialize(config.modules.npmprojector);
83
+
84
+ // Use configured basePath or default
85
+ const basePath = NpmProjectorModule.getBasePath(config.modules.npmprojector);
86
+ app.use(basePath, modules.npmprojector.router);
87
+ } catch (error) {
88
+ serverLog.error('Failed to initialize NpmProjector module:', error);
89
+ throw error;
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Endpoints
95
+
96
+ | Endpoint | Description |
97
+ |----------|-------------|
98
+ | `GET /npmprojector/` | Module info and available resource types |
99
+ | `GET /npmprojector/metadata` | FHIR CapabilityStatement |
100
+ | `GET /npmprojector/_stats` | Index statistics |
101
+ | `POST /npmprojector/_reload` | Trigger manual reload |
102
+ | `GET /npmprojector/[ResourceType]` | Search resources |
103
+ | `GET /npmprojector/[ResourceType]/[id]` | Read a single resource |
104
+
105
+ ## Search Examples
106
+
107
+ ```bash
108
+ # Get all patients
109
+ curl http://localhost:3000/npmprojector/Patient
110
+
111
+ # Search by family name (case-insensitive, starts-with)
112
+ curl "http://localhost:3000/npmprojector/Patient?family=smith"
113
+
114
+ # Search by gender (token)
115
+ curl "http://localhost:3000/npmprojector/Patient?gender=male"
116
+
117
+ # Search by identifier with system
118
+ curl "http://localhost:3000/npmprojector/Patient?identifier=http://example.org/mrn|12345"
119
+
120
+ # Search observations by code (LOINC)
121
+ curl "http://localhost:3000/npmprojector/Observation?code=http://loinc.org|8867-4"
122
+
123
+ # OR search (comma-separated)
124
+ curl "http://localhost:3000/npmprojector/Patient?gender=male,female"
125
+
126
+ # Multiple parameters (AND)
127
+ curl "http://localhost:3000/npmprojector/Patient?family=smith&gender=male"
128
+ ```
129
+
130
+ ## Directory Structure
131
+
132
+ The watched directory should contain JSON files with FHIR resources:
133
+
134
+ ```
135
+ your-fhir-package/
136
+ ├── resources.json # Bundle of Patient, Observation, etc.
137
+ ├── search-parameters.json # Bundle of SearchParameter definitions
138
+ └── more-data/
139
+ └── additional.json # More resources (recursively loaded)
140
+ ```
141
+
142
+ SearchParameter resources can be:
143
+ 1. Mixed in with other resources in the watched directory
144
+ 2. Loaded from a separate `searchParametersPath` location
145
+ 3. Both (they will be merged)
146
+
147
+ ## Supported Search Parameter Types
148
+
149
+ | Type | Behavior |
150
+ |------|----------|
151
+ | `string` | Case-insensitive starts-with matching |
152
+ | `token` | Supports `system|code`, `|code`, `code`, `system|` |
153
+ | `reference` | Exact match on reference string |
154
+ | `date` | Prefix matching on ISO date strings |
155
+ | `quantity` | Numeric equality |
156
+ | `number` | Numeric equality |
157
+ | `uri` | Exact match |
158
+
159
+ ## Limitations
160
+
161
+ - **Read-only**: No create/update/delete operations
162
+ - **No chained search**: `subject.name=John` not supported
163
+ - **No _include/_revinclude**: Related resources not included
164
+ - **No date ranges**: `ge`, `le`, `gt`, `lt` prefixes not supported
165
+ - **No composite parameters**: Multi-field search params not supported
166
+ - **No modifiers**: `:exact`, `:contains`, `:missing` not supported
167
+
168
+ ## Dependencies
169
+
170
+ Requires `fhirpath`, `chokidar`, and `tar` packages:
171
+
172
+ ```bash
173
+ npm install fhirpath chokidar tar
174
+ ```