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
|
@@ -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
|
+
```
|