fhirsmith 0.9.6 → 0.10.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 (49) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/library/folder-content-loader.js +91 -0
  3. package/npmprojector/npmprojector.js +2 -6
  4. package/package.json +1 -1
  5. package/packages/package-crawler.js +123 -6
  6. package/packages/packages.js +104 -0
  7. package/publisher/publisher.js +290 -12
  8. package/registry/crawler.js +85 -6
  9. package/registry/registry.js +24 -7
  10. package/server.js +11 -2
  11. package/stats.js +26 -18
  12. package/translations/Messages.properties +2 -1
  13. package/tx/cs/cs-cs.js +8 -0
  14. package/tx/cs/cs-loinc.js +1 -0
  15. package/tx/cs/cs-provider-list.js +2 -1
  16. package/tx/cs/cs-snomed.js +142 -59
  17. package/tx/data/snomed-testing.cache +0 -0
  18. package/tx/html/home-metrics.liquid +10 -10
  19. package/tx/library/canonical-resource.js +4 -2
  20. package/tx/library/codesystem.js +31 -0
  21. package/tx/library/conceptmap.js +24 -0
  22. package/tx/library/designations.js +27 -20
  23. package/tx/library/renderer.js +303 -22
  24. package/tx/library/ucum-types.js +4 -1
  25. package/tx/library/valueset.js +46 -0
  26. package/tx/library.js +65 -21
  27. package/tx/operation-context.js +122 -27
  28. package/tx/params.js +36 -8
  29. package/tx/provider.js +6 -3
  30. package/tx/tx-html.js +34 -0
  31. package/tx/tx.js +92 -30
  32. package/tx/vs/vs-vsac.js +157 -9
  33. package/tx/workers/cache-control.js +186 -0
  34. package/tx/workers/{related.js → compare.js} +27 -27
  35. package/tx/workers/expand.js +100 -96
  36. package/tx/workers/lookup.js +6 -0
  37. package/tx/workers/metadata.js +11 -6
  38. package/tx/workers/read.js +1 -1
  39. package/tx/workers/translate.js +20 -29
  40. package/tx/workers/validate.js +18 -10
  41. package/tx/workers/worker.js +134 -47
  42. package/tx/xversion/xv-bundle.js +1 -2
  43. package/tx/xversion/xv-codesystem.js +5 -2
  44. package/tx/xversion/xv-parameters.js +4 -4
  45. package/tx/xversion/xv-resource.js +2 -2
  46. package/tx/xversion/xv-terminologyCapabilities.js +11 -6
  47. package/tx/xversion/xv-valueset.js +7 -7
  48. package/publisher/task-draft.js +0 -463
  49. package/tx/data/OperationDefinition-ValueSet-related.json +0 -133
@@ -124,7 +124,7 @@ class ValueSetChecker {
124
124
  }
125
125
  }
126
126
  } catch (error) {
127
- this.log.error(error);
127
+ this.worker.log.error(error);
128
128
  debugLog(error);
129
129
  throw new Error('Exception expanding value set in order to infer system: ' + error.message);
130
130
  }
@@ -467,7 +467,7 @@ class ValueSetChecker {
467
467
  return false;
468
468
  }
469
469
  let cs = await this.worker.findCodeSystem(system, version, this.params, ['complete', 'fragment'], op,true, false, false, this.worker.requiredSupplements);
