source: node_modules/fast-json-patch/module/core.mjs

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

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