UNPKG

@audc/json-diff-ts

Version:

A diff tool for JavaScript based on https://www.npmjs.com/package/diff-json written in TypeScript.

441 lines (440 loc) 16.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.unflattenChanges = exports.flattenChangeset = exports.Operation = exports.revertChangeset = exports.applyChangeset = exports.diff = exports.getTypeOfObj = void 0; const lodash_1 = require("lodash"); const getTypeOfObj = (obj) => { if (typeof obj === 'undefined') { return 'undefined'; } if (obj === null) { return null; } return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1]; }; exports.getTypeOfObj = getTypeOfObj; const getKey = (path) => { const left = path[path.length - 1]; return left != null ? left : '$root'; }; const compare = (oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip) => { let changes = []; const typeOfOldObj = (0, exports.getTypeOfObj)(oldObj); const typeOfNewObj = (0, exports.getTypeOfObj)(newObj); // if type of object changes, consider it as old obj has been deleted and a new object has been added if (typeOfOldObj !== typeOfNewObj) { changes.push({ type: Operation.REMOVE, key: getKey(path), value: oldObj }); changes.push({ type: Operation.ADD, key: getKey(path), value: newObj }); return changes; } switch (typeOfOldObj) { case 'Date': changes = changes.concat(comparePrimitives(oldObj.getTime(), newObj.getTime(), path).map(x => (Object.assign(Object.assign({}, x), { value: new Date(x.value), oldValue: new Date(x.oldValue) })))); break; case 'Object': const diffs = compareObject(oldObj, newObj, path, embeddedObjKeys, keyPath, false, keysToSkip); if (diffs.length) { if (path.length) { changes.push({ type: Operation.UPDATE, key: getKey(path), changes: diffs }); } else { changes = changes.concat(diffs); } } break; case 'Array': changes = changes.concat(compareArray(oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip)); break; case 'Function': break; // do nothing default: changes = changes.concat(comparePrimitives(oldObj, newObj, path)); } return changes; }; const compareObject = (oldObj, newObj, path, embeddedObjKeys, keyPath, skipPath = false, keysToSkip = []) => { let k; let newKeyPath; let newPath; if (skipPath == null) { skipPath = false; } let changes = []; const oldObjKeys = Object.keys(oldObj).filter((key) => keysToSkip.indexOf(key) === -1); const newObjKeys = Object.keys(newObj).filter((key) => keysToSkip.indexOf(key) === -1); const intersectionKeys = (0, lodash_1.intersection)(oldObjKeys, newObjKeys); for (k of intersectionKeys) { newPath = path.concat([k]); newKeyPath = skipPath ? keyPath : keyPath.concat([k]); const diffs = compare(oldObj[k], newObj[k], newPath, embeddedObjKeys, newKeyPath, keysToSkip); if (diffs.length) { changes = changes.concat(diffs); } } const addedKeys = (0, lodash_1.difference)(newObjKeys, oldObjKeys); for (k of addedKeys) { newPath = path.concat([k]); newKeyPath = skipPath ? keyPath : keyPath.concat([k]); changes.push({ type: Operation.ADD, key: getKey(newPath), value: newObj[k] }); } const deletedKeys = (0, lodash_1.difference)(oldObjKeys, newObjKeys); for (k of deletedKeys) { newPath = path.concat([k]); newKeyPath = skipPath ? keyPath : keyPath.concat([k]); changes.push({ type: Operation.REMOVE, key: getKey(newPath), value: oldObj[k] }); } return changes; }; const compareArray = (oldObj, newObj, path, embeddedObjKeys, keyPath, keysToSkip) => { const left = getObjectKey(embeddedObjKeys, keyPath); const uniqKey = left != null ? left : '$index'; const indexedOldObj = convertArrayToObj(oldObj, uniqKey); const indexedNewObj = convertArrayToObj(newObj, uniqKey); const diffs = compareObject(indexedOldObj, indexedNewObj, path, embeddedObjKeys, keyPath, true, keysToSkip); if (diffs.length) { return [ { type: Operation.UPDATE, key: getKey(path), embeddedKey: uniqKey, changes: diffs } ]; } else { return []; } }; const getObjectKey = (embeddedObjKeys, keyPath) => { if (embeddedObjKeys != null) { const path = keyPath.join('.'); const key = embeddedObjKeys[path]; if (key != null) { return key; } for (const regex in embeddedObjKeys) { if (path.match(new RegExp(regex))) { return embeddedObjKeys[regex]; } } } return undefined; }; const convertArrayToObj = (arr, uniqKey) => { let obj = {}; if (uniqKey !== '$index') { obj = (0, lodash_1.keyBy)(arr, uniqKey); } else { for (let i = 0; i < arr.length; i++) { const value = arr[i]; obj[i] = value; } } return obj; }; const comparePrimitives = (oldObj, newObj, path) => { const changes = []; if (oldObj !== newObj) { changes.push({ type: Operation.UPDATE, key: getKey(path), value: newObj, oldValue: oldObj }); } return changes; }; // const isEmbeddedKey = key => /\$.*=/gi.test(key) const removeKey = (obj, key, embeddedKey) => { if (Array.isArray(obj)) { if (embeddedKey === '$index') { obj.splice(key); return; } const index = indexOfItemInArray(obj, embeddedKey, key); if (index === -1) { // tslint:disable-next-line:no-console console.warn(`Element with the key '${embeddedKey}' and value '${key}' could not be found in the array'`); return; } return obj.splice(index != null ? index : key, 1); } else { obj[key] = undefined; delete obj[key]; return; } }; const indexOfItemInArray = (arr, key, value) => { for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (item && item[key] ? item[key].toString() === value.toString() : undefined) { return i; } } return -1; }; const modifyKeyValue = (obj, key, value) => (obj[key] = value); const addKeyValue = (obj, key, value) => { if (Array.isArray(obj)) { return obj.push(value); } else { return obj ? (obj[key] = value) : null; } }; const applyLeafChange = (obj, change, embeddedKey) => { const { type, key, value } = change; switch (type) { case Operation.ADD: return addKeyValue(obj, key, value); case Operation.UPDATE: return modifyKeyValue(obj, key, value); case Operation.REMOVE: return removeKey(obj, key, embeddedKey); } }; const applyArrayChange = (arr, change) => (() => { const result = []; for (const subchange of change.changes) { if (subchange.value != null || subchange.type === Operation.REMOVE) { result.push(applyLeafChange(arr, subchange, change.embeddedKey)); } else { let element; if (change.embeddedKey === '$index') { element = arr[subchange.key]; } else { element = (0, lodash_1.find)(arr, el => el[change.embeddedKey].toString() === subchange.key.toString()); } result.push((0, exports.applyChangeset)(element, subchange.changes)); } } return result; })(); const applyBranchChange = (obj, change) => { if (Array.isArray(obj)) { return applyArrayChange(obj, change); } else { return (0, exports.applyChangeset)(obj, change.changes); } }; const revertLeafChange = (obj, change, embeddedKey = '$index') => { const { type, key, value, oldValue } = change; switch (type) { case Operation.ADD: return removeKey(obj, key, embeddedKey); case Operation.UPDATE: return modifyKeyValue(obj, key, oldValue); case Operation.REMOVE: return addKeyValue(obj, key, value); } }; const revertArrayChange = (arr, change) => (() => { const result = []; for (const subchange of change.changes) { if (subchange.value != null || subchange.type === Operation.REMOVE) { result.push(revertLeafChange(arr, subchange, change.embeddedKey)); } else { let element; if (change.embeddedKey === '$index') { element = arr[+subchange.key]; } else { element = (0, lodash_1.find)(arr, el => el[change.embeddedKey].toString() === subchange.key); } result.push((0, exports.revertChangeset)(element, subchange.changes)); } } return result; })(); const revertBranchChange = (obj, change) => { if (Array.isArray(obj)) { return revertArrayChange(obj, change); } else { return (0, exports.revertChangeset)(obj, change.changes); } }; const diff = (oldObj, newObj, embeddedObjKeys, keysToSkip) => compare(oldObj, newObj, [], embeddedObjKeys, [], keysToSkip); exports.diff = diff; const applyChangeset = (obj, changeset) => { if (changeset) { changeset.forEach(change => (change.value !== null && change.value !== undefined) || change.type === Operation.REMOVE ? applyLeafChange(obj, change, change.embeddedKey) : applyBranchChange(obj[change.key], change)); } return obj; }; exports.applyChangeset = applyChangeset; const revertChangeset = (obj, changeset) => { if (changeset) { changeset .reverse() .forEach((change) => !change.changes ? revertLeafChange(obj, change) : revertBranchChange(obj[change.key], change)); } return obj; }; exports.revertChangeset = revertChangeset; var Operation; (function (Operation) { Operation["REMOVE"] = "REMOVE"; Operation["ADD"] = "ADD"; Operation["UPDATE"] = "UPDATE"; })(Operation = exports.Operation || (exports.Operation = {})); const flattenChangeset = (obj, path = '$', embeddedKey) => { if (Array.isArray(obj)) { return obj.reduce((memo, change) => [...memo, ...(0, exports.flattenChangeset)(change, path, embeddedKey)], []); } else { if (obj.changes || embeddedKey) { path = embeddedKey ? embeddedKey === '$index' ? `${path}[${obj.key}]` : obj.type === Operation.ADD ? path : `${path}[?(@.${embeddedKey}='${obj.key}')]` : (path = `${path}.${obj.key}`); return (0, exports.flattenChangeset)(obj.changes || obj, path, obj.embeddedKey); } else { const valueType = (0, exports.getTypeOfObj)(obj.value); return [ Object.assign(Object.assign({}, obj), { path: valueType === 'Object' || path.endsWith(`[${obj.key}]`) ? path : `${path}.${obj.key}`, valueType }) ]; } } }; exports.flattenChangeset = flattenChangeset; const unflattenChanges = (changes) => { if (!Array.isArray(changes)) { changes = [changes]; } const changesArr = []; changes.forEach(change => { const obj = {}; let ptr = obj; const segments = change.path.split(/([^@])\./).reduce((acc, curr, i) => { const x = Math.floor(i / 2); if (!acc[x]) { acc[x] = ''; } acc[x] += curr; return acc; }, []); // $.childern[@.name='chris'].age // => // $ // childern[@.name='chris'] // age if (segments.length === 1) { ptr.key = change.key; ptr.type = change.type; ptr.value = change.value; ptr.oldValue = change.oldValue; changesArr.push(ptr); } else { for (let i = 1; i < segments.length; i++) { const segment = segments[i]; // check for array const result = /^(.+)\[\?\(@\.(.+)='(.+)'\)]$|^(.+)\[(\d+)\]/.exec(segment); // array if (result) { let key; let embeddedKey; let arrKey; if (result[1]) { key = result[1]; embeddedKey = result[2]; arrKey = result[3]; } else { key = result[4]; embeddedKey = '$index'; arrKey = Number(result[5]); } // leaf if (i === segments.length - 1) { ptr.key = key; ptr.embeddedKey = embeddedKey; ptr.type = Operation.UPDATE; ptr.changes = [ { type: change.type, key: arrKey, value: change.value, oldValue: change.oldValue } ]; } else { // object ptr.key = key; ptr.embeddedKey = embeddedKey; ptr.type = Operation.UPDATE; const newPtr = {}; ptr.changes = [ { type: Operation.UPDATE, key: arrKey, changes: [newPtr] } ]; ptr = newPtr; } } else { // leaf if (i === segments.length - 1) { // check if value is a primitive or object if (change.value !== null && change.valueType === 'Object') { ptr.key = segment; ptr.type = Operation.UPDATE; ptr.changes = [ { key: change.key, type: change.type, value: change.value } ]; } else { ptr.key = change.key; ptr.type = change.type; ptr.value = change.value; ptr.oldValue = change.oldValue; } } else { // branch ptr.key = segment; ptr.type = Operation.UPDATE; const newPtr = {}; ptr.changes = [newPtr]; ptr = newPtr; } } } changesArr.push(obj); } }); return changesArr; }; exports.unflattenChanges = unflattenChanges;