fhirsmith 0.9.5 → 0.9.7

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 (43) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/config-template.json +2 -1
  3. package/library/folder-content-loader.js +91 -0
  4. package/library/regex-utilities.js +49 -12
  5. package/npmprojector/npmprojector.js +2 -6
  6. package/package.json +2 -2
  7. package/publisher/publisher.js +105 -12
  8. package/registry/registry.js +6 -6
  9. package/server.js +6 -2
  10. package/test-scripts/repro-re2-wasm-leak.js +8 -7
  11. package/translations/Messages.properties +1 -1
  12. package/tx/cs/cs-cs.js +8 -0
  13. package/tx/cs/cs-loinc.js +13 -12
  14. package/tx/cs/cs-omop.js +24 -23
  15. package/tx/cs/cs-provider-list.js +2 -1
  16. package/tx/cs/cs-snomed.js +142 -59
  17. package/tx/cs/cs-unii.js +11 -11
  18. package/tx/data/snomed-testing.cache +0 -0
  19. package/tx/library/canonical-resource.js +4 -2
  20. package/tx/library/designations.js +27 -20
  21. package/tx/library/renderer.js +303 -22
  22. package/tx/library/ucum-types.js +4 -1
  23. package/tx/library.js +65 -21
  24. package/tx/operation-context.js +52 -23
  25. package/tx/params.js +36 -8
  26. package/tx/problems.js +0 -4
  27. package/tx/provider.js +7 -3
  28. package/tx/tx-html.js +7 -0
  29. package/tx/tx.js +24 -13
  30. package/tx/vs/vs-vsac.js +157 -9
  31. package/tx/workers/expand.js +100 -96
  32. package/tx/workers/lookup.js +6 -0
  33. package/tx/workers/read.js +1 -1
  34. package/tx/workers/translate.js +21 -29
  35. package/tx/workers/validate.js +18 -10
  36. package/tx/workers/worker.js +5 -1
  37. package/tx/xversion/xv-bundle.js +1 -2
  38. package/tx/xversion/xv-codesystem.js +5 -2
  39. package/tx/xversion/xv-parameters.js +4 -4
  40. package/tx/xversion/xv-resource.js +2 -2
  41. package/tx/xversion/xv-terminologyCapabilities.js +11 -6
  42. package/tx/xversion/xv-valueset.js +7 -7
  43. package/publisher/task-draft.js +0 -458
package/tx/cs/cs-cs.js CHANGED
@@ -1140,6 +1140,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1140
1140
  const allConcepts = this.codeSystem.getAllConcepts();
1141
1141
 
