fhirsmith 0.3.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.
Files changed (277) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/FHIRsmith.png +0 -0
  3. package/README.md +277 -0
  4. package/config-template.json +144 -0
  5. package/library/folder-setup.js +58 -0
  6. package/library/html-server.js +166 -0
  7. package/library/html.js +835 -0
  8. package/library/i18nsupport.js +259 -0
  9. package/library/languages.js +779 -0
  10. package/library/logger-telnet.js +205 -0
  11. package/library/logger.js +279 -0
  12. package/library/package-manager.js +876 -0
  13. package/library/utilities.js +196 -0
  14. package/library/version-utilities.js +1056 -0
  15. package/npmprojector/config-example.json +13 -0
  16. package/npmprojector/indexer.js +394 -0
  17. package/npmprojector/npmprojector.js +395 -0
  18. package/npmprojector/readme.md +174 -0
  19. package/npmprojector/watcher.js +335 -0
  20. package/package.json +119 -0
  21. package/packages/package-crawler.js +846 -0
  22. package/packages/packages-template.html +126 -0
  23. package/packages/packages.js +2838 -0
  24. package/passwords.ini +2 -0
  25. package/publisher/publisher-template.html +208 -0
  26. package/publisher/publisher.js +2167 -0
  27. package/publisher/task-draft.js +458 -0
  28. package/registry/api.js +735 -0
  29. package/registry/crawler.js +637 -0
  30. package/registry/model.js +513 -0
  31. package/registry/readme.md +243 -0
  32. package/registry/registry-data.json +121015 -0
  33. package/registry/registry-template.html +126 -0
  34. package/registry/registry.js +1395 -0
  35. package/registry/test-runner.js +237 -0
  36. package/root-template.html +124 -0
  37. package/server.js +524 -0
  38. package/shl/private-key.pem +5 -0
  39. package/shl/public-key.pem +18 -0
  40. package/shl/shl.js +1125 -0
  41. package/shl/vhl.js +69 -0
  42. package/static/FHIRsmith128.png +0 -0
  43. package/static/FHIRsmith16.png +0 -0
  44. package/static/FHIRsmith32.png +0 -0
  45. package/static/FHIRsmith64.png +0 -0
  46. package/static/assets/css/bootstrap-fhir.css +5302 -0
  47. package/static/assets/css/bootstrap-glyphicons.css +2 -0
  48. package/static/assets/css/bootstrap.css +4097 -0
  49. package/static/assets/css/jquery-ui.css +523 -0
  50. package/static/assets/css/jquery-ui.structure.css +863 -0
  51. package/static/assets/css/jquery-ui.structure.min.css +5 -0
  52. package/static/assets/css/jquery-ui.theme.css +439 -0
  53. package/static/assets/css/jquery-ui.theme.min.css +5 -0
  54. package/static/assets/css/jquery.ui.all.css +7 -0
  55. package/static/assets/css/modules.css +18 -0
  56. package/static/assets/css/project.css +367 -0
  57. package/static/assets/css/pygments-manni.css +66 -0
  58. package/static/assets/css/tags.css +74 -0
  59. package/static/assets/css/xml.css +2 -0
  60. package/static/assets/fonts/glyphiconshalflings-regular.eot +0 -0
  61. package/static/assets/fonts/glyphiconshalflings-regular.otf +0 -0
  62. package/static/assets/fonts/glyphiconshalflings-regular.svg +175 -0
  63. package/static/assets/fonts/glyphiconshalflings-regular.ttf +0 -0
  64. package/static/assets/fonts/glyphiconshalflings-regular.woff +0 -0
  65. package/static/assets/ico/apple-touch-icon-114-precomposed.png +0 -0
  66. package/static/assets/ico/apple-touch-icon-144-precomposed.png +0 -0
  67. package/static/assets/ico/apple-touch-icon-57-precomposed.png +0 -0
  68. package/static/assets/ico/apple-touch-icon-72-precomposed.png +0 -0
  69. package/static/assets/ico/favicon.ico +0 -0
  70. package/static/assets/ico/favicon.png +0 -0
  71. package/static/assets/images/fhir-logo-www.png +0 -0
  72. package/static/assets/images/fhir-logo.png +0 -0
  73. package/static/assets/images/hl7-logo.png +0 -0
  74. package/static/assets/images/logo_ansinew.jpg +0 -0
  75. package/static/assets/images/search.png +0 -0
  76. package/static/assets/images/stripe.png +0 -0
  77. package/static/assets/images/target.png +0 -0
  78. package/static/assets/images/tx-registry-root.gif +0 -0
  79. package/static/assets/images/tx-registry.png +0 -0
  80. package/static/assets/images/tx-server.png +0 -0
  81. package/static/assets/images/tx-version.png +0 -0
  82. package/static/assets/js/bootstrap.min.js +6 -0
  83. package/static/assets/js/fhir-gw.js +259 -0
  84. package/static/assets/js/fhir.js +2 -0
  85. package/static/assets/js/html5shiv.js +8 -0
  86. package/static/assets/js/jcookie.js +96 -0
  87. package/static/assets/js/jquery-ui.min.js +6 -0
  88. package/static/assets/js/jquery.js +10716 -0
  89. package/static/assets/js/jquery.min.js +2 -0
  90. package/static/assets/js/jquery.ui.core.js +314 -0
  91. package/static/assets/js/jquery.ui.draggable.js +825 -0
  92. package/static/assets/js/jquery.ui.mouse.js +162 -0
  93. package/static/assets/js/jquery.ui.resizable.js +842 -0
  94. package/static/assets/js/jquery.ui.widget.js +268 -0
  95. package/static/assets/js/json2.js +487 -0
  96. package/static/assets/js/jtip.js +97 -0
  97. package/static/assets/js/respond.min.js +6 -0
  98. package/static/assets/js/statuspage.js +70 -0
  99. package/static/assets/js/xml.js +2 -0
  100. package/static/dist/js/bootstrap.js +1964 -0
  101. package/static/favicon.png +0 -0
  102. package/static/fhir.css +626 -0
  103. package/static/icon-fhir-16.png +0 -0
  104. package/static/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
  105. package/static/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
  106. package/static/images/ui-bg_flat_10_000000_40x100.png +0 -0
  107. package/static/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
  108. package/static/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
  109. package/static/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  110. package/static/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
  111. package/static/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
  112. package/static/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
  113. package/static/images/ui-icons_222222_256x240.png +0 -0
  114. package/static/images/ui-icons_228ef1_256x240.png +0 -0
  115. package/static/images/ui-icons_ef8c08_256x240.png +0 -0
  116. package/static/images/ui-icons_ffd27a_256x240.png +0 -0
  117. package/static/images/ui-icons_ffffff_256x240.png +0 -0
  118. package/static/js/jquery.effects.blind.js +49 -0
  119. package/static/js/jquery.effects.bounce.js +78 -0
  120. package/static/js/jquery.effects.clip.js +54 -0
  121. package/static/js/jquery.effects.core.js +763 -0
  122. package/static/js/jquery.effects.drop.js +50 -0
  123. package/static/js/jquery.effects.explode.js +79 -0
  124. package/static/js/jquery.effects.fade.js +32 -0
  125. package/static/js/jquery.effects.fold.js +56 -0
  126. package/static/js/jquery.effects.highlight.js +50 -0
  127. package/static/js/jquery.effects.pulsate.js +51 -0
  128. package/static/js/jquery.effects.scale.js +178 -0
  129. package/static/js/jquery.effects.shake.js +57 -0
  130. package/static/js/jquery.effects.slide.js +50 -0
  131. package/static/js/jquery.effects.transfer.js +45 -0
  132. package/static/js/jquery.ui.accordion.js +611 -0
  133. package/static/js/jquery.ui.autocomplete.js +612 -0
  134. package/static/js/jquery.ui.button.js +416 -0
  135. package/static/js/jquery.ui.datepicker.js +1823 -0
  136. package/static/js/jquery.ui.dialog.js +878 -0
  137. package/static/js/jquery.ui.droppable.js +296 -0
  138. package/static/js/jquery.ui.position.js +252 -0
  139. package/static/js/jquery.ui.progressbar.js +109 -0
  140. package/static/js/jquery.ui.selectable.js +266 -0
  141. package/static/js/jquery.ui.slider.js +666 -0
  142. package/static/js/jquery.ui.sortable.js +1077 -0
  143. package/static/js/jquery.ui.tabs.js +758 -0
  144. package/stats.js +80 -0
  145. package/test-cache/vsac/vsac-valuesets.db +0 -0
  146. package/token/nginx_passport_setup.md +383 -0
  147. package/token/security_guide.md +294 -0
  148. package/token/token-template.html +330 -0
  149. package/token/token.js +1300 -0
  150. package/translations/Messages.properties +1510 -0
  151. package/translations/Messages_ar.properties +1399 -0
  152. package/translations/Messages_de.properties +836 -0
  153. package/translations/Messages_es.properties +737 -0
  154. package/translations/Messages_fr.properties +1 -0
  155. package/translations/Messages_ja.properties +893 -0
  156. package/translations/Messages_nl.properties +1357 -0
  157. package/translations/Messages_pt.properties +1302 -0
  158. package/translations/Messages_ru.properties +1 -0
  159. package/translations/Messages_uz.properties +1 -0
  160. package/translations/Messages_zh.properties +1 -0
  161. package/translations/rendering-phrases.properties +1128 -0
  162. package/translations/rendering-phrases_ar.properties +1091 -0
  163. package/translations/rendering-phrases_de.properties +6 -0
  164. package/translations/rendering-phrases_es.properties +6 -0
  165. package/translations/rendering-phrases_fr.properties +624 -0
  166. package/translations/rendering-phrases_ja.properties +21 -0
  167. package/translations/rendering-phrases_nl.properties +970 -0
  168. package/translations/rendering-phrases_pt.properties +1020 -0
  169. package/translations/rendering-phrases_ru.properties +1094 -0
  170. package/translations/rendering-phrases_uz.properties +1 -0
  171. package/translations/rendering-phrases_zh.properties +1 -0
  172. package/tx/README.md +418 -0
  173. package/tx/cm/cm-api.js +110 -0
  174. package/tx/cm/cm-database.js +735 -0
  175. package/tx/cm/cm-package.js +325 -0
  176. package/tx/cs/cs-api.js +789 -0
  177. package/tx/cs/cs-areacode.js +615 -0
  178. package/tx/cs/cs-country.js +1110 -0
  179. package/tx/cs/cs-cpt.js +785 -0
  180. package/tx/cs/cs-cs.js +1579 -0
  181. package/tx/cs/cs-currency.js +539 -0
  182. package/tx/cs/cs-db.js +1321 -0
  183. package/tx/cs/cs-hgvs.js +329 -0
  184. package/tx/cs/cs-lang.js +465 -0
  185. package/tx/cs/cs-loinc.js +1485 -0
  186. package/tx/cs/cs-mimetypes.js +238 -0
  187. package/tx/cs/cs-ndc.js +704 -0
  188. package/tx/cs/cs-omop.js +1025 -0
  189. package/tx/cs/cs-provider-api.js +43 -0
  190. package/tx/cs/cs-provider-list.js +37 -0
  191. package/tx/cs/cs-rxnorm.js +808 -0
  192. package/tx/cs/cs-snomed.js +1102 -0
  193. package/tx/cs/cs-ucum.js +514 -0
  194. package/tx/cs/cs-unii.js +271 -0
  195. package/tx/cs/cs-uri.js +218 -0
  196. package/tx/cs/cs-usstates.js +305 -0
  197. package/tx/dev.fhir.org.yml +14 -0
  198. package/tx/fixtures/test-cases-setup.json +18 -0
  199. package/tx/fixtures/test-cases.yml +16 -0
  200. package/tx/html/codesystem-operations.liquid +25 -0
  201. package/tx/html/home-metrics.liquid +247 -0
  202. package/tx/html/operations-form.liquid +148 -0
  203. package/tx/html/search-form.liquid +62 -0
  204. package/tx/html/tx-template.html +133 -0
  205. package/tx/html/valueset-operations.liquid +54 -0
  206. package/tx/importers/atc-to-fhir.js +316 -0
  207. package/tx/importers/import-loinc.module.js +1536 -0
  208. package/tx/importers/import-ndc.module.js +1088 -0
  209. package/tx/importers/import-rxnorm.module.js +898 -0
  210. package/tx/importers/import-sct.module.js +2457 -0
  211. package/tx/importers/import-unii.module.js +601 -0
  212. package/tx/importers/readme.md +453 -0
  213. package/tx/importers/subset-loinc.module.js +1081 -0
  214. package/tx/importers/subset-rxnorm.module.js +938 -0
  215. package/tx/importers/tx-import-base.js +351 -0
  216. package/tx/importers/tx-import-settings.js +310 -0
  217. package/tx/importers/tx-import.js +357 -0
  218. package/tx/library/canonical-resource.js +88 -0
  219. package/tx/library/capabilitystatement.js +292 -0
  220. package/tx/library/codesystem.js +774 -0
  221. package/tx/library/conceptmap.js +568 -0
  222. package/tx/library/designations.js +932 -0
  223. package/tx/library/errors.js +77 -0
  224. package/tx/library/extensions.js +117 -0
  225. package/tx/library/namingsystem.js +322 -0
  226. package/tx/library/operation-outcome.js +127 -0
  227. package/tx/library/parameters.js +105 -0
  228. package/tx/library/renderer.js +1559 -0
  229. package/tx/library/terminologycapabilities.js +418 -0
  230. package/tx/library/ucum-parsers.js +1029 -0
  231. package/tx/library/ucum-service.js +370 -0
  232. package/tx/library/ucum-types.js +1099 -0
  233. package/tx/library/valueset.js +543 -0
  234. package/tx/library.js +676 -0
  235. package/tx/ocl/cm-ocl.js +106 -0
  236. package/tx/ocl/cs-ocl.js +39 -0
  237. package/tx/ocl/vs-ocl.js +105 -0
  238. package/tx/operation-context.js +568 -0
  239. package/tx/params.js +613 -0
  240. package/tx/provider.js +403 -0
  241. package/tx/sct/ecl.js +1560 -0
  242. package/tx/sct/expressions.js +2077 -0
  243. package/tx/sct/structures.js +1396 -0
  244. package/tx/tx-html.js +1063 -0
  245. package/tx/tx.fhir.org.yml +39 -0
  246. package/tx/tx.js +927 -0
  247. package/tx/vs/vs-api.js +112 -0
  248. package/tx/vs/vs-database.js +786 -0
  249. package/tx/vs/vs-package.js +358 -0
  250. package/tx/vs/vs-vsac.js +366 -0
  251. package/tx/workers/batch-validate.js +129 -0
  252. package/tx/workers/batch.js +361 -0
  253. package/tx/workers/closure.js +32 -0
  254. package/tx/workers/expand.js +1845 -0
  255. package/tx/workers/lookup.js +407 -0
  256. package/tx/workers/metadata.js +467 -0
  257. package/tx/workers/operations.js +34 -0
  258. package/tx/workers/read.js +164 -0
  259. package/tx/workers/search.js +384 -0
  260. package/tx/workers/subsumes.js +334 -0
  261. package/tx/workers/translate.js +492 -0
  262. package/tx/workers/validate.js +2504 -0
  263. package/tx/workers/worker.js +904 -0
  264. package/tx/xml/capabilitystatement-xml.js +63 -0
  265. package/tx/xml/codesystem-xml.js +62 -0
  266. package/tx/xml/conceptmap-xml.js +65 -0
  267. package/tx/xml/namingsystem-xml.js +65 -0
  268. package/tx/xml/operationoutcome-xml.js +127 -0
  269. package/tx/xml/parameters-xml.js +312 -0
  270. package/tx/xml/terminologycapabilities-xml.js +64 -0
  271. package/tx/xml/valueset-xml.js +64 -0
  272. package/tx/xml/xml-base.js +603 -0
  273. package/vcl/vcl-parser.js +1098 -0
  274. package/vcl/vcl.js +253 -0
  275. package/windows-install.js +19 -0
  276. package/xig/xig-template.html +124 -0
  277. package/xig/xig.js +3049 -0
