fhirsmith 0.5.2 → 0.5.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 +27 -0
- package/README.md +1 -1
- package/library/package-manager.js +1 -1
- package/package.json +1 -1
- package/server.js +13 -0
- package/stats.js +21 -7
- package/tx/README.md +12 -8
- package/tx/cs/cs-api.js +87 -8
- package/tx/cs/cs-areacode.js +0 -9
- package/tx/cs/cs-country.js +0 -10
- package/tx/cs/cs-cpt.js +1 -6
- package/tx/cs/cs-cs.js +1 -2
- package/tx/cs/cs-currency.js +0 -9
- package/tx/cs/cs-hgvs.js +0 -5
- package/tx/cs/cs-lang.js +0 -10
- package/tx/cs/cs-loinc.js +0 -7
- package/tx/cs/cs-omop.js +0 -6
- package/tx/cs/cs-rxnorm.js +3 -0
- package/tx/cs/cs-snomed.js +10 -4
- package/tx/cs/cs-ucum.js +0 -9
- package/tx/html/search-form.liquid +5 -2
- package/tx/html/tx-template.html +1 -0
- package/tx/library/designations.js +5 -1
- package/tx/library/renderer.js +6 -1
- package/tx/library.js +1 -0
- package/tx/operation-context.js +1 -0
- package/tx/params.js +7 -1
- package/tx/problems.js +90 -0
- package/tx/provider.js +22 -1
- package/tx/tx-html.js +130 -17
- package/tx/tx.fhir.org.yml +1 -0
- package/tx/tx.js +41 -5
- package/tx/usage-tracker.js +70 -0
- package/tx/vs/vs-api.js +3 -0
- package/tx/vs/vs-database.js +15 -7
- package/tx/vs/vs-package.js +6 -1
- package/tx/vs/vs-vsac.js +30 -14
- package/tx/workers/expand.js +235 -138
- package/tx/workers/search.js +12 -6
- package/tx/workers/validate.js +4 -1
- package/tx/workers/worker.js +14 -0
- package/tx/xversion/xv-parameters.js +8 -1
- package/tx/xversion/xv-resource.js +16 -16
- package/tx/cs/cs-db.js +0 -1308
package/tx/params.js
CHANGED
|
@@ -36,6 +36,7 @@ class TxParameters {
|
|
|
36
36
|
validating = false;
|
|
37
37
|
abstractOk = true; // note true!
|
|
38
38
|
inferSystem = false;
|
|
39
|
+
sort = 'design';
|
|
39
40
|
|
|
40
41
|
constructor(languages, i18n, validating) {
|
|
41
42
|
validateParameter(languages, 'languages', LanguageDefinitions);
|
|
@@ -242,6 +243,10 @@ class TxParameters {
|
|
|
242
243
|
if (getValuePrimitive(p) == true) this.inferSystem = true;
|
|
243
244
|
break;
|
|
244
245
|
}
|
|
246
|
+
case 'sort': {
|
|
247
|
+
this.sort = getValuePrimitive(p);
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
245
250
|
case "exclude-system": {
|
|
246
251
|
throw new Issue('error', 'not-supported', null, null, "The parameter 'exclude-system' is not supported by this system", null, 400);
|
|
247
252
|
}
|
|
@@ -524,7 +529,7 @@ class TxParameters {
|
|
|
524
529
|
this.FUid + '|' + b(this.FMembershipOnly) + '|' + this.FProperties.join(',') + '|' +
|
|
525
530
|
b(this.FActiveOnly) + b(this.FDisplayWarning) + b(this.FExcludeNested) + b(this.FGenerateNarrative) + b(this.FExcludeNotForUI) + b(this.FExcludePostCoordinated) +
|
|
526
531
|
b(this.FIncludeDesignations) + b(this.FIncludeDefinition) + b(this.hasActiveOnly) + b(this.hasExcludeNested) + b(this.hasGenerateNarrative) +
|
|
527
|
-
b(this.hasExcludeNotForUI) + b(this.hasExcludePostCoordinated) + b(this.hasIncludeDesignations) +
|
|
532
|
+
b(this.hasExcludeNotForUI) + b(this.hasExcludePostCoordinated) + b(this.hasIncludeDesignations) + this.sort+'|'+
|
|
528
533
|
b(this.hasIncludeDefinition) + b(this.hasDefaultToLatestVersion) + b(this.hasDisplayWarning) + b(this.hasExcludeNotForUI) + b(this.hasMembershipOnly) + b(this.FDefaultToLatestVersion);
|
|
529
534
|
|
|
530
535
|
if (this.hasHTTPLanguages) {
|
|
@@ -585,6 +590,7 @@ class TxParameters {
|
|
|
585
590
|
this.hasDefaultToLatestVersion = other.hasDefaultToLatestVersion;
|
|
586
591
|
this.hasMembershipOnly = other.hasMembershipOnly;
|
|
587
592
|
this.hasDisplayWarning = other.hasDisplayWarning;
|
|
593
|
+
this.sort = other.sort;
|
|
588
594
|
|
|
589
595
|
if (other.FProperties) {
|
|
590
596
|
this.FProperties = [...other.FProperties];
|
package/tx/problems.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const escape = require('escape-html');
|
|
2
|
+
|
|
3
|
+
class ProblemFinder {
|
|
4
|
+
|
|
5
|
+
constructor() {
|
|
6
|
+
this.map = new Map();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async scanValueSets(provider) {
|
|
10
|
+
let unknownVersions = {}; // system -> Set of versions not known to the server
|
|
11
|
+
for (let vsp of provider.valueSetProviders) {
|
|
12
|
+
let sourceUnknownVersions = unknownVersions[vsp.sourcePackage()];
|
|
13
|
+
if (!sourceUnknownVersions) {
|
|
14
|
+
sourceUnknownVersions = {};
|
|
15
|
+
unknownVersions[vsp.sourcePackage()] = sourceUnknownVersions;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let list = await vsp.listAllValueSets();
|
|
19
|
+
for (let url of list) {
|
|
20
|
+
let vs = await vsp.fetchValueSet(url);
|
|
21
|
+
if (vs && vs.jsonObj.compose) {
|
|
22
|
+
await this.scanValueSet(vs.jsonObj.compose, sourceUnknownVersions);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let result = '';
|
|
27
|
+
for (let sp of provider.listValueSetSourceCodes()) {
|
|
28
|
+
let sourceUnknownVersions = unknownVersions[sp];
|
|
29
|
+
if (sourceUnknownVersions) {
|
|
30
|
+
// Filter to only versions the server doesn't know about
|
|
31
|
+
for (const [system, vset] of Object.entries(sourceUnknownVersions)) {
|
|
32
|
+
for (let v of [...vset]) {
|
|
33
|
+
if (await provider.hasCsVersion(system, v)) {
|
|
34
|
+
vset.delete(v);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (vset.size === 0) {
|
|
38
|
+
delete sourceUnknownVersions[system];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let list = await this.unknownVersionsHtml(sourceUnknownVersions, provider, sp);
|
|
42
|
+
if (list) {
|
|
43
|
+
result = result + `<h4>${sp}</h4>` + list;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async unknownVersionsHtml(unknownVersions, provider, source) {
|
|
51
|
+
const entries = Object.entries(unknownVersions || {});
|
|
52
|
+
if (entries.length === 0) {
|
|
53
|
+
return '<p>No unknown system versions found.</p>';
|
|
54
|
+
}
|
|
55
|
+
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
56
|
+
let html = '<table class="grid"><thead><tr><th>System</th><th>Unknown Versions</th><th>Known Versions</th></tr></thead><tbody>';
|
|
57
|
+
for (const [system, vset] of entries) {
|
|
58
|
+
const systemEsc = escape(system);
|
|
59
|
+
const versions = [...vset].sort((a, b) => a.localeCompare(b));
|
|
60
|
+
const versionRefs = [];
|
|
61
|
+
for (const v of versions) {
|
|
62
|
+
versionRefs.push(`<a href="ValueSet?system=${systemEsc}|${escape(v)}&source=${source}&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate">${v}</a>`);
|
|
63
|
+
}
|
|
64
|
+
const knownVersions = [...await provider.listCodeSystemVersions(system)].join('<br/>');
|
|
65
|
+
html += `<tr><td><a href="CodeSystem?url=${systemEsc}&_elements=url%2Cversion%2Cname%2Ctitle%2Cstatus%2Ccontent%2Cdate">${system}</a></td>`+
|
|
66
|
+
`<td>${versionRefs.join('<br/>')}</td><td>${knownVersions}</td></tr>`;
|
|
67
|
+
}
|
|
68
|
+
html += '</tbody></table>';
|
|
69
|
+
return html;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async scanValueSet(compose, versions) {
|
|
73
|
+
for (let inc of compose.include || []) {
|
|
74
|
+
if (inc.system && inc.version) {
|
|
75
|
+
this.seeVersion(versions, inc.system, inc.version);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
seeVersion(versions, system, version) {
|
|
81
|
+
let set = versions[system];
|
|
82
|
+
if (set == null) {
|
|
83
|
+
set = new Set();
|
|
84
|
+
versions[system] = set;
|
|
85
|
+
}
|
|
86
|
+
set.add(version);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = ProblemFinder;
|
package/tx/provider.js
CHANGED
|
@@ -218,9 +218,16 @@ class Provider {
|
|
|
218
218
|
await csp.findImplicitConceptMap(url, version);
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
|
-
|
|
222
221
|
}
|
|
223
222
|
|
|
223
|
+
listValueSetSourceCodes() {
|
|
224
|
+
let result = [];
|
|
225
|
+
for (let vsp of this.valueSetProviders) {
|
|
226
|
+
result.push(vsp.sourcePackage());
|
|
227
|
+
}
|
|
228
|
+
result.sort((a, b) => {a.localeCompare(b)});
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
224
231
|
|
|
225
232
|
async listCodeSystemVersions(url) {
|
|
226
233
|
let result = new Set();
|
|
@@ -399,6 +406,20 @@ class Provider {
|
|
|
399
406
|
return null;
|
|
400
407
|
}
|
|
401
408
|
|
|
409
|
+
async hasCsVersion(system, version) {
|
|
410
|
+
for (let cs of this.codeSystems.values()) {
|
|
411
|
+
if (cs.url == system && cs.version == version) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (let cp of this.codeSystemFactories.values()) {
|
|
416
|
+
if (cp.system() == system && cp.version() == version) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
402
423
|
}
|
|
403
424
|
|
|
404
425
|
module.exports = { Provider };
|
package/tx/tx-html.js
CHANGED
|
@@ -137,7 +137,7 @@ class TxHtmlRenderer {
|
|
|
137
137
|
|
|
138
138
|
// eslint-disable-next-line no-unused-vars
|
|
139
139
|
async buildSearchForm(req, mode, params) {
|
|
140
|
-
const html = await this.liquid.renderFile('search-form', { baseUrl: escape(req.baseUrl) });
|
|
140
|
+
const html = await this.liquid.renderFile('search-form', { baseUrl: escape(req.baseUrl), sourceOptions : this.buildSourceOptions(req.txProvider) });
|
|
141
141
|
return html;
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -800,14 +800,128 @@ class TxHtmlRenderer {
|
|
|
800
800
|
return firstEntry?.resourceType || 'Resource';
|
|
801
801
|
}
|
|
802
802
|
|
|
803
|
+
/**
|
|
804
|
+
* Build a human-readable description of what this search bundle represents,
|
|
805
|
+
* by parsing the self link URL parameters.
|
|
806
|
+
*/
|
|
807
|
+
describeSearchBundle(json) {
|
|
808
|
+
const selfLink = json.link?.find(l => l.relation === 'self')?.url || '';
|
|
809
|
+
if (!selfLink) return '';
|
|
810
|
+
|
|
811
|
+
let urlObj;
|
|
812
|
+
try {
|
|
813
|
+
urlObj = new URL(selfLink);
|
|
814
|
+
} catch {
|
|
815
|
+
return '';
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Extract resource type from path
|
|
819
|
+
const typeMatch = selfLink.match(/\/(CodeSystem|ValueSet|ConceptMap)\b/);
|
|
820
|
+
const resourceType = typeMatch ? typeMatch[1] : 'Resource';
|
|
821
|
+
|
|
822
|
+
// Human-friendly labels for search params
|
|
823
|
+
const PARAM_LABELS = {
|
|
824
|
+
'url': 'URL',
|
|
825
|
+
'version': 'Version',
|
|
826
|
+
'name': 'Name',
|
|
827
|
+
'title': 'Title',
|
|
828
|
+
'status': 'Status',
|
|
829
|
+
'publisher': 'Publisher',
|
|
830
|
+
'description': 'Description',
|
|
831
|
+
'identifier': 'Identifier',
|
|
832
|
+
'jurisdiction': 'Jurisdiction',
|
|
833
|
+
'date': 'Date',
|
|
834
|
+
'text': 'Text',
|
|
835
|
+
'system': 'System',
|
|
836
|
+
'supplements': 'Supplements',
|
|
837
|
+
'content-mode': 'Content mode',
|
|
838
|
+
'source': 'Source'
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
const WORDS = {
|
|
842
|
+
'url': 'contains',
|
|
843
|
+
'version': 'contains',
|
|
844
|
+
'name': 'contains',
|
|
845
|
+
'title': 'contains',
|
|
846
|
+
'status': 'is',
|
|
847
|
+
'publisher': 'contains',
|
|
848
|
+
'description': 'contains',
|
|
849
|
+
'identifier': 'matches',
|
|
850
|
+
'jurisdiction': 'contains',
|
|
851
|
+
'date': 'matches',
|
|
852
|
+
'text': 'contains',
|
|
853
|
+
'system': 'matches',
|
|
854
|
+
'supplements': 'matches',
|
|
855
|
+
'content-mode': 'is',
|
|
856
|
+
'source': 'is'
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
const CONTROL_PARAMS = new Set(['_offset', '_count', '_sort', '_summary', '_elements', '_total']);
|
|
860
|
+
|
|
861
|
+
// Collect filter criteria
|
|
862
|
+
const criteria = [];
|
|
863
|
+
for (const [key, value] of urlObj.searchParams) {
|
|
864
|
+
if (key != 'mode') {
|
|
865
|
+
if (CONTROL_PARAMS.has(key) || !value) continue;
|
|
866
|
+
const label = PARAM_LABELS[key] || key;
|
|
867
|
+
const word = WORDS[key] || "contains";
|
|
868
|
+
criteria.push(`<strong>${escape(label)}</strong> ${word} “${escape(value)}”`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Collect display/pagination context
|
|
873
|
+
const total = json.total;
|
|
874
|
+
const offset = parseInt(urlObj.searchParams.get('_offset') || '0');
|
|
875
|
+
const count = parseInt(urlObj.searchParams.get('_count') || '20');
|
|
876
|
+
const sort = urlObj.searchParams.get('_sort');
|
|
877
|
+
const summary = urlObj.searchParams.get('_summary');
|
|
878
|
+
const elementsParam = urlObj.searchParams.get('_elements');
|
|
879
|
+
|
|
880
|
+
// Build the description sentence
|
|
881
|
+
let desc = `Searching <strong>${escape(resourceType)}s</strong>`;
|
|
882
|
+
|
|
883
|
+
if (criteria.length > 0) {
|
|
884
|
+
desc += ' where ' + criteria.join(', ');
|
|
885
|
+
} else {
|
|
886
|
+
desc += ' (all)';
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Pagination context
|
|
890
|
+
if (typeof total === 'number') {
|
|
891
|
+
const from = Math.min(offset + 1, total);
|
|
892
|
+
const to = Math.min(offset + count, total);
|
|
893
|
+
if (total === 0) {
|
|
894
|
+
desc += ' — <strong>no results found</strong>';
|
|
895
|
+
} else {
|
|
896
|
+
desc += ` — showing <strong>${from}–${to}</strong> of <strong>${total}</strong>`;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Sort
|
|
901
|
+
if (sort) {
|
|
902
|
+
desc += `, sorted by <strong>${escape(sort)}</strong>`;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Summary mode
|
|
906
|
+
if (summary && summary !== 'false') {
|
|
907
|
+
desc += ` [summary: ${escape(summary)}]`;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Elements
|
|
911
|
+
if (elementsParam) {
|
|
912
|
+
desc += ` [fields: ${escape(elementsParam)}]`;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return `<p class="search-description">${desc}</p>`;
|
|
916
|
+
}
|
|
917
|
+
|
|
803
918
|
/**
|
|
804
919
|
* Render search results as a table (when _elements is specified)
|
|
805
920
|
*/
|
|
806
921
|
async renderSearchTable(json, elements, req) {
|
|
807
922
|
const entries = json.entry || [];
|
|
808
|
-
const total = json.total || 0;
|
|
809
923
|
|
|
810
|
-
let html =
|
|
924
|
+
let html = this.describeSearchBundle(json);
|
|
811
925
|
|
|
812
926
|
// Pagination links
|
|
813
927
|
html += this.renderPaginationLinks(json);
|
|
@@ -859,22 +973,12 @@ class TxHtmlRenderer {
|
|
|
859
973
|
*/
|
|
860
974
|
async renderSearchSummary(json, req) {
|
|
861
975
|
const entries = json.entry || [];
|
|
862
|
-
const total = json.total || 0;
|
|
863
976
|
|
|
864
|
-
let html =
|
|
977
|
+
let html = this.describeSearchBundle(json);
|
|
865
978
|
|
|
866
979
|
// Pagination links
|
|
867
980
|
html += this.renderPaginationLinks(json);
|
|
868
981
|
|
|
869
|
-
// Bundle summary
|
|
870
|
-
html += '<div class="card mb-3">';
|
|
871
|
-
html += '<div class="card-header">Bundle Summary</div>';
|
|
872
|
-
html += '<div class="card-body">';
|
|
873
|
-
html += `<p><strong>Type:</strong> ${escape(json.type)}</p>`;
|
|
874
|
-
html += `<p><strong>Total:</strong> ${total}</p>`;
|
|
875
|
-
html += '</div>';
|
|
876
|
-
html += '</div>';
|
|
877
|
-
|
|
878
982
|
// Each entry
|
|
879
983
|
for (const entry of entries) {
|
|
880
984
|
html += '<hr/>';
|
|
@@ -1010,7 +1114,7 @@ class TxHtmlRenderer {
|
|
|
1010
1114
|
html += '<div class="narrative">(No Narrative)</div>';
|
|
1011
1115
|
}
|
|
1012
1116
|
if (json.text && json.text.div) {
|
|
1013
|
-
|
|
1117
|
+
// Collapsible JSON source
|
|
1014
1118
|
html += '<div class="xhtml">';
|
|
1015
1119
|
html += `<button type="button" class="btn btn-sm btn-outline-secondary" onclick="toggleOriginalNarrative('${resourceId}x')">`;
|
|
1016
1120
|
html += 'Show Original Narrative</button>';
|
|
@@ -1047,8 +1151,17 @@ class TxHtmlRenderer {
|
|
|
1047
1151
|
valueSetsJson: JSON.stringify(json.valueSets || [])
|
|
1048
1152
|
});
|
|
1049
1153
|
}
|
|
1154
|
+
|
|
1155
|
+
buildSourceOptions(provider) {
|
|
1156
|
+
let result = '';
|
|
1157
|
+
result += `<option value="internal">internal</option>`;
|
|
1158
|
+
for (let sp of provider.listValueSetSourceCodes()) {
|
|
1159
|
+
result += `<option value="${sp}">${sp}</option>`;
|
|
1160
|
+
}
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1050
1163
|
}
|
|
1051
1164
|
|
|
1052
1165
|
module.exports = {
|
|
1053
|
-
|
|
1054
|
-
};
|
|
1166
|
+
TxHtmlRenderer, loadTemplate
|
|
1167
|
+
};
|
package/tx/tx.fhir.org.yml
CHANGED
|
@@ -26,6 +26,7 @@ sources:
|
|
|
26
26
|
- snomed:sct_nl_20240930.cache
|
|
27
27
|
- snomed:sct_uk_20230412.cache
|
|
28
28
|
- snomed:sct_us_20230301.cache
|
|
29
|
+
- snomed:sct_us_20240301.cache
|
|
29
30
|
- snomed:sct_us_20250901.cache
|
|
30
31
|
- snomed:sct_test_20250814.cache
|
|
31
32
|
- cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
|
package/tx/tx.js
CHANGED
|
@@ -20,7 +20,7 @@ const packageJson = require("../package.json");
|
|
|
20
20
|
// Import workers
|
|
21
21
|
const ReadWorker = require('./workers/read');
|
|
22
22
|
const SearchWorker = require('./workers/search');
|
|
23
|
-
const { ExpandWorker } = require('./workers/expand');
|
|
23
|
+
const { ExpandWorker, INTERNAL_DEFAULT_LIMIT, EXTERNAL_DEFAULT_LIMIT} = require('./workers/expand');
|
|
24
24
|
const { ValidateWorker } = require('./workers/validate');
|
|
25
25
|
const TranslateWorker = require('./workers/translate');
|
|
26
26
|
const LookupWorker = require('./workers/lookup');
|
|
@@ -48,6 +48,8 @@ const {bundleFromR5} = require("./xversion/xv-bundle");
|
|
|
48
48
|
const {convertResourceToR5} = require("./xversion/xv-resource");
|
|
49
49
|
const ClosureWorker = require("./workers/closure");
|
|
50
50
|
const {BundleXML} = require("./xml/bundle-xml");
|
|
51
|
+
const ConceptUsageTracker = require("./usage-tracker");
|
|
52
|
+
const ProblemFinder = require("./problems");
|
|
51
53
|
// const {writeFileSync} = require("fs");
|
|
52
54
|
|
|
53
55
|
class TXModule {
|
|
@@ -138,6 +140,7 @@ class TXModule {
|
|
|
138
140
|
consoleErrors: config.consoleErrors,
|
|
139
141
|
telnetErrors: config.telnetErrors
|
|
140
142
|
});
|
|
143
|
+
this.usageTracker = new ConceptUsageTracker();
|
|
141
144
|
|
|
142
145
|
this.log.info('Initializing TX module');
|
|
143
146
|
|
|
@@ -280,6 +283,7 @@ class TXModule {
|
|
|
280
283
|
acceptLanguage, this.i18n, requestId, 30,
|
|
281
284
|
endpointInfo.resourceCache, endpointInfo.expansionCache
|
|
282
285
|
);
|
|
286
|
+
opContext.usageTracker = this.usageTracker;
|
|
283
287
|
|
|
284
288
|
// Attach everything to request
|
|
285
289
|
req.txProvider = endpointInfo.provider;
|
|
@@ -448,6 +452,14 @@ class TXModule {
|
|
|
448
452
|
// Set up routes
|
|
449
453
|
this.setupRoutes(router);
|
|
450
454
|
|
|
455
|
+
// Redirect /r5 → /r5/
|
|
456
|
+
app.use((req, res, next) => {
|
|
457
|
+
if (req.path === endpointPath) {
|
|
458
|
+
return res.redirect(301, endpointPath + '/');
|
|
459
|
+
}
|
|
460
|
+
next();
|
|
461
|
+
});
|
|
462
|
+
|
|
451
463
|
// Register the router with the app
|
|
452
464
|
app.use(endpointPath, router);
|
|
453
465
|
this.routers.set(endpointPath, router);
|
|
@@ -609,7 +621,7 @@ class TXModule {
|
|
|
609
621
|
router.get('/ValueSet/\\$expand', async (req, res) => {
|
|
610
622
|
const start = Date.now();
|
|
611
623
|
try {
|
|
612
|
-
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
624
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
|
|
613
625
|
await worker.handle(req, res, this.log);
|
|
614
626
|
} finally {
|
|
615
627
|
this.countRequest('$expand', Date.now() - start);
|
|
@@ -618,7 +630,7 @@ class TXModule {
|
|
|
618
630
|
router.post('/ValueSet/\\$expand', async (req, res) => {
|
|
619
631
|
const start = Date.now();
|
|
620
632
|
try {
|
|
621
|
-
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
633
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
|
|
622
634
|
await worker.handle(req, res, this.log);
|
|
623
635
|
} finally {
|
|
624
636
|
this.countRequest('$expand', Date.now() - start);
|
|
@@ -773,7 +785,7 @@ class TXModule {
|
|
|
773
785
|
router.get('/ValueSet/:id/\\$expand', async (req, res) => {
|
|
774
786
|
const start = Date.now();
|
|
775
787
|
try {
|
|
776
|
-
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
788
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
|
|
777
789
|
await worker.handleInstance(req, res, this.log);
|
|
778
790
|
} finally {
|
|
779
791
|
this.countRequest('$expand', Date.now() - start);
|
|
@@ -782,7 +794,7 @@ class TXModule {
|
|
|
782
794
|
router.post('/ValueSet/:id/\\$expand', async (req, res) => {
|
|
783
795
|
const start = Date.now();
|
|
784
796
|
try {
|
|
785
|
-
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n);
|
|
797
|
+
let worker = new ExpandWorker(req.txOpContext, this.log, req.txProvider, this.languages, this.i18n, this.internalLimit(req), this.externalLimit(req));
|
|
786
798
|
await worker.handleInstance(req, res, this.log);
|
|
787
799
|
} finally {
|
|
788
800
|
this.countRequest('$expand', Date.now() - start);
|
|
@@ -882,6 +894,20 @@ class TXModule {
|
|
|
882
894
|
}
|
|
883
895
|
});
|
|
884
896
|
|
|
897
|
+
router.get('/problems.html', async (req, res) => {
|
|
898
|
+
const start = Date.now();
|
|
899
|
+
try {
|
|
900
|
+
let txhtml = new TxHtmlRenderer(new Renderer(req.txOpContext, req.txProvider), this.liquid);
|
|
901
|
+
const problemFinder = new ProblemFinder();
|
|
902
|
+
const content = await problemFinder.scanValueSets(req.txProvider);
|
|
903
|
+
const html = await txhtml.renderPage('Problems', '<h3>ValueSet dependencies on unknown CodeSystem/Versions</h3>'+content, req.txEndpoint, req.txStartTime);
|
|
904
|
+
res.setHeader('Content-Type', 'text/html');
|
|
905
|
+
res.send(html);
|
|
906
|
+
} finally {
|
|
907
|
+
this.countRequest('problems', Date.now() - start);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
|
|
885
911
|
// Metadata / CapabilityStatement
|
|
886
912
|
router.get('/metadata', async (req, res) => {
|
|
887
913
|
const start = Date.now();
|
|
@@ -1072,6 +1098,16 @@ class TXModule {
|
|
|
1072
1098
|
}
|
|
1073
1099
|
}
|
|
1074
1100
|
|
|
1101
|
+
internalLimit(req) {
|
|
1102
|
+
let isTest = req.header("User-Agent") == 'Tools/Java';
|
|
1103
|
+
if (this.config.internalLimit && !isTest) return this.config.internalLimit; else return INTERNAL_DEFAULT_LIMIT;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
externalLimit(req) {
|
|
1107
|
+
let isTest = req.header("User-Agent") == 'Tools/Java';
|
|
1108
|
+
if (this.config.internalLimit && !isTest) return this.config.externalLimit; else return EXTERNAL_DEFAULT_LIMIT;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1075
1111
|
}
|
|
1076
1112
|
|
|
1077
1113
|
module.exports = TXModule;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
|
|
2
|
+
class ConceptUsageTracker {
|
|
3
|
+
|
|
4
|
+
constructor() {
|
|
5
|
+
this.map = new Map();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async scanValueSets(library) {
|
|
9
|
+
let c = 0;
|
|
10
|
+
for (let vsp of library.valueSetProviders) {
|
|
11
|
+
let list = await vsp.listAllValueSets();
|
|
12
|
+
for (let url of list) {
|
|
13
|
+
let vs = await vsp.fetchValueSet(url);
|
|
14
|
+
if (vs && vs.jsonObj.compose) {
|
|
15
|
+
if (await this.scanValueSet(vs.jsonObj.compose)) {
|
|
16
|
+
c++;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return c;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async scanValueSet(compose, versions, active) {
|
|
25
|
+
let ok = false;
|
|
26
|
+
for (let inc of compose.include || []) {
|
|
27
|
+
if (inc.system) {
|
|
28
|
+
if (active && inc.version) {
|
|
29
|
+
this.seeVersion(versions, inc.system, inc.version);
|
|
30
|
+
}
|
|
31
|
+
for (let c of inc.concept || []) {
|
|
32
|
+
if (c.code) {
|
|
33
|
+
ok = true;
|
|
34
|
+
this.seeConcept(inc.system, c.code);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return ok;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
seeConcept(system, code) {
|
|
43
|
+
let cs = this.map.get(system);
|
|
44
|
+
if (!cs) {
|
|
45
|
+
cs = new Map();
|
|
46
|
+
this.map.set(system, cs);
|
|
47
|
+
}
|
|
48
|
+
let ci = cs.get(code);
|
|
49
|
+
if (!ci) {
|
|
50
|
+
ci = { count : 0 }
|
|
51
|
+
cs.set(code, ci);
|
|
52
|
+
}
|
|
53
|
+
ci.count++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
usages(system) {
|
|
57
|
+
return this.map.get(system) || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
seeVersion(versions, system, version) {
|
|
61
|
+
let set = versions[system];
|
|
62
|
+
if (set == null) {
|
|
63
|
+
set = new Set();
|
|
64
|
+
versions[system] = set;
|
|
65
|
+
}
|
|
66
|
+
set.add(version);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = ConceptUsageTracker;
|
package/tx/vs/vs-api.js
CHANGED
package/tx/vs/vs-database.js
CHANGED
|
@@ -37,7 +37,7 @@ class ValueSetDatabase {
|
|
|
37
37
|
this._db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
|
|
38
38
|
if (err) {
|
|
39
39
|
this._db = null;
|
|
40
|
-
reject(new Error(`Failed to open database: ${err.message}`));
|
|
40
|
+
reject(new Error(`Failed to open database ${this.dbPath}: ${err.message}`));
|
|
41
41
|
} else {
|
|
42
42
|
resolve(this._db);
|
|
43
43
|
}
|
|
@@ -122,7 +122,7 @@ class ValueSetDatabase {
|
|
|
122
122
|
return new Promise((resolve, reject) => {
|
|
123
123
|
const db = new sqlite3.Database(this.dbPath, (err) => {
|
|
124
124
|
if (err) {
|
|
125
|
-
reject(new Error(`Failed to create database: ${err.message}`));
|
|
125
|
+
reject(new Error(`Failed to create database ${this.dbPath}: ${err.message}`));
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
|
|
@@ -177,6 +177,7 @@ class ValueSetDatabase {
|
|
|
177
177
|
CREATE TABLE valueset_systems (
|
|
178
178
|
valueset_id TEXT,
|
|
179
179
|
system TEXT,
|
|
180
|
+
version TEXT,
|
|
180
181
|
FOREIGN KEY (valueset_id) REFERENCES valuesets(url)
|
|
181
182
|
)
|
|
182
183
|
`);
|
|
@@ -193,7 +194,7 @@ class ValueSetDatabase {
|
|
|
193
194
|
db.run('CREATE INDEX idx_identifiers_value ON valueset_identifiers(value)');
|
|
194
195
|
db.run('CREATE INDEX idx_jurisdictions_system ON valueset_jurisdictions(system)');
|
|
195
196
|
db.run('CREATE INDEX idx_jurisdictions_code ON valueset_jurisdictions(code)');
|
|
196
|
-
db.run('CREATE INDEX idx_systems_system ON valueset_systems(system)');
|
|
197
|
+
db.run('CREATE INDEX idx_systems_system ON valueset_systems(system, version)');
|
|
197
198
|
|
|
198
199
|
db.close((err) => {
|
|
199
200
|
if (err) {
|
|
@@ -392,8 +393,8 @@ class ValueSetDatabase {
|
|
|
392
393
|
pendingOperations++;
|
|
393
394
|
|
|
394
395
|
db.run(`
|
|
395
|
-
INSERT INTO valueset_systems (valueset_id, system) VALUES (?, ?)
|
|
396
|
-
`, [valueSet.id, include.system], function(err) {
|
|
396
|
+
INSERT INTO valueset_systems (valueset_id, system, version) VALUES (?, ?, ?)
|
|
397
|
+
`, [valueSet.id, include.system, include.version], function(err) {
|
|
397
398
|
if (err) {
|
|
398
399
|
operationError(new Error(`Failed to insert system: ${err.message}`));
|
|
399
400
|
} else {
|
|
@@ -735,8 +736,15 @@ class ValueSetDatabase {
|
|
|
735
736
|
|
|
736
737
|
case 'system':
|
|
737
738
|
joins.add('JOIN valueset_systems vs ON v.id = vs.valueset_id');
|
|
738
|
-
|
|
739
|
-
|
|
739
|
+
if (value.includes('|')) {
|
|
740
|
+
conditions.push('vs.system = ?');
|
|
741
|
+
params.push(value.substring(0, value.indexOf('|')));
|
|
742
|
+
conditions.push('vs.version = ?');
|
|
743
|
+
params.push(value.substring(value.indexOf('|')+1));
|
|
744
|
+
} else {
|
|
745
|
+
conditions.push('vs.system = ?');
|
|
746
|
+
params.push(value);
|
|
747
|
+
}
|
|
740
748
|
break;
|
|
741
749
|
|
|
742
750
|
default:
|
package/tx/vs/vs-package.js
CHANGED
|
@@ -23,6 +23,11 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
23
23
|
this.valueSetMap = new Map();
|
|
24
24
|
this.initialized = false;
|
|
25
25
|
this.count = 0;
|
|
26
|
+
this.sourcePackageCode = packageLoader.id();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sourcePackage() {
|
|
30
|
+
return this.sourcePackageCode;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
@@ -42,7 +47,7 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
|
|
|
42
47
|
await this._populateDatabase();
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
this.valueSetMap = await this.database.loadAllValueSets(this.
|
|
50
|
+
this.valueSetMap = await this.database.loadAllValueSets(this.sourcePackage());
|
|
46
51
|
this.initialized = true;
|
|
47
52
|
}
|
|
48
53
|
|