immutable-json-patch
Version:
Immutable JSON patch with support for reverting operations
270 lines (257 loc) • 7.73 kB
JavaScript
;
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