fhirsmith 0.18.1 → 1.0.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 (214) hide show
  1. package/ACCESSIBILITY.md +119 -0
  2. package/CHANGELOG.md +190 -9
  3. package/CONFIGURATION.md +305 -0
  4. package/LICENSE +29 -0
  5. package/README.md +29 -122
  6. package/cluster.js +1220 -0
  7. package/cluster.md +707 -0
  8. package/config-template.json +77 -8
  9. package/extension-tracker/extension-tracker-template.html +5 -8
  10. package/extension-tracker/extension-tracker.js +0 -2
  11. package/folder/folder.js +7 -6
  12. package/ips/analysis.js +560 -0
  13. package/ips/ips-template.html +5 -8
  14. package/ips/ips.js +331 -33
  15. package/ips/jurisdictions.js +65 -0
  16. package/ips/nationality.js +120 -0
  17. package/ips/pipeline.js +131 -17
  18. package/ips/presenter.js +1 -1
  19. package/ips/profile-editor.js +283 -4
  20. package/ips/readme.md +170 -20
  21. package/ips/render.js +769 -109
  22. package/ips/scripts.js +46 -0
  23. package/ips/store.js +123 -11
  24. package/ips/structure.js +247 -0
  25. package/ips/terminology.js +1 -1
  26. package/library/body-limit.js +22 -0
  27. package/library/cache-generation.js +15 -0
  28. package/library/cgroup-memory.js +133 -1
  29. package/library/cluster-counts.js +121 -0
  30. package/library/cluster-events.js +153 -0
  31. package/library/cluster-metrics.js +193 -0
  32. package/library/cluster-status.js +468 -0
  33. package/library/config-reader.js +96 -0
  34. package/library/daily-report.js +100 -2
  35. package/library/html-server.js +27 -1
  36. package/library/i18nsupport.js +23 -1
  37. package/library/keep-alive.js +18 -0
  38. package/library/languages.js +17 -3
  39. package/library/logger.js +31 -0
  40. package/library/memory-guard.js +428 -0
  41. package/library/native-memory.js +52 -0
  42. package/library/npm-audit.js +51 -25
  43. package/library/openapi-doc.js +7 -5
  44. package/library/package-manager.js +8 -0
  45. package/library/role.js +197 -0
  46. package/library/server-credentials.js +169 -0
  47. package/library/sqlite-utilities.js +59 -0
  48. package/library/ssrf-guard.js +100 -0
  49. package/library/static-response-stats.js +16 -0
  50. package/library/version-utilities.js +50 -0
  51. package/library/worker-count.js +192 -0
  52. package/nginx.md +30 -11
  53. package/npmprojector/indexer.js +18 -10
  54. package/package.json +11 -8
  55. package/packages/feed-check-page.js +117 -0
  56. package/packages/feed-check.js +279 -0
  57. package/packages/npm-check.js +57 -3
  58. package/packages/npm-signing.js +219 -0
  59. package/packages/openapi.yaml +207 -1
  60. package/packages/package-crawler.js +322 -41
  61. package/packages/packages-template.html +6 -8
  62. package/packages/packages.js +604 -57
  63. package/packages/readme.md +113 -1
  64. package/publisher/publisher-template.html +2 -5
  65. package/publisher/publisher.js +121 -110
  66. package/publisher/readme.md +75 -0
  67. package/registry/api.js +179 -1
  68. package/registry/crawler.js +86 -108
  69. package/registry/documentation/api.md +68 -2
  70. package/registry/documentation/configuration.md +34 -0
  71. package/registry/documentation/internals.md +4 -1
  72. package/registry/documentation/readme.md +3 -2
  73. package/registry/model.js +8 -0
  74. package/registry/openapi.yaml +84 -0
  75. package/registry/registry-template.html +5 -8
  76. package/registry/registry.js +58 -6
  77. package/root-bare-template.html +7 -4
  78. package/root-template.html +5 -8
  79. package/server.js +398 -198
  80. package/shl/shl.js +38 -37
  81. package/static/a11y/a11y-check.js +832 -0
  82. package/static/a11y/axe.min.js +12 -0
  83. package/static/assets/css/bootstrap-fhir.css +15 -15
  84. package/static/assets/images/flags/au.svg +18 -0
  85. package/static/assets/images/flags/unknown.svg +11 -0
  86. package/static/assets/images/flags/us.svg +63 -0
  87. package/static/assets/images/icon_audit.png +0 -0
  88. package/static/assets/images/icon_clinical.png +0 -0
  89. package/static/assets/images/icon_document.png +0 -0
  90. package/static/assets/images/icon_external.png +0 -0
  91. package/static/assets/images/icon_medication.png +0 -0
  92. package/static/assets/images/icon_organization.png +0 -0
  93. package/static/assets/images/icon_other.png +0 -0
  94. package/static/assets/images/icon_person.png +0 -0
  95. package/static/assets/images/icon_section.png +0 -0
  96. package/stats-db.js +45 -0
  97. package/stats.js +197 -10
  98. package/testing/testing-template.html +5 -8
  99. package/token/nginx_passport_setup.md +1 -1
  100. package/token/security_guide.md +1 -1
  101. package/token/token-template.html +2 -5
  102. package/token/token.js +40 -36
  103. package/translations/Messages.properties +19 -0
  104. package/tx/README.md +158 -4
  105. package/tx/closure/closure-store.js +126 -3
  106. package/tx/cm/cm-database.js +69 -57
  107. package/tx/cm/cm-package.js +50 -16
  108. package/tx/cs/cs-api.js +284 -19
  109. package/tx/cs/cs-api.md +41 -4
  110. package/tx/cs/cs-areacode.js +1 -1
  111. package/tx/cs/cs-country.js +17 -5
  112. package/tx/cs/cs-cpt.js +33 -16
  113. package/tx/cs/cs-cs.js +199 -24
  114. package/tx/cs/cs-currency.js +1 -1
  115. package/tx/cs/cs-icd11.js +21 -13
  116. package/tx/cs/cs-icf.js +22 -3
  117. package/tx/cs/cs-lang.js +2 -2
  118. package/tx/cs/cs-loinc.js +193 -67
  119. package/tx/cs/cs-mimetypes.js +12 -2
  120. package/tx/cs/cs-ndc.js +40 -25
  121. package/tx/cs/cs-omop.js +54 -29
  122. package/tx/cs/cs-rxnorm.js +300 -196
  123. package/tx/cs/cs-snomed.js +861 -95
  124. package/tx/cs/cs-ucum.js +14 -4
  125. package/tx/cs/cs-unii.js +18 -14
  126. package/tx/cs/cs-virtual.js +210 -0
  127. package/tx/ecosystem-resolver.js +413 -0
  128. package/tx/html/cluster-dash-metrics.liquid +26 -0
  129. package/tx/html/cluster-home-metrics.liquid +31 -0
  130. package/tx/html/cluster-metrics-script.liquid +251 -0
  131. package/tx/html/codesystem-operations.liquid +3 -3
  132. package/tx/html/ecl-panel.liquid +1 -1
  133. package/tx/html/external-metrics.liquid +17 -0
  134. package/tx/html/operations-form.liquid +17 -17
  135. package/tx/html/search-form.liquid +11 -11
  136. package/tx/html/status-line.liquid +58 -0
  137. package/tx/html/status-panel.liquid +140 -0
  138. package/tx/html/status-tasks.liquid +25 -0
  139. package/tx/html/tx-template.html +2 -5
  140. package/tx/importers/atc-to-fhir.js +17 -16
  141. package/tx/importers/export-icd11-codesystem.js +3 -2
  142. package/tx/importers/import-icd11.js +4 -3
  143. package/tx/importers/import-loinc.module.js +118 -56
  144. package/tx/importers/import-ndc.module.js +56 -44
  145. package/tx/importers/import-rxnorm.module.js +118 -79
  146. package/tx/importers/import-sct.module.js +125 -61
  147. package/tx/importers/import-unii.module.js +37 -36
  148. package/tx/importers/subset-loinc.module.js +49 -48
  149. package/tx/importers/subset-rxnorm.module.js +29 -28
  150. package/tx/importers/terminal.js +18 -0
  151. package/tx/importers/tx-import-base.js +13 -12
  152. package/tx/importers/tx-import-settings.js +5 -4
  153. package/tx/importers/tx-import.js +53 -52
  154. package/tx/library/canonical-resource.js +49 -0
  155. package/tx/library/codesystem.js +94 -5
  156. package/tx/library/designations.js +45 -2
  157. package/tx/library/extensions.js +13 -7
  158. package/tx/library/implicit-valuesets.js +66 -0
  159. package/tx/library/renderer.js +77 -21
  160. package/tx/library/sqlite-utilities.js +2 -0
  161. package/tx/library/tho-precedence.js +40 -0
  162. package/tx/library/ucum-parsers.js +11 -1
  163. package/tx/library/wait-for-cache.js +43 -0
  164. package/tx/library.js +321 -22
  165. package/tx/openapi-operations.js +6 -4
  166. package/tx/operation-context.js +460 -99
  167. package/tx/params.js +25 -0
  168. package/tx/problems.js +3 -5
  169. package/tx/provider.js +314 -67
  170. package/tx/resource-cache-sqlite.js +1050 -0
  171. package/tx/sct/documentation.md +45 -21
  172. package/tx/sct/ecl.js +81 -17
  173. package/tx/sct/editions.js +39 -0
  174. package/tx/sct/expressions.js +10 -1
  175. package/tx/sct/load-config.js +107 -0
  176. package/tx/sct/mmap.js +109 -0
  177. package/tx/sct/structures.js +175 -15
  178. package/tx/sct/text-index.js +5 -1
  179. package/tx/syndication/syndication-feed.js +2 -2
  180. package/tx/tx-html.js +44 -8
  181. package/tx/tx.fhir.org.yml +8 -8
  182. package/tx/tx.js +282 -80
  183. package/tx/usage-tracker.js +39 -11
  184. package/tx/vs/vs-api.js +19 -0
  185. package/tx/vs/vs-database.js +386 -62
  186. package/tx/vs/vs-package.js +44 -17
  187. package/tx/vs/vs-vsac.js +649 -101
  188. package/tx/workers/batch-validate.js +37 -10
  189. package/tx/workers/batch.js +6 -1
  190. package/tx/workers/cache-control.js +15 -4
  191. package/tx/workers/closure.js +156 -13
  192. package/tx/workers/compare.js +8 -5
  193. package/tx/workers/expand.js +1173 -289
  194. package/tx/workers/metadata.js +35 -0
  195. package/tx/workers/search.js +53 -1
  196. package/tx/workers/supplement-filters.js +145 -0
  197. package/tx/workers/translate.js +169 -4
  198. package/tx/workers/validate.js +223 -119
  199. package/tx/workers/worker.js +123 -31
  200. package/tx/xversion/xv-valueset.js +7 -4
  201. package/utilities/submit-test-report.js +3 -1
  202. package/utilities/tx-stress-test.js +482 -0
  203. package/vcl/readme.md +37 -0
  204. package/vcl/vcl-parser.js +15 -0
  205. package/xig/readme.md +52 -0
  206. package/xig/xig-template.html +5 -8
  207. package/xig/xig.js +238 -422
  208. package/changelog-template.md +0 -22
  209. package/registry/test-runner.js +0 -237
  210. package/release.sh +0 -80
  211. package/test-scripts/repro-re2-wasm-leak.js +0 -34
  212. package/tsconfig.eslint.json +0 -9
  213. package/tx/html/dash-metrics.liquid +0 -147
  214. package/tx/html/home-metrics.liquid +0 -225
