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.
Files changed (56) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/changelog-template.md +22 -0
  3. package/library/logger.js +8 -1
  4. package/library/package-manager.js +40 -2
  5. package/npmprojector/watcher.js +1 -1
  6. package/package.json +3 -2
  7. package/packages/packages.js +28 -15
  8. package/publisher/publisher.js +8 -8
  9. package/registry/api.js +92 -31
  10. package/registry/crawler.js +15 -1
  11. package/registry/model.js +158 -8
  12. package/registry/readme.md +5 -13
  13. package/registry/registry.js +87 -46
  14. package/registry/test-runner.js +1 -1
  15. package/server.js +8 -1
  16. package/stats.js +5 -1
  17. package/token/token.js +1 -1
  18. package/translations/Messages.properties +3 -1
  19. package/translations/rendering-phrases.properties +1227 -1218
  20. package/translations/rendering-phrases_ar.properties +1097 -1091
  21. package/translations/rendering-phrases_fr.properties +626 -624
  22. package/translations/rendering-phrases_nl.properties +976 -970
  23. package/translations/rendering-phrases_pt.properties +1026 -1020
  24. package/translations/rendering-phrases_ru.properties +1100 -1094
  25. package/tsconfig.eslint.json +9 -0
  26. package/tx/cs/cs-api.js +57 -4
  27. package/tx/cs/cs-cs.js +98 -25
  28. package/tx/cs/cs-hgvs.js +4 -0
  29. package/tx/cs/cs-lang.js +8 -0
  30. package/tx/cs/cs-mimetypes.js +4 -0
  31. package/tx/cs/cs-snomed.js +530 -75
  32. package/tx/cs/cs-uri.js +4 -0
  33. package/tx/data/snomed-subset.txt +40 -3
  34. package/tx/html/home-metrics.liquid +16 -1
  35. package/tx/importers/import-sct.module.js +18 -0
  36. package/tx/library/designations.js +3 -5
  37. package/tx/library/renderer.js +227 -30
  38. package/tx/library.js +5 -1
  39. package/tx/operation-context.js +131 -5
  40. package/tx/provider.js +38 -9
  41. package/tx/sct/ecl.js +30 -11
  42. package/tx/sct/expressions.js +28 -1
  43. package/tx/sct/structures.js +5 -5
  44. package/tx/tx-html.js +24 -15
  45. package/tx/tx.fhir.org.yml +1 -4
  46. package/tx/tx.js +18 -2
  47. package/tx/vs/vs-package.js +52 -0
  48. package/tx/workers/batch-validate.js +125 -18
  49. package/tx/workers/cache-control.js +31 -2
  50. package/tx/workers/expand.js +150 -76
  51. package/tx/workers/metadata.js +27 -6
  52. package/tx/workers/search.js +7 -1
  53. package/tx/workers/validate.js +30 -18
  54. package/tx/workers/worker.js +39 -7
  55. package/tx/xversion/xv-valueset.js +5 -2
  56. package/xig/xig.js +3 -3
@@ -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
- if (cs != null && cs.expandLimitation > 0) {
318
- let cnt = this.csCounter.get(cs.system);
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 > cs.expandLimitation) {
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.deadCheck('importValueSet');
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.deadCheck('importValueSetItem');
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.deadCheck('importValueSetItem');
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.deadCheck('checkSource');
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.deadCheck('checkSource');
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
- if (cs.isNotClosed()) {
665
- if (cs.specialEnumeration()) {
666
- Extensions.addBoolean(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
667
- Extensions.addString(exp, "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());
668
- } else {
669
- throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics());
670
- }
671
- }
672
- if (!imp && this.limitCount > 0 && cs.totalCount > this.limitCount) {
673
- throw new Issue("error", "too-costly", null, 'VALUESET_TOO_COSTLY', this.worker.i18n.translate('VALUESET_TOO_COSTLY', this.params.httpLanguages, [srcURL, '>' + this.limitCount]), null, 422).withDiagnostics(this.worker.opContext.diagnostics());
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.deadCheck('processCodes#1');
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.deadCheck('processCodes#2');
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.deadCheck('processCodes#2');
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", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
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", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
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
- throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics());
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
- const iter = await cs.iterator(null);
765
- if (valueSets.length === 0 && this.limitCount > 0 && (iter && iter.total > this.limitCount) && this.offset < 0) {
766
- 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());
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
- c = await cs.nextContext(iter);
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.deadCheck('processCodes#4');
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.deadCheck('processCodes#3');
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", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
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.deadCheck('processCodes#4a');
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.deadCheck('processCodes#5');
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.deadCheck('processCodes#1');
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.deadCheck('processCodes#2');
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.deadCheck('processCodes#3');
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.deadCheck('processCodes#3aa');
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", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
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
- throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics());
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.deadCheck('processCodes#3a');
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.deadCheck('processCodes#3');
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", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
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.deadCheck('processCodes#4a');
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.deadCheck('processCodes#5');
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.deadCheck('processCodeAndDescendants');
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(), context.code, await cs.isAbstract(context), await cs.isInactive(context), null, null, null,
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(), context.code, await cs.isAbstract(context), await cs.isInactive(context), await cs.isDeprecated(context), await cs.getStatus(context), cds, await cs.definition(context),
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.deadCheck('processCodeAndDescendants#3');
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.deadCheck('processCodeAndDescendants');
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.deadCheck('processCodeAndDescendants#2');
1154
- this.excludeCode(cs, await cs.system(), await cs.version(), context, expansion, imports, srcUrl);
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.deadCheck('processCodeAndDescendants#3');
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.deadCheck('handleCompose#2');
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.deadCheck('handleCompose#3');
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.deadCheck('handleCompose#4');
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.deadCheck('handleCompose#5');
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.deadCheck('expand');
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.deadCheck('expand#1');
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++;
@@ -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.config.baseUrl || `https://${this.host}${endpoint.path}`;
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.config.baseUrl || `https://${this.host}${endpoint.path}`;
307
+ const baseUrl = this.resolveBaseUrl(req, endpoint);
287
308
  const serverVersion = this.config.serverVersion || '1.0.0';
288
309
 
289
310
  const tc = {
@@ -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
- const count = summary === 'count' ? 0 : Math.min(elements ? 2000 : 200, params._count && Utilities.isInteger(params._count) ? parseInt(params._count) : 20);
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