fhirsmith 0.7.5 → 0.8.0
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 +50 -0
- package/README.md +8 -0
- package/library/html.js +4 -0
- package/library/languages.js +10 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +106 -51
- package/packages/packages.js +14 -0
- package/publisher/publisher.js +118 -28
- package/registry/registry.js +99 -91
- package/root-bare-template.html +92 -0
- package/security.md +32 -0
- package/server.js +99 -22
- package/stats.js +43 -10
- package/tx/README.md +6 -6
- package/tx/cs/cs-api.js +3 -0
- package/tx/cs/cs-api.md +285 -0
- package/tx/cs/cs-loinc.js +14 -2
- package/tx/cs/cs-rxnorm.js +14 -10
- package/tx/cs/cs-snomed.js +166 -5
- package/tx/html/dash-metrics.liquid +147 -0
- package/tx/importers/import-rxnorm.module.js +4 -30
- package/tx/importers/readme.md +3 -1
- package/tx/library/canonical-resource.js +8 -0
- package/tx/library/conceptmap.js +3 -1
- package/tx/library/designations.js +4 -8
- package/tx/library/renderer.js +9 -9
- package/tx/library.js +10 -4
- 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/provider.js +4 -3
- package/tx/sct/structures.js +5 -0
- package/tx/tx-html.js +36 -9
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/tx.js +34 -11
- package/tx/vs/vs-database.js +127 -6
- package/tx/vs/vs-vsac.js +98 -3
- package/tx/workers/search.js +2 -1
- package/tx/workers/translate.js +39 -14
- package/tx/workers/validate.js +3 -3
- package/utilities/dashboard.html +274 -0
- 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,
|
package/tx/importers/readme.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# TX-Import: Medical Terminology Import Tool
|
|
2
2
|
|
|
3
|
-
A comprehensive CLI tool for importing various medical terminology standards into SQLite databases and other formats
|
|
3
|
+
A comprehensive CLI tool for importing various medical terminology standards into SQLite databases and other formats
|
|
4
|
+
for use by FHIRsmith. The tool supports LOINC, SNOMED CT, UNII, NDC, and provides extensible architecture for
|
|
5
|
+
additional terminologies.
|
|
4
6
|
|
|
5
7
|
## Table of Contents
|
|
6
8
|
|
|
@@ -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 = {
|
|
@@ -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/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') + " ");
|
package/tx/library.js
CHANGED
|
@@ -65,7 +65,8 @@ class Library {
|
|
|
65
65
|
*/
|
|
66
66
|
conceptMapProviders;
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
packageSources = [];
|
|
69
|
+
externalSources = [];
|
|
69
70
|
|
|
70
71
|
baseUrl = null;
|
|
71
72
|
cacheFolder = null;
|
|
@@ -360,6 +361,9 @@ class Library {
|
|
|
360
361
|
const codeSystemProvider = new OCLCodeSystemProvider(config);
|
|
361
362
|
const valueSetProvider = new OCLValueSetProvider(config);
|
|
362
363
|
const conceptMapProvider = new OCLConceptMapProvider(config);
|
|
364
|
+
this.externalSources.push(codeSystemProvider);
|
|
365
|
+
this.externalSources.push(valueSetProvider);
|
|
366
|
+
this.externalSources.push(conceptMapProvider);
|
|
363
367
|
providerSet = {
|
|
364
368
|
config,
|
|
365
369
|
codeSystemProvider,
|
|
@@ -470,6 +474,7 @@ class Library {
|
|
|
470
474
|
let vsac = new VSACValueSetProvider(this.vsacCfg, this.stats);
|
|
471
475
|
vsac.initialize();
|
|
472
476
|
this.valueSetProviders.push(vsac);
|
|
477
|
+
this.externalSources.push(vsac);
|
|
473
478
|
//const mem = process.memoryUsage();
|
|
474
479
|
let time = Math.floor(Date.now() - this.lastTime).toString().padStart(5)+" ";
|
|
475
480
|
let system = "vsac".padEnd(50);
|
|
@@ -597,7 +602,7 @@ class Library {
|
|
|
597
602
|
const contentLoader = new PackageContentLoader(fullPackagePath);
|
|
598
603
|
await contentLoader.initialize();
|
|
599
604
|
|
|
600
|
-
this.
|
|
605
|
+
this.packageSources.push(contentLoader.id()+"#"+contentLoader.version());
|
|
601
606
|
|
|
602
607
|
let cp = new ListCodeSystemProvider();
|
|
603
608
|
const resources = await contentLoader.getResourcesByType("CodeSystem");
|
|
@@ -635,7 +640,7 @@ class Library {
|
|
|
635
640
|
const contentLoader = new PackageContentLoader(fullPackagePath);
|
|
636
641
|
await contentLoader.initialize();
|
|
637
642
|
|
|
638
|
-
this.
|
|
643
|
+
this.packageSources.push(contentLoader.id()+"#"+contentLoader.version());
|
|
639
644
|
|
|
640
645
|
let cp = new ListCodeSystemProvider();
|
|
641
646
|
const resources = await contentLoader.getResourcesByType("CodeSystem");
|
|
@@ -847,7 +852,8 @@ class Library {
|
|
|
847
852
|
provider.lastTime = this.lastTime;
|
|
848
853
|
provider.lastMemory = this.lastMemory;
|
|
849
854
|
provider.totalDownloaded = this.totalDownloaded;
|
|
850
|
-
provider.
|
|
855
|
+
provider.packageSources = this.packageSources;
|
|
856
|
+
provider.externalSources = this.externalSources;
|
|
851
857
|
|
|
852
858
|
|
|
853
859
|
// Now add the existing value set providers after the FHIR core packages
|