fhirsmith 0.7.6 → 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.
@@ -371,6 +371,29 @@ class SnomedServices {
371
371
  return result;
372
372
  }
373
373
 
374
+ filterInactive(state) {
375
+ const result = new SnomedFilterContext();
376
+ result.inactive = state;
377
+ return result;
378
+ }
379
+
380
+ filterModuleId(id) {
381
+ const result = new SnomedFilterContext();
382
+ let concept = this.concepts.findConcept(id);
383
+ result.moduleId = concept.index;
384
+ return result;
385
+ }
386
+
387
+ filterByProperty(prop, value) {
388
+ const result = new SnomedFilterContext();
389
+ let p = this.concepts.findConcept(prop);
390
+ let v = this.concepts.findConcept(value);
391
+ result.propProp = p.index;
392
+ result.propValue = v.index;
393
+ return result;
394
+
395
+ }
396
+
374
397
  searchFilter(searchText, includeInactive = false, exactMatch = false) {
375
398
  const result = new SnomedFilterContext();
376
399
 
@@ -765,6 +788,12 @@ class SnomedProvider extends BaseCSServices {
765
788
  this._addCodeProperty(params, 'property', 'child', code, null, description);
766
789
  }
767
790
 
791
+ const moduleId = this.sct.concepts.getModuleId(ctxt.getReference());
792
+ if (moduleId) {
793
+ const code = this.sct.getConceptId(moduleId);
794
+ this._addCodeProperty(params, 'property', 'module', code, null, null);
795
+ }
796
+
768
797
  const relationships = this.sct.getConceptRelationships(ctxt.getReference());
769
798
  let set = new Set();
770
799
  for (let relationshipRef of relationships) {
@@ -812,10 +841,25 @@ class SnomedProvider extends BaseCSServices {
812
841
  return this.sct.conceptExists(value);
813
842
  }
814
843
  }
844
+ if (prop === 'inactive') {
845
+ return op === '=' && ['true', 'false'].includes(value);
846
+ }
847
+
848
+ if (prop === 'moduleId') {
849
+ const id = this.sct.stringToIdOrZero(value);
850
+ return id !== 0n && op === '=';
851
+ }
852
+
815
853
  if (prop == 'expressions' && op == '=' && ['true', 'false'].includes(value)) {
816
854
  return true;
817
855
  }
818
856
 
857
+ const cid = this.sct.stringToIdOrZero(prop);
858
+ if (cid != 0) {
859
+ const id = this.sct.stringToIdOrZero(value);
860
+ return id !== 0n && op === '=';
861
+ }
862
+
819
863
  return false;
820
864
  }
821
865
 
@@ -851,7 +895,38 @@ class SnomedProvider extends BaseCSServices {
851
895
  return null;
852
896
  }
853
897
  default:
854
- throw new Error(`Unsupported filter operation: ${op}`);
898
+ throw new Error(`Unsupported filter operation: concept ${op} ${value}`);
899
+ }
900
+ }
901
+
902
+ if (prop === 'inactive') {
903
+ if (value !== 'true' && value !== 'false') {
904
+ throw new Error(`Invalid filter value: ${value}`);
905
+ }
906
+
907
+ switch (op) {
908
+ case '=': {
909
+ filterContext.filters.push(this.sct.filterInactive(value === 'true'));
910
+ return null;
911
+ }
912
+ default:
913
+ throw new Error(`Unsupported filter operation: inactive ${op} ${value}`);
914
+ }
915
+ }
916
+
917
+ if (prop === 'moduleId') {
918
+ const id = this.sct.stringToIdOrZero(value);
919
+ if (id === 0n) {
920
+ throw new Error(`Invalid concept ID: ${value}`);
921
+ }
922
+
923
+ switch (op) {
924
+ case '=': {
925
+ filterContext.filters.push(this.sct.filterModuleId(id));
926
+ return null;
927
+ }
928
+ default:
929
+ throw new Error(`Unsupported filter operation: moduleId ${op} ${value}`);
855
930
  }
856
931
  }
857
932
 
@@ -862,6 +937,24 @@ class SnomedProvider extends BaseCSServices {
862
937
  return null;
863
938
  }
864
939
 
940
+ const cid = this.sct.stringToIdOrZero(prop);
941
+ if (cid != 0) {
942
+
943
+ const id = this.sct.stringToIdOrZero(value);
944
+ if (id === 0n) {
945
+ throw new Error(`Invalid concept ID: ${value}`);
946
+ }
947
+
948
+ switch (op) {
949
+ case '=': {
950
+ filterContext.filters.push(this.sct.filterByProperty(cid, id));
951
+ return null;
952
+ }
953
+ default:
954
+ throw new Error(`Unsupported filter operation: ${prop} ${op} ${value}`);
955
+ }
956
+ }
957
+
865
958
  throw new Error(`Unsupported filter property: ${prop}`);
866
959
  }
867
960
 