@@ -0,0 +1,384 @@
1
+ //
2
+ // Search Worker - Handles resource search operations
3
+ //
4
+ // GET /{type}?{params}
5
+ // POST /{type}/_search
6
+ //
7
+
8
+ const { TerminologyWorker } = require('./worker');
9
+
10
+ class SearchWorker extends TerminologyWorker {
11
+ /**
12
+ * @param {OperationContext} opContext - Operation context
13
+ * @param {Logger} log - Logger instance
14
+ * @param {Provider} provider - Provider for code systems and resources
15
+ * @param {LanguageDefinitions} languages - Language definitions
16
+ * @param {I18nSupport} i18n - Internationalization support
17
+ */
18
+ constructor(opContext, log, provider, languages, i18n) {
19
+ super(opContext, log, provider, languages, i18n);
20
+ }
21
+
22
+ /**
23
+ * Get operation name
24
+ * @returns {string}
25
+ */
26
+ opName() {
27
+ return 'search';
28
+ }
29
+
30
+
31
+ // Allowed search parameters
32
+ static ALLOWED_PARAMS = [
33
+ '_offset', '_count', '_elements', '_sort',
34
+ 'url', 'version', 'content-mode', 'date', 'description',
35
+ 'supplements', 'identifier', 'jurisdiction', 'name',
36
+ 'publisher', 'status', 'system', 'title', 'text'
37
+ ];
38
+
39
+ // Sortable fields
40
+ static SORT_FIELDS = ['id', 'url', 'version', 'date', 'name', 'vurl'];
41
+
42
+ /**
43
+ * Handle a search request
44
+ * @param {express.Request} req - Express request (with txProvider attached)
45
+ * @param {express.Response} res - Express response
46
+ * @param {string} resourceType - The resource type (CodeSystem, ValueSet, ConceptMap)
47
+ * @param {Object} log - Logger instance
48
+ */
49
+ async handle(req, res, resourceType) {
50
+ const params = req.method === 'POST' ? req.body : req.query;
51
+
52
+ this.log.debug(`Search ${resourceType} with params:`, params);
53
+
54
+ try {
55
+ // Parse pagination parameters
56
+ const offset = Math.max(0, parseInt(params._offset) || 0);
57
+ const elements = params._elements ? decodeURIComponent(params._elements).split(',').map(e => e.trim()) : null;
58
+ const count = Math.min(elements ? 2000 : 200, Math.max(1, parseInt(params._count) || 20));
59
+ const sort = params._sort || "id";
60
+
61
+ // Get matching resources
62
+ let matches = [];
63
+ switch (resourceType) {
64
+ case 'CodeSystem':
65
+ matches = this.searchCodeSystems(params);
66
+ break;
67
+
68
+ case 'ValueSet':
69
+ matches = await this.searchValueSets(params, elements);
70
+ break;
71
+
72
+ case 'ConceptMap':
73
+ // Not implemented yet - return empty set
74
+ matches = [];
75
+ break;
76
+
77
+ default:
78
+ matches = [];
79
+ }
80
+
81
+ // Sort results
82
+ matches = this.sortResults(matches, sort);
83
+
84
+ // Build and return the bundle
85
+ const bundle = this.buildSearchBundle(
86
+ req, resourceType, matches, offset, count, elements
87
+ );
88
+ req.logInfo = `${bundle.entry.length} matches`;
89
+ return res.json(bundle);
90
+
91
+ } catch (error) {
92
+ req.logInfo = "error "+(error.msgId || error.className);
93
+ this.log.error(error);
94
+ return res.status(500).json({
95
+ resourceType: 'OperationOutcome',
96
+ issue: [{
97
+ severity: 'error',
98
+ code: 'exception',
99
+ diagnostics: error.message
100
+ }]
101
+ });
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Search CodeSystems
107
+ */
108
+ searchCodeSystems(params) {
109
+ const matches = [];
110
+
111
+ // Extract search parameters (excluding special params)
112
+ const searchParams = {};
113
+ for (const [key, value] of Object.entries(params)) {
114
+ if (!key.startsWith('_') && value && SearchWorker.ALLOWED_PARAMS.includes(key)) {
115
+ searchParams[key] = value.toLowerCase();
116
+ }
117
+ }
118
+
119
+ // If no search params, return all
120
+ const hasSearchParams = Object.keys(searchParams).length > 0;
121
+
122
+ for (const [key, cs] of this.provider.codeSystems) {
123
+ this.deadCheck('searchCodeSystems');
124
+ if (key == cs.vurl) {
125
+ const json = cs.jsonObj;
126
+
127
+ if (!hasSearchParams) {
128
+ matches.push(json);
129
+ continue;
130
+ }
131
+
132
+ // Check each search parameter for partial match
133
+ let isMatch = true;
134
+ for (const [param, searchValue] of Object.entries(searchParams)) {
135
+ // 'system' doesn't do anything for CodeSystem search
136
+ if (param === 'system') {
137
+ continue;
138
+ }
139
+
140
+ // Map content-mode to content property
141
+ const jsonProp = param === 'content-mode' ? 'content' : param;
142
+
143
+ if (param === 'jurisdiction') {
144
+ // Special handling for jurisdiction - array of CodeableConcept
145
+ if (!this.matchJurisdiction(json.jurisdiction, searchValue)) {
146
+ isMatch = false;
147
+ break;
148
+ }
149
+ } else if (param === 'text') {
150
+ const propValue = json.title + json.description;
151
+ if (!this.matchValue(propValue, searchValue)) {
152
+ isMatch = false;
153
+ break;
154
+ }
155
+ } else {
156
+ // Standard partial text match
157
+ const propValue = json[jsonProp];
158
+ if (!this.matchValue(propValue, searchValue)) {
159
+ isMatch = false;
160
+ break;
161
+ }
162
+ }
163
+ }
164
+
165
+ if (isMatch) {
166
+ matches.push(json);
167
+ }
168
+ }
169
+ }
170
+
171
+ return matches;
172
+ }
173
+
174
+ /**
175
+ * Search ValueSets by delegating to providers
176
+ */
177
+ async searchValueSets(params, elements) {
178
+ const allMatches = [];
179
+
180
+ // Convert params object to array format expected by ValueSet providers
181
+ // Exclude control params (_offset, _count, _elements, _sort)
182
+ const searchParams = [];
183
+ for (const [key, value] of Object.entries(params)) {
184
+ if (!key.startsWith('_') && value && SearchWorker.ALLOWED_PARAMS.includes(key)) {
185
+ searchParams.push({ name: key, value: value });
186
+ }
187
+ }
188
+
189
+ for (const vsp of this.provider.valueSetProviders) {
190
+ this.deadCheck('searchValueSets-providers');
191
+ const results = await vsp.searchValueSets(searchParams, elements);
192
+ if (results && Array.isArray(results)) {
193
+ for (const vs of results) {
194
+ this.deadCheck('searchValueSets-results');
195
+ allMatches.push(vs.jsonObj || vs);
196
+ }
197
+ }
198
+ }
199
+
200
+ return allMatches;
201
+ }
202
+
203
+ /**
204
+ * Check if a value matches the search term (partial, case-insensitive)
205
+ */
206
+ matchValue(propValue, searchValue) {
207
+ if (propValue === undefined || propValue === null) {
208
+ return false;
209
+ }
210
+
211
+ const strValue = String(propValue).toLowerCase();
212
+ return strValue.includes(searchValue);
213
+ }
214
+
215
+ /**
216
+ * Check if jurisdiction matches - jurisdiction is an array of CodeableConcept
217
+ */
218
+ matchJurisdiction(jurisdictions, searchValue) {
219
+ if (!jurisdictions || !Array.isArray(jurisdictions)) {
220
+ return false;
221
+ }
222
+
223
+ for (const cc of jurisdictions) {
224
+ // Check coding array
225
+ if (cc.coding && Array.isArray(cc.coding)) {
226
+ for (const coding of cc.coding) {
227
+ if (coding.code && coding.code.toLowerCase().includes(searchValue)) {
228
+ return true;
229
+ }
230
+ if (coding.display && coding.display.toLowerCase().includes(searchValue)) {
231
+ return true;
232
+ }
233
+ }
234
+ }
235
+ // Check text
236
+ if (cc.text && cc.text.toLowerCase().includes(searchValue)) {
237
+ return true;
238
+ }
239
+ }
240
+
241
+ return false;
242
+ }
243
+
244
+ /**
245
+ * Sort results by the specified field
246
+ */
247
+ sortResults(results, sortField) {
248
+ if (!SearchWorker.SORT_FIELDS.includes(sortField)) {
249
+ return results;
250
+ }
251
+
252
+ return results.sort((a, b) => {
253
+ if (sortField === 'vurl') {
254
+ // Sort by url then version
255
+ const urlCompare = (a.url || '').localeCompare(b.url || '');
256
+ if (urlCompare !== 0) return urlCompare;
257
+ return (a.version || '').localeCompare(b.version || '');
258
+ }
259
+
260
+ const aVal = a[sortField] || '';
261
+ const bVal = b[sortField] || '';
262
+ return String(aVal).localeCompare(String(bVal));
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Build a FHIR search Bundle with pagination
268
+ */
269
+ buildSearchBundle(req, resourceType, allMatches, offset, count, elements) {
270
+ const total = allMatches.length;
271
+
272
+ // Get the slice for this page
273
+ const pageResults = allMatches.slice(offset, offset + count);
274
+
275
+ // Build base URL for pagination links
276
+ const protocol = req.protocol;
277
+ const host = req.get('host');
278
+ const basePath = req.baseUrl + req.path;
279
+ const baseUrl = `${protocol}://${host}${basePath}`;
280
+
281
+ // Preserve search params for pagination links (excluding _offset)
282
+ const searchParams = new URLSearchParams();
283
+ const params = req.method === 'POST' ? req.body : req.query;
284
+ for (const [key, value] of Object.entries(params)) {
285
+ if (key !== '_offset' && value) {
286
+ searchParams.set(key, value);
287
+ }
288
+ }
289
+
290
+ // Build pagination links
291
+ const links = [];
292
+
293
+ // Self link
294
+ const selfParams = new URLSearchParams(searchParams);
295
+ selfParams.set('_offset', offset);
296
+ links.push({
297
+ relation: 'self',
298
+ url: `${baseUrl}?${selfParams.toString()}`
299
+ });
300
+
301
+ // First link
302
+ const firstParams = new URLSearchParams(searchParams);
303
+ firstParams.set('_offset', 0);
304
+ links.push({
305
+ relation: 'first',
306
+ url: `${baseUrl}?${firstParams.toString()}`
307
+ });
308
+
309
+ // Previous link (if not on first page)
310
+ if (offset > 0) {
311
+ const prevParams = new URLSearchParams(searchParams);
312
+ prevParams.set('_offset', Math.max(0, offset - count));
313
+ links.push({
314
+ relation: 'previous',
315
+ url: `${baseUrl}?${prevParams.toString()}`
316
+ });
317
+ }
318
+
319
+ // Next link (if more results)
320
+ if (offset + count < total) {
321
+ const nextParams = new URLSearchParams(searchParams);
322
+ nextParams.set('_offset', offset + count);
323
+ links.push({
324
+ relation: 'next',
325
+ url: `${baseUrl}?${nextParams.toString()}`
326
+ });
327
+ }
328
+
329
+ // Last link
330
+ const lastOffset = Math.max(0, Math.floor((total - 1) / count) * count);
331
+ const lastParams = new URLSearchParams(searchParams);
332
+ lastParams.set('_offset', lastOffset);
333
+ links.push({
334
+ relation: 'last',
335
+ url: `${baseUrl}?${lastParams.toString()}`
336
+ });
337
+
338
+ // Build entries
339
+ const entries = pageResults.map(resource => {
340
+ // Apply _elements filter if specified
341
+ let filteredResource = resource;
342
+ if (elements) {
343
+ filteredResource = this.filterElements(resource, elements);
344
+ }
345
+
346
+ return {
347
+ fullUrl: `${protocol}://${host}${req.baseUrl}/${resourceType}/${resource.id}`,
348
+ resource: filteredResource,
349
+ search: {
350
+ mode: 'match'
351
+ }
352
+ };
353
+ });
354
+
355
+ return {
356
+ resourceType: 'Bundle',
357
+ type: 'searchset',
358
+ total: total,
359
+ link: links,
360
+ entry: entries
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Filter resource to only include specified elements
366
+ */
367
+ filterElements(resource, elements) {
368
+ // Always include resourceType and id
369
+ const filtered = {
370
+ resourceType: resource.resourceType,
371
+ id: resource.id
372
+ };
373
+
374
+ for (const element of elements) {
375
+ if (resource[element] !== undefined) {
376
+ filtered[element] = resource[element];
377
+ }
378
+ }
379
+
380
+ return filtered;
381
+ }
382
+ }
383
+
384
+ module.exports = SearchWorker;
@@ -0,0 +1,334 @@
1
+ //
2
+ // Subsumes Worker - Handles CodeSystem $subsumes operation
3
+ //
4
+ // GET /CodeSystem/$subsumes?{params}
5
+ // POST /CodeSystem/$subsumes
6
+ // GET /CodeSystem/{id}/$subsumes?{params}
7
+ // POST /CodeSystem/{id}/$subsumes
8
+ //
9
+
10
+ const { TerminologyWorker } = require('./worker');
11
+ const { FhirCodeSystemProvider } = require('../cs/cs-cs');
12
+ const {TxParameters} = require("../params");
13
+ const {Parameters} = require("../library/parameters");
14
+ const {Issue, OperationOutcome} = require("../library/operation-outcome");
15
+ class SubsumesWorker extends TerminologyWorker {
16
+ /**
17
+ * @param {OperationContext} opContext - Operation context
18
+ * @param {Logger} log - Logger instance
19
+ * @param {Provider} provider - Provider for code systems and resources
20
+ * @param {LanguageDefinitions} languages - Language definitions
21
+ * @param {I18nSupport} i18n - Internationalization support
22
+ */
23
+ constructor(opContext, log, provider, languages, i18n) {
24
+ super(opContext, log, provider, languages, i18n);
25
+ }
26
+
27
+ /**
28
+ * Get operation name
29
+ * @returns {string}
30
+ */
31
+ opName() {
32
+ return 'subsumes';
33
+ }
34
+
35
+ /**
36
+ * Handle a type-level $subsumes request
37
+ * GET/POST /CodeSystem/$subsumes
38
+ * @param {express.Request} req - Express request
39
+ * @param {express.Response} res - Express response
40
+ */
41
+ async handle(req, res) {
42
+ try {
43
+ await this.handleTypeLevelSubsumes(req, res);
44
+ } catch (error) {
45
+ req.logInfo = "error "+(error.msgId || error.className);
46
+ this.log.error(error);
47
+ if (error instanceof Issue) {
48
+ let oo = new OperationOutcome();
49
+ oo.addIssue(error);
50
+ return res.status(error.statusCode || 500).json(oo.jsonObj);
51
+ } else {
52
+ return res.status(error.statusCode || 500).json(this.operationOutcome(
53
+ 'error', error.issueCode || 'exception', error.message));
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Handle an instance-level $subsumes request
60
+ * GET/POST /CodeSystem/{id}/$subsumes
61
+ * @param {express.Request} req - Express request
62
+ * @param {express.Response} res - Express response
63
+ */
64
+ async handleInstance(req, res) {
65
+ try {
66
+ await this.handleInstanceLevelSubsumes(req, res);
67
+ } catch (error) {
68
+ this.log.error(error);
69
+ if (error instanceof Issue) {
70
+ let oo = new OperationOutcome();
71
+ oo.addIssue(error);
72
+ return res.status(error.statusCode || 500).json(oo.jsonObj);
73
+ } else {
74
+ return res.status(error.statusCode || 500).json(this.operationOutcome(
75
+ 'error', error.issueCode || 'exception', error.message));
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Handle type-level subsumes: /CodeSystem/$subsumes
82
+ * CodeSystem identified by system+version params or from codingA/codingB
83
+ */
84
+ async handleTypeLevelSubsumes(req, res) {
85
+ this.deadCheck('subsumes-type-level');
86
+
87
+ // Handle tx-resource and cache-id parameters from Parameters resource
88
+ if (req.body && req.body.resourceType === 'Parameters') {
89
+ this.setupAdditionalResources(req.body);
90
+ }
91
+
92
+ // Parse parameters from request
93
+ const params = new Parameters(this.parseParameters(req));
94
+ const txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n);
95
+ txp.readParams(params.jsonObj);
96
+
97
+ // Get the codings and code system provider
98
+ let codingA, codingB;
99
+ let csProvider;
100
+
101
+ if (params.has('codingA') && params.has('codingB')) {
102
+ // Using codingA and codingB (only from Parameters resource)
103
+ codingA = params.get('codingA');
104
+ codingB = params.get('codingB');
105
+
106
+ // Codings must have the same system
107
+ if (codingA.system !== codingB.system) {
108
+ throw new Issue('error', 'not-found', null, null, 'codingA and codingB must have the same system', null, 400);
109
+ }
110
+ // Get the code system provider from the coding's system
111
+ csProvider = await this.findCodeSystem(codingA.system, codingA.version || '', txp, ['complete'], null, false);
112
+ this.seeSourceProvider(csProvider, codingA.system);
113
+ } else if (params.has('codeA') && params.has('codeB')) {
114
+ // Using codeA, codeB - system is required
115
+ if (!params.has('system')) {
116
+ throw new Issue('error', 'not-found', null, null, 'system parameter is required when using codeA and codeB', null, 404);
117
+ }
118
+
119
+ csProvider = await this.findCodeSystem(params.get('system'), params.get('version') || '', txp, ['complete'], null, false);
120
+ this.seeSourceProvider(csProvider, params.get('system'));
121
+ // Create codings from the codes
122
+ codingA = {
123
+ system: csProvider.system(),
124
+ version: csProvider.version(),
125
+ code: params.get('codeA')
126
+ };
127
+ codingB = {
128
+ system: csProvider.system(),
129
+ version: csProvider.version(),
130
+ code: params.get('codeB')
131
+ };
132
+
133
+ } else {
134
+ throw new Issue('error', 'invalid', null, null, 'Must provide either codingA and codingB, or codeA and codeB with system', null, 400);
135
+ }
136
+
137
+ // Perform the subsumes check
138
+ const result = await this.doSubsumes(csProvider, codingA, codingB);
139
+ req.logInfo = this.usedSources.join("|")+txp.logInfo();
140
+ return res.status(200).json(result);
141
+ }
142
+
143
+ /**
144
+ * Handle instance-level subsumes: /CodeSystem/{id}/$subsumes
145
+ * CodeSystem identified by resource ID
146
+ */
147
+ async handleInstanceLevelSubsumes(req, res) {
148
+ this.deadCheck('subsumes-instance-level');
149
+
150
+ const { id } = req.params;
151
+
152
+ // Find the CodeSystem by ID
153
+ const codeSystem = await this.provider.getCodeSystemById(this.opContext, id);
154
+
155
+ if (!codeSystem) {
156
+ throw new Issue('error', 'not found', null, null, `CodeSystem/${id} not found`, null, 404);
157
+ }
158
+
159
+ // Handle tx-resource and cache-id parameters from Parameters resource
160
+ if (req.body && req.body.resourceType === 'Parameters') {
161
+ this.setupAdditionalResources(req.body);
162
+ }
163
+
164
+ // Parse parameters from request
165
+ const params = new Parameters(this.parseParameters(req));
166
+ const txp = new TxParameters(this.opContext.i18n.languageDefinitions, this.opContext.i18n);
167
+ txp.readParams(params.jsonObj);
168
+
169
+ // Load any supplements
170
+ const supplements = this.loadSupplements(codeSystem.url, codeSystem.version);
171
+
172
+ // Create a FhirCodeSystemProvider for this CodeSystem
173
+ const csProvider = new FhirCodeSystemProvider(this.opContext, codeSystem, supplements);
174
+
175
+ // Get the codings
176
+ let codingA, codingB;
177
+
178
+ if (params.has('codingA') && params.has('codingB')) {
179
+ codingA = params.get('codingA');
180
+ codingB = params.get('codingB');
181
+ } else if (params.has('codeA') && params.has('codeB')) {
182
+ // Create codings from the codes using this CodeSystem
183
+ codingA = {
184
+ system: csProvider.system(),
185
+ version: csProvider.version(),
186
+ code: params.get('codeA')
187
+ };
188
+ codingB = {
189
+ system: csProvider.system(),
190
+ version: csProvider.version(),
191
+ code: params.get('codeB')
192
+ };
193
+ } else {
194
+ throw new Issue('error', 'invalid', null, null, 'Must provide either codingA and codingB, or codeA and codeB with system', null, 400);
195
+ }
196
+
197
+ // Perform the subsumes check
198
+ const result = await this.doSubsumes(csProvider, codingA, codingB);
199
+ req.logInfo = this.usedSources.join("|")+txp.logInfo();
200
+ return res.json(result);
201
+ }
202
+ /**
203
+ * Parse parameters from request (query params, form body, or Parameters resource)
204
+ * Returns a FHIR Parameters resource
205
+ * @param {express.Request} req - Express request
206
+ * @returns {Object} FHIR Parameters resource
207
+ */
208
+ parseParameters(req) {
209
+ // Check if body is a Parameters resource
210
+ if (req.body && req.body.resourceType === 'Parameters') {
211
+ return req.body;
212
+ }
213
+
214
+ // Parse from query params or form body and convert to Parameters resource
215
+ const params = req.method === 'POST' ? req.body : req.query;
216
+ return this.simpleParamsToParametersResource(params);
217
+ }
218
+
219
+ /**
220
+ * Convert simple parameters (query string or form body) to a FHIR Parameters resource
221
+ * @param {Object} params - Query params or form body
222
+ * @returns {Object} FHIR Parameters resource
223
+ */
224
+ simpleParamsToParametersResource(params) {
225
+ const result = {
226
+ resourceType: 'Parameters',
227
+ parameter: []
228
+ };
229
+
230
+ if (!params) {
231
+ return result;
232
+ }
233
+
234
+ for (const [name, value] of Object.entries(params)) {
235
+ if (value === undefined || value === null) {
236
+ continue;
237
+ }
238
+
239
+ // Handle arrays (e.g., repeated query params)
240
+ if (Array.isArray(value)) {
241
+ for (const v of value) {
242
+ result.parameter.push({
243
+ name: name,
244
+ valueString: String(v)
245
+ });
246
+ }
247
+ } else {
248
+ result.parameter.push({
249
+ name: name,
250
+ valueString: String(value)
251
+ });
252
+ }
253
+ }
254
+
255
+ return result;
256
+ }
257
+
258
+ /**
259
+ * Perform the actual subsumes check
260
+ * @param {CodeSystemProvider} csProvider - CodeSystem provider
261
+ * @param {Object} codingA - First coding
262
+ * @param {Object} codingB - Second coding
263
+ * @returns {Object} Parameters resource with subsumes result
264
+ */
265
+ async doSubsumes(csProvider, codingA, codingB) {
266
+ this.deadCheck('doSubsumes');
267
+
268
+ const csSystem = csProvider.system();
269
+
270
+ // Check system uri matches for both codings
271
+ if (csSystem !== codingA.system) {
272
+ const error = new Error(`System uri / code uri mismatch - not supported at this time (${csSystem}/${codingA.system})`);
273
+ error.statusCode = 400;
274
+ error.issueCode = 'not-supported';
275
+ throw error;
276
+ }
277
+ if (csSystem !== codingB.system) {
278
+ const error = new Error(`System uri / code uri mismatch - not supported at this time (${csSystem}/${codingB.system})`);
279
+ error.statusCode = 400;
280
+ error.issueCode = 'not-supported';
281
+ throw error;
282
+ }
283
+
284
+ // Validate both codes exist
285
+ const locateA = await csProvider.locate(codingA.code);
286
+ if (!locateA || !locateA.context) {
287
+ const error = new Error(`Invalid code: '${codingA.code}' not found in CodeSystem '${csSystem}'`);
288
+ error.statusCode = 404;
289
+ error.issueCode = 'not-found';
290
+ throw error;
291
+ }
292
+
293
+ const locateB = await csProvider.locate(codingB.code);
294
+ if (!locateB || !locateB.context) {
295
+ const error = new Error(`Invalid code: '${codingB.code}' not found in CodeSystem '${csSystem}'`);
296
+ error.statusCode = 404;
297
+ error.issueCode = 'not-found';
298
+ throw error;
299
+ }
300
+
301
+ // Determine the subsumption relationship
302
+ let outcome = await csProvider.subsumesTest(codingA.code, codingB.code);
303
+
304
+ return {
305
+ resourceType: 'Parameters',
306
+ parameter: [
307
+ {
308
+ name: 'outcome',
309
+ valueCode: outcome
310
+ }
311
+ ]
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Build an OperationOutcome
317
+ * @param {string} severity - error, warning, information
318
+ * @param {string} code - Issue code
319
+ * @param {string} message - Diagnostic message
320
+ * @returns {Object} OperationOutcome resource
321
+ */
322
+ operationOutcome(severity, code, message) {
323
+ return {
324
+ resourceType: 'OperationOutcome',
325
+ issue: [{
326
+ severity,
327
+ code,
328
+ diagnostics: message
329
+ }]
330
+ };
331
+ }
332
+ }
333
+
334
+ module.exports = SubsumesWorker;