/* * * Copryright 2018, Craig Condon * Licensed under MIT * * Filter JavaScript objects with mongodb queries */ /** */ function isFunction(value) { return typeof value === 'function'; } /** */ function isArray(value) { return Object.prototype.toString.call(value) === '[object Array]'; } /** */ function comparable(value) { if (value instanceof Date) { return value.getTime(); } else if (isArray(value)) { return value.map(comparable); } else if (value && typeof value.toJSON === 'function') { return value.toJSON(); } else { return value; } } function get(obj, key) { return isFunction(obj.get) ? obj.get(key) : obj[key]; } /** */ function or(validator) { return function(a, b) { if (!isArray(b) || !b.length) { return validator(a, b); } for (var i = 0, n = b.length; i < n; i++) { if (validator(a, get(b,i))) return true; } return false; } } /** */ function and(validator) { return function(a, b) { if (!isArray(b) || !b.length) { return validator(a, b); } for (var i = 0, n = b.length; i < n; i++) { if (!validator(a, get(b, i))) return false; } return true; }; } function validate(validator, b, k, o) { return validator.v(validator.a, b, k, o); } var OPERATORS = { /** */ $eq: or(function(a, b) { return a(b); }), /** */ $ne: and(function(a, b) { return !a(b); }), /** */ $gt: or(function(a, b) { return compare(comparable(b), a) > 0; }), /** */ $gte: or(function(a, b) { return compare(comparable(b), a) >= 0; }), /** */ $lt: or(function(a, b) { return compare(comparable(b), a) < 0; }), /** */ $lte: or(function(a, b) { return compare(comparable(b), a) <= 0; }), /** */ $mod: or(function(a, b) { return b % a[0] == a[1]; }), /** */ $in: function(a, b) { if (b instanceof Array) { for (var i = b.length; i--;) { if (~a.indexOf(comparable(get(b, i)))) { return true; } } } else { var comparableB = comparable(b); if (comparableB === b && typeof b === 'object') { for (var i = a.length; i--;) { if (String(a[i]) === String(b) && String(b) !== '[object Object]') { return true; } } } /* Handles documents that are undefined, whilst also having a 'null' element in the parameters to $in. */ if (typeof comparableB == 'undefined') { for (var i = a.length; i--;) { if (a[i] == null) { return true; } } } /* Handles the case of {'field': {$in: [/regexp1/, /regexp2/, ...]}} */ for (var i = a.length; i--;) { var validator = createRootValidator(get(a, i), undefined); var result = validate(validator, b, i, a); if ((result) && (String(result) !== '[object Object]') && (String(b) !== '[object Object]')) { return true; } } return !!~a.indexOf(comparableB); } return false; }, /** */ $nin: function(a, b, k, o) { return !OPERATORS.$in(a, b, k, o); }, /** */ $not: function(a, b, k, o) { return !validate(a, b, k, o); }, /** */ $type: function(a, b) { return b != void 0 ? b instanceof a || b.constructor == a : false; }, /** */ $all: function(a, b, k, o) { return OPERATORS.$and(a, b, k, o); }, /** */ $size: function(a, b) { return b ? a === b.length : false; }, /** */ $or: function(a, b, k, o) { for (var i = 0, n = a.length; i < n; i++) if (validate(get(a, i), b, k, o)) return true; return false; }, /** */ $nor: function(a, b, k, o) { return !OPERATORS.$or(a, b, k, o); }, /** */ $and: function(a, b, k, o) { for (var i = 0, n = a.length; i < n; i++) { if (!validate(get(a, i), b, k, o)) { return false; } } return true; }, /** */ $regex: or(function(a, b) { return typeof b === 'string' && a.test(b); }), /** */ $where: function(a, b, k, o) { return a.call(b, b, k, o); }, /** */ $elemMatch: function(a, b, k, o) { if (isArray(b)) { return !!~search(b, a); } return validate(a, b, k, o); }, /** */ $exists: function(a, b, k, o) { return o.hasOwnProperty(k) === a; } }; /** */ var prepare = { /** */ $eq: function(a) { if (a instanceof RegExp) { return function(b) { return typeof b === 'string' && a.test(b); }; } else if (a instanceof Function) { return a; } else if (isArray(a) && !a.length) { // Special case of a == [] return function(b) { return (isArray(b) && !b.length); }; } else if (a === null){ return function(b){ //will match both null and undefined return b == null; } } return function(b) { return compare(comparable(b), comparable(a)) === 0; }; }, /** */ $ne: function(a) { return prepare.$eq(a); }, /** */ $and: function(a) { return a.map(parse); }, /** */ $all: function(a) { return prepare.$and(a); }, /** */ $or: function(a) { return a.map(parse); }, /** */ $nor: function(a) { return a.map(parse); }, /** */ $not: function(a) { return parse(a); }, /** */ $regex: function(a, query) { return new RegExp(a, query.$options); }, /** */ $where: function(a) { return typeof a === 'string' ? new Function('obj', 'return ' + a) : a; }, /** */ $elemMatch: function(a) { return parse(a); }, /** */ $exists: function(a) { return !!a; } }; /** */ function search(array, validator) { for (var i = 0; i < array.length; i++) { var result = get(array, i); if (validate(validator, get(array, i))) { return i; } } return -1; } /** */ function createValidator(a, validate) { return { a: a, v: validate }; } /** */ function nestedValidator(a, b) { var values = []; findValues(b, a.k, 0, b, values); if (values.length === 1) { var first = values[0]; return validate(a.nv, first[0], first[1], first[2]); } // If the query contains $ne, need to test all elements ANDed together var inclusive = a && a.q && typeof a.q.$ne !== 'undefined'; var allValid = inclusive; for (var i = 0; i < values.length; i++) { var result = values[i]; var isValid = validate(a.nv, result[0], result[1], result[2]); if (inclusive) { allValid &= isValid; } else { allValid |= isValid; } } return allValid; } /** */ function findValues(current, keypath, index, object, values) { if (index === keypath.length || current == void 0) { values.push([current, keypath[index - 1], object]); return; } var k = get(keypath, index); // ensure that if current is an array, that the current key // is NOT an array index. This sort of thing needs to work: // sift({'foo.0':42}, [{foo: [42]}]); if (isArray(current) && isNaN(Number(k))) { for (var i = 0, n = current.length; i < n; i++) { findValues(get(current, i), keypath, index, current, values); } } else { findValues(get(current, k), keypath, index + 1, current, values); } } /** */ function createNestedValidator(keypath, a, q) { return { a: { k: keypath, nv: a, q: q }, v: nestedValidator }; } /** * flatten the query */ function isVanillaObject(value) { return value && value.constructor === Object; } function parse(query) { query = comparable(query); if (!query || !isVanillaObject(query)) { // cross browser support query = { $eq: query }; } var validators = []; for (var key in query) { var a = query[key]; if (key === '$options') { continue; } if (OPERATORS[key]) { if (prepare[key]) a = prepare[key](a, query); validators.push(createValidator(comparable(a), OPERATORS[key])); } else { if (key.charCodeAt(0) === 36) { throw new Error('Unknown operation ' + key); } validators.push(createNestedValidator(key.split('.'), parse(a), a)); } } return validators.length === 1 ? validators[0] : createValidator(validators, OPERATORS.$and); } /** */ function createRootValidator(query, getter) { var validator = parse(query); if (getter) { validator = { a: validator, v: function(a, b, k, o) { return validate(a, getter(b), k, o); } }; } return validator; } /** */ export default function sift(query, array, getter) { if (isFunction(array)) { getter = array; array = void 0; } var validator = createRootValidator(query, getter); function filter(b, k, o) { return validate(validator, b, k, o); } if (array) { return array.filter(filter); } return filter; } /** */ export function indexOf(query, array, getter) { return search(array, createRootValidator(query, getter)); }; /** */ export function compare(a, b) { if(a===b) return 0; if(typeof a === typeof b) { if (a > b) { return 1; } if (a < b) { return -1; } } };