Repository: FHIR/sushi (ts)

Issue:
SUSHI repeats mappings/invariants in differential
Some arrays in FHIR differentials are intended to be _additive_ -- meaning that they don't _replace_ the array in the snapshot, but rather append to it.  I believe this is the case for invariants and mappings (although we should confirm by testing the behavior of the IG publisher).

If this is the case, SUSHI should be fixed such that it only generates the _additional_ items for these arrays in the differential (rather than repeating all the existing items).

Pull request description:
Differential only includes new constraints and mappings
Fixes #731 and completes task [CIMPL-644](https://standardhealthrecord.atlassian.net/browse/CIMPL-644).

The constraint and mapping properties on an ElementDefinition are defined by FHIR to be additive. Therefore, the differential only needs to contain newly added items.

From http://hl7.org/fhir/elementdefinition.html#interpretation: "Additional constraints and mappings can be defined, but they do not replace the ones in the base definition"

Patch:
diff --git a/src/fhirtypes/ElementDefinition.ts b/src/fhirtypes/ElementDefinition.ts
index 5a830e2..c624b6e 100644
--- a/src/fhirtypes/ElementDefinition.ts
+++ b/src/fhirtypes/ElementDefinition.ts
@@ -5,7 +5,8 @@ import {
   cloneDeep,
   upperFirst,
   intersectionWith,
-  flatten
+  flatten,
+  differenceWith
 } from 'lodash';
 import sax = require('sax');
 import { minify } from 'html-minifier';
@@ -369,8 +370,18 @@ export class ElementDefinition {
       }
       // @ts-ignore
       if (prop && !isEqual(this[prop], original[prop])) {
-        // @ts-ignore
-        diff[prop] = cloneDeep(this[prop]);
+        if (ADDITIVE_PROPS.includes(prop)) {
+          // @ts-ignore
+          diff[prop] = differenceWith(this[prop], original[prop], isEqual);
+          // @ts-ignore
+          if (isEmpty(diff[prop])) {
+            // @ts-ignore
+            delete diff[prop];
+          }
+        } else {
+          // @ts-ignore
+          diff[prop] = cloneDeep(this[prop]);
+        }
       }
     }
     // Set the diff id, which may be different than snapshot id in the case of choices (e.g., value[x] -> valueString)
@@ -2190,3 +2201,10 @@ const PROPS = [
   'binding',
   'mapping'
 ];
+
+/**
+ * These list properties are considered to be additive in the differential.
+ * Thus, the differential should contain only new entries in these lists.
+ * See http://hl7.org/fhir/elementdefinition.html#interpretation.
+ */
+const ADDITIVE_PROPS = ['mapping', 'constraint'];


Test patch:
diff --git a/test/fhirtypes/ElementDefinition.test.ts b/test/fhirtypes/ElementDefinition.test.ts
index 693a5fd..1cd0026 100644
--- a/test/fhirtypes/ElementDefinition.test.ts
+++ b/test/fhirtypes/ElementDefinition.test.ts
@@ -400,6 +400,55 @@ describe('ElementDefinition', () => {
       expect(diff.path).toBe('Observation.valueString');
       expect(diff.sliceName).toBeUndefined();
     });
+
+    it('should include only new constraints in a diff when constraints are added', () => {
+      const myInvariant = new Invariant('inv-1');
+      myInvariant.severity = new FshCode('warning');
+      myInvariant.description = 'This is a good idea.';
+      valueX.applyConstraint(myInvariant, 'http://example.org');
+      const diff = valueX.calculateDiff();
+      expect(valueX.constraint.length).toBeGreaterThan(diff.constraint.length);
+      expect(diff.constraint).toHaveLength(1);
+      expect(diff.constraint[0]).toEqual({
+        key: 'inv-1',
+        source: 'http://example.org',
+        severity: 'warning',
+        human: 'This is a good idea.'
+      });
+    });
+
+    it('should not include an empty mapping property in the diff', () => {
+      valueX.setInstancePropertyByPath(
+        'constraint[0].human',
+        'All FHIR elements must have a @value or children',
+        fisher
+      );
+      const diff = valueX.calculateDiff();
+      expect(diff.constraint).toBeUndefined();
+    });
+
+    it('should include only new mappings in a diff when mappings are added', () => {
+      valueX.applyMapping('test1', 'test1.value', 'Test value mapping', null);
+      const diff = valueX.calculateDiff();
+      expect(valueX.mapping.length).toBeGreaterThan(diff.mapping.length);
+      expect(diff.mapping).toHaveLength(1);
+      expect(diff.mapping[0]).toEqual({
+        identity: 'test1',
+        map: 'test1.value',
+        comment: 'Test value mapping'
+      });
+    });
+
+    it('should not include an empty mapping property in the diff', () => {
+      valueX.setInstancePropertyByPath('mapping[0].identity', 'sct-concept', fisher);
+      valueX.setInstancePropertyByPath(
+        'mapping[0].map',
+        '< 441742003 |Evaluation finding|',
+        fisher
+      );
+      const diff = valueX.calculateDiff();
+      expect(diff.mapping).toBeUndefined();
+    });
   });
 
   describe('#parent', () => {