core.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. Object.defineProperty(exports, "__esModule", { value: true });
  2. var helpers_js_1 = require("./helpers.js");
  3. exports.JsonPatchError = helpers_js_1.PatchError;
  4. exports.deepClone = helpers_js_1._deepClone;
  5. /* We use a Javascript hash to store each
  6. function. Each hash entry (property) uses
  7. the operation identifiers specified in rfc6902.
  8. In this way, we can map each patch operation
  9. to its dedicated function in efficient way.
  10. */
  11. /* The operations applicable to an object */
  12. var objOps = {
  13. add: function (obj, key, document) {
  14. obj[key] = this.value;
  15. return { newDocument: document };
  16. },
  17. remove: function (obj, key, document) {
  18. var removed = obj[key];
  19. delete obj[key];
  20. return { newDocument: document, removed: removed };
  21. },
  22. replace: function (obj, key, document) {
  23. var removed = obj[key];
  24. obj[key] = this.value;
  25. return { newDocument: document, removed: removed };
  26. },
  27. move: function (obj, key, document) {
  28. /* in case move target overwrites an existing value,
  29. return the removed value, this can be taxing performance-wise,
  30. and is potentially unneeded */
  31. var removed = getValueByPointer(document, this.path);
  32. if (removed) {
  33. removed = helpers_js_1._deepClone(removed);
  34. }
  35. var originalValue = applyOperation(document, { op: "remove", path: this.from }).removed;
  36. applyOperation(document, { op: "add", path: this.path, value: originalValue });
  37. return { newDocument: document, removed: removed };
  38. },
  39. copy: function (obj, key, document) {
  40. var valueToCopy = getValueByPointer(document, this.from);
  41. // enforce copy by value so further operations don't affect source (see issue #177)
  42. applyOperation(document, { op: "add", path: this.path, value: helpers_js_1._deepClone(valueToCopy) });
  43. return { newDocument: document };
  44. },
  45. test: function (obj, key, document) {
  46. return { newDocument: document, test: _areEquals(obj[key], this.value) };
  47. },
  48. _get: function (obj, key, document) {
  49. this.value = obj[key];
  50. return { newDocument: document };
  51. }
  52. };
  53. /* The operations applicable to an array. Many are the same as for the object */
  54. var arrOps = {
  55. add: function (arr, i, document) {
  56. if (helpers_js_1.isInteger(i)) {
  57. arr.splice(i, 0, this.value);
  58. }
  59. else { // array props
  60. arr[i] = this.value;
  61. }
  62. // this may be needed when using '-' in an array
  63. return { newDocument: document, index: i };
  64. },
  65. remove: function (arr, i, document) {
  66. var removedList = arr.splice(i, 1);
  67. return { newDocument: document, removed: removedList[0] };
  68. },
  69. replace: function (arr, i, document) {
  70. var removed = arr[i];
  71. arr[i] = this.value;
  72. return { newDocument: document, removed: removed };
  73. },
  74. move: objOps.move,
  75. copy: objOps.copy,
  76. test: objOps.test,
  77. _get: objOps._get
  78. };
  79. /**
  80. * Retrieves a value from a JSON document by a JSON pointer.
  81. * Returns the value.
  82. *
  83. * @param document The document to get the value from
  84. * @param pointer an escaped JSON pointer
  85. * @return The retrieved value
  86. */
  87. function getValueByPointer(document, pointer) {
  88. if (pointer == '') {
  89. return document;
  90. }
  91. var getOriginalDestination = { op: "_get", path: pointer };
  92. applyOperation(document, getOriginalDestination);
  93. return getOriginalDestination.value;
  94. }
  95. exports.getValueByPointer = getValueByPointer;
  96. /**
  97. * Apply a single JSON Patch Operation on a JSON document.
  98. * Returns the {newDocument, result} of the operation.
  99. * It modifies the `document` and `operation` objects - it gets the values by reference.
  100. * If you would like to avoid touching your values, clone them:
  101. * `jsonpatch.applyOperation(document, jsonpatch._deepClone(operation))`.
  102. *
  103. * @param document The document to patch
  104. * @param operation The operation to apply
  105. * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation.
  106. * @param mutateDocument Whether to mutate the original document or clone it before applying
  107. * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`.
  108. * @return `{newDocument, result}` after the operation
  109. */
  110. function applyOperation(document, operation, validateOperation, mutateDocument, banPrototypeModifications, index) {
  111. if (validateOperation === void 0) { validateOperation = false; }
  112. if (mutateDocument === void 0) { mutateDocument = true; }
  113. if (banPrototypeModifications === void 0) { banPrototypeModifications = true; }
  114. if (index === void 0) { index = 0; }
  115. if (validateOperation) {
  116. if (typeof validateOperation == 'function') {
  117. validateOperation(operation, 0, document, operation.path);
  118. }
  119. else {
  120. validator(operation, 0);
  121. }
  122. }
  123. /* ROOT OPERATIONS */
  124. if (operation.path === "") {
  125. var returnValue = { newDocument: document };
  126. if (operation.op === 'add') {
  127. returnValue.newDocument = operation.value;
  128. return returnValue;
  129. }
  130. else if (operation.op === 'replace') {
  131. returnValue.newDocument = operation.value;
  132. returnValue.removed = document; //document we removed
  133. return returnValue;
  134. }
  135. else if (operation.op === 'move' || operation.op === 'copy') { // it's a move or copy to root
  136. returnValue.newDocument = getValueByPointer(document, operation.from); // get the value by json-pointer in `from` field
  137. if (operation.op === 'move') { // report removed item
  138. returnValue.removed = document;
  139. }
  140. return returnValue;
  141. }
  142. else if (operation.op === 'test') {
  143. returnValue.test = _areEquals(document, operation.value);
  144. if (returnValue.test === false) {
  145. throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document);
  146. }
  147. returnValue.newDocument = document;
  148. return returnValue;
  149. }
  150. else if (operation.op === 'remove') { // a remove on root
  151. returnValue.removed = document;
  152. returnValue.newDocument = null;
  153. return returnValue;
  154. }
  155. else if (operation.op === '_get') {
  156. operation.value = document;
  157. return returnValue;
  158. }
  159. else { /* bad operation */
  160. if (validateOperation) {
  161. throw new exports.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document);
  162. }
  163. else {
  164. return returnValue;
  165. }
  166. }
  167. } /* END ROOT OPERATIONS */
  168. else {
  169. if (!mutateDocument) {
  170. document = helpers_js_1._deepClone(document);
  171. }
  172. var path = operation.path || "";
  173. var keys = path.split('/');
  174. var obj = document;
  175. var t = 1; //skip empty element - http://jsperf.com/to-shift-or-not-to-shift
  176. var len = keys.length;
  177. var existingPathFragment = undefined;
  178. var key = void 0;
  179. var validateFunction = void 0;
  180. if (typeof validateOperation == 'function') {
  181. validateFunction = validateOperation;
  182. }
  183. else {
  184. validateFunction = validator;
  185. }
  186. while (true) {
  187. key = keys[t];
  188. if (key && key.indexOf('~') != -1) {
  189. key = helpers_js_1.unescapePathComponent(key);
  190. }
  191. if (banPrototypeModifications &&
  192. (key == '__proto__' ||
  193. (key == 'prototype' && t > 0 && keys[t - 1] == 'constructor'))) {
  194. throw new TypeError('JSON-Patch: modifying `__proto__` or `constructor/prototype` 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');
  195. }
  196. if (validateOperation) {
  197. if (existingPathFragment === undefined) {
  198. if (obj[key] === undefined) {
  199. existingPathFragment = keys.slice(0, t).join('/');
  200. }
  201. else if (t == len - 1) {
  202. existingPathFragment = operation.path;
  203. }
  204. if (existingPathFragment !== undefined) {
  205. validateFunction(operation, 0, document, existingPathFragment);
  206. }
  207. }
  208. }
  209. t++;
  210. if (Array.isArray(obj)) {
  211. if (key === '-') {
  212. key = obj.length;
  213. }
  214. else {
  215. if (validateOperation && !helpers_js_1.isInteger(key)) {
  216. throw new exports.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", index, operation, document);
  217. } // only parse key when it's an integer for `arr.prop` to work
  218. else if (helpers_js_1.isInteger(key)) {
  219. key = ~~key;
  220. }
  221. }
  222. if (t >= len) {
  223. if (validateOperation && operation.op === "add" && key > obj.length) {
  224. throw new exports.JsonPatchError("The specified index MUST NOT be greater than the number of elements in the array", "OPERATION_VALUE_OUT_OF_BOUNDS", index, operation, document);
  225. }
  226. var returnValue = arrOps[operation.op].call(operation, obj, key, document); // Apply patch
  227. if (returnValue.test === false) {
  228. throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document);
  229. }
  230. return returnValue;
  231. }
  232. }
  233. else {
  234. if (t >= len) {
  235. var returnValue = objOps[operation.op].call(operation, obj, key, document); // Apply patch
  236. if (returnValue.test === false) {
  237. throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document);
  238. }
  239. return returnValue;
  240. }
  241. }
  242. obj = obj[key];
  243. // If we have more keys in the path, but the next value isn't a non-null object,
  244. // throw an OPERATION_PATH_UNRESOLVABLE error instead of iterating again.
  245. if (validateOperation && t < len && (!obj || typeof obj !== "object")) {
  246. throw new exports.JsonPatchError('Cannot perform operation at the desired path', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document);
  247. }
  248. }
  249. }
  250. }
  251. exports.applyOperation = applyOperation;
  252. /**
  253. * Apply a full JSON Patch array on a JSON document.
  254. * Returns the {newDocument, result} of the patch.
  255. * It modifies the `document` object and `patch` - it gets the values by reference.
  256. * If you would like to avoid touching your values, clone them:
  257. * `jsonpatch.applyPatch(document, jsonpatch._deepClone(patch))`.
  258. *
  259. * @param document The document to patch
  260. * @param patch The patch to apply
  261. * @param validateOperation `false` is without validation, `true` to use default jsonpatch's validation, or you can pass a `validateOperation` callback to be used for validation.
  262. * @param mutateDocument Whether to mutate the original document or clone it before applying
  263. * @param banPrototypeModifications Whether to ban modifications to `__proto__`, defaults to `true`.
  264. * @return An array of `{newDocument, result}` after the patch
  265. */
  266. function applyPatch(document, patch, validateOperation, mutateDocument, banPrototypeModifications) {
  267. if (mutateDocument === void 0) { mutateDocument = true; }
  268. if (banPrototypeModifications === void 0) { banPrototypeModifications = true; }
  269. if (validateOperation) {
  270. if (!Array.isArray(patch)) {
  271. throw new exports.JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY');
  272. }
  273. }
  274. if (!mutateDocument) {
  275. document = helpers_js_1._deepClone(document);
  276. }
  277. var results = new Array(patch.length);
  278. for (var i = 0, length_1 = patch.length; i < length_1; i++) {
  279. // we don't need to pass mutateDocument argument because if it was true, we already deep cloned the object, we'll just pass `true`
  280. results[i] = applyOperation(document, patch[i], validateOperation, true, banPrototypeModifications, i);
  281. document = results[i].newDocument; // in case root was replaced
  282. }
  283. results.newDocument = document;
  284. return results;
  285. }
  286. exports.applyPatch = applyPatch;
  287. /**
  288. * Apply a single JSON Patch Operation on a JSON document.
  289. * Returns the updated document.
  290. * Suitable as a reducer.
  291. *
  292. * @param document The document to patch
  293. * @param operation The operation to apply
  294. * @return The updated document
  295. */
  296. function applyReducer(document, operation, index) {
  297. var operationResult = applyOperation(document, operation);
  298. if (operationResult.test === false) { // failed test
  299. throw new exports.JsonPatchError("Test operation failed", 'TEST_OPERATION_FAILED', index, operation, document);
  300. }
  301. return operationResult.newDocument;
  302. }
  303. exports.applyReducer = applyReducer;
  304. /**
  305. * Validates a single operation. Called from `jsonpatch.validate`. Throws `JsonPatchError` in case of an error.
  306. * @param {object} operation - operation object (patch)
  307. * @param {number} index - index of operation in the sequence
  308. * @param {object} [document] - object where the operation is supposed to be applied
  309. * @param {string} [existingPathFragment] - comes along with `document`
  310. */
  311. function validator(operation, index, document, existingPathFragment) {
  312. if (typeof operation !== 'object' || operation === null || Array.isArray(operation)) {
  313. throw new exports.JsonPatchError('Operation is not an object', 'OPERATION_NOT_AN_OBJECT', index, operation, document);
  314. }
  315. else if (!objOps[operation.op]) {
  316. throw new exports.JsonPatchError('Operation `op` property is not one of operations defined in RFC-6902', 'OPERATION_OP_INVALID', index, operation, document);
  317. }
  318. else if (typeof operation.path !== 'string') {
  319. throw new exports.JsonPatchError('Operation `path` property is not a string', 'OPERATION_PATH_INVALID', index, operation, document);
  320. }
  321. else if (operation.path.indexOf('/') !== 0 && operation.path.length > 0) {
  322. // paths that aren't empty string should start with "/"
  323. throw new exports.JsonPatchError('Operation `path` property must start with "/"', 'OPERATION_PATH_INVALID', index, operation, document);
  324. }
  325. else if ((operation.op === 'move' || operation.op === 'copy') && typeof operation.from !== 'string') {
  326. throw new exports.JsonPatchError('Operation `from` property is not present (applicable in `move` and `copy` operations)', 'OPERATION_FROM_REQUIRED', index, operation, document);
  327. }
  328. else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && operation.value === undefined) {
  329. throw new exports.JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_REQUIRED', index, operation, document);
  330. }
  331. else if ((operation.op === 'add' || operation.op === 'replace' || operation.op === 'test') && helpers_js_1.hasUndefined(operation.value)) {
  332. throw new exports.JsonPatchError('Operation `value` property is not present (applicable in `add`, `replace` and `test` operations)', 'OPERATION_VALUE_CANNOT_CONTAIN_UNDEFINED', index, operation, document);
  333. }
  334. else if (document) {
  335. if (operation.op == "add") {
  336. var pathLen = operation.path.split("/").length;
  337. var existingPathLen = existingPathFragment.split("/").length;
  338. if (pathLen !== existingPathLen + 1 && pathLen !== existingPathLen) {
  339. throw new exports.JsonPatchError('Cannot perform an `add` operation at the desired path', 'OPERATION_PATH_CANNOT_ADD', index, operation, document);
  340. }
  341. }
  342. else if (operation.op === 'replace' || operation.op === 'remove' || operation.op === '_get') {
  343. if (operation.path !== existingPathFragment) {
  344. throw new exports.JsonPatchError('Cannot perform the operation at a path that does not exist', 'OPERATION_PATH_UNRESOLVABLE', index, operation, document);
  345. }
  346. }
  347. else if (operation.op === 'move' || operation.op === 'copy') {
  348. var existingValue = { op: "_get", path: operation.from, value: undefined };
  349. var error = validate([existingValue], document);
  350. if (error && error.name === 'OPERATION_PATH_UNRESOLVABLE') {
  351. throw new exports.JsonPatchError('Cannot perform the operation from a path that does not exist', 'OPERATION_FROM_UNRESOLVABLE', index, operation, document);
  352. }
  353. }
  354. }
  355. }
  356. exports.validator = validator;
  357. /**
  358. * Validates a sequence of operations. If `document` parameter is provided, the sequence is additionally validated against the object document.
  359. * If error is encountered, returns a JsonPatchError object
  360. * @param sequence
  361. * @param document
  362. * @returns {JsonPatchError|undefined}
  363. */
  364. function validate(sequence, document, externalValidator) {
  365. try {
  366. if (!Array.isArray(sequence)) {
  367. throw new exports.JsonPatchError('Patch sequence must be an array', 'SEQUENCE_NOT_AN_ARRAY');
  368. }
  369. if (document) {
  370. //clone document and sequence so that we can safely try applying operations
  371. applyPatch(helpers_js_1._deepClone(document), helpers_js_1._deepClone(sequence), externalValidator || true);
  372. }
  373. else {
  374. externalValidator = externalValidator || validator;
  375. for (var i = 0; i < sequence.length; i++) {
  376. externalValidator(sequence[i], i, document, undefined);
  377. }
  378. }
  379. }
  380. catch (e) {
  381. if (e instanceof exports.JsonPatchError) {
  382. return e;
  383. }
  384. else {
  385. throw e;
  386. }
  387. }
  388. }
  389. exports.validate = validate;
  390. // based on https://github.com/epoberezkin/fast-deep-equal
  391. // MIT License
  392. // Copyright (c) 2017 Evgeny Poberezkin
  393. // Permission is hereby granted, free of charge, to any person obtaining a copy
  394. // of this software and associated documentation files (the "Software"), to deal
  395. // in the Software without restriction, including without limitation the rights
  396. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  397. // copies of the Software, and to permit persons to whom the Software is
  398. // furnished to do so, subject to the following conditions:
  399. // The above copyright notice and this permission notice shall be included in all
  400. // copies or substantial portions of the Software.
  401. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  402. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  403. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  404. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  405. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  406. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  407. // SOFTWARE.
  408. function _areEquals(a, b) {
  409. if (a === b)
  410. return true;
  411. if (a && b && typeof a == 'object' && typeof b == 'object') {
  412. var arrA = Array.isArray(a), arrB = Array.isArray(b), i, length, key;
  413. if (arrA && arrB) {
  414. length = a.length;
  415. if (length != b.length)
  416. return false;
  417. for (i = length; i-- !== 0;)
  418. if (!_areEquals(a[i], b[i]))
  419. return false;
  420. return true;
  421. }
  422. if (arrA != arrB)
  423. return false;
  424. var keys = Object.keys(a);
  425. length = keys.length;
  426. if (length !== Object.keys(b).length)
  427. return false;
  428. for (i = length; i-- !== 0;)
  429. if (!b.hasOwnProperty(keys[i]))
  430. return false;
  431. for (i = length; i-- !== 0;) {
  432. key = keys[i];
  433. if (!_areEquals(a[key], b[key]))
  434. return false;
  435. }
  436. return true;
  437. }
  438. return a !== a && b !== b;
  439. }
  440. exports._areEquals = _areEquals;
  441. ;