fhirsmith 0.8.5 → 0.9.0

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 (41) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +52 -22
  3. package/extension-tracker/extension-tracker-template.html +3 -1
  4. package/library/html-server.js +7 -0
  5. package/library/logger.js +234 -194
  6. package/library/regex-utilities.js +13 -0
  7. package/package.json +4 -2
  8. package/packages/packages-template.html +3 -1
  9. package/publisher/publisher-template.html +1 -0
  10. package/publisher/publisher.js +28 -7
  11. package/registry/registry-template.html +3 -1
  12. package/root-bare-template.html +9759 -37
  13. package/root-template.html +3 -2
  14. package/server.js +48 -12
  15. package/translations/Messages.properties +2 -1
  16. package/translations/rendering-phrases.properties +3 -1
  17. package/tx/cs/cs-api.js +4 -0
  18. package/tx/cs/cs-country.js +2 -1
  19. package/tx/cs/cs-cs.js +9 -4
  20. package/tx/cs/cs-loinc.js +2 -1
  21. package/tx/cs/cs-snomed.js +5 -1
  22. package/tx/data/OperationDefinition-ValueSet-related.json +133 -0
  23. package/tx/html/tx-template.html +3 -2
  24. package/tx/importers/atc-to-fhir.js +27 -27
  25. package/tx/library/codesystem.js +4 -0
  26. package/tx/library/renderer.js +20 -4
  27. package/tx/library/ucum-parsers.js +2 -1
  28. package/tx/ocl/cs-ocl.cjs +48 -15
  29. package/tx/ocl/vs-ocl.cjs +57 -34
  30. package/tx/operation-context.js +74 -19
  31. package/tx/tx-html.js +5 -5
  32. package/tx/tx.fhir.org.yml +4 -4
  33. package/tx/tx.js +1 -0
  34. package/tx/vs/vs-database.js +150 -100
  35. package/tx/vs/vs-vsac.js +90 -31
  36. package/tx/workers/expand.js +154 -113
  37. package/tx/workers/metadata.js +6 -3
  38. package/tx/workers/read.js +6 -3
  39. package/tx/workers/related.js +228 -87
  40. package/xig/xig-template.html +3 -1
  41. package/library/logger-telnet.js +0 -205
package/tx/ocl/cs-ocl.cjs CHANGED
@@ -13,6 +13,7 @@ const { OCLBackgroundJobQueue } = require('./jobs/background-queue');
13
13
  const { OCLConceptFilterContext } = require('./model/concept-filter-context');
14
14
  const { toConceptContext } = require('./mappers/concept-mapper');
15
15
  const { patchSearchWorkerForOCLCodeFiltering } = require('./shared/patches');
16
+ const regexUtilities = require("../../library/regex-utilities");
16
17
 
17
18
  patchSearchWorkerForOCLCodeFiltering();
18
19
 
@@ -735,6 +736,17 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
735
736
  return { context: this.conceptCache.get(code), message: null };
736
737
  }
737
738
 
739
+ // OCL concept IDs may differ in case from the FHIR code (e.g. "y" vs "Y").
740
+ // Try a case-insensitive cache lookup before hitting the network.
741
+ const codeLower = code.toLowerCase();
742
+ for (const [key, value] of this.conceptCache.entries()) {
743
+ if (key.toLowerCase() === codeLower) {
744
+ // Cache under the requested case as well so future lookups are O(1).
745
+ this.conceptCache.set(code, value);
746
+ return { context: value, message: null };
747
+ }
748
+ }
749
+
738
750
  if (this.scheduleBackgroundLoad) {
739
751
  this.scheduleBackgroundLoad('lookup-miss');
740
752
  }
@@ -1032,23 +1044,26 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
1032
1044
  this.scheduleBackgroundLoad('concept-miss');
1033
1045
  }
1034
1046
 
