fhirsmith 0.9.0 → 0.9.2
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 +31 -0
- package/package.json +1 -1
- package/root-bare-template.html +58 -9775
- package/translations/Messages.properties +2 -2
- package/tx/cs/cs-cs.js +32 -5
- package/tx/importers/import-sct.module.js +167 -79
- package/tx/library/extensions.js +7 -1
- package/tx/library/renderer.js +11 -2
- package/tx/library.js +3 -0
- package/tx/vs/vs-database.js +213 -92
- package/tx/vs/vs-vsac.js +118 -50
- package/tx/workers/validate.js +1 -1
- package/tx/xversion/xv-valueset.js +28 -8
|
@@ -1298,7 +1298,7 @@ VIEWDEFINITION_SHOULD_HAVE_NAME = No name provided. A name is required in many c
|
|
|
1298
1298
|
VIEWDEFINITION_TYPE_MISMATCH = The path expression ''{0}'' does not return a value of the type ''{1}'' - found ''{2}''{3}
|
|
1299
1299
|
VIEWDEFINITION_UNABLE_TO_TYPE = Unable to determine a type (found ''{0}''){1}
|
|
1300
1300
|
VIEWDEFINITION_UNKNOWN_RESOURCE = The name ''{0}'' is not a valid resource{1}
|
|
1301
|
-
VS_EXP_FILTER_UNK = ValueSet ''{0}'' Filter by property ''{1}'' and op ''{2}'' is not supported yet
|
|
1301
|
+
VS_EXP_FILTER_UNK = ValueSet ''{0}'' Filter by property ''{1}'' and op ''{2}'' is not supported (yet?)
|
|
1302
1302
|
VS_EXP_IMPORT_CS = Cannot include value set ''{0}'' because it''s actually a code system
|
|
1303
1303
|
VS_EXP_IMPORT_CS_PINNED = Cannot include value set ''{0}'' version ''{1}'' because it''s actually a code system
|
|
1304
1304
|
VS_EXP_IMPORT_CS_PINNED_X = Cannot exclude value set ''{0}'' version ''{1}'' because it''s actually a code system
|
|
@@ -1400,7 +1400,7 @@ TEXT_LINK_NO_DATA = No data element was found in the textLink extension
|
|
|
1400
1400
|
TEXT_LINK_SELECTOR_INVALID = The textLink selector ''{0}'' is invalid: {1}
|
|
1401
1401
|
SD_CONTEXT_SHOULD_ELEMENT_NOT_FOUND = The element {0} is not valid
|
|
1402
1402
|
SD_CONTEXT_SHOULD_ELEMENT_NOT_FOUND_VER = The element {0} is not valid in version {1}
|
|
1403
|
-
FILTER_NOT_UNDERSTOOD = The filter "{0} {1} {2}"
|
|
1403
|
+
FILTER_NOT_UNDERSTOOD = The filter "{0} {1} {2}" is not understood or supported
|
|
1404
1404
|
XHTML_CONTROL_NO_SOURCE = The xhtml node at ''{0}'' does not have a class attribute to indicate its source (boilerplate, generated, or original) and this is required by the profile {1}
|
|
1405
1405
|
XHTML_XHTML_MIXED_LANG = The xhtml has some language sections ({0}), and also has content that is not in a language section
|
|
1406
1406
|
XHTML_CONTROL_NO_LANGS = The xhtml has some language sections ({0}), but language sections are prohibited in this context by the profile {1}
|
package/tx/cs/cs-cs.js
CHANGED
|
@@ -1224,7 +1224,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1224
1224
|
|
|
1225
1225
|
// Handle concept/code hierarchy filters
|
|
1226
1226
|
if ((prop === 'concept' || prop === 'code')) {
|
|
1227
|
-
results = await this._handleConceptFilter(filterContext, op, value);
|
|
1227
|
+
results = await this._handleConceptFilter(filterContext, prop, op, value);
|
|
1228
1228
|
}
|
|
1229
1229
|
|
|
1230
1230
|
// Handle child existence filter
|
|
@@ -1248,7 +1248,8 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1248
1248
|
}
|
|
1249
1249
|
|
|
1250
1250
|
if (!results) {
|
|
1251
|
-
throw new
|
|
1251
|
+
throw new Issue('error', 'exception', null, 'FILTER_NOT_UNDERSTOOD',
|
|
1252
|
+
this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), 'vs-invalid', 422);
|
|
1252
1253
|
}
|
|
1253
1254
|
// Add to filter context
|
|
1254
1255
|
if (!filterContext.filters) {
|
|
@@ -1267,15 +1268,17 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1267
1268
|
* @returns {Promise<FhirCodeSystemProviderFilterContext>} Filter results
|
|
1268
1269
|
* @private
|
|
1269
1270
|
*/
|
|
1270
|
-
async _handleConceptFilter(filterContext, op, value) {
|
|
1271
|
+
async _handleConceptFilter(filterContext, prop, op, value) {
|
|
1271
1272
|
const results = new FhirCodeSystemProviderFilterContext();
|
|
1272
1273
|
|
|
1273
1274
|
if (op === 'is-a' || op === 'descendent-of') {
|
|
1274
1275
|
// Find all descendants of the specified code
|
|
1275
1276
|
const includeRoot = (op === 'is-a');
|
|
1276
1277
|
await this._addDescendants(results, value, includeRoot);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1278
|
+
} else if (op === 'child-of') {
|
|
1279
|
+
// Find all descendants of the specified code
|
|
1280
|
+
await this._addChildren(results, value);
|
|
1281
|
+
} else if (op === 'is-not-a') {
|
|
1279
1282
|
// Find all concepts that are NOT descendants of the specified code
|
|
1280
1283
|
const excludeDescendants = this.codeSystem.getDescendants(value);
|
|
1281
1284
|
const excludeSet = new Set([value, ...excludeDescendants]);
|
|
@@ -1323,6 +1326,9 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1323
1326
|
} catch (error) {
|
|
1324
1327
|
throw new Issue('error', 'exception', null, 'INVALID_REGEX', this.opContext.i18n.translate('INVALID_REGEX', this.opContext.langs, [value, error.message]), 'vs-invalid', 422);
|
|
1325
1328
|
}
|
|
1329
|
+
} else {
|
|
1330
|
+
throw new Issue('error', 'exception', null, 'FILTER_NOT_UNDERSTOOD',
|
|
1331
|
+
this.opContext.i18n.translate('FILTER_NOT_UNDERSTOOD', this.opContext.langs, [prop, op, value]), 'vs-invalid', 422);
|
|
1326
1332
|
}
|
|
1327
1333
|
|
|
1328
1334
|
return results;
|
|
@@ -1353,6 +1359,27 @@ class FhirCodeSystemProvider extends BaseCSServices {
|
|
|
1353
1359
|
}
|
|
1354
1360
|
}
|
|
1355
1361
|
|
|
1362
|
+
/**
|
|
1363
|
+
* Add immediate children of a code to the results
|
|
1364
|
+
* @param {FhirCodeSystemProviderFilterContext} results - Results to add to
|
|
1365
|
+
* @param {string} ancestorCode - The parent code
|
|
1366
|
+
* @private
|
|
1367
|
+
*/
|
|
1368
|
+
async _addChildren(results, parentCode) {
|
|
1369
|
+
const concept = this.codeSystem.getConceptByCode(parentCode);
|
|
1370
|
+
if (concept) {
|
|
1371
|
+
const descendants = this.codeSystem.getChildren(parentCode);
|
|
1372
|
+
for (const code of descendants) {
|
|
1373
|
+
if (code !== parentCode) { // should not be
|
|
1374
|
+
const concept = this.codeSystem.getConceptByCode(code);
|
|
1375
|
+
if (concept) {
|
|
1376
|
+
results.add(concept, 0);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1356
1383
|
/**
|
|
1357
1384
|
* Handle child exists filter
|
|
1358
1385
|
* @param {FilterExecutionContext} filterContext - Filter context
|
|
@@ -48,37 +48,37 @@ class SnomedModule extends BaseTerminologyModule {
|
|
|
48
48
|
registerCommands(terminologyCommand, globalOptions) {
|
|
49
49
|
// Import command
|
|
50
50
|
terminologyCommand
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1421
|
-
|
|
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
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
1885
|
+
this.refs.addReferences(tracker.activeParents) : 0;
|
|
1804
1886
|
const inactiveParentsRef = tracker.inactiveParents.length > 0 ?
|
|
1805
|
-
|
|
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
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2581
|
-
|
|
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/extensions.js
CHANGED
|
@@ -56,7 +56,13 @@ const Extensions = {
|
|
|
56
56
|
if (!resource) {
|
|
57
57
|
return undefined;
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
let extensions = Array.isArray(resource) ? resource : (resource.extension || []);
|
|
60
|
+
for (let ext of extensions || []) {
|
|
61
|
+
if (ext.url === url) {
|
|
62
|
+
return getValuePrimitive(ext);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
extensions = Array.isArray(resource) ? resource : (resource.modifierExtension || []);
|
|
60
66
|
for (let ext of extensions || []) {
|
|
61
67
|
if (ext.url === url) {
|
|
62
68
|
return getValuePrimitive(ext);
|
package/tx/library/renderer.js
CHANGED
|
@@ -466,14 +466,15 @@ class Renderer {
|
|
|
466
466
|
li.tx(" "+ this.translate('VALUE_SET_WHERE')+" ");
|
|
467
467
|
li.startCommaList("and");
|
|
468
468
|
for (let f of inc.filter) {
|
|
469
|
-
|
|
469
|
+
let op = this.readFilterOp(f);
|
|
470
|
+
if (op == 'exists') {
|
|
470
471
|
if (f.value == "true") {
|
|
471
472
|
li.commaItem(f.property+" "+ this.translate('VALUE_SET_EXISTS'));
|
|
472
473
|
} else {
|
|
473
474
|
li.commaItem(f.property+" "+ this.translate('VALUE_SET_DOESNT_EXIST'));
|
|
474
475
|
}
|
|
475
476
|
} else {
|
|
476
|
-
li.commaItem(f.property + " " +
|
|
477
|
+
li.commaItem(f.property + " " + op + " ");
|
|
477
478
|
const loc = this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, inc.system, inc.version, f.value) : null;
|
|
478
479
|
if (loc) {
|
|
479
480
|
li.ah(loc.link).tx(loc.description);
|
|
@@ -2243,6 +2244,14 @@ class Renderer {
|
|
|
2243
2244
|
return defn+' = ' + value;
|
|
2244
2245
|
}
|
|
2245
2246
|
}
|
|
2247
|
+
|
|
2248
|
+
readFilterOp(f) {
|
|
2249
|
+
if (f._op) {
|
|
2250
|
+
return Extensions.readString(f._op, 'http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op');
|
|
2251
|
+
} else {
|
|
2252
|
+
return f.op;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2246
2255
|
}
|
|
2247
2256
|
|
|
2248
2257
|
module.exports = { Renderer };
|
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
|
}
|