merge-change

1.8.1 • Public • Published

merge-change

Simple library for deep merge of objects and other types, also for patches and immutable updates. By default, merge works for "plain objects". Values of other types are replaced, but you can customize merging between specific types. Also, you can use declarative operations to specific merge like unset, leave, push and other. For example to remove some properties of object, to replace "plain objects", to concat arrays. Calculating diffs between two values.

Install

Install with npm:

$ npm install --save merge-change

API

Merge

Merge with deep cloning without changing the source objects. Great for creating or extending objects from the example (source).

mc.merge(source, ...values);

Example

const mc = require('merge-change');

// Create new object with adding "a.three" and deleting "a.one"
let first = {
  a: {
    one: true,
    two: 2
  }
};
let second = {
  a: {
    three: 3,
    $unset: ['one'] // $unset is a declarative operations
  }
};

const result = mc.merge(first, second);

console.log(result);
{ a: { two: 2,  three: 3} }

Patch

Merge with mutation of the source objects. Nice for patching. New instances will not be created.

mc.patch(source, ...patches);
let first = {
  a: {
    one: true,
    two: 2
  }
};
let second = {
  a: {
    three: 3,
    $unset: ['one'] // $unset is a declarative operations
  }
};

const result = mc.patch(first, second); // => { a: { two: 2,  three: 3} }

// result is a mutated first argument
console.log(result === first); // => true
console.log(result !== second); // => true

Update

Immutable merge - create new instances only if there are diffs (also in inner properties). Nice for state management.

mc.update(source, ...changes);
let first = {
  a: {
    one: true,
    two: 2,
    sub: {
      value: 3
    }
  }
};
let second = {
  a: {
    three: 3,
    $unset: ['one'] // $unset is a declarative operations
  }
};

const result = mc.update(first, second); // => { a: { two: 2,  three: 3, sub: { value: 3 }} }

// result is a new object
console.log(result !== first); // => true
console.log(result !== second); // => true

// object "a.sub" is unchanged
console.log(result.a.sub === first.a.sub); // => true

Declarative operations

When merging objects, you can perform delete and replace properties at the same time. Use declarative operations in second or next arguments. Supported in all merge methods. The syntax is similar to mongodb.

$set

To set (or replace) property without deep merge.

const result = mc.merge(
  {
    a: {
      one: 1, 
      two: 2
    }
  }, 
  {
    $set: {
      a: {
        three: 3
      },
      'a.two': 20 // Fields keys can be path.
    }
  }
);
console.log(result);

Result

{
  "a": {
    "one": 1, 
    "two": 20,
    "three": 3
  }
}

$unset

To unset properties by name (or path)

const result = mc.merge(
  {
    a: {
      one: 1, 
      two: 2
    }
  }, 
  {
    $unset: ['a.two']
  }
);
console.log(result);

Result

{
  "a": {
    "one": 1
  }
}

To unset all fields used *

const result = mc.merge(
  {
    a: {
      one: 1, 
      two: 2
    }
  }, 
  {
    $unset: ['a.*']
  }
);
console.log(result);

Result

{
  "a": {}
}

$leave

To leave properties by name (or path). All other properties will be removed.

const result = mc(
  {
    a: {
      one: 1, 
      two: 2,
      tree: 3
    }
  }, 
  {
    a: {
      $leave: ['two']
    }
  }
);
console.log(result);

Result

 {
   "a": {
     "two": 2
   }
 }

$push

To push one value to the array property. The source property must be an array.

const result = mc(
  // First object
  {
    prop1: ['a', 'b'],
    prop2: ['a', 'b'],
  }, 
  // Merge    
  {
    $push: {
      prop1: ['c', 'd'],
      prop2: {x: 'c'}
    },
  }
);
console.log(result);

Result

{
  "prop1": ["a", "b", ["c", "d"]],
  "prop2": ["a", "b", {"x": "c"}]
}

$concat

To union arrays.

The source property must be an array. The property in secondary arguments may not be an array.

const result = mc(
  // First object
  {
    prop1: ['a', 'b'],
    prop2: ['a', 'b'],
  }, 
  // Merge    
  {
    $concat: {
      prop1: ['c', 'd'],
      prop2: {x: 'c'}
    },
  }
);
console.log(result);

Result

{
  "prop1": ["a", "b", "c", "d"],
  "prop2": ["a", "b", {"x": "c"}]
}

Customize merge

You can declare function for merge custom types (or override default logic). Returns previous merge method.

