├── .gitignore ├── .npmignore ├── .travis.yml ├── README.md ├── dist └── changesets.js ├── index.js ├── package-lock.json ├── package.json ├── renovate.json ├── src └── changesets.coffee └── test └── changesets.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | src 3 | node_modules 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diff-json 2 | [![Build Status](https://travis-ci.org/viruschidai/diff-json.svg?branch=master)](https://travis-ci.org/viruschidai/diff-json) 3 | [![Downloads](https://img.shields.io/npm/dm/diff-json.svg)](https://www.npmjs.com/package/diff-json) 4 | 5 | A diff tool for javascript objects inspired by https://github.com/eugeneware/changeset. 6 | 7 | ## Features 8 | 9 | ### diff 10 | 11 | If a key is specified for an embedded array, the diff will be generated based on the objects have same keys. 12 | 13 | #### Examples: 14 | 15 | ```javascript 16 | 17 | var changesets = require('diff-json'); 18 | var newObj, oldObj; 19 | 20 | oldObj = { 21 | name: 'joe', 22 | age: 55, 23 | coins: [2, 5], 24 | children: [ 25 | {name: 'kid1', age: 1}, 26 | {name: 'kid2', age: 2} 27 | ]}; 28 | 29 | newObj = { 30 | name: 'smith', 31 | coins: [2, 5, 1], 32 | children: [ 33 | {name: 'kid3', age: 3}, 34 | {name: 'kid1', age: 0}, 35 | {name: 'kid2', age: 2} 36 | ]}; 37 | 38 | 39 | # Assume children is an array of child object and the child object has 'name' as its primary key 40 | diffs = changesets.diff(oldObj, newObj, {children: 'name'}); 41 | 42 | expect(diffs).to.eql([ 43 | { 44 | type: 'update', key: 'name', value: 'smith', oldValue: 'joe' 45 | }, 46 | { 47 | type: 'update', key: 'coins', embededKey: '$index', changes: [ 48 | {type: 'add', key: '2', value: 1 } 49 | ] 50 | }, 51 | { 52 | type: 'update', 53 | key: 'children', 54 | embededKey: 'name', 55 | changes: [ 56 | { 57 | type: 'update', key: 'kid1', changes: [ 58 | {type: 'update', key: 'age', value: 0, oldValue: 1 } 59 | ] 60 | }, 61 | { 62 | type: 'add', key: 'kid3', value: {name: 'kid3', age: 3 } 63 | } 64 | ] 65 | }, 66 | { 67 | type: 'remove', key: 'age', value: 55 68 | } 69 | ]); 70 | ``` 71 | 72 | ### applyChange 73 | #### Examples: 74 | 75 | ```javascript 76 | 77 | var changesets = require('diff-json'); 78 | var oldObj = { 79 | name: 'joe', 80 | age: 55, 81 | coins: [2, 5], 82 | children: [ 83 | {name: 'kid1', age: 1}, 84 | {name: 'kid2', age: 2} 85 | ]}; 86 | 87 | 88 | # Assume children is an array of child object and the child object has 'name' as its primary key 89 | diffs = [ 90 | { 91 | type: 'update', key: 'name', value: 'smith', oldValue: 'joe' 92 | }, 93 | { 94 | type: 'update', key: 'coins', embededKey: '$index', changes: [ 95 | {type: 'add', key: '2', value: 1 } 96 | ] 97 | }, 98 | { 99 | type: 'update', 100 | key: 'children', 101 | embededKey: 'name', // The key property name of the elements in an array 102 | changes: [ 103 | { 104 | type: 'update', key: 'kid1', changes: [ 105 | {type: 'update', key: 'age', value: 0, oldValue: 1 } 106 | ] 107 | }, 108 | { 109 | type: 'add', key: 'kid3', value: {name: 'kid3', age: 3 } 110 | } 111 | ] 112 | }, 113 | { 114 | type: 'remove', key: 'age', value: 55 115 | } 116 | ] 117 | 118 | changesets.applyChanges(oldObj, diffs) 119 | expect(oldObj).to.eql({ 120 | name: 'smith', 121 | coins: [2, 5, 1], 122 | children: [ 123 | {name: 'kid3', age: 3}, 124 | {name: 'kid1', age: 0}, 125 | {name: 'kid2', age: 2} 126 | ]}); 127 | 128 | ``` 129 | 130 | ### revertChange 131 | #### Examples: 132 | 133 | ```javascript 134 | 135 | var changesets = require('diff-json'); 136 | 137 | var newObj = { 138 | name: 'smith', 139 | coins: [2, 5, 1], 140 | children: [ 141 | {name: 'kid3', age: 3}, 142 | {name: 'kid1', age: 0}, 143 | {name: 'kid2', age: 2} 144 | ]}; 145 | 146 | # Assume children is an array of child object and the child object has 'name' as its primary key 147 | diffs = [ 148 | { 149 | type: 'update', key: 'name', value: 'smith', oldValue: 'joe' 150 | }, 151 | { 152 | type: 'update', key: 'coins', embededKey: '$index', changes: [ 153 | {type: 'add', key: '2', value: 1 } 154 | ] 155 | }, 156 | { 157 | type: 'update', 158 | key: 'children', 159 | embededKey: 'name', // The key property name of the elements in an array 160 | changes: [ 161 | { 162 | type: 'update', key: 'kid1', changes: [ 163 | {type: 'update', key: 'age', value: 0, oldValue: 1 } 164 | ] 165 | }, 166 | { 167 | type: 'add', key: 'kid3', value: {name: 'kid3', age: 3 } 168 | } 169 | ] 170 | }, 171 | { 172 | type: 'remove', key: 'age', value: 55 173 | } 174 | ] 175 | 176 | changesets.revertChanges(newObj, diffs) 177 | expect(newObj).to.eql { 178 | name: 'joe', 179 | age: 55, 180 | coins: [2, 5], 181 | children: [ 182 | {name: 'kid1', age: 1}, 183 | {name: 'kid2', age: 2} 184 | ]}; 185 | 186 | ``` 187 | 188 | ## Get started 189 | 190 | ``` 191 | npm install diff-json 192 | ``` 193 | 194 | ## Run the test 195 | ``` 196 | npm run test 197 | ``` 198 | 199 | ## Licence 200 | 201 | The MIT License (MIT) 202 | 203 | Copyright (c) 2013 viruschidai@gmail.com 204 | 205 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 206 | 207 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 208 | 209 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 210 | -------------------------------------------------------------------------------- /dist/changesets.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.12.7 2 | (function() { 3 | (function() { 4 | var _difference, _find, _intersection, _keyBy, addKeyValue, applyArrayChange, applyBranchChange, applyLeafChange, changeset, compare, compareArray, compareObject, comparePrimitives, convertArrayToObj, exports, getKey, getTypeOfObj, indexOfItemInArray, isEmbeddedKey, modifyKeyValue, parseEmbeddedKeyValue, removeKey, revertArrayChange, revertBranchChange, revertLeafChange; 5 | changeset = { 6 | VERSION: '0.1.4' 7 | }; 8 | if (typeof module === 'object' && module.exports) { 9 | _intersection = require('lodash.intersection'); 10 | _difference = require('lodash.difference'); 11 | _keyBy = require('lodash.keyby'); 12 | _find = require('lodash.find'); 13 | module.exports = exports = changeset; 14 | } else { 15 | this.changeset = changeset; 16 | } 17 | getTypeOfObj = function(obj) { 18 | if (typeof obj === 'undefined') { 19 | return 'undefined'; 20 | } 21 | if (obj === null) { 22 | return null; 23 | } 24 | return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1]; 25 | }; 26 | getKey = function(path) { 27 | var ref; 28 | return (ref = path[path.length - 1]) != null ? ref : '$root'; 29 | }; 30 | compare = function(oldObj, newObj, path, embededObjKeys, keyPath) { 31 | var changes, diffs, typeOfNewObj, typeOfOldObj; 32 | changes = []; 33 | typeOfOldObj = getTypeOfObj(oldObj); 34 | typeOfNewObj = getTypeOfObj(newObj); 35 | if (typeOfOldObj !== typeOfNewObj) { 36 | changes.push({ 37 | type: changeset.op.REMOVE, 38 | key: getKey(path), 39 | value: oldObj 40 | }); 41 | changes.push({ 42 | type: changeset.op.ADD, 43 | key: getKey(path), 44 | value: newObj 45 | }); 46 | return changes; 47 | } 48 | switch (typeOfOldObj) { 49 | case 'Date': 50 | changes = changes.concat(comparePrimitives(oldObj.getTime(), newObj.getTime(), path)); 51 | break; 52 | case 'Object': 53 | diffs = compareObject(oldObj, newObj, path, embededObjKeys, keyPath); 54 | if (diffs.length) { 55 | if (path.length) { 56 | changes.push({ 57 | type: changeset.op.UPDATE, 58 | key: getKey(path), 59 | changes: diffs 60 | }); 61 | } else { 62 | changes = changes.concat(diffs); 63 | } 64 | } 65 | break; 66 | case 'Array': 67 | changes = changes.concat(compareArray(oldObj, newObj, path, embededObjKeys, keyPath)); 68 | break; 69 | case 'Function': 70 | break; 71 | default: 72 | changes = changes.concat(comparePrimitives(oldObj, newObj, path)); 73 | } 74 | return changes; 75 | }; 76 | compareObject = function(oldObj, newObj, path, embededObjKeys, keyPath, skipPath) { 77 | var addedKeys, changes, deletedKeys, diffs, i, intersectionKeys, j, k, l, len, len1, len2, newKeyPath, newObjKeys, newPath, oldObjKeys; 78 | if (skipPath == null) { 79 | skipPath = false; 80 | } 81 | changes = []; 82 | oldObjKeys = Object.keys(oldObj); 83 | newObjKeys = Object.keys(newObj); 84 | intersectionKeys = _intersection(oldObjKeys, newObjKeys); 85 | for (i = 0, len = intersectionKeys.length; i < len; i++) { 86 | k = intersectionKeys[i]; 87 | newPath = path.concat([k]); 88 | newKeyPath = skipPath ? keyPath : keyPath.concat([k]); 89 | diffs = compare(oldObj[k], newObj[k], newPath, embededObjKeys, newKeyPath); 90 | if (diffs.length) { 91 | changes = changes.concat(diffs); 92 | } 93 | } 94 | addedKeys = _difference(newObjKeys, oldObjKeys); 95 | for (j = 0, len1 = addedKeys.length; j < len1; j++) { 96 | k = addedKeys[j]; 97 | newPath = path.concat([k]); 98 | newKeyPath = skipPath ? keyPath : keyPath.concat([k]); 99 | changes.push({ 100 | type: changeset.op.ADD, 101 | key: getKey(newPath), 102 | value: newObj[k] 103 | }); 104 | } 105 | deletedKeys = _difference(oldObjKeys, newObjKeys); 106 | for (l = 0, len2 = deletedKeys.length; l < len2; l++) { 107 | k = deletedKeys[l]; 108 | newPath = path.concat([k]); 109 | newKeyPath = skipPath ? keyPath : keyPath.concat([k]); 110 | changes.push({ 111 | type: changeset.op.REMOVE, 112 | key: getKey(newPath), 113 | value: oldObj[k] 114 | }); 115 | } 116 | return changes; 117 | }; 118 | compareArray = function(oldObj, newObj, path, embededObjKeys, keyPath) { 119 | var diffs, indexedNewObj, indexedOldObj, ref, uniqKey; 120 | uniqKey = (ref = embededObjKeys != null ? embededObjKeys[keyPath.join('.')] : void 0) != null ? ref : '$index'; 121 | indexedOldObj = convertArrayToObj(oldObj, uniqKey); 122 | indexedNewObj = convertArrayToObj(newObj, uniqKey); 123 | diffs = compareObject(indexedOldObj, indexedNewObj, path, embededObjKeys, keyPath, true); 124 | if (diffs.length) { 125 | return [ 126 | { 127 | type: changeset.op.UPDATE, 128 | key: getKey(path), 129 | embededKey: uniqKey, 130 | changes: diffs 131 | } 132 | ]; 133 | } else { 134 | return []; 135 | } 136 | }; 137 | convertArrayToObj = function(arr, uniqKey) { 138 | var index, obj, value; 139 | obj = {}; 140 | if (uniqKey !== '$index') { 141 | obj = _keyBy(arr, uniqKey); 142 | } else { 143 | for (index in arr) { 144 | value = arr[index]; 145 | obj[index] = value; 146 | } 147 | } 148 | return obj; 149 | }; 150 | comparePrimitives = function(oldObj, newObj, path) { 151 | var changes; 152 | changes = []; 153 | if (oldObj !== newObj) { 154 | changes.push({ 155 | type: changeset.op.UPDATE, 156 | key: getKey(path), 157 | value: newObj, 158 | oldValue: oldObj 159 | }); 160 | } 161 | return changes; 162 | }; 163 | isEmbeddedKey = function(key) { 164 | return /\$.*=/gi.test(key); 165 | }; 166 | removeKey = function(obj, key, embededKey) { 167 | var index; 168 | if (Array.isArray(obj)) { 169 | if (embededKey !== '$index' || !obj[key]) { 170 | index = indexOfItemInArray(obj, embededKey, key); 171 | } 172 | return obj.splice(index != null ? index : key, 1); 173 | } else { 174 | return delete obj[key]; 175 | } 176 | }; 177 | indexOfItemInArray = function(arr, key, value) { 178 | var index, item; 179 | for (index in arr) { 180 | item = arr[index]; 181 | if (key === '$index') { 182 | if (item === value) { 183 | return index; 184 | } 185 | } else if (item[key] === value) { 186 | return index; 187 | } 188 | } 189 | return -1; 190 | }; 191 | modifyKeyValue = function(obj, key, value) { 192 | return obj[key] = value; 193 | }; 194 | addKeyValue = function(obj, key, value) { 195 | if (Array.isArray(obj)) { 196 | return obj.push(value); 197 | } else { 198 | return obj[key] = value; 199 | } 200 | }; 201 | parseEmbeddedKeyValue = function(key) { 202 | var uniqKey, value; 203 | uniqKey = key.substring(1, key.indexOf('=')); 204 | value = key.substring(key.indexOf('=') + 1); 205 | return { 206 | uniqKey: uniqKey, 207 | value: value 208 | }; 209 | }; 210 | applyLeafChange = function(obj, change, embededKey) { 211 | var key, type, value; 212 | type = change.type, key = change.key, value = change.value; 213 | switch (type) { 214 | case changeset.op.ADD: 215 | return addKeyValue(obj, key, value); 216 | case changeset.op.UPDATE: 217 | return modifyKeyValue(obj, key, value); 218 | case changeset.op.REMOVE: 219 | return removeKey(obj, key, embededKey); 220 | } 221 | }; 222 | applyArrayChange = function(arr, change) { 223 | var element, i, len, ref, results, subchange; 224 | ref = change.changes; 225 | results = []; 226 | for (i = 0, len = ref.length; i < len; i++) { 227 | subchange = ref[i]; 228 | if ((subchange.value != null) || subchange.type === changeset.op.REMOVE) { 229 | results.push(applyLeafChange(arr, subchange, change.embededKey)); 230 | } else { 231 | if (change.embededKey === '$index') { 232 | element = arr[+subchange.key]; 233 | } else { 234 | element = _find(arr, function(el) { 235 | return el[change.embededKey] === subchange.key; 236 | }); 237 | } 238 | results.push(changeset.applyChanges(element, subchange.changes)); 239 | } 240 | } 241 | return results; 242 | }; 243 | applyBranchChange = function(obj, change) { 244 | if (Array.isArray(obj)) { 245 | return applyArrayChange(obj, change); 246 | } else { 247 | return changeset.applyChanges(obj, change.changes); 248 | } 249 | }; 250 | revertLeafChange = function(obj, change, embededKey) { 251 | var key, oldValue, type, value; 252 | type = change.type, key = change.key, value = change.value, oldValue = change.oldValue; 253 | switch (type) { 254 | case changeset.op.ADD: 255 | return removeKey(obj, key, embededKey); 256 | case changeset.op.UPDATE: 257 | return modifyKeyValue(obj, key, oldValue); 258 | case changeset.op.REMOVE: 259 | return addKeyValue(obj, key, value); 260 | } 261 | }; 262 | revertArrayChange = function(arr, change) { 263 | var element, i, len, ref, results, subchange; 264 | ref = change.changes; 265 | results = []; 266 | for (i = 0, len = ref.length; i < len; i++) { 267 | subchange = ref[i]; 268 | if ((subchange.value != null) || subchange.type === changeset.op.REMOVE) { 269 | results.push(revertLeafChange(arr, subchange, change.embededKey)); 270 | } else { 271 | if (change.embededKey === '$index') { 272 | element = arr[+subchange.key]; 273 | } else { 274 | element = _find(arr, function(el) { 275 | return el[change.embededKey] === subchange.key; 276 | }); 277 | } 278 | results.push(changeset.revertChanges(element, subchange.changes)); 279 | } 280 | } 281 | return results; 282 | }; 283 | revertBranchChange = function(obj, change) { 284 | if (Array.isArray(obj)) { 285 | return revertArrayChange(obj, change); 286 | } else { 287 | return changeset.revertChanges(obj, change.changes); 288 | } 289 | }; 290 | changeset.diff = function(oldObj, newObj, embededObjKeys) { 291 | return compare(oldObj, newObj, [], embededObjKeys, []); 292 | }; 293 | changeset.applyChanges = function(obj, changesets) { 294 | var change, i, len, results; 295 | results = []; 296 | for (i = 0, len = changesets.length; i < len; i++) { 297 | change = changesets[i]; 298 | if (!change.changes || (change.value != null) || change.type === changeset.op.REMOVE) { 299 | results.push(applyLeafChange(obj, change, change.embededKey)); 300 | } else { 301 | results.push(applyBranchChange(obj[change.key], change)); 302 | } 303 | } 304 | return results; 305 | }; 306 | changeset.revertChanges = function(obj, changeset) { 307 | var change, i, len, ref, results; 308 | ref = changeset.reverse(); 309 | results = []; 310 | for (i = 0, len = ref.length; i < len; i++) { 311 | change = ref[i]; 312 | if (!change.changes) { 313 | results.push(revertLeafChange(obj, change)); 314 | } else { 315 | results.push(revertBranchChange(obj[change.key], change)); 316 | } 317 | } 318 | return results; 319 | }; 320 | changeset.op = { 321 | REMOVE: 'remove', 322 | ADD: 'add', 323 | UPDATE: 'update' 324 | }; 325 | })(); 326 | 327 | }).call(this); 328 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/changesets') 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diff-json", 3 | "version": "2.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.11", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 16 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "^1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "browser-stdout": { 24 | "version": "1.3.1", 25 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 26 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 27 | "dev": true 28 | }, 29 | "coffee-script": { 30 | "version": "1.12.7", 31 | "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", 32 | "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", 33 | "dev": true 34 | }, 35 | "commander": { 36 | "version": "2.15.1", 37 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 38 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 39 | "dev": true 40 | }, 41 | "concat-map": { 42 | "version": "0.0.1", 43 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 44 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 45 | "dev": true 46 | }, 47 | "debug": { 48 | "version": "3.1.0", 49 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 50 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 51 | "dev": true, 52 | "requires": { 53 | "ms": "2.0.0" 54 | } 55 | }, 56 | "diff": { 57 | "version": "3.5.0", 58 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 59 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 60 | "dev": true 61 | }, 62 | "escape-string-regexp": { 63 | "version": "1.0.5", 64 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 65 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 66 | "dev": true 67 | }, 68 | "expect.js": { 69 | "version": "0.3.1", 70 | "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", 71 | "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", 72 | "dev": true 73 | }, 74 | "fs.realpath": { 75 | "version": "1.0.0", 76 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 77 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 78 | "dev": true 79 | }, 80 | "glob": { 81 | "version": "7.1.2", 82 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 83 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 84 | "dev": true, 85 | "requires": { 86 | "fs.realpath": "^1.0.0", 87 | "inflight": "^1.0.4", 88 | "inherits": "2", 89 | "minimatch": "^3.0.4", 90 | "once": "^1.3.0", 91 | "path-is-absolute": "^1.0.0" 92 | } 93 | }, 94 | "growl": { 95 | "version": "1.10.5", 96 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 97 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 98 | "dev": true 99 | }, 100 | "has-flag": { 101 | "version": "3.0.0", 102 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 103 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 104 | "dev": true 105 | }, 106 | "he": { 107 | "version": "1.1.1", 108 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 109 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 110 | "dev": true 111 | }, 112 | "inflight": { 113 | "version": "1.0.6", 114 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 115 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 116 | "dev": true, 117 | "requires": { 118 | "once": "^1.3.0", 119 | "wrappy": "1" 120 | } 121 | }, 122 | "inherits": { 123 | "version": "2.0.3", 124 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 125 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 126 | "dev": true 127 | }, 128 | "lodash.difference": { 129 | "version": "4.5.0", 130 | "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", 131 | "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=" 132 | }, 133 | "lodash.find": { 134 | "version": "4.6.0", 135 | "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", 136 | "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=" 137 | }, 138 | "lodash.intersection": { 139 | "version": "4.4.0", 140 | "resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz", 141 | "integrity": "sha1-ChG6Yx0OlcI8fy9Mu5ppLtF45wU=" 142 | }, 143 | "lodash.keyby": { 144 | "version": "4.6.0", 145 | "resolved": "https://registry.npmjs.org/lodash.keyby/-/lodash.keyby-4.6.0.tgz", 146 | "integrity": "sha1-f2oavak/0k4icopNNh7YvLpaQ1Q=" 147 | }, 148 | "minimatch": { 149 | "version": "3.0.4", 150 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 151 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 152 | "dev": true, 153 | "requires": { 154 | "brace-expansion": "^1.1.7" 155 | } 156 | }, 157 | "minimist": { 158 | "version": "0.0.8", 159 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 160 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 161 | "dev": true 162 | }, 163 | "mkdirp": { 164 | "version": "0.5.1", 165 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 166 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 167 | "dev": true, 168 | "requires": { 169 | "minimist": "0.0.8" 170 | } 171 | }, 172 | "mocha": { 173 | "version": "5.2.0", 174 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 175 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 176 | "dev": true, 177 | "requires": { 178 | "browser-stdout": "1.3.1", 179 | "commander": "2.15.1", 180 | "debug": "3.1.0", 181 | "diff": "3.5.0", 182 | "escape-string-regexp": "1.0.5", 183 | "glob": "7.1.2", 184 | "growl": "1.10.5", 185 | "he": "1.1.1", 186 | "minimatch": "3.0.4", 187 | "mkdirp": "0.5.1", 188 | "supports-color": "5.4.0" 189 | } 190 | }, 191 | "ms": { 192 | "version": "2.0.0", 193 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 194 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 195 | "dev": true 196 | }, 197 | "once": { 198 | "version": "1.4.0", 199 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 200 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 201 | "dev": true, 202 | "requires": { 203 | "wrappy": "1" 204 | } 205 | }, 206 | "path-is-absolute": { 207 | "version": "1.0.1", 208 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 209 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 210 | "dev": true 211 | }, 212 | "supports-color": { 213 | "version": "5.4.0", 214 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 215 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 216 | "dev": true, 217 | "requires": { 218 | "has-flag": "^3.0.0" 219 | } 220 | }, 221 | "wrappy": { 222 | "version": "1.0.2", 223 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 224 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 225 | "dev": true 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diff-json", 3 | "version": "2.0.0", 4 | "description": "Generates diffs of javascript objects.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "NODE_ENV=test mocha --compilers coffee:coffee-script/register -R spec --recursive test", 8 | "prepublish": "coffee --output dist --compile src" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:viruschidai/diff-json.git" 13 | }, 14 | "keywords": [ 15 | "javascript", 16 | "JSON", 17 | "diff", 18 | "patch", 19 | "revert", 20 | "apply changes", 21 | "revert changes" 22 | ], 23 | "author": "viruschidai@gmail.com", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/viruschidai/diff-json/issues" 27 | }, 28 | "homepage": "https://github.com/viruschidai/diff-json", 29 | "dependencies": { 30 | "lodash.intersection": ">= 4.0.0", 31 | "lodash.difference": ">= 4.0.0", 32 | "lodash.keyby": ">= 4.0.0", 33 | "lodash.find": ">= 4.0.0" 34 | }, 35 | "devDependencies": { 36 | "coffee-script": "1.12.7", 37 | "expect.js": "0.3.1", 38 | "mocha": "5.2.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/changesets.coffee: -------------------------------------------------------------------------------- 1 | (-> 2 | 3 | changeset = 4 | VERSION: '0.1.4' 5 | 6 | if typeof module is 'object' and module.exports 7 | _intersection = require 'lodash.intersection' 8 | _difference = require 'lodash.difference' 9 | _keyBy = require 'lodash.keyby' 10 | _find = require 'lodash.find' 11 | module.exports = exports = changeset 12 | else 13 | # just set the global for non-node platforms. 14 | this.changeset = changeset 15 | 16 | 17 | getTypeOfObj = (obj) -> 18 | if typeof obj is 'undefined' 19 | return 'undefined' 20 | 21 | if obj is null 22 | return null 23 | 24 | return Object.prototype.toString.call(obj) .match(/^\[object\s(.*)\]$/)[1]; 25 | 26 | 27 | getKey = (path) -> 28 | path[path.length - 1] ? '$root' 29 | 30 | 31 | compare = (oldObj, newObj, path, embededObjKeys, keyPath) -> 32 | changes = [] 33 | 34 | typeOfOldObj = getTypeOfObj oldObj 35 | typeOfNewObj = getTypeOfObj newObj 36 | 37 | # if type of object changes, consider it as old obj has been deleted and a new object has been added 38 | if typeOfOldObj != typeOfNewObj 39 | changes.push type: changeset.op.REMOVE, key: getKey(path), value: oldObj 40 | changes.push type: changeset.op.ADD, key: getKey(path), value: newObj 41 | return changes 42 | 43 | switch typeOfOldObj 44 | when 'Date' 45 | changes = changes.concat comparePrimitives oldObj.getTime(), newObj.getTime(), path 46 | when 'Object' 47 | diffs = compareObject oldObj, newObj, path, embededObjKeys, keyPath 48 | if diffs.length 49 | if path.length 50 | changes.push type: changeset.op.UPDATE, key: getKey(path), changes: diffs 51 | else 52 | changes = changes.concat diffs 53 | when 'Array' 54 | changes = changes.concat compareArray oldObj, newObj, path, embededObjKeys, keyPath 55 | when 'Function' 56 | # do nothing 57 | else 58 | changes = changes.concat comparePrimitives oldObj, newObj, path 59 | 60 | return changes 61 | 62 | 63 | compareObject = (oldObj, newObj, path, embededObjKeys, keyPath, skipPath = false) -> 64 | changes = [] 65 | 66 | oldObjKeys = Object.keys(oldObj) 67 | newObjKeys = Object.keys(newObj) 68 | 69 | intersectionKeys = _intersection oldObjKeys, newObjKeys 70 | for k in intersectionKeys 71 | newPath = path.concat [k] 72 | newKeyPath = if skipPath then keyPath else keyPath.concat [k] 73 | diffs = compare oldObj[k], newObj[k], newPath, embededObjKeys, newKeyPath 74 | if diffs.length 75 | changes = changes.concat diffs 76 | 77 | addedKeys = _difference newObjKeys, oldObjKeys 78 | for k in addedKeys 79 | newPath = path.concat [k] 80 | newKeyPath = if skipPath then keyPath else keyPath.concat [k] 81 | changes.push type: changeset.op.ADD, key: getKey(newPath), value: newObj[k] 82 | 83 | deletedKeys = _difference oldObjKeys, newObjKeys 84 | for k in deletedKeys 85 | newPath = path.concat [k] 86 | newKeyPath = if skipPath then keyPath else keyPath.concat [k] 87 | changes.push type: changeset.op.REMOVE, key: getKey(newPath), value: oldObj[k] 88 | return changes 89 | 90 | 91 | compareArray = (oldObj, newObj, path, embededObjKeys, keyPath) -> 92 | uniqKey = embededObjKeys?[keyPath.join '.'] ? '$index' 93 | indexedOldObj = convertArrayToObj oldObj, uniqKey 94 | indexedNewObj = convertArrayToObj newObj, uniqKey 95 | diffs = compareObject indexedOldObj, indexedNewObj, path, embededObjKeys, keyPath, true 96 | return if diffs.length then [type: changeset.op.UPDATE, key: getKey(path), embededKey: uniqKey, changes: diffs] else [] 97 | 98 | 99 | convertArrayToObj = (arr, uniqKey) -> 100 | obj = {} 101 | if uniqKey isnt '$index' 102 | obj = _keyBy arr, uniqKey 103 | else 104 | for index, value of arr then obj[index] = value 105 | return obj 106 | 107 | 108 | comparePrimitives = (oldObj, newObj, path) -> 109 | changes = [] 110 | if oldObj isnt newObj 111 | changes.push type: changeset.op.UPDATE, key: getKey(path), value: newObj, oldValue: oldObj 112 | return changes 113 | 114 | 115 | isEmbeddedKey = (key) -> /\$.*=/gi.test key 116 | 117 | 118 | removeKey = (obj, key, embededKey) -> 119 | if Array.isArray obj 120 | if embededKey isnt '$index' or !obj[key] 121 | index = indexOfItemInArray obj, embededKey, key 122 | obj.splice index ? key, 1 123 | else 124 | delete obj[key] 125 | 126 | 127 | indexOfItemInArray = (arr, key, value) -> 128 | for index, item of arr 129 | if key is '$index' 130 | if item is value then return index 131 | else if item[key] is value then return index 132 | 133 | return -1 134 | 135 | 136 | modifyKeyValue = (obj, key, value) -> obj[key] = value 137 | 138 | 139 | addKeyValue = (obj, key, value) -> 140 | if Array.isArray obj then obj.push value else obj[key] = value 141 | 142 | 143 | parseEmbeddedKeyValue = (key) -> 144 | uniqKey = key.substring 1, key.indexOf '=' 145 | value = key.substring key.indexOf('=') + 1 146 | return {uniqKey, value} 147 | 148 | 149 | applyLeafChange = (obj, change, embededKey) -> 150 | {type, key, value} = change 151 | switch type 152 | when changeset.op.ADD 153 | addKeyValue obj, key, value 154 | when changeset.op.UPDATE 155 | modifyKeyValue obj, key, value 156 | when changeset.op.REMOVE 157 | removeKey obj, key, embededKey 158 | 159 | 160 | applyArrayChange = (arr, change) -> 161 | for subchange in change.changes 162 | if subchange.value? or subchange.type is changeset.op.REMOVE 163 | applyLeafChange arr, subchange, change.embededKey 164 | else 165 | if change.embededKey is '$index' 166 | element = arr[+subchange.key] 167 | else 168 | element = _find arr, (el) -> el[change.embededKey] is subchange.key 169 | changeset.applyChanges element, subchange.changes 170 | 171 | 172 | applyBranchChange = (obj, change) -> 173 | if Array.isArray obj 174 | applyArrayChange obj, change 175 | else 176 | changeset.applyChanges obj, change.changes 177 | 178 | 179 | revertLeafChange = (obj, change, embededKey) -> 180 | {type, key, value, oldValue} = change 181 | switch type 182 | when changeset.op.ADD 183 | removeKey obj, key, embededKey 184 | when changeset.op.UPDATE 185 | modifyKeyValue obj, key, oldValue 186 | when changeset.op.REMOVE 187 | addKeyValue obj, key, value 188 | 189 | 190 | revertArrayChange = (arr, change) -> 191 | for subchange in change.changes 192 | if subchange.value? or subchange.type is changeset.op.REMOVE 193 | revertLeafChange arr, subchange, change.embededKey 194 | else 195 | if change.embededKey is '$index' 196 | element = arr[+subchange.key] 197 | else 198 | element = _find arr, (el) -> el[change.embededKey] is subchange.key 199 | changeset.revertChanges element, subchange.changes 200 | 201 | 202 | revertBranchChange = (obj, change) -> 203 | if Array.isArray obj 204 | revertArrayChange obj, change 205 | else 206 | changeset.revertChanges obj, change.changes 207 | 208 | 209 | changeset.diff = (oldObj, newObj, embededObjKeys) -> 210 | return compare oldObj, newObj, [], embededObjKeys, [] 211 | 212 | 213 | changeset.applyChanges = (obj, changesets) -> 214 | for change in changesets 215 | if !change.changes or change.value? or change.type is changeset.op.REMOVE 216 | applyLeafChange obj, change, change.embededKey 217 | else 218 | applyBranchChange obj[change.key], change 219 | 220 | 221 | changeset.revertChanges = (obj, changeset) -> 222 | for change in changeset.reverse() 223 | if !change.changes 224 | revertLeafChange obj, change 225 | else 226 | revertBranchChange obj[change.key], change 227 | 228 | 229 | changeset.op = 230 | REMOVE: 'remove' 231 | ADD: 'add' 232 | UPDATE: 'update' 233 | 234 | return 235 | )() 236 | -------------------------------------------------------------------------------- /test/changesets.coffee: -------------------------------------------------------------------------------- 1 | expect = require 'expect.js' 2 | util = require 'util' 3 | changesets = require '../src/changesets' 4 | {op} = changesets 5 | 6 | describe 'changesets', -> 7 | oldObj = newObj = changeset = changesetWithouEmbeddedKey = null 8 | 9 | beforeEach -> 10 | oldObj = 11 | name: 'joe' 12 | age: 55 13 | mixed: 10 14 | nested: inner: 1 15 | empty: undefined 16 | date: new Date 'October 13, 2014 11:13:00' 17 | coins: [2, 5] 18 | toys: ['car', 'doll', 'car'] 19 | pets: [undefined, null] 20 | children: [ 21 | {name: 'kid1', age: 1, subset: [ 22 | {id: 1, value: 'haha'} 23 | {id: 2, value: 'hehe'} 24 | ]} 25 | {name: 'kid2', age: 2} 26 | ] 27 | 28 | 29 | newObj = 30 | name: 'smith' 31 | mixed: '10' 32 | nested: inner: 2 33 | date: new Date 'October 13, 2014 11:13:00' 34 | coins: [2, 5, 1] 35 | toys: [] 36 | pets: [] 37 | children: [ 38 | {name: 'kid3', age: 3} 39 | {name: 'kid1', age: 0, subset: [ 40 | {id: 1, value: 'heihei'} 41 | ]} 42 | {name: 'kid2', age: 2} 43 | ] 44 | 45 | 46 | changeset = [ 47 | { type: 'update', key: 'name', value: 'smith', oldValue: 'joe' } 48 | { type: 'remove', key: 'mixed', value: 10 } 49 | { type: 'add', key: 'mixed', value: '10' } 50 | { type: 'update', key: 'nested', changes: [ 51 | { type: 'update', key: 'inner', value: 2, oldValue: 1} 52 | ] 53 | } 54 | { type: 'update', key: 'coins', embededKey: '$index', changes: [{ type: 'add', key: '2', value: 1 } ] } 55 | { type: 'update', key: 'toys', embededKey: '$index', changes: [ 56 | { type: 'remove', key: '0', value: 'car' } 57 | { type: 'remove', key: '1', value: 'doll' } 58 | { type: 'remove', key: '2', value: 'car' } 59 | ] 60 | } 61 | { type: 'update', key: 'pets', embededKey: '$index', changes: [ 62 | { type: 'remove', key: '0', value: undefined } 63 | { type: 'remove', key: '1', value: null } 64 | ] 65 | } 66 | { type: 'update', key: 'children', embededKey: 'name', changes: [ 67 | { type: 'update', key: 'kid1', changes: [ 68 | { type: 'update', key: 'age', value: 0, oldValue: 1 } 69 | { type: 'update', key: 'subset', embededKey: 'id', changes: [ 70 | { type: 'update', key: 1, changes: [{ type: 'update', key: 'value', value: 'heihei', oldValue: 'haha' } ] } 71 | { type: 'remove', key: 2, value: {id: 2, value: 'hehe'} } 72 | ] 73 | } 74 | ]} 75 | { type: 'add', key: 'kid3', value: { name: 'kid3', age: 3 } } 76 | ] 77 | } 78 | 79 | { type: 'remove', key: 'age', value: 55 } 80 | { type: 'remove', key: 'empty', value: undefined } 81 | ] 82 | 83 | changesetWithoutEmbeddedKey = [ 84 | { type: 'update', key: 'name', value: 'smith', oldValue: 'joe' } 85 | { type: 'remove', key: 'mixed', value: 10 } 86 | { type: 'add', key: 'mixed', value: '10' } 87 | { type: 'update', key: 'nested', changes: [ 88 | { type: 'update', key: 'inner', value: 2, oldValue: 1} 89 | ] 90 | } 91 | { type: 'update', key: 'coins', embededKey: '$index', changes: [ { type: 'add', key: '2', value: 1 } ] } 92 | { type: 'update', key: 'toys', embededKey: '$index', changes: [ 93 | { type: 'remove', key: '0', value: 'car' } 94 | { type: 'remove', key: '1', value: 'doll' } 95 | { type: 'remove', key: '2', value: 'car' } 96 | ] 97 | } 98 | { type: 'update', key: 'pets', embededKey: '$index', changes: [ 99 | { type: 'remove', key: '0', value: undefined } 100 | { type: 'remove', key: '1', value: null } 101 | ] 102 | } 103 | { type: 'update', key: 'children', embededKey: '$index', changes: [ 104 | { 105 | type: 'update', key: '0', changes: [ 106 | { type: 'update', key: 'name', value: 'kid3', oldValue: 'kid1' } 107 | { type: 'update', key: 'age', value: 3, oldValue: 1 } 108 | { type: 'remove', key: 'subset', value: [ { id: 1, value: 'haha' }, { id: 2, value: 'hehe' } ]} 109 | ] 110 | } 111 | { 112 | type: 'update', key: '1', changes: [ 113 | { type: 'update', key: 'name', value: 'kid1', oldValue: 'kid2' } 114 | { type: 'update', key: 'age', value: 0, oldValue: 2 } 115 | { type: 'add', key: 'subset', value: [ { id: 1, value: 'heihei' } ] } 116 | ] 117 | }, 118 | { type: 'add', key: '2', value: { name: 'kid2', age: 2 } } 119 | ] 120 | } 121 | 122 | { type: 'remove', key: 'age', value: 55 } 123 | { type: 'remove', key: 'empty', value: undefined } 124 | ] 125 | 126 | 127 | describe 'diff()', -> 128 | 129 | it 'should return correct diff for object with embedded array object that does not have key specified', -> 130 | diffs = changesets.diff oldObj, newObj 131 | expect(diffs).to.eql changesetWithoutEmbeddedKey 132 | 133 | it 'should return correct diff for object with embedded array that has key specified', -> 134 | diffs = changesets.diff oldObj, newObj, {'children': 'name', 'children.subset': 'id'} 135 | expect(diffs).to.eql changeset 136 | 137 | 138 | describe 'applyChanges()', -> 139 | 140 | it 'should transfer oldObj to newObj with changeset', -> 141 | changesets.applyChanges oldObj, changeset 142 | newObj.children.sort (a, b) -> a.name > b.name 143 | expect(oldObj).to.eql newObj 144 | 145 | it 'should transfer oldObj to newObj with changesetWithoutEmbeddedKey', -> 146 | changesets.applyChanges oldObj, changesetWithoutEmbeddedKey 147 | newObj.children.sort (a, b) -> a.name > b.name 148 | oldObj.children.sort (a, b) -> a.name > b.name 149 | expect(oldObj).to.eql newObj 150 | 151 | 152 | describe 'revertChanges()', -> 153 | 154 | it 'should transfer newObj to oldObj with changeset', -> 155 | changesets.revertChanges newObj, changeset 156 | oldObj.children.sort (a, b) -> a.name > b.name 157 | newObj.children.sort (a, b) -> a.name > b.name 158 | expect(newObj).to.eql oldObj 159 | 160 | 161 | it 'should transfer newObj to oldObj with changesetWithoutEmbeddedKey', -> 162 | changesets.revertChanges newObj, changesetWithoutEmbeddedKey 163 | oldObj.children.sort (a, b) -> a.name > b.name 164 | newObj.children.sort (a, b) -> a.name > b.name 165 | expect(newObj).to.eql oldObj 166 | --------------------------------------------------------------------------------