├── .gitignore ├── LICENSE ├── README.md ├── dependencies ├── genericDiff ├── myersDiff ├── structDiff ├── structPatch └── typeFunctions ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | 3 | /node_modules 4 | 5 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![npm](https://img.shields.io/npm/v/beholdjs?style=flat-square) 2 | 3 | 4 | # Doc Diff Patch 5 | 6 | Finding the complete diff between two JSONs and recreation of the original JSON from the current version of the JSON and previously calculated diff for version management. 7 | 8 | # Installation 9 | 10 | npm install doc-diff-patch 11 | 12 | # Usage 13 | 14 | Initializing 15 | 16 | ``` 17 | var vm = require('doc-diff-patch') 18 | const previousJson = { 19 | a: "How are you?", 20 | b: [1,2,3,4] 21 | }; 22 | const updatedJson = { 23 | a: "Where are you?", 24 | b: [1,2,3,4,5] 25 | }; 26 | ``` 27 | 28 | Finding diff 29 | 30 | ``` 31 | const diff = vm.structDiff(previousJson, updatedJson); 32 | console.log(JSON.stringify(diff)) 33 | /* 34 | Output: 35 | { 36 | "a": [ 37 | {"action": "ins", "pos": 0, "val": "Where"}, 38 | {"action": "del", "pos": 0, "val": "How"} 39 | ], 40 | "b": [ 41 | {"action": "ins", "pos": 4, "val": [5]} 42 | ] 43 | } 44 | */ 45 | ``` 46 | 47 | Finding diff with filter 48 | 49 | ``` 50 | const filter = {b: 1}; 51 | const diff = vm.structDiff(previousJson, updatedJson); 52 | console.log(JSON.stringify(diff)) 53 | /* 54 | Output: 55 | { 56 | "b": [ 57 | {"action": "ins", "pos": 4, "val": [5]} 58 | ] 59 | } 60 | */ 61 | ``` 62 | 63 | Finding original json from updated json and full diff 64 | 65 | ``` 66 | console.log(JSON.stringify(vm.patchDiff(updatedJson, diff))) 67 | /* 68 | Output: 69 | { 70 | "val": { 71 | "a": "How are you?", 72 | "b": [1,2,3,4] 73 | }, 74 | "err": "" 75 | } 76 | */ 77 | ``` 78 | -------------------------------------------------------------------------------- /dependencies/genericDiff: -------------------------------------------------------------------------------- 1 | const myersDiff = require('./myersDiff'); 2 | const typeFunctions = require('./typeFunctions'); 3 | 4 | const ActionValueCompleteInsertion = "fullIns"; 5 | const ActionValueCompleteDeletion = "fullDel"; 6 | const ActionValueInsertion = "ins"; 7 | const ActionValueDeletion = "del"; 8 | 9 | const ErrorDiffInvalid = "diff data invalid"; 10 | 11 | // change structure of a string or array 12 | function changeStruct(action, pos, val) { 13 | return { 14 | action: action || '', 15 | pos: pos || 0, 16 | val: val || '' 17 | }; 18 | } 19 | 20 | // return structure of mayersPatch function 21 | function patchStruct(val, error) { 22 | return { 23 | originalVal: val, 24 | error: error 25 | }; 26 | } 27 | 28 | // get full change between two arrays using myers diff algo in linear space 29 | function mayersDiff(originalVar, changedVar) { 30 | var myersDiffInstance = new myersDiff(originalVar, changedVar); 31 | // find the diff path 32 | const fullDiffPath = myersDiffInstance.diffPath(); 33 | const allChanges = []; 34 | // change the path into changeStruct object 35 | for (let i = 0; i < fullDiffPath.length; i++) { 36 | const path = fullDiffPath[i]; 37 | if (path.from.x === path.to.x) { 38 | allChanges.push( 39 | changeStruct(ActionValueInsertion, path.from.y, changedVar[path.from.y]) 40 | ); 41 | } else if (path.from.y === path.to.y) { 42 | allChanges.push( 43 | changeStruct(ActionValueDeletion, path.from.x, originalVar[path.from.x]) 44 | ); 45 | } 46 | } 47 | return allChanges; 48 | } 49 | 50 | // get the original value from recent version and the diff between the two calculated from mayersDiff function 51 | function mayersPatch(changedVal, changes, isString) { 52 | // error if changes is empty 53 | if (!(changes && changes.length)) { 54 | return patchStruct(changedVal, ''); 55 | } 56 | // convert string into array of strings split by space 57 | if (isString) { 58 | changedVal = changedVal.split(" "); 59 | } 60 | let numberOfInsertions = 0; 61 | let numberOfDeletions = 0; 62 | const insertionsPositionMap = {}; 63 | for (let i = 0; i < changes.length; i++) { 64 | const change = changes[i]; 65 | if (change.action === ActionValueInsertion) { 66 | numberOfInsertions++; 67 | insertionsPositionMap[change.pos] = true; 68 | // todo : add compatibility for dates in typeFunctions.compareValues 69 | // if (change.pos >= changedVal.length || !typeFunctions.compareValues(changedVal[change.pos], change.val)) { 70 | // error if any change pos for insertion is greater than the size of changedVal 71 | if (change.pos >= changedVal.length) { 72 | return patchStruct([], ErrorDiffInvalid); 73 | } 74 | } else { 75 | numberOfDeletions++; 76 | } 77 | } 78 | // finding original values length 79 | const originalValLen = changedVal.length - numberOfInsertions + numberOfDeletions; 80 | let originalVal = new Array(originalValLen); 81 | for (let i = 0; i < changes.length; i++) { 82 | const change = changes[i]; 83 | if (change.action === ActionValueDeletion) { 84 | // error if any change pos for deletion is greater than the size of originalVal 85 | if (change.pos >= originalValLen) { 86 | return patchStruct([], ErrorDiffInvalid); 87 | } 88 | // if deletion took place originally than the originalVal will have the same values stored in diff at this position 89 | originalVal[change.pos] = change.val; 90 | } 91 | } 92 | let originalValIndex = 0; 93 | for (let changedValIndex = 0; changedValIndex < changedVal.length; changedValIndex++) { 94 | // ignore if already values assigned for the position 95 | if (insertionsPositionMap[changedValIndex]) { 96 | continue; 97 | } 98 | while (originalVal[originalValIndex] !== undefined) { 99 | originalValIndex++; 100 | } 101 | originalVal[originalValIndex] = changedVal[changedValIndex]; 102 | originalValIndex++; 103 | } 104 | if (isString) { 105 | originalVal = originalVal.join(" "); 106 | } 107 | return patchStruct(originalVal, ''); 108 | } 109 | 110 | // consolidate diffs with consecutive insertions or deletions to reduce size of changes array 111 | function consolidateDiff(changes, isString) { 112 | const consolidatedChanges = []; 113 | for (let i = 0; i < changes.length; i++) { 114 | const currentAction = changes[i].action; 115 | const initialPosition = changes[i].pos; 116 | let posIterator = changes[i].pos; 117 | const valChangesArr = [changes[i].val]; 118 | while (i < changes.length - 1 && currentAction === changes[i + 1].action && posIterator + 1 === changes[i + 1].pos) { 119 | valChangesArr.push(changes[i + 1].val); 120 | i++; 121 | posIterator++; 122 | } 123 | let finalValue = !isString ? valChangesArr : valChangesArr.join(" "); 124 | consolidatedChanges.push( 125 | changeStruct(currentAction, initialPosition, finalValue) 126 | ); 127 | } 128 | return consolidatedChanges; 129 | } 130 | 131 | // expand diffs consolidated by the function consolidateDiff 132 | function expandDiff(consolidatedChanges, isString) { 133 | const changes = []; 134 | for (let i = 0; i < consolidatedChanges.length; i++) { 135 | const currentAction = consolidatedChanges[i].action; 136 | const consolidatedVals = !isString ? consolidatedChanges[i].val : consolidatedChanges[i].val.split(" "); 137 | let startingPos = consolidatedChanges[i].pos; 138 | for (let j = 0; j < consolidatedVals.length; j++) { 139 | changes.push(changeStruct(currentAction, startingPos, consolidatedVals[j])); 140 | startingPos++; 141 | } 142 | } 143 | return changes; 144 | } 145 | 146 | // main function to call to find the diff between 2 values 147 | exports.getDiff = function (originalVar, changedVar) { 148 | // if originalVal is null then complete insertion is taking place 149 | if (originalVar === undefined) { 150 | return [ 151 | changeStruct(ActionValueCompleteInsertion, 0, "") 152 | ]; 153 | } 154 | // if changedVal is null or the types of changedVal and originalVal doesn't match then complete change of value can be assumed 155 | if (changedVar === undefined || !typeFunctions.compareTypes(originalVar, changedVar)) { 156 | return [ 157 | changeStruct(ActionValueCompleteDeletion, 0, originalVar) 158 | ]; 159 | } 160 | // types of val currently supported is string or array for mayers diff else complete change of value is assumed 161 | if (typeFunctions.isArray(originalVar)) { 162 | return consolidateDiff(mayersDiff(originalVar, changedVar), false); 163 | } 164 | if (typeFunctions.isString(originalVar)) { 165 | return consolidateDiff(mayersDiff(originalVar.split(" "), changedVar.split(" ")), true); 166 | } 167 | return [ 168 | changeStruct(ActionValueCompleteDeletion, 0, originalVar) 169 | ]; 170 | }; 171 | 172 | // main function to call to find the originalVal from changedVal and diff 173 | exports.patchDiff = function (changedVal, changes) { 174 | // error if changes is empty 175 | if (!(changes && changes.length)) { 176 | return patchStruct([], ErrorDiffInvalid); 177 | } 178 | // error if any object in changes array doesn't have the keys action, pos or val 179 | for (let i = 0; i < changes.length; i++) { 180 | if (!(changes[i].hasOwnProperty('action') && changes[i].hasOwnProperty('pos') && changes[i].hasOwnProperty('val'))) { 181 | return patchStruct([], ErrorDiffInvalid); 182 | } 183 | } 184 | // if complete deletion had taken place 185 | if (changes[0].action === ActionValueCompleteDeletion) { 186 | return patchStruct(changes[0].val, ''); 187 | } 188 | // if complete insertion had taken place 189 | if (changes[0].action === ActionValueCompleteInsertion) { 190 | return patchStruct(undefined, ''); 191 | } 192 | if (typeFunctions.isArray(changes[0].val)) { 193 | if (!changedVal) { 194 | changedVal = []; 195 | } 196 | return mayersPatch(changedVal, expandDiff(changes, false), false); 197 | } 198 | if (typeFunctions.isString(changes[0].val)) { 199 | if (!changedVal) { 200 | changedVal = ''; 201 | } 202 | return mayersPatch(changedVal, expandDiff(changes, true), true); 203 | } 204 | return patchStruct([], ErrorDiffInvalid); 205 | }; 206 | 207 | exports.actionTypes = function () { 208 | return { 209 | fullInsertion: ActionValueCompleteInsertion, 210 | fullDeletion: ActionValueCompleteDeletion, 211 | partialInsertion: ActionValueInsertion, 212 | partialDeletion: ActionValueDeletion, 213 | } 214 | }; -------------------------------------------------------------------------------- /dependencies/myersDiff: -------------------------------------------------------------------------------- 1 | /* 2 | Reference : https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1 3 | */ 4 | const typeFunctions = require('./typeFunctions'); 5 | 6 | /** 7 | * Summary. Class to find the mayers diff using linear space 8 | */ 9 | const MyersDiff = (function() { 10 | // init 11 | function MyersDiff(originalVal, changedVal) { 12 | this._originalVal = originalVal; 13 | this._changedVal = changedVal; 14 | } 15 | 16 | // box structure 17 | function boxStruct(left, top, right, bottom) { 18 | return { 19 | left: left || 0, 20 | top: top || 0, 21 | right: right || 0, 22 | bottom: bottom || 0 23 | }; 24 | } 25 | // xy structure 26 | function xyStruct(x, y) { 27 | return { 28 | x: x || 0, 29 | y: y || 0 30 | }; 31 | } 32 | // snake structure 33 | function snakeStruct(from, to) { 34 | return { 35 | from: from || xyStruct(), 36 | to: to || xyStruct() 37 | } 38 | } 39 | 40 | // function the find the snake going down from top left corner of the box 41 | MyersDiff.prototype._forwards = function (box, vf, vb, depth, maxDepth) { 42 | let px, x, py, y; 43 | const boxDelta = box.right - box.left - (box.bottom - box.top); 44 | for (let k = -depth + maxDepth; k <= depth+maxDepth; k += 2) { 45 | const c = k - boxDelta; 46 | if (k === -depth+maxDepth || (k !== depth+maxDepth && vf[k-1] < vf[k+1])) { 47 | px = vf[k+1]; 48 | x = vf[k+1]; 49 | } else { 50 | px = vf[k-1]; 51 | x = px + 1; 52 | } 53 | y = box.top + (x - box.left) - (k - maxDepth); 54 | if (depth === 0 || x !== px) { 55 | py = y; 56 | } else { 57 | py = y - 1; 58 | } 59 | while (x < box.right && y < box.bottom && typeFunctions.compareValues(this._originalVal[x], this._changedVal[y])) { 60 | x++; 61 | y++; 62 | } 63 | vf[k] = x; 64 | if ((Math.abs(boxDelta) % 2 === 1) && c >= -(depth-1)+maxDepth && c <= maxDepth+depth-1 && y >= vb[c]) { 65 | return snakeStruct( 66 | xyStruct(px, py), 67 | xyStruct(x, y) 68 | ); 69 | } 70 | } 71 | return undefined; 72 | }; 73 | // function the find the snake going up from bottom right corner of the box 74 | MyersDiff.prototype._backwards = function (box, vf, vb, depth, maxDepth) { 75 | let px, x, py, y; 76 | const boxDelta = box.right - box.left - (box.bottom - box.top); 77 | for (let c = -depth + maxDepth; c <= depth+maxDepth; c += 2) { 78 | const k = c + boxDelta; 79 | if (c === -depth+maxDepth || (c !== depth+maxDepth && vb[c-1] > vb[c+1])) { 80 | py = vb[c+1]; 81 | y = vb[c+1]; 82 | } else { 83 | py = vb[c-1]; 84 | y = py - 1; 85 | } 86 | x = box.left + (y - box.top) + (k - maxDepth); 87 | if (depth === 0 || y !== py) { 88 | px = x; 89 | } else { 90 | px = x + 1; 91 | } 92 | while (x > box.left && y > box.top && typeFunctions.compareValues(this._originalVal[x-1], this._changedVal[y-1])) { 93 | x = x-1; 94 | y = y-1; 95 | } 96 | vb[c] = y; 97 | if ((Math.abs(boxDelta) % 2 === 0) && k >= -depth+maxDepth && k <= depth+maxDepth && x <= vf[k]) { 98 | return snakeStruct( 99 | xyStruct(x, y), 100 | xyStruct(px, py) 101 | ); 102 | } 103 | } 104 | return undefined; 105 | }; 106 | 107 | // function to finc the middle snake in a box 108 | MyersDiff.prototype._midpoint = function (box) { 109 | const boxSize = box.right - box.left + box.bottom - box.top; 110 | if (boxSize === 0) { 111 | return undefined; 112 | } 113 | const max = Math.ceil(boxSize/2); 114 | 115 | const vf = new Array(2*max+1); 116 | vf[max+1] = box.left; 117 | const vb = new Array(2*max+1); 118 | vb[max+1] = box.bottom; 119 | // todo: can optimize better to have more insertions and deletions together 120 | for (let depth = 0; depth <= max; depth++) { 121 | let snake = this._forwards(box, vf, vb, depth, max); 122 | if (snake) { 123 | return snake; 124 | } 125 | snake = this._backwards(box, vf, vb, depth, max); 126 | if (snake) { 127 | return snake; 128 | } 129 | } 130 | return undefined; 131 | }; 132 | 133 | // find all the snakes using linear space 134 | MyersDiff.prototype._findPath = function (left, top, right, bottom) { 135 | const box = boxStruct(left, top, right, bottom); 136 | const snake = this._midpoint(box); 137 | if (!snake) { 138 | return undefined; 139 | } 140 | 141 | const head = this._findPath(box.left, box.top, snake.from.x, snake.from.y); 142 | const tail = this._findPath(snake.to.x, snake.to.y, box.right, box.bottom); 143 | 144 | let allSnakes = []; 145 | if (!head) { 146 | allSnakes.push(snake.from); 147 | } else { 148 | allSnakes.push(...head); 149 | } 150 | if (!tail) { 151 | allSnakes.push(snake.to); 152 | } else { 153 | allSnakes.push(...tail); 154 | } 155 | return allSnakes 156 | }; 157 | 158 | MyersDiff.prototype._walkDiagonal = function (start, end, fullPath) { 159 | while(start.x < end.x && start.y < end.y && typeFunctions.compareValues(this._originalVal[start.x], this._changedVal[start.y])) { 160 | fullPath.push( 161 | snakeStruct( 162 | xyStruct(start.x, start.y), 163 | xyStruct(start.x + 1, start.y + 1) 164 | ) 165 | ); 166 | start.x++; 167 | start.y++; 168 | } 169 | return start; 170 | }; 171 | 172 | // finding the complete diff path using all the snakes 173 | MyersDiff.prototype._walkSnakes = function () { 174 | const path = this._findPath(0, 0, this._originalVal.length, this._changedVal.length); 175 | const fullPath = []; 176 | if (!path) { 177 | return fullPath; 178 | } 179 | for (let it = 0; it < path.length - 1; it++) { 180 | let start = path[it]; 181 | let end = path[it + 1]; 182 | start = this._walkDiagonal(start, end, fullPath); 183 | if ((end.x - start.x) < (end.y - start.y)) { 184 | fullPath.push( 185 | snakeStruct( 186 | xyStruct(start.x, start.y), 187 | xyStruct(start.x, start.y + 1) 188 | ) 189 | ); 190 | start.y++; 191 | } else if ((end.x - start.x) > (end.y - start.y)) { 192 | fullPath.push( 193 | snakeStruct( 194 | xyStruct(start.x, start.y), 195 | xyStruct(start.x + 1, start.y) 196 | ) 197 | ); 198 | start.x++; 199 | } 200 | start = this._walkDiagonal(start, end, fullPath); 201 | } 202 | return fullPath; 203 | }; 204 | 205 | // main function to call for finding diff 206 | MyersDiff.prototype.diffPath = function() { 207 | return this._walkSnakes(); 208 | }; 209 | 210 | return MyersDiff; 211 | })(); 212 | 213 | module.exports = MyersDiff; -------------------------------------------------------------------------------- /dependencies/structDiff: -------------------------------------------------------------------------------- 1 | const typeFunctions = require('./typeFunctions'); 2 | const genericDiff = require('./genericDiff'); 3 | 4 | /** 5 | * Summary. Class to find the diff between 2 objects. 6 | */ 7 | var StructDiff = (function() { 8 | // init 9 | function StructDiff(originalStruct, changedStruct, filter) { 10 | this._originalStruct = originalStruct; 11 | this._changedStruct = changedStruct; 12 | this._filter = filter || {}; 13 | } 14 | 15 | // recursive function to find the diff 16 | function getChangedFields(originalStruct, changedStruct, filter) { 17 | const change = {}; 18 | let isFilterApplied = false; 19 | // check if filter is to be applied 20 | if (filter && typeFunctions.isObject(filter) && !typeFunctions.isEmpty(filter)) { 21 | isFilterApplied = true; 22 | } 23 | if (!(filter && typeFunctions.isObject(filter))) { 24 | filter = {}; 25 | } 26 | // iterate through all the keys of the originalStruct 27 | for (const key in originalStruct) { 28 | if (originalStruct.hasOwnProperty(key) && 29 | ( 30 | !isFilterApplied || 31 | (isFilterApplied && filter.hasOwnProperty(key) && filter[key]) 32 | ) 33 | ) { 34 | // if changed struct doesn't have keys present in original struct 35 | if (!changedStruct.hasOwnProperty(key)) { 36 | change[key] = genericDiff.getDiff(originalStruct[key], undefined); 37 | continue; 38 | } 39 | // if the key is of type Object: call the getChangedFields recursively 40 | if (typeFunctions.isObject(originalStruct[key])) { 41 | const internalChanges = getChangedFields(originalStruct[key], changedStruct[key], filter[key]); 42 | if (!typeFunctions.isEmpty(internalChanges)) { 43 | change[key] = internalChanges; 44 | } 45 | continue; 46 | } 47 | // if the key is of type Array of Objects: 48 | // separate each object in the array into individual keys with naming - "key.0", "key.1", etc 49 | if (typeFunctions.isArray(originalStruct[key]) && 50 | typeFunctions.isArray(changedStruct[key]) && 51 | ( 52 | (originalStruct[key].length > 0 && typeFunctions.isObject(originalStruct[key][0])) || 53 | (changedStruct[key].length > 0 && typeFunctions.isObject(changedStruct[key][0])) 54 | ) 55 | ) { 56 | for (let it = 0; it < originalStruct[key].length; it++) { 57 | if (changedStruct[key].length > it) { 58 | const objKey = key + "." + it; 59 | if (!typeFunctions.compareValues(originalStruct[key][it], changedStruct[key][it])) { 60 | change[objKey] = getChangedFields(originalStruct[key][it], changedStruct[key][it], {}); 61 | } 62 | } 63 | } 64 | if (changedStruct[key].length > originalStruct[key].length) { 65 | for (let it = originalStruct[key].length; it < changedStruct[key].length; it++) { 66 | const objKey = key + "." + it; 67 | change[objKey] = genericDiff.getDiff(undefined, changedStruct[key][it]); 68 | } 69 | } else if (changedStruct[key].length < originalStruct[key].length) { 70 | for (let it = changedStruct[key].length; it < originalStruct[key].length; it++) { 71 | const objKey = key + "." + it; 72 | change[objKey] = genericDiff.getDiff(originalStruct[key][it], undefined); 73 | } 74 | } 75 | continue; 76 | } 77 | // if the key for other types get diff 78 | if (!typeFunctions.compareValues(originalStruct[key], changedStruct[key])) { 79 | change[key] = genericDiff.getDiff(originalStruct[key], changedStruct[key]); 80 | } 81 | } 82 | } 83 | // iterate through all the keys of the changedStruct not present in originalStruct 84 | for (const key in changedStruct) { 85 | if (changedStruct.hasOwnProperty(key) && 86 | ( 87 | !isFilterApplied || 88 | (isFilterApplied && filter.hasOwnProperty(key) && filter[key]) 89 | ) 90 | ) { 91 | if (!originalStruct.hasOwnProperty(key)) { 92 | change[key] = genericDiff.getDiff(undefined, changedStruct[key]); 93 | } 94 | } 95 | } 96 | return change; 97 | } 98 | 99 | // main function called from here 100 | StructDiff.prototype.getStructDiff = function() { 101 | if (!typeFunctions.isObject(this._originalStruct) || !typeFunctions.isObject(this._changedStruct)) { 102 | return {}; 103 | } 104 | return getChangedFields(this._originalStruct, this._changedStruct, this._filter); 105 | }; 106 | 107 | return StructDiff; 108 | })(); 109 | 110 | module.exports = StructDiff; -------------------------------------------------------------------------------- /dependencies/structPatch: -------------------------------------------------------------------------------- 1 | const typeFunctions = require('./typeFunctions'); 2 | const genericDiff = require('./genericDiff'); 3 | 4 | const ErrorDiffInvalid = "diff data invalid"; 5 | const ErrorChangedStructInvalid = "new version of the file invalid"; 6 | 7 | /** 8 | * Summary. Class to find the original object from changed json and diff 9 | */ 10 | var StructPatch = (function() { 11 | // init 12 | function StructPatch(changedStruct, changes) { 13 | this._changedStruct = changedStruct; 14 | this._changes = changes; 15 | } 16 | 17 | // return structure - val : the original object, err : error if any 18 | function patchStruct (val, err) { 19 | return { 20 | val: val || {}, 21 | err: err || '' 22 | } 23 | } 24 | 25 | // recursive function to find whole of the original object 26 | function getStructDiffPatch(changedStruct, changes) { 27 | // copy changed object 28 | const originalStruct = JSON.parse(JSON.stringify(changedStruct)); 29 | // iterate through all the keys of originalStruct and find which are of the type array of objects 30 | // change these keys into the format "keys.0", "keys.1", etc 31 | for (const key in originalStruct) { 32 | if (originalStruct.hasOwnProperty(key)) { 33 | if (typeFunctions.isArray(originalStruct[key]) && 34 | originalStruct[key].length > 0 && 35 | typeFunctions.isObject(originalStruct[key][0])) { 36 | // do not convert if fullIns 37 | if ( 38 | !( 39 | changes.hasOwnProperty(key) && 40 | typeFunctions.isArray(changes[key]) && 41 | changes[key].length === 1 && 42 | changes[key][0].action === genericDiff.actionTypes().fullInsertion 43 | ) 44 | ) { 45 | for (let it = 0; it < originalStruct[key].length; it++) { 46 | const objKey = key + "." + it; 47 | originalStruct[objKey] = originalStruct[key][it] 48 | } 49 | delete originalStruct[key]; 50 | } 51 | } 52 | } 53 | } 54 | // iterate through all the keys of diff object 55 | for (const key in changes) { 56 | if (changes.hasOwnProperty(key)) { 57 | // if key is of the type Object: recursively call getStructDiffPatch of getting child data 58 | if (typeFunctions.isObject(changes[key])) { 59 | if (!(originalStruct.hasOwnProperty(key) && originalStruct[key])) { 60 | originalStruct[key] = {}; 61 | } 62 | const childObjPatch = getStructDiffPatch(originalStruct[key], changes[key]); 63 | if (childObjPatch.err) { 64 | return patchStruct({}, childObjPatch.err); 65 | } 66 | originalStruct[key] = childObjPatch.val; 67 | continue; 68 | } 69 | // values of keys in diff obejct cannot be anything other than Object or Array 70 | if (!typeFunctions.isArray(changes[key])) { 71 | return patchStruct({}, ErrorDiffInvalid); 72 | } 73 | // find the original key value 74 | const childPatch = genericDiff.patchDiff(originalStruct[key], changes[key]); 75 | if (childPatch.error) { 76 | return patchStruct({}, childPatch.error); 77 | } 78 | 79 | if (childPatch.originalVal === undefined) { 80 | delete originalStruct[key]; 81 | } else { 82 | originalStruct[key] = childPatch.originalVal; 83 | } 84 | } 85 | } 86 | // the keys of originalStruct which were of the type array of objects 87 | // after finding original values of individual objects inside the array 88 | // patch it back into a array of objects 89 | const arrayKeysData = {}; 90 | const arrayFieldRegex = /\.[0-9]/; 91 | for (const key in originalStruct) { 92 | if (originalStruct.hasOwnProperty(key)) { 93 | const index = key.search(arrayFieldRegex); 94 | if (index !== -1) { 95 | const originalKey = key.substr(0, index); 96 | if (!arrayKeysData.hasOwnProperty(originalKey)) { 97 | arrayKeysData[originalKey] = {}; 98 | } 99 | const originalKeyIndex = key.substr(index + 1); 100 | arrayKeysData[originalKey][originalKeyIndex] = originalStruct[key]; 101 | delete originalStruct[key]; 102 | } 103 | } 104 | } 105 | for (const key in arrayKeysData) { 106 | let it = 0; 107 | originalStruct[key] = []; 108 | while (arrayKeysData[key].hasOwnProperty(it.toString())) { 109 | originalStruct[key].push(arrayKeysData[key][it.toString()]); 110 | it++; 111 | } 112 | } 113 | return patchStruct(originalStruct, ''); 114 | } 115 | 116 | // main function called from here 117 | StructPatch.prototype.getStructPatch = function() { 118 | if (!typeFunctions.isObject(this._changedStruct)) { 119 | return patchStruct({}, ErrorChangedStructInvalid); 120 | } 121 | return getStructDiffPatch(this._changedStruct, this._changes); 122 | }; 123 | 124 | return StructPatch; 125 | })(); 126 | 127 | module.exports = StructPatch; -------------------------------------------------------------------------------- /dependencies/typeFunctions: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const TypeOfObject = "object"; 4 | const TypeOfString = "string"; 5 | 6 | // function to check if obj is of type Object 7 | exports.isObject = function(obj) { 8 | return _.isObject(obj) && !_.isArray(obj); 9 | }; 10 | 11 | // function to check if arr is of type Array 12 | exports.isArray = function(arr) { 13 | return _.isArray(arr) 14 | }; 15 | 16 | // function to check if str is of type String 17 | exports.isString = function (str) { 18 | return _.isString(str); 19 | }; 20 | 21 | // function to check if var1 and var2 are of the same type 22 | exports.compareTypes = function(var1, var2) { 23 | if (typeof(var1) !== TypeOfObject) { 24 | if (typeof(var1) === typeof(var2)) { 25 | return true; 26 | } 27 | } else { 28 | const isVar1Array = this.isArray(var1); 29 | const isVar2Array = this.isArray(var2); 30 | if ((isVar1Array && isVar2Array) || (!isVar1Array && !isVar2Array)) { 31 | return true; 32 | } 33 | } 34 | return false; 35 | }; 36 | 37 | // function to check if obj is empty 38 | exports.isEmpty = function(obj) { 39 | return _.isEmpty(obj); 40 | }; 41 | 42 | // function to check if var1 has same data values as compared to var2 43 | exports.compareValues = function (var1, var2) { 44 | return _.isEqual(var1, var2); 45 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const structDiff = require('./dependencies/structDiff'); 2 | const structPatch = require('./dependencies/structPatch'); 3 | 4 | /** 5 | * Summary. Function to find the difference between 2 objects 6 | * 7 | * @param {Object} originalStruct Original object. 8 | * @param {Object} changedStruct Object with changed values. 9 | * @param {Object} [filter] Object which defines which keys diff is required. 10 | * 11 | * @return {Object} Diff of originalStruct and changedStruct. 12 | */ 13 | exports.structDiff = function(originalStruct, changedStruct, filter) { 14 | const diff = new structDiff(originalStruct, changedStruct, filter); 15 | return diff.getStructDiff(); 16 | }; 17 | 18 | /** 19 | * Summary. Function to get the original object from a already existing diff 20 | * 21 | * @param {Object} changedStruct Recent version of the object. 22 | * @param {Object} changes Diff between the current version and previous version. 23 | * 24 | * @return {{val: Object, err: string}} The previous version and error if any. 25 | */ 26 | exports.patchDiff = function(changedStruct, changes) { 27 | const diff = new structPatch(changedStruct, changes); 28 | return diff.getStructPatch(); 29 | }; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doc-diff-patch", 3 | "version": "1.0.3", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "lodash": { 8 | "version": "4.17.20", 9 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 10 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doc-diff-patch", 3 | "version": "1.0.4", 4 | "description": "Version Version management for structures", 5 | "main": "index.js", 6 | "keywords": [ 7 | "diff", 8 | "jsdiff", 9 | "jsondiff", 10 | "compare", 11 | "patch", 12 | "text", 13 | "json", 14 | "javascript" 15 | ], 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/testbook/doc-diff-patch.git" 22 | }, 23 | "author": "Parv Tiwari", 24 | "license": "Unlicense", 25 | "bugs": { 26 | "url": "https://github.com/testbook/doc-diff-patch/issues" 27 | }, 28 | "homepage": "https://github.com/testbook/doc-diff-patch#readme", 29 | "dependencies": { 30 | "lodash": "^4.17.20" 31 | }, 32 | "devDependencies": {} 33 | } 34 | --------------------------------------------------------------------------------