fhirsmith 0.9.6 → 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.
- package/CHANGELOG.md +20 -0
- package/library/folder-content-loader.js +91 -0
- package/npmprojector/npmprojector.js +2 -6
- package/package.json +1 -1
- package/publisher/publisher.js +101 -9
- package/registry/registry.js +6 -6
- package/server.js +6 -2
- package/translations/Messages.properties +1 -1
- package/tx/cs/cs-cs.js +8 -0
- package/tx/cs/cs-loinc.js +1 -0
- package/tx/cs/cs-provider-list.js +2 -1
- package/tx/cs/cs-snomed.js +142 -59
- package/tx/data/snomed-testing.cache +0 -0
- package/tx/library/canonical-resource.js +4 -2
- package/tx/library/designations.js +27 -20
- package/tx/library/renderer.js +303 -22
- package/tx/library/ucum-types.js +4 -1
- package/tx/library.js +65 -21
- package/tx/operation-context.js +13 -23
- package/tx/params.js +36 -8
- package/tx/provider.js +6 -3
- package/tx/tx-html.js +7 -0
- package/tx/tx.js +12 -13
- package/tx/vs/vs-vsac.js +157 -9
- package/tx/workers/expand.js +100 -96
- package/tx/workers/lookup.js +6 -0
- package/tx/workers/read.js +1 -1
- package/tx/workers/translate.js +20 -29
- package/tx/workers/validate.js +18 -10
- package/tx/workers/worker.js +1 -1
- package/tx/xversion/xv-bundle.js +1 -2
- package/tx/xversion/xv-codesystem.js +5 -2
- package/tx/xversion/xv-parameters.js +4 -4
- package/tx/xversion/xv-resource.js +2 -2
- package/tx/xversion/xv-terminologyCapabilities.js +11 -6
- package/tx/xversion/xv-valueset.js +7 -7
- package/publisher/task-draft.js +0 -463
package/tx/cs/cs-snomed.js
CHANGED
|
@@ -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.
|
|
683
|
-
*
|
|
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
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
693
|
-
|
|
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 (
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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 =
|
|
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
|
-
|
|
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))
|
|
931
|
+
if (valueSet.has(rel.target)) matchedTargets.add(rel.target);
|
|
858
932
|
}
|
|
859
|
-
return
|
|
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 =>
|
|
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 =>
|
|
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
|
|
|
Binary file
|
|
@@ -129,8 +129,10 @@ class CanonicalResource {
|
|
|
129
129
|
return this.dateIsMoreRecent(this.version, other.version);
|
|
130
130
|
case 'integer':
|
|
131
131
|
return parseInt(this.version, 10) > parseInt(other.version, 10);
|
|
132
|
-
case 'alpha':
|
|
133
|
-
default:
|
|
132
|
+
case 'alpha':
|
|
133
|
+
default:
|
|
134
|
+
// Return a boolean: true only when this.version sorts after other.version.
|
|
135
|
+
return this.version.localeCompare(other.version) > 0;
|
|
134
136
|
}
|
|
135
137
|
}
|
|
136
138
|
if (this.date && other.date && this.date != other.date) {
|
|
@@ -46,7 +46,7 @@ class SearchFilterText {
|
|
|
46
46
|
validateParameter(value, 'value', String);
|
|
47
47
|
validateOptionalParameter(returnRating, 'returnRating', Boolean);
|
|
48
48
|
|
|
49
|
-
if (this.
|
|
49
|
+
if (this.isNull) {
|
|
50
50
|
return returnRating ? {passes: true, rating: 0} : true;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -610,35 +610,42 @@ class Designations {
|
|
|
610
610
|
|
|
611
611
|
const matchTypes = [LangMatchType.FULL, LangMatchType.LANG_REGION, LangMatchType.LANG];
|
|
612
612
|
|
|
613
|
-
for (const
|
|
614
|
-
for (const
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
supplements
|
|
613
|
+
for (const activeOnly of [true, false]) {
|
|
614
|
+
for (const matchType of matchTypes) {
|
|
615
|
+
for (const cd of this.designations) {
|
|
616
|
+
if (this._langMatches(lang, cd.language, matchType) && this.isDisplay(cd) && (!activeOnly || cd.isActive())) {
|
|
617
|
+
if (supplements && cd.source) {
|
|
618
|
+
supplements.add(cd.source);
|
|
619
|
+
}
|
|
620
|
+
return cd;
|
|
618
621
|
}
|
|
619
|
-
return cd;
|
|
620
622
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
623
|
+
for (const cd of this.designations) {
|
|
624
|
+
if (this._langMatches(lang, cd.language, matchType) && this._isPreferred(cd)&& (!activeOnly || cd.isActive())) {
|
|
625
|
+
if (supplements && cd.source) {
|
|
626
|
+
supplements.add(cd.source);
|
|
627
|
+
}
|
|
628
|
+
return cd;
|
|
626
629
|
}
|
|
627
|
-
return cd;
|
|
628
630
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
631
|
+
for (const cd of this.designations) {
|
|
632
|
+
if (this._langMatches(lang, cd.language, matchType) && (!activeOnly || cd.isActive())) {
|
|
633
|
+
if (supplements && cd.source) {
|
|
634
|
+
supplements.add(cd.source);
|
|
635
|
+
}
|
|
636
|
+
return cd;
|
|
634
637
|
}
|
|
635
|
-
return cd;
|
|
636
638
|
}
|
|
637
639
|
}
|
|
638
640
|
}
|
|
639
641
|
}
|
|
640
642
|
for (const cd of this.designations) {
|
|
641
|
-
if (!cd.language && this.isDisplay(cd)) {
|
|
643
|
+
if (!cd.language && this.isDisplay(cd) && cd.isActive()) {
|
|
644
|
+
if (supplements && cd.source) {
|
|
645
|
+
supplements.add(cd.source);
|
|
646
|
+
}
|
|
647
|
+
return cd;
|
|
648
|
+
} if (!cd.language && this.isDisplay(cd)) {
|
|
642
649
|
if (supplements && cd.source) {
|
|
643
650
|
supplements.add(cd.source);
|
|
644
651
|
}
|