470
- this.seeSourceProvider(cs, system);
470
+ this.worker.seeSourceProvider(cs, system);
471
471
  if (cs === null) {
472
472
  this.worker.opContext.addNote(this.valueSet, 'Didn\'t find CodeSystem "' + this.worker.renderer.displayCoded(system, version) + '"', this.indentCount);
473
473
  result = null;
@@ -563,7 +563,7 @@ class ValueSetChecker {
563
563
  }
564
564
  let msg = await cs.incompleteValidationMessage(ctxt.context, this.params.HTTPLanguages);
565
565
  if (msg) {
566
- op.addIssueNoId('information', 'informational', addToPath(path, 'code'), msg, 'process-note');
566
+ op.addIssue(new Issue('information', 'informational', addToPath(path, 'code'), null, msg, 'process-note'));
567
567
  }
568
568
  inactive.value = await cs.isInactive(ctxt.context);
569
569
  inactive.path = path;
@@ -824,7 +824,7 @@ class ValueSetChecker {
824
824
  } else {
825
825
  let msg = 'The code system "' + ccc.system + '" version "' + ccc.version + '" in the ValueSet expansion is different to the one in the value ("' + version + '")';
826
826
  messages.push(msg);
827
- op.addIssueNoId('error', 'not-found', addToPath(path, 'version'), msg, 'vs-invalid');
827
+ op.addIssue(new Issue('error', 'not-found', addToPath(path, 'version'), null, msg, 'vs-invalid'));
828
828
  return false;
829
829
  }
830
830
  let cs = await this.worker.findCodeSystem(system, v, this.params, ['complete', 'fragment'], op, true, true, false, this.worker.requiredSupplements);
@@ -1122,7 +1122,7 @@ class ValueSetChecker {
1122
1122
  if ((cause.value === 'not-found' && contentMode.value !== 'complete') || contentMode.value === 'example') {
1123
1123
  let m = 'The system ' + c.system + ' was found but did not contain enough information to properly validate the code "' + c.code + '" ("' + c.display + '") (mode = ' + contentMode.value + ')';
1124
1124
  msg(m);
1125
- op.addIssueNoId('warning', 'not-found', path, m, 'vs-invalid');
1125
+ op.addIssue(new Issue('warning', 'not-found', path, null, m, 'vs-invalid'));
1126
1126
  } else if (c.display && list.designations.length > 0) {
1127
1127
  await this.checkDisplays(list, defLang, c, msg, op, path);
1128
1128
  }
@@ -1350,14 +1350,18 @@ class ValueSetChecker {
1350
1350
  if (vstatus.value && vstatus.value !== 'inactive') {
1351
1351
  result.addParamCode('status', vstatus.value);
1352
1352
  }
1353
+ let mpath = inactive.path;
1354
+ if (!mpath) {
1355
+ mpath = 'code';
1356
+ }
1353
1357
  if (!['inactive', 'DISCOURAGED'].includes(vstatus.value)) {
1354
1358
  let m = this.worker.i18n.translate('INACTIVE_CONCEPT_FOUND', this.params.HTTPLanguages, ['inactive', tcode]);
1355
1359
  msg(m);
1356
- op.addIssue(new Issue('warning', 'business-rule', inactive.path, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
1360
+ op.addIssue(new Issue('warning', 'business-rule', mpath, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
1357
1361
  }
1358
1362
  let m = this.worker.i18n.translate('INACTIVE_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, tcode]);
1359
1363
  msg(m);
1360
- op.addIssue(new Issue('warning', 'business-rule', inactive.path, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
1364
+ op.addIssue(new Issue('warning', 'business-rule', mpath, 'INACTIVE_CONCEPT_FOUND', m, 'code-comment'));
1361
1365
  } else if (vstatus.value && vstatus.value.toLowerCase() === 'deprecated') {
1362
1366
  result.addParamCode('status', 'deprecated');
1363
1367
  let m = this.worker.i18n.translate('DEPRECATED_CONCEPT_FOUND', this.params.HTTPLanguages, [vstatus.value, tcode]);
@@ -1414,7 +1418,11 @@ class ValueSetChecker {
1414
1418
  m = this.worker.i18n.translate('NO_VALID_DISPLAY_AT_ALL', this.params.HTTPLanguages, [c.display, c.system, c.code]);
1415
1419
  mid = 'NO_VALID_DISPLAY_AT_ALL';
1416
1420
  } else {
1417
- if (ds === c.display) {
1421
+ // no displays in the requested language(s): fall back to checking against the
1422
+ // displays + designations in the code system's default language, not just the
1423
+ // single preferred display (see tests validation-simple-*-good-language-none)
1424
+ let hdDef = list.hasDisplay(this.params.workingLanguages(), defLang.value, c.display, false, DisplayCheckingStyle.CASE_INSENSITIVE);
1425
+ if (hdDef.found) {
1418
1426
  m = this.worker.i18n.translate('NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_OK', this.params.HTTPLanguages, [c.display, c.system, c.code, this.params.langSummary(), ds]);
1419
1427
  mid = 'NO_VALID_DISPLAY_FOUND_NONE_FOR_LANG_OK';
1420
1428
  severity = 'information';
@@ -1518,14 +1526,14 @@ class ValueSetChecker {
1518
1526
  } else if (ok === null) {
1519
1527
  result.AddParamBool('result', false);
1520
1528
  result.addParamStr('message', 'The system "' + system + '" is unknown so the /"' + code + '" cannot be confirmed to be in the value set ' + this.valueSet.name);
1521
- op.addIssueNoId('error', cause.value, 'code', 'The system "' + system + '" is unknown so the /"' + code + '" cannot be confirmed to be in the value set ' + this.valueSet.name, 'not-found');
1529
+ op.addIssue(new Issue('error', cause.value, 'code', null, 'The system "' + system + '" is unknown so the /"' + code + '" cannot be confirmed to be in the value set ' + this.valueSet.name, 'not-found'));
1522
1530
  for (let us of unknownSystems) {
1523
1531
  result.addParamCanonical('x-caused-by-unknown-system', us);
1524
1532
  }
1525
1533
  } else {
1526
1534
  result.AddParamBool('result', false);
1527
1535
  result.addParamStr('message', 'The system/code "' + system + '"/"' + code + '" is not in the value set ' + this.valueSet.name);
1528
- op.addIssueNoId('error', cause.value, 'code', 'The system/code "' + system + '"/"' + code + '" is not in the value set ' + this.valueSet.name, 'not-in-vs');
1536
+ op.addIssue(new Issue('error', cause.value, 'code', null, 'The system/code "' + system + '"/"' + code + '" is not in the value set ' + this.valueSet.name, 'not-in-vs'));
1529
1537
  if (cause.value) {
1530
1538
  result.AddParamCode('cause', cause.value);
1531
1539
  }
@@ -1,4 +1,4 @@
1
- const { TerminologyError} = require('../operation-context');
1
+ const { TerminologyError} = require('../library/errors');
2
2
  const { CodeSystem } = require('../library/codesystem');
3
3
  const ValueSet = require('../library/valueset');
4
4
  const {VersionUtilities} = require("../../library/version-utilities");
@@ -8,6 +8,12 @@ const {Languages} = require("../../library/languages");
8
8
  const {ConceptMap} = require("../library/conceptmap");
9
9
  const {Renderer} = require("../library/renderer");
10
10
 
11
+ // The cache-id travels as an HTTP header (not an operation parameter) so proxies /
12
+ // load-balancers can act on it and the server can reject it before parsing the body.
13
+ // Defined here (the base worker) so both the worker and cache-control share one
14
+ // source of truth without a circular require. Express lower-cases header names.
15
+ const CACHE_ID_HEADER = 'x-cache-id';
16
+
11
17
  /**
12
18
  * Custom error for terminology setup issues
13
19
  */
@@ -121,7 +127,11 @@ class TerminologyWorker {
121
127
  latest = i;
122
128
  }
123
129
  }
124
- return matches[latest];
130
+ const found = matches[latest];
131
+ if (this.additionalResourcesCacheId && this.log) {
132
+ this.log.info(`cache-id '${this.additionalResourcesCacheId}': using cached ${found.resourceType} ${found.url}${found.version ? '|' + found.version : ''} for lookup of ${url}${version ? '|' + version : ''}`);
133
+ }
134
+ return found;
125
135
  }
126
136
  }
127
137
 
@@ -487,50 +497,75 @@ class TerminologyWorker {
487
497
  * @returns {Object} Parameters resource
488
498
  */
489
499
  buildParameters(req) {
500
+ let params;
501
+
490
502
  // If POST with Parameters resource, use directly
491
503
  if (req.method === 'POST' && req.body && req.body.resourceType === 'Parameters') {
492
- return req.body;
493
- }
494
- if (req.method === 'POST' && req.body && req.body.resourceType) {
504
+ params = req.body;
505
+ } else if (req.method === 'POST' && req.body && req.body.resourceType) {
495
506
  let langs = this.languages.parse(req.headers['accept-language']);
496
507
  throw new Issue('error', 'invalid', null, 'Wrong_type_for_resource_expected', this.i18n.translate('Wrong_type_for_resource_expected', langs, ["Parameters", req.body.resourceType])).handleAsOO(400);
497
- }
498
-
499
- // Convert query params or form body to Parameters
500
- const source = req.method === 'POST' ? {...req.query, ...req.body} : req.query;
501
- const params = {
502
- resourceType: 'Parameters',
503
- parameter: []
504
- };
508
+ } else {
509
+ // Convert query params or form body to Parameters
510
+ const source = req.method === 'POST' ? {...req.query, ...req.body} : req.query;
511
+ params = {
512
+ resourceType: 'Parameters',
513
+ parameter: []
514
+ };
505
515
 
506
- for (const [name, value] of Object.entries(source)) {
507
- if (value === undefined || value === null) continue;
516
+ for (const [name, value] of Object.entries(source)) {
517
+ if (value === undefined || value === null) continue;
508
518
 
509
- if (Array.isArray(value)) {
510
- // Repeating parameter
511
- for (const v of value) {
512
- params.parameter.push({name, valueString: String(v)});
513
- }
514
- } else if (typeof value === 'object') {
515
- // Could be a resource or complex type - check resourceType
516
- if (value.resourceType) {
517
- params.parameter.push({name, resource: value});
519
+ if (Array.isArray(value)) {
520
+ // Repeating parameter
521
+ for (const v of value) {
522
+ params.parameter.push({name, valueString: String(v)});
523
+ }
524
+ } else if (typeof value === 'object') {
525
+ // Could be a resource or complex type - check resourceType
526
+ if (value.resourceType) {
527
+ params.parameter.push({name, resource: value});
528
+ } else {
529
+ // Assume it's a complex type like Coding or CodeableConcept
530
+ params.parameter.push(this.buildComplexParameter(name, value));
531
+ }
532
+ } else if (value == 'true') {
533
+ params.parameter.push({name, valueBoolean: true});
534
+ } else if (value == 'false') {
535
+ params.parameter.push({name, valueBoolean: false});
518
536
  } else {
519
- // Assume it's a complex type like Coding or CodeableConcept
520
- params.parameter.push(this.buildComplexParameter(name, value));
537
+ params.parameter.push({name, valueString: String(value)});
521
538
  }
522
- } else if (value == 'true') {
523
- params.parameter.push({name, valueBoolean: true});
524
- } else if (value == 'false') {
525
- params.parameter.push({name, valueBoolean: false});
526
- } else {
527
- params.parameter.push({name, valueString: String(value)});
528
539
  }
529
540
  }
530
541
 
542
+ this.applyCacheIdHeader(req, params);
531
543
  return params;
532
544
  }
533
545
 
546
+ /**
547
+ * Normalise the cache-id from the `${CACHE_ID_HEADER}` header into a `cache-id`
548
+ * parameter, so all downstream code can read the cache-id the same way whether
549
+ * the client sent it as a header (the going-forward mechanism) or, for now, still
550
+ * as a parameter. If a cache-id parameter is already present it is left as-is, so
551
+ * an explicit parameter wins and there's no surprising override.
552
+ * @param {express.Request} req
553
+ * @param {Object} params - Parameters resource (mutated in place)
554
+ */
555
+ applyCacheIdHeader(req, params) {
556
+ const headerVal = req && req.headers ? req.headers[CACHE_ID_HEADER] : null;
557
+ if (!headerVal) {
558
+ return;
559
+ }
560
+ if (!params.parameter) {
561
+ params.parameter = [];
562
+ }
563
+ if (params.parameter.some(p => p.name === 'cache-id')) {
564
+ return;
565
+ }
566
+ params.parameter.push({ name: 'cache-id', valueId: String(headerVal) });
567
+ }
568
+
534
569
  /**
535
570
  * Build a parameter for complex types
536
571
  */
@@ -559,36 +594,87 @@ class TerminologyWorker {
559
594
  // ========== Additional Resources Handling ==========
560
595
 
561
596
  /**
562
- * Set up additional resources from tx-resource parameters and cache
597
+ * Collect the resources supplied inline in a Parameters resource: the
598
+ * `tx-resource` parameters, and the primary `valueSet`/`codeSystem` parameters.
599
+ *
600
+ * The primary resource is collected separately because the cache-id protocol
601
+ * needs it retained too: fhir-core sends the main ValueSet as `valueSet` (not
602
+ * `tx-resource`), so if it isn't cached, a later by-reference call can't resolve
603
+ * it ("value set ... could not be found"). A primary resource is only usable by
604
+ * reference if it has a url to key on, so url-less ones are skipped.
605
+ *
563
606
  * @param {Object} params - Parameters resource
607
+ * @returns {{txResources: Array, primaryResources: Array}} wrapped resources
564
608
  */
565
- setupAdditionalResources(params) {
566
- if (!params || !params.parameter) return;
567
-
568
- // Collect tx-resource parameters (resources provided inline)
609
+ collectSuppliedResources(params) {
569
610
  const txResources = [];
611
+ const primaryResources = [];
612
+ if (!params || !params.parameter) {
613
+ return { txResources, primaryResources };
614
+ }
570
615
  for (const param of params.parameter) {
571
- this.deadCheck('setupAdditionalResources');
616
+ this.deadCheck('collectSuppliedResources');
572
617
  if (param.name === 'tx-resource' && param.resource) {
573
- let res = this.wrapRawResource(param.resource);
618
+ const res = this.wrapRawResource(param.resource);
574
619
  if (res) {
575
620
  txResources.push(res);
576
621
  }
622
+ } else if ((param.name === 'valueSet' || param.name === 'codeSystem') && param.resource && param.resource.url) {
623
+ const res = this.wrapRawResource(param.resource);
624
+ if (res) {
625
+ primaryResources.push(res);
626
+ }
577
627
  }
578
628
  }
629
+ return { txResources, primaryResources };
630
+ }
579
631
 
580
- // Check for cache-id
632
+ /**
633
+ * Set up additional resources from tx-resource parameters and cache
634
+ * @param {Object} params - Parameters resource
635
+ */
636
+ setupAdditionalResources(params) {
637
+ if (!params || !params.parameter) return;
638
+
639
+ // Collect the resources supplied inline on this request (tx-resource plus the
640
+ // primary valueSet/codeSystem). See collectSuppliedResources for why the
641
+ // primary resource is included.
642
+ const { txResources, primaryResources } = this.collectSuppliedResources(params);
643
+
644
+ // Check for cache-id. An explicit cache-id *parameter* wins; otherwise fall
645
+ // back to the cache-id the middleware lifted off the X-Cache-Id header onto
646
+ // the operation context. This fallback is what makes the header work on the
647
+ // op paths that don't route their Parameters through buildParameters
648
+ // (expand, related, batch-validate) or that hand setupAdditionalResources a
649
+ // raw req.body (lookup) - previously those silently ignored a front-loaded
650
+ // cache and failed to resolve by-reference resources.
581
651
  const cacheIdParam = this.findParameter(params, 'cache-id');
582
- const cacheId = cacheIdParam ? this.getParameterValue(cacheIdParam) : null;
652
+ const cacheId = (cacheIdParam ? this.getParameterValue(cacheIdParam) : null)
653
+ || (this.opContext ? this.opContext.cacheId : null)
654
+ || null;
583
655
 
584
656
  if (cacheId && this.opContext.resourceCache) {
585
- // Merge tx-resources with cached resources
586
- if (txResources.length > 0) {
587
- this.opContext.resourceCache.add(cacheId, txResources);
657
+ // The cache must already exist: caches are created explicitly via
658
+ // $cache-control?mode=start, which is the only thing that mints a cache-id.
659
+ // A cache-id the server doesn't know is an unambiguous, server-authoritative
660
+ // error condition (never created, or expired / released) - report it with a
661
+ // specific coded issue rather than silently auto-creating a fresh cache and
662
+ // then failing obscurely later when a by-reference resource can't be found.
663
+ if (!this.opContext.resourceCache.has(cacheId)) {
664
+ throw new Issue('error', 'not-found', null, 'CACHE_ID_UNKNOWN',
665
+ this.i18n.translate('CACHE_ID_UNKNOWN', this.opContext.langs, [cacheId]),
666
+ 'cache-id-unknown', 404);
667
+ }
668
+
669
+ // The cache exists: merge any resources supplied on this request into it
670
+ // (incremental population is allowed), then expose the full cache contents.
671
+ const toCache = txResources.concat(primaryResources);
672
+ if (toCache.length > 0) {
673
+ this.opContext.resourceCache.add(cacheId, toCache);
588
674
  }
589
675
 
590
- // Set additional resources to all resources for this cache-id
591
676
  this.additionalResources = this.opContext.resourceCache.get(cacheId);
677
+ this.additionalResourcesCacheId = cacheId;
592
678
  } else {
593
679
  // No cache-id, just use the tx-resources directly
594
680
  this.additionalResources = txResources;
@@ -708,7 +794,7 @@ class TerminologyWorker {
708
794
  // Check for various value types
709
795
  const valueTypes = [
710
796
  'valueString', 'valueCode', 'valueUri', 'valueCanonical', 'valueUrl',
711
- 'valueBoolean', 'valueInteger', 'valueDecimal',
797
+ 'valueBoolean', 'valueInteger', 'valueDecimal', 'valueId',
712
798
  'valueDateTime', 'valueDate', 'valueTime',
713
799
  'valueCoding', 'valueCodeableConcept',
714
800
  'valueIdentifier', 'valueQuantity'
@@ -934,5 +1020,6 @@ class TerminologyWorker {
934
1020
 
935
1021
  module.exports = {
936
1022
  TerminologyWorker,
937
- TerminologySetupError
1023
+ TerminologySetupError,
1024
+ CACHE_ID_HEADER
938
1025
  };
@@ -18,8 +18,7 @@ function bundleToR5(jsonObj, sourceVersion) {
18
18
  for (let be of jsonObj.entry || []) {
19
19
  convertResourceToR5(be.resource, sourceVersion);
20
20
  }
21
-
22
- throw new Error(`Unsupported FHIR version: ${sourceVersion}`);
21
+ return jsonObj;
23
22
  }
24
23
 
25
24
  /**
@@ -139,9 +139,12 @@ function codeSystemR5ToR3(r5Obj) {
139
139
  * @private
140
140
  */
141
141
  function isR5OnlyFilterOperator(operator) {
142
+ // Operators added in R5 that are not valid in R4 or earlier.
143
+ // NOTE: 'generalizes' is NOT R5-only — it is a valid R4 operator
144
+ // (filter-operator value set, 4.0.1), so it must not be stripped here.
142
145
  const r5OnlyOperators = [
143
- 'generalizes', // Added in R5
144
- // Add other R5-only operators as they're identified
146
+ 'child-of', // Added in R5
147
+ 'descendent-leaf', // Added in R5
145
148
  ];
146
149
  return r5OnlyOperators.includes(operator);
147
150
  }
@@ -18,7 +18,7 @@ function parametersToR5(jsonObj, sourceVersion) {
18
18
  }
19
19
 
20
20
  const {convertResourceToR5} = require("./xv-resource");
21
- for (let p of jsonObj.parameter) {
21
+ for (let p of jsonObj.parameter || []) {
22
22
  if (p.resource) {
23
23
  p.resource = convertResourceToR5(p.resource, sourceVersion);
24
24
  }
@@ -59,7 +59,7 @@ function parametersFromR5(r5Obj, targetVersion) {
59
59
  function parametersR5ToR4(r5Obj) {
60
60
  const {convertResourceFromR5} = require("./xv-resource");
61
61
 
62
- for (let p of r5Obj.parameter) {
62
+ for (let p of r5Obj.parameter || []) {
63
63
  if (p.resource) {
64
64
  p.resource = convertResourceFromR5(p.resource, "R4");
65
65
  }
@@ -71,7 +71,7 @@ function parametersR5ToR4(r5Obj) {
71
71
  }
72
72
 
73
73
  function convertResourceWithinR5(r5Obj) {
74
- for (let p of r5Obj.parameter) {
74
+ for (let p of r5Obj.parameter || []) {
75
75
  if (p.name == 'match') {
76
76
  fixMatchParameterfor5(p);
77
77
  }
@@ -135,7 +135,7 @@ function convertParameterR5ToR3(p) {
135
135
  function parametersR5ToR3(r5Obj) {
136
136
  const {convertResourceFromR5} = require("./xv-resource");
137
137
 
138
- for (let p of r5Obj.parameter) {
138
+ for (let p of r5Obj.parameter || []) {
139
139
  if (p.resource) {
140
140
  p.resource = convertResourceFromR5(p.resource, "R3");
141
141
  }
@@ -9,7 +9,7 @@ const {bundleFromR5, bundleToR5} = require("./xv-bundle");
9
9
 
10
10
 
11
11
  function convertResourceToR5(data, sourceVersion) {
12
- if (sourceVersion == "5.0" || !data.resourceType) {
12
+ if (!data || sourceVersion == "5.0" || !data.resourceType) {
13
13
  return data;
14
14
  }
15
15
  switch (data.resourceType) {
@@ -26,7 +26,7 @@ function convertResourceToR5(data, sourceVersion) {
26
26
  }
27
27
 
28
28
  function convertResourceFromR5(data, targetVersion) {
29
- if (targetVersion == "5.0" || !data.resourceType) {
29
+ if (!data || targetVersion == "5.0" || !data.resourceType) {
30
30
  return data;
31
31
  }
32
32
  switch (data.resourceType) {
@@ -15,17 +15,22 @@ function terminologyCapabilitiesToR5(jsonObj, sourceVersion) {
15
15
  }
16
16
 
17
17
  if (VersionUtilities.isR4Ver(sourceVersion)) {
18
+ const contentExtUrl = "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content";
18
19
  for (const cs of jsonObj.codeSystem || []) {
19
- if (cs.content) {
20
- let cnt = Extensions.readString(cs, "http://hl7.org/fhir/5.0/StructureDefinition/extension-TerminologyCapabilities.codeSystem.content");
21
- if (cnt) {
22
- delete cs.extensions;
23
- cs.content = cnt;
20
+ // In R4 the content is carried only by the content extension (there is no
21
+ // native codeSystem.content), so read it from the extension directly.
22
+ const cnt = Extensions.readString(cs, contentExtUrl);
23
+ if (cnt) {
24
+ cs.content = cnt;
25
+ // Remove the now-redundant source extension (the field is `extension`,
26
+ // singular). Filter so any unrelated extensions are preserved.
27
+ cs.extension = (cs.extension || []).filter(e => e.url !== contentExtUrl);
28
+ if (cs.extension.length === 0) {
29
+ delete cs.extension;
24
30
  }
25
31
  }
26
32
  }
27
33
 
28
-
29
34
  return jsonObj;
30
35
  }
31
36
 
@@ -14,10 +14,10 @@ function valueSetToR5(jsonObj, sourceVersion) {
14
14
  if (VersionUtilities.isR5Ver(sourceVersion)) {
15
15
  return jsonObj; // No conversion needed
16
16
  }
17
- for (const inc of jsonObj.compose.include || []) {
17
+ for (const inc of jsonObj.compose?.include || []) {
18
18
  valueSetIncludeToR5(inc);
19
19
  }
20
- for (const inc of jsonObj.compose.exclude || []) {
20
+ for (const inc of jsonObj.compose?.exclude || []) {
21
21
  valueSetIncludeToR5(inc);
22
22
  }
23
23
  if (VersionUtilities.isR4Ver(sourceVersion)) {
@@ -90,7 +90,7 @@ function valueSetR5ToR4(r5Obj) {
90
90
  if (include.filter && Array.isArray(include.filter)) {
91
91
  include.filter = include.filter.map(filter => {
92
92
  if (filter.op && isR5OnlyFilterOperator(filter.op)) {
93
- filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}
93
+ filter._op = { "extension": [{ url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}]};
94
94
  delete filter.op;
95
95
  }
96
96
  return filter;
@@ -105,7 +105,7 @@ function valueSetR5ToR4(r5Obj) {
105
105
  if (exclude.filter && Array.isArray(exclude.filter)) {
106
106
  exclude.filter = exclude.filter.map(filter => {
107
107
  if (filter.op && isR5OnlyFilterOperator(filter.op)) {
108
- filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}
108
+ filter._op = { "extension": [{ url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}]};
109
109
  delete filter.op;
110
110
  }
111
111
  return filter;
@@ -155,7 +155,7 @@ function valueSetR5ToR3(r5Obj) {
155
155
  if (include.filter && Array.isArray(include.filter)) {
156
156
  include.filter = include.filter.map(filter => {
157
157
  if (filter.op && !isR3CompatibleFilterOperator(filter.op)) {
158
- filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}
158
+ filter._op = { "extension": [{ url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}]};
159
159
  delete filter.op;
160
160
  }
161
161
  return filter;
@@ -170,7 +170,7 @@ function valueSetR5ToR3(r5Obj) {
170
170
  if (exclude.filter && Array.isArray(exclude.filter)) {
171
171
  exclude.filter = exclude.filter.map(filter => {
172
172
  if (filter.op && !isR3CompatibleFilterOperator(filter.op)) {
173
- filter._op = { "extension": "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}
173
+ filter._op = { "extension": [{ url: "http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op", "valueCode": filter.op}]};
174
174
  delete filter.op;
175
175
  }
176
176
  return filter;
@@ -223,7 +223,7 @@ function convertContainsPropertyR5ToR4(containsList) {
223
223
  */
224
224
  function isR5OnlyFilterOperator(operator) {
225
225
  const r5OnlyOperators = [
226
- 'child-of', ' descendent-leaf' // Added in R5
226
+ 'child-of', 'descendent-leaf' // Added in R5
227
227
  ];
228
228
  return r5OnlyOperators.includes(operator);
229
229
  }