Repository: exadel-inc/esl
Pull request #346 (2021-07-22 13:33:29)

feat: add deepMerge utility
Little note: this implementation has difference with similar method from lodash library to merge properties which contains arrays. Elements is merged by its indexes. Primitives are replaced, but object merges. E.g.

```
var object = {
  'a': [{ 'b': 2 }, { 'd': 4 }]
};
 
var other = {
  'a': [{ 'c': 3 }, { 'e': 5 }]
};
 
_.merge(object, other);
// => { 'a': [{ 'b': 2, 'c': 3 }, { 'd': 4, 'e': 5 }] } 
```

In our case array is replaced 


Patch:
diff --git a/src/modules/esl-utils/misc/object.ts b/src/modules/esl-utils/misc/object.ts
index 1c0399215..025eca578 100644
--- a/src/modules/esl-utils/misc/object.ts
+++ b/src/modules/esl-utils/misc/object.ts
@@ -87,3 +87,28 @@ export const get = (data: any, path: string, defaultValue?: any): any => {
   }, data);
   return typeof result === 'undefined' ? defaultValue : result;
 };
+
+/**
+ * Performs a deep merge of objects and returns new object.
+ * Does not modify objects (immutable)
+ * @param objects to merge
+ * @returns new object with merged key/values
+ */
+export function deepMerge(...objects: any[]): any {
+  return objects.reduce((res: any, obj: any) => {
+    isObject(obj) && Object.keys(obj).forEach((key) => {
+      const resultVal = res[key];
+      const objectVal = obj[key];
+
+      if (Array.isArray(objectVal)) {
+        res[key] = objectVal.slice();
+      } else if (isObject(resultVal) && isObject(objectVal)) {
+        res[key] = deepMerge(resultVal, objectVal);
+      } else {
+        res[key] = objectVal;
+      }
+    });
+
+    return res;
+  }, {});
+}


Test patch:
diff --git a/src/modules/esl-utils/misc/test/object.test.ts b/src/modules/esl-utils/misc/test/object.test.ts
index b6a9b0e66..711ed84d4 100644
--- a/src/modules/esl-utils/misc/test/object.test.ts
+++ b/src/modules/esl-utils/misc/test/object.test.ts
@@ -1,4 +1,4 @@
-import {defined, deepCompare, getPropertyDescriptor, get, set, copyDefinedKeys, copy, omit} from '../object';
+import {defined, deepCompare, getPropertyDescriptor, get, set, copyDefinedKeys, copy, omit, deepMerge} from '../object';
 
 describe('misc/object', () => {
   describe('deepCompare', () => {
@@ -144,7 +144,6 @@ describe('misc/object', () => {
   });
 
   describe('omit', () => {
-    const predicate = (key: string) => !key.startsWith('_');
     test.each([
       [undefined, ['prop'], {}],
       [null, ['prop'], {}],
@@ -189,4 +188,20 @@ describe('misc/object', () => {
       expect(targ).toEqual(expVal)
     });
   });
+
+  describe('deepMerge', () => {
+    test.each([
+      [{}, {}, {}],
+      [{a: [1]}, {a: 1}, {a: 1}],
+      [{a: 1}, {a: ['2']}, {a: ['2']}],
+      [{a: 1, b: 1}, {a: 2, c: 2}, {a: 2, b: 1, c: 2}],
+      [{a: {a: 1, b: 1}, b: 1}, {a: {a: 3, c: 3}}, {a: {a: 3, b: 1, c: 3}, b: 1}],
+      [{a: 1}, {b: 2}, {c: 3}, {a: 1, b: 2, c: 3}],
+      [{a: 'value', b: 'value'}, {a: null, b: ''}, {a: null, b: ''}],
+      [{a: [1, 2, {a: 1, b: 1}]}, {a: [3, 4, {a: 2, b: 2}]}, {a: [3, 4, {a: 2, b: 2}]}]
+    ])('get key "%s" from %p', (...args: any[]) => {
+      const result = args.pop();
+      expect(deepMerge(...args)).toEqual(result);
+    });
+  });
 });