1035
- const url = this.#buildConceptUrl(code);
1036
1047
  const pending = (async () => {
1037
- let response;
1038
- try {
1039
- response = await this.httpClient.get(url, { params: { verbose: true } });
1040
- } catch (error) {
1041
- // Missing concept should be treated as not-found, not as an internal server failure.
1042
- if (error && error.response && error.response.status === 404) {
1043
- return null;
1044
- }
1045
- throw error;
1048
+ const concept = await this.#fetchConceptByCode(code);
1049
+ if (concept) {
1050
+ return concept;
1046
1051
  }
1047
- const concept = this.#toConceptContext(response.data);
1048
- if (concept && concept.code) {
1049
- this.conceptCache.set(concept.code, concept);
1052
+ // OCL concept IDs may differ in case from the FHIR code (e.g. "y" vs "Y").
1053
+ // Try common case alternatives before giving up.
1054
+ const lower = code.toLowerCase();
1055
+ const upper = code.toUpperCase();
1056
+ for (const alt of [lower, upper]) {
1057
+ if (alt !== code) {
1058
+ const altConcept = await this.#fetchConceptByCode(alt);
1059
+ if (altConcept) {
1060
+ // Cache under the originally requested code so future lookups hit directly.
1061
+ this.conceptCache.set(code, altConcept);
1062
+ return altConcept;
1063
+ }
1064
+ }
1050
1065
  }
1051
- return concept;
1066
+ return null;
1052
1067
  })();
1053
1068
 
1054
1069
  this.pendingConceptRequests.set(pendingKey, pending);
@@ -1059,6 +1074,24 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
1059
1074
  }
1060
1075
  }
1061
1076
 
1077
+ async #fetchConceptByCode(code) {
1078
+ const url = this.#buildConceptUrl(code);
1079
+ let response;
1080
+ try {
1081
+ response = await this.httpClient.get(url, { params: { verbose: true } });
1082
+ } catch (error) {
1083
+ if (error && error.response && error.response.status === 404) {
1084
+ return null;
1085
+ }
1086
+ throw error;
1087
+ }
1088
+ const concept = this.#toConceptContext(response.data);
1089
+ if (concept && concept.code) {
1090
+ this.conceptCache.set(concept.code, concept);
1091
+ }
1092
+ return concept;
1093
+ }
1094
+
1062
1095
  async #allConceptContexts() {
1063
1096
  const concepts = new Map();
1064
1097
 
@@ -1135,7 +1168,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
1135
1168
 
1136
1169
  #buildPropertyMatcher(prop, op, value) {
