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.
@@ -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', 'https://github.com/FHIR/ig-registry.git', registryDir],
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.local_folder }, task.id, 'Pulling latest web folder');
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.local_folder }, task.id, 'Staging web folder changes');
720
- await this.runCommand('git', ['commit', '-m', commitMsg], { cwd: website.local_folder }, task.id, 'Committing web folder changes');
721
- await this.runCommand('git', ['push'], { cwd: website.local_folder }, task.id, 'Pushing web folder changes');
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.history_templates + '</code></td>';
1645
- content += '<td><code>' + website.web_templates + '</code></td>';
1646
- content += '<td><code>' + website.server_update_script + '</code></td>';
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) || 'FHIRsmith Server', content, stats);
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) || 'FHIRsmith Server', content, stats);
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;
@@ -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: 'en-GB',
619
- 3: 'es',
620
- 4: 'fr',
621
- 5: 'de'
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
- return this.sct.searchFilter(filter, false, sort);
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
@@ -110,14 +110,18 @@ const Extensions = {
110
110
  if (!exp.extension) {
111
111
  exp.extension = [];
112
112
  }
113
- exp.extension.push({ url : url, valueBoolean : b });
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
- exp.extension.push({ url : url, valueString : s });
122
+ let ext = { url : url, valueString : s };
123
+ exp.extension.push(ext);
124
+ return ext;
121
125
  }
122
126
  }
123
127
 
@@ -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 filename = sanitizeFilename(canonicalUrl)
19
- + (version ? `_${sanitizeFilename(version)}` : '')
20
- + (paramsKey && paramsKey !== 'default' ? `_p_${sanitizeFilename(paramsKey)}` : '')
21
- + '.json';
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 direct = this.conceptMapMap.get(`${url}|${version}`) || this.conceptMapMap.get(url);
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
- const trimmed = system.trim();
24
+ let trimmed = system.trim();
25
25
  if (!trimmed) {
26
26
  return trimmed;
27
27
  }
28
28
 
29
- // Treat canonical URLs with and without trailing slash as equivalent.
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
- if (pathValue.startsWith('http://') || pathValue.startsWith('https://')) {
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
- // Verificação extra: payload deve ser objeto ou array
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
- return `${normalizedSystem}|${this.version() || ''}`;
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
- this.#insertPendingJobOrdered({
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) {