fhirsmith 0.7.2 → 0.7.4

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 CHANGED
@@ -5,6 +5,40 @@ All notable changes to the Health Intersections Node Server will be documented i
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [v0.7.4] - 2026-03-19
9
+
10
+ ### Changed
11
+
12
+ - XIG: show using resource package explicitly
13
+ - TX: Check conformance statement production at start up
14
+
15
+ ### Fixed
16
+ - TX: Load URI provider on tx.fhir.org
17
+ - TX: fix error getting SCT version for html format
18
+
19
+ ### Tx Conformance Statement
20
+
21
+ FHIRsmith passed all 1452 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1-SNAPSHOT, runner v6.9.0)
22
+
23
+ ## [v0.7.3] - 2026-03-19
24
+
25
+ ### Changed
26
+
27
+ - Show total memory on home page
28
+ - OCL improvements
29
+ - Publisher: Allow editing websites
30
+ - Publisher: separate out target folder and git folder
31
+ - Publisher: use trusted git repo for ig_registry
32
+ - Extend XIG for phinvads analysis
33
+
34
+ ### Fixed
35
+ - Don't exempt tx/data from npm project
36
+ - SNOMED CT fix: align getLanguageCode with mapLanguageCode byte mapping
37
+
38
+ ### Tx Conformance Statement
39
+
40
+ FHIRsmith passed all 1452 HL7 terminology service tests (modes tx.fhir.org+omop+general+snomed, tests v1.9.1-SNAPSHOT, runner v6.9.0)
41
+
8
42
  ## [v0.7.2] - 2026-03-16
9
43
 
10
44
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fhirsmith",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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": {
@@ -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
@@ -401,12 +401,14 @@ async function buildRootPageContent() {
401
401
  const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(2);
402
402
  const heapTotalMB = (memUsage.heapTotal / 1024 / 1024).toFixed(2);
403
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);
404
406
 
405
407
  content += '<table class="grid">';
406
408
  content += '<tr>';
407
- content += `<td><strong>Module Count:</strong> ${mc}</td>`;
408
409
  content += `<td><strong>Uptime:</strong> ${escape(uptimeStr)}</td>`;
409
410
  content += `<td><strong>Request Count:</strong> ${stats.requestCount}</td>`;
411
+ content += `<td><strong>Free Memory:</strong> ${freeMemMB} MB of ${totalMemMB} MB</td>`;
410
412
  content += '</tr>';
411
413
  content += '<tr>';
412
414
  content += `<td><strong>Heap Used:</strong> ${heapUsedMB} MB</td>`;
@@ -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
  }
@@ -1271,7 +1275,7 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
1271
1275
 
1272
1276
  id() {
1273
1277
  const match = this.version().match(/^http:\/\/snomed\.info\/sct\/(\d+)(?:\/version\/(\d{8}))?$/);
1274
- return "SCT-"+match[1]+"-"+match[2];
1278
+ return match && match[1] && match[2] ? "SCT-"+match[1]+"-"+match[2] : null;
1275
1279
  }
1276
1280
 