1137
1170
  if (op === 'regex') {
1138
- const regex = new RegExp(String(value), 'i');
1171
+ const regex = regexUtilities.compile(String(value), 'i');
1139
1172
  return concept => {
1140
1173
  const candidate = this.#valueForFilter(concept, prop);
1141
1174
  if (candidate == null) {
package/tx/ocl/vs-ocl.cjs CHANGED
@@ -389,20 +389,26 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
389
389
  }
390
390
 
391
391
  #indexValueSet(vs) {
392
- const existing = this.valueSetMap.get(vs.url)
393
- || (vs.version ? this.valueSetMap.get(`${vs.url}|${vs.version}`) : null)
394
- || this._idMap.get(vs.id)
395
- || null;
396
-
397
- // indexa se não existe ou se for o mesmo objeto
398
- if (!existing || existing === vs) {
399
- this.valueSetMap.set(vs.url, vs);
400
- if (vs.version) {
401
- this.valueSetMap.set(`${vs.url}|${vs.version}`, vs);
402
- }
403
- this.valueSetMap.set(vs.id, vs);
404
- this._idMap.set(vs.id, vs);
392
+ const existing = this.valueSetMap.get(vs.url) || null;
393
+
394
+ // When fresh discovery metadata replaces a cold-cached entry, carry over
395
+ // the enumerated compose so the expand engine doesn't fall back to
396
+ // "include whole CodeSystem". The background expansion will eventually
397
+ // refresh it with up-to-date collection contents.
398
+ if (existing && existing !== vs
399
+ && Array.isArray(existing.jsonObj?.compose?.include)
400
+ && existing.jsonObj.compose.include.some(inc => Array.isArray(inc.concept) && inc.concept.length > 0)
401
+ && (!vs.jsonObj.compose || !Array.isArray(vs.jsonObj.compose.include) || vs.jsonObj.compose.include.length === 0)
402
+ ) {
403
+ vs.jsonObj.compose = existing.jsonObj.compose;
404
+ }
405
+
406
+ this.valueSetMap.set(vs.url, vs);
407
+ if (vs.version) {
408
+ this.valueSetMap.set(`${vs.url}|${vs.version}`, vs);
405
409
  }
410
+ this.valueSetMap.set(vs.id, vs);
411
+ this._idMap.set(vs.id, vs);
406
412
  }
407
413
 
408
414
  #toValueSet(collection) {
@@ -568,6 +574,17 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
568
574
  ? vs.jsonObj.compose.include
569
575
  : [];
570
576
 
577
+ // If the compose already has enumerated concepts (from background expansion),
578
+ // it is the authoritative representation of the collection contents — don't
579
+ // overwrite it with system-only entries that would cause the expand engine
580
+ // to include ALL concepts from the CodeSystem.
581
+ const hasEnumeratedConcepts = existingInclude.some(
582
+ inc => Array.isArray(inc.concept) && inc.concept.length > 0
583
+ );
584
+ if (hasEnumeratedConcepts) {
585
+ return;
586
+ }
587
+
571
588
  // Always normalize existing compose entries first because discovery metadata
572
589
  // can carry non-canonical preferred_source values.
573
590
  const include = this.#normalizeComposeIncludes(existingInclude);
@@ -972,9 +989,12 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
972
989
  return;
973
990
  }
974
991
 
975
- const cached = this.backgroundExpansionCache.get(cacheKey);
992
+ let cached = this.backgroundExpansionCache.get(cacheKey);
993
+ let invalidated = false;
976
994
  if (cached && !this.#isCachedExpansionValid(vs, cached)) {
977
995
  this.backgroundExpansionCache.delete(cacheKey);
996
+ cached = null;
997
+ invalidated = true;
978
998
  }
979
999
 
980
1000
  // Already have a cached compose ready
@@ -982,23 +1002,22 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
982
1002
  return;
983
1003
  }
984
1004
 
985
- const cacheFilePath = getCacheFilePath(CACHE_VS_DIR, vs.url, vs.version || null, paramsKey);
986
- const cacheAgeFromFileMs = getColdCacheAgeMs(cacheFilePath);
987
- const persistedCache = this.backgroundExpansionCache.get(cacheKey);
988
- const cacheAgeFromMetadataMs = Number.isFinite(persistedCache?.createdAt)
989
- ? Math.max(0, Date.now() - persistedCache.createdAt)
990
- : null;
991
-
992
- // Treat cache as fresh when either file mtime or persisted timestamp is recent.
993
- const freshnessCandidates = [cacheAgeFromFileMs, cacheAgeFromMetadataMs].filter(age => age != null);
994
- const freshestCacheAgeMs = freshnessCandidates.length > 0 ? Math.min(...freshnessCandidates) : null;
995
- if (freshestCacheAgeMs != null && freshestCacheAgeMs <= COLD_CACHE_FRESHNESS_MS) {
996
- const freshnessSource = cacheAgeFromFileMs != null && cacheAgeFromMetadataMs != null
997
- ? 'file+metadata'
998
- : cacheAgeFromFileMs != null
999
- ? 'file'
1000
- : 'metadata';
1001
- return;
1005
+ // Skip freshness check when cache was just invalidated (VS metadata changed
1006
+ // on the server) — the cold cache file is stale even if recently written.
1007
+ if (!invalidated) {
1008
+ const cacheFilePath = getCacheFilePath(CACHE_VS_DIR, vs.url, vs.version || null, paramsKey);
1009
+ const cacheAgeFromFileMs = getColdCacheAgeMs(cacheFilePath);
1010
+ const persistedCache = this.backgroundExpansionCache.get(cacheKey);
1011
+ const cacheAgeFromMetadataMs = Number.isFinite(persistedCache?.createdAt)
1012
+ ? Math.max(0, Date.now() - persistedCache.createdAt)
1013
+ : null;
1014
+
1015
+ // Treat cache as fresh when either file mtime or persisted timestamp is recent.
1016
+ const freshnessCandidates = [cacheAgeFromFileMs, cacheAgeFromMetadataMs].filter(age => age != null);
1017
+ const freshestCacheAgeMs = freshnessCandidates.length > 0 ? Math.min(...freshnessCandidates) : null;
1018
+ if (freshestCacheAgeMs != null && freshestCacheAgeMs <= COLD_CACHE_FRESHNESS_MS) {
1019
+ return;
1020
+ }
1002
1021
  }
1003
1022
 
1004
1023
  const jobKey = `vs:${cacheKey}`;
@@ -1121,7 +1140,11 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1121
1140
  if (!systemConcepts.has(entry.system)) {
1122
1141
  systemConcepts.set(entry.system, []);
1123
1142
  }
1124
- systemConcepts.get(entry.system).push(entry.code);
1143
+ const concept = { code: entry.code };
1144
+ if (Array.isArray(entry.designation) && entry.designation.length > 0) {
1145
+ concept.designation = entry.designation;
1146
+ }
1147
+ systemConcepts.get(entry.system).push(concept);
1125
1148
  totalCount++;
1126
1149
  }
