diff --git a/README.md b/README.md index 9c93221a..3701489a 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,24 @@ var patch = jsonpatch.generate(observer); // ]; ``` +Generating patches with test operations: + +```js +var document = { firstName: "Joachim", lastName: "Wester", contactDetails: { phoneNumbers: [ { number:"555-123" }] } }; +var observer = jsonpatch.observe(document, undefined, true); +document.firstName = "Albert"; +document.contactDetails.phoneNumbers[0].number = "123"; +document.contactDetails.phoneNumbers.push({ number:"456" }); +var patch = jsonpatch.generate(observer); +// patch == [ +// { op: "test", path: "/firstName", value: "Joachim"}, +// { op: "replace", path: "/firstName", value: "Albert"}, +// { op: "test", path: "/contactDetails/phoneNumbers/0/number", value: "555-123" }, +// { op: "replace", path: "/contactDetails/phoneNumbers/0/number", value: "123" }, +// { op: "add", path: "/contactDetails/phoneNumbers/1", value: {number:"456"}} +// ]; +``` + Comparing two object trees: ```js @@ -163,6 +181,18 @@ var diff = jsonpatch.compare(documentA, documentB); //diff == [{op: "replace", path: "/user/lastName", value: "Collins"}] ``` +Comparing two object trees with test operations: + +```js +var documentA = {user: {firstName: "Albert", lastName: "Einstein"}}; +var documentB = {user: {firstName: "Albert", lastName: "Collins"}}; +var diff = jsonpatch.compare(documentA, documentB, {inversible: true}); +//diff == [ +// {op: "test", path: "/user/lastName", value: "Einstein"}, +// {op: "replace", path: "/user/lastName", value: "Collins"} +// ]; +``` + Validating a sequence of patches: ```js @@ -262,17 +292,17 @@ Retrieves a value from a JSON document by a JSON pointer. Returns the value. -#### `jsonpatch.observe(document: any, callback?: Function): Observer` +#### `jsonpatch.observe(document: any, callback?: Function, inversible: boolean = false): Observer` Sets up an deep observer on `document` that listens for changes in object tree. When changes are detected, the optional -callback is called with the generated patches array as the parameter. +callback is called with the generated patches array as the parameter. If inversible is true, then observer will generate test operations. Returns `observer`. -#### `jsonpatch.generate(document: any, observer: Observer): Operation[]` +#### `jsonpatch.generate(document: any, observer: Observer, opts?: { inversible: boolean }): Operation[]` If there are pending changes in `obj`, returns them synchronously. If a `callback` was defined in `observe` -method, it will be triggered synchronously as well. +method, it will be triggered synchronously as well. If opts.inversible is undefined then fallback to observer.inversible. If there are no pending changes in `obj`, returns an empty array (length 0). @@ -282,9 +312,9 @@ Destroys the observer set up on `document`. Any remaining changes are delivered synchronously (as in `jsonpatch.generate`). Note: this is different that ES6/7 `Object.unobserve`, which delivers remaining changes asynchronously. -#### `jsonpatch.compare(document1: any, document2: any): Operation[]` +#### `jsonpatch.compare(document1: any, document2: any, opts?: { inversible: boolean }): Operation[]` -Compares object trees `document1` and `document2` and returns the difference relative to `document1` as a patches array. +Compares object trees `document1` and `document2` and returns the difference relative to `document1` as a patches array. If opts.inversible is true, test operations will be generated. If there are no differences, returns an empty array (length 0). diff --git a/dist/fast-json-patch.js b/dist/fast-json-patch.js index 3c73b217..64971300 100644 --- a/dist/fast-json-patch.js +++ b/dist/fast-json-patch.js @@ -646,7 +646,7 @@ exports.validate = validate; */ var helpers_1 = __webpack_require__(0); var core_1 = __webpack_require__(1); -/* export all core functions */ +/* export all core functions and types */ var core_2 = __webpack_require__(1); exports.applyOperation = core_2.applyOperation; exports.applyPatch = core_2.applyPatch; @@ -694,7 +694,8 @@ exports.unobserve = unobserve; /** * Observes changes made to an object, which can then be retrieved using generate */ -function observe(obj, callback) { +function observe(obj, callback, inversible) { + if (inversible === void 0) { inversible = false; } var patches = []; var observer; var mirror = getMirror(obj); @@ -709,7 +710,7 @@ function observe(obj, callback) { if (observer) { return observer; } - observer = {}; + observer = { inversible: inversible }; mirror.value = helpers_1._deepClone(obj); if (callback) { observer.callback = callback; @@ -766,9 +767,11 @@ exports.observe = observe; /** * Generate an array of patches from an observer */ -function generate(observer) { +function generate(observer, opts) { + if (opts === void 0) { opts = {}; } var mirror = beforeDict.get(observer.object); - _generate(mirror.value, observer.object, observer.patches, ""); + var inversible = typeof opts.inversible !== "undefined" ? opts.inversible : observer.inversible; + _generate(mirror.value, observer.object, observer.patches, "", { inversible: inversible }); if (observer.patches.length) { core_1.applyPatch(mirror.value, observer.patches); } @@ -783,7 +786,8 @@ function generate(observer) { } exports.generate = generate; // Dirty check if obj is different from mirror, generate patches and update mirror -function _generate(mirror, obj, patches, path) { +function _generate(mirror, obj, patches, path, opts) { + if (opts === void 0) { opts = { inversible: false }; } if (obj === mirror) { return; } @@ -794,6 +798,7 @@ function _generate(mirror, obj, patches, path) { var oldKeys = helpers_1._objectKeys(mirror); var changed = false; var deleted = false; + var inversible = opts.inversible; //if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)" for (var t = oldKeys.length - 1; t >= 0; t--) { var key = oldKeys[t]; @@ -801,20 +806,26 @@ function _generate(mirror, obj, patches, path) { if (helpers_1.hasOwnProperty(obj, key) && !(obj[key] === undefined && oldVal !== undefined && Array.isArray(obj) === false)) { var newVal = obj[key]; if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) { - _generate(oldVal, newVal, patches, path + "/" + helpers_1.escapePathComponent(key)); + _generate(oldVal, newVal, patches, path + "/" + helpers_1.escapePathComponent(key), opts); } else { if (oldVal !== newVal) { changed = true; + if (inversible) + patches.push({ op: "test", path: path + "/" + helpers_1.escapePathComponent(key), value: helpers_1._deepClone(oldVal) }); patches.push({ op: "replace", path: path + "/" + helpers_1.escapePathComponent(key), value: helpers_1._deepClone(newVal) }); } } } else if (Array.isArray(mirror) === Array.isArray(obj)) { + if (inversible) + patches.push({ op: "test", path: path + "/" + helpers_1.escapePathComponent(key), value: helpers_1._deepClone(oldVal) }); patches.push({ op: "remove", path: path + "/" + helpers_1.escapePathComponent(key) }); deleted = true; // property has been deleted } else { + if (inversible) + patches.push({ op: "test", path: path, value: mirror }); patches.push({ op: "replace", path: path, value: obj }); changed = true; } @@ -832,9 +843,9 @@ function _generate(mirror, obj, patches, path) { /** * Create an array of patches from the differences in two objects */ -function compare(tree1, tree2) { +function compare(tree1, tree2, opts) { var patches = []; - _generate(tree1, tree2, patches, ''); + _generate(tree1, tree2, patches, '', opts); return patches; } exports.compare = compare; diff --git a/dist/fast-json-patch.min.js b/dist/fast-json-patch.min.js index 61b99b1b..1568b3dc 100644 --- a/dist/fast-json-patch.min.js +++ b/dist/fast-json-patch.min.js @@ -1,2 +1,2 @@ /*! fast-json-patch, version: 2.1.0 */ -var jsonpatch=function(a){function b(d){if(c[d])return c[d].exports;var e=c[d]={i:d,l:!1,exports:{}};return a[d].call(e.exports,e,e.exports,b),e.l=!0,e.exports}var c={};return b.m=a,b.c=c,b.i=function(a){return a},b.d=function(a,c,d){b.o(a,c)||Object.defineProperty(a,c,{configurable:!1,enumerable:!0,get:d})},b.n=function(a){var c=a&&a.__esModule?function(){return a['default']}:function(){return a};return b.d(c,'a',c),c},b.o=function(a,b){return Object.prototype.hasOwnProperty.call(a,b)},b.p='',b(b.s=2)}([function(a,b){function c(a,b){return j.call(a,b)}function d(a){if(Array.isArray(a)){for(var b=Array(a.length),d=0;d=b){c++;continue}return!1}return!0},b.escapePathComponent=e,b.unescapePathComponent=function(a){return a.replace(/~1/g,'/').replace(/~0/g,'~')},b._getPathRecursive=f,b.getPath=function(a,b){if(a===b)return'/';var c=f(a,b);if(''===c)throw new Error('Object not found in root');return'/'+c},b.hasUndefined=g;var k=function(a){function b(b,c,d,e,f){a.call(this,h(b,{name:c,index:d,operation:e,tree:f})),this.name=c,this.index=d,this.operation=e,this.tree=f,this.message=h(b,{name:c,index:d,operation:e,tree:f})}return i(b,a),b}(Error);b.PatchError=k},function(a,b,c){function d(a,b){if(''==b)return a;var c={op:'_get',path:b};return e(a,c),c.value}function e(a,c,e,f,h,i){if(void 0===e&&(e=!1),void 0===f&&(f=!0),void 0===h&&(h=!0),void 0===i&&(i=0),e&&('function'==typeof e?e(c,0,a,c.path):g(c,0)),''===c.path){var j={newDocument:a};if('add'===c.op)return j.newDocument=c.value,j;if('replace'===c.op)return j.newDocument=c.value,j.removed=a,j;if('move'===c.op||'copy'===c.op)return j.newDocument=d(a,c.from),'move'===c.op&&(j.removed=a),j;if('test'===c.op){if(j.test=k(a,c.value),!1===j.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',i,c,a);return j.newDocument=a,j}if('remove'===c.op)return j.removed=a,j.newDocument=null,j;if('_get'===c.op)return c.value=a,j;if(e)throw new b.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902','OPERATION_OP_INVALID',i,c,a);else return j}else{f||(a=l._deepClone(a));var o,p,q,r=c.path||'',s=r.split('/'),u=a,v=1,t=s.length;for(q='function'==typeof e?e:g;;){if(p=s[v],h&&'__proto__'==p)throw new TypeError('JSON-Patch: modifying `__proto__` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README');if(e&&void 0==o&&(void 0===u[p]?o=s.slice(0,v).join('/'):v==t-1&&(o=c.path),void 0!==o&&q(c,0,a,o)),v++,Array.isArray(u)){if('-'===p)p=u.length;else if(e&&!l.isInteger(p))throw new b.JsonPatchError('Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index','OPERATION_PATH_ILLEGAL_ARRAY_INDEX',i,c,a);else l.isInteger(p)&&(p=~~p);if(v>=t){if(e&&'add'===c.op&&p>u.length)throw new b.JsonPatchError('The specified index MUST NOT be greater than the number of elements in the array','OPERATION_VALUE_OUT_OF_BOUNDS',i,c,a);var j=n[c.op].call(c,u,p,a);if(!1===j.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',i,c,a);return j}}else if(p&&-1!=p.indexOf('~')&&(p=l.unescapePathComponent(p)),v>=t){var j=m[c.op].call(c,u,p,a);if(!1===j.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',i,c,a);return j}u=u[p]}}}function f(a,c,d,f,g){if(void 0===f&&(f=!0),void 0===g&&(g=!0),d&&!Array.isArray(c))throw new b.JsonPatchError('Patch sequence must be an array','SEQUENCE_NOT_AN_ARRAY');f||(a=l._deepClone(a));for(var h=Array(c.length),j=0,i=c.length;j=b){c++;continue}return!1}return!0},b.escapePathComponent=e,b.unescapePathComponent=function(a){return a.replace(/~1/g,'/').replace(/~0/g,'~')},b._getPathRecursive=f,b.getPath=function(a,b){if(a===b)return'/';var c=f(a,b);if(''===c)throw new Error('Object not found in root');return'/'+c},b.hasUndefined=g;var k=function(a){function b(b,c,d,e,f){a.call(this,h(b,{name:c,index:d,operation:e,tree:f})),this.name=c,this.index=d,this.operation=e,this.tree=f,this.message=h(b,{name:c,index:d,operation:e,tree:f})}return i(b,a),b}(Error);b.PatchError=k},function(a,b,c){function d(a,b){if(''==b)return a;var c={op:'_get',path:b};return e(a,c),c.value}function e(a,c,e,f,h,i){if(void 0===e&&(e=!1),void 0===f&&(f=!0),void 0===h&&(h=!0),void 0===i&&(i=0),e&&('function'==typeof e?e(c,0,a,c.path):g(c,0)),''===c.path){var j={newDocument:a};if('add'===c.op)return j.newDocument=c.value,j;if('replace'===c.op)return j.newDocument=c.value,j.removed=a,j;if('move'===c.op||'copy'===c.op)return j.newDocument=d(a,c.from),'move'===c.op&&(j.removed=a),j;if('test'===c.op){if(j.test=k(a,c.value),!1===j.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',i,c,a);return j.newDocument=a,j}if('remove'===c.op)return j.removed=a,j.newDocument=null,j;if('_get'===c.op)return c.value=a,j;if(e)throw new b.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902','OPERATION_OP_INVALID',i,c,a);else return j}else{f||(a=l._deepClone(a));var o,p,q,r=c.path||'',s=r.split('/'),u=a,v=1,t=s.length;for(q='function'==typeof e?e:g;;){if(p=s[v],h&&'__proto__'==p)throw new TypeError('JSON-Patch: modifying `__proto__` prop is banned for security reasons, if this was on purpose, please set `banPrototypeModifications` flag false and pass it to this function. More info in fast-json-patch README');if(e&&void 0==o&&(void 0===u[p]?o=s.slice(0,v).join('/'):v==t-1&&(o=c.path),void 0!==o&&q(c,0,a,o)),v++,Array.isArray(u)){if('-'===p)p=u.length;else if(e&&!l.isInteger(p))throw new b.JsonPatchError('Expected an unsigned base-10 integer value, making the new referenced value the array element with the zero-based index','OPERATION_PATH_ILLEGAL_ARRAY_INDEX',i,c,a);else l.isInteger(p)&&(p=~~p);if(v>=t){if(e&&'add'===c.op&&p>u.length)throw new b.JsonPatchError('The specified index MUST NOT be greater than the number of elements in the array','OPERATION_VALUE_OUT_OF_BOUNDS',i,c,a);var j=n[c.op].call(c,u,p,a);if(!1===j.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',i,c,a);return j}}else if(p&&-1!=p.indexOf('~')&&(p=l.unescapePathComponent(p)),v>=t){var j=m[c.op].call(c,u,p,a);if(!1===j.test)throw new b.JsonPatchError('Test operation failed','TEST_OPERATION_FAILED',i,c,a);return j}u=u[p]}}}function f(a,c,d,f,g){if(void 0===f&&(f=!0),void 0===g&&(g=!0),d&&!Array.isArray(c))throw new b.JsonPatchError('Patch sequence must be an array','SEQUENCE_NOT_AN_ARRAY');f||(a=l._deepClone(a));for(var h=Array(c.length),j=0,i=c.length;j { patches: Operation[]; unobserve: () => void; callback: (patches: Operation[]) => void; + inversible: boolean; } var beforeDict = new WeakMap(); @@ -63,7 +64,7 @@ export function unobserve(root: T, observer: Observer) { /** * Observes changes made to an object, which can then be retrieved using generate */ -export function observe(obj: Object|Array, callback?: (patches: Operation[]) => void): Observer { +export function observe(obj: Object|Array, callback?: (patches: Operation[]) => void, inversible: boolean = false): Observer { var patches = []; var observer; var mirror = getMirror(obj); @@ -80,7 +81,7 @@ export function observe(obj: Object|Array, callback?: (patches: Operation[ return observer; } - observer = {}; + observer = { inversible }; mirror.value = _deepClone(obj); @@ -144,9 +145,11 @@ export function observe(obj: Object|Array, callback?: (patches: Operation[ /** * Generate an array of patches from an observer */ -export function generate(observer: Observer): Operation[] { +export function generate(observer: Observer, opts: { inversible?: boolean } = {}): Operation[] { var mirror = beforeDict.get(observer.object); - _generate(mirror.value, observer.object, observer.patches, ""); + var inversible = typeof opts.inversible !== "undefined" ? opts.inversible : observer.inversible; + + _generate(mirror.value, observer.object, observer.patches, "", { inversible }); if (observer.patches.length) { applyPatch(mirror.value, observer.patches); } @@ -161,7 +164,7 @@ export function generate(observer: Observer): Operation[] { } // Dirty check if obj is different from mirror, generate patches and update mirror -function _generate(mirror, obj, patches, path) { +function _generate(mirror, obj, patches, path, opts = { inversible: false }) { if (obj === mirror) { return; } @@ -174,6 +177,7 @@ function _generate(mirror, obj, patches, path) { var oldKeys = _objectKeys(mirror); var changed = false; var deleted = false; + var { inversible } = opts; //if ever "move" operation is implemented here, make sure this test runs OK: "should not generate the same patch twice (move)" @@ -185,19 +189,22 @@ function _generate(mirror, obj, patches, path) { var newVal = obj[key]; if (typeof oldVal == "object" && oldVal != null && typeof newVal == "object" && newVal != null) { - _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key)); + _generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key), opts); } else { if (oldVal !== newVal) { changed = true; + if (inversible) patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }); patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: _deepClone(newVal) }); } } } else if(Array.isArray(mirror) === Array.isArray(obj)) { + if (inversible) patches.push({ op: "test", path: path + "/" + escapePathComponent(key), value: _deepClone(oldVal) }) patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) }); deleted = true; // property has been deleted } else { + if (inversible) patches.push({ op: "test", path, value: mirror }); patches.push({ op: "replace", path, value: obj }); changed = true; } @@ -217,8 +224,8 @@ function _generate(mirror, obj, patches, path) { /** * Create an array of patches from the differences in two objects */ -export function compare(tree1: Object | Array, tree2: Object | Array): Operation[] { +export function compare(tree1: Object | Array, tree2: Object | Array, opts?: { inversible: boolean }): Operation[] { var patches = []; - _generate(tree1, tree2, patches, ''); + _generate(tree1, tree2, patches, '', opts); return patches; } diff --git a/test/spec/duplexBenchmark.js b/test/spec/duplexBenchmark.js index d619f66e..ef278804 100644 --- a/test/spec/duplexBenchmark.js +++ b/test/spec/duplexBenchmark.js @@ -145,6 +145,136 @@ suite.add('compare operation same but deep objects', { } }); +// Benchmark generating test operations +suite.add('generate operation, with inversible set to true', { + setup: function() { + var obj = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + var observer = jsonpatch.observe(obj, undefined, true); + }, + fn: function() { + obj.firstName = 'Joachim'; + obj.lastName = 'Wester'; + obj.phoneNumbers[0].number = '123'; + obj.phoneNumbers[1].number = '456'; + + var patches = jsonpatch.generate(observer); + } +}); +suite.add('generate operation and re-apply, with inversible set to true', { + setup: function() { + var obj = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + var observer = jsonpatch.observe(obj, undefined, true); + }, + fn: function() { + obj.firstName = 'Joachim'; + obj.lastName = 'Wester'; + obj.phoneNumbers[0].number = '123'; + obj.phoneNumbers[1].number = '456'; + + var patches = jsonpatch.generate(observer); + obj2 = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + + jsonpatch.applyPatch(obj2, patches); + } +}); +suite.add('compare operation, with inversible set to true', { + setup: function() { + var obj = { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + var obj2 = { + firstName: 'Joachim', + lastName: 'Wester', + mobileNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + }; + }, + fn: function() { + var patches = jsonpatch.compare(obj, obj2, { inversible: true }); + } +}); + +suite.add('compare operation same but deep objects, with inversible set to true', { + setup: function() { + var depth = 10; + + function shallowObj() { + return { + shallow: { + firstName: 'Tomek', + lastName: 'Wytrebowicz', + mobileNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + } + }; + } + var obj = shallowObj(); + var node = obj; + while (depth-- > 0) { + node.nested = shallowObj(); + node = node.nested; + } + var obj2 = obj; + }, + fn: function() { + var patches = jsonpatch.compare(obj, obj2, { inversible: true }); + } +}); + // if we are in the browser with benchmark < 2.1.2 if (typeof benchmarkReporter !== 'undefined') { benchmarkReporter(suite); diff --git a/test/spec/duplexSpec.js b/test/spec/duplexSpec.js index ce65f1fd..70155e34 100644 --- a/test/spec/duplexSpec.js +++ b/test/spec/duplexSpec.js @@ -198,10 +198,15 @@ describe('duplex', function() { person1.firstName = 'Alexander'; person2.firstName = 'Lucas'; - var patch1 = jsonpatch.generate(observer1); - var patch2 = jsonpatch.generate(observer2); + var patch1 = jsonpatch.generate(observer1, { inversible: true }); + var patch2 = jsonpatch.generate(observer2, { inversible: true }); expect(patch1).toReallyEqual([ + { + op: "test", + path: "/firstName", + value: "Alexandra" + }, { op: 'replace', path: '/firstName', @@ -209,6 +214,11 @@ describe('duplex', function() { } ]); expect(patch2).toReallyEqual([ + { + op: "test", + path: "/firstName", + value: "Lisa" + }, { op: 'replace', path: '/firstName', @@ -231,11 +241,16 @@ describe('duplex', function() { ] }; - var observer = jsonpatch.observe(obj); + var observer = jsonpatch.observe(obj, undefined, true); obj.firstName = 'Marcin'; var patches = jsonpatch.generate(observer); expect(patches).toReallyEqual([ + { + op: "test", + path: "/firstName", + value: "Albert" + }, { op: 'replace', path: '/firstName', @@ -246,6 +261,11 @@ describe('duplex', function() { obj.lastName = 'Warp'; patches = jsonpatch.generate(observer); //first patch should NOT be reported again here expect(patches).toReallyEqual([ + { + op: "test", + path: "/lastName", + value: "Einstein" + }, { op: 'replace', path: '/lastName', @@ -281,11 +301,16 @@ describe('duplex', function() { ] }; - var observer = jsonpatch.observe(obj); + var observer = jsonpatch.observe(obj, undefined, true); obj.phoneNumbers[0].number = '123'; var patches = jsonpatch.generate(observer); expect(patches).toReallyEqual([ + { + op: "test", + path: "/phoneNumbers/0/number", + value: "12345" + }, { op: 'replace', path: '/phoneNumbers/0/number', @@ -296,6 +321,11 @@ describe('duplex', function() { obj.phoneNumbers[1].number = '456'; patches = jsonpatch.generate(observer); //first patch should NOT be reported again here expect(patches).toReallyEqual([ + { + op: "test", + path: "/phoneNumbers/1/number", + value: "45353" + }, { op: 'replace', path: '/phoneNumbers/1/number',