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.
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/library/cron-utilities.js +136 -0
- package/library/html-server.js +13 -29
- package/library/html.js +3 -8
- package/library/languages.js +160 -37
- package/library/package-manager.js +48 -1
- package/library/utilities.js +100 -19
- package/package.json +2 -2
- package/packages/package-crawler.js +6 -1
- package/packages/packages.js +38 -54
- package/publisher/publisher.js +19 -27
- package/registry/api.js +11 -10
- package/registry/crawler.js +31 -29
- package/registry/model.js +5 -26
- package/registry/registry.js +32 -41
- package/server.js +89 -12
- package/shl/shl.js +0 -18
- package/static/assets/js/statuspage.js +1 -9
- package/stats.js +39 -1
- package/token/token.js +14 -9
- package/translations/Messages.properties +2 -1
- package/tx/README.md +17 -6
- package/tx/cs/cs-api.js +19 -1
- package/tx/cs/cs-base.js +77 -0
- package/tx/cs/cs-country.js +46 -0
- package/tx/cs/cs-cpt.js +9 -5
- package/tx/cs/cs-cs.js +27 -13
- package/tx/cs/cs-lang.js +60 -22
- package/tx/cs/cs-loinc.js +69 -98
- package/tx/cs/cs-mimetypes.js +4 -0
- package/tx/cs/cs-ndc.js +6 -0
- package/tx/cs/cs-omop.js +16 -15
- package/tx/cs/cs-rxnorm.js +23 -1
- package/tx/cs/cs-snomed.js +283 -40
- package/tx/cs/cs-ucum.js +90 -70
- package/tx/importers/import-sct.module.js +371 -35
- package/tx/importers/readme.md +117 -7
- package/tx/library/bundle.js +5 -0
- package/tx/library/capabilitystatement.js +3 -142
- package/tx/library/codesystem.js +19 -173
- package/tx/library/conceptmap.js +4 -218
- package/tx/library/designations.js +14 -1
- package/tx/library/extensions.js +7 -0
- package/tx/library/namingsystem.js +3 -89
- package/tx/library/operation-outcome.js +8 -3
- package/tx/library/parameters.js +3 -2
- package/tx/library/renderer.js +10 -6
- package/tx/library/terminologycapabilities.js +3 -243
- package/tx/library/valueset.js +3 -235
- package/tx/library.js +100 -13
- package/tx/operation-context.js +23 -4
- package/tx/params.js +35 -38
- package/tx/provider.js +6 -5
- package/tx/sct/expressions.js +12 -3
- package/tx/tx-html.js +80 -89
- package/tx/tx.fhir.org.yml +6 -5
- package/tx/tx.js +163 -13
- package/tx/vs/vs-database.js +56 -39
- package/tx/vs/vs-package.js +21 -2
- package/tx/vs/vs-vsac.js +175 -39
- package/tx/workers/batch-validate.js +2 -0
- package/tx/workers/batch.js +2 -0
- package/tx/workers/expand.js +132 -112
- package/tx/workers/lookup.js +33 -14
- package/tx/workers/metadata.js +2 -2
- package/tx/workers/read.js +3 -2
- package/tx/workers/related.js +574 -0
- package/tx/workers/search.js +46 -9
- package/tx/workers/subsumes.js +13 -3
- package/tx/workers/translate.js +7 -3
- package/tx/workers/validate.js +258 -285
- package/tx/workers/worker.js +43 -39
- package/tx/xml/bundle-xml.js +237 -0
- package/tx/xml/xml-base.js +215 -64
- package/tx/xversion/xv-bundle.js +71 -0
- package/tx/xversion/xv-capabiliityStatement.js +137 -0
- package/tx/xversion/xv-codesystem.js +169 -0
- package/tx/xversion/xv-conceptmap.js +224 -0
- package/tx/xversion/xv-namingsystem.js +88 -0
- package/tx/xversion/xv-operationoutcome.js +27 -0
- package/tx/xversion/xv-parameters.js +87 -0
- package/tx/xversion/xv-resource.js +45 -0
- package/tx/xversion/xv-terminologyCapabilities.js +214 -0
- package/tx/xversion/xv-valueset.js +234 -0
- package/utilities/dev-proxy-server.js +126 -0
- package/utilities/explode-results.js +58 -0
- package/utilities/split-by-system.js +198 -0
- package/utilities/vsac-cs-fetcher.js +0 -0
- package/{windows-install.js → utilities/windows-install.js} +2 -0
- package/vcl/vcl.js +0 -18
- 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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
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 (
|
|
608
|
+
if (actuallyNeedsBase) {
|
|
273
609
|
additionalQuestions.push({
|
|
274
610
|
type: 'input',
|
|
275
611
|
name: 'base',
|
package/tx/importers/readme.md
CHANGED
|
@@ -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
|
|
228
|
+
# Interactive mode - auto-detects edition and version from RF2 files
|
|
224
229
|
tx-import snomed import
|
|
225
230
|
|
|
226
|
-
#
|
|
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
|