1277
1281
  describeVersion(version) {
package/tx/cs/cs-uri.js CHANGED
@@ -21,7 +21,7 @@ class UriServices extends CodeSystemProvider {
21
21
  }
22
22
 
23
23
  version() {
24
- return 'n/a';
24
+ return null;
25
25
  }
26
26
 
27
27
  description() {
@@ -182,7 +182,7 @@ class UriServicesFactory extends CodeSystemFactoryProvider {
182
182
  }
183
183
 
184
184
  defaultVersion() {
185
- return 'n/a';
185
+ return null;
186
186
  }
187
187
 
188
188
  system() {
@@ -190,7 +190,7 @@ class UriServicesFactory extends CodeSystemFactoryProvider {
190
190
  }
191
191
 
192
192
  version() {
193
- return 'n/a';
193
+ return null;
194
194
  }
195
195
 
196
196
  // eslint-disable-next-line no-unused-vars
package/tx/library.js CHANGED
@@ -34,6 +34,7 @@ const {VSACValueSetProvider} = require("./vs/vs-vsac");
34
34
  const { OCLCodeSystemProvider, OCLSourceCodeSystemFactory } = require('./ocl/cs-ocl');
35
35
  const { OCLValueSetProvider } = require('./ocl/vs-ocl');
36
36
  const { OCLConceptMapProvider } = require('./ocl/cm-ocl');
37
+ const {UriServicesFactory} = require("./cs/cs-uri");
37
38
 
38
39
  /**
39
40
  * This class holds all the loaded content ready for processing
@@ -454,6 +455,12 @@ class Library {
454
455
  this.registerProvider('internal', hgvs);
455
456
  break;
456
457
  }
458
+ case "urls" : {
459
+ const urls = new UriServicesFactory(this.i18n);
460
+ await urls.load();
461
+ this.registerProvider('internal', urls);
462
+ break;
463
+ }
457
464
  case "vsac" : {
458
465
  if (!this.vsacCfg || !this.vsacCfg.apiKey) {
459
466
  throw new Error("Unable to load VSAC provider unless vsacCfg is provided in the configuration");
@@ -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) {
package/tx/ocl/vs-ocl.cjs CHANGED
@@ -15,7 +15,7 @@ const { computeValueSetExpansionFingerprint } = require('./fingerprint/fingerpri
15
15
  const { ensureTxParametersHashIncludesFilter, patchValueSetExpandWholeSystemForOcl } = require('./shared/patches');
16
16
 
17
17
  ensureTxParametersHashIncludesFilter(TxParameters);
18
- patchValueSetExpandWholeSystemForOcl();
18
+ //patchValueSetExpandWholeSystemForOcl();
19
19
 
20
20
  function normalizeCanonicalSystem(system) {
21
21
  if (typeof system !== 'string') {
@@ -93,10 +93,42 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
93
93
  dependencyChecksums: cached.dependencyChecksums || {},
94
94
  createdAt: Number.isFinite(createdAt) ? createdAt : null
95
95
  });
96
-
96
+ // Instancia ValueSet para garantir jsonObj
97
+ // Reconstrói compose.include se não existir
98
+ let compose = cached.expansion?.compose;
99
+ if (!compose || !Array.isArray(compose.include)) {
100
+ // Reconstrói a partir dos sistemas/códigos em expansion.contains
101
+ const systemConcepts = new Map();
102
+ if (Array.isArray(cached.expansion?.contains)) {
103
+ for (const entry of cached.expansion.contains) {
104
+ if (!entry.system || !entry.code) continue;
105
+ if (!systemConcepts.has(entry.system)) {
106
+ systemConcepts.set(entry.system, []);
107
+ }
108
+ systemConcepts.get(entry.system).push({ code: entry.code });
109
+ }
110
+ }
111
+ compose = { include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({ system, concept: concepts })) };
112
+ }
113
+ const valueSetObj = new ValueSet({
114
+ resourceType: 'ValueSet',
115
+ url: cached.canonicalUrl,
116
+ version: cached.version || null,
117
+ expansion: cached.expansion,
118
+ compose,
119
+ id: cached.canonicalUrl // ou outro identificador se necessário
120
+ }, 'R5');
121
+ this.#applyCachedExpansion(valueSetObj, paramsKey);
122
+ // Indexa o ValueSet restaurado para torná-lo disponível via fetchValueSet
123
+ this.valueSetMap.set(valueSetObj.url, valueSetObj);
124
+ if (valueSetObj.version) {
125
+ this.valueSetMap.set(`${valueSetObj.url}|${valueSetObj.version}`, valueSetObj);
126
+ }
127
+ this.valueSetMap.set(valueSetObj.id, valueSetObj);
128
+ this._idMap.set(valueSetObj.id, valueSetObj);
97
129
  this.valueSetFingerprints.set(cacheKey, cached.fingerprint || null);
98
130
  loadedCount++;
99
- console.log(`[OCL-ValueSet] Loaded ValueSet from cold cache: ${cached.canonicalUrl}`);
131
+ console.log(`[OCL-ValueSet] Loaded ValueSet from cold cache into memory: ${cached.canonicalUrl}`);
100
132
  } catch (error) {
101
133
  console.error(`[OCL-ValueSet] Failed to load cold cache file ${file}:`, error.message);
102
134
  }
@@ -220,9 +252,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
220
252
  let key = `${url}|${version}`;
221
253
  if (this.valueSetMap.has(key)) {
222
254
  const vs = this.valueSetMap.get(key);
223
- await this.#ensureComposeIncludes(vs);
255
+ // await this.#ensureComposeIncludes(vs);
224
256
  this.#clearInlineExpansion(vs);
225
- this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset' });
257
+ console.log(`[OCL-ValueSet] fetchValueSet cache hit for ${url} (version: ${version || 'none'})`);
258
+ this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset', userRequested: true });
226
259
  return vs;
227
260
  }
228
261
 
@@ -232,7 +265,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
232
265
  key = `${url}|${majorMinor}`;
233
266
  if (this.valueSetMap.has(key)) {
234
267
  const vs = this.valueSetMap.get(key);
235
- await this.#ensureComposeIncludes(vs);
268
+ // await this.#ensureComposeIncludes(vs);
236
269
  this.#clearInlineExpansion(vs);
237
270
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-mm' });
238
271
  return vs;
@@ -242,7 +275,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
242
275
 
243
276
  if (this.valueSetMap.has(url)) {
244
277
  const vs = this.valueSetMap.get(url);
245
- await this.#ensureComposeIncludes(vs);
278
+ // await this.#ensureComposeIncludes(vs);
246
279
  this.#clearInlineExpansion(vs);
247
280
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-url' });
248
281
  return vs;
@@ -250,7 +283,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
250
283
 
251
284
  const resolved = await this.#resolveValueSetByCanonical(url, version);
252
285
  if (resolved) {
253
- await this.#ensureComposeIncludes(resolved);
286
+ // await this.#ensureComposeIncludes(resolved);
254
287
  this.#clearInlineExpansion(resolved);
255
288
  this.#scheduleBackgroundExpansion(resolved, { reason: 'fetch-valueset-resolved' });
256
289
  return resolved;
@@ -261,7 +294,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
261
294
  key = `${url}|${version}`;
262
295
  if (this.valueSetMap.has(key)) {
263
296
  const vs = this.valueSetMap.get(key);
264
- await this.#ensureComposeIncludes(vs);
297
+ // await this.#ensureComposeIncludes(vs);
265
298
  this.#clearInlineExpansion(vs);
266
299
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init' });
267
300
  return vs;
@@ -273,7 +306,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
273
306
  key = `${url}|${majorMinor}`;
274
307
  if (this.valueSetMap.has(key)) {
275
308
  const vs = this.valueSetMap.get(key);
276
- await this.#ensureComposeIncludes(vs);
309
+ // await this.#ensureComposeIncludes(vs);
277
310
  this.#clearInlineExpansion(vs);
278
311
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-mm' });
279
312
  return vs;
@@ -283,7 +316,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
283
316
 
284
317
  if (this.valueSetMap.has(url)) {
285
318
  const vs = this.valueSetMap.get(url);
286
- await this.#ensureComposeIncludes(vs);
319
+ // await this.#ensureComposeIncludes(vs);
287
320
  this.#clearInlineExpansion(vs);
288
321
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-url' });
289
322
  return vs;
@@ -295,7 +328,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
295
328
  async fetchValueSetById(id) {
296
329
  const local = this.#getLocalValueSetById(id);
297
330
  if (local) {
298
- await this.#ensureComposeIncludes(local);
331
+ // await this.#ensureComposeIncludes(local);
299
332
  this.#clearInlineExpansion(local);
300
333
  this.#scheduleBackgroundExpansion(local, { reason: 'fetch-valueset-by-id' });
301
334
  return local;
@@ -304,7 +337,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
304
337
  await this.initialize();
305
338
 
306
339
  const vs = this.#getLocalValueSetById(id);
307
- await this.#ensureComposeIncludes(vs);
340
+ // await this.#ensureComposeIncludes(vs);
308
341
  this.#clearInlineExpansion(vs);
309
342
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-by-id-init' });
310
343
  return vs;
@@ -369,17 +402,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
369
402
  || this._idMap.get(vs.id)
370
403
  || null;
371
404
 
372
- // Preserve hydrated cold-cache expansions on first index; invalidate only on replacement.
373
- if (existing && existing !== vs) {
374
- this.#invalidateExpansionCache(vs);
375
- }
376
-
377
- this.valueSetMap.set(vs.url, vs);
378
- if (vs.version) {
379
- this.valueSetMap.set(`${vs.url}|${vs.version}`, vs);
405
+ // indexa se não existe ou se for o mesmo objeto
406
+ if (!existing || existing === vs) {
407
+ this.valueSetMap.set(vs.url, vs);
408
+ if (vs.version) {
409
+ this.valueSetMap.set(`${vs.url}|${vs.version}`, vs);
410
+ }
411
+ this.valueSetMap.set(vs.id, vs);
412
+ this._idMap.set(vs.id, vs);
380
413
  }
381
- this.valueSetMap.set(vs.id, vs);
382
- this._idMap.set(vs.id, vs);
383
414
  }
384
415
 
385
416
  #toValueSet(collection) {
@@ -414,11 +445,11 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
414
445
  json.meta = { lastUpdated };
415
446
  }
416
447
 
417
- if (preferredSource) {
418
- json.compose = {
419
- include: [{ system: preferredSource }]
420
- };
421
- }
448
+ // if (preferredSource) {
449
+ // json.compose = {
450
+ // include: [{ system: preferredSource }]
451
+ // };
452
+ // }
422
453
 
423
454
  const conceptsUrl = this.#normalizePath(
424
455
  collection.concepts_url || collection.conceptsUrl || this.#buildCollectionConceptsPath(collection)
@@ -995,7 +1026,8 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
995
1026
  const meta = this.#getCollectionMeta(vs);
996
1027
  queuedJobSize = await this.#fetchConceptCountFromHeaders(meta?.conceptsUrl || null);
997
1028
  return queuedJobSize;
998
- }
1029
+ },
1030
+ userRequested: !!options.userRequested
999
1031
  }
1000
1032
  );
1001
1033
  }
@@ -1080,10 +1112,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1080
1112
  }
1081
1113
 
1082
1114
  const contains = [];
1083
- let offset = 0;
1084
-
1085
- // Pull all concepts in fixed-size pages until exhausted.
1086
- // eslint-disable-next-line no-constant-condition
1115
+ let offset = 0; // Moved this line up
1116
+ // Agrupa conceitos por system
1117
+ const systemConcepts = new Map();
1118
+ // Removed duplicate offset declaration
1087
1119
  while (true) {
1088
1120
  const batch = await this.#fetchCollectionConcepts(meta, {
1089
1121
  count: CONCEPT_PAGE_SIZE,
@@ -1092,17 +1124,14 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1092
1124
  filter: null,
1093
1125
  languageCodes: []
1094
1126
  });
1095
-
1096
1127
  const entries = Array.isArray(batch?.contains) ? batch.contains : [];
1097
1128
  if (entries.length === 0) {
1098
1129
  break;
1099
1130
  }
1100
-
1101
1131
  for (const entry of entries) {
1102
1132
  if (!entry?.system || !entry?.code) {
1103
1133
  continue;
1104
1134
  }
1105
-
1106
1135
  const out = {
1107
1136
  system: entry.system,
1108
1137
  code: entry.code
@@ -1125,18 +1154,28 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1125
1154
  }));
1126
1155
  }
1127
1156
  contains.push(out);
1157
+ // Agrupa por system
1158
+ if (!systemConcepts.has(entry.system)) {
1159
+ systemConcepts.set(entry.system, []);
1160
+ }
1161
+ systemConcepts.get(entry.system).push(entry.code);
1128
1162
  }
1129
-
1130
1163
  if (progressState) {
1131
1164
  progressState.processed = contains.length;
1132
1165
  }
1133
-
1134
1166
  if (entries.length < CONCEPT_PAGE_SIZE) {
1135
1167
  break;
1136
1168
  }
1137
1169
  offset += entries.length;
1138
1170
  }
1139
-
1171
+ // Popular compose.include para cada system
1172
+ if (!vs.jsonObj.compose) {
1173
+ vs.jsonObj.compose = { include: [] };
1174
+ }
1175
+ vs.jsonObj.compose.include = Array.from(systemConcepts.entries()).map(([system, codes]) => ({
1176
+ system,
1177
+ concept: codes.map(code => ({ code }))
1178
+ }));
1140
1179
  return {
1141
1180
  timestamp: new Date().toISOString(),
1142
1181
  identifier: `urn:uuid:${crypto.randomUUID()}`,
@@ -8,6 +8,7 @@ sources:
8
8
  - internal:areacode
9
9
  - internal:mimetypes
10
10
  - internal:usstates
11
+ - internal:urls
11
12
  - internal:hgvs
12
13
  - ucum:tx/data/ucum-essence.xml
13
14
  - loinc:loinc-2.77-a.db
package/tx/tx.js CHANGED
@@ -196,6 +196,9 @@ class TXModule {
196
196
  }
197
197
 
198
198
  this.log.info(`TX module initialized with ${config.endpoints.length} endpoint(s)`);
199
+
200
+ // Self-test: verify metadata generation works for each endpoint before accepting traffic
201
+ await this.selfTest();
199
202
  }
200
203
 
201
204
  /**
@@ -388,8 +391,8 @@ class TXModule {
388
391
  }
389
392
 
390
393
  if (contentType.includes('application/json') ||
391
- contentType.includes('application/fhir+json') ||
392
- contentType.includes('application/json+fhir')) {
394
+ contentType.includes('application/fhir+json') ||
395
+ contentType.includes('application/json+fhir')) {
393
396
 
394
397
  // If body is a Buffer, parse it
395
398
  if (Buffer.isBuffer(req.body)) {
@@ -731,11 +734,11 @@ class TXModule {
731
734
  router.get('/CodeSystem/:id/\\$validate-code', async (req, res) => {
732
735
  const start = Date.now();
733
736
  try {
734
- let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
737
+ let worker = new ValidateWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
735
738
  await worker.handleCodeSystemInstance(req, res, this.log);
736
- } finally {
737
- this.countRequest('$validate', Date.now() - start);
738
- }
739
+ } finally {
740
+ this.countRequest('$validate', Date.now() - start);
741
+ }
739
742
  });
740
743
  router.post('/CodeSystem/:id/\\$validate-code', async (req, res) => {
741
744
  const start = Date.now();
@@ -745,7 +748,7 @@ class TXModule {
745
748
  } finally {
746
749
  this.countRequest('$validate', Date.now() - start);
747
750
  }
748
-
751
+
749
752
  });
750
753
 
751
754
  // ValueSet/[id]/$validate-code
@@ -964,6 +967,79 @@ class TXModule {
964
967
  });
965
968
  }
966
969
 
970
+ /**
971
+ * Self-test: exercise CapabilityStatement and TerminologyCapabilities generation
972
+ * for each endpoint immediately after startup, throwing on any failure.
973
+ */
974
+ async selfTest() {
975
+ this.log.info('Running startup self-test for metadata endpoints...');
976
+
977
+ for (const endpointInfo of this.endpoints) {
978
+ const label = `${endpointInfo.path} (FHIR v${endpointInfo.fhirVersion})`;
979
+
980
+ // Build a minimal mock req/res that captures what metadataHandler.handle() produces
981
+ const makeMockReqRes = (mode) => {
982
+ const captured = { data: null, status: 200 };
983
+
984
+ const req = {
985
+ method: 'GET',
986
+ query: { mode },
987
+ headers: {},
988
+ // eslint-disable-next-line no-unused-vars
989
+ get: (name) => null,
990
+ txEndpoint: endpointInfo,
991
+ txProvider: endpointInfo.provider,
992
+ };
993
+
994
+ const res = {
995
+ statusCode: 200,
996
+ status(code) { captured.status = code; return this; },
997
+ setHeader() { return this; },
998
+ json(data) { captured.data = data; return this; },
999
+ send(data) { captured.data = data; return this; },
1000
+ };
1001
+
1002
+ return { req, res, captured };
1003
+ };
1004
+
1005
+ // Test 1: CapabilityStatement (/metadata with no mode, or mode=full)
1006
+ try {
1007
+ const { req, res, captured } = makeMockReqRes(undefined);
1008
+ await this.metadataHandler.handle(req, res);
1009
+ if (!captured.data) {
1010
+ throw new Error('No response data returned');
1011
+ }
1012
+ const rt = captured.data.resourceType;
1013
+ if (rt !== 'CapabilityStatement') {
1014
+ throw new Error(`Expected CapabilityStatement, got ${rt}`);
1015
+ }
1016
+ this.log.info(` [OK] CapabilityStatement for ${label}`);
1017
+ } catch (err) {
1018
+ this.log.error(` [FAIL] CapabilityStatement for ${label}: ${err.message}`);
1019
+ throw new Error(`Startup self-test failed (CapabilityStatement, ${label}): ${err.message}`);
1020
+ }
1021
+
1022
+ // Test 2: TerminologyCapabilities (/metadata?mode=terminology)
1023
+ try {
1024
+ const { req, res, captured } = makeMockReqRes('terminology');
1025
+ await this.metadataHandler.handle(req, res);
1026
+ if (!captured.data) {
1027
+ throw new Error('No response data returned');
1028
+ }
1029
+ const rt = captured.data.resourceType;
1030
+ if (rt !== 'TerminologyCapabilities') {
1031
+ throw new Error(`Expected TerminologyCapabilities, got ${rt}`);
1032
+ }
1033
+ this.log.info(` [OK] TerminologyCapabilities for ${label}`);
1034
+ } catch (err) {
1035
+ this.log.error(` [FAIL] TerminologyCapabilities for ${label}: ${err.message}`);
1036
+ throw new Error(`Startup self-test failed (TerminologyCapabilities, ${label}): ${err.message}`);
1037
+ }
1038
+ }
1039
+
1040
+ this.log.info('Startup self-test passed.');
1041
+ }
1042
+
967
1043
  /**
968
1044
  * Build an OperationOutcome for errors
969
1045
  */
@@ -1077,21 +1153,21 @@ class TXModule {
1077
1153
  ec = 0;
1078
1154
 
1079
1155
  checkProperJson() { // jsonStr) {
1080
- // const errors = [];
1081
- // if (jsonStr.includes("[]")) errors.push("Found [] in json");
1082
- // if (jsonStr.includes('""')) errors.push('Found "" in json');
1083
- //
1084
- // if (errors.length > 0) {
1085
- // this.ec++;
1086
- // const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
1087
- // writeFileSync(filename, jsonStr);
1088
- // throw new Error(errors.join('; '));
1089
- // }
1156
+ // const errors = [];
1157
+ // if (jsonStr.includes("[]")) errors.push("Found [] in json");
1158
+ // if (jsonStr.includes('""')) errors.push('Found "" in json');
1159
+ //
1160
+ // if (errors.length > 0) {
1161
+ // this.ec++;
1162
+ // const filename = `/Users/grahamegrieve/temp/tx-err-log/err${this.ec}.json`;
1163
+ // writeFileSync(filename, jsonStr);
1164
+ // throw new Error(errors.join('; '));
1165
+ // }
1090
1166
  }
1091
1167
 
1092
1168
  transformResourceForVersion(data, fhirVersion) {
1093
1169
  if (fhirVersion == "5.0" || !data.resourceType) {
1094
- return data;
1170
+ return data;
1095
1171
  }
1096
1172
  switch (data.resourceType) {
1097
1173
  case "CodeSystem": return codeSystemFromR5(data, fhirVersion);
package/xig/xig.js CHANGED
@@ -99,14 +99,15 @@ function validateExternalUrl(url) {
99
99
 
100
100
  // Secure SQL query building with parameterized queries
101
101
  function buildSecureResourceQuery(queryParams, offset = 0, limit = 50) {
102
- const { realm, auth, ver, type, rt, text } = queryParams;
102
+ const { realm, auth, ver, type, rt, text, pkg, onlyUsed } = queryParams;
103
103
 
104
104
  let baseQuery = `
105
105
  SELECT
106
106
  ResourceKey, ResourceType, Type, Kind, Description, PackageKey,
107
107
  Realm, Authority, R2, R2B, R3, R4, R4B, R5, R6,
108
108
  Id, Url, Version, Status, Date, Name, Title, Content,
109
- Supplements, Details, FMM, WG, StandardsStatus, Web
109
+ Supplements, Details, FMM, WG, StandardsStatus, Web,
110
+ (SELECT COUNT(*) FROM DependencyList WHERE TargetKey = Resources.ResourceKey) AS UsageCount
110
111
  FROM Resources
111
112
  WHERE 1=1
112
113
  `;
@@ -225,6 +226,17 @@ function buildSecureResourceQuery(queryParams, offset = 0, limit = 50) {
225
226
  }
226
227
  }
227
228
 
229
+ // Package filter - matches packageId#version containing the search string
230
+ if (pkg && pkg !== '') {
231
+ conditions.push('AND PackageKey IN (SELECT PackageKey FROM Packages WHERE (Id || \'#\' || Version) LIKE ?)');
232
+ params.push(`%${pkg}%`);
233
+ }
234
+
235
+ // Only used filter
236
+ if (onlyUsed === 'true') {
237
+ conditions.push('AND EXISTS (SELECT 1 FROM DependencyList WHERE TargetKey = Resources.ResourceKey)');
238
+ }
239
+
228
240
  // Build final query
229
241
  const fullQuery = baseQuery + ' ' + conditions.join(' ') + ' ORDER BY ResourceType, Type, Description LIMIT ? OFFSET ?';
230
242
  params.push(limit, offset);
@@ -233,7 +245,7 @@ function buildSecureResourceQuery(queryParams, offset = 0, limit = 50) {
233
245
  }
234
246
 
235
247
  function buildSecureResourceCountQuery(queryParams) {
236
- const { realm, auth, ver, type, rt, text } = queryParams;
248
+ const { realm, auth, ver, type, rt, text, pkg, onlyUsed } = queryParams;
237
249
 
238
250
  let baseQuery = 'SELECT COUNT(*) as total FROM Resources WHERE 1=1';
239
251
  const conditions = [];
@@ -324,6 +336,17 @@ function buildSecureResourceCountQuery(queryParams) {
324
336
  }
325
337
  }
326
338
 
339
+ // Package filter - matches packageId#version containing the search string
340
+ if (pkg && pkg !== '') {
341
+ conditions.push('AND PackageKey IN (SELECT PackageKey FROM Packages WHERE (Id || \'#\' || Version) LIKE ?)');
342
+ params.push(`%${pkg}%`);
343
+ }
344
+
345
+ // Only used filter
346
+ if (onlyUsed === 'true') {
347
+ conditions.push('AND EXISTS (SELECT 1 FROM DependencyList WHERE TargetKey = Resources.ResourceKey)');
348
+ }
349
+
327
350
  const fullQuery = baseQuery + ' ' + conditions.join(' ');
328
351
  return { query: fullQuery, params };
329
352
  }
@@ -449,7 +472,7 @@ function sqlEscapeString(str) {
449
472
  }
450
473
 
451
474
  function buildSqlFilter(queryParams) {
452
- const { realm, auth, ver, type, rt, text } = queryParams;
475
+ const { realm, auth, ver, type, rt, text, pkg, onlyUsed } = queryParams;
453
476
  let filter = '';
454
477
 
455
478
  // Realm filter
@@ -555,6 +578,17 @@ function buildSqlFilter(queryParams) {
555
578
  }
556
579
  }
557
580
 
581
+ // Package filter - matches packageId#version containing the search string
582
+ if (pkg && pkg !== '') {
583
+ const escapedPkg = sqlEscapeString(pkg);
584
+ filter += ` and PackageKey in (select PackageKey from Packages where (Id || '#' || Version) like '%${escapedPkg}%')`;
585
+ }
586
+
587
+ // Only used filter
588
+ if (onlyUsed === 'true') {
589
+ filter += ` and exists (select 1 from DependencyList where TargetKey = Resources.ResourceKey)`;
590
+ }
591
+
558
592
  // Convert to proper WHERE clause
559
593
  if (filter !== '') {
560
594
  // Remove the first " and " and prepend "WHERE "
@@ -600,7 +634,8 @@ function buildResourceListQuery(queryParams, offset = 0, limit = 50) {
600
634
  FMM,
601
635
  WG,
602
636
  StandardsStatus,
603
- Web
637
+ Web,
638
+ (SELECT COUNT(*) FROM DependencyList WHERE TargetKey = Resources.ResourceKey) AS UsageCount
604
639
  FROM Resources
605
640
  ${whereClause}
606
641
  ORDER BY ResourceType, Type, Description
@@ -784,6 +819,7 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
784
819
  break;
785
820
  }
786
821
 
822
+ parts.push('<th>Usage #</th>');
787
823
  parts.push('</tr>');
788
824
 
789
825
  const resourceRows = await new Promise((resolve, reject) => {
@@ -912,6 +948,10 @@ async function buildResourceTable(queryParams, resourceCount, offset = 0) {
912
948
  }
913
949
  }
914
950
 
951
+ // Usage count column
952
+ const usageCount = row.UsageCount || 0;
953
+ parts.push(`<td>${usageCount > 0 ? usageCount.toLocaleString() : ''}</td>`);
954
+
915
955
  parts.push('</tr>');
916
956
  }
917
957
 
@@ -1116,7 +1156,7 @@ function makeSelect(selectedValue, optionsList, name = 'rt') {
1116
1156
  }
1117
1157
 
1118
1158
  function buildAdditionalForm(queryParams) {
1119
- const { ver, realm, auth, type, rt, text } = queryParams;
1159
+ const { ver, realm, auth, type, rt, text, pkg, onlyUsed } = queryParams;
1120
1160
 
1121
1161
  let html = '<form method="GET" action="" style="background-color: #eeeeee; border: 1px black solid; padding: 6px; font-size: 12px; font-family: verdana;">';
1122
1162
 
@@ -1142,6 +1182,7 @@ function buildAdditionalForm(queryParams) {
1142
1182
  const profileResources = getCachedSet('profileResources');
1143
1183
  if (profileResources.length > 0) {
1144
1184
  html += 'Type: ' + makeSelect(rt, profileResources) + ' ';
1185
+ html += '<br/>';
1145
1186
  }
1146
1187
  break;
1147
1188
  }
@@ -1150,6 +1191,7 @@ function buildAdditionalForm(queryParams) {
1150
1191
  const profileTypes = getCachedSet('profileTypes');
1151
1192
  if (profileTypes.length > 0) {
1152
1193
  html += 'Type: ' + makeSelect(rt, profileTypes) + ' ';
1194
+ html += '<br/>';
1153
1195
  }
1154
1196
  break;
1155
1197
  }
@@ -1162,6 +1204,7 @@ function buildAdditionalForm(queryParams) {
1162
1204
  const extensionContexts = getCachedSet('extensionContexts');
1163
1205
  if (extensionContexts.length > 0) {
1164
1206
  html += 'Context: ' + makeSelect(rt, extensionContexts) + ' ';
1207
+ html += '<br/>';
1165
1208
  }
1166
1209
  break;
1167
1210
  }
@@ -1172,6 +1215,7 @@ function buildAdditionalForm(queryParams) {
1172
1215
  // Convert txSources map to "code=display" format
1173
1216
  const sourceOptions = Object.keys(txSources).map(code => `${code}=${txSources[code]}`);
1174
1217
  html += 'Source: ' + makeSelect(rt, sourceOptions) + ' ';
1218
+ html += '<br/>';
1175
1219
  }
1176
1220
  break;
1177
1221
  }
@@ -1182,6 +1226,7 @@ function buildAdditionalForm(queryParams) {
1182
1226
  // Convert txSources map to "code=display" format
1183
1227
  const sourceOptionsCM = Object.keys(txSourcesCM).map(code => `${code}=${txSourcesCM[code]}`);
1184
1228
  html += 'Source: ' + makeSelect(rt, sourceOptionsCM) + ' ';
1229
+ html += '<br/>';
1185
1230
  }
1186
1231
  break;
1187
1232
  }
@@ -1190,15 +1235,20 @@ function buildAdditionalForm(queryParams) {
1190
1235
  const resourceTypes = getCachedSet('resourceTypes');
1191
1236
  if (resourceTypes.length > 0) {
1192
1237
  html += 'Type: ' + makeSelect(rt, resourceTypes);
1238
+ html += '<br/>';
1193
1239
  }
1194
1240
  break;
1195
1241
  }
1196
1242
  }
1197
1243
 
1198
- // Add text search field
1244
+
1245
+ // Add text search field and package filter field
1199
1246
  html += `Text: <input type="text" name="text" value="${escape(text || '')}" class="" style="width: 200px;"/> `;
1247
+ html += `Package: <input type="text" name="pkg" value="${escape(pkg || '')}" placeholder="e.g. hl7.fhir.us" class="" style="width: 200px;"/> `;
1200
1248
 
1201
- // Add submit button
1249
+ // Add submit button with 'only used' checkbox immediately before it
1250
+ const onlyUsedChecked = onlyUsed === 'true' ? ' checked' : '';
1251
+ html += `<input type="checkbox" name="onlyUsed" value="true"${onlyUsedChecked}/> Only Show Used `;
1202
1252
  html += '<input type="submit" value="Search" style="color:rgb(89, 137, 241)"/>';
1203
1253
 
1204
1254
  html += '</form>';
@@ -2252,6 +2302,8 @@ router.get('/', async (req, res) => {
2252
2302
  type: req.query.type || '',
2253
2303
  rt: req.query.rt || '',
2254
2304
  text: req.query.text || '',
2305
+ pkg: req.query.pkg || '',
2306
+ onlyUsed: req.query.onlyUsed || '',
2255
2307
  offset: req.query.offset || '0'
2256
2308
  };
2257
2309
 
@@ -2696,11 +2748,14 @@ function buildDependencyTable(dependencies) {
2696
2748
  }
2697
2749
  currentType = dep.ResourceType;
2698
2750
  html += '<table class="table table-bordered">';
2699
- html += `<tr style="background-color: #eeeeee"><td colspan="2"><strong>${escape(currentType)}</strong></td></tr>`;
2751
+ html += `<tr style="background-color: #eeeeee"><td colspan="3"><strong>${escape(currentType)}</strong></td></tr>`;
2700
2752
  }
2701
2753
 
2702
2754
  html += '<tr>';
2703
2755
 
2756
+ // Package column
2757
+ html += `<td>${escape(dep.PID || '')}</td>`;
2758
+
2704
2759
  // Build the link to the resource detail page
2705
2760
  const packagePid = dep.PID.replace(/#/g, '|'); // Convert # to | for URL
2706
2761
  const resourceUrl = `/xig/resource/${encodeURIComponent(packagePid)}/${encodeURIComponent(dep.ResourceType)}/${encodeURIComponent(dep.Id)}`;