UNPKG

immutable-json-patch

Version:

Immutable JSON patch with support for reverting operations

270 lines (257 loc) 7.73 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.applyProp = applyProp; exports.deleteIn = deleteIn; exports.existsIn = existsIn; exports.getIn = getIn; exports.insertAt = insertAt; exports.setIn = setIn; exports.shallowClone = shallowClone; exports.transform = transform; exports.updateIn = updateIn; var _typeguards = require("./typeguards.js"); var _utils = require("./utils.js"); /** * Immutability helpers * * inspiration: * * https://www.npmjs.com/package/seamless-immutable * https://www.npmjs.com/package/ih * https://www.npmjs.com/package/mutatis * https://github.com/mariocasciaro/object-path-immutable */ /** * Shallow clone of an Object, Array, or value * Symbols are cloned too. */ function shallowClone(value) { if ((0, _typeguards.isJSONArray)(value)) { // copy array items const copy = value.slice(); // copy all symbols Object.getOwnPropertySymbols(value).forEach(symbol => { // @ts-ignore copy[symbol] = value[symbol]; }); return copy; } if ((0, _typeguards.isJSONObject)(value)) { // copy object properties const copy = { ...value }; // copy all symbols Object.getOwnPropertySymbols(value).forEach(symbol => { // @ts-ignore copy[symbol] = value[symbol]; }); return copy; } return value; } /** * Update a value in an object in an immutable way. * If the value is unchanged, the original object will be returned */ function applyProp(object, key, value) { // @ts-ignore if (object[key] === value) { // return original object unchanged when the new value is identical to the old one return object; } const updatedObject = shallowClone(object); // @ts-ignore updatedObject[key] = value; return updatedObject; } /** * helper function to get a nested property in an object or array * * @return Returns the field when found, or undefined when the path doesn't exist */ function getIn(object, path) { let value = object; let i = 0; while (i < path.length) { if ((0, _typeguards.isJSONObject)(value)) { value = value[path[i]]; } else if ((0, _typeguards.isJSONArray)(value)) { value = value[Number.parseInt(path[i])]; } else { value = undefined; } i++; } return value; } /** * helper function to replace a nested property in an object with a new value * without mutating the object itself. * * @param object * @param path * @param value * @param [createPath=false] * If true, `path` will be created when (partly) missing in * the object. For correctly creating nested Arrays or * Objects, the function relies on `path` containing number * in case of array indexes. * If false (default), an error will be thrown when the * path doesn't exist. * @return Returns a new, updated object or array */ function setIn(object, path, value) { let createPath = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; if (path.length === 0) { return value; } const key = path[0]; // @ts-ignore const updatedValue = setIn(object ? object[key] : undefined, path.slice(1), value, createPath); if ((0, _typeguards.isJSONObject)(object) || (0, _typeguards.isJSONArray)(object)) { return applyProp(object, key, updatedValue); } if (createPath) { const newObject = IS_INTEGER_REGEX.test(key) ? [] : {}; // @ts-ignore newObject[key] = updatedValue; return newObject; } throw new Error('Path does not exist'); } const IS_INTEGER_REGEX = /^\d+$/; /** * helper function to replace a nested property in an object with a new value * without mutating the object itself. * * @return Returns a new, updated object or array */ function updateIn(object, path, transform) { if (path.length === 0) { return transform(object); } if (!(0, _utils.isObjectOrArray)(object)) { throw new Error("Path doesn't exist"); } const key = path[0]; // @ts-ignore const updatedValue = updateIn(object[key], path.slice(1), transform); // @ts-ignore return applyProp(object, key, updatedValue); } /** * helper function to delete a nested property in an object * without mutating the object itself. * * @return Returns a new, updated object or array */ function deleteIn(object, path) { if (path.length === 0) { return object; } if (!(0, _utils.isObjectOrArray)(object)) { throw new Error('Path does not exist'); } if (path.length === 1) { const key = path[0]; if (!(key in object)) { // key doesn't exist. return object unchanged return object; } const updatedObject = shallowClone(object); if ((0, _typeguards.isJSONArray)(updatedObject)) { updatedObject.splice(Number.parseInt(key), 1); } if ((0, _typeguards.isJSONObject)(updatedObject)) { delete updatedObject[key]; } return updatedObject; } const key = path[0]; // @ts-ignore const updatedValue = deleteIn(object[key], path.slice(1)); // @ts-ignore return applyProp(object, key, updatedValue); } /** * Insert a new item in an array at a specific index. * Example usage: * * insertAt({arr: [1,2,3]}, ['arr', '2'], 'inserted') // [1,2,'inserted',3] */ function insertAt(document, path, value) { const parentPath = path.slice(0, path.length - 1); const index = path[path.length - 1]; return updateIn(document, parentPath, items => { if (!Array.isArray(items)) { throw new TypeError(`Array expected at path ${JSON.stringify(parentPath)}`); } const updatedItems = shallowClone(items); updatedItems.splice(Number.parseInt(index), 0, value); return updatedItems; }); } /** * Transform a JSON object, traverse over the whole object, * and allow replacing Objects/Arrays/values. */ function transform(document, callback) { let path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; const updated1 = callback(document, path); if ((0, _typeguards.isJSONArray)(updated1)) { let updated2; for (let i = 0; i < updated1.length; i++) { const before = updated1[i]; // we stringify the index here, so the path only contains strings and can be safely // stringified/parsed to JSONPointer without loosing information. // We do not want to rely on path keys being numeric/string. const after = transform(before, callback, path.concat(String(i))); if (after !== before) { if (!updated2) { updated2 = shallowClone(updated1); } updated2[i] = after; } } return updated2 || updated1; } if ((0, _typeguards.isJSONObject)(updated1)) { let updated2; for (const key in updated1) { // biome-ignore lint/suspicious/noPrototypeBuiltins: keep using the old way for now for backward compatibility if (Object.hasOwnProperty.call(updated1, key)) { const before = updated1[key]; const after = transform(before, callback, path.concat(key)); if (after !== before) { if (!updated2) { updated2 = shallowClone(updated1); } updated2[key] = after; } } } return updated2 || updated1; } return updated1; } /** * Test whether a path exists in a JSON object * @return Returns true if the path exists, else returns false */ function existsIn(document, path) { if (document === undefined) { return false; } if (path.length === 0) { return true; } if (document === null) { return false; } // @ts-ignore return existsIn(document[path[0]], path.slice(1)); } //# sourceMappingURL=immutabilityHelpers.js.map