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
package/publisher/publisher.js
CHANGED
|
@@ -198,6 +198,24 @@ class PublisherModule {
|
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
|
+
const websiteColumns = await new Promise((resolve, reject) => {
|
|
202
|
+
this.db.all("PRAGMA table_info(websites)", (err, rows) => {
|
|
203
|
+
if (err) reject(err);
|
|
204
|
+
else resolve(rows || []);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
const websiteColumnNames = websiteColumns.map(c => c.name);
|
|
208
|
+
if (!websiteColumnNames.includes('git_root')) {
|
|
209
|
+
await new Promise((resolve, reject) => {
|
|
210
|
+
this.db.run('ALTER TABLE websites ADD COLUMN git_root TEXT', (err) => {
|
|
211
|
+
if (err) reject(err);
|
|
212
|
+
else {
|
|
213
|
+
this.logger.info('Migration: added git_root column to websites table');
|
|
214
|
+
resolve();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
async createDefaultAdmin() {
|
|
@@ -268,6 +286,8 @@ class PublisherModule {
|
|
|
268
286
|
// Admin routes
|
|
269
287
|
this.router.get('/admin/websites', this.requireAdmin.bind(this), this.renderWebsites.bind(this));
|
|
270
288
|
this.router.post('/admin/websites', this.requireAdmin.bind(this), this.createWebsite.bind(this));
|
|
289
|
+
this.router.get('/admin/websites/:id/edit', this.requireAdmin.bind(this), this.renderEditWebsite.bind(this));
|
|
290
|
+
this.router.post('/admin/websites/:id/edit', this.requireAdmin.bind(this), this.updateWebsite.bind(this));
|
|
271
291
|
this.router.get('/admin/users', this.requireAdmin.bind(this), this.renderUsers.bind(this));
|
|
272
292
|
this.router.post('/admin/users', this.requireAdmin.bind(this), this.createUser.bind(this));
|
|
273
293
|
this.router.post('/admin/permissions', this.requireAdmin.bind(this), this.updatePermissions.bind(this));
|
|
@@ -682,7 +702,7 @@ class PublisherModule {
|
|
|
682
702
|
const historyDir = path.join(taskDir, 'fhir-ig-history-template');
|
|
683
703
|
const templatesDir = path.join(taskDir, 'fhir-web-templates');
|
|
684
704
|
|
|
685
|
-
await this.runCommand('git', ['clone', '
|
|
705
|
+
await this.runCommand('git', ['clone', 'git@github.com:FHIR/ig-registry.git', registryDir],
|
|
686
706
|
{}, task.id, 'Cloning ig-registry');
|
|
687
707
|
|
|
688
708
|
await this.runCommand('git', ['clone', 'https://github.com/HL7/fhir-ig-history-template.git', historyDir],
|
|
@@ -698,7 +718,7 @@ class PublisherModule {
|
|
|
698
718
|
}
|
|
699
719
|
|
|
700
720
|
// Step 3: Pull latest web folder before publishing into it
|
|
701
|
-
await this.runCommand('git', ['pull'], { cwd: website.
|
|
721
|
+
await this.runCommand('git', ['pull'], { cwd: website.git_root }, task.id, 'Pulling latest web folder');
|
|
702
722
|
|
|
703
723
|
// Step 4: Run the IG publisher in go-publish mode
|
|
704
724
|
await this.runPublisherGoPublish(task.id, publisherJar, draftDir, website.local_folder,
|
|
@@ -716,9 +736,9 @@ class PublisherModule {
|
|
|
716
736
|
await this.logTaskMessage(task.id, 'info', 'Committing changes to web folder...');
|
|
717
737
|
const gitUrl = 'https://github.com/' + task.github_org + '/' + task.github_repo + '.git';
|
|
718
738
|
const commitMsg = 'publish ' + task.npm_package_id + '#' + task.version + ' from ' + gitUrl + ' ' + task.git_branch;
|
|
719
|
-
await this.runCommand('git', ['add', '.'], { cwd: website.
|
|
720
|
-
await this.runCommand('git', ['commit', '-m', commitMsg], { cwd: website.
|
|
721
|
-
await this.runCommand('git', ['push'], { cwd: website.
|
|
739
|
+
await this.runCommand('git', ['add', '.'], { cwd: website.git_root }, task.id, 'Staging web folder changes');
|
|
740
|
+
await this.runCommand('git', ['commit', '-m', commitMsg], { cwd: website.git_root }, task.id, 'Committing web folder changes');
|
|
741
|
+
await this.runCommand('git', ['push'], { cwd: website.git_root }, task.id, 'Pushing web folder changes');
|
|
722
742
|
|
|
723
743
|
// Step 7: Commit and push the ig-registry
|
|
724
744
|
await this.logTaskMessage(task.id, 'info', 'Committing changes to ig-registry...');
|
|
@@ -1583,6 +1603,71 @@ class PublisherModule {
|
|
|
1583
1603
|
}
|
|
1584
1604
|
}
|
|
1585
1605
|
|
|
1606
|
+
async renderEditWebsite(req, res) {
|
|
1607
|
+
const start = Date.now();
|
|
1608
|
+
try {
|
|
1609
|
+
const htmlServer = require('../library/html-server');
|
|
1610
|
+
const website = await this.getWebsite(req.params.id);
|
|
1611
|
+
if (!website) return res.status(404).send('Website not found');
|
|
1612
|
+
|
|
1613
|
+
let content = '<h3>Edit Website</h3>';
|
|
1614
|
+
content += '<form method="post" action="/publisher/admin/websites/' + website.id + '/edit" class="row g-3">';
|
|
1615
|
+
content += '<div class="col-md-4"><label class="form-label">Website Name</label>';
|
|
1616
|
+
content += '<input type="text" class="form-control" name="name" value="' + escape(website.name) + '" required></div>';
|
|
1617
|
+
content += '<div class="col-md-4"><label class="form-label">Local Folder</label>';
|
|
1618
|
+
content += '<input type="text" class="form-control" name="local_folder" value="' + escape(website.local_folder) + '" required></div>';
|
|
1619
|
+
content += '<div class="col-md-4"><label class="form-label">Git Root (repo root for git operations)</label>';
|
|
1620
|
+
content += '<input type="text" class="form-control" name="git_root" value="' + escape(website.git_root || '') + '"></div>';
|
|
1621
|
+
content += '<div class="col-md-4"><label class="form-label">History Templates</label>';
|
|
1622
|
+
content += '<input type="text" class="form-control" name="history_templates" value="' + escape(website.history_templates) + '" required></div>';
|
|
1623
|
+
content += '<div class="col-md-4"><label class="form-label">Web Templates</label>';
|
|
1624
|
+
content += '<input type="text" class="form-control" name="web_templates" value="' + escape(website.web_templates) + '" required></div>';
|
|
1625
|
+
content += '<div class="col-md-4"><label class="form-label">Update Script</label>';
|
|
1626
|
+
content += '<input type="text" class="form-control" name="server_update_script" value="' + escape(website.server_update_script) + '" required></div>';
|
|
1627
|
+
content += '<div class="col-md-4"><label class="form-label">Active</label>';
|
|
1628
|
+
content += '<select class="form-control" name="is_active"><option value="1"' + (website.is_active ? ' selected' : '') + '>Yes</option><option value="0"' + (!website.is_active ? ' selected' : '') + '>No</option></select></div>';
|
|
1629
|
+
content += '<div class="col-12"><button type="submit" class="btn btn-primary">Save Changes</button> ';
|
|
1630
|
+
content += '<a href="/publisher/admin/websites" class="btn btn-secondary">Cancel</a></div>';
|
|
1631
|
+
content += '</form>';
|
|
1632
|
+
|
|
1633
|
+
const html = htmlServer.renderPage('publisher', 'Edit Website - FHIR Publisher', content, {
|
|
1634
|
+
templateVars: { loginTitle: 'Logout', loginPath: 'logout', loginAction: 'POST' }
|
|
1635
|
+
});
|
|
1636
|
+
res.setHeader('Content-Type', 'text/html');
|
|
1637
|
+
res.send(html);
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
this.logger.error('Error rendering edit website:', error);
|
|
1640
|
+
res.status(500).send('Internal server error');
|
|
1641
|
+
} finally {
|
|
1642
|
+
this.stats.countRequest('websites-edit', Date.now() - start);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
async updateWebsite(req, res) {
|
|
1647
|
+
const start = Date.now();
|
|
1648
|
+
try {
|
|
1649
|
+
const { name, local_folder, git_root, history_templates, web_templates, server_update_script, is_active } = req.body;
|
|
1650
|
+
const websiteId = req.params.id;
|
|
1651
|
+
|
|
1652
|
+
await new Promise((resolve, reject) => {
|
|
1653
|
+
this.db.run(
|
|
1654
|
+
'UPDATE websites SET name=?, local_folder=?, git_root = ?, history_templates=?, web_templates=?, server_update_script=?, is_active=? WHERE id=?',
|
|
1655
|
+
[name, local_folder, git_root, history_templates, web_templates, server_update_script, is_active === '1' ? 1 : 0, websiteId],
|
|
1656
|
+
(err) => err ? reject(err) : resolve()
|
|
1657
|
+
);
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
this.logUserAction(req.session.userId, 'update_website', websiteId, req.ip);
|
|
1661
|
+
this.logger.info('Website updated: ' + websiteId + ' by user ' + req.session.userId);
|
|
1662
|
+
res.redirect('/publisher/admin/websites');
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
this.logger.error('Error updating website:', error);
|
|
1665
|
+
res.status(500).send('Failed to update website');
|
|
1666
|
+
} finally {
|
|
1667
|
+
this.stats.countRequest('websites-update', Date.now() - start);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1586
1671
|
async renderWebsites(req, res) {
|
|
1587
1672
|
const start = Date.now();
|
|
1588
1673
|
try {
|
|
@@ -1607,6 +1692,10 @@ class PublisherModule {
|
|
|
1607
1692
|
content += '<input type="text" class="form-control" id="local_folder" name="local_folder" required>';
|
|
1608
1693
|
content += '</div>';
|
|
1609
1694
|
content += '<div class="col-md-4">';
|
|
1695
|
+
content += '<label class="form-label">Git Root (repo root for git operations)</label>';
|
|
1696
|
+
content += '<input type="text" class="form-control" name="git_root" required>';
|
|
1697
|
+
content += '</div>';
|
|
1698
|
+
content += '<div class="col-md-4">';
|
|
1610
1699
|
content += '<label for="history_templates" class="form-label">History Templates</label>';
|
|
1611
1700
|
content += '<input type="text" class="form-control" id="history_templates" name="history_templates" required>';
|
|
1612
1701
|
content += '</div>';
|
|
@@ -1634,18 +1723,20 @@ class PublisherModule {
|
|
|
1634
1723
|
} else {
|
|
1635
1724
|
content += '<div class="table-responsive">';
|
|
1636
1725
|
content += '<table class="table table-striped">';
|
|
1637
|
-
content += '<thead><tr><th>Name</th><th>Local Folder</th><th>Update Script</th><th>Active</th><th>Created</th></tr></thead>';
|
|
1726
|
+
content += '<thead><tr><th>Name</th><th>Local Folder</th><th>Git Root</th><th>Update Script</th><th>Active</th><th>Created</th><th>Actions</th></tr></thead>';
|
|
1638
1727
|
content += '<tbody>';
|
|
1639
1728
|
|
|
1640
1729
|
websites.forEach(website => {
|
|
1641
1730
|
content += '<tr>';
|
|
1642
1731
|
content += '<td>' + website.name + '</td>';
|
|
1643
|
-
content += '<td><code>' + website.local_folder + '</code></td>';
|
|
1644
|
-
content += '<td><code>' + website.
|
|
1645
|
-
content += '<td><code>' + website.
|
|
1646
|
-
content += '<td><code>' + website.
|
|
1732
|
+
content += '<td><code>' + escape(website.local_folder) + '</code></td>';
|
|
1733
|
+
content += '<td><code>' + escape(website.git_root || '') + '</code></td>';
|
|
1734
|
+
content += '<td><code>' + escape(website.history_templates) + '</code></td>';
|
|
1735
|
+
content += '<td><code>' + escape(website.web_templates) + '</code></td>';
|
|
1736
|
+
content += '<td><code>' + escape(website.server_update_script) + '</code></td>';
|
|
1647
1737
|
content += '<td>' + (website.is_active ? '✓' : '✗') + '</td>';
|
|
1648
1738
|
content += '<td>' + new Date(website.created_at).toLocaleString() + '</td>';
|
|
1739
|
+
content += '<td><a href="/publisher/admin/websites/' + website.id + '/edit" class="btn btn-sm btn-outline-primary">Edit</a></td>';
|
|
1649
1740
|
content += '</tr>';
|
|
1650
1741
|
});
|
|
1651
1742
|
|
|
@@ -1677,12 +1768,12 @@ class PublisherModule {
|
|
|
1677
1768
|
const start = Date.now();
|
|
1678
1769
|
try {
|
|
1679
1770
|
try {
|
|
1680
|
-
const {name, local_folder, history_templates, web_templates, server_update_script} = req.body;
|
|
1771
|
+
const {name, local_folder, git_root, history_templates, web_templates, server_update_script} = req.body;
|
|
1681
1772
|
|
|
1682
1773
|
await new Promise((resolve, reject) => {
|
|
1683
1774
|
this.db.run(
|
|
1684
|
-
'INSERT INTO websites (name, local_folder, history_templates, web_templates, server_update_script) VALUES (?, ?, ?, ?, ?)',
|
|
1685
|
-
[name, local_folder, history_templates, web_templates, server_update_script],
|
|
1775
|
+
'INSERT INTO websites (name, local_folder, git_root, history_templates, web_templates, server_update_script) VALUES (?, ?, ?, ?, ?, ?)',
|
|
1776
|
+
[name, local_folder, git_root, history_templates, web_templates, server_update_script],
|
|
1686
1777
|
function (err) {
|
|
1687
1778
|
if (err) reject(err);
|
|
1688
1779
|
else resolve();
|
package/server.js
CHANGED
|
@@ -59,6 +59,8 @@ const TXModule = require('./tx/tx.js');
|
|
|
59
59
|
const htmlServer = require('./library/html-server');
|
|
60
60
|
const ServerStats = require("./stats");
|
|
61
61
|
const {Liquid} = require("liquidjs");
|
|
62
|
+
const FolderModule = require("./folder/folder");
|
|
63
|
+
const ExtensionTrackerModule = require("./extension-tracker/extension-tracker");
|
|
62
64
|
|
|
63
65
|
htmlServer.useLog(serverLog);
|
|
64
66
|
|
|
@@ -195,6 +197,17 @@ async function initializeModules() {
|
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
|
|
201
|
+
if (config.modules?.['ext-tracker']?.enabled) {
|
|
202
|
+
try {
|
|
203
|
+
serverLog.info('Initializing module: ext-tracker...');
|
|
204
|
+
modules.extTracker = new ExtensionTrackerModule(stats);
|
|
205
|
+
await modules.extTracker.initialize(config.modules['ext-tracker'], app);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
serverLog.error('Failed to initialize extension tracker module:', error);
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
198
211
|
// Initialize TX module
|
|
199
212
|
// Note: TX module registers its own endpoints directly on the app
|
|
200
213
|
// because it supports multiple endpoints at different paths
|
|
@@ -208,6 +221,18 @@ async function initializeModules() {
|
|
|
208
221
|
throw error;
|
|
209
222
|
}
|
|
210
223
|
}
|
|
224
|
+
|
|
225
|
+
if (config.modules?.folder?.enabled) {
|
|
226
|
+
try {
|
|
227
|
+
serverLog.info('Initializing module: folder...');
|
|
228
|
+
modules.folder = new FolderModule(stats);
|
|
229
|
+
await modules.folder.initialize(config.modules.folder, app);
|
|
230
|
+
// mount the router
|
|
231
|
+
} catch (error) {
|
|
232
|
+
serverLog.error('Failed to initialize folder module:', error);
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
211
236
|
}
|
|
212
237
|
|
|
213
238
|
async function loadTemplates() {
|
|
@@ -312,6 +337,32 @@ async function buildRootPageContent() {
|
|
|
312
337
|
content += '</li>';
|
|
313
338
|
}
|
|
314
339
|
|
|
340
|
+
if (config.modules?.['ext-tracker']?.enabled) {
|
|
341
|
+
mc++;
|
|
342
|
+
content += '<li class="list-group-item">';
|
|
343
|
+
content += '<a href="/ext-tracker" class="text-decoration-none">Extension Tracker</a>: ';
|
|
344
|
+
content += 'View of Extension Usage';
|
|
345
|
+
content += '</li>';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (config.modules.folder && config.modules.folder.enabled) {
|
|
349
|
+
content += '<li class="list-group-item">';
|
|
350
|
+
content += '<strong>Cache Folder</strong>: ';
|
|
351
|
+
content += 'Cache Folder for Kindling';
|
|
352
|
+
const folders = config.modules.folder.folders || [];
|
|
353
|
+
content += '<ul class="mt-2 mb-0">';
|
|
354
|
+
for (const fc of folders) {
|
|
355
|
+
if (fc.enabled === false) continue;
|
|
356
|
+
mc++;
|
|
357
|
+
content += '<li>';
|
|
358
|
+
content += `<a href="${fc.url}" class="text-decoration-none">${fc.name}</a>: `;
|
|
359
|
+
content += 'File folder with write control';
|
|
360
|
+
content += '</li>';
|
|
361
|
+
}
|
|
362
|
+
content += '</ul>';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
315
366
|
if (config.modules.tx && config.modules.tx.enabled) {
|
|
316
367
|
content += '<li class="list-group-item">';
|
|
317
368
|
content += '<strong>TX Terminology Server</strong>: ';
|
|
@@ -350,12 +401,14 @@ async function buildRootPageContent() {
|
|
|
350
401
|
const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(2);
|
|
351
402
|
const heapTotalMB = (memUsage.heapTotal / 1024 / 1024).toFixed(2);
|
|
352
403
|
const rssMB = (memUsage.rss / 1024 / 1024).toFixed(2);
|
|
404
|
+
const freeMemMB = (os.freemem() / 1024 / 1024).toFixed(0);
|
|
405
|
+
const totalMemMB = (os.totalmem() / 1024 / 1024).toFixed(0);
|
|
353
406
|
|
|
354
407
|
content += '<table class="grid">';
|
|
355
408
|
content += '<tr>';
|
|
356
|
-
content += `<td><strong>Module Count:</strong> ${mc}</td>`;
|
|
357
409
|
content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
|
|
358
410
|
content += `<td><strong>Request Count:</strong> ${stats.requestCount}</td>`;
|
|
411
|
+
content += `<td><strong>Free Memory:</strong> ${freeMemMB} MB of ${totalMemMB} MB</td>`;
|
|
359
412
|
content += '</tr>';
|
|
360
413
|
content += '<tr>';
|
|
361
414
|
content += `<td><strong>Heap Used:</strong> ${heapUsedMB} MB</td>`;
|
|
@@ -433,7 +486,7 @@ app.get('/', async (req, res) => {
|
|
|
433
486
|
about
|
|
434
487
|
};
|
|
435
488
|
|
|
436
|
-
const html = htmlServer.renderPage('root', escape(config.hostName)
|
|
489
|
+
const html = htmlServer.renderPage('root', config.hostName ? escape(config.hostName) : 'FHIRsmith Server', content, stats);
|
|
437
490
|
res.setHeader('Content-Type', 'text/html');
|
|
438
491
|
res.send(html);
|
|
439
492
|
return;
|
|
@@ -442,8 +495,9 @@ app.get('/', async (req, res) => {
|
|
|
442
495
|
htmlServer.sendErrorResponse(res, 'root', error);
|
|
443
496
|
return;
|
|
444
497
|
}
|
|
498
|
+
} else {
|
|
499
|
+
return serveFhirsmithHome(req, res);
|
|
445
500
|
}
|
|
446
|
-
return serveFhirsmithHome(req, res);
|
|
447
501
|
});
|
|
448
502
|
|
|
449
503
|
app.get('/fhirsmith', (req, res) => serveFhirsmithHome(req, res));
|
|
@@ -622,7 +676,7 @@ async function serveFhirsmithHome(req, res) {
|
|
|
622
676
|
processingTime: Date.now() - startTime
|
|
623
677
|
};
|
|
624
678
|
|
|
625
|
-
const html = htmlServer.renderPage('root', escape(config.hostName)
|
|
679
|
+
const html = htmlServer.renderPage('root', config.hostName ? escape(config.hostName) : 'FHIRsmith Server', content, stats);
|
|
626
680
|
res.setHeader('Content-Type', 'text/html');
|
|
627
681
|
res.send(html);
|
|
628
682
|
return;
|
package/tx/cs/cs-snomed.js
CHANGED
|
@@ -410,9 +410,9 @@ class SnomedServices {
|
|
|
410
410
|
|
|
411
411
|
if (matchFound) {
|
|
412
412
|
// Calculate priority based on match quality
|
|
413
|
-
if (term === searchText.toLowerCase()) {
|
|
413
|
+
if (term === searchText.filter.toLowerCase()) {
|
|
414
414
|
priority = 100; // Exact match
|
|
415
|
-
} else if (term.startsWith(searchText.toLowerCase())) {
|
|
415
|
+
} else if (term.startsWith(searchText.filter.toLowerCase())) {
|
|
416
416
|
priority = 50; // Prefix match
|
|
417
417
|
} else {
|
|
418
418
|
priority = 10; // Contains match
|
|
@@ -615,10 +615,14 @@ class SnomedProvider extends BaseCSServices {
|
|
|
615
615
|
getLanguageCode(langIndex) {
|
|
616
616
|
const languageMap = {
|
|
617
617
|
1: 'en',
|
|
618
|
-
2: '
|
|
619
|
-
3: '
|
|
620
|
-
4: '
|
|
621
|
-
5: '
|
|
618
|
+
2: 'fr',
|
|
619
|
+
3: 'nl',
|
|
620
|
+
4: 'es',
|
|
621
|
+
5: 'sv',
|
|
622
|
+
6: 'da',
|
|
623
|
+
7: 'de',
|
|
624
|
+
8: 'it',
|
|
625
|
+
9: 'cs'
|
|
622
626
|
};
|
|
623
627
|
return languageMap[langIndex] || 'en';
|
|
624
628
|
}
|
|
@@ -969,7 +973,9 @@ class SnomedProvider extends BaseCSServices {
|
|
|
969
973
|
|
|
970
974
|
// Search filter
|
|
971
975
|
async searchFilter(filterContext, filter, sort) {
|
|
972
|
-
|
|
976
|
+
let f = this.sct.searchFilter(filter, false, sort);
|
|
977
|
+
filterContext.filters.push(f);
|
|
978
|
+
return f;
|
|
973
979
|
}
|
|
974
980
|
|
|
975
981
|
// Subsumption testing
|
package/tx/library/extensions.js
CHANGED
|
@@ -110,14 +110,18 @@ const Extensions = {
|
|
|
110
110
|
if (!exp.extension) {
|
|
111
111
|
exp.extension = [];
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
let ext = { url : url, valueBoolean : b };
|
|
114
|
+
exp.extension.push(ext);
|
|
115
|
+
return ext;
|
|
114
116
|
},
|
|
115
117
|
|
|
116
118
|
addString(exp, url, s) {
|
|
117
119
|
if (!exp.extension) {
|
|
118
120
|
exp.extension = [];
|
|
119
121
|
}
|
|
120
|
-
|
|
122
|
+
let ext = { url : url, valueString : s };
|
|
123
|
+
exp.extension.push(ext);
|
|
124
|
+
return ext;
|
|
121
125
|
}
|
|
122
126
|
}
|
|
123
127
|
|
package/tx/library/renderer.js
CHANGED
|
@@ -439,7 +439,7 @@ class Renderer {
|
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
} else {
|
|
442
|
-
li.tx(this.translate('VALUE_SET_CODES_FROM'));
|
|
442
|
+
li.tx(this.translate('VALUE_SET_CODES_FROM')+" ");
|
|
443
443
|
await this.renderLink(li,inc.system+(inc.version ? "|"+inc.version : ""));
|
|
444
444
|
li.tx(" "+ this.translate('VALUE_SET_WHERE')+" ");
|
|
445
445
|
li.startCommaList("and");
|
|
@@ -15,11 +15,10 @@ function sanitizeFilename(text) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function getCacheFilePath(baseDir, canonicalUrl, version = null, paramsKey = null) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
const base = `${canonicalUrl}|${version || ''}|${paramsKey || 'default'}`;
|
|
20
|
+
const hash = crypto.createHash('sha256').update(base).digest('hex');
|
|
21
|
+
const filename = `${hash}.json`;
|
|
23
22
|
return path.join(baseDir, filename);
|
|
24
23
|
}
|
|
25
24
|
|
package/tx/ocl/cm-ocl.cjs
CHANGED
|
@@ -46,7 +46,10 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
46
46
|
async fetchConceptMap(url, version) {
|
|
47
47
|
this._validateFetchParams(url, version);
|
|
48
48
|
|
|
49
|
-
const
|
|
49
|
+
const crypto = require('crypto');
|
|
50
|
+
const base = `${url}|${version || ''}`;
|
|
51
|
+
const hash = crypto.createHash('sha256').update(base).digest('hex');
|
|
52
|
+
const direct = this.conceptMapMap.get(hash);
|
|
50
53
|
if (direct) {
|
|
51
54
|
return direct;
|
|
52
55
|
}
|
package/tx/ocl/cs-ocl.cjs
CHANGED
|
@@ -21,13 +21,12 @@ function normalizeCanonicalSystem(system) {
|
|
|
21
21
|
return system;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
let trimmed = system.trim();
|
|
25
25
|
if (!trimmed) {
|
|
26
26
|
return trimmed;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
return trimmed.replace(/\/+$/, '');
|
|
29
|
+
return trimmed;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
@@ -517,16 +516,14 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
517
516
|
}
|
|
518
517
|
|
|
519
518
|
#normalizePath(pathValue) {
|
|
519
|
+
// Não normaliza nem remove barras, retorna exatamente o valor fornecido pelo autor
|
|
520
520
|
if (!pathValue) {
|
|
521
521
|
return null;
|
|
522
522
|
}
|
|
523
523
|
if (typeof pathValue !== 'string') {
|
|
524
524
|
return null;
|
|
525
525
|
}
|
|
526
|
-
|
|
527
|
-
return pathValue;
|
|
528
|
-
}
|
|
529
|
-
return `${this.baseUrl}${pathValue.startsWith('/') ? '' : '/'}${pathValue}`;
|
|
526
|
+
return pathValue;
|
|
530
527
|
}
|
|
531
528
|
|
|
532
529
|
async #fetchAllPages(path) {
|
|
@@ -537,7 +534,7 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
537
534
|
logger: console,
|
|
538
535
|
loggerPrefix: '[OCL]'
|
|
539
536
|
});
|
|
540
|
-
//
|
|
537
|
+
// Extra check: payload must be object or array
|
|
541
538
|
if (!result || (typeof result !== 'object' && !Array.isArray(result))) {
|
|
542
539
|
throw new Error('[OCL] Invalid response format: expected object or array');
|
|
543
540
|
}
|
|
@@ -1733,8 +1730,11 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1733
1730
|
}
|
|
1734
1731
|
|
|
1735
1732
|
#resourceKey() {
|
|
1733
|
+
const crypto = require('crypto');
|
|
1736
1734
|
const normalizedSystem = OCLSourceCodeSystemFactory.#normalizeSystem(this.system());
|
|
1737
|
-
|
|
1735
|
+
const base = `${normalizedSystem}|${this.version() || ''}`;
|
|
1736
|
+
const hash = crypto.createHash('sha256').update(base).digest('hex');
|
|
1737
|
+
return hash;
|
|
1738
1738
|
}
|
|
1739
1739
|
|
|
1740
1740
|
currentChecksum() {
|
|
@@ -22,15 +22,17 @@ class OCLBackgroundJobQueue {
|
|
|
22
22
|
const resolveAndEnqueue = async () => {
|
|
23
23
|
const resolvedSize = await this.#resolveJobSize(options);
|
|
24
24
|
const normalizedSize = this.#normalizeJobSize(resolvedSize);
|
|
25
|
-
|
|
25
|
+
const job = {
|
|
26
26
|
jobKey,
|
|
27
27
|
jobType: jobType || 'background-job',
|
|
28
28
|
jobId: options?.jobId || jobKey,
|
|
29
29
|
jobSize: normalizedSize,
|
|
30
30
|
getProgress: typeof options?.getProgress === 'function' ? options.getProgress : null,
|
|
31
31
|
runJob,
|
|
32
|
-
enqueueOrder: this.enqueueSequence
|
|
33
|
-
|
|
32
|
+
enqueueOrder: this.enqueueSequence++,
|
|
33
|
+
userRequested: !!options.userRequested
|
|
34
|
+
};
|
|
35
|
+
this.#insertPendingJobOrdered(job);
|
|
34
36
|
this.ensureHeartbeatRunning();
|
|
35
37
|
console.log(`[OCL] ${jobType || 'Background job'} enqueued: ${jobKey} (size=${normalizedSize}, queue=${this.pendingJobs.length}, active=${this.activeCount})`);
|
|
36
38
|
this.processNext();
|
|
@@ -72,17 +74,21 @@ class OCLBackgroundJobQueue {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
static #insertPendingJobOrdered(job) {
|
|
77
|
+
// Prioridade máxima para userRequested
|
|
78
|
+
if (job.userRequested) {
|
|
79
|
+
this.pendingJobs.unshift(job);
|
|
80
|
+
console.log(`[OCL] User-requested job prioritized: ${job.jobKey}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
75
83
|
let index = this.pendingJobs.findIndex(existing => {
|
|
76
84
|
if (existing.jobSize === job.jobSize) {
|
|
77
85
|
return existing.enqueueOrder > job.enqueueOrder;
|
|
78
86
|
}
|
|
79
87
|
return existing.jobSize > job.jobSize;
|
|
80
88
|
});
|
|
81
|
-
|
|
82
89
|
if (index < 0) {
|
|
83
90
|
index = this.pendingJobs.length;
|
|
84
91
|
}
|
|
85
|
-
|
|
86
92
|
this.pendingJobs.splice(index, 0, job);
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -178,6 +178,43 @@ function patchValueSetExpandWholeSystemForOcl() {
|
|
|
178
178
|
|
|
179
179
|
const originalIncludeCodes = proto.includeCodes;
|
|
180
180
|
proto.includeCodes = async function patchedIncludeCodes(cset, path, vsSrc, compose, filter, expansion, excludeInactive, notClosed) {
|
|
181
|
+
// ...existing code...
|
|
182
|
+
// PATCH: Para OCL ValueSet, só expandir códigos explicitamente listados em compose.include.code
|
|
183
|
+
if (Array.isArray(compose?.include)) {
|
|
184
|
+
const explicitCodes = [];
|
|
185
|
+
for (const include of compose.include) {
|
|
186
|
+
// Normaliza o system para URL canônico
|
|
187
|
+
const canonicalSystem = typeof include.system === 'string' ? include.system.trim() : include.system;
|
|
188
|
+
if (Array.isArray(include.code)) {
|
|
189
|
+
for (const code of include.code) {
|
|
190
|
+
explicitCodes.push({ system: canonicalSystem, code });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Também verifica se há conceitos explícitos em include.concept
|
|
194
|
+
if (Array.isArray(include.concept)) {
|
|
195
|
+
for (const concept of include.concept) {
|
|
196
|
+
if (concept && concept.code) {
|
|
197
|
+
explicitCodes.push({ system: canonicalSystem, code: concept.code });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (explicitCodes.length > 0) {
|
|
203
|
+
// Filtra expansão para só retornar os códigos explicitamente referenciados
|
|
204
|
+
const filtered = [];
|
|
205
|
+
for (const { system, code } of explicitCodes) {
|
|
206
|
+
// Busca conceito no CodeSystem
|
|
207
|
+
const resources = await originalIncludeCodes.call(this, { system, code }, path, vsSrc, compose, filter, expansion, excludeInactive, notClosed);
|
|
208
|
+
if (Array.isArray(resources)) {
|
|
209
|
+
filtered.push(...resources);
|
|
210
|
+
} else if (resources) {
|
|
211
|
+
filtered.push(resources);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return filtered;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Fallback para comportamento original
|
|
181
218
|
try {
|
|
182
219
|
return await originalIncludeCodes.call(this, cset, path, vsSrc, compose, filter, expansion, excludeInactive, notClosed);
|
|
183
220
|
} catch (error) {
|