fhirsmith 0.4.2 → 0.5.1

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 (92) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/library/cron-utilities.js +136 -0
  4. package/library/html-server.js +13 -29
  5. package/library/html.js +3 -8
  6. package/library/languages.js +160 -37
  7. package/library/package-manager.js +48 -1
  8. package/library/utilities.js +100 -19
  9. package/package.json +2 -2
  10. package/packages/package-crawler.js +6 -1
  11. package/packages/packages.js +38 -54
  12. package/publisher/publisher.js +19 -27
  13. package/registry/api.js +11 -10
  14. package/registry/crawler.js +31 -29
  15. package/registry/model.js +5 -26
  16. package/registry/registry.js +32 -41
  17. package/server.js +89 -12
  18. package/shl/shl.js +0 -18
  19. package/static/assets/js/statuspage.js +1 -9
  20. package/stats.js +39 -1
  21. package/token/token.js +14 -9
  22. package/translations/Messages.properties +2 -1
  23. package/tx/README.md +17 -6
  24. package/tx/cs/cs-api.js +19 -1
  25. package/tx/cs/cs-base.js +77 -0
  26. package/tx/cs/cs-country.js +46 -0
  27. package/tx/cs/cs-cpt.js +9 -5
  28. package/tx/cs/cs-cs.js +27 -13
  29. package/tx/cs/cs-lang.js +60 -22
  30. package/tx/cs/cs-loinc.js +69 -98
  31. package/tx/cs/cs-mimetypes.js +4 -0
  32. package/tx/cs/cs-ndc.js +6 -0
  33. package/tx/cs/cs-omop.js +16 -15
  34. package/tx/cs/cs-rxnorm.js +23 -1
  35. package/tx/cs/cs-snomed.js +283 -40
  36. package/tx/cs/cs-ucum.js +90 -70
  37. package/tx/importers/import-sct.module.js +371 -35
  38. package/tx/importers/readme.md +117 -7
  39. package/tx/library/bundle.js +5 -0
  40. package/tx/library/capabilitystatement.js +3 -142
  41. package/tx/library/codesystem.js +19 -173
  42. package/tx/library/conceptmap.js +4 -218
  43. package/tx/library/designations.js +14 -1
  44. package/tx/library/extensions.js +7 -0
  45. package/tx/library/namingsystem.js +3 -89
  46. package/tx/library/operation-outcome.js +8 -3
  47. package/tx/library/parameters.js +3 -2
  48. package/tx/library/renderer.js +10 -6
  49. package/tx/library/terminologycapabilities.js +3 -243
  50. package/tx/library/valueset.js +3 -235
  51. package/tx/library.js +100 -13
  52. package/tx/operation-context.js +23 -4
  53. package/tx/params.js +35 -38
  54. package/tx/provider.js +6 -5
  55. package/tx/sct/expressions.js +12 -3
  56. package/tx/tx-html.js +80 -89
  57. package/tx/tx.fhir.org.yml +6 -5
  58. package/tx/tx.js +163 -13
  59. package/tx/vs/vs-database.js +56 -39
  60. package/tx/vs/vs-package.js +21 -2
  61. package/tx/vs/vs-vsac.js +175 -39
  62. package/tx/workers/batch-validate.js +2 -0
  63. package/tx/workers/batch.js +2 -0
  64. package/tx/workers/expand.js +132 -112
  65. package/tx/workers/lookup.js +33 -14
  66. package/tx/workers/metadata.js +2 -2
  67. package/tx/workers/read.js +3 -2
  68. package/tx/workers/related.js +574 -0
  69. package/tx/workers/search.js +46 -9
  70. package/tx/workers/subsumes.js +13 -3
  71. package/tx/workers/translate.js +7 -3
  72. package/tx/workers/validate.js +258 -285
  73. package/tx/workers/worker.js +43 -39
  74. package/tx/xml/bundle-xml.js +237 -0
  75. package/tx/xml/xml-base.js +215 -64
  76. package/tx/xversion/xv-bundle.js +71 -0
  77. package/tx/xversion/xv-capabiliityStatement.js +137 -0
  78. package/tx/xversion/xv-codesystem.js +169 -0
  79. package/tx/xversion/xv-conceptmap.js +224 -0
  80. package/tx/xversion/xv-namingsystem.js +88 -0
  81. package/tx/xversion/xv-operationoutcome.js +27 -0
  82. package/tx/xversion/xv-parameters.js +87 -0
  83. package/tx/xversion/xv-resource.js +45 -0
  84. package/tx/xversion/xv-terminologyCapabilities.js +214 -0
  85. package/tx/xversion/xv-valueset.js +234 -0
  86. package/utilities/dev-proxy-server.js +126 -0
  87. package/utilities/explode-results.js +58 -0
  88. package/utilities/split-by-system.js +198 -0
  89. package/utilities/vsac-cs-fetcher.js +0 -0
  90. package/{windows-install.js → utilities/windows-install.js} +2 -0
  91. package/vcl/vcl.js +0 -18
  92. package/xig/xig.js +241 -230
