Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions tx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Add the `tx` section to your `config.json`:
"enabled": true,
"librarySource": "/path/to/library.yml",
"cacheTimeout": 30,
"internalLimit" : 10000,
"externalLimit" : 1000,
"expansionCacheSize": 1000,
"expansionCacheMemoryThreshold": 0,
"endpoints": [
Expand Down Expand Up @@ -57,14 +59,16 @@ Add the `tx` section to your `config.json`:

### Configuration Options

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `enabled` | boolean | Yes | Whether the module is enabled |
| `cacheTimeout` | integer | No | How many minutes to keep client side caches (for cache-id parameter). Default: 30 |
| `expansionCacheSize` | integer | No | Maximum number of expanded ValueSets to cache. Default: 1000 |
| `expansionCacheMemoryThreshold` | integer | No | Heap memory usage in MB that triggers evicting oldest half of expansion cache. 0 = disabled. Default: 0 |
| `librarySource` | string | Yes | Path to the YAML file that defines the terminology sources to load |
| `endpoints` | array | Yes | List of endpoint configurations (at least one required) |
| Option | Type | Required | Description |
|---------------------------------|---------|----------|---------------------------------------------------------------------------------------------------------|
| `enabled` | boolean | Yes | Whether the module is enabled |
| `cacheTimeout` | integer | No | How many minutes to keep client side caches (for cache-id parameter). Default: 30 |
| `expansionCacheSize` | integer | No | Maximum number of expanded ValueSets to cache. Default: 1000 |
| `expansionCacheMemoryThreshold` | integer | No | Heap memory usage in MB that triggers evicting oldest half of expansion cache. 0 = disabled. Default: 0 |
| `librarySource` | string | Yes | Path to the YAML file that defines the terminology sources to load |
| `internalLimit` | integer | No | Largest number of codes in internal expansions |
| `externalLimit` | integer | No | Largest number of codes the server will return in an expansion |
| `endpoints` | array | Yes | List of endpoint configurations (at least one required) |

### Endpoint Configuration

Expand Down
26 changes: 26 additions & 0 deletions tx/cs/cs-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,32 @@ class CodeSystemFactoryProvider {
return null;
}

/**
* If the data available to the provider includes the definition of some supplements,
* then the provider has to declare them to the server by overriding this method. The
* method returns a list of CodeSystem resources, with jsonObj provided. The jsonObj
* in this case must include the correct metadata, with content = supplement, but need
* not include any actual content (which might be anticipated to be large). If the
* server sees a CodeSystem supplement with no content that comes from a provider
* then the server will use fillOutSupplement to ask for the details to be populated
* if a client has done something that means the server needs it (mostly, it doesn't)
*
* @returns {CodeSystem[]}
*/
async registerSupplements() {
return [];
}

/**
* see comemnts for registerSupplements()
*
* @param {CodeSystem} supplement - the supplement to flesh out
* @returns void
*/
async fillOutSupplement(supplement) {
// nothing
}

/**
* build and return a known concept map from the URL, if there is one.
*
Expand Down
1 change: 0 additions & 1 deletion tx/cs/cs-country.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class CountryCodeServices extends CodeSystemProvider {
super(opContext, supplements);
this.codes = codes || [];
this.codeMap = codeMap || new Map();
this.supplements = [];
}

// Metadata methods
Expand Down
3 changes: 3 additions & 0 deletions tx/cs/cs-rxnorm.js
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,9 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
if (d.includes('_')) {
d = d.substring(d.lastIndexOf('_') + 1);
}
if (d.includes('-')) {
d = d.substring(0, d.lastIndexOf('-'));
}
if (/^\d+$/.test(d)) {
version = d;
}
Expand Down
1 change: 1 addition & 0 deletions tx/html/tx-template.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<a href="[%endpoint-path%]/metadata" style="color: gold">Capability Statement</a> &nbsp;|&nbsp;
<a href="[%endpoint-path%]/metadata?mode=terminology" style="color: gold">Terminology Capabilities</a> &nbsp;|&nbsp;
<a href="[%endpoint-path%]/op.html" style="color: gold">Operations</a> &nbsp;|&nbsp;
<a href="[%endpoint-path%]/problems.html" style="color: gold">Problems</a> &nbsp;|&nbsp;
&nbsp;
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions tx/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,7 @@ class Library {
cmp.close();
}
}

}

module.exports = { Library };
68 changes: 68 additions & 0 deletions tx/problems.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@

class ProblemFinder {

constructor() {
this.map = new Map();
}

async scanValueSets(provider) {
let unknownVersions = {}; // system -> Set of versions not known to the server
for (let vsp of provider.valueSetProviders) {
let list = await vsp.listAllValueSets();
for (let url of list) {
let vs = await vsp.fetchValueSet(url);
if (vs && vs.jsonObj.compose) {
await this.scanValueSet(vs.jsonObj.compose, unknownVersions, vs.jsonObj.status != 'retired');
}
}
}
// Filter to only versions the server doesn't know about
for (const [system, vset] of Object.entries(unknownVersions)) {
for (let v of [...vset]) {
if (await provider.hasCsVersion(system, v)) {
vset.delete(v);
}
}
if (vset.size === 0) {
delete unknownVersions[system];
}
}
return this.unknownVersionsHtml(unknownVersions);
}

unknownVersionsHtml(unknownVersions) {
const entries = Object.entries(unknownVersions || {});
if (entries.length === 0) {
return '<p>No unknown system versions found.</p>';
}
entries.sort((a, b) => a[0].localeCompare(b[0]));
let html = '<table class="grid"><thead><tr><th>System</th><th>Unknown Versions</th></tr></thead><tbody>';
for (const [system, vset] of entries) {
const versions = [...vset].sort((a, b) => a.localeCompare(b)).join('<br/>');
html += `<tr><td>${system}</td><td>${versions}</td></tr>`;
}
html += '</tbody></table>';
return html;
}

async scanValueSet(compose, versions, active) {
for (let inc of compose.include || []) {
if (inc.system) {
if (active && inc.version) {
this.seeVersion(versions, inc.system, inc.version);
}
}
}
}

seeVersion(versions, system, version) {
let set = versions[system];
if (set == null) {
set = new Set();
versions[system] = set;
}
set.add(version);
}
}

module.exports = ProblemFinder;
14 changes: 14 additions & 0 deletions tx/provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,20 @@ class Provider {
return null;
}

async hasCsVersion(system, version) {
for (let cs of this.codeSystems.values()) {
if (cs.url == system && cs.version == version) {
return true;
}
}
for (let cp of this.codeSystemFactories.values()) {
if (cp.system() == system && cp.version() == version) {
return true;
}
}
return false;
}

}

module.exports = { Provider };
1 change: 1 addition & 0 deletions tx/tx.fhir.org.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ sources:
- snomed:sct_nl_20240930.cache
- snomed:sct_uk_20230412.cache
- snomed:sct_us_20230301.cache
- snomed:sct_us_20240301.cache
- snomed:sct_us_20250901.cache
- snomed:sct_test_20250814.cache
- cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
Expand Down
35 changes: 30 additions & 5 deletions tx/tx.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const packageJson = require("../package.json");
// Import workers
const ReadWorker = require('./workers/read');
const SearchWorker = require('./workers/search');
const { ExpandWorker } = require('./workers/expand');
const { ExpandWorker, INTERNAL_DEFAULT_LIMIT, EXTERNAL_DEFAULT_LIMIT} = require('./workers/expand');
const { ValidateWorker } = require('./workers/validate');
const TranslateWorker = require('./workers/translate');
const LookupWorker = require('./workers/lookup');
Expand Down Expand Up @@ -49,6 +49,7 @@ const {convertResourceToR5} = require("./xversion/xv-resource");
const ClosureWorker = require("./workers/closure");
const {BundleXML} = require("./xml/bundle-xml");
const ConceptUsageTracker = require("./usage-tracker");
const ProblemFinder = require("./problems");
// const {writeFileSync} = require("fs");

class TXModule {
Expand Down Expand Up @@ -620,7 +621,7 @@ class TXModule {
router.get('/ValueSet/\\$expand', async (req, res) => {
const start = Date.now();
try {
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
await worker.handle(req, res, this.log);
} finally {
this.countRequest('$expand', Date.now() - start);
Expand All @@ -629,7 +630,7 @@ class TXModule {
router.post('/ValueSet/\\$expand', async (req, res) => {
const start = Date.now();
try {
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
await worker.handle(req, res, this.log);
} finally {
this.countRequest('$expand', Date.now() - start);
Expand Down Expand Up @@ -784,7 +785,7 @@ class TXModule {
router.get('/ValueSet/:id/\\$expand', async (req, res) => {
const start = Date.now();
try {
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
await worker.handleInstance(req, res, this.log);
} finally {
this.countRequest('$expand', Date.now() - start);
Expand All @@ -793,7 +794,7 @@ class TXModule {
router.post('/ValueSet/:id/\\$expand', async (req, res) => {
const start = Date.now();
try {
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
await worker.handleInstance(req, res, this.log);
} finally {
this.countRequest('$expand', Date.now() - start);
Expand Down Expand Up @@ -893,6 +894,20 @@ class TXModule {
}
});

router.get('/problems.html', async (req, res) => {
const start = Date.now();
try {
let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid);
const problemFinder = new ProblemFinder();
const content = await problemFinder.scanValueSets(req.txProvider);
const html = await txhtml.renderPage('Problems', '<h3>ValueSet dependencies on unknown CodeSystem/Versions</h3>'+content, req.txEndpoint, req.txStartTime);
res.setHeader('Content-Type', 'text/html');
res.send(html);
} finally {
this.countRequest('problems', Date.now() - start);
}
});

// Metadata / CapabilityStatement
router.get('/metadata', async (req, res) => {
const start = Date.now();
Expand Down Expand Up @@ -1083,6 +1098,16 @@ class TXModule {
}
}

internalLimit(req) {
let isTest = req.header("User-Agent") == 'Tools/Java';
if (this.config.internalLimit && !isTest) return this.config.internalLimit; else return INTERNAL_DEFAULT_LIMIT;
}

externalLimit(req) {
let isTest = req.header("User-Agent") == 'Tools/Java';
if (this.config.internalLimit && !isTest) return this.config.externalLimit; else return EXTERNAL_DEFAULT_LIMIT;
}

}

module.exports = TXModule;
14 changes: 13 additions & 1 deletion tx/usage-tracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ class ConceptUsageTracker {
return c;
}

async scanValueSet(compose) {
async scanValueSet(compose, versions, active) {
let ok = false;
for (let inc of compose.include || []) {
if (inc.system) {
if (active && inc.version) {
this.seeVersion(versions, inc.system, inc.version);
}
for (let c of inc.concept || []) {
if (c.code) {
ok = true;
Expand Down Expand Up @@ -53,6 +56,15 @@ class ConceptUsageTracker {
usages(system) {
return this.map.get(system) || null;
}

seeVersion(versions, system, version) {
let set = versions[system];
if (set == null) {
set = new Set();
versions[system] = set;
}
set.add(version);
}
}

module.exports = ConceptUsageTracker;
Loading
Loading