fhirsmith 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ //
2
+ // THO precedence for value sets and concept maps (see Provider.applyTHOPrecedence).
3
+ //
4
+ // Where hl7.terminology provides a resource with the same url as one in the FHIR core
5
+ // package, THO's copy is the one used - versions were reset when content moved to THO, so
6
+ // version order can't decide. But the core copy is still what a reference that names its
7
+ // version (url|5.0.0) means, so it stays available to that reference, and only to it.
8
+ //
9
+
10
+ const { VersionUtilities } = require('../../library/version-utilities');
11
+
12
+ /**
13
+ * Whether a package provider should keep a map key for a resource that THO also provides.
14
+ * Only the keys that name a version are kept - url|version, and url|major.minor for a
15
+ * semver version - and only where THO doesn't provide that version itself. The bare url
16
+ * (the default for a reference with no version) and the resource's id are not kept.
17
+ *
18
+ * @param {string} key - the map key
19
+ * @param {{url: string, version: string}} resource - the core package's resource
20
+ * @param {Set<string>} thoVersions - the versions of the url that hl7.terminology provides
21
+ * @returns {boolean}
22
+ */
23
+ function keepForPinnedReference(key, resource, thoVersions) {
24
+ const version = resource.version;
25
+ if (!version) {
26
+ return false;
27
+ }
28
+ if (key === resource.url + '|' + version) {
29
+ return !thoVersions.has(version);
30
+ }
31
+ if (VersionUtilities.isSemVer(version)) {
32
+ const mm = VersionUtilities.getMajMin(version);
33
+ if (mm && key === resource.url + '|' + mm) {
34
+ return ![...thoVersions].some(v => v && VersionUtilities.isSemVer(v) && VersionUtilities.getMajMin(v) === mm);
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+
40
+ module.exports = { keepForPinnedReference };
package/tx/provider.js CHANGED
@@ -632,27 +632,20 @@ class Provider {
632
632
  * moved from the FHIR core packages to terminology.hl7.org has been QA'd there since, and
633
633
  * its versions went backwards (coverage-selfpay is 4.0.1 in hl7.fhir.r4.core but 1.0.1 in
634
634
  * hl7.terminology), so version precedence can't decide between them. Instead, a code system
635
- * from an hl7.terminology package replaces every code system with the same url from a FHIR
636
- * core package - none of the core versions stay available.
635
+ * from an hl7.terminology package is always the default for its url - what a reference with
636
+ * no version gets - whatever versions the FHIR core packages have.
637
637
  *
638
- * Except: hl7.fhir.r4.core has the v2 tables 0006, 0360 and 0391 in two versions each,
639
- * with the version in the url (http://terminology.hl7.org/CodeSystem/v2-0006|2.1 - see
640
- * urlHadVersion in loadNpm). hl7.terminology doesn't publish those versions, so they stay
641
- * available by url|version; THO's code system is the default for the url.
638
+ * The core versions stay available by url|version, where THO doesn't provide that version
639
+ * itself: a reference that names its version (url|5.0.0) means the core one, and there's
640
+ * nothing else it can mean. They are never the default while THO provides the url (see also
641
+ * deleteCodeSystem).
642
642
  */
643
643
  addCodeSystem(cs) {
644
644
  if (Provider.#isTHOPackage(cs.sourcePackage)) {
645
- for (const [key, t] of [...this.codeSystems]) {
646
- if (t.url === cs.url && VersionUtilities.isCorePackage(t.sourcePackage)
647
- && (key === t.url || !t.urlHadVersion)) {
648
- this.codeSystems.delete(key);
649
- }
650
- }
651
- } else if (VersionUtilities.isCorePackage(cs.sourcePackage) && !cs.urlHadVersion) {
652
- // THO's code system is always the default for its url
645
+ // THO's code system takes over the default from a core one; the core versions stay
653
646
  const t = this.codeSystems.get(cs.url);
654
- if (t && Provider.#isTHOPackage(t.sourcePackage)) {
655
- return;
647
+ if (t && t.url === cs.url && VersionUtilities.isCorePackage(t.sourcePackage)) {
648
+ this.codeSystems.delete(cs.url);
656
649
  }
657
650
  }
658
651
  const existing = this.codeSystems.get(cs.url);
@@ -677,45 +670,63 @@ class Provider {
677
670
  * THO precedence for value sets and concept maps, as for code systems (see addCodeSystem):
678
671
  * once the library's providers have been added after this endpoint's core package, the core
679
672
  * package's value sets and concept maps whose urls an hl7.terminology package also provides
680
- * are left out. (No core value set or concept map has the version in its url.)
673
+ * are only available to a reference that names their version (and THO doesn't provide that
674
+ * version itself). They are never what a reference with no version gets, and they aren't
675
+ * read by id, found by search, or used for translation unless asked for.
681
676
  */
682
677
  applyTHOPrecedence() {
683
- const thoValueSets = new Set();
678
+ const thoValueSets = new Map();
684
679
  for (const vp of this.valueSetProviders) {
685
680
  if (vp instanceof PackageValueSetProvider && Provider.#isTHOPackage(vp.sourcePackage())) {
686
681
  for (const vs of vp.valueSetMap.values()) {
687
- thoValueSets.add(vs.url);
682
+ Provider.#addVersion(thoValueSets, vs);
688
683
  }
689
684
  }
690
685
  }
691
- const thoConceptMaps = new Set();
686
+ const thoConceptMaps = new Map();
692
687
  for (const cmp of this.conceptMapProviders) {
693
688
  if (cmp instanceof PackageConceptMapProvider && Provider.#isTHOPackage(cmp.sourcePackage())) {
694
689
  for (const cm of cmp.conceptMapMap.values()) {
695
- thoConceptMaps.add(cm.url);
690
+ Provider.#addVersion(thoConceptMaps, cm);
696
691
  }
697
692
  }
698
693
  }
699
694
  for (const vp of this.valueSetProviders) {
700
695
  if (vp instanceof PackageValueSetProvider && VersionUtilities.isCorePackage(vp.sourcePackage())) {
701
- vp.excludeUrls(thoValueSets);
696
+ vp.keepPinnedOnly(thoValueSets);
702
697
  }
703
698
  }
704
699
  for (const cmp of this.conceptMapProviders) {
705
700
  if (cmp instanceof PackageConceptMapProvider && VersionUtilities.isCorePackage(cmp.sourcePackage())) {
706
- cmp.excludeUrls(thoConceptMaps);
701
+ cmp.keepPinnedOnly(thoConceptMaps);
707
702
  }
708
703
  }
709
704
  }
710
705
 
706
+ static #addVersion(map, resource) {
707
+ if (!resource || !resource.url) {
708
+ return;
709
+ }
710
+ if (!map.has(resource.url)) {
711
+ map.set(resource.url, new Set());
712
+ }
713
+ if (resource.version) {
714
+ map.get(resource.url).add(resource.version);
715
+ }
716
+ }
717
+
711
718
  deleteCodeSystem(cs) {
712
719
  this.codeSystems.delete(cs.vurl);
713
720
  this.codeSystems.delete(cs.url);
714
721
  // If other versions of the SAME url survive, re-point the unversioned [url]
715
722
  // entry at the most-recent surviving version. Otherwise leave it deleted.
723
+ // THO precedence (see addCodeSystem): while a THO version survives, a core one
724
+ // can't become the default, however recent its (reset) version looks
725
+ const survivors = [...new Set(this.codeSystems.values())].filter(t => t.url === cs.url);
726
+ const tho = survivors.filter(t => Provider.#isTHOPackage(t.sourcePackage));
716
727
  let existing = null;
717
- for (let t of this.codeSystems.values()) {
718
- if (t.url === cs.url && (!existing || t.isMoreRecent(existing))) {
728
+ for (let t of (tho.length > 0 ? tho : survivors)) {
729
+ if (!existing || t.isMoreRecent(existing)) {
719
730
  existing = t;
720
731
  }
721
732
  }
@@ -35,14 +35,20 @@ that the server does not use. `filterECL` is the one that matters.
35
35
  reference set's own fields (concept, integer and string typed from the RF2
36
36
  file name). Since inactive member rows are dropped at import, `active = false`
37
37
  never matches.
38
- - `AND` / `OR` / `MINUS` (and `,`), with the ECL 6.4 rule that conjunction and
38
+ - Refinements: `=` and `!=` against an expression value (`!=` counts the
39
+ occurrences whose value is outside the set, so `A != X` needs at least one
40
+ A that is not X, and `[0..0] A != X` means every A is X); reversed
41
+ attributes (`R A = X`: the concept is the value of an A on something in X,
42
+ evaluated over the concept's inbound relationships); attribute names that
43
+ are a concept, `*`, or any sub expression (`<< 762705008 = ...`); attribute
44
+ sets joined by `AND`, `OR` or `,`, with brackets; attribute groups,
45
+ including `AND`/`OR` inside and between groups; attribute and group
46
+ cardinality, including `[0..0]`. Cardinality counts occurrences per role
47
+ group, per the SNOMED Computable Languages Group ruling (see
48
+ `_countAttributeMatches`).
49
+ - `AND` / `OR` / `MINUS`, and `,` as a conjunction at the expression level as
50
+ well as in refinements, with the ECL 6.4 rule that conjunction and
39
51
  disjunction cannot be mixed without brackets.
40
- - Refinements: `=` against an expression value; attribute names that are a
41
- concept, `*`, or any sub expression (`<< 762705008 = ...`); attribute sets
42
- joined by `AND`, `OR` or `,`, with brackets; attribute groups, including
43
- `AND`/`OR` inside and between groups; attribute and group cardinality,
44
- including `[0..0]`. Cardinality counts occurrences per role group, per the
45
- SNOMED Computable Languages Group ruling (see `_countAttributeMatches`).
46
52
  - Dotted expressions (`< X . 363698007`, `. << 363698007`, `. (A OR B)`).
47
53
  - `{{ C ... }}` concept filters on `active`, `definitionStatus`,
48
54
  `definitionStatusId`, `moduleId` and `effectiveTime`.
@@ -56,26 +62,37 @@ that the server does not use. `filterECL` is the one that matters.
56
62
  must be satisfied by a single description; separate blocks may be satisfied
57
63
  by different descriptions. Only active descriptions are considered unless
58
64
  the block has an `active` filter.
65
+ - Concrete values in refinements - numeric (`= #5`, `>= #1.5`, `!= #-1`),
66
+ string (`= "x"`, `= match:"x"`, a set) and boolean (`= true`) - against the
67
+ concrete value relationships, which the importer reads from
68
+ `sct2_RelationshipConcreteValues_*` into cache version 19. Numbers compare
69
+ as numbers (`#5` matches `#5.0`); strings use the same match / wild
70
+ semantics as description terms; a value of another kind never matches.
71
+ An edition imported before version 19 has no concrete values and reports
72
+ such a refinement as unsupported rather than answering with nothing.
73
+ - History supplements: `{{ + HISTORY }}` (= MAX), `HISTORY-MIN` (SAME AS),
74
+ `HISTORY-MOD` (SAME AS, WAS A, REPLACED BY, PARTIALLY EQUIVALENT TO),
75
+ `HISTORY-MAX` (every reference set under 900000000000522004 |Historical
76
+ association|), `HISTORY (*)` and `HISTORY (expression)`: the inactive
77
+ concepts that the selected association reference sets associate with a
78
+ concept in the result are added to it. A reference set the edition does not
79
+ carry contributes nothing. Whether a bare `HISTORY` means MAX is taken from
80
+ Snow Owl's reading of the specification - to be confirmed.
81
+ - Alternate identifiers (`LOINC#14394-1`, `"LOINC#14394-1"`): the scheme
82
+ alias names an identifier scheme (`IDENTIFIER_SCHEMES` in `cs-snomed.js`:
83
+ LOINC = 30051010000102, from the LOINC Extension's `sct2_Identifier` file,
84
+ cache version 18+), the code resolves to the concept(s) carrying it, and
85
+ hierarchy operators apply to them as to any set. An unknown scheme or a
86
+ code nobody carries is `INVALID_ECL`.
59
87
  - Comments (`/* ... */`), terms (`|...|`, not checked against descriptions).
60
88
 
61
89
  ### Parsed but reported as unsupported
62
90
 
63
- - Reverse attributes (`R 363698007 = ...`).
64
- - `!=` against an expression value in a refinement.
65
- - Concrete values in refinements - numeric (`= #5`, `>= #1.5`), string
66
- (`= "x"`, `= match:"x"`), boolean (`= true`). The importer does not read
67
- `sct2_RelationshipConcreteValues_*`, so no concrete values are in the cache
68
- to compare against. Supporting them means extending the importer and the
69
- relationship record, and rebuilding every cache.
70
- - Alternate identifiers (`"ICD10#A01.1"`, `SCTID#123`). No alternate
71
- identifier reference sets are loaded.
72
91
  - Reference set field selection on member-of (`^ [targetComponentId] refset`,
73
92
  `^ [referencedComponentId, targetComponentId] refset`). Deliberate: the
74
93
  selected fields, and the referenced components of many reference sets,
75
94
  are descriptions or other non-concept components, which an expansion has
76
95
  no way to represent.
77
- - History supplements (`{{ + HISTORY-MIN / MOD / MAX }}`). The association
78
- reference sets they draw on are loaded, so this is implementable.
79
96
 
80
97
  ### Not parsed
81
98
 
@@ -99,7 +116,13 @@ that the server does not use. `filterECL` is the one that matters.
99
116
  - `^` on a concept that is not a reference set yields an empty result rather
100
117
  than an error (Snowstorm's behaviour; agreed in the tx-ecosystem suite).
101
118
  `^` on a reference set whose members are descriptions (a language reference
102
- set) is reported as unsupported - there are no concepts to return.
119
+ set) is reported as unsupported when that reference set is named explicitly
120
+ - there are no concepts to return. Inside a computed operand (`^ *`,
121
+ `^ (< 900000000000455006)`) such a reference set contributes nothing and
122
+ the rest of the operand still counts.
123
+ - An attribute name without a comparison (`< X : 363698007`, or `(< X : A = v
124
+ OR B)` where the OR binds inside the refinement) is a syntax error, as the
125
+ grammar requires.
103
126
  - The ECL result for a value set filter is cached per (value set, code system
104
127
  version), so the first expansion of a large constraint is slow and later ones
105
128
  are not.
@@ -181,8 +204,9 @@ role group pairing.
181
204
 
182
205
  - Stated relationships and the OWL axiom reference sets. Everything is driven
183
206
  by the distributed inferred relationships; there is no classification.
184
- - Concrete value relationships (see ECL above).
185
- - Alternate identifier reference sets.
207
+ - Concrete value relationships are read into cache version 19 (October 2026);
208
+ caches built before that have none, and a concrete-value ECL refinement on
209
+ them is reported as unsupported.
186
210
  - Inactive reference set member rows (only active rows are stored).
187
211
  - Module ids and definition status on concepts are recorded by the current
188
212
  importer; caches built before mid-2026 have neither (every concept looks
package/tx/sct/ecl.js CHANGED
@@ -538,8 +538,11 @@ class ECLParser {
538
538
  // this compound sequence and reject a mix (e.g. "<< A OR << B AND << C").
539
539
  let compoundOp = null;
540
540
 
541
- while (this.match(ECLTokenType.AND, ECLTokenType.OR, ECLTokenType.MINUS)) {
542
- const operator = this.current;
541
+ // "," is a conjunction in the ECL grammar (conjunction = "AND" / ","), at
542
+ // this level as well as in refinements; a comma that reaches here was not
543
+ // claimed by a refinement.
544
+ while (this.match(ECLTokenType.AND, ECLTokenType.OR, ECLTokenType.MINUS, ECLTokenType.COMMA)) {
545
+ const operator = this.match(ECLTokenType.COMMA) ? { ...this.current, type: ECLTokenType.AND } : this.current;
543
546
  if (compoundOp !== null && operator.type !== compoundOp) {
544
547
  throw new Error(
545
548
  'Ambiguous ECL expression: conjunction, disjunction and exclusion ' +
@@ -726,18 +729,30 @@ class ECLParser {
726
729
  return { type: ECLNodeType.FILTER_CONSTRAINT, kind, filters };
727
730
  }
728
731
 
729
- if (kind !== 'M') {
730
- // Skip to the closing }} keeping the raw text for diagnostics
731
- const raw = [];
732
- while (!this.match(ECLTokenType.RFILTER)) {
733
- if (this.match(ECLTokenType.EOF)) {
734
- this.error('Unterminated filter constraint - missing }}');
732
+ if (kind === '+') {
733
+ // historySupplement = "{{" "+" "HISTORY" [ "-MIN" / "-MOD" / "-MAX" / "(" (expressionConstraint / "*") ")" ] "}}"
734
+ if (!this.match(ECLTokenType.SCTID) || !/^HISTORY(-MIN|-MOD|-MAX)?$/i.test(this.current.value)) {
735
+ this.error('Expected HISTORY, HISTORY-MIN, HISTORY-MOD or HISTORY-MAX after {{ +');
736
+ }
737
+ const word = this.current.value.toUpperCase();
738
+ this.advance();
739
+ const node = { type: ECLNodeType.FILTER_CONSTRAINT, kind, profile: word === 'HISTORY' ? 'MAX' : word.substring(8) };
740
+ if (this.match(ECLTokenType.LPAREN)) {
741
+ if (node.profile !== 'MAX' || word !== 'HISTORY') {
742
+ this.error('A history profile (HISTORY-MIN/MOD/MAX) cannot also name reference sets');
735
743
  }
736
- raw.push(this.current.value);
737
744
  this.advance();
745
+ if (this.match(ECLTokenType.WILDCARD)) {
746
+ this.advance();
747
+ node.profile = 'MAX';
748
+ } else {
749
+ node.profile = 'EXPLICIT';
750
+ node.refsets = this.parseExpressionConstraint();
751
+ }
752
+ this.expect(ECLTokenType.RPAREN);
738
753
  }
739
- this.advance(); // }}
740
- return { type: ECLNodeType.FILTER_CONSTRAINT, kind, raw: raw.join(' ') };
754
+ this.expect(ECLTokenType.RFILTER);
755
+ return node;
741
756
  }
742
757
 
743
758
  const filters = [this.parseMemberFilter()];
@@ -1430,6 +1445,14 @@ class ECLParser {
1430
1445
  };
1431
1446
  }
1432
1447
 
1448
+ if (!comparison) {
1449
+ // eclAttribute = [cardinality] [reverseFlag] eclAttributeName comparison - a
1450
+ // bare name is a syntax error. This is what "(A : B = C OR D)" trips on:
1451
+ // the OR binds inside the refinement, so D is an attribute name without a
1452
+ // comparison.
1453
+ this.error('Expected a comparison operator (=, !=, <, <=, >, >=) after the attribute name');
1454
+ }
1455
+
1433
1456
  return {
1434
1457
  type: ECLNodeType.ATTRIBUTE,
1435
1458
  cardinality,
@@ -1358,7 +1358,7 @@ class SnomedFileReader {
1358
1358
  }
1359
1359
 
1360
1360
  // Determine settings based on cache version string (matching Pascal logic)
1361
- if (result.cacheVersion === '17' || result.cacheVersion === '18') { // SNOMED_CACHE_VERSION_CURRENT (18 = 17 + alternate identifiers)
1361
+ if (result.cacheVersion === '17' || result.cacheVersion === '18' || result.cacheVersion === '19') { // 18 = 17 + alternate identifiers; 19 = 18 + concrete values
1362
1362
  result.hasLangs = true;
1363
1363
  } else if (result.cacheVersion === '16') { // Assuming this is UTF8 version for now
1364
1364
  result.isUTF16 = false;
@@ -1384,9 +1384,16 @@ class SnomedFileReader {
1384
1384
 
1385
1385
  // Alternate identifiers (version 18): [[scheme, module, code, conceptId], ...]
1386
1386
  result.identifiers = [];
1387
- if (result.cacheVersion === '18') {
1387
+ if (result.cacheVersion === '18' || result.cacheVersion === '19') {
1388
1388
  result.identifiers = JSON.parse(this.readBytes().toString('utf8'));
1389
1389
  }
1390
+ // Concrete value relationships (version 19): [[sourceId, typeId, group, value], ...].
1391
+ // null (not []) when the cache predates them, so the server can tell "none" from
1392
+ // "not loaded".
1393
+ result.concreteValues = null;
1394
+ if (result.cacheVersion === '19') {
1395
+ result.concreteValues = JSON.parse(this.readBytes().toString('utf8'));
1396
+ }
1390
1397
 
1391
1398
  return result;
1392
1399
  }
@@ -32,7 +32,7 @@ sources:
32
32
  - snomed:sct_us_20230301.cache
33
33
  - snomed:sct_us_20240301.cache
34
34
  - snomed:sct_us_20250901.cache
35
- - snomed:sct_test_20250909d.cache
35
+ - snomed:sct_test_20250909e.cache
36
36
  - cpt:CodeSystem-cpt.db|cpt-2023-fragment-0.1.db
37
37
  - omop:omop_v20250227.db
38
38
  - npm:hl7.terminology=tho
@@ -8,6 +8,7 @@ const { checkContained } = require('../library/canonical-resource');
8
8
  const { VersionUtilities } = require('../../library/version-utilities');
9
9
  const {validateParameter} = require("../../library/utilities");
10
10
  const { applyIdPrefix, stripIdPrefix } = require('../library/resource-ids');
11
+ const { keepForPinnedReference } = require('../library/tho-precedence');
11
12
 
12
13
  /**
13
14
  * Package-based ValueSet provider using shared database layer
@@ -267,7 +268,7 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
267
268
 
268
269
  // Only process each ValueSet once (use URL to deduplicate)
269
270
  const vsUrl = json.url;
270
- if (seen.has(vsUrl)) {
271
+ if (seen.has(vsUrl) || this.#excludedUrls.has(vsUrl)) {
271
272
  continue;
272
273
  }
273
274
 
@@ -460,35 +461,38 @@ class PackageValueSetProvider extends AbstractValueSetProvider {
460
461
  return this.#excludedUrls.size === 0 ? urls : urls.filter(url => !this.#excludedUrls.has(url));
461
462
  }
462
463
 
463
- // the value sets left out by excludeUrls - by url, and by their own (unprefixed) id
464
+ // the value sets left out by keepPinnedOnly - by url, and by their own (unprefixed) id
464
465
  #excludedUrls = new Set();
465
466
  #excludedIds = new Set();
466
467
 
467
468
  /**
468
- * Leave out the value sets with any of these urls: they can't be fetched, and they aren't
469
- * found by search. The FHIR core package's value sets are left out where hl7.terminology
470
- * provides the same url (see Provider.applyTHOPrecedence). Must be called once the
471
- * provider is initialized - the database is shared, so it isn't changed.
469
+ * The value sets with these urls are hl7.terminology's to provide (see
470
+ * Provider.applyTHOPrecedence): from this (core) package they are only available to a
471
+ * reference that names their version, where THO doesn't provide that version itself. They
472
+ * aren't the default for their url, can't be read by id, aren't found by search or listed.
473
+ * Must be called once the provider is initialized - the database is shared, so it isn't
474
+ * changed.
472
475
  *
473
- * @param {Set<string>} urls
476
+ * @param {Map<string, Set<string>>} thoVersions - url -> the versions hl7.terminology provides
474
477
  */
475
- excludeUrls(urls) {
476
- const excluded = new Set();
478
+ keepPinnedOnly(thoVersions) {
479
+ const affected = new Set();
477
480
  for (const vs of this.valueSetMap.values()) {
478
- if (vs && urls.has(vs.url)) {
479
- excluded.add(vs);
481
+ if (vs && thoVersions.has(vs.url)) {
482
+ affected.add(vs);
480
483
  }
481
484
  }
482
485
  for (const [key, vs] of [...this.valueSetMap]) {
483
- if (excluded.has(vs)) {
486
+ if (affected.has(vs) && !keepForPinnedReference(key, vs, thoVersions.get(vs.url))) {
484
487
  this.valueSetMap.delete(key);
485
488
  }
486
489
  }
487
- for (const vs of excluded) {
490
+ for (const vs of affected) {
488
491
  this.#excludedUrls.add(vs.url);
489
492
  this.#excludedIds.add(stripIdPrefix(vs.id, this.spaceId) ?? vs.id);
490
493
  }
491
494
  }
495
+
492
496
  }
493
497
 
494
498
  module.exports = {
package/tx/vs/vs-vsac.js CHANGED
@@ -42,7 +42,7 @@ class VSACValueSetProvider extends AbstractValueSetProvider {
42
42
  this.apiKey = config.apiKey;
43
43
  // Optional operator password for the on-demand resync form (see info()).
44
44
  this.resyncPassword = config.resyncPassword || null;
45
- this.cacheFolder = folders.ensureFilePath("terminology-cache/vsac");
45
+ this.cacheFolder = folders.ensureFolder("terminology-cache", "vsac");
46
46
  // https: every request carries the API key. (VSAC redirects http to https, but only after
47
47
  // the key has been sent in the clear)
48
48
  this.baseUrl = (config.baseUrl || 'https://cts.nlm.nih.gov/fhir').replace(/\/+$/, '');