@@ -154,9 +154,271 @@ class SnomedModule extends BaseTerminologyModule {
154
154
  return confirmed;
155
155
  }
156
156
 
157
+ /**
158
+ * Auto-detect edition and version from RF2 files
159
+ * Reads concept files to extract moduleId and effectiveTime
160
+ * Prefers non-International modules since national editions contain International content
161
+ */
162
+ async detectEditionAndVersion(sourceDir) {
163
+ const detected = {
164
+ edition: null,
165
+ version: null,
166
+ editionName: null
167
+ };
168
+
169
+ // Known editions mapping
170
+ const editions = {
171
+ "900000000000207008": "International",
172
+ "731000124108": "US Edition",
173
+ "32506021000036107": "Australian Edition",
174
+ "449081005": "Spanish Edition (International)",
175
+ "11000279109": "Czech Edition",
176
+ "554471000005108": "Danish Edition",
177
+ "11000146104": "Dutch Edition",
178
+ "45991000052106": "Swedish Edition",
179
+ "83821000000107": "UK Edition",
180
+ "11000172109": "Belgian Edition",
181
+ "11000221109": "Argentinian Edition",
182
+ "11000234105": "Austrian Edition",
183
+ "20621000087109": "Canadian Edition (English)",
184
+ "20611000087101": "Canadian Edition (French)",
185
+ "11000181102": "Estonian Edition",
186
+ "11000229106": "Finnish Edition",
187
+ "11000274103": "German Edition",
188
+ "1121000189102": "Indian Edition",
189
+ "11000220105": "Irish Edition",
190
+ "21000210109": "New Zealand Edition",
191
+ "51000202101": "Norwegian Edition",
192
+ "11000267109": "Korean Edition",
193
+ "900000001000122104": "Spanish Edition (Spain)",
194
+ "2011000195101": "Swiss Edition",
195
+ "999000021000000109": "UK Clinical Edition",
196
+ "5631000179106": "Uruguayan Edition",
197
+ "21000325107": "Chilean Edition",
198
+ "5991000124107": "US Edition + ICD10CM"
199
+ };
200
+
201
+ // Filename patterns for edition codes (2-letter country codes in filenames)
202
+ const filenameEditionCodes = {
203
+ 'INT': '900000000000207008',
204
+ 'US': '731000124108',
205
+ 'AU': '32506021000036107',
206
+ 'UK': '83821000000107',
207
+ 'BE': '11000172109',
208
+ 'NL': '11000146104',
209
+ 'SE': '45991000052106',
210
+ 'DK': '554471000005108',
211
+ 'NO': '51000202101',
212
+ 'FI': '11000229106',
213
+ 'DE': '11000274103',
214
+ 'AT': '11000234105',
215
+ 'CH': '2011000195101',
216
+ 'ES': '900000001000122104',
217
+ 'AR': '11000221109',
218
+ 'CL': '21000325107',
219
+ 'UY': '5631000179106',
220
+ 'NZ': '21000210109',
221
+ 'IE': '11000220105',
222
+ 'EE': '11000181102',
223
+ 'CZ': '11000279109',
224
+ 'KR': '11000267109',
225
+ 'IN': '1121000189102'
226
+ };
227
+
228
+ const INTERNATIONAL_MODULE = "900000000000207008";
229
+
230
+ try {
231
+ const files = this.discoverRF2Files(sourceDir);
232
+
233
+ if (files.concepts.length > 0) {
234
+ const conceptFile = files.concepts[0];
235
+ const fileName = path.basename(conceptFile);
236
+
237
+ // Try to extract edition and version from filename
238
+ // Patterns: sct2_Concept_Snapshot_BE1000172_20260215.txt
239
+ // sct2_Concept_Snapshot_INT_20250201.txt
240
+ // sct2_Concept_Snapshot-BE_20260215.txt
241
+ // sct2_Concept_Snapshot-en-BE_20260215.txt
242
+
243
+ // First, extract the version (8-digit date) - it's always at the end before .txt
244
+ const versionMatch = fileName.match(/_(\d{8})\.txt$/i);
245
+ if (versionMatch) {
246
+ detected.version = versionMatch[1];
247
+ }
248
+
249
+ // Then extract the edition code (2-3 letters after Snapshot)
250
+ const editionMatch = fileName.match(/Snapshot[_-](?:en-)?([A-Z]{2,3})/i);
251
+ if (editionMatch) {
252
+ const editionCode = editionMatch[1].toUpperCase();
253
+
254
+ // Look up edition from the 2-3 letter code
255
+ if (filenameEditionCodes[editionCode]) {
256
+ detected.edition = filenameEditionCodes[editionCode];
257
+ detected.editionName = editions[detected.edition];
258
+ }
259
+ }
260
+
261
+ // If we didn't detect edition from filename, scan the file for non-International modules
262
+ if (!detected.edition) {
263
+ const moduleIds = new Map(); // moduleId -> count
264
+ let latestEffectiveTime = null;
265
+
266
+ const rl = readline.createInterface({
267
+ input: fs.createReadStream(conceptFile),
268
+ crlfDelay: Infinity
269
+ });
270
+
271
+ let lineCount = 0;
272
+ const MAX_LINES_TO_SCAN = 50000; // Scan enough lines to find edition-specific content
273
+
274
+ for await (const line of rl) {
275
+ lineCount++;
276
+ if (lineCount === 1) continue; // Skip header
277
+
278
+ const parts = line.split('\t');
279
+ if (parts.length >= 4) {
280
+ const effectiveTime = parts[1];
281
+ const moduleId = parts[3];
282
+
283
+ // Track the latest effectiveTime for version
284
+ if (effectiveTime && /^\d{8}$/.test(effectiveTime)) {
285
+ if (!latestEffectiveTime || effectiveTime > latestEffectiveTime) {
286
+ latestEffectiveTime = effectiveTime;
287
+ }
288
+ }
289
+
290
+ // Count module occurrences
291
+ if (moduleId) {
292
+ moduleIds.set(moduleId, (moduleIds.get(moduleId) || 0) + 1);
293
+ }
294
+ }
295
+
296
+ if (lineCount >= MAX_LINES_TO_SCAN) break;
297
+ }
298
+
299
+ rl.close();
300
+
301
+ // Use latest effectiveTime as version if not already detected
302
+ if (!detected.version && latestEffectiveTime) {
303
+ detected.version = latestEffectiveTime;
304
+ }
305
+
306
+ // Find the best edition - prefer non-International modules
307
+ let bestModule = null;
308
+ let bestCount = 0;
309
+
310
+ for (const [moduleId, count] of moduleIds) {
311
+ // Skip International module if we have other known editions
312
+ if (moduleId === INTERNATIONAL_MODULE) {
313
+ continue;
314
+ }
315
+
316
+ // Prefer known editions with highest count
317
+ if (editions[moduleId] && count > bestCount) {
318
+ bestModule = moduleId;
319
+ bestCount = count;
320
+ }
321
+ }
322
+
323
+ // If no non-International module found, fall back to International
324
+ if (!bestModule && moduleIds.has(INTERNATIONAL_MODULE)) {
325
+ bestModule = INTERNATIONAL_MODULE;
326
+ }
327
+
328
+ if (bestModule) {
329
+ detected.edition = bestModule;
330
+ detected.editionName = editions[bestModule] || `Unknown Edition (${bestModule})`;
331
+ }
332
+ }
333
+
334
+ // If we still don't have version, read first few lines
335
+ if (!detected.version) {
336
+ const fd = fs.openSync(conceptFile, 'r');
337
+ try {
338
+ const buffer = Buffer.alloc(2000);
339
+ const bytesRead = fs.readSync(fd, buffer, 0, 2000, 0);
340
+ const content = buffer.toString('utf8', 0, bytesRead);
341
+ const lines = content.split('\n');
342
+
343
+ if (lines.length >= 2) {
344
+ const parts = lines[1].split('\t');
345
+ if (parts.length >= 2 && /^\d{8}$/.test(parts[1])) {
346
+ detected.version = parts[1];
347
+ }
348
+ }
349
+ } finally {
350
+ fs.closeSync(fd);
351
+ }
352
+ }
353
+ }
354
+ } catch (error) {
355
+ // Silent fail - auto-detection is best-effort
356
+ if (this.config?.verbose) {
357
+ console.log(`Auto-detection warning: ${error.message}`);
358
+ }
359
+ }
360
+
361
+ return detected;
362
+ }
363
+
364
+ /**
365
+ * Check if the source directory contains International content
366
+ * (i.e., concepts with the International module ID)
367
+ * If true, this is a combined/complete release that doesn't need a separate base
368
+ */
369
+ async checkForInternationalContent(sourceDir) {
370
+ const INTERNATIONAL_MODULE = "900000000000207008";
371
+
372
+ try {
373
+ const files = this.discoverRF2Files(sourceDir);
374
+ if (files.concepts.length === 0) return false;
375
+
376
+ const conceptFile = files.concepts[0];
377
+ const rl = readline.createInterface({
378
+ input: fs.createReadStream(conceptFile),
379
+ crlfDelay: Infinity
380
+ });
381
+
382
+ let lineCount = 0;
383
+ let hasInternational = false;
384
+
385
+ for await (const line of rl) {
386
+ lineCount++;
387
+ if (lineCount === 1) continue; // Skip header
388
+
389
+ const parts = line.split('\t');
390
+ if (parts.length >= 4 && parts[3] === INTERNATIONAL_MODULE) {
391
+ hasInternational = true;
392
+ break;
393
+ }
394
+
395
+ // Only check first 100 lines - International concepts are always at the start
396
+ if (lineCount > 100) break;
397
+ }
398
+
399
+ rl.close();
400
+ return hasInternational;
401
+ } catch (error) {
402
+ return false;
403
+ }
404
+ }
405
+
157
406
  async gatherSnomedConfig(options) {
158
407
  const baseConfig = await this.gatherCommonConfig(options);
159
408
 
409
+ // Try to auto-detect edition and version from RF2 files
410
+ let autoDetected = { edition: null, version: null, editionName: null };
411
+ if (baseConfig.source && !options.edition && !options.version && !options.uri) {
412
+ this.logInfo('Auto-detecting edition and version from RF2 files...');
413
+ autoDetected = await this.detectEditionAndVersion(baseConfig.source);
414
+
415
+ if (autoDetected.edition && autoDetected.version) {
416
+ this.logSuccess(`Detected: ${autoDetected.editionName} version ${autoDetected.version}`);
417
+ } else if (autoDetected.version) {
418
+ this.logInfo(`Detected version: ${autoDetected.version}`);
419
+ }
420
+ }
421
+
160
422
  const editions = {
161
423
  "900000000000207008": { name: "International", needsBase: false, lang: "en-US" },
162
424
  "731000124108": { name: "US Edition", needsBase: false, lang: "en-US" },
@@ -193,50 +455,112 @@ class SnomedModule extends BaseTerminologyModule {
193
455
 
194
456
  // Edition selection (if not provided via options and no URI override)
195
457
  if (!options.edition && !options.uri) {
196
- const editionChoices = Object.entries(editions).map(([id, info]) => ({
197
- name: info.name,
198
- value: id
199
- }));
200
-
201
- questions.push({
202
- type: 'list',
203
- name: 'edition',
204
- message: 'Select SNOMED CT Edition:',
205
- choices: editionChoices,
206
- default: '900000000000207008' // International edition
207
- });
458
+ // If we auto-detected the edition, use confirm instead of list
459
+ if (autoDetected.edition) {
460
+ questions.push({
461
+ type: 'confirm',
462
+ name: 'useDetectedEdition',
463
+ message: `Use detected edition: ${autoDetected.editionName} (${autoDetected.edition})?`,
464
+ default: true
465
+ });
466
+
467
+ questions.push({
468
+ type: 'list',
469
+ name: 'edition',
470
+ message: 'Select SNOMED CT Edition:',
471
+ choices: Object.entries(editions).map(([id, info]) => ({
472
+ name: info.name,
473
+ value: id
474
+ })),
475
+ default: autoDetected.edition,
476
+ when: (answers) => !answers.useDetectedEdition
477
+ });
478
+ } else {
479
+ const editionChoices = Object.entries(editions).map(([id, info]) => ({
480
+ name: info.name,
481
+ value: id
482
+ }));
483
+
484
+ questions.push({
485
+ type: 'list',
486
+ name: 'edition',
487
+ message: 'Select SNOMED CT Edition:',
488
+ choices: editionChoices,
489
+ default: '900000000000207008' // International edition
490
+ });
491
+ }
208
492
  }
209
493
 
210
494
  // Version in YYYYMMDD format (if not provided and no URI override)
211
495
  if (!options.version && !options.uri) {
212
- questions.push({
213
- type: 'input',
214
- name: 'version',
215
- message: 'Version (YYYYMMDD format, e.g., 20250801):',
216
- validate: (input) => {
217
- if (!input) return 'Version is required';
218
- if (!/^\d{8}$/.test(input)) return 'Version must be in YYYYMMDD format (8 digits)';
219
-
220
- // Basic date validation
221
- const year = parseInt(input.substring(0, 4));
222
- const month = parseInt(input.substring(4, 6));
223
- const day = parseInt(input.substring(6, 8));
224
-
225
- if (year < 1900 || year > 2100) return 'Invalid year';
226
- if (month < 1 || month > 12) return 'Invalid month';
227
- if (day < 1 || day > 31) return 'Invalid day';
496
+ // If we auto-detected the version, use confirm instead of input
497
+ if (autoDetected.version) {
498
+ questions.push({
499
+ type: 'confirm',
500
+ name: 'useDetectedVersion',
501
+ message: `Use detected version: ${autoDetected.version}?`,
502
+ default: true
503
+ });
228
504
 
229
- return true;
230
- }
231
- });
505
+ questions.push({
506
+ type: 'input',
507
+ name: 'version',
508
+ message: 'Version (YYYYMMDD format, e.g., 20250801):',
509
+ default: autoDetected.version,
510
+ when: (answers) => !answers.useDetectedVersion,
511
+ validate: (input) => {
512
+ if (!input) return 'Version is required';
513
+ if (!/^\d{8}$/.test(input)) return 'Version must be in YYYYMMDD format (8 digits)';
514
+
515
+ const year = parseInt(input.substring(0, 4));
516
+ const month = parseInt(input.substring(4, 6));
517
+ const day = parseInt(input.substring(6, 8));
518
+
519
+ if (year < 1900 || year > 2100) return 'Invalid year';
520
+ if (month < 1 || month > 12) return 'Invalid month';
521
+ if (day < 1 || day > 31) return 'Invalid day';
522
+
523
+ return true;
524
+ }
525
+ });
526
+ } else {
527
+ questions.push({
528
+ type: 'input',
529
+ name: 'version',
530
+ message: 'Version (YYYYMMDD format, e.g., 20250801):',
531
+ validate: (input) => {
532
+ if (!input) return 'Version is required';
533
+ if (!/^\d{8}$/.test(input)) return 'Version must be in YYYYMMDD format (8 digits)';
534
+
535
+ // Basic date validation
536
+ const year = parseInt(input.substring(0, 4));
537
+ const month = parseInt(input.substring(4, 6));
538
+ const day = parseInt(input.substring(6, 8));
539
+
540
+ if (year < 1900 || year > 2100) return 'Invalid year';
541
+ if (month < 1 || month > 12) return 'Invalid month';
542
+ if (day < 1 || day > 31) return 'Invalid day';
543
+
544
+ return true;
545
+ }
546
+ });
547
+ }
232
548
  }
233
549
 
234
550
  // Get answers for edition and version first
235
551
  const primaryAnswers = await inquirer.prompt(questions);
236
552
 
237
- // Determine the selected edition and version
238
- const selectedEdition = options.edition || primaryAnswers.edition;
239
- const selectedVersion = options.version || primaryAnswers.version;
553
+ // Determine the selected edition and version (use auto-detected if confirmed)
554
+ let selectedEdition = options.edition || primaryAnswers.edition;
555
+ let selectedVersion = options.version || primaryAnswers.version;
556
+
557
+ // If user confirmed auto-detected values, use them
558
+ if (primaryAnswers.useDetectedEdition !== false && autoDetected.edition && !selectedEdition) {
559
+ selectedEdition = autoDetected.edition;
560
+ }
561
+ if (primaryAnswers.useDetectedVersion !== false && autoDetected.version && !selectedVersion) {
562
+ selectedVersion = autoDetected.version;
563
+ }
240
564
 
241
565
  let editionInfo = null;
242
566
  let needsBase = false;
@@ -268,8 +592,20 @@ class SnomedModule extends BaseTerminologyModule {
268
592
  // Additional questions based on edition requirements
269
593
  const additionalQuestions = [];
270
594
 
595
+ // Check if base is actually needed - modern releases often include International content
596
+ // If we detected International module in the source, it's a combined release
597
+ let actuallyNeedsBase = needsBase && !options.base;
598
+ if (actuallyNeedsBase && baseConfig.source) {
599
+ // Check if source already contains International content
600
+ const hasInternational = await this.checkForInternationalContent(baseConfig.source);
601
+ if (hasInternational) {
602
+ actuallyNeedsBase = false;
603
+ this.logInfo('Source contains International content - no base edition needed');
604
+ }
605
+ }
606
+
271
607
  // Base directory for extensions (only if edition needs base and not already provided)
272
- if (needsBase && !options.base) {
608
+ if (actuallyNeedsBase) {
273
609
  additionalQuestions.push({
274
610
  type: 'input',
275
611
  name: 'base',
@@ -50,6 +50,8 @@ A comprehensive CLI tool for importing various medical terminology standards int
50
50
  | **LOINC** | `loinc` | Logical Observation Identifiers Names and Codes | 45-120 min |
51
51
  | **LOINC Subset** | `loinc-subset` | Create LOINC subsets for testing | 5-15 min |
52
52
  | **SNOMED CT** | `snomed` | SNOMED Clinical Terms | 2-6 hours |
53
+ | **RxNorm** | `rxnorm` | Prescribable drug nomenclature (NLM) | 15-45 min |
54
+ | **RxNorm Subset** | `rxnorm-subset` | Create RxNorm subsets for testing | 10-30 min |
53
55
  | **UNII** | `unii` | Unique Ingredient Identifier (FDA) | 15-45 min |
54
56
  | **NDC** | `ndc` | National Drug Code Directory | 30-90 min |
55
57
 
@@ -60,9 +62,11 @@ To import:
60
62
  ```bash
61
63
  tx-import loinc import
62
64
  tx-import snomed import
65
+ tx-import rxnorm import
63
66
  tx-import unii import
64
67
  tx-import ndc import
65
68
  tx-import loinc-subset subset
69
+ tx-import rxnorm-subset subset
66
70
  ```
67
71
 
68
72
  See additional commands below.
@@ -70,9 +74,10 @@ See additional commands below.
70
74
  ## Basic Functionality
71
75
 
72
76
  * LOINC: Import from a full Download (all files, including accessories)
73
- * SNOMED: Import from a full snapshot download
77
+ * SNOMED: Import from a full snapshot download
78
+ * RxNorm: Import from RxNorm Full Monthly Release (RRF files)
74
79
  * UNII: Import from a set of past downloads (see discussion below about UNII)
75
- * NDC: import from NDC downloads
80
+ * NDC: import from NDC downloads
76
81
 
77
82
  In addition, there's functionality to create test subsets for LOINC and RxNorm.
78
83
  For SNOMED CT, use the SCT subset functionality (documented in the tx-ecosystem IG).
@@ -220,10 +225,16 @@ tx-import loinc-subset subset \
220
225
 
221
226
  **Import SNOMED CT:**
222
227
  ```bash
223
- # Interactive mode (will prompt for edition selection)
228
+ # Interactive mode - auto-detects edition and version from RF2 files
224
229
  tx-import snomed import
225
230
 
226
- # International Edition
231
+ # Fully automatic (auto-detects edition/version, no prompts)
232
+ tx-import snomed import \
233
+ --source /path/to/rf2/files \
234
+ --dest ./data/snomed.cache \
235
+ --yes
236
+
237
+ # Manual edition/version specification
227
238
  tx-import snomed import \
228
239
  --source /path/to/rf2/files \
229
240
  --dest ./data/snomed.cache \
@@ -231,7 +242,7 @@ tx-import snomed import \
231
242
  --version "20250801" \
232
243
  --yes
233
244
 
234
- # With custom URI
245
+ # With custom URI (overrides edition/version)
235
246
  tx-import snomed import \
236
247
  --source /path/to/rf2/files \
237
248
  --dest ./data/snomed.cache \
@@ -239,9 +250,16 @@ tx-import snomed import \
239
250
  --yes
240
251
  ```
241
252
 
253
+ **Auto-Detection:**
254
+ The importer automatically detects the edition and version from RF2 files by:
255
+ 1. Parsing filenames like `sct2_Concept_Snapshot_INT_20250201.txt`
256
+ 2. Reading the `moduleId` and `effectiveTime` from the first concept record
257
+
258
+ In interactive mode, you'll be asked to confirm the detected values. With `--yes`, the detected values are used automatically.
259
+
242
260
  **Supported Editions:**
243
261
  - International (900000000000207008)
244
- - US Edition (731000124108)
262
+ - US Edition (731000124108)
245
263
  - UK Edition (83821000000107)
246
264
  - Australian Edition (32506021000036107)
247
265
  - [And many more...]
@@ -257,6 +275,96 @@ rf2_files/
257
275
  └── [various refset files]
258
276
  ```
259
277
 
278
+ ### RxNorm Import
279
+
280
+ **Import RxNorm Data:**
281
+ ```bash
282
+ # Interactive mode
283
+ tx-import rxnorm import
284
+
285
+ # Batch mode
286
+ tx-import rxnorm import \
287
+ --source /path/to/RxNorm_full_MMDDYYYY/rrf \
288
+ --dest ./data/rxnorm.db \
289
+ --version "RXNORM-2025-02-03" \
290
+ --yes
291
+
292
+ # Skip stem generation for faster import
293
+ tx-import rxnorm import \
294
+ --source /path/to/rrf/files \
295
+ --dest ./data/rxnorm.db \
296
+ --no-stems \
297
+ --yes
298
+ ```
299
+
300
+ **Options:**
301
+ - `--no-indexes`: Skip index creation for faster import
302
+ - `--no-stems`: Skip stem generation (word stems used for text search)
303
+
304
+ **Required RRF Structure:**
305
+ ```
306
+ rrf_files/
307
+ ├── RXNCONSO.RRF (required - concepts/names)
308
+ ├── RXNREL.RRF (required - relationships)
309
+ ├── RXNSTY.RRF (required - semantic types)
310
+ ├── RXNSAB.RRF (optional - source info)
311
+ ├── RXNATOMARCHIVE.RRF (optional - archived atoms)
312
+ └── RXNCUI.RRF (optional - concept history)
313
+ ```
314
+
315
+ **Source File Format:**
316
+ RxNorm uses Rich Release Format (RRF), which is pipe-delimited with fields ending in `|`.
317
+ Download the "RxNorm Full Monthly Release" from the [NLM RxNorm page](https://www.nlm.nih.gov/research/umls/rxnorm/docs/rxnormfiles.html).
318
+
319
+ **Version Auto-Detection:**
320
+ The importer can auto-detect the version from directory names like `RxNorm_full_08042025`.
321
+
322
+ ### RxNorm Subset Creation
323
+
324
+ **Create Test Subset:**
325
+ ```bash
326
+ # Interactive mode
327
+ tx-import rxnorm-subset subset
328
+
329
+ # With parameters
330
+ tx-import rxnorm-subset subset \
331
+ --source /path/to/rxnorm/rrf \
332
+ --dest ./rxnorm-subset \
333
+ --codes ./my-codes.txt \
334
+ --yes
335
+
336
+ # Without relationship expansion (faster, smaller subset)
337
+ tx-import rxnorm-subset subset \
338
+ --source /path/to/rxnorm/rrf \
339
+ --dest ./rxnorm-subset \
340
+ --codes ./my-codes.txt \
341
+ --no-expand \
342
+ --yes
343
+ ```
344
+
345
+ **Codes File Format** (`my-codes.txt`):
346
+ ```
347
+ # One RxNorm CUI per line
348
+ # Comments start with #
349
+ 161 # acetaminophen
350
+ 1191 # aspirin
351
+ 5640 # ibuprofen
352
+ ```
353
+
354
+ **Options:**
355
+ - `--no-expand`: Skip relationship expansion (just include listed codes)
356
+ - `--include-synonyms`: Include synonym (SY) term types
357
+ - `--include-archived`: Include archived concepts from RXNATOMARCHIVE
358
+ - `--max-iterations <n>`: Maximum relationship expansion iterations (default: 5)
359
+
360
+ **Relationship Expansion:**
361
+ By default, the subset tool expands your target codes to include related concepts:
362
+ - Ingredients of drug products
363
+ - Drug forms and dose forms
364
+ - Components and constituents
365
+
366
+ This ensures that if you include a branded drug, you also get its ingredients, which are required for terminology operations.
367
+
260
368
  ### UNII Import
261
369
 
262
370
  **Import UNII Data:**
@@ -448,6 +556,8 @@ tx-import help
448
556
 
449
557
  - **LOINC**: SQLite database with normalized tables
450
558
  - **SNOMED CT**: Binary cache file optimized for fast loading
559
+ - **RxNorm**: SQLite database with RRF tables and word stems
451
560
  - **UNII**: SQLite database with simple structure
452
561
  - **NDC**: SQLite database supporting multiple versions
453
- - **LOINC Subset**: File-based subset matching original structure
562
+ - **LOINC Subset**: File-based subset matching original structure
563
+ - **RxNorm Subset**: RRF file-based subset matching original structure
@@ -0,0 +1,5 @@
1
+
2
+ class Bundle {
3
+ }
4
+
5
+ module.exports = { Bundle };