fhirsmith 0.7.1 → 0.7.3
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 -2
- package/config-template.json +16 -0
- package/extension-tracker/extension-tracker-template.html +124 -0
- package/extension-tracker/extension-tracker.js +697 -0
- package/extension-tracker/readme.md +63 -0
- package/folder/folder.js +305 -0
- package/folder/readme.md +57 -0
- package/library/html-server.js +8 -2
- package/package.json +4 -2
- package/packages/packages.js +8 -8
- package/publisher/publisher.js +104 -13
- package/server.js +58 -4
- package/tx/cs/cs-snomed.js +13 -7
- package/tx/library/extensions.js +6 -2
- package/tx/library/renderer.js +1 -1
- package/tx/ocl/cache/cache-paths.cjs +4 -5
- package/tx/ocl/cm-ocl.cjs +4 -1
- package/tx/ocl/cs-ocl.cjs +9 -9
- package/tx/ocl/jobs/background-queue.cjs +11 -5
- package/tx/ocl/shared/patches.cjs +37 -0
- package/tx/ocl/vs-ocl.cjs +92 -44
- package/tx/workers/expand.js +23 -12
- package/tx/workers/search.js +26 -15
- package/xig/xig.js +59 -8
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# FHIRsmith Extension Tracker Module
|
|
2
|
+
|
|
3
|
+
Tracks extensions, profiles, and extension usage across FHIR Implementation Guides. IGs submit data via POST, and the module provides browsable views of the collected data.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
"ext-tracker": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"database": "/var/fhir/data/extension-tracker.db",
|
|
11
|
+
"url": "/ext-tracker"
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **database**: path to the SQLite database file (auto-created if missing)
|
|
16
|
+
- **url** (optional): URL base to mount at (defaults to `/ext-tracker`)
|
|
17
|
+
|
|
18
|
+
## POST — Submit IG Data
|
|
19
|
+
|
|
20
|
+
POST JSON to the module root. Required fields: `package`, `version`, `fhirVersion`. Optional: `jurisdiction`, `extensions`, `profiles`, `usage`.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
POST /ext-tracker
|
|
24
|
+
Content-Type: application/json
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"package": "hl7.fhir.uv.tx-ecosystem",
|
|
28
|
+
"version": "1.9.1",
|
|
29
|
+
"fhirVersion": "5.0.0",
|
|
30
|
+
"jurisdiction": "001",
|
|
31
|
+
"extensions": [
|
|
32
|
+
{ "url": "http://example.org/ext", "title": "My Extension", "types": ["string"] }
|
|
33
|
+
],
|
|
34
|
+
"profiles": {
|
|
35
|
+
"Patient": [
|
|
36
|
+
{ "url": "http://example.org/profile", "title": "My Profile" }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"usage": {
|
|
40
|
+
"http://example.org/ext": ["Patient", "Patient.name"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Submitting a new version for an existing package replaces the old data entirely — only the latest submission per package is kept.
|
|
46
|
+
|
|
47
|
+
Duplicate types in extensions are automatically deduplicated.
|
|
48
|
+
|
|
49
|
+
## GET — Browse Data
|
|
50
|
+
|
|
51
|
+
- `/ext-tracker` — Summary dashboard with package list
|
|
52
|
+
- `/ext-tracker/extensions` — All extensions, filterable by `?package=`
|
|
53
|
+
- `/ext-tracker/profiles` — All profiles, filterable by `?resource=` and `?package=`
|
|
54
|
+
- `/ext-tracker/usage` — Extension usage, filterable by `?url=` and `?location=`
|
|
55
|
+
- `/ext-tracker/package/:name` — Detail page for a specific package
|
|
56
|
+
|
|
57
|
+
## Dependencies
|
|
58
|
+
|
|
59
|
+
Requires `better-sqlite3` (`npm install better-sqlite3`).
|
|
60
|
+
|
|
61
|
+
## Database
|
|
62
|
+
|
|
63
|
+
The SQLite database is auto-created on first run with five tables: `packages`, `extensions`, `extension_types`, `profiles`, and `usages`. Foreign keys cascade deletes, so removing a package cleans up all related data.
|
package/folder/folder.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const USERS_FILE = '.users.json';
|
|
5
|
+
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
6
|
+
const SAFE_NAME = /^[a-zA-Z0-9._-]+$/;
|
|
7
|
+
const AUTH_FAIL_DELAY_MS = 5000;
|
|
8
|
+
|
|
9
|
+
function escapeHtml(str) {
|
|
10
|
+
return String(str)
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class FolderModule {
|
|
19
|
+
constructor(stats) {
|
|
20
|
+
this.folders = [];
|
|
21
|
+
this.stats = stats;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
initialize(config, app) {
|
|
25
|
+
this.folders = [];
|
|
26
|
+
|
|
27
|
+
const folderConfigs = config.folders || [];
|
|
28
|
+
for (const fc of folderConfigs) {
|
|
29
|
+
if (fc.enabled === false) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!fc.folder || !fc.url || !fc.name) {
|
|
33
|
+
console.log(`Folder config entry missing required fields (name, folder, url) - skipping`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const rootDir = path.resolve(fc.folder);
|
|
37
|
+
if (!fs.existsSync(rootDir)) {
|
|
38
|
+
console.log(`Folder path ${rootDir} does not exist - skipping "${fc.name}"`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const urlBase = fc.url.startsWith('/') ? fc.url : '/' + fc.url;
|
|
43
|
+
const router = express.Router();
|
|
44
|
+
|
|
45
|
+
// GET - serve files and directory listings
|
|
46
|
+
router.get(urlBase + '/{*subpath}', (req, res) => {
|
|
47
|
+
this.handleGet(req, res, rootDir, urlBase);
|
|
48
|
+
});
|
|
49
|
+
router.get(urlBase, (req, res) => {
|
|
50
|
+
this.handleGet(req, res, rootDir, urlBase);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// PUT - upload with basic auth (express.raw captures body before any global JSON parser)
|
|
54
|
+
router.put(urlBase + '/{*subpath}', express.raw({ type: '*/*', limit: MAX_UPLOAD_BYTES }), (req, res) => {
|
|
55
|
+
this.handlePut(req, res, rootDir, urlBase);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.use('/', router);
|
|
59
|
+
this.folders.push({ name: fc.name, folder: rootDir, url: urlBase });
|
|
60
|
+
console.log(`Folder module: serving "${fc.name}" from ${rootDir} at ${urlBase}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.folders.length === 0) {
|
|
64
|
+
console.log('Folder module: no folders configured');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
handleGet(req, res, rootDir, urlBase) {
|
|
69
|
+
this.stats.countRequest('search', 0);
|
|
70
|
+
const subPath = req.path.substring(urlBase.length) || '/';
|
|
71
|
+
const safePath = path.normalize(subPath);
|
|
72
|
+
if (safePath.includes('..')) {
|
|
73
|
+
return res.status(403).send('Forbidden');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const fullPath = path.join(rootDir, safePath);
|
|
77
|
+
|
|
78
|
+
// never serve .users.json
|
|
79
|
+
if (path.basename(fullPath) === '.users.json') {
|
|
80
|
+
return res.status(403).send('Forbidden');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!fs.existsSync(fullPath)) {
|
|
84
|
+
return res.status(404).send('Not found');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const stat = fs.statSync(fullPath);
|
|
88
|
+
if (stat.isFile()) {
|
|
89
|
+
return res.sendFile(fullPath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (stat.isDirectory()) {
|
|
93
|
+
return this.sendDirectoryListing(res, fullPath, req.path);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return res.status(404).send('Not found');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sendDirectoryListing(res, dirPath, requestPath) {
|
|
100
|
+
const start = Date.now();
|
|
101
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
102
|
+
const urlPath = requestPath.endsWith('/') ? requestPath : requestPath + '/';
|
|
103
|
+
|
|
104
|
+
// separate dirs and files, exclude .users.json
|
|
105
|
+
const dirs = entries.filter(e => e.isDirectory()).sort((a, b) => a.name.localeCompare(b.name));
|
|
106
|
+
const files = entries.filter(e => e.isFile() && e.name !== '.users.json').sort((a, b) => a.name.localeCompare(b.name));
|
|
107
|
+
|
|
108
|
+
const safeRequestPath = escapeHtml(requestPath);
|
|
109
|
+
let html = `<html><head><title>Index of ${safeRequestPath}</title></head><body>`;
|
|
110
|
+
html += `<h1>Index of ${safeRequestPath}</h1><pre>`;
|
|
111
|
+
|
|
112
|
+
// parent directory link (if not at mount root)
|
|
113
|
+
if (requestPath.split('/').filter(Boolean).length > 1) {
|
|
114
|
+
html += `<a href="${urlPath}..">../</a>\n`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const dir of dirs) {
|
|
118
|
+
const safeDirName = escapeHtml(dir.name);
|
|
119
|
+
html += `<a href="${urlPath}${encodeURIComponent(dir.name)}/">${safeDirName}/</a>\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const stat = fs.statSync(path.join(dirPath, file.name));
|
|
124
|
+
const size = this.formatSize(stat.size);
|
|
125
|
+
const safeFileName = escapeHtml(file.name);
|
|
126
|
+
html += `<a href="${urlPath}${encodeURIComponent(file.name)}">${safeFileName}</a>${' '.repeat(Math.max(1, 60 - file.name.length))}${size}\n`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
html += '</pre></body></html>';
|
|
130
|
+
this.stats.countRequest('folder', Date.now() - start);
|
|
131
|
+
res.type('html').send(html);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
formatSize(bytes) {
|
|
135
|
+
if (bytes < 1024) return bytes + ' B';
|
|
136
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
137
|
+
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
138
|
+
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
handlePut(req, res, rootDir, urlBase) {
|
|
142
|
+
this.stats.countRequest('submit', 0);
|
|
143
|
+
const subPath = req.path.substring(urlBase.length);
|
|
144
|
+
const safePath = path.normalize(subPath);
|
|
145
|
+
if (safePath.includes('..')) {
|
|
146
|
+
return res.status(403).send('Forbidden');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const fullPath = path.join(rootDir, safePath);
|
|
150
|
+
|
|
151
|
+
// must be a file path, not a directory
|
|
152
|
+
if (safePath === '/' || safePath === '') {
|
|
153
|
+
return res.status(400).send('Cannot PUT to directory root');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// validate every path segment: alphanumeric, dots, dashes, underscores only
|
|
157
|
+
// split on both / and \ since path.normalize uses \ on Windows
|
|
158
|
+
const segments = safePath.split(/[/\\]/).filter(Boolean);
|
|
159
|
+
for (const seg of segments) {
|
|
160
|
+
if (!SAFE_NAME.test(seg)) {
|
|
161
|
+
return res.status(400).send('Invalid path: names may only contain letters, numbers, dots, dashes and underscores');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// only allow .zip files
|
|
166
|
+
const filename = path.basename(fullPath);
|
|
167
|
+
if (!filename.toLowerCase().endsWith('.zip')) {
|
|
168
|
+
return res.status(400).send('Only .zip files may be uploaded');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// check content-length before reading body
|
|
172
|
+
const contentLength = parseInt(req.headers['content-length'], 10);
|
|
173
|
+
if (contentLength > MAX_UPLOAD_BYTES) {
|
|
174
|
+
return res.status(413).send(`File too large (limit ${MAX_UPLOAD_BYTES / 1024 / 1024} MB)`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// authenticate
|
|
178
|
+
const credentials = this.parseBasicAuth(req);
|
|
179
|
+
if (!credentials) {
|
|
180
|
+
res.set('WWW-Authenticate', 'Basic realm="Folder Upload"');
|
|
181
|
+
return res.status(401).send('Authentication required');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!this.checkUser(rootDir, path.dirname(fullPath), credentials.username, credentials.password)) {
|
|
185
|
+
// deliberate delay to slow down brute-force attempts
|
|
186
|
+
return setTimeout(() => {
|
|
187
|
+
res.set('WWW-Authenticate', 'Basic realm="Folder Upload"');
|
|
188
|
+
res.status(403).send('Access denied');
|
|
189
|
+
}, AUTH_FAIL_DELAY_MS);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
this.getBody(req).then(body => {
|
|
193
|
+
// double-check actual body size
|
|
194
|
+
if (body.length > MAX_UPLOAD_BYTES) {
|
|
195
|
+
return res.status(413).send(`File too large (limit ${MAX_UPLOAD_BYTES / 1024 / 1024} MB)`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ensure parent directory exists
|
|
199
|
+
const parentDir = path.dirname(fullPath);
|
|
200
|
+
if (!fs.existsSync(parentDir)) {
|
|
201
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// write the file
|
|
205
|
+
fs.writeFileSync(fullPath, body);
|
|
206
|
+
|
|
207
|
+
// if it's main.zip or master.zip, also save as default.zip
|
|
208
|
+
const basename = path.basename(fullPath).toLowerCase();
|
|
209
|
+
if (basename === 'main.zip' || basename === 'master.zip') {
|
|
210
|
+
const defaultPath = path.join(parentDir, 'default.zip');
|
|
211
|
+
fs.copyFileSync(fullPath, defaultPath);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return res.status(200).send('OK');
|
|
215
|
+
}).catch(err => {
|
|
216
|
+
console.log(`Folder module: PUT error for ${fullPath}: ${err.message}`);
|
|
217
|
+
return res.status(500).send('Upload failed');
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// get request body as a Buffer, whether or not global middleware already parsed it
|
|
222
|
+
getBody(req) {
|
|
223
|
+
// if global middleware already parsed it, use that
|
|
224
|
+
if (Buffer.isBuffer(req.body)) {
|
|
225
|
+
return Promise.resolve(req.body);
|
|
226
|
+
}
|
|
227
|
+
if (req.body && typeof req.body === 'object') {
|
|
228
|
+
return Promise.resolve(Buffer.from(JSON.stringify(req.body)));
|
|
229
|
+
}
|
|
230
|
+
if (req.body) {
|
|
231
|
+
return Promise.resolve(Buffer.from(req.body));
|
|
232
|
+
}
|
|
233
|
+
// no middleware parsed it — read from stream
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const chunks = [];
|
|
236
|
+
req.on('data', chunk => chunks.push(chunk));
|
|
237
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
238
|
+
req.on('error', reject);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
parseBasicAuth(req) {
|
|
243
|
+
const header = req.headers.authorization;
|
|
244
|
+
if (!header || !header.startsWith('Basic ')) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const decoded = Buffer.from(header.substring(6), 'base64').toString('utf-8');
|
|
248
|
+
const colon = decoded.indexOf(':');
|
|
249
|
+
if (colon < 0) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
username: decoded.substring(0, colon),
|
|
254
|
+
password: decoded.substring(colon + 1)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// walk up from the target directory to rootDir looking for .users.json
|
|
259
|
+
checkUser(rootDir, dir, username, password) {
|
|
260
|
+
let current = path.resolve(dir);
|
|
261
|
+
const root = path.resolve(rootDir);
|
|
262
|
+
|
|
263
|
+
while (true) {
|
|
264
|
+
const usersPath = path.join(current, USERS_FILE);
|
|
265
|
+
if (fs.existsSync(usersPath)) {
|
|
266
|
+
try {
|
|
267
|
+
const users = JSON.parse(fs.readFileSync(usersPath, 'utf-8'));
|
|
268
|
+
if (users[username] && users[username] === password) {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
} catch (e) {
|
|
272
|
+
// malformed json, keep walking up
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (current === root) {
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const parent = path.dirname(current);
|
|
281
|
+
if (parent === current) {
|
|
282
|
+
break; // filesystem root - stop
|
|
283
|
+
}
|
|
284
|
+
current = parent;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
shutdown() {
|
|
291
|
+
// nothing to clean up
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
getStatus() {
|
|
295
|
+
return {
|
|
296
|
+
folders: this.folders.map(f => ({
|
|
297
|
+
name: f.name,
|
|
298
|
+
folder: f.folder,
|
|
299
|
+
url: f.url
|
|
300
|
+
}))
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = FolderModule;
|
package/folder/readme.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# FHIRsmith Folder Module
|
|
2
|
+
|
|
3
|
+
Serves static files from one or more directories, with optional authenticated upload via PUT.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Add a `folder` entry under `modules` in your config, following the standard FHIRsmith module pattern:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
"folder": {
|
|
11
|
+
"enabled": true,
|
|
12
|
+
"folders": [
|
|
13
|
+
{ "name": "packages", "folder": "/var/fhir/packages", "url": "/packages" },
|
|
14
|
+
{ "name": "igs", "folder": "/var/fhir/igs", "url": "/igs", "enabled": false }
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The top-level `enabled` controls whether the module loads at all. Each folder entry also supports its own `enabled: false` to disable individual folders without removing them from the config.
|
|
20
|
+
|
|
21
|
+
- **name**: display name (used in status and logging)
|
|
22
|
+
- **folder**: absolute path to the directory to serve
|
|
23
|
+
- **url**: URL path to mount at
|
|
24
|
+
- **enabled** (optional): set to `false` to skip this entry
|
|
25
|
+
|
|
26
|
+
Directories that don't exist at startup are skipped with a warning. Zero folders is valid.
|
|
27
|
+
|
|
28
|
+
## GET — File Serving
|
|
29
|
+
|
|
30
|
+
Any GET request under the configured URL serves files directly. If the path is a directory, it returns an HTML listing showing subdirectories first, then files with sizes.
|
|
31
|
+
|
|
32
|
+
## PUT — Authenticated Upload
|
|
33
|
+
|
|
34
|
+
PUT writes a file to the path specified in the URL. The parent directory is created automatically if it doesn't exist.
|
|
35
|
+
|
|
36
|
+
All PUT requests require HTTP Basic authentication. Credentials are checked against `.users.json` files (see below).
|
|
37
|
+
|
|
38
|
+
Special case: uploading `main.zip` or `master.zip` automatically creates a copy named `default.zip` in the same directory.
|
|
39
|
+
|
|
40
|
+
## .users.json
|
|
41
|
+
|
|
42
|
+
Authentication for PUT is controlled by `.users.json` files placed anywhere in the served directory tree. Format is a simple JSON object mapping usernames to passwords:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"grahame": "secretpassword",
|
|
47
|
+
"ci-bot": "buildtoken123"
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
When a PUT comes in, the module walks up from the target directory to the folder root looking for a `.users.json` file. The first one found that contains a matching username and password grants access. If no match is found at any level, the request is rejected with 403.
|
|
52
|
+
|
|
53
|
+
This means you can put a `.users.json` at the root to cover everything, or use per-subdirectory files to grant more specific access. `.users.json` files are never served by GET.
|
|
54
|
+
|
|
55
|
+
## Dependencies
|
|
56
|
+
|
|
57
|
+
No additional npm dependencies required.
|
package/library/html-server.js
CHANGED
|
@@ -100,20 +100,26 @@ class HtmlServer {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
sendErrorResponse(res, templateName, error, statusCode = 500) {
|
|
103
|
+
if (res.headersSent) {
|
|
104
|
+
this.log.error('[HtmlServer] Cannot send error response - headers already sent:', error.message || error);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
103
107
|
const errorContent = `
|
|
104
108
|
<div class="alert alert-danger">
|
|
105
109
|
<h4>Error</h4>
|
|
106
110
|
<p>${escape(error.message || error)}</p>
|
|
107
111
|
</div>
|
|
108
112
|
`;
|
|
109
|
-
|
|
113
|
+
|
|
110
114
|
try {
|
|
111
115
|
const html = this.renderPage(templateName, 'Error', errorContent);
|
|
112
116
|
res.status(statusCode).setHeader('Content-Type', 'text/html');
|
|
113
117
|
res.send(html);
|
|
114
118
|
} catch (renderError) {
|
|
115
119
|
this.log.error('[HtmlServer] Error rendering error page:', renderError);
|
|
116
|
-
res.
|
|
120
|
+
if (!res.headersSent) {
|
|
121
|
+
res.status(statusCode).send(`<h1>Error</h1><p>Failed to render error page: ${escape(renderError.message)}</p>`);
|
|
122
|
+
}
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fhirsmith",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "A Node.js server that provides a collection of tools to serve the FHIR ecosystem",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"engines": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"axios": "^1.13.4",
|
|
33
33
|
"base45": "^3.0.0",
|
|
34
34
|
"bcrypt": "^6.0.0",
|
|
35
|
+
"better-sqlite3": "^12.8.0",
|
|
35
36
|
"cbor": "^9.0.1",
|
|
36
37
|
"chalk": "^4.1.2",
|
|
37
38
|
"chokidar": "^4.0.3",
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"fhir-validator-wrapper": "1.2.2",
|
|
48
49
|
"fhirpath": "^4.8.3",
|
|
49
50
|
"fs-extra": "^11.3.3",
|
|
51
|
+
"ini": "^6.0.0",
|
|
50
52
|
"inquirer": "^8.2.5",
|
|
51
53
|
"liquidjs": "^10.24.0",
|
|
52
54
|
"lusca": "^1.7.0",
|
|
@@ -116,4 +118,4 @@
|
|
|
116
118
|
"url": "https://github.com/HealthIntersections/fhirsmith/issues"
|
|
117
119
|
},
|
|
118
120
|
"homepage": "https://github.com/HealthIntersections/fhirsmith#readme"
|
|
119
|
-
}
|
|
121
|
+
}
|
package/packages/packages.js
CHANGED
|
@@ -1721,10 +1721,10 @@ class PackagesModule {
|
|
|
1721
1721
|
}
|
|
1722
1722
|
|
|
1723
1723
|
const dependency = row.Dependency;
|
|
1724
|
-
const
|
|
1725
|
-
if (
|
|
1726
|
-
const depName = dependency.substring(0,
|
|
1727
|
-
const depVersion = dependency.substring(
|
|
1724
|
+
const atIndex = dependency.indexOf('@');
|
|
1725
|
+
if (atIndex > 0) {
|
|
1726
|
+
const depName = dependency.substring(0, atIndex);
|
|
1727
|
+
const depVersion = dependency.substring(atIndex + 1);
|
|
1728
1728
|
deps[row.PackageVersionKey][depName] = depVersion;
|
|
1729
1729
|
}
|
|
1730
1730
|
}
|
|
@@ -2493,10 +2493,10 @@ class PackagesModule {
|
|
|
2493
2493
|
}
|
|
2494
2494
|
|
|
2495
2495
|
// Extract dependency name and version
|
|
2496
|
-
const
|
|
2497
|
-
if (
|
|
2498
|
-
const depName = dependency.substring(0,
|
|
2499
|
-
const depVersion = dependency.substring(
|
|
2496
|
+
const atIndex = dependency.indexOf('@');
|
|
2497
|
+
if (atIndex > 0) {
|
|
2498
|
+
const depName = dependency.substring(0, atIndex);
|
|
2499
|
+
const depVersion = dependency.substring(atIndex + 1);
|
|
2500
2500
|
const depMajorMinor = this.getMajorMinorVersion(depVersion);
|
|
2501
2501
|
const depRef = `${depName}#${depMajorMinor}`;
|
|
2502
2502
|
|