/** * @name utils * @memberof module:js-data * @type {Object} * @property {Function} addHiddenPropsToTarget TODO * @property {Function} classCallCheck TODO * @property {Function} copy TODO * @property {Function} deepMixIn TODO * @property {Function} eventify TODO * @property {Function} extend TODO * @property {Function} fillIn TODO * @property {Function} fromJson TODO * @property {Function} get TODO * @property {Function} getSuper TODO * @property {Function} intersection TODO * @property {Function} isArray TODO * @property {Function} isBlacklisted TODO * @property {boolean} isBrowser TODO * @property {Function} isBoolean TODO * @property {Function} isFunction TODO * @property {Function} isInteger TODO * @property {Function} isNull TODO * @property {Function} isNumber TODO * @property {Function} isObject TODO * @property {Function} isRegExp TODO * @property {Function} isSorN TODO * @property {Function} isString TODO * @property {Function} isUndefined TODO * @property {Function} reject TODO * @property {Function} resolve TODO * @property {Function} set TODO * @property {Function} toJson TODO */ const DOMAIN = 'utils' const INFINITY = 1 / 0 const MAX_INTEGER = 1.7976931348623157e+308 const BOOL_TAG = '[object Boolean]' const DATE_TAG = '[object Date]' const FUNC_TAG = '[object Function]' const NUMBER_TAG = '[object Number]' const OBJECT_TAG = '[object Object]' const REGEXP_TAG = '[object RegExp]' const STRING_TAG = '[object String]' const objToString = Object.prototype.toString const PATH = /^(.+)\.(.+)$/ const ERRORS = { '400' () { return `expected: ${arguments[0]}, found: ${arguments[2] ? arguments[1] : typeof arguments[1]}` }, '404' () { return `${arguments[0]} not found` } } const toInteger = function (value) { if (!value) { return 0 } // Coerce to number value = +value if (value === INFINITY || value === -INFINITY) { const sign = (value < 0 ? -1 : 1) return sign * MAX_INTEGER } const remainder = value % 1 return value === value ? (remainder ? value - remainder : value) : 0 // eslint-disable-line } const toStr = function (value) { return objToString.call(value) } const isPlainObject = function (value) { return (!!value && typeof value === 'object' && value.constructor === Object) } const mkdirP = function (object, path) { if (!path) { return object } const parts = path.split('.') parts.forEach(function (key) { if (!object[key]) { object[key] = {} } object = object[key] }) return object } const utils = { /** * TODO * * @ignore */ Promise: Promise, /** * Shallow copy properties from src to dest that meet the following criteria: * - own enumerable * - not a function * - does not start with "_" * * @ignore * @param {Object} dest Destination object. * @param {Object} src Source object. */ _ (dest, src) { utils.forOwn(src, function (value, key) { if (key && utils.isUndefined(dest[key]) && !utils.isFunction(value) && key.indexOf('_') !== 0) { dest[key] = value } }) }, /** * TODO * * @ignore */ _forRelation (opts, def, fn, ctx) { const relationName = def.relation let containedName = null let index opts || (opts = {}) opts.with || (opts.with = []) if ((index = utils._getIndex(opts.with, relationName)) >= 0) { containedName = relationName } else if ((index = utils._getIndex(opts.with, def.localField)) >= 0) { containedName = def.localField } if (opts.withAll) { fn.call(ctx, def, {}) return } else if (!containedName) { return } let optsCopy = {} utils.fillIn(optsCopy, def.getRelation()) utils.fillIn(optsCopy, opts) optsCopy.with = opts.with.slice() optsCopy._activeWith = optsCopy.with.splice(index, 1)[0] optsCopy.with.forEach(function (relation, i) { if (relation && relation.indexOf(containedName) === 0 && relation.length >= containedName.length && relation[containedName.length] === '.') { optsCopy.with[i] = relation.substr(containedName.length + 1) } else { optsCopy.with[i] = '' } }) fn.call(ctx, def, optsCopy) }, /** * TODO * * @ignore */ _getIndex (list, relation) { let index = -1 list.forEach(function (_relation, i) { if (_relation === relation) { index = i return false } else if (utils.isObject(_relation)) { if (_relation.relation === relation) { index = i return false } } }) return index }, /** * TODO * * @ignore */ addHiddenPropsToTarget (target, props) { const map = {} utils.forOwn(props, function (value, key) { map[key] = { writable: true, value } }) Object.defineProperties(target, map) }, /** * TODO * * @ignore */ areDifferent (a, b, opts) { opts || (opts = {}) const diff = utils.diffObjects(a, b, opts) const diffCount = Object.keys(diff.added).length + Object.keys(diff.removed).length + Object.keys(diff.changed).length return diffCount > 0 }, /** * TODO * * @ignore */ classCallCheck (instance, ctor) { if (!(instance instanceof ctor)) { throw utils.err(`${ctor.name}`)(500, 'Cannot call a class as a function') } }, /** * Deep copy a value. * * @ignore * @param {*} from Value to deep copy. * @return {*} Deep copy of `from`. */ copy (from, to, stackFrom, stackTo, blacklist, plain) { if (!to) { to = from if (from) { if (utils.isArray(from)) { to = utils.copy(from, [], stackFrom, stackTo, blacklist, plain) } else if (utils.isDate(from)) { to = new Date(from.getTime()) } else if (utils.isRegExp(from)) { to = new RegExp(from.source, from.toString().match(/[^\/]*$/)[0]) to.lastIndex = from.lastIndex } else if (utils.isObject(from)) { if (plain) { to = utils.copy(from, {}, stackFrom, stackTo, blacklist, plain) } else { to = utils.copy(from, Object.create(Object.getPrototypeOf(from)), stackFrom, stackTo, blacklist, plain) } } } } else { if (from === to) { throw utils.err(`${DOMAIN}.copy`)(500, 'Cannot copy! Source and destination are identical.') } stackFrom = stackFrom || [] stackTo = stackTo || [] if (utils.isObject(from)) { let index = stackFrom.indexOf(from) if (index !== -1) { return stackTo[index] } stackFrom.push(from) stackTo.push(to) } let result if (utils.isArray(from)) { let i to.length = 0 for (i = 0; i < from.length; i++) { result = utils.copy(from[i], null, stackFrom, stackTo, blacklist, plain) if (utils.isObject(from[i])) { stackFrom.push(from[i]) stackTo.push(result) } to.push(result) } } else { if (utils.isArray(to)) { to.length = 0 } else { utils.forOwn(to, function (value, key) { delete to[key] }) } for (var key in from) { if (from.hasOwnProperty(key)) { if (utils.isBlacklisted(key, blacklist)) { continue } result = utils.copy(from[key], null, stackFrom, stackTo, blacklist, plain) if (utils.isObject(from[key])) { stackFrom.push(from[key]) stackTo.push(result) } to[key] = result } } } } return to }, /** * Recursively shallow fill in own enumberable properties from `source` to `dest`. * * @ignore * @param {Object} dest The destination object. * @param {Object} source The source object. */ deepFillIn (dest, source) { if (source) { utils.forOwn(source, function (value, key) { const existing = dest[key] if (isPlainObject(value) && isPlainObject(existing)) { utils.deepFillIn(existing, value) } else if (!dest.hasOwnProperty(key) || dest[key] === undefined) { dest[key] = value } }) } return dest }, /** * Recursively shallow copy own enumberable properties from `source` to `dest`. * * @ignore * @param {Object} dest The destination object. * @param {Object} source The source object. */ deepMixIn (dest, source) { if (source) { utils.forOwn(source, function (value, key) { const existing = dest[key] if (isPlainObject(value) && isPlainObject(existing)) { utils.deepMixIn(existing, value) } else { dest[key] = value } }) } return dest }, /** * @param {Object} a Base object. * @param {Object} b Comparison object. * @return {Object} Diff. */ diffObjects (a, b, opts) { opts || (opts = {}) let equalsFn = opts.equalsFn let bl = opts.ignore const diff = { added: {}, changed: {}, removed: {} } if (!utils.isFunction(equalsFn)) { equalsFn = utils.strictEqual } utils.forOwn(b, function (oldValue, key) { const newValue = a[key] if (utils.isBlacklisted(key, bl) || equalsFn(newValue, oldValue)) { return } if (utils.isUndefined(newValue)) { diff.removed[key] = undefined } else if (!equalsFn(newValue, oldValue)) { diff.changed[key] = newValue } }) utils.forOwn(a, function (newValue, key) { if (!utils.isUndefined(b[key]) || utils.isBlacklisted(key, bl)) { return } diff.added[key] = newValue }) return diff }, /** * TODO */ equal (a, b) { return a == b // eslint-disable-line }, /** * TODO */ err (domain, target) { return function (code) { const prefix = `[${domain}:${target}] ` let message = ERRORS[code].apply(null, Array.prototype.slice.call(arguments, 1)) message = `${prefix}${message}\nhttp://www.js-data.io/v3.0/docs/errors#${code}` return new Error(message) } }, /** * Add eventing capabilities into the target object. * * @ignore * @param {Object} target Target object. * @param {Function} [getter] Custom getter for retrieving the object's event * listeners. * @param {Function} [setter] Custom setter for setting the object's event * listeners. */ eventify (target, getter, setter) { target = target || this let _events = {} if (!getter && !setter) { getter = function () { return _events } setter = function (value) { _events = value } } Object.defineProperties(target, { emit: { value (...args) { const events = getter.call(this) || {} const type = args.shift() let listeners = events[type] || [] let i for (i = 0; i < listeners.length; i++) { listeners[i].f.apply(listeners[i].c, args) } listeners = events.all || [] args.unshift(type) for (i = 0; i < listeners.length; i++) { listeners[i].f.apply(listeners[i].c, args) } } }, off: { value (type, func) { const events = getter.call(this) const listeners = events[type] if (!listeners) { setter.call(this, {}) } else if (func) { for (let i = 0; i < listeners.length; i++) { if (listeners[i].f === func) { listeners.splice(i, 1) break } } } else { listeners.splice(0, listeners.length) } } }, on: { value (type, func, ctx) { if (!getter.call(this)) { setter.call(this, {}) } const events = getter.call(this) events[type] = events[type] || [] events[type].push({ c: ctx, f: func }) } } }) }, /** * TODO * * @ignore */ extend (props, classProps) { const superClass = this let subClass props || (props = {}) classProps || (classProps = {}) if (props.hasOwnProperty('constructor')) { subClass = props.constructor delete props.constructor } else { subClass = function (...args) { utils.classCallCheck(this, subClass) superClass.apply(this, args) } } // Setup inheritance of instance members subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { configurable: true, enumerable: false, value: subClass, writable: true } }) const obj = Object // Setup inheritance of static members if (obj.setPrototypeOf) { obj.setPrototypeOf(subClass, superClass) } else if (classProps.strictEs6Class) { subClass.__proto__ = superClass // eslint-disable-line } else { utils.forOwn(superClass, function (value, key) { subClass[key] = value }) } Object.defineProperty(subClass, '__super__', { configurable: true, value: superClass }) utils.addHiddenPropsToTarget(subClass.prototype, props) utils.fillIn(subClass, classProps) return subClass }, /** * Shallow copy own enumerable properties from `src` to `dest` that are on `src` * but are missing from `dest. * * @ignore * @param {Object} dest The destination object. * @param {Object} source The source object. */ fillIn (dest, src) { utils.forOwn(src, function (value, key) { if (!dest.hasOwnProperty(key) || dest[key] === undefined) { dest[key] = value } }) return dest }, /** * Find the index of something according to the given checker function. * * @ignore * @param {Array} array The array to search. * @param {Function} fn Checker function. * @param {number} Index if found or -1 if not found. */ findIndex (array, fn) { let index = -1 if (!array) { return index } array.forEach(function (record, i) { if (fn(record)) { index = i return false } }) return index }, /** * TODO * * @ignore */ forEachRelation (mapper, opts, fn, ctx) { const relationList = mapper.relationList || [] if (!relationList.length) { return } relationList.forEach(function (def) { utils._forRelation(opts, def, fn, ctx) }) }, /** * Iterate over an object's own enumerable properties. * * @ignore * @param {Object} object The object whose properties are to be enumerated. * @param {Function} fn Iteration function. * @param {Object} [thisArg] Content to which to bind `fn`. */ forOwn (obj, fn, thisArg) { const keys = Object.keys(obj) const len = keys.length let i for (i = 0; i < len; i++) { fn.call(thisArg, obj[keys[i]], keys[i], obj) } }, /** * Proxy for `JSON.parse`. * * @ignore * @param {string} json JSON to parse. * @return {Object} Parsed object. */ fromJson (json) { return utils.isString(json) ? JSON.parse(json) : json }, /** * TODO * * @ignore */ 'get': function (object, prop) { if (!prop) { return } const parts = prop.split('.') const last = parts.pop() while (prop = parts.shift()) { // eslint-disable-line object = object[prop] if (object == null) { // eslint-disable-line return } } return object[last] }, /** * TODO * * @ignore */ getSuper (instance, isCtor) { const ctor = isCtor ? instance : instance.constructor return (ctor.__super__ || Object.getPrototypeOf(ctor) || ctor.__proto__) // eslint-disable-line }, /** * Return the intersection of two arrays. * * @ignore * @param {Array} array1 First array. * @param {Array} array2 Second array. * @return {Array} Array of elements common to both arrays. */ intersection (array1, array2) { if (!array1 || !array2) { return [] } const result = [] let item let i const len = array1.length for (i = 0; i < len; i++) { item = array1[i] if (result.indexOf(item) !== -1) { continue } if (array2.indexOf(item) !== -1) { result.push(item) } } return result }, /** * TODO * * @ignore */ isArray: Array.isArray, /** * Return whether `prop` is matched by any string or regular expression in `bl`. * * @ignore * @param {string} prop The name of a property. * @param {Array} bl Array of strings and regular expressions. * @return {boolean} Whether `prop` was matched. */ isBlacklisted (prop, bl) { if (!bl || !bl.length) { return false } let matches for (var i = 0; i < bl.length; i++) { if ((toStr(bl[i]) === REGEXP_TAG && bl[i].test(prop)) || bl[i] === prop) { matches = prop return matches } } return !!matches }, /** * TODO * * @ignore */ isBoolean (value) { return toStr(value) === BOOL_TAG }, /** * TODO * * @ignore */ isBrowser: false, /** * TODO * * @ignore */ isDate (value) { return (value && typeof value === 'object' && toStr(value) === DATE_TAG) }, /** * TODO * * @ignore */ isFunction (value) { return typeof value === 'function' || (value && toStr(value) === FUNC_TAG) }, /** * TODO * * @ignore */ isInteger (value) { return toStr(value) === NUMBER_TAG && value == toInteger(value) // eslint-disable-line }, /** * TODO * * @ignore */ isNull (value) { return value === null }, /** * TODO * * @ignore */ isNumber (value) { const type = typeof value return type === 'number' || (value && type === 'object' && toStr(value) === NUMBER_TAG) }, /** * TODO * * @ignore */ isObject (value) { return toStr(value) === OBJECT_TAG }, /** * TODO * * @ignore */ isRegExp (value) { return toStr(value) === REGEXP_TAG }, /** * TODO * * @ignore */ isSorN (value) { return utils.isString(value) || utils.isNumber(value) }, /** * TODO * * @ignore */ isString (value) { return typeof value === 'string' || (value && typeof value === 'object' && toStr(value) === STRING_TAG) }, /** * TODO * * @ignore */ isUndefined (value) { return value === undefined }, /** * TODO * * @ignore */ logify (target) { utils.addHiddenPropsToTarget(target, { dbg (...args) { this.log('debug', ...args) }, log (level, ...args) { if (level && !args.length) { args.push(level) level = 'debug' } if (level === 'debug' && !this.debug) { return } const prefix = `${level.toUpperCase()}: (${this.name || this.constructor.name})` if (console[level]) { console[level](prefix, ...args) } else { console.log(prefix, ...args) } } }) }, /** * TODO * * @ignore */ noDupeAdd (array, record, fn) { if (!array) { return } const index = this.findIndex(array, fn) if (index < 0) { array.push(record) } }, /** * TODO * * @ignore */ omit (props, keys) { // Remove relations const _props = {} utils.forOwn(props, function (value, key) { if (keys.indexOf(key) === -1) { _props[key] = value } }) return _props }, /** * TODO * * @ignore */ plainCopy (from) { return utils.copy(from, undefined, undefined, undefined, undefined, true) }, /** * Proxy for `Promise.reject`. * * @ignore * @param {*} [value] Value with which to reject the Promise. * @return {Promise} Promise reject with `value`. */ reject (value) { return utils.Promise.reject(value) }, /** * TODO * * @ignore */ remove (array, fn) { if (!array || !array.length) { return } const index = this.findIndex(array, fn) if (index >= 0) { array.splice(index, 1) } }, /** * Proxy for `Promise.resolve`. * * @ignore * @param {*} [value] Value with which to resolve the Promise. * @return {Promise} Promise resolved with `value`. */ resolve (value) { return utils.Promise.resolve(value) }, /** * Set the value at the provided key or path. * * @ignore * @param {Object} object The object on which to set a property. * @param {(string|Object)} path The key or path to the property. Can also * pass in an object of path/value pairs, which will all be set on the target * object. * @param {*} [value] The value to set. */ set: function (object, path, value) { if (utils.isObject(path)) { utils.forOwn(path, function (value, _path) { utils.set(object, _path, value) }) } else { const parts = PATH.exec(path) if (parts) { mkdirP(object, parts[1])[parts[2]] = value } else { object[path] = value } } }, /** * TODO * * @ignore */ strictEqual (a, b) { let _equal = a === b if (!_equal) { if (utils.isObject(a) && utils.isObject(b)) { utils.forOwn(a, function (value, key) { _equal = _equal && utils.strictEqual(value, b[key]) }) utils.forOwn(b, function (value, key) { _equal = _equal && utils.strictEqual(value, a[key]) }) } else if (utils.isArray(a) && utils.isArray(b)) { a.forEach(function (value, i) { _equal = _equal && utils.strictEqual(value, b[i]) }) } } return _equal }, /** * Proxy for `JSON.stringify`. * * @ignore * @param {*} value Value to serialize to JSON. * @return {string} JSON string. */ toJson: JSON.stringify, /** * Unset the value at the provided key or path. * * @ignore * @param {Object} object The object from which to delete the property. * @param {string} path The key or path to the property. */ unset (object, path) { const parts = path.split('.') const last = parts.pop() while (path = parts.shift()) { // eslint-disable-line object = object[path] if (object == null) { // eslint-disable-line return } } object[last] = undefined } } // Attempt to detect whether we are in the browser. try { utils.isBrowser = !!window } catch (e) { utils.isBrowser = false } export default utils