fhirsmith 0.7.6 → 0.8.2
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 +48 -0
- package/README.md +5 -1
- package/library/languages.js +10 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +2 -2
- package/publisher/publisher.js +1 -1
- package/registry/registry.js +2 -2
- package/root-bare-template.html +1 -2
- package/security.md +3 -0
- package/server.js +100 -70
- package/stats.js +37 -6
- package/tx/cs/cs-api.js +8 -4
- package/tx/cs/cs-loinc.js +14 -2
- package/tx/cs/cs-omop.js +5 -3
- package/tx/cs/cs-rxnorm.js +18 -16
- package/tx/cs/cs-snomed.js +279 -6
- package/tx/data/cpt-fragment.db +0 -0
- package/tx/data/cs-de.json +186 -0
- package/tx/data/cs-extensions.json +92 -0
- package/tx/data/cs-simple.json +130 -0
- package/tx/data/cs-supplement.json +78 -0
- package/tx/data/lang.dat +49180 -0
- package/tx/data/languages.csv +191 -0
- package/tx/data/loinc-subset.txt +75 -0
- package/tx/data/omop-fragment.db +0 -0
- package/tx/data/readme.md +43 -0
- package/tx/data/regions.csv +273 -0
- package/tx/data/rxnorm-subset.txt +22 -0
- package/tx/data/snomed-subset.txt +47 -0
- package/tx/data/ucum-essence.xml +2059 -0
- package/tx/html/dash-metrics.liquid +147 -0
- package/tx/importers/import-rxnorm.module.js +4 -30
- package/tx/library/canonical-resource.js +8 -0
- package/tx/library/conceptmap.js +29 -1
- package/tx/library/designations.js +4 -8
- package/tx/library/extensions.js +4 -3
- package/tx/library/renderer.js +9 -9
- package/tx/ocl/cm-ocl.cjs +185 -65
- package/tx/ocl/cs-ocl.cjs +69 -50
- package/tx/ocl/jobs/background-queue.cjs +0 -8
- package/tx/ocl/mappers/concept-mapper.cjs +13 -3
- package/tx/ocl/shared/patches.cjs +1 -0
- package/tx/ocl/vs-ocl.cjs +137 -157
- package/tx/operation-context.js +3 -3
- package/tx/params.js +2 -2
- package/tx/provider.js +6 -3
- package/tx/sct/structures.js +6 -1
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/vs/vs-database.js +107 -23
- package/tx/vs/vs-vsac.js +66 -19
- package/tx/workers/expand.js +10 -10
- package/tx/workers/related.js +2 -2
- package/tx/workers/search.js +2 -1
- package/tx/workers/translate.js +222 -33
- package/tx/workers/validate.js +13 -13
- package/tx/xversion/xv-parameters.js +54 -1
- package/xig/xig.js +171 -9
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
|
2
|
+
<div style="flex: 1;">
|
|
3
|
+
<h6>Memory Usage (Heap Used)</h6>
|
|
4
|
+
<canvas id="memoryChart" height="200"></canvas>
|
|
5
|
+
</div>
|
|
6
|
+
<div style="flex: 1;">
|
|
7
|
+
<h6>Turnaround Time</h6>
|
|
8
|
+
<canvas id="tatChart" height="200"></canvas>
|
|
9
|
+
</div>
|
|
10
|
+
<div style="flex: 1;">
|
|
11
|
+
<h6>Requests per Minute</h6>
|
|
12
|
+
<canvas id="requestChart" height="200"></canvas>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
17
|
+
<script>
|
|
18
|
+
(function() {
|
|
19
|
+
const historyData = {{ historyJson }};
|
|
20
|
+
const startTime = {{ startTime }};
|
|
21
|
+
|
|
22
|
+
function formatTime(timestamp) {
|
|
23
|
+
const d = new Date(timestamp);
|
|
24
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatMB(bytes) {
|
|
28
|
+
return (bytes / 1024 / 1024).toFixed(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatDuration(ms) {
|
|
32
|
+
const hours = ms / 3600000;
|
|
33
|
+
if (hours < 1) {
|
|
34
|
+
return Math.round(ms / 60000) + 'm ago';
|
|
35
|
+
}
|
|
36
|
+
return hours.toFixed(1) + 'h ago';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const chartOptions = {
|
|
40
|
+
responsive: false,
|
|
41
|
+
maintainAspectRatio: false,
|
|
42
|
+
scales: {
|
|
43
|
+
x: {
|
|
44
|
+
ticks: {
|
|
45
|
+
maxTicksAuto: 6
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
y: {
|
|
49
|
+
beginAtZero: true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
plugins: {
|
|
53
|
+
legend: {
|
|
54
|
+
display: false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Memory chart
|
|
60
|
+
new Chart(document.getElementById('memoryChart'), {
|
|
61
|
+
type: 'line',
|
|
62
|
+
data: {
|
|
63
|
+
labels: historyData.map(d => formatTime(d.time)),
|
|
64
|
+
datasets: [{
|
|
65
|
+
data: historyData.map(d => parseFloat(formatMB(d.mem))),
|
|
66
|
+
borderColor: '#007bff',
|
|
67
|
+
backgroundColor: 'rgba(0, 123, 255, 0.1)',
|
|
68
|
+
fill: true,
|
|
69
|
+
tension: 0.3,
|
|
70
|
+
pointRadius: 2
|
|
71
|
+
}]
|
|
72
|
+
},
|
|
73
|
+
options: {
|
|
74
|
+
...chartOptions,
|
|
75
|
+
scales: {
|
|
76
|
+
...chartOptions.scales,
|
|
77
|
+
y: {
|
|
78
|
+
...chartOptions.scales.y,
|
|
79
|
+
title: {
|
|
80
|
+
display: true,
|
|
81
|
+
text: 'MB (𝚫 from start)'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
// Tat chart
|
|
90
|
+
new Chart(document.getElementById('tatChart'), {
|
|
91
|
+
type: 'line',
|
|
92
|
+
data: {
|
|
93
|
+
labels: historyData.map(d => formatTime(d.time)),
|
|
94
|
+
datasets: [{
|
|
95
|
+
data: historyData.map(d => d.tat),
|
|
96
|
+
borderColor: '#7b7b7b',
|
|
97
|
+
backgroundColor: 'rgba(123, 123, 123, 0.1)',
|
|
98
|
+
fill: true,
|
|
99
|
+
tension: 0.3,
|
|
100
|
+
pointRadius: 2
|
|
101
|
+
}]
|
|
102
|
+
},
|
|
103
|
+
options: {
|
|
104
|
+
...chartOptions,
|
|
105
|
+
scales: {
|
|
106
|
+
...chartOptions.scales,
|
|
107
|
+
y: {
|
|
108
|
+
...chartOptions.scales.y,
|
|
109
|
+
title: {
|
|
110
|
+
display: true,
|
|
111
|
+
text: 'ms'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Requests chart
|
|
119
|
+
new Chart(document.getElementById('requestChart'), {
|
|
120
|
+
type: 'line',
|
|
121
|
+
data: {
|
|
122
|
+
labels: historyData.map(d => formatTime(d.time)),
|
|
123
|
+
datasets: [{
|
|
124
|
+
data: historyData.map(d => d.rpm.toFixed(2)),
|
|
125
|
+
borderColor: '#28a745',
|
|
126
|
+
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
|
127
|
+
fill: true,
|
|
128
|
+
tension: 0.3,
|
|
129
|
+
pointRadius: 2
|
|
130
|
+
}]
|
|
131
|
+
},
|
|
132
|
+
options: {
|
|
133
|
+
...chartOptions,
|
|
134
|
+
scales: {
|
|
135
|
+
...chartOptions.scales,
|
|
136
|
+
y: {
|
|
137
|
+
...chartOptions.scales.y,
|
|
138
|
+
title: {
|
|
139
|
+
display: true,
|
|
140
|
+
text: 'req/min'
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
})();
|
|
147
|
+
</script>
|
|
@@ -428,7 +428,8 @@ class RxNormModule extends BaseTerminologyModule {
|
|
|
428
428
|
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rel ON RXNREL(REL)',
|
|
429
429
|
'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela ON RXNREL(RELA)',
|
|
430
430
|
'CREATE INDEX IF NOT EXISTS X_RXNSTY_2 ON RXNSTY(TUI)',
|
|
431
|
-
'CREATE INDEX IF NOT EXISTS idx_rxnstems_stem_cui ON RXNSTEMS(stem, CUI)'
|
|
431
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnstems_stem_cui ON RXNSTEMS(stem, CUI)',
|
|
432
|
+
'CREATE INDEX IF NOT EXISTS idx_rxnstems_cui_stem ON RXNSTEMS(CUI, stem)'
|
|
432
433
|
];
|
|
433
434
|
|
|
434
435
|
return new Promise((resolve, reject) => {
|
|
@@ -779,7 +780,7 @@ class RxNormImporter {
|
|
|
779
780
|
if (this.options.verbose) console.log('Generating word stems...');
|
|
780
781
|
|
|
781
782
|
// Simple English stemmer implementation
|
|
782
|
-
|
|
783
|
+
var natural = require('natural');
|
|
783
784
|
const stems = new Map();
|
|
784
785
|
|
|
785
786
|
// Get all RXNORM concepts
|
|
@@ -791,7 +792,7 @@ class RxNormImporter {
|
|
|
791
792
|
rows.forEach(row => {
|
|
792
793
|
const words = this.extractWords(row.STR);
|
|
793
794
|
words.forEach(word => {
|
|
794
|
-
const stem =
|
|
795
|
+
const stem = natural.PorterStemmer.stem(word.toLowerCase());
|
|
795
796
|
if (stem.length > 0 && stem.length <= 20) {
|
|
796
797
|
if (!stems.has(stem)) {
|
|
797
798
|
stems.set(stem, new Set());
|
|
@@ -864,33 +865,6 @@ class RxNormImporter {
|
|
|
864
865
|
}
|
|
865
866
|
}
|
|
866
867
|
|
|
867
|
-
// Simple English stemmer (basic implementation)
|
|
868
|
-
class SimpleStemmer {
|
|
869
|
-
constructor() {
|
|
870
|
-
// Common English suffixes to remove
|
|
871
|
-
this.suffixes = [
|
|
872
|
-
'ing', 'ly', 'ed', 'ies', 'ied', 'ies', 'ies', 'y', 's',
|
|
873
|
-
'tion', 'sion', 'ness', 'ment', 'able', 'ible', 'ant', 'ent'
|
|
874
|
-
].sort((a, b) => b.length - a.length); // Longest first
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
stem(word) {
|
|
878
|
-
if (word.length <= 3) return word;
|
|
879
|
-
|
|
880
|
-
// Try to remove suffixes
|
|
881
|
-
for (const suffix of this.suffixes) {
|
|
882
|
-
if (word.endsWith(suffix) && word.length > suffix.length + 2) {
|
|
883
|
-
const stem = word.substring(0, word.length - suffix.length);
|
|
884
|
-
// Basic validation - stem should still be reasonable length
|
|
885
|
-
if (stem.length >= 3) {
|
|
886
|
-
return stem;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
return word;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
868
|
|
|
895
869
|
module.exports = {
|
|
896
870
|
RxNormModule,
|
|
@@ -64,6 +64,14 @@ class CanonicalResource {
|
|
|
64
64
|
return this.version ? this.url+'|' + this.version : this.url;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
get vurlOrMsg() {
|
|
68
|
+
if (this.url) {
|
|
69
|
+
return this.version ? this.url+'|' + this.version : this.url;
|
|
70
|
+
} else {
|
|
71
|
+
return '(unidentified)';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
get fhirType() {
|
|
68
76
|
return this.jsonObj.resourceType;
|
|
69
77
|
}
|
package/tx/library/conceptmap.js
CHANGED
|
@@ -132,7 +132,9 @@ class ConceptMap extends CanonicalResource {
|
|
|
132
132
|
|
|
133
133
|
let all = this.canonicalMatches(targetScope, this.targetScope);
|
|
134
134
|
for (const g of this.jsonObj.group || []) {
|
|
135
|
-
|
|
135
|
+
const sourceOk = this.canonicalMatches(vurl, g.source);
|
|
136
|
+
const targetOk = !targetSystem || this.canonicalMatches(targetSystem, g.target);
|
|
137
|
+
if (all || (sourceOk && targetOk)) {
|
|
136
138
|
for (const em of g.element || []) {
|
|
137
139
|
if (em.code === coding.code) {
|
|
138
140
|
let match = {
|
|
@@ -146,6 +148,32 @@ class ConceptMap extends CanonicalResource {
|
|
|
146
148
|
}
|
|
147
149
|
return result;
|
|
148
150
|
}
|
|
151
|
+
|
|
152
|
+
listTranslationsReverse(coding, targetScope, sourceSystem) {
|
|
153
|
+
let result = [];
|
|
154
|
+
let vurl = VersionUtilities.vurl(coding.system, coding.version);
|
|
155
|
+
|
|
156
|
+
let all = this.canonicalMatches(targetScope, this.targetScope);
|
|
157
|
+
for (const g of this.jsonObj.group || []) {
|
|
158
|
+
const targetOk = this.canonicalMatches(vurl, g.target);
|
|
159
|
+
const sourceOk = !sourceSystem || this.canonicalMatches(sourceSystem, g.source);
|
|
160
|
+
if (all || (sourceOk && targetOk)) {
|
|
161
|
+
for (const em of g.element || []) {
|
|
162
|
+
for (const tm of em.target || []) {
|
|
163
|
+
if (tm.code === coding.code) {
|
|
164
|
+
let match = {
|
|
165
|
+
group: g,
|
|
166
|
+
match: em,
|
|
167
|
+
target: tm
|
|
168
|
+
};
|
|
169
|
+
result.push(match);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
149
177
|
/**
|
|
150
178
|
* Gets the source scope (R5) or source system (R3/R4)
|
|
151
179
|
* @returns {string|undefined} Source scope/system
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const { LanguagePartType, Languages, Language, LanguageDefinitions} = require('../../library/languages');
|
|
2
2
|
const {validateParameter, validateOptionalParameter, validateArrayParameter} = require("../../library/utilities");
|
|
3
|
-
|
|
3
|
+
const natural = require('natural');
|
|
4
4
|
/**
|
|
5
5
|
* Display checking modes for concept designations
|
|
6
6
|
*/
|
|
@@ -26,6 +26,7 @@ class SearchFilterText {
|
|
|
26
26
|
|
|
27
27
|
this.filter = filter ? filter.toLowerCase() : null;
|
|
28
28
|
this.stems = [];
|
|
29
|
+
this.stemmer = natural.PorterStemmer;
|
|
29
30
|
if (filter) {
|
|
30
31
|
this._process();
|
|
31
32
|
}
|
|
@@ -59,7 +60,7 @@ class SearchFilterText {
|
|
|
59
60
|
i++;
|
|
60
61
|
}
|
|
61
62
|
const word = value.substring(j, i).toLowerCase();
|
|
62
|
-
const stemmed = this.
|
|
63
|
+
const stemmed = this.stemmer.stem(word);
|
|
63
64
|
|
|
64
65
|
if (this._find(stemmed)) {
|
|
65
66
|
if (returnRating) {
|
|
@@ -142,7 +143,7 @@ class SearchFilterText {
|
|
|
142
143
|
i++;
|
|
143
144
|
}
|
|
144
145
|
const word = this.filter.substring(j, i);
|
|
145
|
-
this.stems.push(this.
|
|
146
|
+
this.stems.push(this.stemmer.stem(word.toLowerCase()));
|
|
146
147
|
} else {
|
|
147
148
|
i++;
|
|
148
149
|
}
|
|
@@ -155,11 +156,6 @@ class SearchFilterText {
|
|
|
155
156
|
return /[0-9a-zA-Z]/.test(char);
|
|
156
157
|
}
|
|
157
158
|
|
|
158
|
-
_stem(word) {
|
|
159
|
-
// Simple stemming - in practice you'd want a proper stemmer
|
|
160
|
-
return word.toLowerCase();
|
|
161
|
-
}
|
|
162
|
-
|
|
163
159
|
_find(stem) {
|
|
164
160
|
// Binary search
|
|
165
161
|
let left = 0;
|
package/tx/library/extensions.js
CHANGED
|
@@ -29,7 +29,7 @@ const Extensions = {
|
|
|
29
29
|
}
|
|
30
30
|
},
|
|
31
31
|
|
|
32
|
-
checkNoModifiers(element, place, name) {
|
|
32
|
+
checkNoModifiers(element, place, name, resource) {
|
|
33
33
|
if (!element) {
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
@@ -41,11 +41,12 @@ const Extensions = {
|
|
|
41
41
|
for (const extension of element.modifierExtension) {
|
|
42
42
|
urls.add(extension.url);
|
|
43
43
|
}
|
|
44
|
+
const resId = resource ? resource : "";
|
|
44
45
|
const urlList = [...urls].join('\', \'');
|
|
45
46
|
if (urls.size > 1) {
|
|
46
|
-
throw new Issue("error", "business-rule", null, null, 'Cannot process resource at "' + name + '" due to the presence of modifier extensions '+urlList);
|
|
47
|
+
throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of modifier extensions '+urlList);
|
|
47
48
|
} else {
|
|
48
|
-
throw new Issue("error", "business-rule", null, null, 'Cannot process resource at "' + name + '" due to the presence of the modifier extension '+urlList);
|
|
49
|
+
throw new Issue("error", "business-rule", null, null, 'Cannot process resource '+resId+' at "' + name + '" due to the presence of the modifier extension '+urlList);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
return true;
|
package/tx/library/renderer.js
CHANGED
|
@@ -1800,12 +1800,12 @@ class Renderer {
|
|
|
1800
1800
|
if (elem.noMap) {
|
|
1801
1801
|
const nomapComment = Extensions.readString(elem, 'http://hl7.org/fhir/StructureDefinition/conceptmap-nomap-comment');
|
|
1802
1802
|
if (!hasComment) {
|
|
1803
|
-
tr.td().colspan
|
|
1803
|
+
tr.td().setAttribute('colspan',"2").style("background-color: #efefef").tx("(not mapped)");
|
|
1804
1804
|
} else if (nomapComment) {
|
|
1805
|
-
tr.td().colspan
|
|
1805
|
+
tr.td().setAttribute('colspan',"2").style("background-color: #efefef").tx("(not mapped)");
|
|
1806
1806
|
tr.td().style("background-color: #efefef").tx(nomapComment);
|
|
1807
1807
|
} else {
|
|
1808
|
-
tr.td().colspan
|
|
1808
|
+
tr.td().setAttribute('colspan',"3").style("background-color: #efefef").tx("(not mapped)");
|
|
1809
1809
|
}
|
|
1810
1810
|
} else {
|
|
1811
1811
|
let first = true;
|
|
@@ -1858,16 +1858,16 @@ class Renderer {
|
|
|
1858
1858
|
let tr = tbl.tr();
|
|
1859
1859
|
const sourceColCount = 1 + Object.keys(sources).length - 1; // code + dependsOn attributes
|
|
1860
1860
|
const targetColCount = 1 + Object.keys(targets).length - 1; // code + product attributes
|
|
1861
|
-
tr.td().colspan
|
|
1861
|
+
tr.td().setAttribute('colspan', String(sourceColCount + 1)).b().tx(this.translate('CONC_MAP_SRC_DET'));
|
|
1862
1862
|
if (hasRelationships) {
|
|
1863
1863
|
tr.td().b().tx(this.translate('CONC_MAP_REL'));
|
|
1864
1864
|
}
|
|
1865
|
-
tr.td().colspan
|
|
1865
|
+
tr.td().setAttribute('colspan', String(targetColCount + 1)).b().tx(this.translate('CONC_MAP_TRGT_DET'));
|
|
1866
1866
|
if (hasComment) {
|
|
1867
1867
|
tr.td().b().tx(this.translate('GENERAL_COMMENT'));
|
|
1868
1868
|
}
|
|
1869
1869
|
if (hasProperties) {
|
|
1870
|
-
tr.td().colspan
|
|
1870
|
+
tr.td().setAttribute('colspan', String(Object.keys(props).length)).b().tx(this.translate('GENERAL_PROPS'));
|
|
1871
1871
|
}
|
|
1872
1872
|
|
|
1873
1873
|
// Second header row: actual column headers
|
|
@@ -1944,10 +1944,10 @@ class Renderer {
|
|
|
1944
1944
|
|
|
1945
1945
|
const nomapComment = Extensions.readString(elem, 'http://hl7.org/fhir/StructureDefinition/conceptmap-nomap-comment');
|
|
1946
1946
|
if (nomapComment) {
|
|
1947
|
-
tr.td().colspan
|
|
1947
|
+
tr.td().setAttribute('colspan',"3").style("background-color: #efefef").tx("(not mapped)");
|
|
1948
1948
|
tr.td().style("background-color: #efefef").tx(nomapComment);
|
|
1949
1949
|
} else {
|
|
1950
|
-
tr.td().colspan
|
|
1950
|
+
tr.td().setAttribute('colspan',"4").style("background-color: #efefef").tx("(not mapped)");
|
|
1951
1951
|
}
|
|
1952
1952
|
} else {
|
|
1953
1953
|
let first = true;
|
|
@@ -2072,7 +2072,7 @@ class Renderer {
|
|
|
2072
2072
|
async renderCSDetailsLink(tr, url, span2) {
|
|
2073
2073
|
const td = tr.td();
|
|
2074
2074
|
if (span2) {
|
|
2075
|
-
td.colspan
|
|
2075
|
+
td.setAttribute('colspan',"2");
|
|
2076
2076
|
}
|
|
2077
2077
|
td.b().tx(this.translate('CONC_MAP_CODES'));
|
|
2078
2078
|
td.tx(" " + this.translate('CONC_MAP_FRM') + " ");
|