mc.addMerge(type1, type2, callback)

  • type1, type2 - constructor name of the first and second values: Number, String, Boolean, Object, Array, Date, RegExp, Function, Undefined, Null, Symbol, Set, Map and other system and custom constructor names
  • callback - merge function with argument: (first, second, kind)
    • first - first value for merge
    • second - second value for merge
    • kind - name of merging method, such as "merge", "patch", "update".

For example, if you always need to union arrays, you can declare method to merge array with array.

const previous = mc.addMerge('Array', 'Array', function(first, second, kind){
  // merge - creaete new array with deep clone
  if (kind === 'merge'){
    return first.concat(second).map(item => mc.merge(undefined, item));
  }
  // patch - mutate first array
  if (kind === 'patch'){
    first.splice(first.length, 0, ...second);
    return first;
  }
  // update - return first array if second is empty, or create new without clone
  if (second.length === 0){
    return first;
  } else {
    return first.concat(second);
  }
});

// reset custom method
mc.addMerge('Array', 'Array', previous);

Customize declarative operation

You can declare function for declarative operation (or override default logic). Returns previous operation method.

mc.addOperation(name, callback)

  • name - operation name, for example "$concat"
  • callback - operation function with argument: (source, params). Return new value or source.
    • source - the value in which the operation is defined (source: {$concat: params})
    • params - value of operator ($concat: params)

For example, if sometimes need to union arrays, you can declare declarative operation $concat (it exists in the library).

const previous = mc.addOperation('$concat', function(source, params){
  const paths = Object.keys(params);
  for (const path of paths) {
    let value = params[path];
    let array = utils.get(source, path, []);
    if (Array.isArray(array)) {
      array = array.concat(value);
      utils.set(source, path, array);
    } else {
      throw new Error('Cannot concat on not array');
    }
  }
  return paths.length > 0;
});

// reset custom operation
mc.addOperation('$concat', previous);

Utils

Useful functions - utilities

const utils = require('merge-change').utils;

utils.diff(source, compare, {ignore = [], separator = '.'})

To calculate the difference between source and compare value. The return value is an object with $set and $unset operators. Return value can be used in merge functions. The ignore parameter - is a list of properties that are not included in the comparison.

const first = {
  name: 'value',
  profile: {
    surname: 'Surname',
    birthday: new Date(),
    avatar: {
      url: 'pic.png'
    }
  },
  access: [100, 350, 200],
  secret: 'x'
}

const second = {
  login: 'value',
  profile: {
    surname: 'Surname2',
    avatar: {
      url: 'new/pic.png'
    }
  },
  access: [700]
}

const diff = utils.diff(first, second, {ignore: ['secret'], separator: '/'});

Result (diff)

{
  $set: {
    'login': 'value',
    'profile.surname': 'Surname2',
    'profile.avatar.url': 'new/pic.png',
    'access': [ 700 ]
  },
  $unset: [ 
    'profile.birthday', 
    'name'
  ]
}

utils.type(value)

Get real type of any value. The return value is a string - the name of the constructor.

utils.type(null); // => 'Null'
utils.type(true); // => 'Boolean'
utils.type(new ObjectId()); // => 'ObjectID'

utils.instanceof(value, className)

Checking instance of class. className is string (not constructor). The return value is a boolean.

utils.instanceof(100, 'Number'); // => true
utils.instanceof(new MyClass(), 'MyClass'); // => true
utils.instanceof(new MyClass(), 'Object'); // => true

utils.plain(value)

Converting deep value to plain types if value has plain representation. For example, all dates are converted to a string, but RegEx not. To customize conversion, you can define the [methods.toPlain]() method in your object. Nice for unit tests.

The method is similar to converting to JSON, only objects (arrays, functions...) are not converted to string representation.

const plain = utils.plain({
  date: new Date('2021-01-07T19:10:21.759Z'),
  prop: {
    _id: new ObjectId('6010a8c75b9b393070e42e68')
  }
});

Result (plain)

{
  date: '2021-01-07T19:10:21.759Z',
  prop: { 
    _id: '6010a8c75b9b393070e42e68' 
  }
}

utils.flat(value, path = '', separator = '.', clearUndefined = false)

Converting a nested structure to a flat object. Property names become path with separator. To customize conversion, you can define the [methods.toFlat]() method in your object.

const value = {
  a: {
    b: {
      c: 100
    }
  }
};
const flat = utils.flat(value, 'parent', '.');

Result (flat)

{
  'parent.a.b.c': 100
}

License

Copyright © 2020, VladimirShestakov. Released under the MIT License.

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 1.8.1
    165
    • latest

Version History

Package Sidebar

Install

npm i merge-change

Weekly Downloads

166

Version

1.8.1

License

MIT

Unpacked Size

75.4 kB

Total Files

24

Last publish

Collaborators

  • boolive