@@ -1,9 +1,11 @@
1
1
  const sqlite3 = require('sqlite3').verbose();
2
2
  const assert = require('assert');
3
3
  const { CodeSystem } = require('../library/codesystem');
4
- const { CodeSystemProvider, CodeSystemFactoryProvider } = require('./cs-api');
5
- const {Designations} = require("../library/designations");
4
+ const { CodeSystemFactoryProvider } = require('./cs-api');
5
+ const { BaseCSServices } = require('./cs-base');
6
6
  const {validateArrayParameter, formatDateMMDDYYYY} = require("../../library/utilities");
7
+ const Logger = require("../../library/logger");
8
+ const { guarded } = require('../library/sqlite-utilities');
7
9
 
8
10
  // Context for RxNorm concepts
9
11
  class RxNormConcept {
@@ -12,9 +14,43 @@ class RxNormConcept {
12
14
  this.display = display;
13
15
  this.others = []; // Array of alternative displays (SY terms, etc.)
14
16
  this.archived = false;
17
+ this.suppress = null; // SUPPRESS of the display atom, when known: N, E, O (obsolete) or Y
18
+ }
19
+
20
+ get obsolete() {
21
+ return this.suppress === 'O';
22
+ }
23
+ }
24
+
25
+ // Which RXNORM atom gives a concept its display: the atom with its main term type (IN, SCD,
26
+ // BN...), or failing that a prescribable name, an entry term, or a tall man synonym. SY atoms
27
+ // are never the display. As an SQL expression that is lowest for the best atom
28
+ const DISPLAY_RANK_SQL = `CASE TTY WHEN 'PSN' THEN 1 WHEN 'ET' THEN 2 WHEN 'TMSY' THEN 3 WHEN 'SY' THEN 4 ELSE 0 END`;
29
+ function displayRank(tty) {
30
+ switch (tty) {
31
+ case 'PSN': return 1;
32
+ case 'ET': return 2;
33
+ case 'TMSY': return 3;
34
+ case 'SY': return 4;
35
+ default: return 0;
15
36
  }
16
37
  }
