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,1485 @@
1
+ const sqlite3 = require('sqlite3').verbose();
2
+ const assert = require('assert');
3
+ const { CodeSystem } = require('../library/codesystem');
4
+ const { Language, Languages} = require('../../library/languages');
5
+ const { CodeSystemProvider, CodeSystemFactoryProvider} = require('./cs-api');
6
+ const { validateOptionalParameter, validateArrayParameter} = require("../../library/utilities");
7
+
8
+ // Context kinds matching Pascal enum
9
+ const LoincProviderContextKind = {
10
+ CODE: 0, // lpckCode
11
+ PART: 1, // lpckPart
12
+ LIST: 2, // lpckList
13
+ ANSWER: 3 // lpckAnswer
14
+ };
15
+
16
+ class DescriptionCacheEntry {
17
+ constructor(display, lang, value, dtype) {
18
+ this.display = display;
19
+ this.lang = lang;
20
+ this.value = value;
21
+ this.dtype = dtype;
22
+ }
23
+ }
24
+
25
+ class LoincProviderContext {
26
+ constructor(key, kind, code, desc, status) {
27
+ this.key = key;
28
+ this.kind = kind;
29
+ this.code = code;
30
+ this.desc = desc;
31
+ this.status = status;
32
+ this.displays = []; // Array of DescriptionCacheEntry
33
+ this.children = null; // Will be Set of keys if this has children
34
+ }
35
+
36
+ addChild(key) {
37
+ if (!this.children) {
38
+ this.children = [];
39
+ }
40
+ this.children.push(key);
41
+ }
42
+ }
43
+
44
+ class LoincDisplay {
45
+ constructor(language, value) {
46
+ this.language = language;
47
+ this.value = value;
48
+ }
49
+ }
50
+
51
+ class LoincIteratorContext {
52
+ constructor(context, keys) {
53
+ this.context = context;
54
+ this.keys = keys || [];
55
+ this.current = 0;
56
+ this.total = this.keys.length;
57
+ }
58
+
59
+ more() {
60
+ return this.current < this.total;
61
+ }
62
+
63
+ next() {
64
+ this.current++;
65
+ }
66
+ }
67
+
68
+ class LoincFilterHolder {
69
+ constructor() {
70
+ this.keys = [];
71
+ this.cursor = 0;
72
+ this.lsql = '';
73
+ }
74
+
75
+ hasKey(key) {
76
+ // Binary search since keys should be sorted
77
+ let l = 0;
78
+ let r = this.keys.length - 1;
79
+ while (l <= r) {
80
+ const m = Math.floor((l + r) / 2);
81
+ if (this.keys[m] < key) {
82
+ l = m + 1;
83
+ } else if (this.keys[m] > key) {
84
+ r = m - 1;
85
+ } else {
86
+ return true;
87
+ }
88
+ }
89
+ return false;
90
+ }
91
+ }
92
+
93
+ class LoincPrep {
94
+ constructor() {
95
+ this.filters = [];
96
+ }
97
+ }
98
+
99
+ class LoincServices extends CodeSystemProvider {
100
+ constructor(opContext, supplements, db, sharedData) {
101
+ super(opContext, supplements);
102
+ this.db = db;
103
+
104
+ // Shared data from factory
105
+ this.langs = sharedData.langs;
106
+ this.codes = sharedData.codes;
107
+ this.codeList = sharedData.codeList;
108
+ this.allKeys = sharedData.allKeys;
109
+ this._version = sharedData._version;
110
+ this.root = sharedData.root;
111
+ this.firstCodeKey = sharedData.firstCodeKey;
112
+ this.relationships = sharedData.relationships;
113
+ this.propertyList = sharedData.propertyList;
114
+ this.statusKeys = sharedData.statusKeys;
115
+ this.statusCodes = sharedData.statusCodes;
116
+ }
117
+
118
+ close() {
119
+ if (this.db) {
120
+ this.db.close();
121
+ this.db = null;
122
+ }
123
+ }
124
+
125
+ // Metadata methods
126
+ system() {
127
+ return 'http://loinc.org';
128
+ }
129
+
130
+ version() {
131
+ return this._version;
132
+ }
133
+
134
+ name() {
135
+ return 'LOINC';
136
+ }
137
+
138
+ description() {
139
+ return 'LOINC';
140
+ }
141
+
142
+ async totalCount() {
143
+ return this.codes.size;
144
+ }
145
+
146
+ hasParents() {
147
+ return true; // LOINC has hierarchical relationships
148
+ }
149
+
150
+ hasAnyDisplays(languages) {
151
+ const langs = this._ensureLanguages(languages);
152
+
153
+ // Check supplements first
154
+ if (this._hasAnySupplementDisplays(langs)) {
155
+ return true;
156
+ }
157
+
158
+ // Check if any requested languages are available in LOINC data
159
+ for (const requestedLang of langs.languages) {
160
+ for (const [loincLangCode] of this.langs) {
161
+ const loincLang = new Language(loincLangCode);
162
+ if (loincLang.matchesForDisplay(requestedLang)) {
163
+ return true;
164
+ }
165
+ }
166
+ }
167
+
168
+ return super.hasAnyDisplays(langs);
169
+ }
170
+
171
+ // Core concept methods
172
+ async code(context) {
173
+
174
+ const ctxt = await this.#ensureContext(context);
175
+ return ctxt ? ctxt.code : null;
176
+ }
177
+
178
+ async display(context) {
179
+
180
+ const ctxt = await this.#ensureContext(context);
181
+ if (!ctxt) {
182
+ return null;
183
+ }
184
+
185
+ // Check supplements first
186
+ let disp = this._displayFromSupplements(ctxt.code);
187
+ if (disp) {
188
+ return disp;
189
+ }
190
+
191
+ // Use language-aware display logic
192
+ if (this.opContext.langs && !this.opContext.langs.isEnglishOrNothing()) {
193
+ const displays = await this.#getDisplaysForContext(ctxt, this.opContext.langs);
194
+
195
+ // Try to find exact language match
196
+ for (const lang of this.opContext.langs.langs) {
197
+ for (const display of displays) {
198
+ if (lang.matches(display.language, true)) {
199
+ return display.value;
200
+ }
201
+ }
202
+ }
203
+
204
+ // Try partial language match
205
+ for (const lang of this.opContext.langs.langs) {
206
+ for (const display of displays) {
207
+ if (lang.matches(display.language, false)) {
208
+ return display.value;
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ return ctxt.desc || '';
215
+ }
216
+
217
+ async definition(context) {
218
+ await this.#ensureContext(context);
219
+ return null; // LOINC doesn't provide definitions
220
+ }
221
+
222
+ async isAbstract(context) {
223
+ await this.#ensureContext(context);
224
+ return false; // LOINC codes are not abstract
225
+ }
226
+
227
+ async isInactive(context) {
228
+ await this.#ensureContext(context);
229
+ return context.status == 'DISCOURAGED'; // Handle via status if needed
230
+ }
231
+
232
+ async getStatus(context) {
233
+ await this.#ensureContext(context);
234
+ return context.status == 'NotStated' ? null : context.status; // Handle via status if needed
235
+ }
236
+
237
+ async isDeprecated(context) {
238
+ await this.#ensureContext(context);
239
+ return false; // Handle via status if needed
240
+ }
241
+
242
+ async designations(context, displays) {
243
+ const ctxt = await this.#ensureContext(context);
244
+ if (ctxt) {
245
+ // Add main display
246
+ displays.addDesignation(true, 'active', 'en-US', CodeSystem.makeUseForDisplay(), ctxt.desc.trim());
247
+
248
+ // Add cached designations — load into local array then assign atomically.
249
+ // This avoids duplication from concurrent pushes and allows retry if a prior load failed.
250
+ if (ctxt.displays.length === 0) {
251
+ const loaded = await this.#loadDesignationsForContext(ctxt);
252
+ if (ctxt.displays.length === 0) {
253
+ ctxt.displays = loaded;
254
+ }
255
+ }
256
+
257
+ for (const entry of ctxt.displays) {
258
+ let use = undefined;
259
+ if (entry.dtype) {
260
+ use = {
261
+ system: 'http://loinc.org',
262
+ code: entry.dtype,
263
+ display: entry.dtype
264
+ }
265
+ }
266
+ if (!use) {
267
+ use = entry.display ? CodeSystem.makeUseForDisplay() : null;
268
+ }
269
+ displays.addDesignation(false, 'active', entry.lang, use, entry.value.trim());
270
+ }
271
+
272
+ // Add supplement designations
273
+ this._listSupplementDesignations(ctxt.code, displays);
274
+ }
275
+
276
+ }
277
+
278
+ async extendLookup(ctxt, props, params) {
279
+ validateArrayParameter(props, 'props', String);
280
+ validateArrayParameter(params, 'params', Object);
281
+
282
+ if (typeof ctxt === 'string') {
283
+ const located = await this.locate(ctxt);
284
+ if (!located.context) {
285
+ throw new Error(located.message);
286
+ }
287
+ ctxt = located.context;
288
+ }
289
+
290
+ if (!(ctxt instanceof LoincProviderContext)) {
291
+ throw new Error('Invalid context for LOINC lookup');
292
+ }
293
+
294
+ // Run all property queries in parallel — they're independent reads on the same key
295
+ await Promise.all([
296
+ this.#addRelationshipProperties(ctxt, params),
297
+ this.#addConceptProperties(ctxt, params),
298
+ this.#addStatusProperty(ctxt, params),
299
+ this.#addRelatedNames(ctxt, params)
300
+ ]);
301
+ }
302
+
303
+ #getDesignationUse(kind) {
304
+ switch (kind) {
305
+ case LoincProviderContextKind.CODE:
306
+ return 'LONG_COMMON_NAME';
307
+ case LoincProviderContextKind.PART:
308
+ return 'PartDisplayName';
309
+ default:
310
+ return 'LONG_COMMON_NAME';
311
+ }
312
+ }
313
+
314
+ async #addRelationshipProperties(ctxt, params) {
315
+ return new Promise((resolve, reject) => {
316
+ const sql = `
317
+ SELECT RelationshipTypes.Description as Relationship, Codes.Code, Codes.Description as Value
318
+ FROM Relationships, RelationshipTypes, Codes
319
+ WHERE Relationships.SourceKey = ?
320
+ AND Relationships.RelationshipTypeKey = RelationshipTypes.RelationshipTypeKey
321
+ AND Relationships.TargetKey = Codes.CodeKey
322
+ `;
323
+
324
+ this.db.all(sql, [ctxt.key], (err, rows) => {
325
+ if (err) {
326
+ reject(err);
327
+ } else {
328
+ for (const row of rows) {
329
+ this.#addCodeProperty(params, 'property', row.Relationship, row.Code);
330
+ }
331
+ resolve();
332
+ }
333
+ });
334
+ });
335
+ }
336
+
337
+ async #addConceptProperties(ctxt, params) {
338
+ return new Promise((resolve, reject) => {
339
+ const sql = `
340
+ SELECT PropertyTypes.Description, PropertyValues.Value
341
+ FROM Properties, PropertyTypes, PropertyValues
342
+ WHERE Properties.CodeKey = ?
343
+ AND Properties.PropertyTypeKey = PropertyTypes.PropertyTypeKey
344
+ AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
345
+ `;
346
+
347
+ this.db.all(sql, [ctxt.key], (err, rows) => {
348
+ if (err) {
349
+ reject(err);
350
+ } else {
351
+ for (const row of rows) {
352
+ this.#addStringProperty(params, 'property', row.Description, row.Value);
353
+ }
354
+ resolve();
355
+ }
356
+ });
357
+ });
358
+ }
359
+
360
+ async #addStatusProperty(ctxt, params) {
361
+ return new Promise((resolve, reject) => {
362
+ const sql = 'SELECT StatusKey FROM Codes WHERE CodeKey = ? AND StatusKey != 0';
363
+
364
+ this.db.get(sql, [ctxt.key], (err, row) => {
365
+ if (err) {
366
+ reject(err);
367
+ } else if (row) {
368
+ const statusDesc = this.statusCodes.get(row.StatusKey.toString());
369
+ if (row.StatusKey && statusDesc) {
370
+ this.#addStringProperty(params, 'property', 'STATUS', statusDesc);
371
+ }
372
+ resolve();
373
+ } else {
374
+ resolve();
375
+ }
376
+ });
377
+ });
378
+ }
379
+
380
+ async #addRelatedNames(ctxt, params) {
381
+ const loaded = await this.#loadRelatedNames(ctxt);
382
+ for (let d of loaded) {
383
+ this.#addProperty(params, 'property', 'RELATEDNAMES2', d.value, d.lang);
384
+ }
385
+ }
386
+
387
+ async #addAllDesignations(ctxt, params) {
388
+ return new Promise((resolve, reject) => {
389
+ const sql = `
390
+ SELECT Languages.Code as Lang, DescriptionTypes.Description as DType, Descriptions.Value
391
+ FROM Descriptions, Languages, DescriptionTypes
392
+ WHERE Descriptions.CodeKey = ?
393
+ AND Descriptions.DescriptionTypeKey != 4
394
+ AND Descriptions.DescriptionTypeKey = DescriptionTypes.DescriptionTypeKey
395
+ AND Descriptions.LanguageKey = Languages.LanguageKey
396
+ `;
397
+
398
+ this.db.all(sql, [ctxt.key], (err, rows) => {
399
+ if (err) {
400
+ reject(err);
401
+ } else {
402
+ for (const row of rows) {
403
+ this.#addProperty(params, 'designation', row.dtype, row.value, row.lang);
404
+ }
405
+ resolve();
406
+ }
407
+ });
408
+ });
409
+ }
410
+
411
+ #addProperty(params, type, name, value, language = null) {
412
+
413
+ const property = {
414
+ name: type,
415
+ part: [
416
+ { name: 'code', valueCode: name },
417
+ { name: 'value', valueString: value }
418
+ ]
419
+ };
420
+
421
+ if (language) {
422
+ property.part.push({ name: 'language', valueCode: language });
423
+ }
424
+
425
+ params.push(property);
426
+ }
427
+
428
+ #addCodeProperty(params, type, name, value, language = null) {
429
+
430
+ const property = {
431
+ name: type,
432
+ part: [
433
+ { name: 'code', valueCode: name },
434
+ { name: 'value', valueCode: value }
435
+ ]
436
+ };
437
+
438
+ if (language) {
439
+ property.part.push({ name: 'language', valueCode: language });
440
+ }
441
+
442
+ params.push(property);
443
+ }
444
+
445
+ #addStringProperty(params, type, name, value, language = null) {
446
+
447
+ const property = {
448
+ name: type,
449
+ part: [
450
+ { name: 'code', valueCode: name },
451
+ { name: 'value', valueString: value }
452
+ ]
453
+ };
454
+
455
+ if (language) {
456
+ property.part.push({ name: 'language', valueCode: language });
457
+ }
458
+
459
+ params.push(property);
460
+ }
461
+
462
+ async #getDisplaysForContext(ctxt, langs) {
463
+ validateOptionalParameter(langs, "langs", Languages);
464
+ const displays = [new LoincDisplay('en-US', ctxt.desc)];
465
+
466
+ return new Promise((resolve, reject) => {
467
+ const sql = `
468
+ SELECT Languages.Code as Lang, Descriptions.Value
469
+ FROM Descriptions, Languages
470
+ WHERE Descriptions.CodeKey = ?
471
+ AND Descriptions.DescriptionTypeKey IN (1,2,5)
472
+ AND Descriptions.LanguageKey = Languages.LanguageKey
473
+ ORDER BY DescriptionTypeKey
474
+ `;
475
+
476
+ this.db.all(sql, [ctxt.key], (err, rows) => {
477
+ if (err) {
478
+ reject(err);
479
+ } else {
480
+ for (const row of rows) {
481
+ displays.push(new LoincDisplay(row.Lang, row.Value));
482
+ }
483
+
484
+ // Add supplement displays
485
+ this.#addSupplementDisplays(displays, ctxt.code);
486
+
487
+ resolve(displays);
488
+ }
489
+ });
490
+ });
491
+ }
492
+
493
+ #addSupplementDisplays(displays, code) {
494
+ if (this.supplements) {
495
+ for (const supplement of this.supplements) {
496
+ const concept = supplement.getConceptByCode(code);
497
+ if (concept) {
498
+ if (concept.display) {
499
+ displays.push(new LoincDisplay(supplement.jsonObj.language || 'en', concept.display));
500
+ }
501
+ if (concept.designation) {
502
+ for (const designation of concept.designation) {
503
+ const lang = designation.language || supplement.jsonObj.language || 'en';
504
+ displays.push(new LoincDisplay(lang, designation.value));
505
+ }
506
+ }
507
+ }
508
+ }
509
+ }
510
+ }
511
+
512
+ async #loadDesignationsForContext(ctxt) {
513
+ return new Promise((resolve, reject) => {
514
+ const sql = `
515
+ SELECT Languages.Code as Lang, DescriptionTypes.Description as DType, Descriptions.Value
516
+ FROM Descriptions, Languages, DescriptionTypes
517
+ WHERE Descriptions.CodeKey = ?
518
+ AND Descriptions.DescriptionTypeKey != 4
519
+ AND Descriptions.DescriptionTypeKey = DescriptionTypes.DescriptionTypeKey
520
+ AND Descriptions.LanguageKey = Languages.LanguageKey
521
+ `;
522
+
523
+ this.db.all(sql, [ctxt.key], (err, rows) => {
524
+ if (err) {
525
+ reject(err);
526
+ } else {
527
+ const results = [];
528
+ for (const row of rows) {
529
+ const isDisplay = row.DType === 'LONG_COMMON_NAME';
530
+ results.push(new DescriptionCacheEntry(isDisplay, row.Lang, row.Value, row.DType));
531
+ }
532
+ resolve(results);
533
+ }
534
+ });
535
+ });
536
+ }
537
+
538
+ async #loadRelatedNames(ctxt) {
539
+ return new Promise((resolve, reject) => {
540
+ const sql = `
541
+ SELECT Languages.Code as Lang, Descriptions.Value
542
+ FROM Descriptions, Languages
543
+ WHERE Descriptions.CodeKey = ?
544
+ AND Descriptions.DescriptionTypeKey = 4
545
+ AND Descriptions.LanguageKey = Languages.LanguageKey
546
+ `;
547
+
548
+ this.db.all(sql, [ctxt.key], (err, rows) => {
549
+ if (err) {
550
+ reject(err);
551
+ } else {
552
+ const results = [];
553
+ for (const row of rows) {
554
+ results.push(new DescriptionCacheEntry(false, row.Lang, row.Value, 'RELATEDNAMES2'));
555
+ }
556
+ resolve(results);
557
+ }
558
+ });
559
+ });
560
+ }
561
+
562
+ async #ensureContext(context) {
563
+ if (!context) {
564
+ return null;
565
+ }
566
+ if (typeof context === 'string') {
567
+ const ctxt = await this.locate(context);
568
+ if (!ctxt.context) {
569
+ throw new Error(ctxt.message);
570
+ } else {
571
+ return ctxt.context;
572
+ }
573
+ }
574
+ if (context instanceof LoincProviderContext) {
575
+ return context;
576
+ }
577
+ throw new Error("Unknown Type at #ensureContext: " + (typeof context));
578
+ }
579
+
580
+ // Lookup methods
581
+ async locate(code) {
582
+
583
+ assert(!code || typeof code === 'string', 'code must be string');
584
+ if (!code) return { context: null, message: 'Empty code' };
585
+
586
+ const context = this.codes.get(code);
587
+ if (context) {
588
+ return { context: context, message: null };
589
+ }
590
+
591
+ return { context: null, message: undefined };
592
+ }
593
+
594
+ // Iterator methods
595
+ async iterator(context) {
596
+
597
+
598
+ if (!context) {
599
+ // Iterate all codes starting from first code
600
+ return new LoincIteratorContext(null, this.allKeys);
601
+ } else {
602
+ const ctxt = await this.#ensureContext(context);
603
+ if (ctxt.kind === LoincProviderContextKind.PART && ctxt.children) {
604
+ return new LoincIteratorContext(ctxt, ctxt.children);
605
+ } else {
606
+ return new LoincIteratorContext(ctxt, []);
607
+ }
608
+ }
609
+ }
610
+
611
+ async nextContext(iteratorContext) {
612
+
613
+
614
+ if (!iteratorContext.more()) {
615
+ return null;
616
+ }
617
+
618
+ const key = iteratorContext.keys[iteratorContext.current];
619
+ iteratorContext.next();
620
+
621
+ return this.codeList[key];
622
+ }
623
+
624
+ // Filter support
625
+ async doesFilter(prop, op, value) {
626
+ // Relationship filters
627
+ if (this.relationships.has(prop) && ['=', 'in', 'exists', 'regex'].includes(op)) {
628
+ return true;
629
+ }
630
+
631
+ // Property filters
632
+ if (this.propertyList.has(prop) && ['=', 'in', 'exists', 'regex'].includes(op)) {
633
+ return true;
634
+ }
635
+
636
+ // Status filter
637
+ if (prop === 'STATUS' && op === '=' && this.statusKeys.has(value)) {
638
+ return true;
639
+ }
640
+
641
+ // LIST filter
642
+ if (prop === 'LIST' && op === '=' && this.codes.has(value)) {
643
+ return true;
644
+ }
645
+
646
+ // CLASSSTYPE filter
647
+ if (prop === 'CLASSTYPE' && op === '=' && ["1", "2", "3", "4"].includes(value)) {
648
+ return true;
649
+ }
650
+
651
+ // answers-for filter
652
+ if (prop === 'answers-for' && op === '=') {
653
+ return true;
654
+ }
655
+
656
+ // concept filters
657
+ if (prop === 'concept' && ['is-a', 'descendent-of', '=', 'in', 'not-in'].includes(op)) {
658
+ return true;
659
+ }
660
+
661
+ // code filters (VSAC workaround)
662
+ if (prop === 'code' && ['is-a', 'descendent-of', '='].includes(op)) {
663
+ return true;
664
+ }
665
+
666
+ // copyright filter
667
+ if (prop === 'copyright' && op === '=' && ['LOINC', '3rdParty'].includes(value)) {
668
+ return true;
669
+ }
670
+
671
+ return false;
672
+ }
673
+
674
+ async getPrepContext(iterate) {
675
+ return new LoincPrep(iterate);
676
+ }
677
+
678
+ async filter(filterContext, prop, op, value) {
679
+
680
+
681
+ const filter = new LoincFilterHolder();
682
+ await this.#executeFilterQuery(prop, op, value, filter);
683
+ filterContext.filters.push(filter);
684
+ }
685
+
686
+ async #executeFilterQuery(prop, op, value, filter) {
687
+ let sql = '';
688
+ let lsql = '';
689
+
690
+ // LIST filter
691
+ if (prop === 'LIST' && op === '=' && this.codes.has(value)) {
692
+ sql = `SELECT TargetKey as Key FROM Relationships
693
+ WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
694
+ AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
695
+ ORDER BY SourceKey ASC`;
696
+ lsql = `SELECT COUNT(TargetKey) FROM Relationships
697
+ WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
698
+ AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
699
+ AND TargetKey = `;
700
+ }
701
+ // answers-for filter
702
+ else if (prop === 'answers-for' && op === '=') {
703
+ if (value.startsWith('LL')) {
704
+ sql = `SELECT TargetKey as Key FROM Relationships
705
+ WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
706
+ AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
707
+ ORDER BY SourceKey ASC`;
708
+ lsql = `SELECT COUNT(TargetKey) FROM Relationships
709
+ WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
710
+ AND SourceKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
711
+ AND TargetKey = `;
712
+ } else {
713
+ sql = `SELECT TargetKey as Key FROM Relationships
714
+ WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
715
+ AND SourceKey IN (
716
+ SELECT SourceKey FROM Relationships
717
+ WHERE RelationshipTypeKey = ${this.relationships.get('answers-for')}
718
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
719
+ )
720
+ ORDER BY SourceKey ASC`;
721
+ lsql = `SELECT COUNT(TargetKey) FROM Relationships
722
+ WHERE RelationshipTypeKey = ${this.relationships.get('Answer')}
723
+ AND SourceKey IN (SELECT SourceKey FROM Relationships
724
+ WHERE RelationshipTypeKey = ${this.relationships.get('answers-for')}
725
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}'))
726
+ AND TargetKey = `;
727
+ }
728
+ }
729
+ // Relationship equal filter
730
+ else if (this.relationships.has(prop) && op === '=') {
731
+ if (this.codes.has(value)) {
732
+ sql = `SELECT SourceKey as Key FROM Relationships
733
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
734
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
735
+ ORDER BY SourceKey ASC`;
736
+ lsql = `SELECT COUNT(SourceKey) FROM Relationships
737
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
738
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
739
+ AND SourceKey = `;
740
+ } else {
741
+ sql = `SELECT SourceKey as Key FROM Relationships
742
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
743
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
744
+ ORDER BY SourceKey ASC`;
745
+ lsql = `SELECT COUNT(SourceKey) FROM Relationships
746
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
747
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
748
+ AND SourceKey = `;
749
+ }
750
+ }
751
+ // Relationship 'in' filter
752
+ else if (this.relationships.has(prop) && op === 'in') {
753
+ const codes = this.#commaListOfCodes(value);
754
+ sql = `SELECT SourceKey as Key FROM Relationships
755
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
756
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))
757
+ ORDER BY SourceKey ASC`;
758
+ lsql = `SELECT COUNT(SourceKey) FROM Relationships
759
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
760
+ AND TargetKey IN (SELECT CodeKey FROM Codes WHERE Code IN (${codes}))
761
+ AND SourceKey = `;
762
+ }
763
+ // Relationship 'exists' filter
764
+ else if (this.relationships.has(prop) && op === 'exists') {
765
+ if (this.codes.has(value)) {
766
+ sql = `SELECT SourceKey as Key FROM Relationships
767
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
768
+ AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
769
+ ORDER BY SourceKey ASC`;
770
+ lsql = `SELECT COUNT(SourceKey) FROM Relationships
771
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
772
+ AND EXISTS (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
773
+ AND SourceKey = `;
774
+ } else {
775
+ sql = `SELECT SourceKey as Key FROM Relationships
776
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
777
+ AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
778
+ ORDER BY SourceKey ASC`;
779
+ lsql = `SELECT COUNT(SourceKey) FROM Relationships
780
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
781
+ AND EXISTS (SELECT CodeKey FROM Codes WHERE Description = '${this.#sqlWrapString(value)}' COLLATE NOCASE)
782
+ AND SourceKey = `;
783
+ }
784
+ }
785
+ // Relationship regex filter
786
+ else if (this.relationships.has(prop) && op === 'regex') {
787
+ const matchingKeys = await this.#findRegexMatches(
788
+ `SELECT CodeKey as Key, Description FROM Codes
789
+ WHERE CodeKey IN (SELECT TargetKey FROM Relationships WHERE RelationshipTypeKey = ${this.relationships.get(prop)})`,
790
+ value,
791
+ 'Description'
792
+ );
793
+ if (matchingKeys.length > 0) {
794
+ sql = `SELECT SourceKey as Key FROM Relationships
795
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
796
+ AND TargetKey IN (${matchingKeys.join(',')})
797
+ ORDER BY SourceKey ASC`;
798
+ lsql = `SELECT COUNT(SourceKey) FROM Relationships
799
+ WHERE RelationshipTypeKey = ${this.relationships.get(prop)}
800
+ AND TargetKey IN (${matchingKeys.join(',')})
801
+ AND SourceKey = `;
802
+ }
803
+ }
804
+ // Property equal filter (with CLASSTYPE handling)
805
+ else if (this.propertyList.has(prop) && op === '=') {
806
+ let actualValue = value;
807
+ if (prop === 'CLASSTYPE' && ['1', '2', '3', '4'].includes(value)) {
808
+ const classTypes = {
809
+ '1': 'Laboratory class',
810
+ '2': 'Clinical class',
811
+ '3': 'Claims attachments',
812
+ '4': 'Surveys'
813
+ };
814
+ actualValue = classTypes[value];
815
+ }
816
+ sql = `SELECT CodeKey as Key FROM Properties, PropertyValues
817
+ WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
818
+ AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
819
+ AND PropertyValues.Value = '${this.#sqlWrapString(actualValue)}' COLLATE NOCASE
820
+ ORDER BY CodeKey ASC`;
821
+ lsql = `SELECT COUNT(CodeKey) FROM Properties, PropertyValues
822
+ WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
823
+ AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
824
+ AND PropertyValues.Value = '${this.#sqlWrapString(actualValue)}' COLLATE NOCASE
825
+ AND CodeKey = `;
826
+ }
827
+ // Property 'in' filter
828
+ else if (this.propertyList.has(prop) && op === 'in') {
829
+ const codes = this.#commaListOfCodes(value);
830
+ sql = `SELECT CodeKey as Key FROM Properties, PropertyValues
831
+ WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
832
+ AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
833
+ AND PropertyValues.Value IN (${codes}) COLLATE NOCASE
834
+ ORDER BY CodeKey ASC`;
835
+ lsql = `SELECT COUNT(CodeKey) FROM Properties, PropertyValues
836
+ WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
837
+ AND Properties.PropertyValueKey = PropertyValues.PropertyValueKey
838
+ AND PropertyValues.Value IN (${codes}) COLLATE NOCASE
839
+ AND CodeKey = `;
840
+ }
841
+ // Property 'exists' filter
842
+ else if (this.propertyList.has(prop) && op === 'exists') {
843
+ sql = `SELECT DISTINCT CodeKey as Key FROM Properties
844
+ WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
845
+ ORDER BY CodeKey ASC`;
846
+ lsql = `SELECT COUNT(CodeKey) FROM Properties
847
+ WHERE Properties.PropertyTypeKey = ${this.propertyList.get(prop)}
848
+ AND CodeKey = `;
849
+ }
850
+ // Property regex filter
851
+ else if (this.propertyList.has(prop) && op === 'regex') {
852
+ const matchingKeys = await this.#findRegexMatches(
853
+ `SELECT PropertyValueKey, Value FROM PropertyValues
854
+ WHERE PropertyValueKey IN (SELECT PropertyValueKey FROM Properties WHERE PropertyTypeKey = ${this.propertyList.get(prop)})`,
855
+ value,
856
+ 'Value',
857
+ 'PropertyValueKey'
858
+ );
859
+ if (matchingKeys.length > 0) {
860
+ sql = `SELECT CodeKey as Key FROM Properties
861
+ WHERE PropertyTypeKey = ${this.propertyList.get(prop)}
862
+ AND PropertyValueKey IN (${matchingKeys.join(',')})
863
+ ORDER BY CodeKey ASC`;
864
+ lsql = `SELECT COUNT(CodeKey) FROM Properties
865
+ WHERE PropertyTypeKey = ${this.propertyList.get(prop)}
866
+ AND PropertyValueKey IN (${matchingKeys.join(',')})
867
+ AND CodeKey = `;
868
+ }
869
+ }
870
+ // Status filter
871
+ else if (prop === 'STATUS' && op === '=' && this.statusKeys.has(value)) {
872
+ sql = `SELECT CodeKey as Key FROM Codes
873
+ WHERE StatusKey = ${this.statusKeys.get(value)}
874
+ ORDER BY CodeKey ASC`;
875
+ lsql = `SELECT COUNT(CodeKey) FROM Codes
876
+ WHERE StatusKey = ${this.statusKeys.get(value)}
877
+ AND CodeKey = `;
878
+ }
879
+ // Concept hierarchy filters (is-a, descendent-of)
880
+ else if (prop === 'concept' && ['is-a', 'descendent-of'].includes(op)) {
881
+ sql = `SELECT DescendentKey as Key FROM Closure
882
+ WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
883
+ ORDER BY DescendentKey ASC`;
884
+ lsql = `SELECT COUNT(DescendentKey) FROM Closure
885
+ WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
886
+ AND DescendentKey = `;
887
+ }
888
+ // Concept equal filter (workaround for VSAC misuse)
889
+ else if (prop === 'concept' && op === '=') {
890
+ sql = `SELECT CodeKey as Key FROM Codes
891
+ WHERE Code = '${this.#sqlWrapString(value)}'
892
+ ORDER BY CodeKey ASC`;
893
+ lsql = `SELECT COUNT(CodeKey) FROM Codes
894
+ WHERE Code = '${this.#sqlWrapString(value)}'
895
+ AND CodeKey = `;
896
+ }
897
+ // Concept 'in' filter (workaround for VSAC misuse)
898
+ else if (prop === 'concept' && op === 'in') {
899
+ const codes = this.#commaListOfCodes(value);
900
+ sql = `SELECT CodeKey as Key FROM Codes
901
+ WHERE Code IN (${codes})
902
+ ORDER BY CodeKey ASC`;
903
+ lsql = `SELECT COUNT(CodeKey) FROM Codes
904
+ WHERE Code IN (${codes})
905
+ AND CodeKey = `;
906
+ }
907
+ // Code property filters (workaround for VSAC misuse)
908
+ else if (prop === 'code' && ['is-a', 'descendent-of'].includes(op)) {
909
+ sql = `SELECT DescendentKey as Key FROM Closure
910
+ WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
911
+ ORDER BY DescendentKey ASC`;
912
+ lsql = `SELECT COUNT(DescendentKey) FROM Closure
913
+ WHERE AncestorKey IN (SELECT CodeKey FROM Codes WHERE Code = '${this.#sqlWrapString(value)}')
914
+ AND DescendentKey = `;
915
+ }
916
+ else if (prop === 'code' && op === '=') {
917
+ sql = `SELECT CodeKey as Key FROM Codes
918
+ WHERE Code = '${this.#sqlWrapString(value)}'
919
+ ORDER BY CodeKey ASC`;
920
+ lsql = `SELECT COUNT(CodeKey) FROM Codes
921
+ WHERE Code = '${this.#sqlWrapString(value)}'
922
+ AND CodeKey = `;
923
+ }
924
+ // Copyright filters
925
+ else if (prop === 'copyright' && op === '=') {
926
+ if (value === 'LOINC') {
927
+ sql = `SELECT CodeKey as Key FROM Codes
928
+ WHERE NOT CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
929
+ ORDER BY CodeKey ASC`;
930
+ lsql = `SELECT COUNT(CodeKey) FROM Codes
931
+ WHERE NOT CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
932
+ AND CodeKey = `;
933
+ } else if (value === '3rdParty') {
934
+ sql = `SELECT CodeKey as Key FROM Codes
935
+ WHERE CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
936
+ ORDER BY CodeKey ASC`;
937
+ lsql = `SELECT COUNT(CodeKey) FROM Codes
938
+ WHERE CodeKey IN (SELECT CodeKey FROM Properties WHERE PropertyTypeKey = 9)
939
+ AND CodeKey = `;
940
+ }
941
+ }
942
+
943
+ if (sql) {
944
+ await this.#executeSQL(sql, filter);
945
+ filter.lsql = lsql;
946
+ } else {
947
+ throw new Error(`The filter "${prop} ${op} ${value}" is not supported for LOINC`);
948
+ }
949
+ }
950
+
951
+ // Helper method for regex matching
952
+ async #findRegexMatches(sql, pattern, valueColumn, keyColumn = 'Key') {
953
+ return new Promise((resolve, reject) => {
954
+ const regex = new RegExp(pattern);
955
+ const matchingKeys = [];
956
+
957
+ this.db.all(sql, (err, rows) => {
958
+ if (err) {
959
+ reject(err);
960
+ } else {
961
+ for (const row of rows) {
962
+ if (regex.test(row[valueColumn])) {
963
+ matchingKeys.push(row[keyColumn]);
964
+ }
965
+ }
966
+ resolve(matchingKeys);
967
+ }
968
+ });
969
+ });
970
+ }
971
+
972
+ // Helper method for comma-separated code lists
973
+ #commaListOfCodes(source) {
974
+ const codes = source.split(',')
975
+ .filter(s => this.codes.has(s.trim()))
976
+ .map(s => `'${this.#sqlWrapString(s.trim())}'`);
977
+ return codes.join(',');
978
+ }
979
+
980
+ async #executeSQL(sql, filter) {
981
+ return new Promise((resolve, reject) => {
982
+ this.db.all(sql, (err, rows) => {
983
+ if (err) {
984
+ reject(err);
985
+ } else {
986
+ filter.keys = rows.map(row => row.Key).filter(key => key !== 0);
987
+ resolve();
988
+ }
989
+ });
990
+ });
991
+ }
992
+
993
+ #sqlWrapString(str) {
994
+ return str.replace(/'/g, "''");
995
+ }
996
+
997
+ async executeFilters(filterContext) {
998
+
999
+ return filterContext.filters;
1000
+ }
1001
+
1002
+ async filterSize(filterContext, set) {
1003
+
1004
+ return set.keys.length;
1005
+ }
1006
+
1007
+ async filterMore(filterContext, set) {
1008
+
1009
+ set.cursor = set.cursor || 0;
1010
+ return set.cursor < set.keys.length;
1011
+ }
1012
+
1013
+ async filterConcept(filterContext, set) {
1014
+
1015
+
1016
+ if (set.cursor >= set.keys.length) {
1017
+ return null;
1018
+ }
1019
+
1020
+ const key = set.keys[set.cursor];
1021
+ set.cursor++;
1022
+
1023
+ return this.codeList[key];
1024
+ }
1025
+
1026
+ async filterLocate(filterContext, set, code) {
1027
+ const context = this.codes.get(code);
1028
+ if (!context) {
1029
+ return `Not a valid code: ${code}`;
1030
+ }
1031
+
1032
+ if (!set.lsql) {
1033
+ return 'Filter not understood';
1034
+ }
1035
+
1036
+ // Check if this context's key is in the filter
1037
+ if (set.hasKey(context.key)) {
1038
+ return context;
1039
+ } else {
1040
+ return null; // `Code ${code} is not in the specified filter`;
1041
+ }
1042
+ }
1043
+
1044
+ async filterCheck(filterContext, set, concept) {
1045
+ if (!(concept instanceof LoincProviderContext)) {
1046
+ return false;
1047
+ }
1048
+
1049
+ return set.hasKey(concept.key);
1050
+ }
1051
+
1052
+ // Search filter - placeholder for text search
1053
+ // eslint-disable-next-line no-unused-vars
1054
+ async searchFilter(filterContext, filter, sort) {
1055
+
1056
+ throw new Error('Text search not implemented yet');
1057
+ }
1058
+
1059
+ // Subsumption testing
1060
+ async subsumesTest(codeA, codeB) {
1061
+ await this.#ensureContext(codeA);
1062
+ await this.#ensureContext(codeB);
1063
+
1064
+ return 'not-subsumed'; // Not implemented yet
1065
+ }
1066
+
1067
+ versionAlgorithm() {
1068
+ return 'natural';
1069
+ }
1070
+
1071
+ isDisplay(designation) {
1072
+ return designation.use.code == "SHORTNAME" || designation.use.code == "LONG_COMMON_NAME" || designation.use.code == "LinguisticVariantDisplayName";
1073
+ }
1074
+ }
1075
+
1076
+ class LoincServicesFactory extends CodeSystemFactoryProvider {
1077
+ constructor(i18n, dbPath) {
1078
+ super(i18n);
1079
+ this.dbPath = dbPath;
1080
+ this.uses = 0;
1081
+ this._loaded = false;
1082
+ this._sharedData = null;
1083
+ }
1084
+
1085
+ system() {
1086
+ return 'http://loinc.org';
1087
+ }
1088
+
1089
+ version() {
1090
+ return this._sharedData._version;
1091
+ }
1092
+
1093
+ name() {
1094
+ return 'LOINC';
1095
+ }
1096
+
1097
+ async #ensureLoaded() {
1098
+ if (!this._loaded) {
1099
+ await this.load();
1100
+ }
1101
+ }
1102
+
1103
+ async load() {
1104
+ const db = new sqlite3.Database(this.dbPath);
1105
+
1106
+ // Enable performance optimizations
1107
+ await this.#optimizeDatabase(db);
1108
+
1109
+ try {
1110
+ this._sharedData = {
1111
+ langs: new Map(),
1112
+ codes: new Map(),
1113
+ codeList: [null],
1114
+ allKeys: [],
1115
+ relationships: new Map(),
1116
+ propertyList: new Map(),
1117
+ statusKeys: new Map(),
1118
+ statusCodes: new Map(),
1119
+ _version: '',
1120
+ root: '',
1121
+ firstCodeKey: 0
1122
+ };
1123
+
1124
+ // Load small lookup tables in parallel
1125
+ // eslint-disable-next-line no-unused-vars
1126
+ const [langs, statusCodes, relationships, propertyList, config] = await Promise.all([
1127
+ this.#loadLanguages(db),
1128
+ this.#loadStatusCodes(db),
1129
+ this.#loadRelationshipTypes(db),
1130
+ this.#loadPropertyTypes(db),
1131
+ this.#loadConfig(db)
1132
+ ]);
1133
+
1134
+ // Load codes (largest operation)
1135
+ await this.#loadCodes(db);
1136
+
1137
+ // Load dependent data in parallel
1138
+ await Promise.all([
1139
+ // this.#loadDesignationsCache(db),
1140
+ this.#loadHierarchy(db)
1141
+ ]);
1142
+
1143
+ } finally {
1144
+ db.close();
1145
+ }
1146
+ this._loaded = true;
1147
+ }
1148
+
1149
+ async #optimizeDatabase(db) {
1150
+ return new Promise((resolve, reject) => {
1151
+ db.serialize(() => {
1152
+ db.run('PRAGMA journal_mode = WAL');
1153
+ db.run('PRAGMA synchronous = NORMAL');
1154
+ db.run('PRAGMA cache_size = 10000');
1155
+ db.run('PRAGMA temp_store = MEMORY');
1156
+ db.run('PRAGMA mmap_size = 268435456'); // 256MB
1157
+
1158
+ // Ensure indexes exist for per-request query patterns
1159
+ db.run('CREATE INDEX IF NOT EXISTS idx_descriptions_codekey_typkey ON Descriptions(CodeKey, DescriptionTypeKey)');
1160
+ db.run('CREATE INDEX IF NOT EXISTS idx_relationships_sourcekey ON Relationships(SourceKey)');
1161
+ db.run('CREATE INDEX IF NOT EXISTS idx_properties_codekey ON Properties(CodeKey)', (err) => {
1162
+ if (err) reject(err);
1163
+ else resolve();
1164
+ });
1165
+ });
1166
+ });
1167
+ }
1168
+
1169
+ async #loadLanguages(db) {
1170
+ return new Promise((resolve, reject) => {
1171
+ db.all('SELECT LanguageKey, Code FROM Languages', (err, rows) => {
1172
+ if (err) {
1173
+ reject(err);
1174
+ } else {
1175
+ for (const row of rows) {
1176
+ this._sharedData.langs.set(row.Code, row.LanguageKey);
1177
+ }
1178
+ resolve();
1179
+ }
1180
+ });
1181
+ });
1182
+ }
1183
+
1184
+ async #loadStatusCodes(db) {
1185
+ return new Promise((resolve, reject) => {
1186
+ db.all('SELECT StatusKey, Description FROM StatusCodes', (err, rows) => {
1187
+ if (err) {
1188
+ reject(err);
1189
+ } else {
1190
+ for (const row of rows) {
1191
+ this._sharedData.statusKeys.set(row.Description, row.StatusKey.toString());
1192
+ this._sharedData.statusCodes.set(row.StatusKey.toString(), row.Description);
1193
+ }
1194
+ resolve();
1195
+ }
1196
+ });
1197
+ });
1198
+ }
1199
+
1200
+ async #loadRelationshipTypes(db) {
1201
+ return new Promise((resolve, reject) => {
1202
+ db.all('SELECT RelationshipTypeKey, Description FROM RelationshipTypes', (err, rows) => {
1203
+ if (err) {
1204
+ reject(err);
1205
+ } else {
1206
+ for (const row of rows) {
1207
+ this._sharedData.relationships.set(row.Description, row.RelationshipTypeKey.toString());
1208
+ }
1209
+ resolve();
1210
+ }
1211
+ });
1212
+ });
1213
+ }
1214
+
1215
+ async #loadPropertyTypes(db) {
1216
+ return new Promise((resolve, reject) => {
1217
+ db.all('SELECT PropertyTypeKey, Description FROM PropertyTypes', (err, rows) => {
1218
+ if (err) {
1219
+ reject(err);
1220
+ } else {
1221
+ for (const row of rows) {
1222
+ this._sharedData.propertyList.set(row.Description, row.PropertyTypeKey.toString());
1223
+ }
1224
+ resolve();
1225
+ }
1226
+ });
1227
+ });
1228
+ }
1229
+
1230
+ async #loadCodes(db) {
1231
+ return new Promise((resolve, reject) => {
1232
+ // First get the count to pre-allocate array
1233
+ db.get('SELECT MAX(CodeKey) as maxKey FROM Codes', (err, row) => {
1234
+ if (err) return reject(err);
1235
+
1236
+ // Pre-allocate the array to avoid repeated resizing
1237
+ const maxKey = row.maxKey || 0;
1238
+ this._sharedData.codeList = new Array(maxKey + 1).fill(null);
1239
+
1240
+ // Now load all codes
1241
+ db.all('SELECT CodeKey, Code, Type, Codes.Description, StatusCodes.Description as Status FROM Codes, StatusCodes where StatusCodes.StatusKey = Codes.StatusKey order by Type Asc, CodeKey Asc', (err, rows) => {
1242
+ if (err) return reject(err);
1243
+
1244
+ // Batch process rows
1245
+ for (const row of rows) {
1246
+ const context = new LoincProviderContext(
1247
+ row.CodeKey,
1248
+ row.Type - 1,
1249
+ row.Code,
1250
+ row.Description,
1251
+ row.Status
1252
+ );
1253
+
1254
+ this._sharedData.codes.set(row.Code, context);
1255
+ this._sharedData.codeList[row.CodeKey] = context;
1256
+ this._sharedData.allKeys.push(row.CodeKey);
1257
+
1258
+ if (this._sharedData.firstCodeKey === 0 && context.kind === LoincProviderContextKind.CODE) {
1259
+ this._sharedData.firstCodeKey = context.key;
1260
+ }
1261
+ }
1262
+ resolve();
1263
+ });
1264
+ });
1265
+ });
1266
+ }
1267
+
1268
+ async #loadDesignationsCache(db) {
1269
+ return new Promise((resolve, reject) => {
1270
+ const sql = `
1271
+ SELECT
1272
+ d.CodeKey,
1273
+ l.Code as Lang,
1274
+ dt.Description as DType,
1275
+ d.Value,
1276
+ dt.Description = 'LONG_COMMON_NAME' as IsDisplay
1277
+ FROM Descriptions d
1278
+ JOIN Languages l ON d.LanguageKey = l.LanguageKey
1279
+ JOIN DescriptionTypes dt ON d.DescriptionTypeKey = dt.DescriptionTypeKey
1280
+ WHERE d.DescriptionTypeKey != 4
1281
+ ORDER BY d.CodeKey
1282
+ `;
1283
+
1284
+ db.all(sql, (err, rows) => {
1285
+ if (err) return reject(err);
1286
+
1287
+ // Batch process by CodeKey to reduce lookups
1288
+ let currentKey = null;
1289
+ let currentContext = null;
1290
+
1291
+ for (const row of rows) {
1292
+ if (row.CodeKey !== currentKey) {
1293
+ currentKey = row.CodeKey;
1294
+ currentContext = this._sharedData.codeList[currentKey];
1295
+ }
1296
+
1297
+ if (currentContext) {
1298
+ currentContext.displays.push(
1299
+ new DescriptionCacheEntry(row.IsDisplay, row.Lang, row.Value, row.DType)
1300
+ );
1301
+ }
1302
+ }
1303
+ resolve();
1304
+ });
1305
+ });
1306
+ }
1307
+
1308
+ async #loadHierarchy(db) {
1309
+ const childRelKey = this._sharedData.relationships.get('child');
1310
+ if (!childRelKey) {
1311
+ return; // No child relationships defined
1312
+ }
1313
+
1314
+ return new Promise((resolve, reject) => {
1315
+ const sql = `
1316
+ SELECT SourceKey, TargetKey FROM Relationships
1317
+ WHERE RelationshipTypeKey = ${childRelKey}
1318
+ `;
1319
+
1320
+ db.all(sql, (err, rows) => {
1321
+ if (err) {
1322
+ reject(err);
1323
+ } else {
1324
+ for (const row of rows) {
1325
+ if (row.SourceKey !== 0 && row.TargetKey !== 0) {
1326
+ const parentContext = this._sharedData.codeList[row.SourceKey];
1327
+ if (parentContext) {
1328
+ parentContext.addChild(row.TargetKey);
1329
+ }
1330
+ }
1331
+ }
1332
+ resolve();
1333
+ }
1334
+ });
1335
+ });
1336
+ }
1337
+
1338
+ async #loadConfig(db) {
1339
+ return new Promise((resolve, reject) => {
1340
+ db.all('SELECT ConfigKey, Value FROM Config WHERE ConfigKey IN (2, 3)', (err, rows) => {
1341
+ if (err) {
1342
+ reject(err);
1343
+ } else {
1344
+ for (const row of rows) {
1345
+ if (row.ConfigKey === 2) {
1346
+ this._sharedData._version = row.Value;
1347
+ } else if (row.ConfigKey === 3) {
1348
+ this._sharedData.root = row.Value;
1349
+ }
1350
+ }
1351
+ resolve();
1352
+ }
1353
+ });
1354
+ });
1355
+ }
1356
+
1357
+ defaultVersion() {
1358
+ return this._sharedData?._version || 'unknown';
1359
+ }
1360
+
1361
+ async build(opContext, supplements) {
1362
+ await this.#ensureLoaded();
1363
+ this.recordUse();
1364
+
1365
+ // Create read-only database connection for this provider instance
1366
+ const db = await new Promise((resolve, reject) => {
1367
+ const conn = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY, (err) => {
1368
+ if (err) reject(err);
1369
+ else resolve(conn);
1370
+ });
1371
+ });
1372
+ // Apply performance PRAGMAs to per-request connection
1373
+ await new Promise((resolve, reject) => {
1374
+ db.serialize(() => {
1375
+ db.run('PRAGMA cache_size = 10000');
1376
+ db.run('PRAGMA temp_store = MEMORY');
1377
+ db.run('PRAGMA mmap_size = 268435456', (err) => {
1378
+ if (err) reject(err);
1379
+ else resolve();
1380
+ });
1381
+ });
1382
+ });
1383
+
1384
+ return new LoincServices(opContext, supplements, db, this._sharedData);
1385
+ }
1386
+
1387
+ useCount() {
1388
+ return this.uses;
1389
+ }
1390
+
1391
+ recordUse() {
1392
+ this.uses++;
1393
+ }
1394
+
1395
+ async buildKnownValueSet(url, version) {
1396
+
1397
+ if (version && version != this.version()) {
1398
+ return null;
1399
+ }
1400
+ if (!url.startsWith('http://loinc.org/vs')) {
1401
+ return null;
1402
+ }
1403
+ if (url == 'http://loinc.org/vs') {
1404
+ // All LOINC codes
1405
+ return {
1406
+ resourceType: 'ValueSet', url: 'http://loinc.org/vs', version: this.version(), status: 'active',
1407
+ name: 'LOINC Value Set - all LOINC codes', description: 'All LOINC codes',
1408
+ date: new Date().toISOString(), experimental: false,
1409
+ compose: { include: [{ system: this.system() }] }
1410
+ };
1411
+ }
1412
+
1413
+ if (url.startsWith('http://loinc.org/vs/')) {
1414
+ const code = url.substring(20);
1415
+ const ci = this._sharedData.codes.get(code);
1416
+ if (!ci) {
1417
+ return null;
1418
+ }
1419
+
1420
+ if (ci.kind === LoincProviderContextKind.PART) {
1421
+ // Part-based value set with ancestor filter
1422
+ return {
1423
+ resourceType: 'ValueSet', url: url, version: this.version(), status: 'active',
1424
+ name: 'LOINCValueSetFor' + ci.code.replace(/-/g, '_'), description: 'LOINC value set for code ' + ci.code + ': ' + ci.desc,
1425
+ date: new Date().toISOString(), experimental: false,
1426
+ compose: { include: [{ system: this.system(), filter: [{ property: 'ancestor', op: '=', value: code }] }]
1427
+ }
1428
+ };
1429
+ }
1430
+
1431
+ if (ci.kind === LoincProviderContextKind.LIST) {
1432
+ // Answer list - enumerate concepts from database
1433
+ const concepts = await this.#getAnswerListConcepts(ci.key);
1434
+ return {
1435
+ resourceType: 'ValueSet', url: url, version: this.version(), status: 'active',
1436
+ name: 'LOINCAnswerList' + ci.code.replace(/-/g, '_'), description: 'LOINC Answer list for code ' + ci.code + ': ' + ci.desc,
1437
+ date: new Date().toISOString(), experimental: false,
1438
+ compose: { include: [{ system: this.system(), concept: concepts }] }
1439
+ };
1440
+ }
1441
+ }
1442
+
1443
+ return null;
1444
+ }
1445
+
1446
+ /**
1447
+ * Get answer list concepts from database
1448
+ * @param {number} sourceKey - Key of the answer list
1449
+ * @returns {Promise<Array>} Array of {code, display} objects
1450
+ */
1451
+ async #getAnswerListConcepts(sourceKey) {
1452
+ return new Promise((resolve, reject) => {
1453
+ let db = new sqlite3.Database(this.dbPath);
1454
+ const sql = `
1455
+ SELECT Code, Description
1456
+ FROM Relationships, Codes
1457
+ WHERE SourceKey = ?
1458
+ AND RelationshipTypeKey = 40
1459
+ AND Relationships.TargetKey = Codes.CodeKey
1460
+ `;
1461
+
1462
+ db.all(sql, [sourceKey], (err, rows) => {
1463
+ if (err) {
1464
+ reject(err);
1465
+ } else {
1466
+ const concepts = rows.map(row => ({
1467
+ code: row.Code
1468
+ }));
1469
+ resolve(concepts);
1470
+ }
1471
+ });
1472
+ });
1473
+ }
1474
+
1475
+ id() {
1476
+ return "loinc"+this.version();
1477
+ }
1478
+ }
1479
+
1480
+ module.exports = {
1481
+ LoincServices,
1482
+ LoincServicesFactory,
1483
+ LoincProviderContext,
1484
+ LoincProviderContextKind
1485
+ };