1127
1150
  if (progressState) {
@@ -1138,9 +1161,9 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1138
1161
  }
1139
1162
 
1140
1163
  return {
1141
- include: Array.from(systemConcepts.entries()).map(([system, codes]) => ({
1164
+ include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({
1142
1165
  system,
1143
- concept: codes.map(code => ({ code }))
1166
+ concept: concepts
1144
1167
  }))
1145
1168
  };
1146
1169
  }
@@ -15,7 +15,7 @@ function isDebugging() {
15
15
  }
16
16
  // Also check for debug flags in case inspector not yet attached
17
17
  return process.execArgv.some(arg =>
18
- arg.includes('--inspect') || arg.includes('--debug')
18
+ arg.includes('--inspect') || arg.includes('--debug')
19
19
  );
20
20
  }
21
21
 
@@ -233,16 +233,16 @@ class ExpansionCache {
233
233
  // Resources are now CodeSystem/ValueSet wrappers, not raw JSON
234
234
  if (additionalResources && additionalResources.length > 0) {
235
235
  const resourceHashes = additionalResources
236
- .map(r => {
237
- // Get the JSON object from wrapper or use directly
238
- const json = r.jsonObj || r;
239
- // Create a content hash for this resource
240
- return crypto.createHash('sha256')
241
- .update(JSON.stringify(json))
242
- .digest('hex')
243
- .substring(0, 16); // Use first 16 chars for brevity
244
- })
245
- .sort();
236
+ .map(r => {
237
+ // Get the JSON object from wrapper or use directly
238
+ const json = r.jsonObj || r;
239
+ // Create a content hash for this resource
240
+ return crypto.createHash('sha256')
241
+ .update(JSON.stringify(json))
242
+ .digest('hex')
243
+ .substring(0, 16); // Use first 16 chars for brevity
244
+ })
245
+ .sort();
246
246
  keyParts.push(`additional:${resourceHashes.join(',')}`);
247
247
  }
248
248
 
@@ -314,7 +314,7 @@ class ExpansionCache {
314
314
 
315
315
  // Get entries sorted by lastUsed (oldest first)
316
316
  const entries = Array.from(this.cache.entries())
317
- .sort((a, b) => a[1].lastUsed - b[1].lastUsed);
317
+ .sort((a, b) => a[1].lastUsed - b[1].lastUsed);
318
318
 
319
319
  const toEvict = Math.min(count, entries.length);
320
320
  for (let i = 0; i < toEvict; i++) {
@@ -413,7 +413,29 @@ class ExpansionCache {
413
413
  }
414
414
 
415
415
 
416
+ /**
417
+ * Read the cgroup memory limit once at startup.
418
+ * Returns the byte limit, or 0 if unavailable (disables the check).
419
+ */
420
+ function readMemoryLimit() {
421
+ try {
422
+ const raw = require('fs').readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim();
423
+ if (raw === 'max') return 0; // no cgroup limit
424
+ return parseInt(raw);
425
+ } catch {
426
+ return 0; // not on Linux / no cgroup
427
+ }
428
+ }
429
+
430
+ const MEMORY_LIMIT = readMemoryLimit();
431
+ const MEMORY_FRACTION = 0.98;
432
+ const MEMORY_THRESHOLD = MEMORY_LIMIT > 0 ? MEMORY_LIMIT * MEMORY_FRACTION : 0; // 90% of cgroup limit
433
+ const CHECK_FREQUENCY = 100;
434
+
416
435
  class OperationContext {
436
+ // Shared counter across all instances — only check RSS every CHECK_FREQUENCY calls
437
+ static _checkCounter = 0;
438
+
417
439
  constructor(langs, i18n = null, id = null, timeLimit = 30, resourceCache = null, expansionCache = null) {
418
440
  this.i18n = i18n;
419
441
  this.langs = this._ensureLanguages(langs);
@@ -445,8 +467,8 @@ class OperationContext {
445
467
  */
446
468
  copy() {
447
469
  const newContext = new OperationContext(
448
- this.langs, this.i18n, this.id, this.timeLimit / 1000,
449
- this.resourceCache, this.expansionCache
470
+ this.langs, this.i18n, this.id, this.timeLimit / 1000,
471
+ this.resourceCache, this.expansionCache
450
472
  );
451
473
  newContext.contexts = [...this.contexts];
452
474
  newContext.startTime = this.startTime;
@@ -458,31 +480,64 @@ class OperationContext {
458
480
  }
459
481
 
460
482
  /**
461
- * Check if operation has exceeded time limit
483
+ * Check if operation has exceeded time limit, or is pushing is over the memory limit
462
484
  * Skipped when running under debugger
485
+ *
486
+ * note: if the server pushes over the memory limit for the process, the process is terminated.
487
+ * the memory check here is intended to prevent process termination on the grounds that some
488
+ * big operation is pushing the limit. It might not be the big operation that is terminated first,
489
+ * but eventually it'll get terminated.
490
+ *
491
+ * this is called a *lot* so it's important to be efficient. Only check every CHECK_FREQUENCY
492
+ * times means that there could be a small overrun, but it's called often enough that the
493
+ * overrun won't be that signiifcant
494
+ *
463
495
  * @param {string} place - Location identifier for debugging
464
496
  * @returns {boolean} true if operation should be terminated
465
497
  */
466
498
  deadCheck(place = 'unknown') {
467
- // Skip time limit checks when debugging
468
499
  if (this.debugging) {
469
500
  return false;
470
501
  }
471
502
 
472
- const elapsed = performance.now() - this.startTime;
503
+ OperationContext._checkCounter++;
504
+ if (OperationContext._checkCounter < CHECK_FREQUENCY) {
505
+ return false;
506
+ }
507
+ OperationContext._checkCounter = 0;
473
508
 
509
+ // Time check
510
+ const elapsed = performance.now() - this.startTime;
474
511
  if (elapsed > this.timeLimit) {
475
512
  const timeInSeconds = Math.round(this.timeLimit / 1000);
476
513
  this.log(`Operation took too long @ ${place} (${this.constructor.name})`);
477
-
478
- const error = new Issue("error", "too-costly", null, `Operation exceeded time limit of ${timeInSeconds} seconds at ${place}`);
514
+ const error = new Issue("error", "too-costly", null,
515
+ `Operation exceeded time limit of ${timeInSeconds} seconds at ${place}`);
479
516
  error.diagnostics = this.diagnostics();
480
517
  throw error;
481
518
  }
482
519
 
520
+ // Memory check (piggyback on same sample)
521
+ if (MEMORY_THRESHOLD > 0) {
522
+ const rss = process.memoryUsage.rss();
523
+ if (rss > MEMORY_THRESHOLD) {
524
+ const usedGB = (rss / 1024 / 1024 / 1024).toFixed(1);
525
+ const limitGB = (MEMORY_LIMIT / 1024 / 1024 / 1024).toFixed(1);
526
+ this.log(`Memory Limit: ${usedGB} GB of ${limitGB} GB limit @ ${place}`);
527
+ const error = new Issue("error", "too-costly", null,
528
+ `Operation aborted: server memory usage (${usedGB} GB) exceeds safe threshold (${MEMORY_FRACTION * 100}% of ${limitGB} GB limit) at ${place}`);
529
+ error.diagnostics = this.diagnostics();
530
+ throw error;
531
+ }
532
+ }
533
+
483
534
  return false;
484
535
  }
485
536
 
537
+ unSeeAll() {
538
+ this.contexts = [];
539
+ }
540
+
486
541
  /**
487
542
  * Track a context URL and detect circular references
488
543
  * @param {string} vurl - Value set URL to track
package/tx/tx-html.js CHANGED
@@ -319,7 +319,7 @@ class TxHtmlRenderer {
319
319
  case 'Parameters':
320
320
  return await this.renderParameters(json);
321
321
  case 'CodeSystem':
322
- return await this.renderCodeSystem(json, inBundle, _fmt, op);
322
+ return await this.renderCodeSystem(json, inBundle, _fmt, op, req.sourcePackage);
323
323
  case 'ValueSet': {
324
324
  let exp = undefined;
325
325
  if (!inBundle && !op && (!_fmt || _fmt == 'html')) {
@@ -330,10 +330,10 @@ class TxHtmlRenderer {
330
330
  exp = error;
331
331
  }
332
332
  }
333
- return await this.renderValueSet(json, inBundle, _fmt, op, exp);
333
+ return await this.renderValueSet(json, inBundle, _fmt, op, exp, req.sourcePackage);
334
334
  }
335
335
  case 'ConceptMap':
336
- return await this.renderConceptMap(json, inBundle, _fmt, op);
336
+ return await this.renderConceptMap(json, inBundle, _fmt, op, req.sourcePackage);
337
337
  case 'CapabilityStatement':
338
338
  return await this.renderCapabilityStatement(json, inBundle);
339
339
  case 'TerminologyCapabilities':
@@ -632,7 +632,7 @@ class TxHtmlRenderer {
632
632
  /**
633
633
  * Render CodeSystem resource
634
634
  */
635
- async renderCodeSystem(json, inBundle, _fmt) {
635
+ async renderCodeSystem(json, inBundle, _fmt, op, sourcePackage) {
636
636
  if (inBundle) {
637
637
  return await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
638
638
  } else {
@@ -645,7 +645,7 @@ class TxHtmlRenderer {
645
645
  html += `</ul>`;
646
646
 
647
647
  if (!_fmt || _fmt == 'html') {
648
- html += await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json));
648
+ html += await this.renderResourceWithNarrative(json, await this.renderer.renderCodeSystem(json, sourcePackage));
649
649
  } else if (_fmt == "html/json") {
650
650
  html += await this.renderResourceJson(json);
651
651
  } else if (_fmt == "html/xml") {
@@ -18,11 +18,11 @@ sources:
18
18
  - unii:unii_20240622.db
19
19
  - snomed:sct_intl_20240201.cache
20
20
  - snomed!:sct_intl_20250201.cache
21
- - snomed:sct_se_20231130.cache
22
- - snomed:sct_au_20230731.cache
23
- - snomed:sct_be_20231115.cache
21
+ # - snomed:sct_se_20231130.cache
22
+ # - snomed:sct_au_20230731.cache
23
+ # - snomed:sct_be_20231115.cache
24
24
  - snomed:sct_ch_20230607.cache
25
- - snomed:sct_dk_20250930.cache
25
+ # - snomed:sct_dk_20250930.cache
26
26
  - snomed:sct_ips_20241216.cache
27
27
  - snomed:sct_nl_20240930.cache
28
28
  - snomed:sct_uk_20230412.cache
package/tx/tx.js CHANGED
@@ -173,6 +173,7 @@ class TXModule {
173
173
  this.metadataHandler = new MetadataHandler({
174
174
  baseUrl: config.baseUrl,
175
175
  serverVersion: packageJson.version,
176
+ txVersion: packageJson.txVersion,
176
177
  softwareName: config.softwareName || 'FHIRsmith',
177
178
  name: config.name || 'FHIRTerminologyServer',
178
179
  title: config.title || 'FHIR Terminology Server',