@irrelon/path
Version:
A powerful JSON path processor. Allows you to drill into and manipulate JSON objects with a simple dot-delimited path format e.g. "obj.name".
1,277 lines • 70 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.merge = exports.chop = exports.distill = exports.unSetImmutable = exports.pullValImmutable = exports.pushValImmutable = exports.setImmutable = exports.isNotEqual = exports.isEqual = exports.diff = exports.diffValues = exports.keyDedup = exports.findOnePath = exports.findPath = exports.match = exports.type = exports.countMatchingPathsInObject = exports.hasMatchingPathsInObject = exports.leafNodes = exports.countLeafNodes = exports.joinEscaped = exports.join = exports.flattenValues = exports.flatten = exports.values = exports.furthest = exports.splicePath = exports.pullVal = exports.pushVal = exports.decouple = exports.updateImmutable = exports.update = exports.unSet = exports.set = exports.getMany = exports.get = exports.unEscape = exports.escape = exports.split = exports.clean = exports.numberToWildcard = exports.wildcardToZero = exports.returnWhatWasGiven = exports.shift = exports.push = exports.pop = exports.down = exports.up = exports.isNonCompositePath = exports.isCompositePath = void 0;
exports.traverse = exports.query = exports.mergeImmutable = void 0;
/**
* @typedef {object} FindOptionsType
* @property {number} [maxDepth=Infinity] The maximum depth to scan inside
* the source object for matching data.
* @property {number} [currentDepth=0] The current depth of the
* operation scan.
* @property {boolean} [includeRoot=true] If true, will include the
* root source object if it matches the query.
*/
/**
* Scans an object for all keys that are either objects or arrays
* and returns an array of those keys only.
* @param {ObjectType} obj The object to scan.
* @returns {string[]} An array of string keys.
* @private
*/
const _iterableKeys = (obj) => {
return Object.entries(obj).reduce((arr, [key, val]) => {
const valType = (0, exports.type)(val);
if (valType === "object" || valType === "array") {
arr.push(key);
}
return arr;
}, []);
};
/**
* Creates a new instance of "item" that is dereferenced. Useful
* when you want to return a new version of "item" with the same
* data for immutable data structures.
* @param {ObjectType} item The item to mimic.
* @param {string} key The key to set data in.
* @param {*} val The data to set in the key.
* @returns {*} A new dereferenced version of "item" with the "key"
* containing the "val" data.
* @private
*/
const _newInstance = (item, key, val) => {
const objType = (0, exports.type)(item);
let newObj;
if (objType === "object") {
newObj = Object.assign({}, item);
}
if (objType === "array") {
// @ts-ignore
newObj = [...item];
}
if (key !== undefined) {
// @ts-ignore
newObj[key] = val;
}
return newObj;
};
/**
* Determines if the given path points to a root leaf node (has no delimiter)
* or contains a dot delimiter so will drill down before reaching a leaf node.
* If it has a delimiter, it is called a "composite" path.
* @param {string} path The path to evaluate.
* @returns {boolean} True if delimiter found, false if not.
*/
const isCompositePath = (path) => {
const regExp = /\./g;
let result;
while (result = regExp.exec(path)) {
// Check if the previous character was an escape
// and if so, ignore this delimiter
if (result.index === 0 || path.substr(result.index - 1, 1) !== "\\") {
// This is not an escaped path, so it IS a composite path
return true;
}
}
return false;
};
exports.isCompositePath = isCompositePath;
/**
* Provides the opposite of `isCompositePath()`. If a delimiter is found, this
* function returns false.
* @param {string} path The path to evaluate.
* @returns {boolean} False if delimiter found, true if not.
*/
const isNonCompositePath = (path) => {
return !(0, exports.isCompositePath)(path);
};
exports.isNonCompositePath = isNonCompositePath;
/**
* Returns the given path after removing the last
* leaf from the path. E.g. "foo.bar.thing" becomes
* "foo.bar".
* @param {string} path The path to operate on.
* @param {number} [levels=1] The number of levels to
* move up.
* @returns {string} The new path string.
*/
const up = (path, levels = 1) => {
const parts = (0, exports.split)(path);
for (let i = 0; i < levels; i++) {
parts.pop();
}
return parts.join(".");
};
exports.up = up;
/**
* Returns the given path after removing the first
* leaf from the path. E.g. "foo.bar.thing" becomes
* "bar.thing".
* @param {string} path The path to operate on.
* @param {number} [levels] The number of levels to
* move down.
* @returns {string} The new path string.
*/
const down = (path, levels = 1) => {
const parts = (0, exports.split)(path);
for (let i = 0; i < levels; i++) {
parts.shift();
}
return parts.join(".");
};
exports.down = down;
/**
* Returns the last leaf from the path. E.g.
* "foo.bar.thing" returns "thing".
* @param {string} path The path to operate on.
* @param {number} [levels] The number of levels to
* pop.
* @returns {string} The new path string.
*/
const pop = (path, levels = 1) => {
const parts = (0, exports.split)(path);
let part;
for (let i = 0; i < levels; i++) {
part = parts.pop();
}
return part || "";
};
exports.pop = pop;
/**
* Adds a leaf to the end of the path. E.g.
* pushing "goo" to path "foo.bar.thing" returns
* "foo.bar.thing.goo".
* @param {string} path The path to operate on.
* @param {string} val The string value to push
* to the end of the path.
* @returns {string} The new path string.
*/
const push = (path, val = "") => {
return `${path}.${val}`;
};
exports.push = push;
/**
* Returns the first leaf from the path. E.g.
* "foo.bar.thing" returns "foo".
* @param {string} path The path to operate on.
* @param {number} [levels=1] The number of levels to
* shift.
* @returns {string} The new path string.
*/
const shift = (path, levels = 1) => {
const parts = (0, exports.split)(path);
let part;
for (let i = 0; i < levels; i++) {
part = parts.shift();
}
return part || "";
};
exports.shift = shift;
/**
* A function that just returns the first argument.
* @param {*} val The argument to return.
* @param {*} [currentObj] The current object hierarchy.
* @returns {*} The passed argument.
*/
const returnWhatWasGiven = (val, currentObj) => val;
exports.returnWhatWasGiven = returnWhatWasGiven;
/**
* Converts any key matching the wildcard to a zero.
* @param {string} key The key to test.
* @param {*} [currentObj] The current object hierarchy.
* @returns {string} The key.
*/
const wildcardToZero = (key, currentObj) => {
return key === "$" ? "0" : key;
};
exports.wildcardToZero = wildcardToZero;
/**
* If a key is a number, will return a wildcard, otherwise
* will return the originally passed key.
* @param {string} key The key to test.
* @returns {string} The original key or a wildcard.
*/
const numberToWildcard = (key) => {
// Check if the key is a number
if (String(parseInt(key, 10)) === key) {
// The key is a number, convert to a wildcard
return "$";
}
return key;
};
exports.numberToWildcard = numberToWildcard;
/**
* Removes leading period (.) from string and returns new string.
* @param {string} str The string to clean.
* @returns {*} The cleaned string.
*/
const clean = (str) => {
if (!str) {
return str;
}
if (str.substr(0, 1) === ".") {
str = str.substr(1, str.length - 1);
}
return str;
};
exports.clean = clean;
/**
* Splits a path by period character, taking into account
* escaped period characters.
* @param {string} path The path to split into an array.
* @return {Array<string>} The component parts of the path, split
* by period character.
*/
const split = (path) => {
// Convert all \. (escaped periods) to another character
// temporarily
const escapedPath = path.replace(/\\\./g, "[--]");
const splitPath = escapedPath.split(".");
// Loop the split path array and convert any escaped period
// placeholders back to their real period characters
for (let i = 0; i < splitPath.length; i++) {
splitPath[i] = splitPath[i].replace(/\[--]/g, "\\.");
}
return splitPath;
};
exports.split = split;
/**
* Escapes any periods in the passed string so they will
* not be identified as part of a path. Useful if you have
* a path like "domains.www.google.com.data" where the
* "www.google.com" should not be considered part of the
* traversal as it is actually in an object like:
* {
* "domains": {
* "www.google.com": {
* "data": "foo"
* }
* }
* }
* @param {string} str The string to escape periods in.
* @return {string} The escaped string.
*/
const escape = (str) => {
return str.replace(/\./g, "\\.");
};
exports.escape = escape;
/**
* Converts a string previously escaped with the `escape()`
* function back to its original value.
* @param {string} str The string to unescape.
* @returns {string} The unescaped string.
*/
const unEscape = (str) => {
return str.replace(/\\./g, ".");
};
exports.unEscape = unEscape;
/**
* Gets a single value from the passed object and given path.
* @param {ObjectType} obj The object to operate on.
* @param {string} path The path to retrieve data from.
* @param {*=} defaultVal Optional default to return if the
* value retrieved from the given object and path equals undefined.
* @param {OptionsType} [options] Optional options object.
* @returns {*} The value retrieved from the passed object at
* the passed path.
*/
const get = (obj, path, defaultVal = undefined, options = {}) => {
let internalPath = path, objPart;
if (path instanceof Array) {
return path.map((individualPath) => {
(0, exports.get)(obj, individualPath, defaultVal, options);
});
}
options.transformRead = options.transformRead || exports.returnWhatWasGiven;
options.transformKey = options.transformKey || exports.returnWhatWasGiven;
options.transformWrite = options.transformWrite || exports.returnWhatWasGiven;
// No object data, return default data
if (obj === undefined || obj === null) {
return defaultVal;
}
// No path string, return the base obj
if (!internalPath) {
return obj;
}
// @ts-ignore
internalPath = (0, exports.clean)(internalPath);
// Path is not a string, throw error
if (typeof internalPath !== "string") {
throw new Error("Path argument must be a string");
}
// Path has no dot-notation, return key/value
if ((0, exports.isNonCompositePath)(internalPath)) {
// @ts-ignore
return obj[internalPath] !== undefined ? obj[internalPath] : defaultVal;
}
if (typeof obj !== "object") {
return defaultVal !== undefined ? defaultVal : undefined;
}
const pathParts = (0, exports.split)(internalPath);
objPart = obj;
for (let i = 0; i < pathParts.length; i++) {
const pathPart = pathParts[i];
const transformedKey = options.transformKey((0, exports.unEscape)(pathPart), objPart);
options.pathRoot = (0, exports.join)(options.pathRoot || "", pathPart);
// @ts-ignore
objPart = objPart[transformedKey];
const isPartAnArray = objPart instanceof Array;
if (isPartAnArray && options.wildcardExpansion === true) {
const nextKey = options.transformKey((0, exports.unEscape)(pathParts[i + 1] || ""), objPart);
if (nextKey === "$") {
// Define an array to store our results in down the tree
options.expandedResult = options.expandedResult || [];
// The key is a wildcard and wildcardExpansion is enabled
objPart.forEach((arrItem) => {
const innerKey = pathParts.slice(i + 2).join(".");
if (innerKey === "") {
// @ts-ignore
options.expandedResult.push(arrItem);
}
else {
const innerResult = (0, exports.get)(arrItem, innerKey, defaultVal, options);
if (innerKey.indexOf(".$") === -1) {
// @ts-ignore
options.expandedResult.push(innerResult);
}
}
});
return options.expandedResult.length !== 0 ? options.expandedResult : defaultVal;
}
}
if (isPartAnArray && options.arrayTraversal === true) {
// The data is an array and we have arrayTraversal enabled
// Check for auto-expansion
if (options.arrayExpansion === true) {
return (0, exports.getMany)(objPart, pathParts.slice(i + 1).join("."), defaultVal, options);
}
// Loop the array items and return the first non-undefined
// value from any array item leaf node that matches the path
for (let objPartIndex = 0; objPartIndex < objPart.length; objPartIndex++) {
const arrItem = objPart[objPartIndex];
const innerResult = (0, exports.get)(arrItem, pathParts.slice(i + 1).join("."), defaultVal, options);
if (innerResult !== undefined)
return innerResult;
}
return defaultVal;
}
else if ((!objPart || typeof objPart !== "object") && i !== pathParts.length - 1) {
// The path terminated in the object before we reached
// the end node we wanted so make sure we return undefined
return defaultVal;
}
}
return objPart !== undefined ? objPart : defaultVal;
};
exports.get = get;
/**
* Gets multiple values from the passed arr and given path.
* @param {ObjectType} data The array or object to operate on.
* @param {string} path The path to retrieve data from.
* @param {*=} defaultVal Optional default to return if the
* value retrieved from the given object and path equals undefined.
* @param {OptionsType} [options] Optional options object.
* @returns {Array}
*/
const getMany = (data, path, defaultVal = undefined, options = {}) => {
var _a, _b;
const isDataAnArray = data instanceof Array;
const pathRoot = options.pathRoot || "";
if (!isDataAnArray) {
const innerResult = (0, exports.get)(data, path, defaultVal, options);
const isInnerResultAnArray = innerResult instanceof Array;
(_b = (_a = options.pathData) === null || _a === void 0 ? void 0 : _a.directPaths) === null || _b === void 0 ? void 0 : _b.push((0, exports.join)(options.pathRoot || "", path));
if (isInnerResultAnArray)
return innerResult;
if (innerResult === undefined && defaultVal === undefined)
return [];
if (innerResult === undefined && defaultVal !== undefined)
return [defaultVal];
return [innerResult];
}
const parts = (0, exports.split)(path);
const firstPart = parts[0];
const pathRemainder = parts.slice(1).join(".");
const resultArr = data.reduce((innerResult, arrItem, arrIndex) => {
var _a, _b;
const isArrItemAnArray = arrItem[firstPart] instanceof Array;
if (isArrItemAnArray) {
options.pathRoot = (0, exports.join)(pathRoot || "", String(arrIndex), firstPart);
const recurseResult = (0, exports.getMany)(arrItem[firstPart], pathRemainder, defaultVal, options);
innerResult.push(...recurseResult);
return innerResult;
}
const val = (0, exports.get)(arrItem, path, defaultVal, options);
if (val !== undefined) {
(_b = (_a = options.pathData) === null || _a === void 0 ? void 0 : _a.directPaths) === null || _b === void 0 ? void 0 : _b.push((0, exports.join)(options.pathRoot || "", String(arrIndex), path));
innerResult.push(val);
}
return innerResult;
}, []);
if (resultArr.length === 0 && defaultVal !== undefined)
return [defaultVal];
return resultArr;
};
exports.getMany = getMany;
/**
* Sets a single value on the passed object and given path. This
* will directly modify the "obj" object. If you need immutable
* updates, use setImmutable() instead.
* @param {ObjectType} obj The object to operate on.
* @param {string} path The path to set data on.
* @param {*} val The value to assign to the obj at the path.
* @param {SetOptionsType} [options] The options object.
* @returns {*} Nothing.
*/
const set = (obj, path, val, options = {}) => {
let internalPath = path, objPart;
options.transformRead = options.transformRead || exports.returnWhatWasGiven;
options.transformKey = options.transformKey || exports.returnWhatWasGiven;
options.transformWrite = options.transformWrite || exports.returnWhatWasGiven;
// No object data
if (obj === undefined || obj === null) {
return;
}
// No path string
if (!internalPath) {
return;
}
internalPath = (0, exports.clean)(internalPath);
// Path is not a string, throw error
if (typeof internalPath !== "string") {
throw new Error("Path argument must be a string");
}
if (typeof obj !== "object") {
return;
}
// Path has no dot-notation, set key/value
if ((0, exports.isNonCompositePath)(internalPath)) {
const unescapedPath = (0, exports.unEscape)(internalPath);
// Do not allow prototype pollution
if (unescapedPath === "__proto__") {
return obj;
}
obj = (0, exports.decouple)(obj, options);
// @ts-ignore
obj[options.transformKey(unescapedPath)] = val;
return obj;
}
const newObj = (0, exports.decouple)(obj, options);
const pathParts = (0, exports.split)(internalPath);
const pathPart = pathParts.shift();
// @ts-ignore
const transformedPathPart = options.transformKey(pathPart);
// Do not allow prototype pollution
if (transformedPathPart === "__proto__") {
return obj;
}
let childPart = newObj[transformedPathPart];
if (typeof childPart !== "object" || childPart === null) {
// Create an object or array on the path
if (String(parseInt(transformedPathPart, 10)) === transformedPathPart || (pathParts.length > 0 && String(parseInt(pathParts[0], 10)) === pathParts[0])) {
// This is an array index
newObj[transformedPathPart] = [];
}
else {
newObj[transformedPathPart] = {};
}
objPart = newObj[transformedPathPart];
}
else {
objPart = childPart;
}
return (0, exports.set)(newObj, transformedPathPart, (0, exports.set)(objPart, pathParts.join("."), val, options), options);
};
exports.set = set;
/**
* Deletes a key from an object by the given path.
* @param {ObjectType} obj The object to operate on.
* @param {string} path The path to delete.
* @param {SetOptionsType} [options] The options object.
* @param {Object=} tracking Do not use.
*/
const unSet = (obj, path, options = {}, tracking = {}) => {
let internalPath = path;
options.transformRead = options.transformRead || exports.returnWhatWasGiven;
options.transformKey = options.transformKey || exports.returnWhatWasGiven;
options.transformWrite = options.transformWrite || exports.returnWhatWasGiven;
// No object data
if (obj === undefined || obj === null) {
return;
}
// No path string
if (!internalPath) {
return;
}
internalPath = (0, exports.clean)(internalPath);
// Path is not a string, throw error
if (typeof internalPath !== "string") {
throw new Error("Path argument must be a string");
}
if (typeof obj !== "object") {
return;
}
const newObj = (0, exports.decouple)(obj, options);
// Path has no dot-notation, set key/value
if ((0, exports.isNonCompositePath)(internalPath)) {
const unescapedPath = (0, exports.unEscape)(internalPath);
// Do not allow prototype pollution
if (unescapedPath === "__proto__")
return obj;
if (newObj.hasOwnProperty(unescapedPath)) {
// @ts-ignore
delete newObj[options.transformKey(unescapedPath)];
return newObj;
}
tracking.returnOriginal = true;
return obj;
}
const pathParts = (0, exports.split)(internalPath);
const pathPart = pathParts.shift();
// @ts-ignore
const transformedPathPart = options.transformKey((0, exports.unEscape)(pathPart));
// Do not allow prototype pollution
if (transformedPathPart === "__proto__")
return obj;
let childPart = newObj[transformedPathPart];
if (!childPart) {
// No child part available, nothing to unset!
tracking.returnOriginal = true;
return obj;
}
newObj[transformedPathPart] = (0, exports.unSet)(childPart, pathParts.join("."), options, tracking);
if (tracking.returnOriginal) {
return obj;
}
return newObj;
};
exports.unSet = unSet;
/**
* Takes an update object or array and iterates the keys of it, then
* sets data on the target object or array at the specified path with
* the corresponding value from the path key, effectively doing
* multiple set() operations in a single call. This will directly
* modify the "obj" object. If you need immutable updates, use
* updateImmutable() instead.
* @param {ObjectType} obj The object to operate on.
* @param {string} [basePath=""] The path to the object to operate on relative
* to the `obj`. If `obj` is the object to be directly operated on, leave
* `basePath` as an empty string.
* @param {ObjectType} updateData The update data to apply with
* keys as string paths.
* @param {SetOptionsType} options The options object.
* @returns {*} The object with the modified data.
*/
const update = (obj, basePath = "", updateData, options = {}) => {
let newObj = obj;
for (let path in updateData) {
if (updateData.hasOwnProperty(path)) {
// @ts-ignore
const data = updateData[path];
newObj = (0, exports.set)(newObj, (0, exports.join)(basePath, path), data, options);
}
}
return newObj;
};
exports.update = update;
/**
* Same as update() but will not change or modify the existing `obj`.
* References to objects that were not modified remain the same.
* @param {ObjectType} obj The object to operate on.
* @param {string} [basePath=""] The path to the object to operate on relative
* to the `obj`. If `obj` is the object to be directly operated on, leave
* `basePath` as an empty string.
* @param {ObjectType} updateData The update data to apply with
* keys as string paths.
* @param {SetOptionsType} [options] The options object.
* @returns {*} The new object with the modified data.
*/
const updateImmutable = (obj, basePath = "", updateData, options = {}) => {
return (0, exports.update)(obj, basePath, updateData, Object.assign(Object.assign({}, options), { immutable: true }));
};
exports.updateImmutable = updateImmutable;
/**
* If `options.immutable` === true then return a new de-referenced
* instance of the passed object/array. If immutable is false
* then simply return the same `obj` that was passed.
* @param {*} obj The object or array to decouple.
* @param {OptionsType} [options] The options object.
* @param {boolean} options.immutable
* @returns {*} The new decoupled instance (if immutable is true)
* or the original `obj` if immutable is false.
*/
const decouple = (obj, options = {}) => {
if (!options.immutable) {
return obj;
}
return _newInstance(obj);
};
exports.decouple = decouple;
/**
* Push a value to an array on an object for the specified path.
* @param {ObjectType} obj The object to update.
* @param {string} path The path to the array to push to.
* @param {*} val The value to push to the array at the object path.
* @param {OptionsType} [options] An options object.
* @returns {ObjectType} The original object passed in "obj" but with
* the array at the path specified having the newly pushed value.
*/
const pushVal = (obj, path, val, options = {}) => {
if (obj === undefined || obj === null || path === undefined) {
return obj;
}
// Clean the path
path = (0, exports.clean)(path);
const pathParts = (0, exports.split)(path);
const part = pathParts.shift();
if (part === "__proto__")
return obj;
if (pathParts.length) {
// Generate the path part in the object if it does not already exist
// @ts-ignore
obj[part] = (0, exports.decouple)(obj[part], options) || {};
// Recurse
// @ts-ignore
(0, exports.pushVal)(obj[part], pathParts.join("."), val, options);
}
else if (part) {
// We have found the target array, push the value
// @ts-ignore
obj[part] = (0, exports.decouple)(obj[part], options) || [];
// @ts-ignore
if (!(obj[part] instanceof Array)) {
throw ("Cannot push to a path whose leaf node is not an array!");
}
// @ts-ignore
obj[part].push(val);
}
else {
// We have found the target array, push the value
obj = (0, exports.decouple)(obj, options) || [];
if (!(obj instanceof Array)) {
throw ("Cannot push to a path whose leaf node is not an array!");
}
obj.push(val);
}
return (0, exports.decouple)(obj, options);
};
exports.pushVal = pushVal;
/**
* Pull a value to from an array at the specified path. Removes the first
* matching value, not every matching value.
* @param {ObjectType} obj The object to update.
* @param {string} path The path to the array to pull from.
* @param {*} val The value to pull from the array.
* @param {OptionsType} [options] An options object.
* @returns {ObjectType} The original object passed in "obj" but with
* the array at the path specified having removed the newly pulled value.
*/
const pullVal = (obj, path, val, options = { strict: true }) => {
if (obj === undefined || obj === null || path === undefined) {
return obj;
}
// Clean the path
path = (0, exports.clean)(path);
const pathParts = (0, exports.split)(path);
const part = pathParts.shift();
if (part === "__proto__")
return obj;
obj = (0, exports.decouple)(obj, options);
if (pathParts.length && part !== undefined) {
// Recurse - we don't need to assign obj[part] the result of this call because
// we are modifying by reference since we haven't reached the furthest path
// part (leaf) node yet
obj[part] = (0, exports.pullVal)(obj[part], pathParts.join("."), val, options);
}
else if (part) {
// Recurse - this is the leaf node so assign the response to obj[part] in
// case it is set to an immutable response
obj[part] = (0, exports.pullVal)(obj[part], "", val, options);
}
else {
// The target array is the root object, pull the value
if (!(obj instanceof Array)) {
throw ("Cannot pull from a path whose leaf node is not an array!");
}
let index;
// Find the index of the passed value
if (options.strict === true) {
index = obj.indexOf(val);
}
else {
// Do a non-strict check
index = obj.findIndex((item) => {
return (0, exports.match)(item, val);
});
}
if (index > -1) {
// Remove the item from the array
obj.splice(index, 1);
}
}
return obj;
};
exports.pullVal = pullVal;
/**
* Inserts or deletes from/into the array at the specified path.
* @param {ObjectType} obj The object to update.
* @param {string} path The path to the array to operate on.
* @param {number} start The index to operate from.
* @param {number} deleteCount The number of items to delete.
* @param {any[]} itemsToAdd The items to add to the array or an empty array
* if no items are to be added.
* @param {OptionsType} [options] An options object.
* @returns {ObjectType} The original object passed in "obj" but with
* the array at the path specified having inserted or removed based on splice.
*/
const splicePath = (obj, path, start, deleteCount, itemsToAdd = [], options = { strict: true }) => {
if (obj === undefined || obj === null || path === undefined) {
return obj;
}
// Clean the path
path = (0, exports.clean)(path);
const pathParts = (0, exports.split)(path);
const part = pathParts.shift();
if (part === "__proto__")
return obj;
obj = (0, exports.decouple)(obj, options);
if (pathParts.length && part !== undefined) {
// Recurse - we don't need to assign obj[part] the result of this call because
// we are modifying by reference since we haven't reached the furthest path
// part (leaf) node yet
obj[part] = (0, exports.splicePath)(obj[part], pathParts.join("."), start, deleteCount, itemsToAdd, options);
}
else if (part) {
if (!(obj[part] instanceof Array)) {
throw ("Cannot splice from a path whose leaf node is not an array!");
}
// We've reached our destination leaf node
// Remove the item from the array
obj[part] = (0, exports.decouple)(obj[part], options);
obj[part].splice(start, deleteCount, ...itemsToAdd);
}
else {
if (!(obj instanceof Array)) {
throw ("Cannot splice from a path whose leaf node is not an array!");
}
// We've reached our destination leaf node
// Remove the item from the array
obj.splice(start, deleteCount, ...itemsToAdd);
}
return obj;
};
exports.splicePath = splicePath;
/**
* Given a path and an object, determines the outermost leaf node
* that can be reached where the leaf value is not undefined.
* @param {ObjectType} obj The object to operate on.
* @param {string} path The path to retrieve data from.
* @param {OptionsType} [options] Optional options object.
* @returns {string} The path to the furthest non-undefined value.
*/
const furthest = (obj, path, options = {}) => {
let internalPath = path, objPart;
options.transformRead = options.transformRead || exports.returnWhatWasGiven;
options.transformKey = options.transformKey || exports.wildcardToZero;
options.transformWrite = options.transformWrite || exports.returnWhatWasGiven;
const finalPath = [];
// No path string, return the base obj
if (!internalPath) {
return finalPath.join(".");
}
internalPath = (0, exports.clean)(internalPath);
// Path is not a string, throw error
if (typeof internalPath !== "string") {
throw new Error("Path argument must be a string");
}
if (typeof obj !== "object" || obj === null) {
return finalPath.join(".");
}
// Path has no dot-notation, return key/value
if ((0, exports.isNonCompositePath)(internalPath)) {
if (obj[internalPath] !== undefined) {
return internalPath;
}
return finalPath.join(".");
}
const pathParts = (0, exports.split)(internalPath);
objPart = obj;
for (let i = 0; i < pathParts.length; i++) {
const pathPart = pathParts[i];
objPart = objPart[options.transformKey((0, exports.unEscape)(pathPart))];
if (objPart === undefined) {
break;
}
finalPath.push(pathPart);
}
return finalPath.join(".");
};
exports.furthest = furthest;
/**
* Traverses the object by the given path and returns an object where
* each key is a path pointing to a leaf node and contains the value
* from the leaf node from the overall object in the obj argument,
* essentially providing all available paths in an object and all the
* values for each path.
* @param {ObjectType} obj The object to operate on.
* @param {string} path The path to retrieve data from.
* @param {OptionsType} [options] Optional options object.
* @returns {ObjectType} The result of the traversal.
*/
const values = (obj, path, options = {}) => {
const internalPath = (0, exports.clean)(path);
const pathParts = (0, exports.split)(internalPath);
const currentPath = [];
const valueData = {};
options.transformRead = options.transformRead || exports.returnWhatWasGiven;
options.transformKey = options.transformKey || exports.returnWhatWasGiven;
options.transformWrite = options.transformWrite || exports.returnWhatWasGiven;
for (let i = 0; i < pathParts.length; i++) {
const pathPart = options.transformKey(pathParts[i]);
currentPath.push(pathPart);
const tmpPath = currentPath.join(".");
// @ts-ignore
valueData[tmpPath] = (0, exports.get)(obj, tmpPath);
}
// @ts-ignore
return valueData;
};
exports.values = values;
/**
* Takes an object and finds all paths, then returns the paths as an
* array of strings.
* @param obj The object to scan.
* @param finalArr An object used to collect the path keys.
* (Do not pass this in directly - use undefined).
* @param parentPath The path of the parent object. (Do not
* pass this in directly - use undefined).
* @param [options] An options object.
* @param [objCache] Internal, do not use.
* @returns An array containing path strings.
*/
const flatten = (obj, finalArr = [], parentPath = "", options = {}, objCache = []) => {
options.transformRead = options.transformRead || exports.returnWhatWasGiven;
options.transformKey = options.transformKey || exports.returnWhatWasGiven;
options.transformWrite = options.transformWrite || exports.returnWhatWasGiven;
const transformedObj = options.transformRead(obj);
// Check that we haven't visited this object before (avoid infinite recursion)
// @ts-ignore
if (objCache.indexOf(transformedObj) > -1) {
return finalArr;
}
// Add object to cache to make sure we don't traverse it twice
// @ts-ignore
objCache.push(transformedObj);
const currentPath = (key) => {
// @ts-ignore
const tKey = options.transformKey(key);
return parentPath ? (0, exports.join)(parentPath, tKey) : tKey;
};
for (const i in transformedObj) {
if (!transformedObj.hasOwnProperty(i))
continue;
if (options.ignore && options.ignore.test(i)) {
continue;
}
const pathToChild = currentPath(i);
const childObj = transformedObj[i];
finalArr.push(pathToChild);
if (typeof childObj === "object" && childObj !== null) {
(0, exports.flatten)(childObj, finalArr, pathToChild, options, objCache);
}
}
return finalArr;
};
exports.flatten = flatten;
/**
* Takes an object and finds all paths, then returns the paths as keys
* and the values of each path as the values.
* @param {ObjectType} obj The object to scan.
* @param {Object=} finalObj An object used to collect the path keys.
* (Do not pass this in directly).
* @param {string=} parentPath The path of the parent object. (Do not
* pass this in directly).
* @param {OptionsType} [options] An options object.
* @param {any[]} [objCache] Internal, do not use.
* @returns {ObjectType} An object containing path keys and their values.
*/
const flattenValues = (obj, finalObj = {}, parentPath = "", options = {}, objCache = []) => {
options.transformRead = options.transformRead || exports.returnWhatWasGiven;
options.transformKey = options.transformKey || exports.returnWhatWasGiven;
options.transformWrite = options.transformWrite || exports.returnWhatWasGiven;
const transformedObj = options.transformRead(obj);
// Check that we haven't visited this object before (avoid infinite recursion)
// @ts-ignore
if (objCache.indexOf(transformedObj) > -1) {
// @ts-ignore
return finalObj;
}
// Add object to cache to make sure we don't traverse it twice
// @ts-ignore
objCache.push(transformedObj);
const currentPath = (i, info) => {
// @ts-ignore
const tKey = options.transformKey(i, info);
return parentPath ? parentPath + "." + tKey : tKey;
};
for (const i in transformedObj) {
if (transformedObj.hasOwnProperty(i)) {
const type = typeof transformedObj[i];
const info = {
type,
isArrayIndex: Array.isArray(transformedObj),
isFlat: type !== "object" || transformedObj[i] instanceof Date || transformedObj[i] instanceof RegExp
};
const pathKey = currentPath(i, info);
if (!info.isFlat) {
if (transformedObj[i] !== null) {
(0, exports.flattenValues)(transformedObj[i], finalObj, pathKey, options, objCache);
}
}
else if (options.leavesOnly) {
// Found leaf node!
// @ts-ignore
finalObj[pathKey] = options.transformWrite(transformedObj[i]);
}
if (!options.leavesOnly) {
// @ts-ignore
finalObj[pathKey] = options.transformWrite(transformedObj[i]);
}
}
}
// @ts-ignore
return finalObj;
};
exports.flattenValues = flattenValues;
/**
* Joins multiple string arguments into a path string.
* Ignores blank or undefined path parts and also ensures
* that each part is escaped so passing "foo.bar" will
* result in an escaped version.
* @param args args Path to join.
* @returns A final path string.
*/
const join = (...args) => {
return args.reduce((arr, item) => {
if (item !== undefined && String(item)) {
// @ts-ignore
arr.push(item);
}
return arr;
}, []).join(".");
};
exports.join = join;
/**
* Joins multiple string arguments into a path string.
* Ignores blank or undefined path parts and also ensures
* that each part is escaped so passing "foo.bar" will
* result in an escaped version.
* @param {...string} args Path to join.
* @returns {string} A final path string.
*/
const joinEscaped = (...args) => {
const escapedArgs = args.map((item) => {
return (0, exports.escape)(item);
});
return (0, exports.join)(...escapedArgs);
};
exports.joinEscaped = joinEscaped;
/**
* Counts the total number of key leaf nodes in the passed object.
* @param {ObjectType} obj The object to count key leaf nodes for.
* @param {Array=} objCache Do not use. Internal array to track
* visited leafs.
* @returns {number} The number of keys.
*/
const countLeafNodes = (obj, objCache = []) => {
let totalKeys = 0;
// Add object to cache to make sure we don't traverse it twice
objCache.push(obj);
for (const i in obj) {
if (obj.hasOwnProperty(i)) {
if (obj[i] !== undefined) {
if (obj[i] === null || typeof obj[i] !== "object" || objCache.indexOf(obj[i]) > -1) {
totalKeys++;
}
else {
totalKeys += (0, exports.countLeafNodes)(obj[i], objCache);
}
}
}
}
return totalKeys;
};
exports.countLeafNodes = countLeafNodes;
/**
* Finds all the leaf nodes for a given object and returns an array of paths
* to them. This is different from `flatten()` in that it only includes leaf
* nodes and will not include every intermediary path traversed to get to a
* leaf node.
* @param {ObjectType} obj The object to traverse.
* @param {string} [parentPath=""] The path to use as a root/base path to
* start scanning for leaf nodes under.
* @param {any[]} [objCache=[]] Internal usage to check for cyclic structures.
* @returns {[]}
*/
const leafNodes = (obj, parentPath = "", objCache = []) => {
const paths = [];
// Add object to cache to make sure we don't traverse it twice
objCache.push(obj);
for (const i in obj) {
if (obj.hasOwnProperty(i)) {
if (obj[i] !== undefined) {
const currentPath = (0, exports.join)(parentPath, i);
if (obj[i] === null || typeof obj[i] !== "object" || objCache.indexOf(obj[i]) > -1) {
paths.push(currentPath);
}
else {
paths.push(...(0, exports.leafNodes)(obj[i], currentPath, objCache));
}
}
}
}
return paths;
};
exports.leafNodes = leafNodes;
/**
* Tests if the passed object has the paths that are specified and that
* a value exists in those paths. MAY NOT BE INFINITE RECURSION SAFE.
* @param {ObjectType} testKeys The object describing the paths to test for.
* @param {ObjectType} testObj The object to test paths against.
* @returns {boolean} True if the object paths exist.
*/
const hasMatchingPathsInObject = (testKeys, testObj) => {
let result = true;
for (const i in testKeys) {
if (testKeys.hasOwnProperty(i)) {
if (testObj[i] === undefined) {
return false;
}
if (typeof testKeys[i] === "object" && testKeys[i] !== null) {
// Recurse object
result = (0, exports.hasMatchingPathsInObject)(testKeys[i], testObj[i]);
// Should we exit early?
if (!result) {
return false;
}
}
}
}
return result;
};
exports.hasMatchingPathsInObject = hasMatchingPathsInObject;
/**
* Tests if the passed object has the paths that are specified and that
* a value exists in those paths and if so returns the number matched.
* MAY NOT BE INFINITE RECURSION SAFE.
* @param {ObjectType} testKeys The object describing the paths to test for.
* @param {ObjectType} testObj The object to test paths against.
* @returns {{matchedKeys: ObjectType, matchedKeyCount: number, totalKeyCount: number}} Stats on the matched keys.
*/
const countMatchingPathsInObject = (testKeys, testObj) => {
const matchedKeys = {};
let matchData, matchedKeyCount = 0, totalKeyCount = 0;
for (const i in testObj) {
if (testObj.hasOwnProperty(i)) {
if (typeof testObj[i] === "object" && testObj[i] !== null) {
// The test / query object key is an object, recurse
matchData = (0, exports.countMatchingPathsInObject)(testKeys[i], testObj[i]);
// @ts-ignore
matchedKeys[i] = matchData.matchedKeys;
totalKeyCount += matchData.totalKeyCount;
matchedKeyCount += matchData.matchedKeyCount;
}
else {
// The test / query object has a property that is not an object so add it as a key
totalKeyCount++;
// Check if the test keys also have this key and it is also not an object
if (testKeys && testKeys[i] && (typeof testKeys[i] !== "object" || testKeys[i] === null)) {
// @ts-ignore
matchedKeys[i] = true;
matchedKeyCount++;
}
else {
// @ts-ignore
matchedKeys[i] = false;
}
}
}
}
return {
matchedKeys,
matchedKeyCount,
totalKeyCount
};
};
exports.countMatchingPathsInObject = countMatchingPathsInObject;
/**
* Returns the type from the item passed. Similar to JavaScript's
* built-in typeof except it will distinguish between arrays, nulls
* and objects as well.
* @param item The item to get the type of.
* @returns The string name of the type.
*/
const type = (item) => {
if (item === null) {
return "null";
}
if (Array.isArray(item)) {
return "array";
}
return typeof item;
};
exports.type = type;
/**
* Determines if the query data exists anywhere inside the source
* data. Will recurse into arrays and objects to find query.
* @param {*} source The source data to check.
* @param {*} query The query data to find.
* @param {OptionsType} [options] An options object.
* @returns {boolean} True if query was matched, false if not.
*/
const match = (source, query, options = {}) => {
const sourceType = typeof source;
const queryType = typeof query;
if (sourceType !== queryType) {
return false;
}
if (sourceType !== "object" || source === null) {
// Simple test
return source === query;
}
// The source is an object-like (array or object) structure
const entries = Object.entries(query);
const foundNonMatch = entries.find(([key, val]) => {
// Recurse if type is array or object
if (typeof val === "object" && val !== null) {
return !(0, exports.match)(source[key], val);
}
return source[key] !== val;
});
return !foundNonMatch;
};
exports.match = match;
/**
* Finds all items in `source` that match the structure of `query` and
* returns the path to them as an array of strings.
* @param {*} source The source to test.
* @param {*} query The query to match.
* @param {FindOptionsType} [options] Options object.
* @param {string=""} parentPath Do not use. The aggregated
* path to the current structure in source.
* @returns {Object} Contains match<Boolean> and path<Array>.
*/
const findPath = (source, query, options = {
maxDepth: Infinity,
currentDepth: 0,
includeRoot: true
}, parentPath = "") => {
const resultArr = [];
const sourceType = typeof source;
options = Object.assign({ maxDepth: Infinity, currentDepth: 0, includeRoot: true }, options);
if (options.currentDepth !== 0 || (options.currentDepth === 0 && options.includeRoot)) {
if ((0, exports.match)(source, query)) {
resultArr.push(parentPath);
}
}
// @ts-ignore
options.currentDepth++;
// @ts-ignore
if (options.currentDepth <= options.maxDepth && sourceType === "object") {
for (let key in source) {
if (source.hasOwnProperty(key)) {
const val = source[key];
// Recurse down object to find more instances
const result = (0, exports.findPath)(val, query, options, (0, exports.join)(parentPath, key));
// @ts-ignore
if (result.match) {
// @ts-ignore
resultArr.push(...result.path);
}
}
}
}
return { match: resultArr.length > 0, path: resultArr };
};
exports.findPath = findPath;
/**
* Finds the first item that matches the structure of `query`
* and returns the path to it.
* @param {*} source The source to test.
* @param {*} query The query to match.
* @param {FindOptionsType} [options] Options object.
* @param {string=""} parentPath Do not use. The aggregated
* path to the current structure in source.
* @returns {Object} Contains match<boolean> and path<string>.
*/
const findOnePath = (source, query, options = {
maxDepth: Infinity,
currentDepth: 0,
includeRoot: true
}, parentPath = "") => {
const sourceType = typeof source;
options = Object.assign({ maxDepth: Infinity, currentDepth: 0, includeRoot: true }, options);
if (options.currentDepth !== 0 || (options.currentDepth === 0 && options.includeRoot)) {
if ((0, exports.match)(source, query)) {
return {
match: true,
path: parentPath
};
}
}
// @ts-ignore
options.currentDepth++;
// @ts-ignore
if (options.currentDepth <= options.maxDepth && sourceType === "object" && source !== null) {
for (let key in source) {
if (source.hasOwnProperty(key)) {
const val = source[key];
// Recurse down object to find more instances
const subPath = (0, exports.join)(parentPath, key);
const result = (0, exports.findOnePath)(val, query, options, subPath);
// @ts-ignore
if (result.match) {
return result;
}
}
}
}
return { match: false };
};
exports.findOnePath = findOnePath;
/**
* Returns a deduplicated array of strings.
* @param {string[]} keys An array of strings to deduplicate.
* @returns {string[]} The deduplicated array.
*/
const keyDedup = (keys) => {
return keys.filter((elem, pos, arr) => {
return arr.indexOf(elem) === pos;
});
};
exports.keyDedup = keyDedup;
/**
* Compares two provided objects / arrays and returns and object
* where the keys are dot-notation paths and the values are any
* differences.
*
* e.g.
* {
* "path.to.new.value": {
* "val1": "the value from obj1",
* "val2": "the value from obj2",
* "type1": "string", // the value type from obj1 (see ValueType for supported values)
* "type2": "string", // the value type from obj2 (see ValueType for supported values)
* "difference": "value" // (see DifferenceType for supported values)
* }
* }
*
* If you only want an array of paths to values that have changed
* see the `diff()` function instead.
* @param {ObjectType} obj1 The first object / array to compare.
* @param {ObjectType} obj2 The second object / array to compare.
* @param {DiffOptionsType} [options] Options object
* @param {string|string[]} options.basePath="" The base path from which to check for
* differences. Differences outside the base path will not be
* returned as part of the array of differences. Leave blank to check
* for all differences between the two objects to compare.
* @param {boolean}