1142
1142
  for (const concept of allConcepts) {
1143
+ this.opContext.deadCheck('cs:searchFilter');
1143
1144
  const rating = this._calculateSearchRating(concept, searchTerm);
1144
1145
  if (rating > 0) {
1145
1146
  results.add(concept, rating);
@@ -1285,6 +1286,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1285
1286
 
1286
1287
  const allCodes = this.codeSystem.getAllCodes();
1287
1288
  for (const code of allCodes) {
1289
+ this.opContext.deadCheck('cs:conceptFilter:is-not-a');
1288
1290
  if (!excludeSet.has(code)) {
1289
1291
  const concept = this.codeSystem.getConceptByCode(code);
1290
1292
  if (concept) {
@@ -1316,6 +1318,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1316
1318
  const regex = regexUtilities.compile('^' + value + '$');
1317
1319
  const allCodes = this.codeSystem.getAllCodes();
1318
1320
  for (const code of allCodes) {
1321
+ this.opContext.deadCheck('cs:conceptFilter:regex');
1319
1322
  if (regex.test(code)) {
1320
1323
  const concept = this.codeSystem.getConceptByCode(code);
1321
1324
  if (concept) {
@@ -1349,6 +1352,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1349
1352
  }
1350
1353
  const descendants = this.codeSystem.getDescendants(ancestorCode);
1351
1354
  for (const code of descendants) {
1355
+ this.opContext.deadCheck('cs:addDescendants');
1352
1356
  if (code !== ancestorCode) {
1353
1357
  const concept = this.codeSystem.getConceptByCode(code);
1354
1358
  if (concept) {
@@ -1370,6 +1374,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1370
1374
  if (concept) {
1371
1375
  const descendants = this.codeSystem.getChildren(parentCode);
1372
1376
  for (const code of descendants) {
1377
+ this.opContext.deadCheck('cs:addChildren');
1373
1378
  if (code !== parentCode) { // should not be
1374
1379
  const concept = this.codeSystem.getConceptByCode(code);
1375
1380
  if (concept) {
@@ -1393,6 +1398,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1393
1398
 
1394
1399
  const allCodes = this.codeSystem.getAllCodes();
1395
1400
  for (const code of allCodes) {
1401
+ this.opContext.deadCheck('cs:childExistsFilter');
1396
1402
  const hasChildren = this.codeSystem.getChildren(code).length > 0;
1397
1403
  if (hasChildren === wantChildren) {
1398
1404
  const concept = this.codeSystem.getConceptByCode(code);
@@ -1419,6 +1425,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1419
1425
  const allConcepts = this.codeSystem.getAllConcepts();
1420
1426
 
1421
1427
  for (const concept of allConcepts) {
1428
+ this.opContext.deadCheck('cs:propertyFilter');
1422
1429
  if (this._conceptMatchesPropertyFilter(concept, propertyDef, op, value)) {
1423
1430
  results.add(concept, 0);
1424
1431
  }
@@ -1498,6 +1505,7 @@ class FhirCodeSystemProvider extends BaseCSServices {
1498
1505
  const allConcepts = this.codeSystem.getAllConcepts();
1499
1506
 
1500
1507
  for (const concept of allConcepts) {
1508
+ this.opContext.deadCheck('cs:knownPropertyFilter');
1501
1509
  let matches = false;
1502
1510
 
1503
1511
  if (prop === 'notSelectable') {
package/tx/cs/cs-loinc.js CHANGED
@@ -947,6 +947,7 @@ class LoincServices extends BaseCSServices {
947
947
  reject(err);
948
948
  } else {
949
949
  for (const row of rows) {
950
+ if (this.opContext) this.opContext.deadCheck('loinc:findRegexMatches');
950
951
  if (regex.test(row[valueColumn])) {
951
952
  matchingKeys.push(row[keyColumn]);
952
953
  }
@@ -1429,8 +1430,8 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1429
1430
  * @returns {Promise<Array>} Array of {code, display} objects
1430
1431
  */
1431
1432
  async #getAnswerListConcepts(sourceKey) {
1432
- return new Promise((resolve, reject) => {
1433
- let db = new sqlite3.Database(this.dbPath);
1433
+ const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY);
1434
+ try {
1434
1435
  const sql = `
1435
1436
  SELECT Code, Description
1436
1437
  FROM Relationships, Codes
@@ -1439,17 +1440,17 @@ class LoincServicesFactory extends CodeSystemFactoryProvider {
1439
1440
  AND Relationships.TargetKey = Codes.CodeKey
1440
1441
  `;
1441
1442
 
1442
- db.all(sql, [sourceKey], (err, rows) => {
1443
- if (err) {
1444
- reject(err);
1445
- } else {
1446
- const concepts = rows.map(row => ({
1447
- code: row.Code
1448
- }));
1449
- resolve(concepts);
1450
- }
1443
+ const rows = await new Promise((resolve, reject) => {
1444
+ db.all(sql, [sourceKey], (err, result) => {
1445
+ if (err) reject(err);
1446
+ else resolve(result);
1447
+ });
1451
1448
  });
1452
- });
1449
+
1450
+ return rows.map(row => ({ code: row.Code }));
1451
+ } finally {
1452
+ await new Promise((resolve) => db.close(() => resolve()));
1453
+ }
1453
1454
  }
1454
1455
 
1455
1456
  id() {
package/tx/cs/cs-omop.js CHANGED
@@ -906,41 +906,42 @@ class OMOPServicesFactory extends CodeSystemFactoryProvider {
906
906
  return new OMOPServices(opContext, supplements, db, this._sharedData);
907
907
  }
908
908
 
909
- static checkDB(dbPath) {
909
+ static async checkDB(dbPath) {
910
+ const fs = require('fs');
910
911
  try {
911
- const fs = require('fs');
912
-
913
- // Check if file exists
914
912
  if (!fs.existsSync(dbPath)) {
915
913
  return 'Database file not found';
916
914
  }
917
-
918
- // Check file size
919
915
  const stats = fs.statSync(dbPath);
920
916
  if (stats.size < 1024) {
921
917
  return 'Database file too small';
922
918
  }
919
+ } catch (e) {
920
+ return `Database error: ${e.message}`;
921
+ }
923
922
 
924
- // Try to open database and check for required tables
925
- const db = new sqlite3.Database(dbPath);
926
-
927
- try {
928
- // Simple count query to verify database integrity
929
- db.get('SELECT COUNT(*) as count FROM Concepts', (err) => {
930
- if (err) {
931
- db.close();
932
- return 'Missing Tables - needs re-importing (by java)';
933
- }
934
- });
935
-
936
- db.close();
937
- return 'OK (check via provider for count)';
938
- } catch (e) {
939
- return 'Missing Tables - needs re-importing (by java)';
940
- }
923
+ let db;
924
+ try {
925
+ db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY);
941
926
  } catch (e) {
942
927
  return `Database error: ${e.message}`;
943
928
  }
929
+
930
+ try {
931
+ // Simple count query to verify database integrity. If the Concepts
932
+ // table is missing, db.get rejects and we fall through to the catch.
933
+ const row = await new Promise((resolve, reject) => {
934
+ db.get('SELECT COUNT(*) as count FROM Concepts', (err, result) => {
935
+ if (err) reject(err);
936
+ else resolve(result);
937
+ });
938
+ });
939
+ return `OK (${row && row.count != null ? row.count : 0} Concepts)`;
940
+ } catch (_e) {
941
+ return 'Missing Tables - needs re-importing (by java)';
942
+ } finally {
943
+ await new Promise((resolve) => db.close(() => resolve()));
944
+ }
944
945
  }
945
946
 
946
947
 
@@ -5,7 +5,8 @@ const { AbstractCodeSystemProvider } = require('./cs-provider-api');
5
5
  */
6
6
  class ListCodeSystemProvider extends AbstractCodeSystemProvider {
7
7
  /**
8
- * {Map<String, CodeSystem>} A list of code system factories that contains all the preloaded native code systems
8
+ * {CodeSystem[]} The preloaded FHIR code systems in this list. This is an
9
+ * array — append with .push(), not Map-style .set().
9
10
  */
10
11
  codeSystems = [];
11
12
 
@@ -326,6 +326,20 @@ class SnomedServices {
326
326
  return 0;
327
327
  }
328
328
 
329
+ // Like getConceptRefSet, but distinguishes "this concept is not a reference
330
+ // set" (returns null) from "it is a reference set that happens to have no
331
+ // members" (returns a numeric membersByRef, which may be 0 or the
332
+ // MAGIC_NO_CHILDREN sentinel). Used by memberOf to reject non-refset operands.
333
+ _findRefSetMembersRef(conceptIndex) {
334
+ for (let i = 0; i < this.refSetIndex.count(); i++) {
335
+ const refSet = this.refSetIndex.getReferenceSet(i);
336
+ if (refSet.definition === conceptIndex) {
337
+ return refSet.membersByRef;
338
+ }
339
+ }
340
+ return null;
341
+ }
342
+
329
343
  // Filter support methods
330
344
  filterEquals(id) {
331
345
  const result = new SnomedFilterContext();
@@ -374,7 +388,7 @@ class SnomedServices {
374
388
  }
375
389
 
376
390
 
377
- filterGeneralizes(id = true) {
391
+ filterGeneralizes(id = true, opContext = null) {
378
392
  const result = new SnomedFilterContext();
379
393
  const conceptResult = this.concepts.findConcept(id);
380
394
 
@@ -386,6 +400,7 @@ class SnomedServices {
386
400
  let parents = this.getConceptParents(conceptResult.index);
387
401
  let isNew = true;
388
402
  while (isNew) {
403
+ if (opContext) opContext.deadCheck('ecl:filterGeneralizes');
389
404
  isNew = false;
390
405
  let np = [];
391
406
  for (let parent of parents) {
@@ -485,7 +500,7 @@ class SnomedServices {
485
500
  }
486
501
  let result;
487
502
  try {
488
- result = this._evalECLNode(ast);
503
+ result = this._evalECLNode(ast, opContext);
489
504
  } catch (err) {
490
505
  debugLog(err);
491
506
  throw new Issue('error', 'invalid', null, 'UNSUPPORTED_ECL', opContext.i18n.translate('UNSUPPORTED_ECL', opContext.langs, [eclExpression, err.message]), 'vs-invalid').handleAsOO(400);
@@ -495,7 +510,7 @@ class SnomedServices {
495
510
  // we actually need the full concept list, otherwise filterSize returns 0
496
511
  // and the iteration yields nothing. Materialise active concepts now.
497
512
  if (forIteration && result.eclWildcard && (!result.descendants || result.descendants.length === 0)) {
498
- result.descendants = this._eclEnumerateActiveConcepts();
513
+ result.descendants = this._eclEnumerateActiveConcepts(opContext);
499
514
  delete result.eclWildcard;
500
515
  }
501
516
  return result;
@@ -506,10 +521,11 @@ class SnomedServices {
506
521
  * when the filter needs to be iterated over (e.g. $expand).
507
522
  * @returns {number[]}
508
523
  */
509
- _eclEnumerateActiveConcepts = function () {
524
+ _eclEnumerateActiveConcepts = function (opContext) {
510
525
  const all = [];
511
526
  const n = this.concepts.count();
512
527
  for (let i = 0; i < n; i++) {
528
+ if (opContext) opContext.deadCheck('ecl:enumerateActiveConcepts');
513
529
  const concept = this.concepts.getConceptByCount(i);
514
530
  if ((concept.flags & 0x0F) === 0) { // active
515
531
  all.push(concept.index);
@@ -523,36 +539,37 @@ class SnomedServices {
523
539
  * @param {object} node
524
540
  * @returns {SnomedFilterContext}
525
541
  */
526
- _evalECLNode = function (node) {
542
+ _evalECLNode = function (node, opContext) {
527
543
  if (!node) {
528
544
  throw new Error('ECL evaluation error: null AST node');
529
545
  }
546
+ if (opContext) opContext.deadCheck('ecl:evalNode');
530
547
 
531
548
  switch (node.type) {
532
549
 
533
550
  case ECLNodeType.SUB_EXPRESSION_CONSTRAINT:
534
- return this._evalSubExpression(node);
551
+ return this._evalSubExpression(node, opContext);
535
552
 
536
553
  case ECLNodeType.COMPOUND_EXPRESSION_CONSTRAINT: {
537
- const left = this._evalECLNode(node.left);
538
- const right = this._evalECLNode(node.right);
554
+ const left = this._evalECLNode(node.left, opContext);
555
+ const right = this._evalECLNode(node.right, opContext);
539
556
  switch (node.operator) {
540
557
  case ECLNodeType.CONJUNCTION:
541
- return this._eclIntersect(left, right);
558
+ return this._eclIntersect(left, right, opContext);
542
559
  case ECLNodeType.DISJUNCTION:
543
- return this._eclUnion(left, right);
560
+ return this._eclUnion(left, right, opContext);
544
561
  case ECLNodeType.EXCLUSION:
545
- return this._eclMinus(left, right);
562
+ return this._eclMinus(left, right, opContext);
546
563
  default:
547
564
  throw new Error(`Unsupported ECL compound operator: ${node.operator}`);
548
565
  }
549
566
  }
550
567
 
551
568
  case ECLNodeType.REFINED_EXPRESSION_CONSTRAINT:
552
- return this._evalRefined(node);
569
+ return this._evalRefined(node, opContext);
553
570
 
554
571
  case ECLNodeType.DOTTED_EXPRESSION_CONSTRAINT:
555
- return this._evalDotted(node);
572
+ return this._evalDotted(node, opContext);
556
573
 
557
574
  default:
558
575
  // Could be a bare concept reference or wildcard passed in directly
@@ -561,7 +578,7 @@ class SnomedServices {
561
578
  node.type === ECLNodeType.WILDCARD ||
562
579
  node.type === ECLNodeType.MEMBER_OF) {
563
580
  // Wrap it as if it came from a no-operator SubExpressionConstraint
564
- return this._evalSubExpression({type: ECLNodeType.SUB_EXPRESSION_CONSTRAINT, operator: null, focus: node});
581
+ return this._evalSubExpression({type: ECLNodeType.SUB_EXPRESSION_CONSTRAINT, operator: null, focus: node}, opContext);
565
582
  }
566
583
  throw new Error(`Unsupported ECL node type: ${node.type}`);
567
584
  }
@@ -573,7 +590,7 @@ class SnomedServices {
573
590
  * @param {object} node
574
591
  * @returns {SnomedFilterContext}
575
592
  */
576
- _evalSubExpression = function (node) {
593
+ _evalSubExpression = function (node, opContext) {
577
594
  const operator = node.operator; // an ECLTokenType string, or null
578
595
  const focus = node.focus;
579
596
 
@@ -590,16 +607,16 @@ class SnomedServices {
590
607
  if (operator) {
591
608
  throw new Error('ECL hierarchy operators combined with ^ (member-of) are not yet supported');
592
609
  }
593
- return this._evalMemberOf(focus);
610
+ return this._evalMemberOf(focus, opContext);
594
611
  }
595
612
 
596
613
  // Plain concept reference
597
614
  if (focus.type === ECLNodeType.CONCEPT_REFERENCE) {
598
- return this._evalConceptWithOperator(focus.conceptId, operator);
615
+ return this._evalConceptWithOperator(focus.conceptId, operator, opContext);
599
616
  }
600
617
 
601
618
  // Parenthesised sub-expression: focus is itself a full constraint node
602
- return this._evalECLNode(focus);
619
+ return this._evalECLNode(focus, opContext);
603
620
  };
604
621
 
605
622
  /**
@@ -608,7 +625,7 @@ class SnomedServices {
608
625
  * @param {string|null} operator ECLTokenType constant
609
626
  * @returns {SnomedFilterContext}
610
627
  */
611
- _evalConceptWithOperator = function (conceptId, operator) {
628
+ _evalConceptWithOperator = function (conceptId, operator, opContext) {
612
629
  switch (operator) {
613
630
  case null:
614
631
  case undefined:
@@ -640,7 +657,7 @@ class SnomedServices {
640
657
 
641
658
  // ── Ancestors ──────────────────────────────────────────────────────────
642
659
  case ECLTokenType.ANCESTOR_OR_SELF_OF: { // >> self + all transitive ancestors
643
- const result = this.filterGeneralizes(conceptId);
660
+ const result = this.filterGeneralizes(conceptId, opContext);
644
661
  const self = this.concepts.findConcept(conceptId);
645
662
  if (self.found && !result.descendants.includes(self.index)) {
646
663
  result.descendants.push(self.index);
@@ -649,7 +666,7 @@ class SnomedServices {
649
666
  }
650
667
 
651
668
  case ECLTokenType.ANCESTOR_OF: { // > all transitive ancestors, no self
652
- return this.filterGeneralizes(conceptId);
669
+ return this.filterGeneralizes(conceptId, opContext);
653
670
  }
654
671
 
655
672
  case ECLTokenType.PARENT_OR_SELF_OF: { // >>! self + direct parents only
@@ -679,18 +696,54 @@ class SnomedServices {
679
696
  };
680
697
 
681
698
  /**
682
- * Evaluate a MEMBER_OF node. Only plain concept-reference refsets are
683
- * supported; complex expressions inside ^ are not yet supported.
699
+ * Evaluate a MEMBER_OF (^) node. The operand may be any ECL expression: it is
700
+ * resolved to a set of candidate reference-set concepts, and the result is the
701
+ * union of their active concept members' referenced components.
702
+ *
703
+ * When the operand is a bare, explicitly-named concept that is not a reference
704
+ * set, this throws (the caller asked for an error in that case). When the
705
+ * operand is a computed expression (e.g. ^(<<900000000000455006)), concepts in
706
+ * the resolved set that are not reference sets are simply skipped — the closure
707
+ * of "Reference set" necessarily includes the non-refset parent itself.
708
+ *
684
709
  * @param {object} memberOfNode
710
+ * @param {OperationContext} [opContext]
685
711
  * @returns {SnomedFilterContext}
686
712
  */
687
- _evalMemberOf = function (memberOfNode) {
688
- const refSet = memberOfNode.refSet;
689
- if (refSet.type !== ECLNodeType.CONCEPT_REFERENCE) {
690
- throw new Error('ECL ^ (member-of) with a non-concept-reference refset is not yet supported');
713
+ _evalMemberOf = function (memberOfNode, opContext) {
714
+ const operandIsBareRef = memberOfNode.refSet.type === ECLNodeType.CONCEPT_REFERENCE;
715
+ const refsetConcepts = this._eclResolveSet(this._evalECLNode(memberOfNode.refSet, opContext), opContext);
716
+
717
+ const members = new Set();
718
+ for (const refsetIdx of refsetConcepts) {
719
+ if (opContext) opContext.deadCheck('ecl:memberOf');
720
+ const membersRef = this._findRefSetMembersRef(refsetIdx);
721
+ if (membersRef === null) {
722
+ if (operandIsBareRef) {
723
+ const code = this.concepts.getConceptId(refsetIdx).toString();
724
+ throw new Error(`The SNOMED CT Concept ${code} is not a reference set`);
725
+ }
726
+ continue; // computed operand: ignore concepts that aren't reference sets
727
+ }
728
+ if (membersRef === 0 || membersRef === 0xFFFFFFFF) {
729
+ continue; // a reference set with no members
730
+ }
731
+ const memberList = this.refSetMembers.getMembers(membersRef);
732
+ for (const m of memberList || []) {
733
+ // Concept referenced components only (kind 0). Description/other members
734
+ // are excluded, which also prevents the no-component sentinel (0xFFFFFFFF)
735
+ // from leaking in. The referenced concept may itself be INACTIVE and is
736
+ // still returned — "active" in the spec qualifies the membership row (and
737
+ // inactive rows are already dropped at import), not the referenced
738
+ // concept. The expansion marks/handles inactivity (e.g. activeOnly).
739
+ if (m.kind === 0) {
740
+ members.add(m.ref);
741
+ }
742
+ }
691
743
  }
692
- // filterIn accepts a comma-separated string; a single ID works fine
693
- return this.filterIn(refSet.conceptId);
744
+ const result = new SnomedFilterContext();
745
+ result.descendants = [...members];
746
+ return result;
694
747
  };
695
748
 
696
749
  /**
@@ -714,8 +767,8 @@ class SnomedServices {
714
767
  * @param {object} node
715
768
  * @returns {SnomedFilterContext}
716
769
  */
717
- _evalDotted = function (node) {
718
- let current = this._eclResolveSet(this._evalECLNode(node.base));
770
+ _evalDotted = function (node, opContext) {
771
+ let current = this._eclResolveSet(this._evalECLNode(node.base, opContext), opContext);
719
772
 
720
773
  for (const attr of node.attributes || []) {
721
774
  if (attr.type !== ECLNodeType.CONCEPT_REFERENCE) {
@@ -729,6 +782,7 @@ class SnomedServices {
729
782
 
730
783
  const next = new Set();
731
784
  for (const conceptIdx of current) {
785
+ if (opContext) opContext.deadCheck('ecl:dotted');
732
786
  const relIdxs = this.getConceptRelationships(conceptIdx);
733
787
  for (const relIdx of relIdxs) {
734
788
  const rel = this.relationships.getRelationship(relIdx);
@@ -758,11 +812,12 @@ class SnomedServices {
758
812
  * @param {object} node
759
813
  * @returns {SnomedFilterContext}
760
814
  */
761
- _evalRefined = function (node) {
762
- const baseSet = this._eclResolveSet(this._evalECLNode(node.base));
815
+ _evalRefined = function (node, opContext) {
816
+ const baseSet = this._eclResolveSet(this._evalECLNode(node.base, opContext), opContext);
763
817
  const matching = [];
764
818
  for (const conceptIdx of baseSet) {
765
- if (this._refinementMatches(conceptIdx, node.refinement)) {
819
+ if (opContext) opContext.deadCheck('ecl:refined');
820
+ if (this._refinementMatches(conceptIdx, node.refinement, opContext)) {
766
821
  matching.push(conceptIdx);
767
822
  }
768
823
  }
@@ -778,17 +833,17 @@ class SnomedServices {
778
833
  * @param {object} refinement
779
834
  * @returns {boolean}
780
835
  */
781
- _refinementMatches = function (conceptIdx, refinement) {
836
+ _refinementMatches = function (conceptIdx, refinement, opContext) {
782
837
  switch (refinement.type) {
783
838
  case ECLNodeType.ATTRIBUTE:
784
- return this._attributeMatches(conceptIdx, refinement, null);
839
+ return this._attributeMatches(conceptIdx, refinement, null, opContext);
785
840
  case ECLNodeType.ATTRIBUTE_SET:
786
841
  for (const a of refinement.attributes) {
787
- if (!this._refinementMatches(conceptIdx, a)) return false;
842
+ if (!this._refinementMatches(conceptIdx, a, opContext)) return false;
788
843
  }
789
844
  return true;
790
845
  case ECLNodeType.ATTRIBUTE_GROUP:
791
- return this._attributeGroupMatches(conceptIdx, refinement);
846
+ return this._attributeGroupMatches(conceptIdx, refinement, opContext);
792
847
  default:
793
848
  throw new Error(`Unsupported refinement node type: ${refinement.type}`);
794
849
  }
@@ -804,7 +859,7 @@ class SnomedServices {
804
859
  * @param {number|null} groupFilter
805
860
  * @returns {boolean}
806
861
  */
807
- _attributeMatches = function (conceptIdx, attr, groupFilter) {
862
+ _attributeMatches = function (conceptIdx, attr, groupFilter, opContext) {
808
863
  if (attr.reverse) {
809
864
  throw new Error('ECL reverse attributes (R) are not yet supported');
810
865
  }
@@ -821,7 +876,7 @@ class SnomedServices {
821
876
  throw new Error('ECL refinements only support plain concept-reference attribute names');
822
877
  }
823
878
 
824
- const count = this._countAttributeMatches(conceptIdx, attr, groupFilter);
879
+ const count = this._countAttributeMatches(conceptIdx, attr, groupFilter, opContext);
825
880
 
826
881
  if (attr.cardinality) {
827
882
  return this._cardinalityAccepts(attr.cardinality, count);
@@ -838,25 +893,44 @@ class SnomedServices {
838
893
  * @param {number|null} groupFilter
839
894
  * @returns {number}
840
895
  */
841
- _countAttributeMatches = function (conceptIdx, attr, groupFilter) {
842
- const attrResult = this.concepts.findConcept(attr.name.conceptId);
843
- if (!attrResult.found) {
844
- throw new Error(`The SNOMED CT Concept ${attr.name.conceptId} is not known`);
896
+ _countAttributeMatches = function (conceptIdx, attr, groupFilter, opContext) {
897
+ // The attribute type and the value set depend only on the (static) AST node,
898
+ // not on the concept being tested, so resolve them once per refinement
899
+ // attribute and reuse across the whole base set. Without this memo the value
900
+ // expression is re-evaluated for every concept — and for a wildcard value
901
+ // (`= *`) the entire active-concept set is re-enumerated per concept —
902
+ // making refinement evaluation O(baseSet × valueExpr). The AST is parsed
903
+ // fresh per request, so memoising on the node is safe within a request.
904
+ let resolved = attr._eclResolved;
905
+ if (!resolved) {
906
+ const attrResult = this.concepts.findConcept(attr.name.conceptId);
907
+ if (!attrResult.found) {
908
+ throw new Error(`The SNOMED CT Concept ${attr.name.conceptId} is not known`);
909
+ }
910
+ resolved = {
911
+ attrTypeIdx: attrResult.index,
912
+ valueSet: new Set(this._eclResolveSet(this._evalECLNode(attr.comparison.value, opContext), opContext))
913
+ };
914
+ attr._eclResolved = resolved;
845
915
  }
846
- const attrTypeIdx = attrResult.index;
847
-
848
- const valueSet = new Set(this._eclResolveSet(this._evalECLNode(attr.comparison.value)));
916
+ const attrTypeIdx = resolved.attrTypeIdx;
917
+ const valueSet = resolved.valueSet;
849
918
 
850
919
  const relIdxs = this.getConceptRelationships(conceptIdx);
851
- let count = 0;
920
+ // ECL cardinality counts non-redundant matching attributes — i.e. distinct
921
+ // matching values — not raw relationship rows. The same (type, value) can
922
+ // appear in multiple role groups; counting rows over-counts and makes e.g.
923
+ // [1..1] fail and [2..*] succeed for a concept with a single morphology.
924
+ const matchedTargets = new Set();
852
925
  for (const relIdx of relIdxs) {
926
+ if (opContext) opContext.deadCheck('ecl:countAttributeMatches');
853
927
  const rel = this.relationships.getRelationship(relIdx);
854
928
  if (!rel.active) continue;
855
929
  if (rel.relType !== attrTypeIdx) continue;
856
930
  if (groupFilter !== null && rel.group !== groupFilter) continue;
857
- if (valueSet.has(rel.target)) count++;
931
+ if (valueSet.has(rel.target)) matchedTargets.add(rel.target);
858
932
  }
859
- return count;
933
+ return matchedTargets.size;
860
934
  };
861
935
 
862
936
  /**
@@ -884,7 +958,7 @@ class SnomedServices {
884
958
  * @param {object} group
885
959
  * @returns {boolean}
886
960
  */
887
- _attributeGroupMatches = function (conceptIdx, group) {
961
+ _attributeGroupMatches = function (conceptIdx, group, opContext) {
888
962
  const relIdxs = this.getConceptRelationships(conceptIdx);
889
963
  const groupNumbers = new Set();
890
964
  for (const relIdx of relIdxs) {
@@ -896,9 +970,10 @@ class SnomedServices {
896
970
 
897
971
  let matchingGroupCount = 0;
898
972
  for (const g of groupNumbers) {
973
+ if (opContext) opContext.deadCheck('ecl:attributeGroup');
899
974
  let allMatch = true;
900
975
  for (const attr of group.attributes) {
901
- if (!this._attributeMatches(conceptIdx, attr, g)) {
976
+ if (!this._attributeMatches(conceptIdx, attr, g, opContext)) {
902
977
  allMatch = false;
903
978
  break;
904
979
  }
@@ -939,9 +1014,9 @@ class SnomedServices {
939
1014
  * @param {SnomedFilterContext} ctx
940
1015
  * @returns {number[]}
941
1016
  */
942
- _eclResolveSet = function (ctx) {
1017
+ _eclResolveSet = function (ctx, opContext) {
943
1018
  if (ctx.eclWildcard && (!ctx.descendants || ctx.descendants.length === 0)) {
944
- return this._eclEnumerateActiveConcepts();
1019
+ return this._eclEnumerateActiveConcepts(opContext);
945
1020
  }
946
1021
  return this._eclToIndexArray(ctx);
947
1022
  };
@@ -949,20 +1024,24 @@ class SnomedServices {
949
1024
  /**
950
1025
  * AND: concepts present in both sets.
951
1026
  */
952
- _eclIntersect = function (left, right) {
1027
+ _eclIntersect = function (left, right, opContext) {
953
1028
  if (left.eclWildcard) return right;
954
1029
  if (right.eclWildcard) return left;
955
1030
  const leftSet = new Set(this._eclToIndexArray(left));
956
1031
  const result = new SnomedFilterContext();
957
- result.descendants = this._eclToIndexArray(right).filter(idx => leftSet.has(idx));
1032
+ result.descendants = this._eclToIndexArray(right).filter(idx => {
1033
+ if (opContext) opContext.deadCheck('ecl:intersect');
1034
+ return leftSet.has(idx);
1035
+ });
958
1036
  return result;
959
1037
  };
960
1038
 
961
1039
  /**
962
1040
  * OR: concepts present in either set.
963
1041
  */
964
- _eclUnion = function (left, right) {
1042
+ _eclUnion = function (left, right, opContext) {
965
1043
  if (left.eclWildcard || right.eclWildcard) return this._eclWildcard();
1044
+ if (opContext) opContext.deadCheck('ecl:union');
966
1045
  const combined = new Set([
967
1046
  ...this._eclToIndexArray(left),
968
1047
  ...this._eclToIndexArray(right)
@@ -975,7 +1054,7 @@ class SnomedServices {
975
1054
  /**
976
1055
  * MINUS: concepts in left that are not in right.
977
1056
  */
978
- _eclMinus = function (left, right) {
1057
+ _eclMinus = function (left, right, opContext) {
979
1058
  const result = new SnomedFilterContext();
980
1059
 
981
1060
  if (right.eclWildcard) {
@@ -989,6 +1068,7 @@ class SnomedServices {
989
1068
  // Enumerate all active concepts minus the right set
990
1069
  const all = [];
991
1070
  for (let i = 0; i < this.concepts.count(); i++) {
1071
+ if (opContext) opContext.deadCheck('ecl:minus');
992
1072
  const concept = this.concepts.getConceptByCount(i);
993
1073
  if (this.isActive(concept.index) && !rightSet.has(concept.index)) {
994
1074
  all.push(concept.index);
@@ -998,7 +1078,10 @@ class SnomedServices {
998
1078
  return result;
999
1079
  }
1000
1080
 
1001
- result.descendants = this._eclToIndexArray(left).filter(idx => !rightSet.has(idx));
1081
+ result.descendants = this._eclToIndexArray(left).filter(idx => {
1082
+ if (opContext) opContext.deadCheck('ecl:minus');
1083
+ return !rightSet.has(idx);
1084
+ });
1002
1085
  return result;
1003
1086
  };
1004
1087
 
package/tx/cs/cs-unii.js CHANGED
@@ -210,18 +210,18 @@ class UniiServicesFactory extends CodeSystemFactoryProvider {
210
210
  }
211
211
 
212
212
  async load() {
213
- let db = new sqlite3.Database(this.dbPath);
214
-
215
- return new Promise((resolve, reject) => {
216
- db.get('SELECT Version FROM UniiVersion', (err, row) => {
217
- if (err) {
218
- reject(new Error(err));
219
- } else {
220
- this._version = row ? row.Version : 'unknown';
221
- resolve(); // This resolves the Promise
222
- }
213
+ const db = new sqlite3.Database(this.dbPath, sqlite3.OPEN_READONLY);
214
+ try {
215
+ const row = await new Promise((resolve, reject) => {
216
+ db.get('SELECT Version FROM UniiVersion', (err, result) => {
217
+ if (err) reject(new Error(err));
218
+ else resolve(result);
219
+ });
223
220
  });
224
- });
221
+ this._version = row ? row.Version : 'unknown';
222
+ } finally {
223
+ await new Promise((resolve) => db.close(() => resolve()));
224
+ }
225
225
  }
226
226
 
227
227
  defaultVersion() {
Binary file