UNPKG

forerunnerdb

Version:

A NoSQL document store database for browsers and Node.js.

445 lines (376 loc) 10.1 kB
"use strict"; var Shared = require('./Shared'); /** * Path object used to resolve object paths and retrieve data from * objects by using paths. * @param {String=} path The path to assign. * @constructor */ var Path = function (path) { this.init.apply(this, arguments); }; Path.prototype.init = function (path) { if (path) { this.path(path); } }; Shared.addModule('Path', Path); Shared.mixin(Path.prototype, 'Mixin.Common'); Shared.mixin(Path.prototype, 'Mixin.ChainReactor'); /** * Gets / sets the given path for the Path instance. * @param {String=} path The path to assign. */ Path.prototype.path = function (path) { if (path !== undefined) { this._path = this.clean(path); this._pathParts = this._path.split('.'); return this; } return this._path; }; /** * Tests if the passed object has the paths that are specified and that * a value exists in those paths. * @param {Object} testKeys The object describing the paths to test for. * @param {Object} testObj The object to test paths against. * @returns {Boolean} True if the object paths exist. */ Path.prototype.hasObjectPaths = function (testKeys, testObj) { var result = true, i; for (i in testKeys) { if (testKeys.hasOwnProperty(i)) { if (testObj[i] === undefined) { return false; } if (typeof testKeys[i] === 'object') { // Recurse object result = this.hasObjectPaths(testKeys[i], testObj[i]); // Should we exit early? if (!result) { return false; } } } } return result; }; /** * Counts the total number of key endpoints in the passed object. * @param {Object} testObj The object to count key endpoints for. * @returns {Number} The number of endpoints. */ Path.prototype.countKeys = function (testObj) { var totalKeys = 0, i; for (i in testObj) { if (testObj.hasOwnProperty(i)) { if (testObj[i] !== undefined) { if (typeof testObj[i] !== 'object') { totalKeys++; } else { totalKeys += this.countKeys(testObj[i]); } } } } return totalKeys; }; /** * 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. * @param {Object} testKeys The object describing the paths to test for. * @param {Object} testObj The object to test paths against. * @returns {Object} Stats on the matched keys */ Path.prototype.countObjectPaths = function (testKeys, testObj) { var matchData, matchedKeys = {}, matchedKeyCount = 0, totalKeyCount = 0, i; for (i in testObj) { if (testObj.hasOwnProperty(i)) { if (typeof testObj[i] === 'object') { // The test / query object key is an object, recurse matchData = this.countObjectPaths(testKeys[i], testObj[i]); 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') { matchedKeys[i] = true; matchedKeyCount++; } else { matchedKeys[i] = false; } } } } return { matchedKeys: matchedKeys, matchedKeyCount: matchedKeyCount, totalKeyCount: totalKeyCount }; }; /** * Takes a non-recursive object and converts the object hierarchy into * a path string. * @param {Object} obj The object to parse. * @param {Boolean=} withValue If true will include a 'value' key in the returned * object that represents the value the object path points to. * @returns {Object} */ Path.prototype.parse = function (obj, withValue) { var paths = [], path = '', resultData, i, k; for (i in obj) { if (obj.hasOwnProperty(i)) { // Set the path to the key path = i; if (typeof(obj[i]) === 'object') { if (withValue) { resultData = this.parse(obj[i], withValue); for (k = 0; k < resultData.length; k++) { paths.push({ path: path + '.' + resultData[k].path, value: resultData[k].value }); } } else { resultData = this.parse(obj[i]); for (k = 0; k < resultData.length; k++) { paths.push({ path: path + '.' + resultData[k].path }); } } } else { if (withValue) { paths.push({ path: path, value: obj[i] }); } else { paths.push({ path: path }); } } } } return paths; }; /** * Takes a non-recursive object and converts the object hierarchy into * an array of path strings that allow you to target all possible paths * in an object. * * @returns {Array} */ Path.prototype.parseArr = function (obj, options) { options = options || {}; return this._parseArr(obj, '', [], options); }; Path.prototype._parseArr = function (obj, path, paths, options) { var i, newPath = ''; path = path || ''; paths = paths || []; for (i in obj) { if (obj.hasOwnProperty(i)) { if (!options.ignore || (options.ignore && !options.ignore.test(i))) { if (path) { newPath = path + '.' + i; } else { newPath = i; } if (typeof(obj[i]) === 'object') { this._parseArr(obj[i], newPath, paths, options); } else { paths.push(newPath); } } } } return paths; }; /** * Gets the value(s) that the object contains for the currently assigned path string. * @param {Object} obj The object to evaluate the path against. * @param {String=} path A path to use instead of the existing one passed in path(). * @returns {Array} An array of values for the given path. */ Path.prototype.value = function (obj, path) { if (obj !== undefined && typeof obj === 'object') { var pathParts, arr, arrCount, objPart, objPartParent, valuesArr = [], i, k; if (path !== undefined) { path = this.clean(path); pathParts = path.split('.'); } arr = pathParts || this._pathParts; arrCount = arr.length; objPart = obj; for (i = 0; i < arrCount; i++) { objPart = objPart[arr[i]]; if (objPartParent instanceof Array) { // Search inside the array for the next key for (k = 0; k < objPartParent.length; k++) { valuesArr = valuesArr.concat(this.value(objPartParent, k + '.' + arr[i])); } return valuesArr; } else { if (!objPart || typeof(objPart) !== 'object') { break; } } objPartParent = objPart; } return [objPart]; } else { return []; } }; /** * Sets a value on an object for the specified path. * @param {Object} obj The object to update. * @param {String} path The path to update. * @param {*} val The value to set the object path to. * @returns {*} */ Path.prototype.set = function (obj, path, val) { if (obj !== undefined && path !== undefined) { var pathParts, part; path = this.clean(path); pathParts = path.split('.'); part = pathParts.shift(); if (pathParts.length) { // Generate the path part in the object if it does not already exist obj[part] = obj[part] || {}; // Recurse this.set(obj[part], pathParts.join('.'), val); } else { // Set the value obj[part] = val; } } return obj; }; Path.prototype.get = function (obj, path) { return this.value(obj, path)[0]; }; /** * Push a value to an array on an object for the specified path. * @param {Object} 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. * @returns {*} */ Path.prototype.push = function (obj, path, val) { if (obj !== undefined && path !== undefined) { var pathParts, part; path = this.clean(path); pathParts = path.split('.'); part = pathParts.shift(); if (pathParts.length) { // Generate the path part in the object if it does not already exist obj[part] = obj[part] || {}; // Recurse this.set(obj[part], pathParts.join('.'), val); } else { // Set the value obj[part] = obj[part] || []; if (obj[part] instanceof Array) { obj[part].push(val); } else { throw('ForerunnerDB.Path: Cannot push to a path whose endpoint is not an array!'); } } } return obj; }; /** * Gets the value(s) that the object contains for the currently assigned path string * with their associated keys. * @param {Object} obj The object to evaluate the path against. * @param {String=} path A path to use instead of the existing one passed in path(). * @returns {Array} An array of values for the given path with the associated key. */ Path.prototype.keyValue = function (obj, path) { var pathParts, arr, arrCount, objPart, objPartParent, objPartHash, i; if (path !== undefined) { path = this.clean(path); pathParts = path.split('.'); } arr = pathParts || this._pathParts; arrCount = arr.length; objPart = obj; for (i = 0; i < arrCount; i++) { objPart = objPart[arr[i]]; if (!objPart || typeof(objPart) !== 'object') { objPartHash = arr[i] + ':' + objPart; break; } objPartParent = objPart; } return objPartHash; }; /** * Sets a value on an object for the specified path. * @param {Object} obj The object to update. * @param {String} path The path to update. * @param {*} val The value to set the object path to. * @returns {*} */ Path.prototype.set = function (obj, path, val) { if (obj !== undefined && path !== undefined) { var pathParts, part; path = this.clean(path); pathParts = path.split('.'); part = pathParts.shift(); if (pathParts.length) { // Generate the path part in the object if it does not already exist obj[part] = obj[part] || {}; // Recurse this.set(obj[part], pathParts.join('.'), val); } else { // Set the value obj[part] = val; } } return obj; }; /** * Removes leading period (.) from string and returns it. * @param {String} str The string to clean. * @returns {*} */ Path.prototype.clean = function (str) { if (str.substr(0, 1) === '.') { str = str.substr(1, str.length -1); } return str; }; Shared.finishModule('Path'); module.exports = Path;