fhirsmith 0.9.1 → 0.9.3

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/tx/cs/cs-ucum.js CHANGED
@@ -256,7 +256,7 @@ class UcumCodeSystemProvider extends BaseCSServices {
256
256
  // filterContext.filters.push(ucumFilter);
257
257
  }
258
258
 
259
- async filter(filterContext, prop, op, value) {
259
+ async filter(filterContext, forIteration, prop, op, value) {
260
260
  assert(filterContext && filterContext instanceof FilterExecutionContext, 'filterContext must be a FilterExecutionContext');
261
261
  assert(prop != null && typeof prop === 'string', 'prop must be a non-null string');
262
262
  assert(op != null && typeof op === 'string', 'op must be a non-null string');
@@ -48,37 +48,37 @@ class SnomedModule extends BaseTerminologyModule {
48
48
  registerCommands(terminologyCommand, globalOptions) {
49
49
  // Import command
50
50
  terminologyCommand
51
- .command('import')
52
- .description('Import SNOMED CT data from RF2 source directory')
53
- .option('-s, --source <directory>', 'Source directory containing RF2 files')
54
- .option('-b, --base <directory>', 'Base edition directory (for extensions)')
55
- .option('-d, --dest <file>', 'Destination cache file')
56
- .option('-e, --edition <code>', 'Edition code (e.g., 900000000000207008 for International)')
57
- .option('-v, --version <version>', 'Version in YYYYMMDD format (e.g., 20250801)')
58
- .option('-u, --uri <uri>', 'Version URI (overrides edition/version if provided)')
59
- .option('-l, --language <code>', 'Default language code (overrides edition default if provided)')
60
- .option('-y, --yes', 'Skip confirmations')
61
- .action(async (options) => {
62
- await this.handleImportCommand({...globalOptions, ...options});
63
- });
51
+ .command('import')
52
+ .description('Import SNOMED CT data from RF2 source directory')
53
+ .option('-s, --source <directory>', 'Source directory containing RF2 files')
54
+ .option('-b, --base <directory>', 'Base edition directory (for extensions)')
55
+ .option('-d, --dest <file>', 'Destination cache file')
56
+ .option('-e, --edition <code>', 'Edition code (e.g., 900000000000207008 for International)')
57
+ .option('-v, --version <version>', 'Version in YYYYMMDD format (e.g., 20250801)')
58
+ .option('-u, --uri <uri>', 'Version URI (overrides edition/version if provided)')
59
+ .option('-l, --language <code>', 'Default language code (overrides edition default if provided)')
60
+ .option('-y, --yes', 'Skip confirmations')
61
+ .action(async (options) => {
62
+ await this.handleImportCommand({...globalOptions, ...options});
63
+ });
64
64
 
65
65
  // Validate command
66
66
  terminologyCommand
67
- .command('validate')
68
- .description('Validate SNOMED CT RF2 directory structure')
69
- .option('-s, --source <directory>', 'Source directory to validate')
70
- .action(async (options) => {
71
- await this.handleValidateCommand({...globalOptions, ...options});
72
- });
67
+ .command('validate')
68
+ .description('Validate SNOMED CT RF2 directory structure')
69
+ .option('-s, --source <directory>', 'Source directory to validate')
70
+ .action(async (options) => {
71
+ await this.handleValidateCommand({...globalOptions, ...options});
72
+ });
73
73
 
74
74
  // Status command
75
75
  terminologyCommand
76
- .command('status')
77
- .description('Show status of SNOMED CT cache')
78
- .option('-d, --dest <file>', 'Cache file to check')
79
- .action(async (options) => {
80
- await this.handleStatusCommand({...globalOptions, ...options});
81
- });
76
+ .command('status')
77
+ .description('Show status of SNOMED CT cache')
78
+ .option('-d, --dest <file>', 'Cache file to check')
79
+ .action(async (options) => {
80
+ await this.handleStatusCommand({...globalOptions, ...options});
81
+ });
82
82
  }
83
83
 
84
84
  async handleImportCommand(options) {
@@ -633,7 +633,7 @@ class SnomedModule extends BaseTerminologyModule {
633
633
  }
634
634
 
635
635
  const additionalAnswers = additionalQuestions.length > 0 ?
636
- await inquirer.prompt(additionalQuestions) : {};
636
+ await inquirer.prompt(additionalQuestions) : {};
637
637
 
638
638
  // Build the final configuration
639
639
  const config = {
@@ -774,7 +774,7 @@ class SnomedModule extends BaseTerminologyModule {
774
774
  } else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tconceptId\tlanguageCode\ttypeId\tterm\tcaseSignificanceId')) {
775
775
  files.descriptions.push(filePath);
776
776
  } else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tsourceId\tdestinationId\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId') &&
777
- !filePath.includes('StatedRelationship')) {
777
+ !filePath.includes('StatedRelationship')) {
778
778
  files.relationships.push(filePath);
779
779
  }
780
780
  } catch (error) {
@@ -1165,6 +1165,19 @@ class SnomedImporter {
1165
1165
  refsetDirectories: []
1166
1166
  };
1167
1167
 
1168
+ // For extensions: load base edition files first so that all International
1169
+ // Edition concepts, descriptions, and relationships are present before the
1170
+ // extension content is layered on top.
1171
+ if (this.config.base) {
1172
+ if (this.config.verbose) {
1173
+ console.log(`Loading base edition from: ${this.config.base}`);
1174
+ }
1175
+ this._scanDirectory(this.config.base, files);
1176
+ }
1177
+
1178
+ // Then load the extension (or standalone edition) source files.
1179
+ // For extensions these come second so that extension rows can override
1180
+ // base rows where the same component has been updated.
1168
1181
  this._scanDirectory(this.config.source, files);
1169
1182
  return files;
1170
1183
  }
@@ -1200,7 +1213,7 @@ class SnomedImporter {
1200
1213
  } else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tconceptId\tlanguageCode\ttypeId\tterm\tcaseSignificanceId')) {
1201
1214
  files.descriptions.push(filePath);
1202
1215
  } else if (firstLine.startsWith('id\teffectiveTime\tactive\tmoduleId\tsourceId\tdestinationId\trelationshipGroup\ttypeId\tcharacteristicTypeId\tmodifierId') &&
1203
- !filePath.includes('StatedRelationship')) {
1216
+ !filePath.includes('StatedRelationship')) {
1204
1217
  files.relationships.push(filePath);
1205
1218
  }
1206
1219
  } catch (error) {
@@ -1250,6 +1263,9 @@ class SnomedImporter {
1250
1263
  this.conceptList = [];
1251
1264
  let processedLines = 0;
1252
1265
 
1266
+ // When loading base + extension, track list indices for fast replacement
1267
+ const conceptIdToListIndex = this.config.base ? new Map() : null;
1268
+
1253
1269
  for (let i = 0; i < this.files.concepts.length; i++) {
1254
1270
  const file = this.files.concepts[i];
1255
1271
  const rl = readline.createInterface({
@@ -1275,8 +1291,23 @@ class SnomedImporter {
1275
1291
  };
1276
1292
 
1277
1293
  if (this.conceptMap.has(concept.id)) {
1278
- throw new Error(`Duplicate Concept Id at line ${lineCount}: ${concept.id} - check you are processing the snapshot not the full edition`);
1294
+ // When loading base + extension, the same concept may appear in both.
1295
+ // The extension snapshot row takes precedence (it is loaded second).
1296
+ // If there is no base directory this is a genuine duplicate in a single
1297
+ // snapshot and we should still raise an error.
1298
+ if (!this.config.base) {
1299
+ throw new Error(`Duplicate Concept Id at line ${lineCount}: ${concept.id} - check you are processing the snapshot not the full edition`);
1300
+ }
1301
+ // Replace the base edition row with the extension row
1302
+ const idx = conceptIdToListIndex.get(concept.id);
1303
+ if (idx !== undefined) {
1304
+ this.conceptList[idx] = concept;
1305
+ }
1306
+ this.conceptMap.set(concept.id, concept);
1279
1307
  } else {
1308
+ if (conceptIdToListIndex) {
1309
+ conceptIdToListIndex.set(concept.id, this.conceptList.length);
1310
+ }
1280
1311
  this.conceptList.push(concept);
1281
1312
  this.conceptMap.set(concept.id, concept);
1282
1313
  }
@@ -1347,6 +1378,12 @@ class SnomedImporter {
1347
1378
  const descriptionList = [];
1348
1379
  let processedLines = 0;
1349
1380
 
1381
+ // Build a lookup from description id -> index in descriptionList so that
1382
+ // extension rows can replace base rows for the same description.
1383
+ if (this.config.base) {
1384
+ this._descriptionIdSet = new Map();
1385
+ }
1386
+
1350
1387
  for (const file of this.files.descriptions) {
1351
1388
  const rl = readline.createInterface({
1352
1389
  input: fs.createReadStream(file),
@@ -1372,7 +1409,19 @@ class SnomedImporter {
1372
1409
  caseSignificanceId: BigInt(parts[8])
1373
1410
  };
1374
1411
 
1375
- descriptionList.push(desc);
1412
+ // When loading base + extension, the same description may appear in
1413
+ // both. The extension row (loaded second) takes precedence.
1414
+ if (this.config.base && this._descriptionIdSet) {
1415
+ const existingIdx = this._descriptionIdSet.get(desc.id);
1416
+ if (existingIdx !== undefined) {
1417
+ descriptionList[existingIdx] = desc;
1418
+ } else {
1419
+ this._descriptionIdSet.set(desc.id, descriptionList.length);
1420
+ descriptionList.push(desc);
1421
+ }
1422
+ } else {
1423
+ descriptionList.push(desc);
1424
+ }
1376
1425
  }
1377
1426
 
1378
1427
  processedLines++;
@@ -1417,8 +1466,8 @@ class SnomedImporter {
1417
1466
  const caps = this.conceptMap.get(desc.caseSignificanceId);
1418
1467
 
1419
1468
  const descOffset = this.descriptions.addDescription(
1420
- termOffset, desc.id, effectiveTime, concept.index,
1421
- module.index, kind.index, caps.index, desc.active, lang
1469
+ termOffset, desc.id, effectiveTime, concept.index,
1470
+ module.index, kind.index, caps.index, desc.active, lang
1422
1471
  );
1423
1472
 
1424
1473
  // Track description on concept
@@ -1692,6 +1741,11 @@ class SnomedImporter {
1692
1741
  }
1693
1742
  this.isAIndex = isAConcept.index;
1694
1743
 
1744
+ // Pass 1: collect all relationship rows, deduplicating so that extension
1745
+ // rows (loaded second) override base rows with the same relationship id.
1746
+ const relationshipRows = [];
1747
+ const relationshipIdMap = this.config.base ? new Map() : null; // id -> index in relationshipRows
1748
+
1695
1749
  for (const file of this.files.relationships) {
1696
1750
  const rl = readline.createInterface({
1697
1751
  input: fs.createReadStream(file),
@@ -1718,40 +1772,16 @@ class SnomedImporter {
1718
1772
  modifierId: BigInt(parts[9])
1719
1773
  };
1720
1774
 
1721
- const source = this.conceptMap.get(rel.sourceId);
1722
- const destination = this.conceptMap.get(rel.destinationId);
1723
- const type = this.conceptMap.get(rel.typeId);
1724
-
1725
- if (source && destination && type) {
1726
- const effectiveTime = this.convertDateToSnomedDate(rel.effectiveTime);
1727
-
1728
- // Check if this is a defining relationship
1729
- const defining = rel.characteristicTypeId === RF2_MAGIC_RELN_DEFINING ||
1730
- rel.characteristicTypeId === RF2_MAGIC_RELN_STATED ||
1731
- rel.characteristicTypeId === RF2_MAGIC_RELN_INFERRED;
1732
-
1733
- const relationshipIndex = this.relationships.addRelationship(
1734
- rel.id, source.index, destination.index, type.index,
1735
- 0, 0, 0, effectiveTime, rel.active, defining, rel.relationshipGroup
1736
- );
1737
-
1738
- // Track parent/child relationships for is-a relationships
1739
- if (type.index === this.isAIndex && defining) {
1740
- const sourceTracker = this.getOrCreateConceptTracker(source.index);
1741
- if (rel.active) {
1742
- sourceTracker.addActiveParent(destination.index);
1743
- } else {
1744
- sourceTracker.addInactiveParent(destination.index);
1745
- }
1775
+ if (relationshipIdMap) {
1776
+ const existingIdx = relationshipIdMap.get(rel.id);
1777
+ if (existingIdx !== undefined) {
1778
+ relationshipRows[existingIdx] = rel;
1779
+ } else {
1780
+ relationshipIdMap.set(rel.id, relationshipRows.length);
1781
+ relationshipRows.push(rel);
1746
1782
  }
1747
-
1748
- // Track inbound/outbound relationships
1749
- const sourceTracker = this.getOrCreateConceptTracker(source.index);
1750
- const destTracker = this.getOrCreateConceptTracker(destination.index);
1751
-
1752
- sourceTracker.addOutbound(relationshipIndex);
1753
- destTracker.addInbound(relationshipIndex);
1754
-
1783
+ } else {
1784
+ relationshipRows.push(rel);
1755
1785
  }
1756
1786
  }
1757
1787
 
@@ -1762,10 +1792,62 @@ class SnomedImporter {
1762
1792
  }
1763
1793
  }
1764
1794
 
1795
+ if (this.progressReporter) {
1796
+ this.progressReporter.completeTask('Reading Relationships', processedLines, totalLines);
1797
+ }
1798
+
1799
+ // Pass 2: process the deduplicated relationship rows into the binary
1800
+ // structures and concept trackers.
1801
+ const buildProgressBar = this.progressReporter?.createTaskProgressBar('Building Relationships');
1802
+ buildProgressBar?.start(relationshipRows.length, 0);
1803
+
1804
+ for (let i = 0; i < relationshipRows.length; i++) {
1805
+ const rel = relationshipRows[i];
1806
+
1807
+ const source = this.conceptMap.get(rel.sourceId);
1808
+ const destination = this.conceptMap.get(rel.destinationId);
1809
+ const type = this.conceptMap.get(rel.typeId);
1810
+
1811
+ if (source && destination && type) {
1812
+ const effectiveTime = this.convertDateToSnomedDate(rel.effectiveTime);
1813
+
1814
+ // Check if this is a defining relationship
1815
+ const defining = rel.characteristicTypeId === RF2_MAGIC_RELN_DEFINING ||
1816
+ rel.characteristicTypeId === RF2_MAGIC_RELN_STATED ||
1817
+ rel.characteristicTypeId === RF2_MAGIC_RELN_INFERRED;
1818
+
1819
+ const relationshipIndex = this.relationships.addRelationship(
1820
+ rel.id, source.index, destination.index, type.index,
1821
+ 0, 0, 0, effectiveTime, rel.active, defining, rel.relationshipGroup
1822
+ );
1823
+
1824
+ // Track parent/child relationships for is-a relationships
1825
+ if (type.index === this.isAIndex && defining) {
1826
+ const sourceTracker = this.getOrCreateConceptTracker(source.index);
1827
+ if (rel.active) {
1828
+ sourceTracker.addActiveParent(destination.index);
1829
+ } else {
1830
+ sourceTracker.addInactiveParent(destination.index);
1831
+ }
1832
+ }
1833
+
1834
+ // Track inbound/outbound relationships
1835
+ const sourceTracker = this.getOrCreateConceptTracker(source.index);
1836
+ const destTracker = this.getOrCreateConceptTracker(destination.index);
1837
+
1838
+ sourceTracker.addOutbound(relationshipIndex);
1839
+ destTracker.addInbound(relationshipIndex);
1840
+ }
1841
+
1842
+ if (i % 1000 === 0) {
1843
+ buildProgressBar?.update(i);
1844
+ }
1845
+ }
1846
+
1765
1847
  this.relationships.doneBuild();
1766
1848
 
1767
1849
  if (this.progressReporter) {
1768
- this.progressReporter.completeTask('Reading Relationships', processedLines, totalLines);
1850
+ this.progressReporter.completeTask('Building Relationships', relationshipRows.length, relationshipRows.length);
1769
1851
  }
1770
1852
  }
1771
1853
 
@@ -1800,9 +1882,9 @@ class SnomedImporter {
1800
1882
  // Set parents if concept has any
1801
1883
  if (tracker.activeParents.length > 0 || tracker.inactiveParents.length > 0) {
1802
1884
  const activeParentsRef = tracker.activeParents.length > 0 ?
1803
- this.refs.addReferences(tracker.activeParents) : 0;
1885
+ this.refs.addReferences(tracker.activeParents) : 0;
1804
1886
  const inactiveParentsRef = tracker.inactiveParents.length > 0 ?
1805
- this.refs.addReferences(tracker.inactiveParents) : 0;
1887
+ this.refs.addReferences(tracker.inactiveParents) : 0;
1806
1888
 
1807
1889
  this.concepts.setParents(concept.index, activeParentsRef, inactiveParentsRef);
1808
1890
  } else {
@@ -2104,14 +2186,14 @@ class SnomedImporter {
2104
2186
  // NOTE: This calls addString() so it must happen AFTER strings.reopen()
2105
2187
  for (const refSet of refSetsArray) {
2106
2188
  this.refsetIndex.addReferenceSet(
2107
- this.addString(refSet.title), // This needs strings builder to be active
2108
- refSet.filename,
2109
- refSet.index,
2110
- refSet.membersByRef,
2111
- refSet.membersByName,
2112
- refSet.fieldTypes,
2113
- refSet.fieldNames,
2114
- refSet.langs
2189
+ this.addString(refSet.title), // This needs strings builder to be active
2190
+ refSet.filename,
2191
+ refSet.index,
2192
+ refSet.membersByRef,
2193
+ refSet.membersByName,
2194
+ refSet.fieldTypes,
2195
+ refSet.fieldNames,
2196
+ refSet.langs
2115
2197
  );
2116
2198
  }
2117
2199
  }
@@ -2216,7 +2298,13 @@ class SnomedImporter {
2216
2298
  if (!refSet || currentRefSetId !== refSetId) {
2217
2299
  currentRefSetId = refSetId;
2218
2300
  refSet = this.getOrCreateRefSet(refSetId, displayName, isLangRefset);
2219
- refSet.filename = this.addString(path.relative(this.config.source, filePath));
2301
+ // Compute relative path — the file may live under the base directory
2302
+ // rather than the extension source directory.
2303
+ let relPath = path.relative(this.config.source, filePath);
2304
+ if (this.config.base && relPath.startsWith('..')) {
2305
+ relPath = path.relative(this.config.base, filePath);
2306
+ }
2307
+ refSet.filename = this.addString(relPath);
2220
2308
  refSet.fieldTypes = this.getOrCreateFieldTypes(fieldTypes);
2221
2309
  refSet.fieldNames = this.getOrCreateFieldNames(headers.slice(6), fieldTypes); // Additional fields beyond standard 6
2222
2310
  }
@@ -2577,8 +2665,8 @@ class SnomedImporter {
2577
2665
  };
2578
2666
 
2579
2667
  const services = new SnomedExpressionServices(
2580
- snomedStructures,
2581
- this.isAIndex
2668
+ snomedStructures,
2669
+ this.isAIndex
2582
2670
  );
2583
2671
 
2584
2672
  // Set building flag to true so services will generate normal forms dynamically
package/tx/library.js CHANGED
@@ -35,6 +35,7 @@ const { OCLCodeSystemProvider, OCLSourceCodeSystemFactory } = require('./ocl/cs-
35
35
  const { OCLValueSetProvider } = require('./ocl/vs-ocl');
36
36
  const { OCLConceptMapProvider } = require('./ocl/cm-ocl');
37
37
  const {UriServicesFactory} = require("./cs/cs-uri");
38
+ const {debugLog} = require("./operation-context");
38
39
 
39
40
  /**
40
41
  * This class holds all the loaded content ready for processing
@@ -185,6 +186,7 @@ class Library {
185
186
  try {
186
187
  await this.processSource(source, this.packageManager, "cs");
187
188
  } catch (error) {
189
+ debugLog(error);
188
190
  console.error(`Failed to load code systems from '${source}': ${error.message}`);
189
191
  throw error;
190
192
  }
@@ -196,6 +198,7 @@ class Library {
196
198
  try {
197
199
  await this.processSource(source, this.packageManager, "npm");
198
200
  } catch (error) {
201
+ debugLog(error);
199
202
  console.error(`Failed to load package '${source}': ${error.message}`);
200
203
  throw error;
201
204
  }