17
38
 
39
+ // RxNorm's own relationships are the only ones used, for now (filters and $lookup). RXNREL
40
+ // holds every source's (MTHSPL, VANDF...), and they are all imported
41
+ const RELATIONSHIP_SAB = 'RXNORM';
42
+
43
+ // indexes the provider needs beyond what older imports made (see also import-rxnorm.module.js)
44
+ const RXNORM_LOAD_INDEXES = [
45
+ 'CREATE INDEX IF NOT EXISTS idx_rxnstems_cui_stem ON RXNSTEMS(CUI, stem)',
46
+ 'CREATE INDEX IF NOT EXISTS idx_rxnrel_rxcui2 ON RXNREL(RXCUI2, SAB)',
47
+ 'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela_rxcui1 ON RXNREL(RELA, RXCUI1)',
48
+ 'CREATE INDEX IF NOT EXISTS idx_rxnrel_rela_rxaui1 ON RXNREL(RELA, RXAUI1)',
49
+ 'CREATE INDEX IF NOT EXISTS idx_rxnsty_rxcui ON RXNSTY(RXCUI)',
50
+ // locate() looks an unknown code up in the archive; without this, each miss scans the table
51
+ 'CREATE INDEX IF NOT EXISTS idx_rxnatomarchive_rxcui_sab ON RXNATOMARCHIVE(RXCUI, SAB)'
52
+ ];
53
+
18
54
  // Filter holder for query building and iteration
