UNPKG

fly-json-odm

Version:

An Object Document Mapper to handle JSON on the fly for NodeJS or Browser

546 lines (496 loc) 15.4 kB
// The reason we allow new function because for custom function at json transform feature. /* eslint no-new-func:0 */ 'use strict'; /** * Helper class */ class Helper { /** * Determine value is string * @param {*} value * @return {bool} */ isString (value) { return typeof value === 'string' || value instanceof String; } /** * Determine value is integer * @param {*} value * @return {bool} */ isInteger (value) { return Number.isInteger(value); } /** * Determine value is boolean * @param {*} value * @return {bool} */ isBoolean (value) { return typeof value === 'boolean' || (typeof value === 'object' && value !== null && typeof value.valueOf() === 'boolean'); } /** * Determine value is array * @param {*} value * @return {bool} */ isArray (value) { if (value === undefined || value === '') { return false; } return value && value !== '' && typeof value === 'object' && value.constructor === Array; } /** * Determine value is an array object based 5 part items. * We don't check all items to keep maintain performance. * @param {*} value * @returns {bool} */ fastCheckArrayObject (value) { if (!this.isArray(value)) return false; const count = value.length; if (count > 0) { const first = 0; const middle = Math.floor(count / 2); const last = (count - 1); const fquarter = Math.floor(middle / 2); const lquarter = Math.floor((middle + last) / 2); if (typeof value[first] !== 'object') return false; if ((fquarter > first) && typeof value[fquarter] !== 'object') return false; if ((middle > fquarter) && typeof value[middle] !== 'object') return false; if ((lquarter > middle) && typeof value[lquarter] !== 'object') return false; if (typeof value[last] !== 'object') return false; } return true; } /** * Determine value is object * @param {*} value * @return {bool} */ isObject (value) { if (value === undefined || value === '') { return false; } return value && typeof value === 'object' && value.constructor === Object; } /** * Determine value is empty * @param {*} value * @return {bool} */ isEmpty (value) { return (value === undefined || value === null || value === ''); } /** * Determine value is empty and array * @param {*} value * @return {bool} */ isEmptyArray (value) { return (value === undefined || value === null || value.length === 0); } /** * Determine object value is empty * @param {*} value * @return {bool} */ isEmptyObject (value) { return (value === undefined || value === null || (Object.keys(value).length === 0 && value.constructor === Object)); } /** * Foreach for an array or object * @param {array|object} data * @param {callback} callback */ foreach (data, callback) { if (this.isObject(data)) { const keys = Object.keys(data); const values = Object.keys(data).map(function (e) { return data[e]; }); let i = 0; const l = keys.length; for (i; i < l; i++) { callback(values[i], keys[i]); } } else { if (Array.isArray(data)) { let i = 0; const l = data.length; for (i; i < l; i++) { callback(data[i], i); } } else { throw new Error('Failed to iteration. Data is not an array or object.'); } } } /** * Blocking test for asynchronous * @param {integer} ms [Optional] this is miliseconds value for event block. Default value is 1000 ms. * @return {int} */ blockingTest (ms) { ms = (ms === undefined ? 1000 : ms); const start = Date.now(); const time = start + ms; while (Date.now() < time) { // empty progress } return start; } /** * Safe JSON.stringify to avoid type error converting circular structure to json * @param {object} value this is the json object * @param {*} space * @return {string} */ safeStringify (value, space) { let cache = []; const output = JSON.stringify(value, function (key, value) { // filters vue.js internal properties if (key && key.length > 0 && (key.charAt(0) === '$' || key.charAt(0) === '_')) { return; } if (typeof value === 'object' && value !== null) { if (cache.indexOf(value) !== -1) { // Circular reference found, discard key return; } // Store value in our collection cache.push(value); } return value; }, space); cache = null; // Enable garbage collection return output; } /** * Shallow clone an array * @param {array} array * @return {array} */ shallowClone (array) { return [...array]; } /** * Very safe deep clone an array * @param {array} array * @return {array} */ deepClone (array) { let clone, i; if (typeof array !== 'object' || !array) return array; if (Object.prototype.toString.apply(array) === '[object Array]') { clone = []; const len = array.length; for (i = 0; i < len; i++) clone[i] = this.deepClone(array[i]); return clone; } clone = {}; for (i in array) if (Object.prototype.hasOwnProperty.call(array, i)) clone[i] = this.deepClone(array[i]); return clone; } /** * jsonTransform for restructuring and performing operations on JSON * @param {object} data * @param {object} map * @return {array} */ jsonTransform (data, map) { const helper = new Helper(); return { defaultOrNull: function (key) { return key && map.defaults ? map.defaults[key] : null; }, getValue: function (obj, key, newKey) { if (typeof obj === 'undefined') { return; } if (key === undefined || key === null || key === '') { return obj; } let value = obj; let keys = null; keys = key.split('.'); let i = 0; const l = keys.length; for (i; i < l; i++) { if (typeof (value) !== 'undefined' && keys[i] in value) { value = value[keys[i]]; } else { return this.defaultOrNull(newKey); } } return value; }, setValue: function (obj, key, newValue) { if (typeof obj === 'undefined' || key === '' || key === undefined || key == null) { return; } const keys = key.split('.'); let target = obj; let i = 0; const l = keys.length; for (i; i < l; i++) { if (i === keys.length - 1) { target[keys[i]] = newValue; return; } if (keys[i] in target) { target = target[keys[i]]; } else { return; } } }, getList: function () { return this.getValue(data, map.list); }, make: function (context) { const value = this.getValue(data, map.list); let normalized = []; if (!helper.isEmptyObject(value)) { const list = this.getList(); normalized = map.item ? list.map(this.iterator.bind(this, map.item)) : list; normalized = this.operate.bind(this, normalized)(context); normalized = this.each(normalized, context); normalized = this.removeAll(normalized); } return normalized; }, removeAll: function (data) { if (Array.isArray(map.remove)) { helper.foreach(data, this.remove); } return data; }, remove: function (item) { let i = 0; const l = map.remove.length; for (i; i < l; i++) { delete item[map.remove[i]]; } return item; }, operate: function (data, context) { if (map.operate) { helper.foreach(map.operate, function (method) { data = data.map(function (item) { let fn; if (typeof method.run === 'string') { fn = new Function('return ' + method.run)(); } else { fn = method.run; } this.setValue(item, method.on, fn(this.getValue(item, method.on), context)); return item; }.bind(this)); }.bind(this)); } return data; }, each: function (data, context) { if (map.each) { data.forEach(function (value, index, collection) { return map.each(value, index, collection, context); }); } return data; }, iterator: function (map, item) { const obj = {}; // to support simple arrays with recursion if (typeof map === 'string') { return this.getValue(item, map); } helper.foreach(map, function (oldkey, newkey) { if (typeof oldkey === 'string' && oldkey.length > 0) { obj[newkey] = this.getValue(item, oldkey, newkey); } else if (Array.isArray(oldkey)) { const array = oldkey.map(function (item, map) { return this.iterator(map, item); }.bind(this, item));// need to swap arguments for bind obj[newkey] = array; } else if (typeof oldkey === 'object') { const bound = this.iterator.bind(this, oldkey, item); obj[newkey] = bound(); } else { obj[newkey] = ''; } }.bind(this)); return obj; } }; } /** * Get Descendant Property in json object * @param {array|object} object * @param {string} path * @param {array} list * @returns {array} */ getDescendantProperty (object, path, list = []) { let firstSegment; let remaining; let dotIndex; let value; let index; let length; if (path) { dotIndex = path.indexOf('.'); if (dotIndex === -1) { firstSegment = path; } else { firstSegment = path.slice(0, dotIndex); remaining = path.slice(dotIndex + 1); } value = object[firstSegment]; if (value !== null && typeof value !== 'undefined') { if (!remaining && (typeof value === 'string' || typeof value === 'number')) { list.push(value); } else if (Object.prototype.toString.call(value) === '[object Array]') { for (index = 0, length = value.length; index < length; index++) { this.getDescendantProperty(value[index], remaining, list); } } else if (remaining) { this.getDescendantProperty(value, remaining, list); } } } else { list.push(object); } return list; } /** * Fuzzy * @param {array} haystack List data array or object. * @param {string|number} query [Optional] Text or number to search. * @param {array} keys [Optional] Keys is required if the list is array object. Default is empty array. * @param {boolean} caseSensitive [Optional] Search with match case sensitive. Default is false. * @param {boolean} sort [Optional] When true it will sort the results by best match. Default is false. * @returns {array} */ fuzzy (haystack, query = '', keys = [], caseSensitive = false, sort = false) { if (query === '') return haystack; const results = []; for (let i = 0; i < haystack.length; i++) { const item = haystack[i]; if (keys.length === 0) { const score = this._fuzzyIsMatch(item, query, caseSensitive); if (score) { results.push({ item, score }); } } else { for (let y = 0; y < keys.length; y++) { const propertyValues = this.getDescendantProperty(item, keys[y]); let found = false; for (let z = 0; z < propertyValues.length; z++) { const score = this._fuzzyIsMatch(propertyValues[z], query, caseSensitive); if (score) { found = true; results.push({ item, score }); break; } } if (found) { break; } } } } if (sort) { results.sort((a, b) => a.score - b.score); } return results.map(result => result.item); } /** * Is Match: Giving score depend on best matches. * @param {string|number} item Value from data list * @param {string|number} query Value from search * @param {boolean} caseSensitive Search with match case sensitive. * @returns {number} */ _fuzzyIsMatch (item, query, caseSensitive) { item = String(item); query = String(query); if (!caseSensitive) { item = item.toLocaleLowerCase(); query = query.toLocaleLowerCase(); } const indexes = this._fuzzyNearestIndexesFor(item, query); if (!indexes) { return false; } // Exact matches should be first. if (item === query) { return 1; } // If we hit abbreviation it should go before others (except exact match). const abbreviationIndicies = [0]; for (let i = 0; i < item.length; i++) { if (item[i] === ' ') abbreviationIndicies.push(i + 1); } if (indexes.reduce((accumulator, currentValue) => abbreviationIndicies.includes(currentValue) && accumulator, true)) { return 2 + indexes.reduce((accumulator, currentValue, i) => { return accumulator + abbreviationIndicies.indexOf(currentValue); }, 0); } // If we have more than 2 letters, matches close to each other should be first. if (indexes.length > 1) { return 3 + (indexes[indexes.length - 1] - indexes[0]); } // Matches closest to the start of the string should be first. return 3 + indexes[0]; } /** * Nearest Indexed For Value and Search * @param {string} item Value from data list * @param {string} query Value from search * @returns {array} number */ _fuzzyNearestIndexesFor (item, query) { const letters = query.split(''); let indexes = []; const indexesOfFirstLetter = this._fuzzyIndexesOfFirstLetter(item, query); indexesOfFirstLetter.forEach((startingIndex, loopingIndex) => { let index = startingIndex + 1; indexes[loopingIndex] = [startingIndex]; for (let i = 1; i < letters.length; i++) { const letter = letters[i]; index = item.indexOf(letter, index); if (index === -1) { indexes[loopingIndex] = false; break; } indexes[loopingIndex].push(index); index++; } }); indexes = indexes.filter(letterIndexes => letterIndexes !== false); if (!indexes.length) { return false; } return indexes.sort((a, b) => { if (a.length === 1) { return a[0] - b[0]; } a = a[a.length - 1] - a[0]; b = b[b.length - 1] - b[0]; return a - b; })[0]; } /** * Indexes Of First Letter * @param {string} item Value from data list * @param {string} query Value from search * @returns {array} number */ _fuzzyIndexesOfFirstLetter (item, query) { const match = query[0]; return item.split('').map((letter, index) => { if (letter !== match) { return false; } return index; }).filter(index => index !== false); } } module.exports = Helper;