fhirsmith 0.10.1 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/changelog-template.md +22 -0
- package/library/logger.js +8 -1
- package/library/package-manager.js +40 -2
- package/npmprojector/watcher.js +1 -1
- package/package.json +3 -2
- package/packages/packages.js +28 -15
- package/publisher/publisher.js +8 -8
- package/registry/api.js +92 -31
- package/registry/crawler.js +15 -1
- package/registry/model.js +158 -8
- package/registry/readme.md +5 -13
- package/registry/registry.js +87 -46
- package/registry/test-runner.js +1 -1
- package/server.js +8 -1
- package/stats.js +5 -1
- package/token/token.js +1 -1
- package/translations/Messages.properties +3 -1
- package/translations/rendering-phrases.properties +1227 -1218
- package/translations/rendering-phrases_ar.properties +1097 -1091
- package/translations/rendering-phrases_fr.properties +626 -624
- package/translations/rendering-phrases_nl.properties +976 -970
- package/translations/rendering-phrases_pt.properties +1026 -1020
- package/translations/rendering-phrases_ru.properties +1100 -1094
- package/tsconfig.eslint.json +9 -0
- package/tx/cs/cs-api.js +57 -4
- package/tx/cs/cs-cs.js +98 -25
- package/tx/cs/cs-hgvs.js +4 -0
- package/tx/cs/cs-lang.js +8 -0
- package/tx/cs/cs-mimetypes.js +4 -0
- package/tx/cs/cs-snomed.js +530 -75
- package/tx/cs/cs-uri.js +4 -0
- package/tx/data/snomed-subset.txt +40 -3
- package/tx/html/home-metrics.liquid +16 -1
- package/tx/importers/import-sct.module.js +18 -0
- package/tx/library/designations.js +3 -5
- package/tx/library/renderer.js +227 -30
- package/tx/library.js +5 -1
- package/tx/operation-context.js +131 -5
- package/tx/provider.js +38 -9
- package/tx/sct/ecl.js +30 -11
- package/tx/sct/expressions.js +28 -1
- package/tx/sct/structures.js +5 -5
- package/tx/tx-html.js +24 -15
- package/tx/tx.fhir.org.yml +1 -4
- package/tx/tx.js +18 -2
- package/tx/vs/vs-package.js +52 -0
- package/tx/workers/batch-validate.js +125 -18
- package/tx/workers/cache-control.js +31 -2
- package/tx/workers/expand.js +150 -76
- package/tx/workers/metadata.js +27 -6
- package/tx/workers/search.js +7 -1
- package/tx/workers/validate.js +30 -18
- package/tx/workers/worker.js +39 -7
- package/tx/xversion/xv-valueset.js +5 -2
- package/xig/xig.js +3 -3
package/tx/workers/expand.js
CHANGED
|
@@ -220,7 +220,7 @@ class ValueSetExpander {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
async listDisplaysFromProvider(displays, cs, context) {
|
|
223
|
-
await cs.designations(context, displays);
|
|
223
|
+
await cs.designations(context, displays, true);
|
|
224
224
|
displays.source = cs;
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -314,14 +314,20 @@ class ValueSetExpander {
|
|
|
314
314
|
return null;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
// expandLimitation() caps how many codes a single code system may contribute
|
|
318
|
+
// (e.g. CPT limits enumeration to 1000 codes by agreement with the AMA).
|
|
319
|
+
// NB: expandLimitation and system are methods - reading them as properties
|
|
320
|
+
// (cs.expandLimitation > 0) compares the function object and is always false,
|
|
321
|
+
// which silently disabled this cap.
|
|
322
|
+
const expandLimit = cs != null ? cs.expandLimitation() : 0;
|
|
323
|
+
if (expandLimit > 0) {
|
|
324
|
+
let cnt = this.csCounter.get(cs.system());
|
|
319
325
|
if (cnt == null) {
|
|
320
326
|
cnt = new ValueSetCounter();
|
|
321
|
-
this.csCounter.set(cs.system, cnt);
|
|
327
|
+
this.csCounter.set(cs.system(), cnt);
|
|
322
328
|
}
|
|
323
329
|
cnt.increment();
|
|
324
|
-
if (cnt.count >
|
|
330
|
+
if (cnt.count > expandLimit) {
|
|
325
331
|
return null;
|
|
326
332
|
}
|
|
327
333
|
}
|
|
@@ -566,7 +572,7 @@ class ValueSetExpander {
|
|
|
566
572
|
this.checkResourceCanonicalStatus(expansion, vs, this.valueSet);
|
|
567
573
|
|
|
568
574
|
for (const c of vs.expansion.contains || []) {
|
|
569
|
-
this.worker.
|
|
575
|
+
await this.worker.checkAndYield('importValueSet');
|
|
570
576
|
count += await this.importValueSetItem(null, c, imports, offset);
|
|
571
577
|
}
|
|
572
578
|
return count;
|
|
@@ -574,7 +580,7 @@ class ValueSetExpander {
|
|
|
574
580
|
|
|
575
581
|
async importValueSetItem(p, c, imports, offset) {
|
|
576
582
|
let count = 0;
|
|
577
|
-
this.worker.
|
|
583
|
+
await this.worker.checkAndYield('importValueSetItem');
|
|
578
584
|
const s = this.keyC(c);
|
|
579
585
|
if (this.passesImports(imports, c.system, c.code, offset) && !this.map.has(s) && !this.isExcluded(c.system, c.version, c.code)) {
|
|
580
586
|
count++;
|
|
@@ -588,7 +594,7 @@ class ValueSetExpander {
|
|
|
588
594
|
this.map.set(s, c);
|
|
589
595
|
}
|
|
590
596
|
for (const cc of c.contains || []) {
|
|
591
|
-
this.worker.
|
|
597
|
+
await this.worker.checkAndYield('importValueSetItem');
|
|
592
598
|
count += await this.importValueSetItem(c, cc, imports, offset);
|
|
593
599
|
}
|
|
594
600
|
return count;
|
|
@@ -610,11 +616,11 @@ class ValueSetExpander {
|
|
|
610
616
|
}
|
|
611
617
|
|
|
612
618
|
async checkSource(cset, exp, filter, srcURL, ts, vsInfo , source) {
|
|
613
|
-
this.worker.
|
|
619
|
+
await this.worker.checkAndYield('checkSource');
|
|
614
620
|
Extensions.checkNoModifiers(cset, 'ValueSetExpander.checkSource', 'set', srcURL);
|
|
615
621
|
let imp = false;
|
|
616
622
|
for (const u of cset.valueSet || []) {
|
|
617
|
-
this.worker.
|
|
623
|
+
await this.worker.checkAndYield('checkSource');
|
|
618
624
|
const s = this.worker.pinValueSet(u);
|
|
619
625
|
await this.checkCanExpandValueSet(s, '', source);
|
|
620
626
|
imp = true;
|
|
@@ -661,16 +667,23 @@ class ValueSetExpander {
|
|
|
661
667
|
if (cs.specialEnumeration()) {
|
|
662
668
|
await this.checkCanExpandValueSet(cs.specialEnumeration(), '', null);
|
|
663
669
|
} else if (filter.isNull) {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
670
|
+
// The unclosed marking for a grammar code system is added by
|
|
671
|
+
// processCodes (the actual expansion); checkSource only guards size.
|
|
672
|
+
// Refuse up-front only when enumeration could not succeed anyway:
|
|
673
|
+
// count=0 is answered by counting (no enumeration), and a count/offset
|
|
674
|
+
// window that fits inside limitCount is answered by partial
|
|
675
|
+
// enumeration (includeCode stops at limitCount). Otherwise the walk
|
|
676
|
+
// would only ever end in VALUESET_TOO_COSTLY at limitCount codes -
|
|
677
|
+
// throw the same error now without doing the work.
|
|
678
|
+
// NB: totalCount is a method - as a property read (cs.totalCount) the
|
|
679
|
+
// comparison was against the function object, always false, so this
|
|
680
|
+
// guard was silently dead.
|
|
681
|
+
if (!imp && this.count !== 0 && this.limitCount > 0 && await cs.totalCount() > this.limitCount
|
|
682
|
+
&& !(this.count > 0 && this.count + Math.max(this.offset, 0) <= this.limitCount)) {
|
|
683
|
+
// Report the versioned url (vurl), matching the message the
|
|
684
|
+
// enumeration path produces when it hits the same limit - this
|
|
685
|
+
// guard is just that refusal moved earlier.
|
|
686
|
+
throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [source && source.vurl ? source.vurl : srcURL, '>' + this.limitCount]), null, 422).withDiagnostics(this.worker.opContext.diagnostics());
|
|
674
687
|
}
|
|
675
688
|
}
|
|
676
689
|
}
|
|
@@ -690,7 +703,7 @@ class ValueSetExpander {
|
|
|
690
703
|
}
|
|
691
704
|
|
|
692
705
|
async includeCodes(cset, path, vsSrc, compose, filter, expansion, excludeInactive, notClosed) {
|
|
693
|
-
this.worker.
|
|
706
|
+
await this.worker.checkAndYield('processCodes#1');
|
|
694
707
|
const valueSets = [];
|
|
695
708
|
|
|
696
709
|
Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set', vsSrc.vurl);
|
|
@@ -701,7 +714,7 @@ class ValueSetExpander {
|
|
|
701
714
|
|
|
702
715
|
if (!cset.system) {
|
|
703
716
|
for (const u of cset.valueSet) {
|
|
704
|
-
this.worker.
|
|
717
|
+
await this.worker.checkAndYield('processCodes#2');
|
|
705
718
|
const s = this.worker.pinValueSet(u);
|
|
706
719
|
this.worker.opContext.log('import value set ' + s);
|
|
707
720
|
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
@@ -728,7 +741,7 @@ class ValueSetExpander {
|
|
|
728
741
|
this.addParamUri(expansion, 'used-codesystem', sv);
|
|
729
742
|
|
|
730
743
|
for (const u of cset.valueSet || []) {
|
|
731
|
-
this.worker.
|
|
744
|
+
await this.worker.checkAndYield('processCodes#2');
|
|
732
745
|
const s = this.worker.pinValueSet(u);
|
|
733
746
|
this.worker.opContext.log('import value set ' + s);
|
|
734
747
|
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
@@ -746,48 +759,70 @@ class ValueSetExpander {
|
|
|
746
759
|
let vs = await this.worker.findValueSet(cs.specialEnumeration(), '', null);
|
|
747
760
|
const base = await this.expandValueSet(cs.specialEnumeration(), '', vs, filter, notClosed);
|
|
748
761
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
749
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
762
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
750
763
|
await this.importValueSet(base, expansion, valueSets, 0);
|
|
751
764
|
notClosed.value = true;
|
|
765
|
+
} else if (!cs.canBeExpanded()) {
|
|
766
|
+
throw new Issue("error", "not-supported", null, 'CODESYSTEM_NOT_ENUMERABLE', this.worker.i18n.translate('CODESYSTEM_NOT_ENUMERABLE', this.params.httpLanguages, [cs.system()]), null, 422).withDiagnostics(this.worker.opContext.diagnostics());
|
|
752
767
|
} else if (filter.isNull) {
|
|
753
768
|
this.worker.opContext.log('add whole code system');
|
|
754
769
|
if (cs.isNotClosed()) {
|
|
755
770
|
if (cs.specialEnumeration()) {
|
|
756
771
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
757
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
772
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
758
773
|
} else {
|
|
759
|
-
|
|
774
|
+
// Grammar code system with no special enumeration (e.g. SNOMED CT):
|
|
775
|
+
// enumerate the precoordinated concepts we can, and mark the
|
|
776
|
+
// expansion incomplete rather than refusing it.
|
|
777
|
+
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
778
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members");
|
|
760
779
|
}
|
|
761
780
|
notClosed.value = true;
|
|
762
781
|
}
|
|
763
782
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
783
|
+
if (this.count === 0 && filters.length === 0 && valueSets.length === 0) {
|
|
784
|
+
// count=0 (count only) with a single whole-code-system include and no
|
|
785
|
+
// additional filters: report the code count directly rather than
|
|
786
|
+
// enumerating - which for a poly-hierarchy (e.g. SNOMED) is both
|
|
787
|
+
// expensive and pointless when no codes are being materialised.
|
|
788
|
+
this.addToTotal(await cs.countAllCodes(excludeInactive));
|
|
789
|
+
} else {
|
|
790
|
+
const iter = await cs.iterator(null);
|
|
791
|
+
if (valueSets.length === 0 && this.limitCount > 0 && (iter && iter.total > this.limitCount) && this.offset < 0) {
|
|
792
|
+
throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [vsSrc.vurl, '>' + this.limitCount]), null, 422).withDiagnostics(this.worker.opContext.diagnostics());
|
|
767
793
|
|
|
768
|
-
}
|
|
769
|
-
let tcount = 0;
|
|
770
|
-
let c = await cs.nextContext(iter);
|
|
771
|
-
while (c) {
|
|
772
|
-
this.worker.deadCheck('processCodes#3a');
|
|
773
|
-
if (await this.passesFilters(cs, c, prep, filters, 0)) {
|
|
774
|
-
tcount += await this.includeCodeAndDescendants(cs, c, expansion, valueSets, null, excludeInactive, vsSrc.vurl);
|
|
775
794
|
}
|
|
776
|
-
|
|
795
|
+
let tcount = 0;
|
|
796
|
+
// One visited set across all roots: in a poly-hierarchy the walks
|
|
797
|
+
// from different roots can reach the same concepts.
|
|
798
|
+
const visited = new Set();
|
|
799
|
+
let c = await cs.nextContext(iter);
|
|
800
|
+
while (c) {
|
|
801
|
+
await this.worker.checkAndYield('processCodes#3a');
|
|
802
|
+
if (await this.passesFilters(cs, c, prep, filters, 0)) {
|
|
803
|
+
tcount += await this.includeCodeAndDescendants(cs, c, expansion, valueSets, null, excludeInactive, vsSrc.vurl, visited);
|
|
804
|
+
}
|
|
805
|
+
c = await cs.nextContext(iter);
|
|
806
|
+
}
|
|
807
|
+
this.addToTotal(tcount);
|
|
777
808
|
}
|
|
778
|
-
this.addToTotal(tcount);
|
|
779
809
|
} else {
|
|
780
810
|
this.worker.opContext.log('prepare filters');
|
|
781
|
-
this.noTotal();
|
|
782
811
|
if (cs.isNotClosed(filter)) {
|
|
783
812
|
notClosed.value = true;
|
|
784
813
|
}
|
|
785
814
|
const prep = await cs.getPrepContext(true);
|
|
786
815
|
const ctxt = await cs.searchFilter(prep, filter, false);
|
|
787
816
|
let set = await cs.executeFilters(prep);
|
|
817
|
+
if (this.count === 0 && valueSets.length === 0) {
|
|
818
|
+
// count=0 (count only): report the filtered size directly without
|
|
819
|
+
// materialising the codes.
|
|
820
|
+
this.addToTotal(await cs.filterSize(ctxt, set[0]));
|
|
821
|
+
} else {
|
|
822
|
+
this.noTotal();
|
|
788
823
|
this.worker.opContext.log('iterate filters');
|
|
789
824
|
while (await cs.filterMore(ctxt, set[0])) {
|
|
790
|
-
this.worker.
|
|
825
|
+
await this.worker.checkAndYield('processCodes#4');
|
|
791
826
|
const c = await cs.filterConcept(ctxt, set[0]);
|
|
792
827
|
if (await this.passesFilters(cs, c, prep, set, 1)) {
|
|
793
828
|
const cds = new Designations(this.worker.i18n.languageDefinitions);
|
|
@@ -808,6 +843,7 @@ class ValueSetExpander {
|
|
|
808
843
|
}
|
|
809
844
|
}
|
|
810
845
|
this.worker.opContext.log('iterate filters done');
|
|
846
|
+
}
|
|
811
847
|
}
|
|
812
848
|
}
|
|
813
849
|
|
|
@@ -816,7 +852,7 @@ class ValueSetExpander {
|
|
|
816
852
|
const cds = new Designations(this.worker.i18n.languageDefinitions);
|
|
817
853
|
|
|
818
854
|
for (const cc of cset.concept) {
|
|
819
|
-
this.worker.
|
|
855
|
+
await this.worker.checkAndYield('processCodes#3');
|
|
820
856
|
cds.clear();
|
|
821
857
|
Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference', vsSrc.vurl);
|
|
822
858
|
const cctxt = await cs.locate(cc.code, this.allAltCodes);
|
|
@@ -856,18 +892,18 @@ class ValueSetExpander {
|
|
|
856
892
|
|
|
857
893
|
if (cs.specialEnumeration()) {
|
|
858
894
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
859
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
895
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
860
896
|
notClosed.value = true;
|
|
861
897
|
}
|
|
862
898
|
|
|
863
899
|
for (let i = 0; i < fcl.length; i++) {
|
|
864
|
-
this.worker.
|
|
900
|
+
await this.worker.checkAndYield('processCodes#4a');
|
|
865
901
|
const fc = fcl[i];
|
|
866
902
|
if (!fc.value) {
|
|
867
903
|
throw new Issue('error', 'invalid', path + ".filter[" + i + "]", 'UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.worker.i18n.translate('UNABLE_TO_HANDLE_SYSTEM_FILTER_WITH_NO_VALUE', this.params.httpLanguages, [cs.system(), fc.property, fc.op]), 'vs-invalid', 400);
|
|
868
904
|
}
|
|
869
905
|
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
|
|
870
|
-
await cs.filter(prep, i == 0, fc.property, fc.op, fc.value);
|
|
906
|
+
await cs.filter(prep, i == 0, fc.property, fc.op, fc.value, vsSrc.isCached ? fc : null);
|
|
871
907
|
}
|
|
872
908
|
|
|
873
909
|
const fset = await cs.executeFilters(prep);
|
|
@@ -881,7 +917,7 @@ class ValueSetExpander {
|
|
|
881
917
|
this.addToTotal(0);
|
|
882
918
|
const cds = new Designations(this.worker.i18n.languageDefinitions);
|
|
883
919
|
while (await cs.filterMore(prep, fset[0])) {
|
|
884
|
-
this.worker.
|
|
920
|
+
await this.worker.checkAndYield('processCodes#5');
|
|
885
921
|
const c = await cs.filterConcept(prep, fset[0]);
|
|
886
922
|
const ok = (!this.params.activeOnly || !await cs.isInactive(c)) && (await this.passesFilters(cs, c, prep, fset, 1));
|
|
887
923
|
if (ok) {
|
|
@@ -930,7 +966,7 @@ class ValueSetExpander {
|
|
|
930
966
|
}
|
|
931
967
|
|
|
932
968
|
async excludeCodes(cset, path, vsSrc, filter, expansion, excludeInactive, notClosed) {
|
|
933
|
-
this.worker.
|
|
969
|
+
await this.worker.checkAndYield('processCodes#1');
|
|
934
970
|
const valueSets = [];
|
|
935
971
|
|
|
936
972
|
Extensions.checkNoModifiers(cset, 'ValueSetExpander.processCodes', 'set', vsSrc.vurl);
|
|
@@ -944,7 +980,7 @@ class ValueSetExpander {
|
|
|
944
980
|
this.noTotal();
|
|
945
981
|
for (const u of cset.valueSet) {
|
|
946
982
|
const s = this.worker.pinValueSet(u);
|
|
947
|
-
this.worker.
|
|
983
|
+
await this.worker.checkAndYield('processCodes#2');
|
|
948
984
|
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
949
985
|
const ivs = new ImportedValueSet(await this.expandValueSet(s, '', vs, filter, notClosed));
|
|
950
986
|
this.checkResourceCanonicalStatus(expansion, ivs.valueSet, this.valueSet);
|
|
@@ -970,7 +1006,7 @@ class ValueSetExpander {
|
|
|
970
1006
|
this.addParamUri(expansion, 'used-codesystem', sv);
|
|
971
1007
|
|
|
972
1008
|
for (const u of cset.valueSet || []) {
|
|
973
|
-
this.worker.
|
|
1009
|
+
await this.worker.checkAndYield('processCodes#3');
|
|
974
1010
|
const s = this.worker.pinValueSet(u);
|
|
975
1011
|
this.worker.opContext.log('import value set ' + s);
|
|
976
1012
|
let vs = await this.worker.findValueSet(s, '', vsSrc);
|
|
@@ -993,7 +1029,7 @@ class ValueSetExpander {
|
|
|
993
1029
|
if (iter) {
|
|
994
1030
|
let c = await cs.nextContext(iter);
|
|
995
1031
|
while (c) {
|
|
996
|
-
this.worker.
|
|
1032
|
+
await this.worker.checkAndYield('processCodes#3aa');
|
|
997
1033
|
this.excludeCode(cs, cs.system(), cs.version(), await cs.code(c), expansion, valueSets, vsSrc.url);
|
|
998
1034
|
c = await cs.nextContext(iter);
|
|
999
1035
|
}
|
|
@@ -1003,9 +1039,13 @@ class ValueSetExpander {
|
|
|
1003
1039
|
if (cs.isNotClosed(filter)) {
|
|
1004
1040
|
if (cs.specialEnumeration()) {
|
|
1005
1041
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
1006
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
1042
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
1007
1043
|
} else {
|
|
1008
|
-
|
|
1044
|
+
// Grammar code system with no special enumeration (e.g. SNOMED CT):
|
|
1045
|
+
// enumerate the precoordinated concepts we can, and mark the
|
|
1046
|
+
// expansion incomplete rather than refusing it.
|
|
1047
|
+
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
1048
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members");
|
|
1009
1049
|
}
|
|
1010
1050
|
}
|
|
1011
1051
|
|
|
@@ -1013,7 +1053,7 @@ class ValueSetExpander {
|
|
|
1013
1053
|
if (iter) {
|
|
1014
1054
|
let c = await cs.nextContext(iter);
|
|
1015
1055
|
while (c) {
|
|
1016
|
-
this.worker.
|
|
1056
|
+
await this.worker.checkAndYield('processCodes#3a');
|
|
1017
1057
|
if (await this.passesFilters(cs, c, prep, filters, 0)) {
|
|
1018
1058
|
this.excludeCode(cs, cs.system(), cs.version(), await cs.code(c), expansion, valueSets, vsSrc.url);
|
|
1019
1059
|
}
|
|
@@ -1027,7 +1067,7 @@ class ValueSetExpander {
|
|
|
1027
1067
|
this.worker.opContext.log('iterate concepts');
|
|
1028
1068
|
const cds = new Designations(this.worker.i18n.languageDefinitions);
|
|
1029
1069
|
for (const cc of cset.concept) {
|
|
1030
|
-
this.worker.
|
|
1070
|
+
await this.worker.checkAndYield('processCodes#3');
|
|
1031
1071
|
cds.clear();
|
|
1032
1072
|
Extensions.checkNoModifiers(cc, 'ValueSetExpander.processCodes', 'set concept reference', vsSrc.vurl);
|
|
1033
1073
|
const cctxt = await cs.locate(cc.code, this.allAltCodes);
|
|
@@ -1053,14 +1093,14 @@ class ValueSetExpander {
|
|
|
1053
1093
|
if (cs.specialEnumeration()) {
|
|
1054
1094
|
await cs.specialFilter(prep, true);
|
|
1055
1095
|
Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
|
|
1056
|
-
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason",
|
|
1096
|
+
Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", "The code System '" + cs.system() + "' has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
|
|
1057
1097
|
}
|
|
1058
1098
|
|
|
1059
1099
|
let first = true;
|
|
1060
1100
|
for (let fc of cset.filter) {
|
|
1061
|
-
this.worker.
|
|
1101
|
+
await this.worker.checkAndYield('processCodes#4a');
|
|
1062
1102
|
Extensions.checkNoModifiers(fc, 'ValueSetExpander.processCodes', 'filter', vsSrc.vurl);
|
|
1063
|
-
await cs.filter(prep, first, fc.property, fc.op, fc.value);
|
|
1103
|
+
await cs.filter(prep, first, fc.property, fc.op, fc.value, vsSrc.isCached ? fc : null);
|
|
1064
1104
|
first = false;
|
|
1065
1105
|
}
|
|
1066
1106
|
|
|
@@ -1071,7 +1111,7 @@ class ValueSetExpander {
|
|
|
1071
1111
|
}
|
|
1072
1112
|
//let count = 0;
|
|
1073
1113
|
while (await cs.filterMore(prep, fset[0])) {
|
|
1074
|
-
this.worker.
|
|
1114
|
+
await this.worker.checkAndYield('processCodes#5');
|
|
1075
1115
|
const c = await cs.filterConcept(prep, fset[0]);
|
|
1076
1116
|
const ok = (!this.params.activeOnly || !await cs.isInactive(c)) && (await this.passesFilters(cs, c, prep, fset, 1));
|
|
1077
1117
|
if (ok) {
|
|
@@ -1087,9 +1127,30 @@ class ValueSetExpander {
|
|
|
1087
1127
|
}
|
|
1088
1128
|
}
|
|
1089
1129
|
|
|
1090
|
-
async includeCodeAndDescendants(cs, context, expansion, imports, parent, excludeInactive, srcUrl) {
|
|
1130
|
+
async includeCodeAndDescendants(cs, context, expansion, imports, parent, excludeInactive, srcUrl, visited = null) {
|
|
1091
1131
|
let result = 0;
|
|
1092
|
-
this.worker.
|
|
1132
|
+
await this.worker.checkAndYield('processCodeAndDescendants');
|
|
1133
|
+
|
|
1134
|
+
// In a poly-hierarchy (e.g. SNOMED CT) a concept is reachable through every
|
|
1135
|
+
// one of its parents; without a visited set the recursion walks every *path*
|
|
1136
|
+
// rather than every *node* - for the full SNOMED International edition that
|
|
1137
|
+
// is ~15.8M visits for ~520k concepts, which is what turned an aborted
|
|
1138
|
+
// expansion into a 30-second event-loop stall. Reaching a concept a second
|
|
1139
|
+
// time means it has multiple parents, so (as when includeCode detects a
|
|
1140
|
+
// duplicate) the expansion cannot be represented as a tree.
|
|
1141
|
+
if (visited == null) {
|
|
1142
|
+
visited = new Set();
|
|
1143
|
+
}
|
|
1144
|
+
// NB: contexts do not reliably carry a .code property (SnomedExpressionContext
|
|
1145
|
+
// does not) - cs.code(context) is the accessor. Reading context.code here
|
|
1146
|
+
// used to yield undefined for every SNOMED concept, collapsing the whole
|
|
1147
|
+
// expansion onto one map key so no size guard could ever fire.
|
|
1148
|
+
const code = await cs.code(context);
|
|
1149
|
+
if (visited.has(code)) {
|
|
1150
|
+
this.canBeHierarchy = false;
|
|
1151
|
+
return 0;
|
|
1152
|
+
}
|
|
1153
|
+
visited.add(code);
|
|
1093
1154
|
|
|
1094
1155
|
if (expansion) {
|
|
1095
1156
|
const vs = this.canonical(await cs.system(), await cs.version());
|
|
@@ -1105,12 +1166,12 @@ class ValueSetExpander {
|
|
|
1105
1166
|
const cds = new Designations(this.worker.i18n.languageDefinitions);
|
|
1106
1167
|
let t;
|
|
1107
1168
|
if (this.noDetails) {
|
|
1108
|
-
t = await this.includeCode(cs, null, await cs.system(), await cs.version(),
|
|
1109
|
-
null, expansion, imports, null, null, null, null, excludeInactive, srcUrl);
|
|
1169
|
+
t = await this.includeCode(cs, null, await cs.system(), await cs.version(), code, await cs.isAbstract(context), await cs.isInactive(context), null, null, null,
|
|
1170
|
+
null, null, expansion, imports, null, null, null, null, excludeInactive, srcUrl);
|
|
1110
1171
|
|
|
1111
1172
|
} else {
|
|
1112
1173
|
await this.listDisplaysFromProvider(cds, cs, context);
|
|
1113
|
-
t = await this.includeCode(cs, parent, await cs.system(), await cs.version(),
|
|
1174
|
+
t = await this.includeCode(cs, parent, await cs.system(), await cs.version(), code, await cs.isAbstract(context), await cs.isInactive(context), await cs.isDeprecated(context), await cs.getStatus(context), cds, await cs.definition(context),
|
|
1114
1175
|
await cs.itemWeight(context), expansion, imports, await cs.extensions(context), null, await cs.properties(context), null, excludeInactive, srcUrl);
|
|
1115
1176
|
}
|
|
1116
1177
|
if (t != null) {
|
|
@@ -1127,16 +1188,29 @@ class ValueSetExpander {
|
|
|
1127
1188
|
if (iter) {
|
|
1128
1189
|
let c = await cs.nextContext(iter);
|
|
1129
1190
|
while (c) {
|
|
1130
|
-
this.worker.
|
|
1131
|
-
result += await this.includeCodeAndDescendants(cs, c, expansion, imports, n, excludeInactive, srcUrl);
|
|
1191
|
+
await this.worker.checkAndYield('processCodeAndDescendants#3');
|
|
1192
|
+
result += await this.includeCodeAndDescendants(cs, c, expansion, imports, n, excludeInactive, srcUrl, visited);
|
|
1132
1193
|
c = await cs.nextContext(iter);
|
|
1133
1194
|
}
|
|
1134
1195
|
}
|
|
1135
1196
|
return result;
|
|
1136
1197
|
}
|
|
1137
1198
|
|
|
1138
|
-
async excludeCodeAndDescendants(cs, context, expansion, imports, excludeInactive, srcUrl) {
|
|
1139
|
-
this.worker.
|
|
1199
|
+
async excludeCodeAndDescendants(cs, context, expansion, imports, excludeInactive, srcUrl, visited = null) {
|
|
1200
|
+
await this.worker.checkAndYield('processCodeAndDescendants');
|
|
1201
|
+
|
|
1202
|
+
// Same visited-set protection as includeCodeAndDescendants: walk nodes, not
|
|
1203
|
+
// paths (see that method for why), and same accessor fix - context.code is
|
|
1204
|
+
// not reliably present, and passing the raw context as the code produced
|
|
1205
|
+
// useless "[object Object]" exclusion keys.
|
|
1206
|
+
if (visited == null) {
|
|
1207
|
+
visited = new Set();
|
|
1208
|
+
}
|
|
1209
|
+
const code = await cs.code(context);
|
|
1210
|
+
if (visited.has(code)) {
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
visited.add(code);
|
|
1140
1214
|
|
|
1141
1215
|
if (expansion) {
|
|
1142
1216
|
const vs = this.canonical(await cs.system(), await cs.version());
|
|
@@ -1150,15 +1224,15 @@ class ValueSetExpander {
|
|
|
1150
1224
|
if ((!this.params.excludeNotForUI || !await cs.isAbstract(context)) && (!this.params.activeOnly || !await cs.isInactive(context))) {
|
|
1151
1225
|
const cds = new Designations(this.worker.i18n.languageDefinitions);
|
|
1152
1226
|
await this.listDisplaysFromProvider(cds, cs, context);
|
|
1153
|
-
this.worker.
|
|
1154
|
-
this.excludeCode(cs, await cs.system(), await cs.version(),
|
|
1227
|
+
await this.worker.checkAndYield('processCodeAndDescendants#2');
|
|
1228
|
+
this.excludeCode(cs, await cs.system(), await cs.version(), code, expansion, imports, srcUrl);
|
|
1155
1229
|
}
|
|
1156
1230
|
|
|
1157
1231
|
const iter = await cs.iterator(context);
|
|
1158
1232
|
let c = await cs.nextContext(iter);
|
|
1159
1233
|
while (c) {
|
|
1160
|
-
this.worker.
|
|
1161
|
-
await this.excludeCodeAndDescendants(cs, c, expansion, imports, excludeInactive, srcUrl);
|
|
1234
|
+
await this.worker.checkAndYield('processCodeAndDescendants#3');
|
|
1235
|
+
await this.excludeCodeAndDescendants(cs, c, expansion, imports, excludeInactive, srcUrl, visited);
|
|
1162
1236
|
c = await cs.nextContext(iter);
|
|
1163
1237
|
}
|
|
1164
1238
|
}
|
|
@@ -1169,11 +1243,11 @@ class ValueSetExpander {
|
|
|
1169
1243
|
this.doingVersion = false;
|
|
1170
1244
|
const ts = new Map();
|
|
1171
1245
|
for (const c of source.jsonObj.compose.include || []) {
|
|
1172
|
-
this.worker.
|
|
1246
|
+
await this.worker.checkAndYield('handleCompose#2');
|
|
1173
1247
|
await this.checkSource(c, expansion, filter, source.url, ts, vsInfo, source);
|
|
1174
1248
|
}
|
|
1175
1249
|
for (const c of source.jsonObj.compose.exclude || []) {
|
|
1176
|
-
this.worker.
|
|
1250
|
+
await this.worker.checkAndYield('handleCompose#3');
|
|
1177
1251
|
this.hasExclusions = true;
|
|
1178
1252
|
await this.checkSource(c, expansion, filter, source.url, ts, null, source);
|
|
1179
1253
|
}
|
|
@@ -1187,7 +1261,7 @@ class ValueSetExpander {
|
|
|
1187
1261
|
|
|
1188
1262
|
let i = 0;
|
|
1189
1263
|
for (const c of source.jsonObj.compose.exclude || []) {
|
|
1190
|
-
this.worker.
|
|
1264
|
+
await this.worker.checkAndYield('handleCompose#4');
|
|
1191
1265
|
await this.excludeCodes(c, "ValueSet.compose.exclude[" + i + "]", source, filter, expansion, this.excludeInactives(source), notClosed);
|
|
1192
1266
|
}
|
|
1193
1267
|
|
|
@@ -1200,7 +1274,7 @@ class ValueSetExpander {
|
|
|
1200
1274
|
return 0;
|
|
1201
1275
|
});
|
|
1202
1276
|
for (const c of includes) {
|
|
1203
|
-
this.worker.
|
|
1277
|
+
await this.worker.checkAndYield('handleCompose#5');
|
|
1204
1278
|
await this.includeCodes(c, "ValueSet.compose.include[" + i + "]", source, source.jsonObj.compose, filter, expansion, this.excludeInactives(source), notClosed);
|
|
1205
1279
|
i++;
|
|
1206
1280
|
}
|
|
@@ -1328,7 +1402,7 @@ class ValueSetExpander {
|
|
|
1328
1402
|
}
|
|
1329
1403
|
|
|
1330
1404
|
this.worker.opContext.log('start working');
|
|
1331
|
-
this.worker.
|
|
1405
|
+
await this.worker.checkAndYield('expand');
|
|
1332
1406
|
|
|
1333
1407
|
let notClosed = { value : false};
|
|
1334
1408
|
|
|
@@ -1415,7 +1489,7 @@ class ValueSetExpander {
|
|
|
1415
1489
|
let t = 0;
|
|
1416
1490
|
let o = 0;
|
|
1417
1491
|
for (let i = 0; i < list.length; i++) {
|
|
1418
|
-
this.worker.
|
|
1492
|
+
await this.worker.checkAndYield('expand#1');
|
|
1419
1493
|
const c = list[i];
|
|
1420
1494
|
if (this.map.has(this.keyC(c))) {
|
|
1421
1495
|
o++;
|
package/tx/workers/metadata.js
CHANGED
|
@@ -36,16 +36,37 @@ class MetadataHandler {
|
|
|
36
36
|
|
|
37
37
|
if (mode === 'terminology') {
|
|
38
38
|
this.logInfo = 'termcaps';
|
|
39
|
-
const tc = new TerminologyCapabilities(await this.buildTerminologyCapabilities(endpoint, provider));
|
|
39
|
+
const tc = new TerminologyCapabilities(await this.buildTerminologyCapabilities(endpoint, provider, req));
|
|
40
40
|
return res.json(tc.jsonObj);
|
|
41
41
|
}
|
|
42
42
|
this.logInfo = 'metadata';
|
|
43
43
|
|
|
44
44
|
// Default: return CapabilityStatement
|
|
45
|
-
const cs = new CapabilityStatement(this.buildCapabilityStatement(endpoint, provider));
|
|
45
|
+
const cs = new CapabilityStatement(this.buildCapabilityStatement(endpoint, provider, req));
|
|
46
46
|
return res.json(cs.jsonObj);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the public base URL for this request.
|
|
51
|
+
* Derived from what the client actually asked for (scheme + host) plus the
|
|
52
|
+
* endpoint path, so the server echoes the host the client used rather than a
|
|
53
|
+
* fixed, hardcoded value. Falls back to config/host only when no request is
|
|
54
|
+
* available (e.g. unit tests calling the build methods directly).
|
|
55
|
+
* @param {express.Request} [req] - Express request
|
|
56
|
+
* @param {Object} endpoint - Endpoint info {path, ...}
|
|
57
|
+
* @returns {string} Base URL, e.g. https://tx.fhir.org/r4
|
|
58
|
+
*/
|
|
59
|
+
resolveBaseUrl(req, endpoint) {
|
|
60
|
+
if (!req) {
|
|
61
|
+
return this.config.baseUrl || `https://${this.host}${endpoint.path}`;
|
|
62
|
+
}
|
|
63
|
+
// Requires app.set('trust proxy', ...) and nginx forwarding X-Forwarded-Proto
|
|
64
|
+
// for the scheme to be correct behind the reverse proxy.
|
|
65
|
+
const proto = req.protocol || 'https';
|
|
66
|
+
const host = req.get('host') || this.host;
|
|
67
|
+
return `${proto}://${host}${endpoint.path}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
49
70
|
/**
|
|
50
71
|
* Handle GET /$versions request
|
|
51
72
|
* @param {express.Request} req - Express request (with txEndpoint attached)
|
|
@@ -106,11 +127,11 @@ class MetadataHandler {
|
|
|
106
127
|
* @param {Object} provider - Provider for code systems and resources
|
|
107
128
|
* @returns {Object} CapabilityStatement resource
|
|
108
129
|
*/
|
|
109
|
-
buildCapabilityStatement(endpoint) {
|
|
130
|
+
buildCapabilityStatement(endpoint, provider, req) {
|
|
110
131
|
|
|
111
132
|
const now = new Date().toISOString();
|
|
112
133
|
const fhirVersion = this.mapFhirVersion(endpoint.fhirVersion);
|
|
113
|
-
const baseUrl = this.
|
|
134
|
+
const baseUrl = this.resolveBaseUrl(req, endpoint);
|
|
114
135
|
const serverVersion = this.config.serverVersion;
|
|
115
136
|
|
|
116
137
|
return {
|
|
@@ -281,9 +302,9 @@ class MetadataHandler {
|
|
|
281
302
|
* @param {Object} provider - Provider for code systems and resources
|
|
282
303
|
* @returns {Object} TerminologyCapabilities resource
|
|
283
304
|
*/
|
|
284
|
-
async buildTerminologyCapabilities(endpoint, provider) {
|
|
305
|
+
async buildTerminologyCapabilities(endpoint, provider, req) {
|
|
285
306
|
const now = new Date().toISOString();
|
|
286
|
-
const baseUrl = this.
|
|
307
|
+
const baseUrl = this.resolveBaseUrl(req, endpoint);
|
|
287
308
|
const serverVersion = this.config.serverVersion || '1.0.0';
|
|
288
309
|
|
|
289
310
|
const tc = {
|
package/tx/workers/search.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const { TerminologyWorker } = require('./worker');
|
|
9
9
|
const {Utilities} = require("../../library/utilities");
|
|
10
10
|
const {debugLog} = require("../operation-context");
|
|
11
|
+
const {acceptsHtml} = require("../tx-html");
|
|
11
12
|
|
|
12
13
|
class SearchWorker extends TerminologyWorker {
|
|
13
14
|
/**
|
|
@@ -84,7 +85,12 @@ class SearchWorker extends TerminologyWorker {
|
|
|
84
85
|
break;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
// Default page size depends on the return format: browsers get small pages
|
|
89
|
+
// (20), while API clients (json/xml) get big ones (1000) - they just want
|
|
90
|
+
// the data, and small pages make crawlers (e.g. the tx registry) painfully
|
|
91
|
+
// slow. An explicit _count always wins, subject to the caps.
|
|
92
|
+
const defaultCount = acceptsHtml(req) ? 20 : 1000;
|
|
93
|
+
const count = summary === 'count' ? 0 : Math.min(elements ? 2000 : 200, params._count && Utilities.isInteger(params._count) ? parseInt(params._count) : defaultCount);
|
|
88
94
|
const sort = params._sort || "id";
|
|
89
95
|
|
|
90
96
|
// Get matching resources
|