19
55
  class RxNormFilterHolder {
20
56
  constructor() {
@@ -68,7 +104,7 @@ class RxNormIteratorContext {
68
104
  }
69
105
  }
70
106
 
71
- class RxNormServices extends CodeSystemProvider {
107
+ class RxNormServices extends BaseCSServices {
72
108
  constructor(opContext, supplements, db, sharedData, isNCI = false) {
73
109
  super(opContext, supplements);
74
110
  this.db = db;
@@ -82,10 +118,7 @@ class RxNormServices extends CodeSystemProvider {
82
118
  }
83
119
 
84
120
  close() {
85
- if (this.db) {
86
- this.db.close();
87
- this.db = null;
88
- }
121
+ this.closeDb();
89
122
  }
90
123
 
91
124
  // Metadata methods
@@ -162,19 +195,7 @@ class RxNormServices extends CodeSystemProvider {
162
195
  if (ctxt && ctxt.archived) {
163
196
  return 'archived';
164
197
  }
165
-
166
- // Check suppress flag
167
- return new Promise((resolve, reject) => {
168
- const sql = `SELECT suppress FROM rxnconso WHERE ${this.getCodeField()} = ? AND SAB = ? AND TTY <> 'SY'`;
169
-
170
- this.db.get(sql, [ctxt.code, this.getSAB()], (err, row) => {
171
- if (err) {
172
- reject(err);
173
- } else {
174
- resolve(row ? row.suppress === '1' ? 'suppressed' : null : null);
175
- }
176
- });
177
- });
198
+ return (await this.#isObsolete(ctxt)) ? 'obsolete' : null;
178
199
  }
179
200
 
180
201
  async isInactive(context) {
@@ -184,19 +205,24 @@ class RxNormServices extends CodeSystemProvider {
184
205
  if (ctxt && ctxt.archived) {
185
206
  return true;
186
207
  }
208
+ return this.#isObsolete(ctxt);
209
+ }
187
210
 
188
- // Check suppress flag
189
- return new Promise((resolve, reject) => {
190
- const sql = `SELECT suppress FROM rxnconso WHERE ${this.getCodeField()} = ? AND SAB = ? AND TTY <> 'SY'`;
191
-
192
- this.db.get(sql, [ctxt.code, this.getSAB()], (err, row) => {
193
- if (err) {
194
- reject(err);
195
- } else {
196
- resolve(row ? row.suppress === '1' : false);
197
- }
198
- });
199
- });
211
+ /**
212
+ * A concept is obsolete (inactive) when its display atom is: SUPPRESS = O
213
+ * @private
214
+ */
215
+ async #isObsolete(ctxt) {
216
+ if (!ctxt) {
217
+ return false;
218
+ }
219
+ if (ctxt.suppress == null) {
220
+ const row = await this.#dbGet(
221
+ `SELECT SUPPRESS, MIN(${DISPLAY_RANK_SQL}) AS rnk FROM rxnconso WHERE ${this.getCodeField()} = ? AND SAB = ? AND TTY <> 'SY'`,
222
+ [ctxt.code, this.getSAB()]);
223
+ ctxt.suppress = row && row.SUPPRESS ? row.SUPPRESS : 'N';
224
+ }
225
+ return ctxt.obsolete;
200
226
  }
201
227
 
202
228
  async isDeprecated(context) {
@@ -248,9 +274,9 @@ class RxNormServices extends CodeSystemProvider {
248
274
  if (!code) return { context: null, message: 'Empty code' };
249
275
 
250
276
  return new Promise((resolve, reject) => {
251
- let sql = `SELECT STR, TTY FROM rxnconso WHERE ${this.getCodeField()} = ? AND SAB = ?`;
277
+ let sql = `SELECT STR, TTY, SUPPRESS FROM rxnconso WHERE ${this.getCodeField()} = ? AND SAB = ?`;
252
278
 
253
- this.db.all(sql, [code, this.getSAB()], (err, rows) => {
279
+ this.db.all(sql, [code, this.getSAB()], guarded(reject, (err, rows) => {
254
280
  if (err) {
255
281
  reject(err);
256
282
  return;
@@ -259,7 +285,7 @@ class RxNormServices extends CodeSystemProvider {
259
285
  if (rows.length === 0) {
260
286
  // Try archive
261
287
  sql = `SELECT STR, TTY FROM RXNATOMARCHIVE WHERE ${this.getCodeField()} = ? AND SAB = ?`;
262
- this.db.all(sql, [code, this.getSAB()], (err, archiveRows) => {
288
+ this.db.all(sql, [code, this.getSAB()], guarded(reject, (err, archiveRows) => {
263
289
  if (err) {
264
290
  reject(err);
265
291
  return;
@@ -272,12 +298,12 @@ class RxNormServices extends CodeSystemProvider {
272
298
 
273
299
  const concept = this.#createConceptFromRows(code, archiveRows, true);
274
300
  resolve({ context: concept, message: null });
275
- });
301
+ }));
276
302
  } else {
277
303
  const concept = this.#createConceptFromRows(code, rows, false);
278
304
  resolve({ context: concept, message: null });
279
305
  }
280
- });
306
+ }));
281
307
  });
282
308
  }
283
309
 
@@ -285,24 +311,68 @@ class RxNormServices extends CodeSystemProvider {
285
311
  const concept = new RxNormConcept(code);
286
312
  concept.archived = archived;
287
313
 
314
+ // the display is the best ranked atom (see DISPLAY_RANK_SQL), the rest are other names
315
+ let best = null;
288
316
  for (const row of rows) {
289
- if (row.TTY === 'SY' || concept.display && concept.display) {
290
- concept.others.push(row.STR.trim());
291
- } else {
292
- concept.display = row.STR.trim();
317
+ if (row.TTY !== 'SY' && (!best || displayRank(row.TTY) < displayRank(best.TTY))) {
318
+ best = row;
319
+ }
320
+ }
321
+ if (best) {
322
+ concept.display = best.STR.trim();
323
+ if (!archived) {
324
+ concept.suppress = best.SUPPRESS || 'N';
293
325
  }
294
326
  }
327
+ for (const row of rows) {
328
+ const str = row.STR.trim();
329
+ if (row !== best && str !== concept.display && !concept.others.includes(str)) {
330
+ concept.others.push(str);
331
+ }
332
+ }
333
+
334
+ return concept;
335
+ }
336
+
337
+ /**
338
+ * The query for a set of concepts: one row per concept, with its display, whatever atoms
339
+ * the conditions matched on. Every condition is about the concept (its code), not the atom
340
+ * @param {string} conditions - SQL 'AND ...' conditions on rxnconso.<code field>
341
+ * @param {string} [from] - more tables to join (the text search stems)
342
+ * @returns {string}
343
+ * @private
344
+ */
345
+ #conceptQuery(conditions, from = '') {
346
+ const cf = this.getCodeField();
347
+ return `SELECT rxnconso.${cf} AS ${cf}, STR, SUPPRESS, MIN(${DISPLAY_RANK_SQL}) AS rnk FROM rxnconso${from} ` +
348
+ `WHERE SAB = $sab AND TTY <> 'SY' ${conditions} GROUP BY rxnconso.${cf}`;
349
+ }
295
350
 
351
+ #conceptFromRow(row) {
352
+ const concept = new RxNormConcept(row[this.getCodeField()], row.STR);
353
+ concept.suppress = row.SUPPRESS || 'N';
296
354
  return concept;
297
355
  }
298
356
 
357
+ #dbGet(sql, params) {
358
+ return new Promise((resolve, reject) => {
359
+ this.db.get(sql, params, (err, row) => err ? reject(err) : resolve(row));
360
+ });
361
+ }
362
+
363
+ #dbAll(sql, params) {
364
+ return new Promise((resolve, reject) => {
365
+ this.db.all(sql, params, (err, rows) => err ? reject(err) : resolve(rows));
366
+ });
367
+ }
368
+
299
369
  // Iterator methods
300
370
  async iterator(context) {
301
371
 
302
372
 
303
373
  if (!context) {
304
374
  // Iterate all codes
305
- const query = `SELECT ${this.getCodeField()}, STR FROM rxnconso WHERE SAB = ? AND TTY <> 'SY' ORDER BY ${this.getCodeField()}`;
375
+ const query = this.#conceptQuery('') + ` ORDER BY rxnconso.${this.getCodeField()}`;
306
376
  return new RxNormIteratorContext(query, { sab: this.getSAB() }, this.totalCodeCount);
307
377
  } else {
308
378
  // No hierarchical iteration for specific contexts in this implementation. The expander
@@ -326,8 +396,7 @@ class RxNormServices extends CodeSystemProvider {
326
396
  const row = iteratorContext.results[iteratorContext.cursor];
327
397
  iteratorContext.next();
328
398
 
329
- const concept = new RxNormConcept(row[this.getCodeField()], row.STR);
330
- return concept;
399
+ return this.#conceptFromRow(row);
331
400
  }
332
401
 
333
402
  async #executeIterator(iteratorContext) {
@@ -345,36 +414,42 @@ class RxNormServices extends CodeSystemProvider {
345
414
  }
346
415
 
347
416
  // Filter support
348
- async doesFilter(prop, op, value) {
349
-
417
+ //
418
+ // Every filter is a condition on the concept (rxnconso.<code field> IN ...), not on the atom,
419
+ // so a concept with several atoms (SCD + PSN + TMSY...) is one result, whichever atoms matched.
420
+ // Filters in one include are ANDed. TTY, STY, SAB, concept (or code), and the relationships
421
+ // (REL or RELA, with CUI: or AUI: values) all take = or in; in is the union
350
422
 
351
- let propUC = prop.toUpperCase();
352
-
353
- // TTY filters
354
- if (propUC === 'TTY' && ['=', 'in'].includes(op)) {
355
- return true;
423
+ /**
424
+ * @returns {string[]} the values of an = or in filter
425
+ * @private
426
+ */
427
+ static #filterValues(op, value) {
428
+ if (op === 'in') {
429
+ return (value || '').split(',').map(v => v.trim()).filter(v => v);
356
430
  }
431
+ return value ? [value.trim()] : [];
432
+ }
357
433
 
358
- // STY filter
359
- if (propUC === 'STY' && op === '=') {
360
- return true;
361
- }
434
+ static #isRelValue(v) {
435
+ return v.startsWith('CUI:') || v.startsWith('AUI:');
436
+ }
362
437
 
363
- // SAB filter
364
- if (propUC === 'SAB' && op === '=') {
365
- return true;
438
+ async doesFilter(prop, op, value) {
439
+ if (!['=', 'in'].includes(op)) {
440
+ return false;
366
441
  }
367
-
368
- // Relationship filters (REL values like 'SY', 'RN', etc.)
369
- if (this.rels.includes(prop) && op === '=' && (value.startsWith('CUI:') || value.startsWith('AUI:'))) {
370
- return true;
442
+ const values = RxNormServices.#filterValues(op, value);
443
+ if (values.length === 0) {
444
+ return false;
371
445
  }
372
-
373
- // Relationship type filters (RELA values)
374
- if (this.reltypes.includes(prop) && op === '=' && (value.startsWith('CUI:') || value.startsWith('AUI:'))) {
446
+ const propUC = prop.toUpperCase();
447
+ if (['TTY', 'STY', 'SAB'].includes(propUC) || prop === 'concept' || prop === 'code') {
375
448
  return true;
376
449
  }
377
-
450
+ if (this.rels.includes(prop) || this.reltypes.includes(prop)) {
451
+ return values.every(v => RxNormServices.#isRelValue(v));
452
+ }
378
453
  return false;
379
454
  }
380
455
 
@@ -384,55 +459,47 @@ class RxNormServices extends CodeSystemProvider {
384
459
  }
385
460
 
386
461
  async filter(filterContext, forIteration, prop, op, value) {
387
-
388
-
389
462
  const filter = new RxNormFilterHolder();
390
- let propUC = prop.toUpperCase();
463
+ const propUC = prop.toUpperCase();
464
+ const cf = this.getCodeField();
465
+ const values = ['=', 'in'].includes(op) ? RxNormServices.#filterValues(op, value) : [];
466
+ // each filter gets its own parameter names, so two filters of the same kind in one
467
+ // include don't overwrite each other's values (or the SAB the whole query is about)
468
+ const pfx = `f${filterContext.filters.length}_`;
469
+ const params = {};
470
+ const list = (name, vals) => vals.map((v, i) => {
471
+ params[`${pfx}${name}${i}`] = v;
472
+ return `$${pfx}${name}${i}`;
473
+ }).join(', ');
391
474
 
392
475
  let sql = '';
393
- let params = {};
394
-
395
- if (op === 'in' && propUC === 'TTY') {
396
- const values = value.split(',').map(v => v.trim()).filter(v => v);
397
- const placeholders = values.map((_, i) => `$tty${i}`).join(',');
398
- sql = `AND TTY IN (${placeholders})`;
399
- values.forEach((val, i) => {
400
- params[`tty${i}`] = this.#sqlWrapString(val);
401
- });
402
- } else if (op === '=') {
403
- if (propUC === 'STY') {
404
- sql = `AND ${this.getCodeField()} IN (SELECT RXCUI FROM rxnsty WHERE TUI = $sty)`;
405
- params.sty = this.#sqlWrapString(value);
476
+ if (values.length > 0) {
477
+ if (propUC === 'TTY') {
478
+ sql = `AND rxnconso.${cf} IN (SELECT ${cf} FROM rxnconso WHERE SAB = $sab AND TTY IN (${list('tty', values)}))`;
479
+ } else if (propUC === 'STY') {
480
+ sql = `AND rxnconso.${cf} IN (SELECT RXCUI FROM rxnsty WHERE TUI IN (${list('sty', values)}))`;
406
481
  } else if (propUC === 'SAB') {
407
- sql = `AND ${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE SAB = $sab)`;
408
- params.sab = this.#sqlWrapString(value);
409
- } else if (propUC === 'TTY') {
410
- sql = `AND TTY = $tty`;
411
- params.tty = this.#sqlWrapString(value);
412
- } else if (this.rels.includes(prop)) {
413
- if (value.startsWith('CUI:')) {
414
- const cui = value.substring(4);
415
- sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI2 FROM rxnrel WHERE REL = $rel AND RXCUI1 = $cui2)))`;
416
- params.rel = this.#sqlWrapString(prop);
417
- params.cui2 = this.#sqlWrapString(cui);
418
- } else if (value.startsWith('AUI:')) {
419
- const aui = value.substring(4);
420
- sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI2 FROM rxnrel WHERE REL = $rel AND RXAUI1 = $aui2)))`;
421
- params.rel = this.#sqlWrapString(prop);
422
- params.aui2 = this.#sqlWrapString(aui);
482
+ sql = `AND rxnconso.${cf} IN (SELECT ${cf} FROM rxnconso WHERE SAB IN (${list('sab', values)}))`;
483
+ } else if (prop === 'concept' || prop === 'code') {
484
+ sql = `AND rxnconso.${cf} IN (${list('code', values)})`;
485
+ } else if ((this.rels.includes(prop) || this.reltypes.includes(prop)) && values.every(v => RxNormServices.#isRelValue(v))) {
486
+ // RXNREL rows read 'RXCUI2 <RELA> RXCUI1': has_ingredient = CUI:161 is the concepts
487
+ // that have acetaminophen as an ingredient. Concept-level rows: RxNorm's own (SAB=RXNORM)
488
+ const field = this.rels.includes(prop) ? 'REL' : 'RELA';
489
+ params[`${pfx}rel`] = prop;
490
+ const cuis = values.filter(v => v.startsWith('CUI:')).map(v => v.substring(4));
491
+ const auis = values.filter(v => v.startsWith('AUI:')).map(v => v.substring(4));
492
+ const targets = [];
493
+ if (cuis.length > 0) {
494
+ params[`${pfx}rsab`] = RELATIONSHIP_SAB;
495
+ targets.push(`rxnconso.${cf} IN (SELECT RXCUI2 FROM rxnrel WHERE ${field} = $${pfx}rel AND SAB = $${pfx}rsab AND RXCUI1 IN (${list('cui', cuis)}))`);
423
496
  }
424
- } else if (this.reltypes.includes(prop)) {
425
- if (value.startsWith('CUI:')) {
426
- const cui = value.substring(4);
427
- sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXCUI IN (SELECT RXCUI2 FROM rxnrel WHERE RELA = $rela AND RXCUI1 = $cui2)))`;
428
- params.rela = this.#sqlWrapString(prop);
429
- params.cui2 = this.#sqlWrapString(cui);
430
- } else if (value.startsWith('AUI:')) {
431
- const aui = value.substring(4);
432
- sql = `AND (${this.getCodeField()} IN (SELECT ${this.getCodeField()} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI2 FROM rxnrel WHERE RELA = $rela AND RXAUI1 = $aui2)))`;
433
- params.rela = this.#sqlWrapString(prop);
434
- params.aui2 = this.#sqlWrapString(aui);
497
+ if (auis.length > 0) {
498
+ // Atom-level rows leave RXCUI2 empty, and most come from other sources (MMSL, MTHSPL...):
499
+ // the target atom's concept comes from rxnconso, and every source's rows count
500
+ targets.push(`rxnconso.${cf} IN (SELECT ${cf} FROM rxnconso WHERE RXAUI IN (SELECT RXAUI2 FROM rxnrel WHERE ${field} = $${pfx}rel AND RXAUI1 IN (${list('aui', auis)})))`);
435
501
  }
502
+ sql = `AND (${targets.join(' OR ')})`;
436
503
  }
437
504
  }
438
505
 
@@ -475,9 +542,11 @@ class RxNormServices extends CodeSystemProvider {
475
542
  } else {
476
543
  // Not in the stem table, so match the token against the text itself. Left as a
477
544
  // plain (non-text) filter so that executeFilters does not join RXNSTEMS for it.
478
- // Unindexed, but it only ever narrows rows the other tokens have already found,
479
- // and the tokens are alphanumeric, so there are no LIKE wildcards to escape.
480
- rxnormFilter.sql = ` AND STR LIKE $text${i}`;
545
+ // Unindexed, but it only ever narrows concepts the other tokens have already found,
546
+ // and the tokens are alphanumeric, so there are no LIKE wildcards to escape. Any of
547
+ // the concept's RxNorm names can match, as for the stems
548
+ const cf = this.getCodeField();
549
+ rxnormFilter.sql = ` AND EXISTS (SELECT 1 FROM rxnconso t WHERE t.${cf} = rxnconso.${cf} AND t.SAB = $sab AND t.STR LIKE $text${i})`;
481
550
  rxnormFilter.params[`text${i}`] = '%' + ((filter.words && filter.words[i]) || stem) + '%';
482
551
  }
483
552
 
@@ -530,12 +599,13 @@ class RxNormServices extends CodeSystemProvider {
530
599
  }
531
600
  }
532
601
 
533
- const fullQuery = `SELECT ${this.getCodeField()}, STR ${sql2} WHERE SAB = $sab AND TTY <> 'SY' ${sql1}`;
534
602
  allParams.sab = this.getSAB();
535
603
 
536
604
  // Create a single filter holder with the combined query
537
605
  const combinedFilter = new RxNormFilterHolder();
538
- combinedFilter.sql = fullQuery;
606
+ combinedFilter.conditions = sql1;
607
+ combinedFilter.from = sql2.substring('FROM rxnconso'.length);
608
+ combinedFilter.sql = this.#conceptQuery(sql1, combinedFilter.from);
539
609
  combinedFilter.params = allParams;
540
610
 
541
611
  return [combinedFilter];
@@ -550,8 +620,6 @@ class RxNormServices extends CodeSystemProvider {
550
620
  }
551
621
 
552
622
  async filterMore(filterContext, set) {
553
-
554
-
555
623
  if (!set.executed) {
556
624
  await this.#executeFilter(set);
557
625
  }
@@ -560,8 +628,6 @@ class RxNormServices extends CodeSystemProvider {
560
628
  }
561
629
 
562
630
  async filterConcept(filterContext, set) {
563
-
564
-
565
631
  if (!set.executed) {
566
632
  await this.#executeFilter(set);
567
633
  }
@@ -572,36 +638,18 @@ class RxNormServices extends CodeSystemProvider {
572
638
 
573
639
  const row = set.results[set.cursor];
574
640
  set.cursor++;
575
-
576
- const concept = new RxNormConcept(row[this.getCodeField()], row.STR);
577
- return concept;
641
+ return this.#conceptFromRow(row);
578
642
  }
579
643
 
580
644
  async filterLocate(filterContext, set, code) {
581
-
582
-
583
- return new Promise((resolve, reject) => {
584
- // Build query to check if code exists in filter
585
- const checkQuery = `SELECT ${this.getCodeField()}, STR FROM rxnconso WHERE SAB = $sab AND TTY <> 'SY' AND ${this.getCodeField()} = $code ${set.sql.replace(/SELECT.*?FROM rxnconso/, '').replace(/WHERE SAB = \$sab AND TTY <> 'SY'/, '')}`;
586
-
587
- const params = { ...set.params, code };
588
-
589
- this.db.get(checkQuery, this.#buildParamArray(checkQuery, params), (err, row) => {
590
- if (err) {
591
- reject(err);
592
- } else if (!row) {
593
- resolve(null);
594
- } else {
595
- const concept = new RxNormConcept(row[this.getCodeField()], row.STR);
596
- resolve(concept);
597
- }
598
- });
599
- });
645
+ const cf = this.getCodeField();
646
+ const sql = this.#conceptQuery((set.conditions || '') + ` AND rxnconso.${cf} = $code`, set.from || '');
647
+ const params = { ...set.params, code };
648
+ const row = await this.#dbGet(sql, this.#buildParamArray(sql, params));
649
+ return row ? this.#conceptFromRow(row) : null;
600
650
  }
601
651
 
602
652
  async filterCheck(filterContext, set, concept) {
603
-
604
-
605
653
  if (!(concept instanceof RxNormConcept)) {
606
654
  return false;
607
655
  }
@@ -609,8 +657,10 @@ class RxNormServices extends CodeSystemProvider {
609
657
  if (!set.executed) {
610
658
  await this.#executeFilter(set);
611
659
  }
612
-
613
- return set.results.some(row => row[this.getCodeField()] === concept.code);
660
+ if (!set.codes) {
661
+ set.codes = new Set(set.results.map(row => row[this.getCodeField()]));
662
+ }
663
+ return set.codes.has(concept.code);
614
664
  }
615
665
 
616
666
  async #executeFilter(filter) {
@@ -653,23 +703,31 @@ class RxNormServices extends CodeSystemProvider {
653
703
  return paramArray;
654
704
  }
655
705
 
656
- #sqlWrapString(str) {
657
- return str.replace(/'/g, "''");
658
- }
659
-
660
- // Subsumption testing
706
+ // Subsumption: RxNorm codes have no subsumption relationships (no hierarchy), so two codes
707
+ // that both exist are not-subsumed (the caller deals with equal codes: equivalent)
661
708
  async subsumesTest(codeA, codeB) {
662
709
  await this.#ensureContext(codeA);
663
710
  await this.#ensureContext(codeB);
664
- return 'not-subsumed'; // Not implemented yet
711
+ return 'not-subsumed';
712
+ }
713
+
714
+ /**
715
+ * The concept's properties for $expand: built by extendLookup, so that an expansion reports
716
+ * the same properties $lookup does (see CodeSystemProvider._propertiesFromLookup)
717
+ */
718
+ async properties(context, names) {
719
+ return this._propertiesFromLookup(context, names);
665
720
  }
666
721
 
667
722
  // Extension for lookup operation
723
+ //
724
+ // TTY, STY and SAB are returned unless other properties are asked for; the relationships
725
+ // (one property per REL/RELA, valued with the bare RXCUI of the other concept, RxNorm's
726
+ // own relationships only) only when asked for, by name or with *
668
727
  async extendLookup(ctxt, props, params) {
669
728
  validateArrayParameter(props, 'props', String);
670
729
  validateArrayParameter(params, 'params', Object);
671
730
 
672
-
673
731
  if (typeof ctxt === 'string') {
674
732
  const located = await this.locate(ctxt);
675
733
  if (!located.context) {
@@ -681,36 +739,43 @@ class RxNormServices extends CodeSystemProvider {
681
739
  if (!(ctxt instanceof RxNormConcept)) {
682
740
  throw new Error('Invalid context for RxNorm lookup');
683
741
  }
684
-
685
- // Set abstract status
686
- params.abstract = false;
687
-
688
- // Add designations
689
- const designations = new Designations(this.opContext.i18n.languageDefinitions);
690
- await this.designations(ctxt, designations);
691
- for (const designation of designations) {
692
- this.#addProperty(params, 'designation', 'display', designation.value, designation.language);
742
+ if (ctxt.archived) {
743
+ return;
693
744
  }
694
- }
695
745
 
696
- #addProperty(params, type, name, value, language = null) {
697
- if (!params.parameter) {
698
- params.parameter = [];
746
+ const cf = this.getCodeField();
747
+ if (this._hasProp(props, 'TTY', true)) {
748
+ const rows = await this.#dbAll(`SELECT DISTINCT TTY FROM rxnconso WHERE ${cf} = ? AND SAB = ? ORDER BY TTY`, [ctxt.code, this.getSAB()]);
749
+ for (const row of rows) {
750
+ this._addCodeProperty(params, 'property', 'TTY', row.TTY);
751
+ }
699
752
  }
700
-
701
- const property = {
702
- name: type,
703
- part: [
704
- { name: 'code', valueCode: name },
705
- { name: 'value', valueString: value }
706
- ]
707
- };
708
-
709
- if (language) {
710
- property.part.push({ name: 'language', valueCode: language });
753
+ if (this._hasProp(props, 'STY', true)) {
754
+ const rows = await this.#dbAll('SELECT DISTINCT TUI FROM rxnsty WHERE RXCUI = ? ORDER BY TUI', [ctxt.code]);
755
+ for (const row of rows) {
756
+ this._addCodeProperty(params, 'property', 'STY', row.TUI);
757
+ }
758
+ }
759
+ if (this._hasProp(props, 'SAB', true)) {
760
+ const rows = await this.#dbAll(`SELECT DISTINCT SAB FROM rxnconso WHERE ${cf} = ? ORDER BY SAB`, [ctxt.code]);
761
+ for (const row of rows) {
762
+ this._addCodeProperty(params, 'property', 'SAB', row.SAB);
763
+ }
764
+ }
765
+ if (props && props.length > 0) {
766
+ const rows = await this.#dbAll(
767
+ 'SELECT DISTINCT REL, RELA, RXCUI1 FROM rxnrel WHERE RXCUI2 = ? AND SAB = ? ORDER BY RELA, REL, RXCUI1',
768
+ [ctxt.code, RELATIONSHIP_SAB]);
769
+ const seen = new Set();
770
+ for (const row of rows) {
771
+ const name = row.RELA || row.REL;
772
+ const key = name + '|' + row.RXCUI1;
773
+ if (name && row.RXCUI1 && !seen.has(key) && this._hasProp(props, name, false)) {
774
+ seen.add(key);
775
+ this._addCodeProperty(params, 'property', name, row.RXCUI1);
776
+ }
777
+ }
711
778
  }
712
-
713
- params.parameter.push(property);
714
779
  }
715
780
 
716
781
  versionAlgorithm() {
@@ -719,10 +784,15 @@ class RxNormServices extends CodeSystemProvider {
719
784
  }
720
785
 
721
786
  class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
722
- constructor(i18n, dbPath, isNCI = false) {
787
+ /**
788
+ * @param {Object} [options] - {readOnly: true} in a tx-worker (cluster.md): the master
789
+ * makes the indexes; this process opens the file read-only
790
+ */
791
+ constructor(i18n, dbPath, isNCI = false, options = {}) {
723
792
  super(i18n);
724
793
  this.dbPath = dbPath;
725
794
  this.isNCI = isNCI;
795
+ this.readOnly = !!options.readOnly;
726
796
  this._loaded = false;
727
797
  this._sharedData = null;
728
798
  }
@@ -746,14 +816,32 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
746
816
  }
747
817
  }
748
818
 
819
+ #open() {
820
+ return this.readOnly ? new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY) : new sqlite3.Database(this.dbPath);
821
+ }
822
+
749
823
  async load() {
750
- const db = new sqlite3.Database(this.dbPath);
824
+ const db = this.#open();
751
825
 
752
826
  try {
753
- await new Promise((resolve, reject) => {
754
- db.run(`CREATE INDEX IF NOT EXISTS idx_rxnstems_cui_stem ON RXNSTEMS(CUI, stem)`,
755
- err => err ? reject(err) : resolve());
756
- });
827
+ // indexes the importer makes now, for databases imported before it did. A one-off cost
828
+ // the first time an older database is loaded. A database that can't be written (read-only
829
+ // file or mount) is used as it is: the indexes only make lookups faster. A tx-worker
830
+ // leaves them to the master
831
+ for (const sql of this.readOnly ? [] : RXNORM_LOAD_INDEXES) {
832
+ await new Promise((resolve, reject) => {
833
+ db.run(sql, guarded(reject, err => {
834
+ if (err && err.code === 'SQLITE_READONLY') {
835
+ Logger.getInstance().child({module: 'tx-rxnorm'}).warn(`${this.dbPath} is read-only; lookups will be slower without: ${sql}`);
836
+ resolve();
837
+ } else if (err) {
838
+ reject(err);
839
+ } else {
840
+ resolve();
841
+ }
842
+ }));
843
+ });
844
+ }
757
845
 
758
846
  this._sharedData = {
759
847
  version: '',
@@ -773,7 +861,8 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
773
861
 
774
862
  // Get total count
775
863
  const sab = this.isNCI ? 'NCI' : 'RXNORM';
776
- this._sharedData.totalCodeCount = await this.#getCount(db, `SELECT COUNT(RXCUI) FROM rxnconso WHERE SAB = ? AND TTY <> 'SY'`, [sab]);
864
+ // concepts, not atoms
865
+ this._sharedData.totalCodeCount = await this.#getCount(db, `SELECT COUNT(DISTINCT RXCUI) FROM rxnconso WHERE SAB = ? AND TTY <> 'SY'`, [sab]);
777
866
 
778
867
  } finally {
779
868
  db.close();
@@ -782,8 +871,8 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
782
871
  }
783
872
 
784
873
  async #readVersion(db) {
785
- return new Promise((resolve) => {
786
- db.get('SELECT version FROM RXNVer', (err, row) => {
874
+ return new Promise((resolve, reject) => {
875
+ db.get('SELECT version FROM RXNVer', guarded(reject, (err, row) => {
787
876
  if (err || !row) {
788
877
  // Fallback: try to extract version from database path
789
878
  const dbDetails = this.dbPath;
@@ -805,31 +894,31 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
805
894
  } else {
806
895
  resolve(row.version.toString());
807
896
  }
808
- });
897
+ }));
809
898
  });
810
899
  }
811
900
 
812
901
  async #loadList(db, sql) {
813
902
  return new Promise((resolve, reject) => {
814
- db.all(sql, (err, rows) => {
903
+ db.all(sql, guarded(reject, (err, rows) => {
815
904
  if (err) {
816
905
  reject(err);
817
906
  } else {
818
907
  resolve(rows.map(row => Object.values(row)[0]));
819
908
  }
820
- });
909
+ }));
821
910
  });
822
911
  }
823
912
 
824
913
  async #getCount(db, sql, params = []) {
825
914
  return new Promise((resolve, reject) => {
826
- db.get(sql, params, (err, row) => {
915
+ db.get(sql, params, guarded(reject, (err, row) => {
827
916
  if (err) {
828
917
  reject(err);
829
918
  } else {
830
919
  resolve(row ? Object.values(row)[0] : 0);
831
920
  }
832
- });
921
+ }));
833
922
  });
834
923
  }
835
924
 
@@ -842,7 +931,7 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
842
931
  this.recordUse();
843
932
 
844
933
  // Create fresh database connection for this provider instance
845
- const db = new sqlite3.Database(this.dbPath);
934
+ const db = this.#open();
846
935
 
847
936
  return new RxNormServices(opContext, supplements, db, this._sharedData, this.isNCI);
848
937
  }
@@ -855,6 +944,21 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
855
944
  return this.name()+"-"+this.version();
856
945
  }
857
946
 
947
+ /**
948
+ * RxNorm codes link to RxNav (as DataRenderer.getLinkForCode in org.hl7.fhir.core). The NCI
949
+ * code system's codes are NCI Thesaurus codes (the SCUIs of SAB NCI), which link to EVS
950
+ * Explore: the NCIt browser link core uses now lands on EVS Explore's home page.
951
+ */
952
+ codeLink(context) {
953
+ if (!context || !context.code) {
954
+ return undefined;
955
+ }
956
+ const code = encodeURIComponent(context.code);
957
+ return this.isNCI
958
+ ? `https://evsexplore.semantics.cancer.gov/evsexplore/concept/ncit/${code}`
959
+ : `https://mor.nlm.nih.gov/RxNav/search?searchBy=RXCUI&searchTerm=${code}`;
960
+ }
961
+
858
962
  describeVersion(version) {
859
963
  try {
860
964
  return formatDateMMDDYYYY(version);
@@ -866,15 +970,15 @@ class RxNormTypeServicesFactory extends CodeSystemFactoryProvider {
866
970
 
867
971
  // Specific RxNorm implementation
868
972
  class RxNormServicesFactory extends RxNormTypeServicesFactory {
869
- constructor(languageDefinitions, dbPath) {
870
- super(languageDefinitions, dbPath, false);
973
+ constructor(languageDefinitions, dbPath, options = {}) {
974
+ super(languageDefinitions, dbPath, false, options);
871
975
  }
872
976
  }
873
977
 
874
978
  // NCI Meta implementation
875
979
  class NCIServicesFactory extends RxNormTypeServicesFactory {
876
- constructor(languageDefinitions, dbPath) {
877
- super(languageDefinitions, dbPath, true);
980
+ constructor(languageDefinitions, dbPath, options = {}) {
981
+ super(languageDefinitions, dbPath, true, options);
878
982
  }
879
983
  }
880
984