@@ -893,7 +986,7 @@ class SnomedProvider extends BaseCSServices {
893
986
 
894
987
  async filterMore(filterContext, set) {
895
988
  set.cursor = set.cursor || 0;
896
-
989
+ this.#ensurePopulated(set);
897
990
  const size = await this.filterSize(filterContext, set);
898
991
  return set.cursor < size;
899
992
  }
@@ -927,12 +1020,27 @@ class SnomedProvider extends BaseCSServices {
927
1020
  }
928
1021
 
929
1022
  const ctxt = conceptResult.context;
930
-
931
-
932
1023
  const reference = ctxt.getReference();
933
1024
  let found = false;
934
1025
 
935
- if (set.matches && set.matches.length > 0) {
1026
+ if (set.inactive !== undefined) {
1027
+ let concept = this.sct.concepts.getConcept(reference);
1028
+ let active = (concept.flags & 0x0F) === 0;
1029
+ found = active !== set.inactive
1030
+ } else if (set.moduleId) {
1031
+ let concept = this.sct.concepts.getConcept(reference);
1032
+ let moduleId = this.sct.concepts.getModuleId(concept.index);
1033
+ found = moduleId === set.moduleId;
1034
+ } else if (set.propProp || set.propValue) {
1035
+ found = false;
1036
+ const relationships = this.sct.getConceptRelationships(reference);
1037
+ for (let relationshipRef of relationships) {
1038
+ const relationship = this.sct.relationships.getRelationship(relationshipRef);
1039
+ if (set.propProp === relationship.relType && set.propValue === relationship.target) {
1040
+ found = true;
1041
+ }
1042
+ }
1043
+ } else if (set.matches && set.matches.length > 0) {
936
1044
  found = set.matches.some(m => m.index === reference);
937
1045
  } else if (set.members && set.members.length > 0) {
938
1046
  found = set.members.some(m => m.ref === reference);
@@ -958,6 +1066,23 @@ class SnomedProvider extends BaseCSServices {
958
1066
  }
959
1067
 
960
1068
  const reference = concept.getReference();
1069
+ if (set.inactive !== undefined) {
1070
+ return this.sct.isActive(reference) !== set.inactive;
1071
+ }
1072
+
1073
+ if (set.moduleId) {
1074
+ return this.sct.concepts.getModuleId(reference) === set.moduleId;
1075
+ }
1076
+
1077
+ if (set.propProp || set.propValue) {
1078
+ const relationships = this.sct.getConceptRelationships(reference);
1079
+ for (let relationshipRef of relationships) {
1080
+ const relationship = this.sct.relationships.getRelationship(relationshipRef);
1081
+ if (set.propProp === relationship.relType && set.propValue === relationship.target) {
1082
+ return true;
1083
+ }
1084
+ }
1085
+ }
961
1086
 
962
1087
  if (set.matches && set.matches.length > 0) {
963
1088
  return set.matches.some(m => m.index === reference);
@@ -970,6 +1095,42 @@ class SnomedProvider extends BaseCSServices {
970
1095
  return false;
971
1096
  }
972
1097
 
1098
+ #ensurePopulated(set) {
1099
+ if (set.populationDone) {
1100
+ return;
1101
+ }
1102
+ if (set.inactive !== undefined && set.descendants.length === 0) {
1103
+ for (let i = 0; i < this.sct.concepts.count(); i++) {
1104
+ let concept = this.sct.concepts.getConceptByCount(i);
1105
+ let active = (concept.flags & 0x0F) === 0;
1106
+ if (active !== set.inactive) {
1107
+ set.descendants.push(concept.index);
1108
+ }
1109
+ }
1110
+ }
1111
+ if (set.moduleId) {
1112
+ for (let i = 0; i < this.sct.concepts.count(); i++) {
1113
+ let concept = this.sct.concepts.getConceptByCount(i);
1114
+ let moduleId = this.sct.concepts.getModuleId(concept.index);
1115
+ if (moduleId === set.moduleId) {
1116
+ set.descendants.push(concept.index);
1117
+ }
1118
+ }
1119
+ }
1120
+ if (set.propProp || set.propValue) {
1121
+ for (let i = 0; i < this.sct.concepts.count(); i++) {
1122
+ let concept = this.sct.concepts.getConceptByCount(i);
1123
+ const relationships = this.sct.getConceptRelationships(concept.getReference());
1124
+ for (let relationshipRef of relationships) {
1125
+ const relationship = this.sct.relationships.getRelationship(relationshipRef);
1126
+ if (set.propProp === relationship.relType && set.propValue === relationship.target) {
1127
+ set.descendants.push(concept.index);
1128
+ }
1129
+ }
1130
+ }
1131
+ }
1132
+ set.populationDone = true;
1133
+ }
973
1134
 
974
1135
  // Search filter
975
1136
  async searchFilter(filterContext, filter, sort) {
@@ -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
- const stemmer = new SimpleStemmer();
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 = stemmer.stem(word.toLowerCase());
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
  }
@@ -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
- if (all || (this.canonicalMatches(vurl, g.source) && this.canonicalMatches(targetSystem, g.target) )) {
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._stem(word);
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._stem(word));
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;
@@ -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("2").style("background-color: #efefef").tx("(not mapped)");
1803
+ tr.td().setAttribute('colspan',"2").style("background-color: #efefef").tx("(not mapped)");
1804
1804
  } else if (nomapComment) {
1805
- tr.td().colspan("2").style("background-color: #efefef").tx("(not mapped)");
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("3").style("background-color: #efefef").tx("(not mapped)");
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(String(sourceColCount + 1)).b().tx(this.translate('CONC_MAP_SRC_DET'));
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(String(targetColCount + 1)).b().tx(this.translate('CONC_MAP_TRGT_DET'));
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(String(Object.keys(props).length)).b().tx(this.translate('GENERAL_PROPS'));
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("3").style("background-color: #efefef").tx("(not mapped)");
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("4").style("background-color: #efefef").tx("(not mapped)");
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("2");
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') + " ");