rittenhop-dev/versions/5.94.2/node_modules/mingo/dist/mingo.es6.js
2024-09-23 19:40:12 -04:00

4536 lines
113 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! mingo.js 2.5.3
//! Copyright (c) 2020 Francis Asante
//! MIT
// Javascript native types
const T_NULL = 'null';
const T_UNDEFINED = 'undefined';
const T_BOOL = 'bool';
const T_BOOLEAN = 'boolean';
const T_NUMBER = 'number';
const T_STRING = 'string';
const T_DATE = 'date';
const T_REGEX = 'regex';
const T_REGEXP = 'regexp';
const T_ARRAY = 'array';
const T_OBJECT = 'object';
const T_FUNCTION = 'function';
// no array, object, or function types
const JS_SIMPLE_TYPES = [T_NULL, T_UNDEFINED, T_BOOLEAN, T_NUMBER, T_STRING, T_DATE, T_REGEXP];
// operator classes
const OP_EXPRESSION = 'expression';
const OP_GROUP = 'group';
const OP_PIPELINE = 'pipeline';
const OP_PROJECTION = 'projection';
const OP_QUERY = 'query';
const MISSING = () => {};
/**
* Utility functions
*/
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function(valueToFind, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(valueToFind, elementK) is true, return true.
if (sameValueZero(o[k], valueToFind)) {
return true;
}
// c. Increase k by 1.
k++;
}
// 8. Return false
return false;
}
});
}
const arrayPush = Array.prototype.push;
function assert (condition, message) {
if (!condition) err(message);
}
/**
* Deep clone an object
*/
function cloneDeep (obj) {
switch (jsType(obj)) {
case T_ARRAY: return obj.map(cloneDeep)
case T_OBJECT: return objectMap(obj, cloneDeep)
default: return obj
}
}
/**
* Shallow clone an object
*/
function clone (obj) {
switch (jsType(obj)) {
case T_ARRAY:
return into([], obj)
case T_OBJECT:
return Object.assign({}, obj)
default:
return obj
}
}
function getType (v) {
if (v === null) return 'Null'
if (v === undefined) return 'Undefined'
return v.constructor.name
}
function jsType (v) { return getType(v).toLowerCase() }
function isBoolean (v) { return jsType(v) === T_BOOLEAN }
function isString (v) { return jsType(v) === T_STRING }
function isNumber (v) { return jsType(v) === T_NUMBER }
const isArray = Array.isArray || (v => !!v && v.constructor === Array);
function isObject(v) { return !!v && v.constructor === Object }
function isObjectLike (v) { return v === Object(v) } // objects, arrays, functions, date, custom object
function isDate (v) { return jsType(v) === T_DATE }
function isRegExp (v) { return jsType(v) === T_REGEXP }
function isFunction (v) { return jsType(v) === T_FUNCTION }
function isNil (v) { return v === null || v === undefined }
function isNull (v) { return v === null }
function isUndefined (v) { return v === undefined }
function inArray (arr, item) { return arr.includes(item) }
function notInArray (arr, item) { return !inArray(arr, item) }
function truthy (arg) { return !!arg }
function isEmpty (x) {
return isNil(x) ||
isArray(x) && x.length === 0 ||
isObject(x) && keys(x).length === 0 || !x
}
// ensure a value is an array
function ensureArray (x) { return isArray(x) ? x : [x] }
function has (obj, prop) { return obj.hasOwnProperty(prop) }
function err (s) { throw new Error(s) }
const keys = Object.keys;
// ////////////////// UTILS ////////////////////
/**
* Iterate over an array or object
* @param {Array|Object} obj An object-like value
* @param {Function} fn The callback to run per item
* @param {*} ctx The object to use a context
* @return {void}
*/
function each (obj, fn, ctx) {
fn = fn.bind(ctx);
if (isArray(obj)) {
for (let i = 0, len = obj.length; i < len; i++) {
if (fn(obj[i], i, obj) === false) break
}
} else {
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
if (fn(obj[k], k, obj) === false) break
}
}
}
}
/**
* Transform values in an object
*
* @param {Object} obj An object whose values to transform
* @param {Function} fn The transform function
* @param {*} ctx The value to use as the "this" context for the transform
* @return {Array|Object} Result object after applying the transform
*/
function objectMap (obj, fn, ctx) {
fn = fn.bind(ctx);
let o = {};
let objKeys = keys(obj);
for (let i = 0; i < objKeys.length; i++) {
let k = objKeys[i];
o[k] = fn(obj[k], k);
}
return o
}
/**
* Deep merge objects or arrays.
* When the inputs have unmergeable types, the source value (right hand side) is returned.
* If inputs are arrays of same length and all elements are mergable, elements in the same position are merged together.
* If any of the elements are unmergeable, elements in the source are appended to the target.
* @param target {Object|Array} the target to merge into
* @param obj {Object|Array} the source object
*/
function merge(target, obj, opt = {}) {
// take care of missing inputs
if (target === MISSING) return obj
if (obj === MISSING) return target
const inputs = [target, obj];
if (!(inputs.every(isObject) || inputs.every(isArray))) {
throw Error('mismatched types. must both be array or object')
}
// default options
opt.flatten = opt.flatten || false;
if (isArray(target)) {
if (opt.flatten) {
let i = 0;
let j = 0;
while (i < target.length && j < obj.length) {
target[i] = merge(target[i++], obj[j++], opt);
}
while (j < obj.length) {
target.push(obj[j++]);
}
} else {
arrayPush.apply(target, obj);
}
} else {
Object.keys(obj).forEach((k) => {
if (target.hasOwnProperty(k)) {
target[k] = merge(target[k], obj[k], opt);
} else {
target[k] = obj[k];
}
});
}
return target
}
/**
* Reduce any array-like object
* @param collection
* @param fn
* @param accumulator
* @returns {*}
*/
function reduce (collection, fn, accumulator) {
if (isArray(collection)) return collection.reduce(fn, accumulator)
// array-like objects
each(collection, (v, k) => accumulator = fn(accumulator, v, k, collection));
return accumulator
}
/**
* Returns the intersection between two arrays
*
* @param {Array} xs The first array
* @param {Array} ys The second array
* @return {Array} Result array
*/
function intersection (xs, ys) {
let hashes = ys.map(hashCode);
return xs.filter(v => inArray(hashes, hashCode(v)))
}
/**
* Returns the union of two arrays
*
* @param {Array} xs The first array
* @param {Array} ys The second array
* @return {Array} The result array
*/
function union (xs, ys) {
return into(into([], xs), ys.filter(notInArray.bind(null, xs)))
}
/**
* Flatten the array
*
* @param {Array} xs The array to flatten
* @param {Number} depth The number of nested lists to iterate
*/
function flatten (xs, depth = -1) {
let arr = [];
function flatten2(ys, iter) {
for (let i = 0, len = ys.length; i < len; i++) {
if (isArray(ys[i]) && (iter > 0 || iter < 0)) {
flatten2(ys[i], Math.max(-1, iter - 1));
} else {
arr.push(ys[i]);
}
}
}
flatten2(xs, depth);
return arr
}
/**
* Unwrap a single element array to specified depth
* @param {Array} arr
* @param {Number} depth
*/
function unwrap(arr, depth) {
if (depth < 1) return arr
while (depth-- && isArray(arr) && arr.length === 1) arr = arr[0];
return arr
}
/**
* Determine whether two values are the same or strictly equivalent
*
* @param {*} a The first value
* @param {*} b The second value
* @return {Boolean} Result of comparison
*/
function isEqual (a, b) {
let lhs = [a];
let rhs = [b];
while (lhs.length > 0) {
a = lhs.pop();
b = rhs.pop();
// strictly equal must be equal.
if (a === b) continue
// unequal types and functions cannot be equal.
let type = jsType(a);
if (type !== jsType(b) || type === T_FUNCTION) return false
// leverage toString for Date and RegExp types
switch (type) {
case T_ARRAY:
if (a.length !== b.length) return false
//if (a.length === b.length && a.length === 0) continue
into(lhs, a);
into(rhs, b);
break
case T_OBJECT:
// deep compare objects
let ka = keys(a);
let kb = keys(b);
// check length of keys early
if (ka.length !== kb.length) return false
// we know keys are strings so we sort before comparing
ka.sort();
kb.sort();
// compare keys
for (let i = 0, len = ka.length; i < len; i++) {
let temp = ka[i];
if (temp !== kb[i]) {
return false
} else {
// save later work
lhs.push(a[temp]);
rhs.push(b[temp]);
}
}
break
default:
// compare encoded values
if (encode(a) !== encode(b)) return false
}
}
return lhs.length === 0
}
/**
* Return a new unique version of the collection
* @param {Array} xs The input collection
* @return {Array} A new collection with unique values
*/
function unique (xs) {
let h = {};
let arr = [];
each(xs, (item) => {
let k = hashCode(item);
if (!has(h, k)) {
arr.push(item);
h[k] = 0;
}
});
return arr
}
/**
* Encode value to string using a simple non-colliding stable scheme.
*
* @param value
* @returns {*}
*/
function encode (value) {
let type = jsType(value);
switch (type) {
case T_BOOLEAN:
case T_NUMBER:
case T_REGEXP:
return value.toString()
case T_STRING:
return JSON.stringify(value)
case T_DATE:
return value.toISOString()
case T_NULL:
case T_UNDEFINED:
return type
case T_ARRAY:
return '[' + value.map(encode) + ']'
default:
let prefix = (type === T_OBJECT)? '' : `${getType(value)}`;
let objKeys = keys(value);
objKeys.sort();
return `${prefix}{` + objKeys.map(k => `${encode(k)}:${encode(value[k])}`) + '}'
}
}
/**
* Generate hash code
* This selected function is the result of benchmarking various hash functions.
* This version performs well and can hash 10^6 documents in ~3s with on average 100 collisions.
*
* @param value
* @returns {*}
*/
function hashCode (value) {
if (isNil(value)) return null
let hash = 0;
let s = encode(value);
let i = s.length;
while (i) hash = ((hash << 5) - hash) ^ s.charCodeAt(--i);
return hash >>> 0
}
/**
* Default compare function
* @param {*} a
* @param {*} b
*/
function compare (a, b) {
if (a < b) return -1
if (a > b) return 1
return 0
}
/**
* Returns a (stably) sorted copy of list, ranked in ascending order by the results of running each value through iteratee
*
* This implementation treats null/undefined sort keys as less than every other type
*
* @param {Array} collection
* @param {Function} fn The function used to resolve sort keys
* @param {Function} cmp The comparator function to use for comparing values
* @return {Array} Returns a new sorted array by the given iteratee
*/
function sortBy (collection, fn, cmp) {
let sorted = [];
let result = [];
let hash = {};
cmp = cmp || compare;
if (isEmpty(collection)) return collection
for (let i = 0; i < collection.length; i++) {
let obj = collection[i];
let key = fn(obj, i);
// objects with nil keys will go in first
if (isNil(key)) {
result.push(obj);
} else {
if (hash[key]) {
hash[key].push(obj);
} else {
hash[key] = [obj];
}
sorted.push(key);
}
}
// use native array sorting but enforce stableness
sorted.sort(cmp);
for (let i = 0; i < sorted.length; i++) {
into(result, hash[sorted[i]]);
}
return result
}
/**
* Groups the collection into sets by the returned key
*
* @param collection
* @param fn {Function} to compute the group key of an item in the collection
* @returns {{keys: Array, groups: Array}}
*/
function groupBy (collection, fn) {
let result = {
'keys': [],
'groups': []
};
let lookup = {};
each(collection, obj => {
let key = fn(obj);
let hash = hashCode(key);
let index = -1;
if (lookup[hash] === undefined) {
index = result.keys.length;
lookup[hash] = index;
result.keys.push(key);
result.groups.push([]);
}
index = lookup[hash];
result.groups[index].push(obj);
});
return result
}
/**
* Push elements in given array into target array
*
* @param {*} target The array to push into
* @param {*} xs The array of elements to push
*/
function into (target, xs) {
arrayPush.apply(target, xs);
return target
}
/**
* Find the insert index for the given key in a sorted array.
*
* @param {*} array The sorted array to search
* @param {*} item The search key
*/
function findInsertIndex (array, item) {
// uses binary search
let lo = 0;
let hi = array.length - 1;
while (lo <= hi) {
let mid = Math.round(lo + (hi - lo) / 2);
if (item < array[mid]) {
hi = mid - 1;
} else if (item > array[mid]) {
lo = mid + 1;
} else {
return mid
}
}
return lo
}
/**
* This is a generic memoization function
*
* This implementation uses a cache independent of the function being memoized
* to allow old values to be garbage collected when the memoized function goes out of scope.
*
* @param {*} fn The function object to memoize
*/
function memoize (fn) {
return ((memo) => {
return (...args) => {
let key = hashCode(args);
if (!has(memo, key)) {
memo[key] = fn.apply(this, args);
}
return memo[key]
}
})({/* storage */})
}
// mingo internal
/**
* Retrieve the value of a given key on an object
* @param obj
* @param field
* @returns {*}
* @private
*/
function getValue (obj, field) {
return isObjectLike(obj) ? obj[field] : undefined
}
/**
* Resolve the value of the field (dot separated) on the given object
* @param obj {Object} the object context
* @param selector {String} dot separated path to field
* @returns {*}
*/
function resolve (obj, selector, options = {}) {
let depth = 0;
// options
options.meta = options.meta || false;
function resolve2(o, path) {
let value = o;
for (let i = 0; i < path.length; i++) {
let field = path[i];
let isText = field.match(/^\d+$/) === null;
if (isText && isArray(value)) {
// On the first iteration, we check if we received a stop flag.
// If so, we stop to prevent iterating over a nested array value
// on consecutive object keys in the selector.
if (i === 0 && depth > 0) break
depth += 1;
path = path.slice(i);
value = reduce(value, (acc, item) => {
let v = resolve2(item, path);
if (v !== undefined) acc.push(v);
return acc
}, []);
break
} else {
value = getValue(value, field);
}
if (value === undefined) break
}
return value
}
obj = inArray(JS_SIMPLE_TYPES, jsType(obj)) ? obj : resolve2(obj, selector.split('.'));
return options.meta
? { result: obj, depth: depth }
: obj
}
/**
* Returns the full object to the resolved value given by the selector.
* This function excludes empty values as they aren't practically useful.
*
* @param obj {Object} the object context
* @param selector {String} dot separated path to field
*/
function resolveObj (obj, selector, options = {}) {
// options
options.preserveMissingValues = options.preserveMissingValues || false;
let names = selector.split('.');
let key = names[0];
// get the next part of the selector
let next = names.length === 1 || names.slice(1).join('.');
let isIndex = key.match(/^\d+$/) !== null;
let hasNext = names.length > 1;
let result;
let value;
try {
if (isArray(obj)) {
if (isIndex) {
result = getValue(obj, Number(key));
if (hasNext) {
result = resolveObj(result, next, options);
}
result = [result];
} else {
result = [];
each(obj, item => {
value = resolveObj(item, selector, options);
if (options.preserveMissingValues) {
if (value === undefined) {
value = MISSING;
}
result.push(value);
} else if (value !== undefined) {
result.push(value);
}
});
}
} else {
value = getValue(obj, key);
if (hasNext) {
value = resolveObj(value, next, options);
}
assert(value !== undefined);
result = {};
result[key] = value;
}
} catch (e) {
result = undefined;
}
return result
}
/**
* Filter out all MISSING values from the object in-place
* @param {*} obj The object the filter
*/
function filterMissing(obj) {
if (isArray(obj)) {
for (let i = obj.length - 1; i >= 0; i--) {
if (obj[i] === MISSING) {
obj.splice(i,1);
} else {
filterMissing(obj[i]);
}
}
} else if (isObject(obj)) {
for (let k in obj) {
if (obj.hasOwnProperty(k)) {
filterMissing(obj[k]);
}
}
}
return obj
}
/**
* Walk the object graph and execute the given transform function
* @param {Object|Array} obj The object to traverse
* @param {String} selector The selector
* @param {Function} fn Function to execute for value at the end the traversal
* @param {Boolean} force Force generating missing parts of object graph
* @return {*}
*/
function traverse (obj, selector, fn, force = false) {
let names = selector.split('.');
let key = names[0];
let next = names.length === 1 || names.slice(1).join('.');
if (names.length === 1) {
fn(obj, key);
} else {
// force the rest of the graph while traversing
if (force === true && isNil(obj[key])) {
obj[key] = {};
}
traverse(obj[key], next, fn, force);
}
}
/**
* Set the value of the given object field
*
* @param obj {Object|Array} the object context
* @param selector {String} path to field
* @param value {*} the value to set
*/
function setValue (obj, selector, value) {
traverse(obj, selector, (item, key) => {
item[key] = value;
}, true);
}
function removeValue (obj, selector) {
traverse(obj, selector, (item, key) => {
if (isArray(item) && /^\d+$/.test(key)) {
item.splice(parseInt(key), 1);
} else if (isObject(item)) {
delete item[key];
}
});
}
/**
* Check whether the given name is an operator. We assume any field name starting with '$' is an operator.
* This is cheap and safe to do since keys beginning with '$' should be reserved for internal use.
* @param {String} name
*/
function isOperator(name) {
return !!name && name[0] === '$'
}
/**
* Simplify expression for easy evaluation with query operators map
* @param expr
* @returns {*}
*/
function normalize (expr) {
// normalized primitives
if (inArray(JS_SIMPLE_TYPES, jsType(expr))) {
return isRegExp(expr) ? { '$regex': expr } : { '$eq': expr }
}
// normalize object expression
if (isObjectLike(expr)) {
let exprKeys = keys(expr);
// no valid query operator found, so we do simple comparison
if (!exprKeys.some(isOperator)) {
return { '$eq': expr }
}
// ensure valid regex
if (inArray(exprKeys, '$regex')) {
let regex = expr['$regex'];
let options = expr['$options'] || '';
let modifiers = '';
if (isString(regex)) {
modifiers += (regex.ignoreCase || options.indexOf('i') >= 0) ? 'i' : '';
modifiers += (regex.multiline || options.indexOf('m') >= 0) ? 'm' : '';
modifiers += (regex.global || options.indexOf('g') >= 0) ? 'g' : '';
regex = new RegExp(regex, modifiers);
}
expr['$regex'] = regex;
delete expr['$options'];
}
}
return expr
}
/**
* Returns a slice of the array
*
* @param {Array} xs
* @param {Number} skip
* @param {Number} limit
* @return {Array}
*/
function slice (xs, skip, limit = null) {
// MongoDB $slice works a bit differently from Array.slice
// Uses single argument for 'limit' and array argument [skip, limit]
if (isNil(limit)) {
if (skip < 0) {
skip = Math.max(0, xs.length + skip);
limit = xs.length - skip + 1;
} else {
limit = skip;
skip = 0;
}
} else {
if (skip < 0) {
skip = Math.max(0, xs.length + skip);
}
assert(limit > 0, 'Invalid argument value for $slice operator. Limit must be a positive number');
limit += skip;
}
return xs.slice(skip, limit)
}
/**
* Compute the standard deviation of the data set
* @param {Array} array of numbers
* @param {Boolean} if true calculates a sample standard deviation, otherwise calculates a population stddev
* @return {Number}
*/
function stddev (data, sampled) {
let sum = reduce(data, (acc, n) => acc + n, 0);
let N = data.length || 1;
let correction = (sampled && 1) || 0;
let avg = sum / N;
return Math.sqrt(reduce(data, (acc, n) => acc + Math.pow(n - avg, 2), 0) / (N - correction))
}
/**
* Exported to the users to allow writing custom operators
*/
function moduleApi () {
return {
assert,
clone,
cloneDeep,
each,
err,
hashCode,
getType,
has,
includes: inArray.bind(null),
isArray,
isBoolean,
isDate,
isEmpty,
isEqual,
isFunction,
isNil,
isNull,
isNumber,
isObject,
isRegExp,
isString,
isUndefined,
keys,
reduce,
resolve,
resolveObj
}
}
// internal functions available to external operators
const _internal = () => Object.assign({ computeValue, ops }, moduleApi());
// Settings used by Mingo internally
const settings = {
key: '_id'
};
/**
* Setup default settings for Mingo
* @param options
*/
function setup (options) {
Object.assign(settings, options || {});
}
/**
* Implementation of system variables
* @type {Object}
*/
const systemVariables = {
'$$ROOT' (obj, expr, opt) {
return opt.root
},
'$$CURRENT' (obj, expr, opt) {
return obj
},
'$$REMOVE' (obj, expr, opt) {
return undefined
}
};
/**
* Implementation of $redact variables
*
* Each function accepts 3 arguments (obj, expr, opt)
*
* @type {Object}
*/
const redactVariables = {
'$$KEEP' (obj) {
return obj
},
'$$PRUNE' () {
return undefined
},
'$$DESCEND' (obj, expr, opt) {
// traverse nested documents iff there is a $cond
if (!has(expr, '$cond')) return obj
let result;
each(obj, (current, key) => {
if (isObjectLike(current)) {
if (isArray(current)) {
result = [];
each(current, (elem) => {
if (isObject(elem)) {
elem = redactObj(elem, expr, opt);
}
if (!isNil(elem)) result.push(elem);
});
} else {
result = redactObj(current, expr, opt);
}
if (isNil(result)) {
delete obj[key]; // pruned result
} else {
obj[key] = result;
}
}
});
return obj
}
};
// system variables
const SYS_VARS = keys(systemVariables);
const REDACT_VARS = keys(redactVariables);
/**
* Returns the key used as the collection's objects ids
*/
function idKey () {
return settings.key
}
/**
* Returns the operators defined for the given operator classes
*/
function ops () {
// Workaround for browser-compatibility bug: on iPhone 6S Safari (and
// probably some other platforms), `arguments` isn't detected as an array,
// but has a length field, so functions like `reduce` end up including the
// length field in their iteration. Copy to a real array.
let args = Array.prototype.slice.call(arguments);
return reduce(args, (acc, cls) => into(acc, keys(OPERATORS[cls])), [])
}
/**
* Returns the result of evaluating a $group operation over a collection
*
* @param collection
* @param field the name of the aggregate operator or field
* @param expr the expression of the aggregate operator for the field
* @returns {*}
*/
function accumulate (collection, field, expr) {
if (has(OPERATORS[OP_GROUP], field)) {
return OPERATORS[OP_GROUP][field](collection, expr)
}
if (isObject(expr)) {
let result = {};
each(expr, (val, key) => {
result[key] = accumulate(collection, key, expr[key]);
// must run ONLY one group operator per expression
// if so, return result of the computed value
if (has(OPERATORS[OP_GROUP], key)) {
result = result[key];
// if there are more keys in expression this is bad
assert(keys(expr).length === 1, "Invalid $group expression '" + JSON.stringify(expr) + "'");
return false // break
}
});
return result
}
}
/**
* Computes the actual value of the expression using the given object as context
*
* @param obj the current object from the collection
* @param expr the expression for the given field
* @param operator the operator to resolve the field with
* @param opt {Object} extra options
* @returns {*}
*/
function computeValue (obj, expr, operator = null, opt = {}) {
opt.root = opt.root || obj;
// if the field of the object is a valid operator
if (has(OPERATORS[OP_EXPRESSION], operator)) {
return OPERATORS[OP_EXPRESSION][operator](obj, expr, opt)
}
// we also handle $group accumulator operators
if (has(OPERATORS[OP_GROUP], operator)) {
// we first fully resolve the expression
obj = computeValue(obj, expr, null, opt);
assert(isArray(obj), operator + ' expression must resolve to an array');
// we pass a null expression because all values have been resolved
return OPERATORS[OP_GROUP][operator](obj, null, opt)
}
// if expr is a variable for an object field
// field not used in this case
if (isString(expr) && expr.length > 0 && expr[0] === '$') {
// we return system variables as literals
if (inArray(SYS_VARS, expr)) {
return systemVariables[expr](obj, null, opt)
} else if (inArray(REDACT_VARS, expr)) {
return expr
}
// handle selectors with explicit prefix
let sysVar = SYS_VARS.filter((v) => expr.indexOf(v + '.') === 0);
if (sysVar.length === 1) {
sysVar = sysVar[0];
if (sysVar === '$$ROOT') {
obj = opt.root;
}
expr = expr.substr(sysVar.length); // '.' prefix will be sliced off below
}
return resolve(obj, expr.slice(1))
}
// check and return value if already in a resolved state
switch (jsType(expr)) {
case T_ARRAY:
return expr.map(item => computeValue(obj, item))
case T_OBJECT:
let result = {};
each(expr, (val, key) => {
result[key] = computeValue(obj, val, key, opt);
// must run ONLY one aggregate operator per expression
// if so, return result of the computed value
if ([OP_EXPRESSION, OP_GROUP].some(c => has(OPERATORS[c], key))) {
// there should be only one operator
assert(keys(expr).length === 1, "Invalid aggregation expression '" + JSON.stringify(expr) + "'");
result = result[key];
return false // break
}
});
return result
default:
return expr
}
}
/**
* Redact an object
* @param {Object} obj The object to redact
* @param {*} expr The redact expression
* @param {*} opt Options for value
* @return {*} Returns the redacted value
*/
function redactObj (obj, expr, opt = {}) {
opt.root = opt.root || obj;
let result = computeValue(obj, expr, null, opt);
return inArray(REDACT_VARS, result)
? redactVariables[result](obj, expr, opt)
: result
}
/**
* Returns the absolute value of a number.
* https://docs.mongodb.com/manual/reference/operator/aggregation/abs/#exp._S_abs
*
* @param obj
* @param expr
* @return {Number|null|NaN}
*/
function $abs (obj, expr) {
let val = computeValue(obj, expr);
return (val === null || val === undefined) ? null : Math.abs(val)
}
/**
* Computes the sum of an array of numbers.
*
* @param obj
* @param expr
* @returns {Object}
*/
function $add (obj, expr) {
let args = computeValue(obj, expr);
let foundDate = false;
let result = reduce(args, (acc, val) => {
if (isDate(val)) {
assert(!foundDate, "'$add' can only have one date value");
foundDate = true;
val = val.getTime();
}
// assume val is a number
acc += val;
return acc
}, 0);
return foundDate ? new Date(result) : result
}
/**
* Returns the smallest integer greater than or equal to the specified number.
*
* @param obj
* @param expr
* @returns {number}
*/
function $ceil (obj, expr) {
let arg = computeValue(obj, expr);
if (isNil(arg)) return null
assert(isNumber(arg) || isNaN(arg), '$ceil expression must resolve to a number.');
return Math.ceil(arg)
}
/**
* Takes two numbers and divides the first number by the second.
*
* @param obj
* @param expr
* @returns {number}
*/
function $divide (obj, expr) {
let args = computeValue(obj, expr);
return args[0] / args[1]
}
/**
* Raises Eulers number (i.e. e ) to the specified exponent and returns the result.
*
* @param obj
* @param expr
* @returns {number}
*/
function $exp (obj, expr) {
let arg = computeValue(obj, expr);
if (isNil(arg)) return null
assert(isNumber(arg) || isNaN(arg), '$exp expression must resolve to a number.');
return Math.exp(arg)
}
/**
* Returns the largest integer less than or equal to the specified number.
*
* @param obj
* @param expr
* @returns {number}
*/
function $floor (obj, expr) {
let arg = computeValue(obj, expr);
if (isNil(arg)) return null
assert(isNumber(arg) || isNaN(arg), '$floor expression must resolve to a number.');
return Math.floor(arg)
}
/**
* Calculates the natural logarithm ln (i.e loge) of a number and returns the result as a double.
*
* @param obj
* @param expr
* @returns {number}
*/
function $ln (obj, expr) {
let arg = computeValue(obj, expr);
if (isNil(arg)) return null
assert(isNumber(arg) || isNaN(arg), '$ln expression must resolve to a number.');
return Math.log(arg)
}
/**
* Calculates the log of a number in the specified base and returns the result as a double.
*
* @param obj
* @param expr
* @returns {number}
*/
function $log (obj, expr) {
let args = computeValue(obj, expr);
const msg = '$log expression must resolve to array(2) of numbers';
assert(isArray(args) && args.length === 2, msg);
if (args.some(isNil)) return null
assert(args.some(isNaN) || args.every(isNumber), msg);
return Math.log10(args[0]) / Math.log10(args[1])
}
/**
* Calculates the log base 10 of a number and returns the result as a double.
*
* @param obj
* @param expr
* @returns {number}
*/
function $log10 (obj, expr) {
let arg = computeValue(obj, expr);
if (isNil(arg)) return null
assert(isNumber(arg) || isNaN(arg), '$log10 expression must resolve to a number.');
return Math.log10(arg)
}
/**
* Takes two numbers and calculates the modulo of the first number divided by the second.
*
* @param obj
* @param expr
* @returns {number}
*/
function $mod (obj, expr) {
let args = computeValue(obj, expr);
return args[0] % args[1]
}
/**
* Computes the product of an array of numbers.
*
* @param obj
* @param expr
* @returns {Object}
*/
function $multiply (obj, expr) {
let args = computeValue(obj, expr);
return reduce(args, (acc, num) => acc * num, 1)
}
/**
* Raises a number to the specified exponent and returns the result.
*
* @param obj
* @param expr
* @returns {Object}
*/
function $pow (obj, expr) {
let args = computeValue(obj, expr);
assert(isArray(args) && args.length === 2 && args.every(isNumber), '$pow expression must resolve to array(2) of numbers');
assert(!(args[0] === 0 && args[1] < 0), '$pow cannot raise 0 to a negative exponent');
return Math.pow(args[0], args[1])
}
/**
* Rounds a number to to a whole integer or to a specified decimal place.
* @param {*} obj
* @param {*} expr
*/
function $round (obj, expr) {
let args = computeValue(obj, expr);
let num = args[0];
let place = args[1];
if (isNil(num) || isNaN(num) || Math.abs(num) === Infinity) return num
assert(isNumber(num), '$round expression must resolve to a number.');
return truncate(num, place, true)
}
/**
* Calculates the square root of a positive number and returns the result as a double.
*
* @param obj
* @param expr
* @returns {number}
*/
function $sqrt (obj, expr) {
let n = computeValue(obj, expr);
if (isNil(n)) return null
assert(isNumber(n) && n > 0 || isNaN(n), '$sqrt expression must resolve to non-negative number.');
return Math.sqrt(n)
}
/**
* Takes an array that contains two numbers or two dates and subtracts the second value from the first.
*
* @param obj
* @param expr
* @returns {number}
*/
function $subtract (obj, expr) {
let args = computeValue(obj, expr);
return args[0] - args[1]
}
/**
* Truncates a number to a whole integer or to a specified decimal place.
*
* @param obj
* @param expr
* @returns {number}
*/
function $trunc (obj, expr) {
let arr = computeValue(obj, expr);
let num = arr[0];
let places = arr[1];
if (isNil(num) || isNaN(num) || Math.abs(num) === Infinity) return num
assert(isNumber(num), '$trunc expression must resolve to a number.');
assert(isNil(places) || (isNumber(places) && places > -20 && places < 100), "$trunc expression has invalid place");
return truncate(num, places, false)
}
/**
* Truncates integer value to number of places. If roundOff is specified round value instead to the number of places
* @param {Number} num
* @param {Number} places
* @param {Boolean} roundOff
*/
function truncate(num, places, roundOff) {
places = places || 0;
let sign = Math.abs(num) === num ? 1 : -1;
num = Math.abs(num);
let result = Math.trunc(num);
let decimals = num - result;
if (places === 0) {
let firstDigit = Math.trunc(10 * decimals);
if (roundOff && (result & 1) === 1 && firstDigit >= 5) {
result++;
}
} else if (places > 0) {
let offset = Math.pow(10, places);
let remainder = Math.trunc(decimals * offset);
// last digit before cut off
let lastDigit = Math.trunc(decimals * offset * 10) % 10;
// add one if last digit is greater than 5
if (roundOff && lastDigit > 5) {
remainder += 1;
}
// compute decimal remainder and add to whole number
result += (remainder / offset);
} else if (places < 0) {
// handle negative decimal places
let offset = Math.pow(10, -1*places);
let excess = result % offset;
result = Math.max(0, result - excess);
// for negative values the absolute must increase so we round up the last digit if >= 5
if (roundOff && sign === -1) {
while (excess > 10) {
excess -= excess % 10;
}
if (result > 0 && excess >= 5) {
result += offset;
}
}
}
return result * sign
}
/**
* Returns the element at the specified array index.
*
* @param {Object} obj
* @param {*} expr
* @return {*}
*/
function $arrayElemAt (obj, expr) {
let arr = computeValue(obj, expr);
assert(isArray(arr) && arr.length === 2, '$arrayElemAt expression must resolve to array(2)');
assert(isArray(arr[0]), 'First operand to $arrayElemAt must resolve to an array');
assert(isNumber(arr[1]), 'Second operand to $arrayElemAt must resolve to an integer');
let idx = arr[1];
arr = arr[0];
if (idx < 0 && Math.abs(idx) <= arr.length) {
return arr[idx + arr.length]
} else if (idx >= 0 && idx < arr.length) {
return arr[idx]
}
return undefined
}
/**
* Converts an array of key value pairs to a document.
*/
function $arrayToObject (obj, expr) {
let arr = computeValue(obj, expr);
assert(isArray(arr), '$arrayToObject expression must resolve to an array');
return reduce(arr, (newObj, val) => {
if (isArray(val) && val.length == 2) {
newObj[val[0]] = val[1];
} else {
assert(isObject(val) && has(val, 'k') && has(val, 'v'), '$arrayToObject expression is invalid.');
newObj[val.k] = val.v;
}
return newObj
}, {})
}
/**
* Concatenates arrays to return the concatenated array.
*
* @param {Object} obj
* @param {*} expr
* @return {*}
*/
function $concatArrays (obj, expr) {
let arr = computeValue(obj, expr, null);
assert(isArray(arr), '$concatArrays must resolve to an array');
if (arr.some(isNil)) return null
return arr.reduce((acc, item) => into(acc, item), [])
}
/**
* Selects a subset of the array to return an array with only the elements that match the filter condition.
*
* @param {Object} obj [description]
* @param {*} expr [description]
* @return {*} [description]
*/
function $filter (obj, expr) {
let input = computeValue(obj, expr.input);
let asVar = expr['as'];
let condExpr = expr['cond'];
assert(isArray(input), "$filter 'input' expression must resolve to an array");
return input.filter((o) => {
// inject variable
let tempObj = {};
tempObj['$' + asVar] = o;
return computeValue(tempObj, condExpr) === true
})
}
/**
* Returns a boolean indicating whether a specified value is in an array.
*
* @param {Object} obj
* @param {Array} expr
*/
function $in (obj, expr) {
let val = computeValue(obj, expr[0]);
let arr = computeValue(obj, expr[1]);
assert(isArray(arr), '$in second argument must be an array');
return arr.some(isEqual.bind(null, val))
}
/**
* Searches an array for an occurrence of a specified value and returns the array index of the first occurrence.
* If the substring is not found, returns -1.
*
* @param {Object} obj
* @param {*} expr
* @return {*}
*/
function $indexOfArray (obj, expr) {
let args = computeValue(obj, expr);
if (isNil(args)) return null
let arr = args[0];
let searchValue = args[1];
if (isNil(arr)) return null
assert(isArray(arr), '$indexOfArray expression must resolve to an array.');
let start = args[2] || 0;
let end = args[3];
if (isNil(end)) end = arr.length;
if (start > end) return -1
assert(start >= 0 && end >= 0, '$indexOfArray expression is invalid');
if (start > 0 || end < arr.length) {
arr = arr.slice(start, end);
}
return arr.findIndex(isEqual.bind(null, searchValue)) + start
}
/**
* Determines if the operand is an array. Returns a boolean.
*
* @param {Object} obj
* @param {*} expr
* @return {Boolean}
*/
function $isArray (obj, expr) {
return isArray(computeValue(obj, expr[0]))
}
/**
* Applies a sub-expression to each element of an array and returns the array of resulting values in order.
*
* @param obj
* @param expr
* @returns {Array|*}
*/
function $map (obj, expr) {
let inputExpr = computeValue(obj, expr.input);
assert(isArray(inputExpr), `$map 'input' expression must resolve to an array`);
let asExpr = expr['as'];
let inExpr = expr['in'];
// HACK: add the "as" expression as a value on the object to take advantage of "resolve()"
// which will reduce to that value when invoked. The reference to the as expression will be prefixed with "$$".
// But since a "$" is stripped of before passing the name to "resolve()" we just need to prepend "$" to the key.
let tempKey = '$' + asExpr;
return inputExpr.map(item => {
obj[tempKey] = item;
return computeValue(obj, inExpr)
})
}
/**
* Converts a document to an array of documents representing key-value pairs.
*/
function $objectToArray (obj, expr) {
let val = computeValue(obj, expr);
assert(isObject(val), '$objectToArray expression must resolve to an object');
let arr = [];
each(val, (v,k) => arr.push({k,v}));
return arr
}
/**
* Returns an array whose elements are a generated sequence of numbers.
*
* @param {Object} obj
* @param {*} expr
* @return {*}
*/
function $range (obj, expr) {
let arr = computeValue(obj, expr);
let start = arr[0];
let end = arr[1];
let step = arr[2] || 1;
let result = [];
while ((start < end && step > 0) || (start > end && step < 0)) {
result.push(start);
start += step;
}
return result
}
/**
* Applies an expression to each element in an array and combines them into a single value.
*
* @param {Object} obj
* @param {*} expr
*/
function $reduce (obj, expr) {
let input = computeValue(obj, expr.input);
let initialValue = computeValue(obj, expr.initialValue);
let inExpr = expr['in'];
if (isNil(input)) return null
assert(isArray(input), "$reduce 'input' expression must resolve to an array");
return reduce(input, (acc, n) => computeValue({ '$value': acc, '$this': n }, inExpr), initialValue)
}
/**
* Returns an array with the elements in reverse order.
*
* @param {Object} obj
* @param {*} expr
* @return {*}
*/
function $reverseArray (obj, expr) {
let arr = computeValue(obj, expr);
if (isNil(arr)) return null
assert(isArray(arr), '$reverseArray expression must resolve to an array');
let result = [];
into(result, arr);
result.reverse();
return result
}
/**
* Counts and returns the total the number of items in an array.
*
* @param obj
* @param expr
*/
function $size (obj, expr) {
let value = computeValue(obj, expr);
return isArray(value) ? value.length : undefined
}
/**
* Returns a subset of an array.
*
* @param {Object} obj
* @param {*} expr
* @return {*}
*/
function $slice (obj, expr) {
let arr = computeValue(obj, expr);
return slice(arr[0], arr[1], arr[2])
}
/**
* Merge two lists together.
*
* Transposes an array of input arrays so that the first element of the output array would be an array containing,
* the first element of the first input array, the first element of the second input array, etc.
*
* @param {Obj} obj
* @param {*} expr
* @return {*}
*/
function $zip (obj, expr) {
let inputs = computeValue(obj, expr.inputs);
let useLongestLength = expr.useLongestLength || false;
assert(isArray(inputs), "'inputs' expression must resolve to an array");
assert(isBoolean(useLongestLength), "'useLongestLength' must be a boolean");
if (isArray(expr.defaults)) {
assert(truthy(useLongestLength), "'useLongestLength' must be set to true to use 'defaults'");
}
let zipCount = 0;
for (let i = 0, len = inputs.length; i < len; i++) {
let arr = inputs[i];
if (isNil(arr)) return null
assert(isArray(arr), "'inputs' expression values must resolve to an array or null");
zipCount = useLongestLength
? Math.max(zipCount, arr.length)
: Math.min(zipCount || arr.length, arr.length);
}
let result = [];
let defaults = expr.defaults || [];
for (let i = 0; i < zipCount; i++) {
let temp = inputs.map((val, index) => {
return isNil(val[i]) ? (defaults[index] || null) : val[i]
});
result.push(temp);
}
return result
}
/**
* Combines multiple documents into a single document.
* @param {*} obj
* @param {*} expr
*/
function $mergeObjects (obj, expr) {
let docs = computeValue(obj, expr);
if (isArray(docs)) {
return reduce(docs, (memo, o) => Object.assign(memo, o), {})
}
return {}
}
/**
* Returns true only when all its expressions evaluate to true. Accepts any number of argument expressions.
*
* @param obj
* @param expr
* @returns {boolean}
*/
function $and (obj, expr) {
let value = computeValue(obj, expr);
return truthy(value) && value.every(truthy)
}
/**
* Returns true when any of its expressions evaluates to true. Accepts any number of argument expressions.
*
* @param obj
* @param expr
* @returns {boolean}
*/
function $or (obj, expr) {
let value = computeValue(obj, expr);
return truthy(value) && value.some(truthy)
}
/**
* Returns the boolean value that is the opposite of its argument expression. Accepts a single argument expression.
*
* @param obj
* @param expr
* @returns {boolean}
*/
function $not (obj, expr) {
return !computeValue(obj, expr[0])
}
/**
* Returns an iterator
* @param {*} source An iterable source (Array, Function, Object{next:Function})
*/
function Lazy (source) {
return (source instanceof Iterator) ? source : new Iterator(source)
}
Lazy.isIterator = isIterator;
/**
* Checks whether the given object is compatible with iterator i.e Object{next:Function}
* @param {*} o An object
*/
function isIterator (o) {
return !!o && typeof o === 'object' && isFn(o.next)
}
function isFn (f) {
return !!f && typeof f === 'function'
}
function dropItem (array, i) {
let rest = array.slice(i + 1);
array.splice(i);
Array.prototype.push.apply(array, rest);
}
// stop iteration error
const DONE = new Error();
// Lazy function type flags
const LAZY_MAP = 1;
const LAZY_FILTER = 2;
const LAZY_TAKE = 3;
const LAZY_DROP = 4;
function baseIterator (nextFn, iteratees, buffer) {
let done = false;
let index = -1;
let bIndex = 0; // index for the buffer
return function (b) {
// special hack to collect all values into buffer
b = b === buffer;
try {
outer: while (!done) {
let o = nextFn();
index++;
let mIndex = -1;
let mSize = iteratees.length;
let innerDone = false;
while (++mIndex < mSize) {
let member = iteratees[mIndex],
func = member.func,
type = member.type;
switch (type) {
case LAZY_MAP:
o = func(o, index);
break
case LAZY_FILTER:
if (!func(o, index)) continue outer
break
case LAZY_TAKE:
--member.func;
if (!member.func) innerDone = true;
break
case LAZY_DROP:
--member.func;
if (!member.func) dropItem(iteratees, mIndex);
continue outer
default:
break outer
}
}
done = innerDone;
if (b) {
buffer[bIndex++] = o;
} else {
return { value: o, done: false }
}
}
} catch (e) {
if (e !== DONE) throw e
}
done = true;
return { done: true }
}
}
class Iterator {
/**
* @param {*} source An iterable object or function.
* Array - return one element per cycle
* Object{next:Function} - call next() for the next value (this also handles generator functions)
* Function - call to return the next value
* @param {Function} fn An optional transformation function
*/
constructor (source) {
this.__iteratees = []; // lazy function chain
this.__first = false; // flag whether to return a single value
this.__done = false;
this.__buf = [];
if (isFn(source)) {
// make iterable
source = { next: source };
}
if (isIterator(source)) {
const src = source;
source = () => {
let o = src.next();
if (o.done) throw DONE
return o.value
};
} else if (Array.isArray(source)) {
const data = source;
const size = data.length;
let index = 0;
source = () => {
if (index < size) return data[index++]
throw DONE
};
} else if (!isFn(source)) {
throw new Error("Source is not iterable. Must be Array, Function or Object{next:Function}")
}
// create next function
this.next = baseIterator(source, this.__iteratees, this.__buf);
}
_validate () {
if (this.__first) throw new Error("Cannot add iteratee/transform after `first()`")
}
/**
* Add an iteratee to this lazy sequence
* @param {Object} iteratee
*/
_push (iteratee) {
this._validate();
this.__iteratees.push(iteratee);
return this
}
// Iteratees methods
/**
* Transform each item in the sequence to a new value
* @param {Function} f
*/
map (f) {
return this._push({ type: LAZY_MAP, func: f })
}
/**
* Select only items matching the given predicate
* @param {Function} pred
*/
filter (pred) {
return this._push({ type: LAZY_FILTER, func: pred })
}
/**
* Take given numbe for values from sequence
* @param {Number} n A number greater than 0
*/
take (n) {
return n > 0 ? this._push({ type: LAZY_TAKE, func: n }) : this
}
/**
* Drop a number of values from the sequence
* @param {Number} n Number of items to drop greater than 0
*/
drop (n) {
return n > 0 ? this._push({ type: LAZY_DROP, func: n }) : this
}
// Transformations
/**
* Returns a new lazy object with results of the transformation
* The entire sequence is realized.
*
* @param {Function} fn Tranform function of type (Array) => (Any)
*/
transform (fn) {
this._validate();
let self = this;
let iter;
return Lazy(() => {
if (!iter) {
iter = Lazy(fn(self.value()));
}
return iter.next()
})
}
/**
* Mark this lazy object to return only the first result on `lazy.value()`.
* No more iteratees or transformations can be added after this method is called.
*/
first () {
this.take(1);
this.__first = true;
return this
}
// Terminal methods
/**
* Returns the fully realized values of the iterators.
* The return value will be an array unless `lazy.first()` was used.
* The realized values are cached for subsequent calls
*/
value () {
if (!this.__done) {
this.__done = this.next(this.__buf).done;
}
return this.__first ? this.__buf[0] : this.__buf
}
/**
* Execute the funcion for each value. Will stop when an execution returns false.
* @param {Function} f
* @returns {Boolean} false iff `f` return false for any execution, otherwise true
*/
each (f) {
while (1) {
let o = this.next();
if (o.done) break
if (f(o.value) === false) return false
}
return true
}
/**
* Returns the reduction of sequence according the reducing function
*
* @param {*} f a reducing function
* @param {*} init
*/
reduce (f, init) {
let o = this.next();
let i = 0;
if (init === undefined && !o.done) {
init = o.value;
o = this.next();
i++;
}
while (!o.done) {
init = f(init, o.value, i++);
o = this.next();
}
return init
}
/**
* Returns the number of matched items in the sequence
*/
size () {
return this.reduce((acc,n) => ++acc, 0)
}
}
if (typeof Symbol === 'function') {
Iterator.prototype[Symbol.iterator] = function() {
return this
};
}
/**
* Aggregator for defining filter using mongoDB aggregation pipeline syntax
*
* @param operators an Array of pipeline operators
* @constructor
*/
class Aggregator {
constructor (operators, options) {
this.__operators = operators;
this.__options = options;
}
/**
* Returns an `Lazy` iterator for processing results of pipeline
*
* @param {*} collection An array or iterator object
* @param {Query} query the `Query` object to use as context
* @returns {Iterator} an iterator object
*/
stream (collection, query) {
collection = Lazy(collection);
const pipelineOperators = OPERATORS[OP_PIPELINE];
if (!isEmpty(this.__operators)) {
// run aggregation pipeline
each(this.__operators, (operator) => {
let key = keys(operator);
assert(key.length === 1 && inArray(ops(OP_PIPELINE), key[0]), `invalid aggregation operator ${key}`);
key = key[0];
if (query && query instanceof Query) {
collection = pipelineOperators[key].call(query, collection, operator[key], this.__options);
} else {
collection = pipelineOperators[key](collection, operator[key], this.__options);
}
});
}
return collection
}
/**
* Return the results of the aggregation as an array.
* @param {*} collection
* @param {*} query
*/
run (collection, query) {
return this.stream(collection, query).value()
}
}
/**
* Return the result collection after running the aggregation pipeline for the given collection.
* Shorthand for `(new Aggregator(pipeline, options)).run(collection)`
*
* @param {Array} collection Collection or stream of objects
* @param {Array} pipeline The pipeline operators to use
* @returns {Array}
*/
function aggregate (collection, pipeline, options) {
assert(isArray(pipeline), 'Aggregation pipeline must be an array');
return (new Aggregator(pipeline, options)).run(collection)
}
/**
* Cursor to iterate and perform filtering on matched objects
* @param collection
* @param query
* @param projection
* @constructor
*/
class Cursor {
constructor (source, query, projection) {
this.__filterFn = query.test.bind(query);
this.__query = query;
this.__source = source;
this.__projection = projection || query.__projection;
this.__operators = [];
this.__result = null;
this.__stack = [];
this.__options = {};
}
_fetch () {
if (!!this.__result) return this.__result
// add projection operator
if (isObject(this.__projection)) this.__operators.push({ '$project': this.__projection });
// filter collection
this.__result = Lazy(this.__source).filter(this.__filterFn);
if (this.__operators.length > 0) {
this.__result = (new Aggregator(this.__operators, this.__options)).stream(this.__result, this.__query);
}
return this.__result
}
/**
* Return remaining objects in the cursor as an array. This method exhausts the cursor
* @returns {Array}
*/
all () {
return this._fetch().value()
}
/**
* Returns the number of objects return in the cursor. This method exhausts the cursor
* @returns {Number}
*/
count () {
return this.all().length
}
/**
* Returns a cursor that begins returning results only after passing or skipping a number of documents.
* @param {Number} n the number of results to skip.
* @return {Cursor} Returns the cursor, so you can chain this call.
*/
skip (n) {
this.__operators.push({ '$skip': n });
return this
}
/**
* Constrains the size of a cursor's result set.
* @param {Number} n the number of results to limit to.
* @return {Cursor} Returns the cursor, so you can chain this call.
*/
limit (n) {
this.__operators.push({ '$limit': n });
return this
}
/**
* Returns results ordered according to a sort specification.
* @param {Object} modifier an object of key and values specifying the sort order. 1 for ascending and -1 for descending
* @return {Cursor} Returns the cursor, so you can chain this call.
*/
sort (modifier) {
this.__operators.push({ '$sort': modifier });
return this
}
/**
* Specifies the collation for the cursor returned by the `mingo.Query.find`
* @param {*} options
*/
collation (options) {
this.__options['collation'] = options;
return this
}
/**
* Returns the next document in a cursor.
* @returns {Object | Boolean}
*/
next () {
if (!this.__stack) return // done
if (this.__stack.length > 0) return this.__stack.pop() // yield value obtains in hasNext()
let o = this._fetch().next();
if (!o.done) return o.value
this.__stack = null;
return
}
/**
* Returns true if the cursor has documents and can be iterated.
* @returns {boolean}
*/
hasNext () {
if (!this.__stack) return false // done
if (this.__stack.length > 0) return true // there is a value on stack
let o = this._fetch().next();
if (!o.done) {
this.__stack.push(o.value);
} else {
this.__stack = null;
}
return !!this.__stack
}
/**
* Applies a function to each document in a cursor and collects the return values in an array.
* @param callback
* @returns {Array}
*/
map (callback) {
return this._fetch().map(callback).value()
}
/**
* Applies a JavaScript function for every document in a cursor.
* @param callback
*/
forEach (callback) {
this._fetch().each(callback);
}
}
if (typeof Symbol === 'function') {
/**
* Applies an [ES2015 Iteration protocol][] compatible implementation
* [ES2015 Iteration protocol]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
* @returns {Object}
*/
Cursor.prototype[Symbol.iterator] = function () {
return this._fetch()
};
}
/**
* Query object to test collection elements with
* @param criteria the pass criteria for the query
* @param projection optional projection specifiers
* @constructor
*/
class Query {
constructor (criteria, projection) {
this.__criteria = criteria;
this.__projection = projection || {};
this.__compiled = [];
this._compile();
}
_compile () {
assert(isObject(this.__criteria), 'query criteria must be an object');
let whereOperator;
each(this.__criteria, (expr, field) => {
// save $where operators to be executed after other operators
if ('$where' === field) {
whereOperator = { field: field, expr: expr };
} else if ('$expr' === field) {
this._processOperator(field, field, expr);
} else if (inArray(['$and', '$or', '$nor'], field)) {
this._processOperator(field, field, expr);
} else {
// normalize expression
assert(!isOperator(field), `unknown top level operator: ${field}`);
expr = normalize(expr);
each(expr, (val, op) => {
this._processOperator(field, op, val);
});
}
if (isObject(whereOperator)) {
this._processOperator(whereOperator.field, whereOperator.field, whereOperator.expr);
}
});
}
_processOperator (field, operator, value) {
assert(has(OPERATORS[OP_QUERY], operator), `invalid query operator ${operator} detected`);
this.__compiled.push(OPERATORS[OP_QUERY][operator](field, value));
}
/**
* Checks if the object passes the query criteria. Returns true if so, false otherwise.
* @param obj
* @returns {boolean}
*/
test (obj) {
for (let i = 0, len = this.__compiled.length; i < len; i++) {
if (!this.__compiled[i](obj)) {
return false
}
}
return true
}
/**
* Performs a query on a collection and returns a cursor object.
* @param collection
* @param projection
* @returns {Cursor}
*/
find (collection, projection) {
return new Cursor(collection, this, projection)
}
/**
* Remove matched documents from the collection returning the remainder
* @param collection
* @returns {Array}
*/
remove (collection) {
return reduce(collection, (acc, obj) => {
if (!this.test(obj)) acc.push(obj);
return acc
}, [])
}
}
/**
* Performs a query on a collection and returns a cursor object.
*
* @param collection
* @param criteria
* @param projection
* @returns {Cursor}
*/
function find (collection, criteria, projection) {
return new Query(criteria).find(collection, projection)
}
/**
* Returns a new array without objects which match the criteria
*
* @param collection
* @param criteria
* @returns {Array}
*/
function remove (collection, criteria) {
return new Query(criteria).remove(collection)
}
/**
* Query and Projection Operators. https://docs.mongodb.com/manual/reference/operator/query/
*/
/**
* Checks that two values are equal.
*
* @param a The lhs operand as resolved from the object by the given selector
* @param b The rhs operand provided by the user
* @returns {*}
*/
function $eq (a, b) {
// start with simple equality check
if (isEqual(a, b)) return true
// https://docs.mongodb.com/manual/tutorial/query-for-null-fields/
if (isNil(a) && isNil(b)) return true
// check
if (isArray(a)) {
let eq = isEqual.bind(null, b);
return a.some(eq) || flatten(a, 1).some(eq)
}
return false
}
/**
* Matches all values that are not equal to the value specified in the query.
*
* @param a
* @param b
* @returns {boolean}
*/
function $ne (a, b) {
return !$eq(a, b)
}
/**
* Matches any of the values that exist in an array specified in the query.
*
* @param a
* @param b
* @returns {*}
*/
function $in$1 (a, b) {
// queries for null should be able to find undefined fields
if (isNil(a)) return b.some(isNull)
return intersection(ensureArray(a), b).length > 0
}
/**
* Matches values that do not exist in an array specified to the query.
*
* @param a
* @param b
* @returns {*|boolean}
*/
function $nin (a, b) {
return !$in$1(a, b)
}
/**
* Matches values that are less than the value specified in the query.
*
* @param a
* @param b
* @returns {boolean}
*/
function $lt (a, b) {
return compare$1(a, b, (x,y) => x < y)
}
/**
* Matches values that are less than or equal to the value specified in the query.
*
* @param a
* @param b
* @returns {boolean}
*/
function $lte (a, b) {
return compare$1(a, b, (x,y) => x <= y)
}
/**
* Matches values that are greater than the value specified in the query.
*
* @param a
* @param b
* @returns {boolean}
*/
function $gt (a, b) {
return compare$1(a, b, (x,y) => x > y)
}
/**
* Matches values that are greater than or equal to the value specified in the query.
*
* @param a
* @param b
* @returns {boolean}
*/
function $gte (a, b) {
return compare$1(a, b, (x,y) => x >= y)
}
/**
* Performs a modulo operation on the value of a field and selects documents with a specified result.
*
* @param a
* @param b
* @returns {boolean}
*/
function $mod$1 (a, b) {
return ensureArray(a).some(x => isNumber(x) && isArray(b) && b.length === 2 && (x % b[0]) === b[1])
}
/**
* Selects documents where values match a specified regular expression.
*
* @param a
* @param b
* @returns {boolean}
*/
function $regex (a, b) {
a = ensureArray(a);
let match = (x => isString(x) && !!x.match(b));
return a.some(match) || flatten(a, 1).some(match)
}
/**
* Matches documents that have the specified field.
*
* @param a
* @param b
* @returns {boolean}
*/
function $exists (a, b) {
return ((b === false || b === 0) && a === undefined) || ((b === true || b === 1) && a !== undefined)
}
/**
* Matches arrays that contain all elements specified in the query.
*
* @param a
* @param b
* @returns boolean
*/
function $all (a, b) {
let matched = false;
if (isArray(a) && isArray(b)) {
for (let i = 0, len = b.length; i < len; i++) {
if (isObject(b[i]) && inArray(keys(b[i]), '$elemMatch')) {
matched = matched || $elemMatch(a, b[i].$elemMatch);
} else {
// order of arguments matter
return intersection(b, a).length === len
}
}
}
return matched
}
/**
* Selects documents if the array field is a specified size.
*
* @param a
* @param b
* @returns {*|boolean}
*/
function $size$1 (a, b) {
return isArray(a) && isNumber(b) && (a.length === b)
}
/**
* Selects documents if element in the array field matches all the specified $elemMatch condition.
*
* @param a {Array} element to match against
* @param b {Object} subquery
*/
function $elemMatch (a, b) {
if (isArray(a) && !isEmpty(a)) {
let format = x => x;
let criteria = b;
// If we find an operator in the subquery, we fake a field to point to it.
// This is an attempt to ensure that it a valid criteria.
if (keys(b).every(isOperator)) {
criteria = { temp: b };
format = x => ({ temp: x });
}
let query = new Query(criteria);
for (let i = 0, len = a.length; i < len; i++) {
if (query.test(format(a[i]))) {
return true
}
}
}
return false
}
/**
* Selects documents if a field is of the specified type.
*
* @param a
* @param b
* @returns {boolean}
*/
function $type (a, b) {
switch (b) {
case 1:
case 'double':
return isNumber(a) && (a + '').indexOf('.') !== -1
case 2:
case T_STRING:
return isString(a)
case 3:
case T_OBJECT:
return isObject(a)
case 4:
case T_ARRAY:
return isArray(a)
case 6:
case T_UNDEFINED:
return isNil(a)
case 8:
case T_BOOL:
return isBoolean(a)
case 9:
case T_DATE:
return isDate(a)
case 10:
case T_NULL:
return isNull(a)
case 11:
case T_REGEX:
return isRegExp(a)
case 16:
case 'int':
return isNumber(a) && a <= 2147483647 && (a + '').indexOf('.') === -1
case 18:
case 'long':
return isNumber(a) && a > 2147483647 && a <= 9223372036854775807 && (a + '').indexOf('.') === -1
case 19:
case 'decimal':
return isNumber(a)
default:
return false
}
}
function compare$1(a, b, f) {
return ensureArray(a).some(x => getType(x) === getType(b) && f(x,b))
}
function createComparison (f) {
return (obj, expr) => {
let args = computeValue(obj, expr);
return f(args[0], args[1])
}
}
const $eq$1 = createComparison($eq);
const $ne$1 = createComparison($ne);
const $gt$1 = createComparison($gt);
const $lt$1 = createComparison($lt);
const $gte$1 = createComparison($gte);
const $lte$1 = createComparison($lte);
const $nin$1 = createComparison($nin);
/**
* Compares two values and returns the result of the comparison as an integer.
*
* @param obj
* @param expr
* @returns {number}
*/
function $cmp (obj, expr) {
let args = computeValue(obj, expr);
if (args[0] > args[1]) return 1
if (args[0] < args[1]) return -1
return 0
}
/**
* Conditional operators
*/
/**
* A ternary operator that evaluates one expression,
* and depending on the result returns the value of one following expressions.
*
* @param obj
* @param expr
*/
function $cond (obj, expr) {
let ifExpr, thenExpr, elseExpr;
const errorMsg = '$cond: invalid arguments';
if (isArray(expr)) {
assert(expr.length === 3, errorMsg);
ifExpr = expr[0];
thenExpr = expr[1];
elseExpr = expr[2];
} else {
assert(isObject(expr), errorMsg);
ifExpr = expr['if'];
thenExpr = expr['then'];
elseExpr = expr['else'];
}
let condition = computeValue(obj, ifExpr);
return condition ? computeValue(obj, thenExpr) : computeValue(obj, elseExpr)
}
/**
* An operator that evaluates a series of case expressions. When it finds an expression which
* evaluates to true, it returns the resulting expression for that case. If none of the cases
* evaluate to true, it returns the default expression.
*
* @param obj
* @param expr
*/
function $switch (obj, expr) {
const errorMsg = 'Invalid arguments for $switch operator';
assert(expr.branches, errorMsg);
let validBranch = expr.branches.find((branch) => {
assert(branch['case'] && branch['then'], errorMsg);
return computeValue(obj, branch['case'])
});
if (validBranch) {
return computeValue(obj, validBranch.then)
} else {
assert(expr['default'], errorMsg);
return computeValue(obj, expr.default)
}
}
/**
* Evaluates an expression and returns the first expression if it evaluates to a non-null value.
* Otherwise, $ifNull returns the second expression's value.
*
* @param obj
* @param expr
* @returns {*}
*/
function $ifNull (obj, expr) {
assert(isArray(expr) && expr.length === 2, '$ifNull expression must resolve to array(2)');
let args = computeValue(obj, expr);
return isNil(args[0]) ? args[1] : args[0]
}
/**
* Returns the day of the year for a date as a number between 1 and 366 (leap year).
* @param obj
* @param expr
*/
function $dayOfYear (obj, expr) {
let d = computeValue(obj, expr);
let start = new Date(d.getFullYear(), 0, 0);
let diff = d - start;
let oneDay = 1000 * 60 * 60 * 24;
return Math.round(diff / oneDay)
}
/**
* Returns the day of the month for a date as a number between 1 and 31.
* @param obj
* @param expr
*/
function $dayOfMonth (obj, expr) {
let d = computeValue(obj, expr);
return d.getDate()
}
/**
* Returns the day of the week for a date as a number between 1 (Sunday) and 7 (Saturday).
* @param obj
* @param expr
*/
function $dayOfWeek (obj, expr) {
let d = computeValue(obj, expr);
return d.getDay() + 1
}
/**
* Returns the year for a date as a number (e.g. 2014).
* @param obj
* @param expr
*/
function $year (obj, expr) {
let d = computeValue(obj, expr);
return d.getFullYear()
}
/**
* Returns the month for a date as a number between 1 (January) and 12 (December).
* @param obj
* @param expr
*/
function $month (obj, expr) {
let d = computeValue(obj, expr);
return d.getMonth() + 1
}
/**
* Returns the week number for a date as a number between 0
* (the partial week that precedes the first Sunday of the year) and 53 (leap year).
* @param obj
* @param expr
*/
function $week (obj, expr) {
// source: http://stackoverflow.com/a/6117889/1370481
let d = computeValue(obj, expr);
// Copy date so don't modify original
d = new Date(+d);
d.setHours(0, 0, 0);
// Set to nearest Thursday: current date + 4 - current day number
// Make Sunday's day number 7
d.setDate(d.getDate() + 4 - (d.getDay() || 7));
// Get first day of year
let yearStart = new Date(d.getFullYear(), 0, 1);
// Calculate full weeks to nearest Thursday
return Math.floor((((d - yearStart) / 8.64e7) + 1) / 7)
}
/**
* Returns the hour for a date as a number between 0 and 23.
* @param obj
* @param expr
*/
function $hour (obj, expr) {
let d = computeValue(obj, expr);
return d.getUTCHours()
}
/**
* Returns the minute for a date as a number between 0 and 59.
* @param obj
* @param expr
*/
function $minute (obj, expr) {
let d = computeValue(obj, expr);
return d.getMinutes()
}
/**
* Returns the seconds for a date as a number between 0 and 60 (leap seconds).
* @param obj
* @param expr
*/
function $second (obj, expr) {
let d = computeValue(obj, expr);
return d.getSeconds()
}
/**
* Returns the milliseconds of a date as a number between 0 and 999.
* @param obj
* @param expr
*/
function $millisecond (obj, expr) {
let d = computeValue(obj, expr);
return d.getMilliseconds()
}
// used for formatting dates in $dateToString operator
const DATE_SYM_TABLE = {
'%Y': [$year, 4],
'%m': [$month, 2],
'%d': [$dayOfMonth, 2],
'%H': [$hour, 2],
'%M': [$minute, 2],
'%S': [$second, 2],
'%L': [$millisecond, 3],
'%j': [$dayOfYear, 3],
'%w': [$dayOfWeek, 1],
'%U': [$week, 2],
'%%': '%'
};
/**
* Returns the date as a formatted string.
*
* %Y Year (4 digits, zero padded) 0000-9999
* %m Month (2 digits, zero padded) 01-12
* %d Day of Month (2 digits, zero padded) 01-31
* %H Hour (2 digits, zero padded, 24-hour clock) 00-23
* %M Minute (2 digits, zero padded) 00-59
* %S Second (2 digits, zero padded) 00-60
* %L Millisecond (3 digits, zero padded) 000-999
* %j Day of year (3 digits, zero padded) 001-366
* %w Day of week (1-Sunday, 7-Saturday) 1-7
* %U Week of year (2 digits, zero padded) 00-53
* %% Percent Character as a Literal %
*
* @param obj current object
* @param expr operator expression
*/
function $dateToString (obj, expr) {
let fmt = expr['format'];
let date = computeValue(obj, expr['date']);
let matches = fmt.match(/(%%|%Y|%m|%d|%H|%M|%S|%L|%j|%w|%U)/g);
for (let i = 0, len = matches.length; i < len; i++) {
let hdlr = DATE_SYM_TABLE[matches[i]];
let value = hdlr;
if (isArray(hdlr)) {
// reuse date operators
let fn = hdlr[0];
let pad = hdlr[1];
value = padDigits(fn(obj, date), pad);
}
// replace the match with resolved value
fmt = fmt.replace(matches[i], value);
}
return fmt
}
function padDigits (number, digits) {
return new Array(Math.max(digits - String(number).length + 1, 0)).join('0') + number
}
/**
* Return a value without parsing.
* @param obj
* @param expr
*/
function $literal (obj, expr) {
return expr
}
/**
* Returns true if two sets have the same elements.
* @param obj
* @param expr
*/
function $setEquals (obj, expr) {
let args = computeValue(obj, expr);
let xs = unique(args[0]);
let ys = unique(args[1]);
return xs.length === ys.length && xs.length === intersection(xs, ys).length
}
/**
* Returns the common elements of the input sets.
* @param obj
* @param expr
*/
function $setIntersection (obj, expr) {
let args = computeValue(obj, expr);
return intersection(args[0], args[1])
}
/**
* Returns elements of a set that do not appear in a second set.
* @param obj
* @param expr
*/
function $setDifference (obj, expr) {
let args = computeValue(obj, expr);
return args[0].filter(notInArray.bind(null, args[1]))
}
/**
* Returns a set that holds all elements of the input sets.
* @param obj
* @param expr
*/
function $setUnion (obj, expr) {
let args = computeValue(obj, expr);
return union(args[0], args[1])
}
/**
* Returns true if all elements of a set appear in a second set.
* @param obj
* @param expr
*/
function $setIsSubset (obj, expr) {
let args = computeValue(obj, expr);
return intersection(args[0], args[1]).length === args[0].length
}
/**
* Returns true if any elements of a set evaluate to true, and false otherwise.
* @param obj
* @param expr
*/
function $anyElementTrue (obj, expr) {
// mongodb nests the array expression in another
let args = computeValue(obj, expr)[0];
return args.some(truthy)
}
/**
* Returns true if all elements of a set evaluate to true, and false otherwise.
* @param obj
* @param expr
*/
function $allElementsTrue (obj, expr) {
// mongodb nests the array expression in another
let args = computeValue(obj, expr)[0];
return args.every(truthy)
}
/**
* Concatenates two strings.
*
* @param obj
* @param expr
* @returns {string|*}
*/
function $concat (obj, expr) {
let args = computeValue(obj, expr);
// does not allow concatenation with nulls
if ([null, undefined].some(inArray.bind(null, args))) return null
return args.join('')
}
/**
* Searches a string for an occurrence of a substring and returns the UTF-8 code point index of the first occurence.
* If the substring is not found, returns -1.
*
* @param {Object} obj
* @param {*} expr
* @return {*}
*/
function $indexOfBytes (obj, expr) {
let arr = computeValue(obj, expr);
const errorMsg = '$indexOfBytes expression resolves to invalid an argument';
if (isNil(arr[0])) return null
assert(isString(arr[0]) && isString(arr[1]), errorMsg);
let str = arr[0];
let searchStr = arr[1];
let start = arr[2];
let end = arr[3];
let valid = isNil(start) || (isNumber(start) && start >= 0 && Math.round(start) === start);
valid = valid && (isNil(end) || (isNumber(end) && end >= 0 && Math.round(end) === end));
assert(valid, errorMsg);
start = start || 0;
end = end || str.length;
if (start > end) return -1
let index = str.substring(start, end).indexOf(searchStr);
return (index > -1)
? index + start
: index
}
/**
* Splits a string into substrings based on a delimiter.
* If the delimiter is not found within the string, returns an array containing the original string.
*
* @param {Object} obj
* @param {Array} expr
* @return {Array} Returns an array of substrings.
*/
function $split (obj, expr) {
let args = computeValue(obj, expr);
if (isNil(args[0])) return null
assert(args.every(isString), '$split expression must result to array(2) of strings');
return args[0].split(args[1])
}
/**
* Returns the number of UTF-8 encoded bytes in the specified string.
*
* @param {Object} obj
* @param {String} expr
* @return {Number}
*/
function $strLenBytes (obj, expr) {
return ~-encodeURI(computeValue(obj, expr)).split(/%..|./).length
}
/**
* Returns the number of UTF-8 code points in the specified string.
*
* @param {Object} obj
* @param {String} expr
* @return {Number}
*/
function $strLenCP (obj, expr) {
return computeValue(obj, expr).length
}
/**
* Compares two strings and returns an integer that reflects the comparison.
*
* @param obj
* @param expr
* @returns {number}
*/
function $strcasecmp (obj, expr) {
let args = computeValue(obj, expr);
let a = args[0];
let b = args[1];
if (isEqual(a, b) || args.every(isNil)) return 0
assert(args.every(isString), '$strcasecmp must resolve to array(2) of strings');
a = a.toUpperCase();
b = b.toUpperCase();
return (a > b && 1) || (a < b && -1) || 0
}
/**
* Returns a substring of a string, starting at a specified index position and including the specified number of characters.
* The index is zero-based.
*
* @param obj
* @param expr
* @returns {string}
*/
function $substrBytes (obj, expr) {
let args = computeValue(obj, expr);
let s = args[0];
let index = args[1];
let count = args[2];
assert(isString(s) && isNumber(index) && index >= 0 && isNumber(count) && count >= 0, '$substrBytes: invalid arguments');
let buf = utf8Encode(s);
let validIndex = [];
let acc = 0;
for (let i = 0; i < buf.length; i++) {
validIndex.push(acc);
acc += buf[i].length;
}
let begin = validIndex.indexOf(index);
let end = validIndex.indexOf(index + count);
assert(begin > -1 && end > -1, '$substrBytes: invalid range, start or end index is a UTF-8 continuation byte.');
return s.substring(begin, end)
}
/**
* Returns a substring of a string, starting at a specified index position and including the specified number of characters.
* The index is zero-based.
*
* @param obj
* @param expr
* @returns {string}
*/
function $substr (obj, expr) {
let args = computeValue(obj, expr);
let s = args[0];
let index = args[1];
let count = args[2];
if (isString(s)) {
if (index < 0) {
return ''
} else if (count < 0) {
return s.substr(index)
} else {
return s.substr(index, count)
}
}
return ''
}
function $substrCP (obj, expr) {
return $substr(obj, expr)
}
/**
* Converts a string to lowercase.
*
* @param obj
* @param expr
* @returns {string}
*/
function $toLower (obj, expr) {
let value = computeValue(obj, expr);
return isEmpty(value) ? '' : value.toLowerCase()
}
/**
* Converts a string to uppercase.
*
* @param obj
* @param expr
* @returns {string}
*/
function $toUpper (obj, expr) {
let value = computeValue(obj, expr);
return isEmpty(value) ? '' : value.toUpperCase()
}
const UTF8_MASK = [0xC0, 0xE0, 0xF0];
// encodes a unicode code point to a utf8 byte sequence
// https://encoding.spec.whatwg.org/#utf-8
function toUtf8 (n) {
if (n < 0x80) return [n]
let count = ((n < 0x0800) && 1) || ((n < 0x10000) && 2) || 3;
const offset = UTF8_MASK[count - 1];
let utf8 = [(n >> (6 * count)) + offset];
while (count > 0) utf8.push(0x80 | ((n >> (6 * --count)) & 0x3F));
return utf8
}
function utf8Encode(s) {
let buf = [];
for (let i = 0, len = s.length; i < len; i++) {
buf.push(toUtf8(s.codePointAt(i)));
}
return buf
}
/**
* Aggregation framework variable operators
*/
/**
* Defines variables for use within the scope of a sub-expression and returns the result of the sub-expression.
*
* @param obj
* @param expr
* @returns {*}
*/
function $let (obj, expr) {
let varsExpr = expr['vars'];
let inExpr = expr['in'];
// resolve vars
let varsKeys = keys(varsExpr);
each(varsKeys, key => {
let val = computeValue(obj, varsExpr[key]);
let tempKey = '$' + key;
obj[tempKey] = val;
});
return computeValue(obj, inExpr)
}
var expressionOperators = /*#__PURE__*/Object.freeze({
__proto__: null,
$abs: $abs,
$add: $add,
$ceil: $ceil,
$divide: $divide,
$exp: $exp,
$floor: $floor,
$ln: $ln,
$log: $log,
$log10: $log10,
$mod: $mod,
$multiply: $multiply,
$pow: $pow,
$round: $round,
$sqrt: $sqrt,
$subtract: $subtract,
$trunc: $trunc,
$arrayElemAt: $arrayElemAt,
$arrayToObject: $arrayToObject,
$concatArrays: $concatArrays,
$filter: $filter,
$in: $in,
$indexOfArray: $indexOfArray,
$isArray: $isArray,
$map: $map,
$objectToArray: $objectToArray,
$range: $range,
$reduce: $reduce,
$reverseArray: $reverseArray,
$size: $size,
$slice: $slice,
$zip: $zip,
$mergeObjects: $mergeObjects,
$and: $and,
$or: $or,
$not: $not,
$eq: $eq$1,
$ne: $ne$1,
$gt: $gt$1,
$lt: $lt$1,
$gte: $gte$1,
$lte: $lte$1,
$nin: $nin$1,
$cmp: $cmp,
$cond: $cond,
$switch: $switch,
$ifNull: $ifNull,
$dayOfYear: $dayOfYear,
$dayOfMonth: $dayOfMonth,
$dayOfWeek: $dayOfWeek,
$year: $year,
$month: $month,
$week: $week,
$hour: $hour,
$minute: $minute,
$second: $second,
$millisecond: $millisecond,
$dateToString: $dateToString,
$literal: $literal,
$setEquals: $setEquals,
$setIntersection: $setIntersection,
$setDifference: $setDifference,
$setUnion: $setUnion,
$setIsSubset: $setIsSubset,
$anyElementTrue: $anyElementTrue,
$allElementsTrue: $allElementsTrue,
$concat: $concat,
$indexOfBytes: $indexOfBytes,
$split: $split,
$strLenBytes: $strLenBytes,
$strLenCP: $strLenCP,
$strcasecmp: $strcasecmp,
$substrBytes: $substrBytes,
$substr: $substr,
$substrCP: $substrCP,
$toLower: $toLower,
$toUpper: $toUpper,
$let: $let
});
/**
* Returns an array of all values for the selected field among for each document in that group.
*
* @param collection
* @param expr
* @returns {Array|*}
*/
function $push (collection, expr) {
if (isNil(expr)) return collection
return collection.map(obj => computeValue(obj, expr))
}
/**
* Returns an array of all the unique values for the selected field among for each document in that group.
*
* @param collection
* @param expr
* @returns {*}
*/
function $addToSet (collection, expr) {
return unique($push(collection, expr))
}
/**
* Returns an average of all the values in a group.
*
* @param collection
* @param expr
* @returns {number}
*/
function $avg (collection, expr) {
let data = $push(collection, expr).filter(isNumber);
let sum = reduce(data, (acc, n) => acc + n, 0);
return sum / (data.length || 1)
}
/**
* Returns the first value in a group.
*
* @param collection
* @param expr
* @returns {*}
*/
function $first (collection, expr) {
return collection.length > 0 ? computeValue(collection[0], expr) : undefined
}
/**
* Returns the last value in a group.
*
* @param collection
* @param expr
* @returns {*}
*/
function $last (collection, expr) {
return collection.length > 0 ? computeValue(collection[collection.length - 1], expr) : undefined
}
/**
* Returns the highest value in a group.
*
* @param collection
* @param expr
* @returns {*}
*/
function $max (collection, expr) {
return reduce($push(collection, expr), (acc, n) => (isNil(acc) || n > acc) ? n : acc, undefined)
}
/**
* Combines multiple documents into a single document.
*
* @param collection
* @param expr
* @returns {Array|*}
*/
function $mergeObjects$1 (collection, expr) {
return reduce(collection, (memo, o) => Object.assign(memo, computeValue(o, expr)), {})
}
/**
* Returns the lowest value in a group.
*
* @param collection
* @param expr
* @returns {*}
*/
function $min (collection, expr) {
return reduce($push(collection, expr), (acc, n) => (isNil(acc) || n < acc) ? n : acc, undefined)
}
/**
* Returns the population standard deviation of the input values.
*
* @param {Array} collection
* @param {Object} expr
* @return {Number}
*/
function $stdDevPop (collection, expr) {
return stddev($push(collection, expr).filter(isNumber), false)
}
/**
* Returns the sample standard deviation of the input values.
* @param {Array} collection
* @param {Object} expr
* @return {Number|null}
*/
function $stdDevSamp (collection, expr) {
return stddev($push(collection, expr).filter(isNumber), true)
}
/**
* Returns the sum of all the values in a group.
*
* @param collection
* @param expr
* @returns {*}
*/
function $sum (collection, expr) {
if (!isArray(collection)) return 0
// take a short cut if expr is number literal
if (isNumber(expr)) return collection.length * expr
return reduce($push(collection, expr).filter(isNumber), (acc, n) => acc + n, 0)
}
/**
* Group stage Accumulator Operators. https://docs.mongodb.com/manual/reference/operator/aggregation-
*/
var groupOperators = /*#__PURE__*/Object.freeze({
__proto__: null,
$addToSet: $addToSet,
$avg: $avg,
$first: $first,
$last: $last,
$max: $max,
$mergeObjects: $mergeObjects$1,
$min: $min,
$push: $push,
$stdDevPop: $stdDevPop,
$stdDevSamp: $stdDevSamp,
$sum: $sum
});
/**
* Adds new fields to documents.
* Outputs documents that contain all existing fields from the input documents and newly added fields.
*
* @param {Array} collection
* @param {*} expr
* @param {Object} opt Pipeline options
*/
function $addFields (collection, expr, opt) {
let newFields = keys(expr);
if (newFields.length === 0) return collection
return collection.map(obj => {
let newObj = cloneDeep(obj);
each(newFields, (field) => {
let newValue = computeValue(obj, expr[field]);
setValue(newObj, field, newValue);
});
return newObj
})
}
/**
* Alias for $addFields.
*/
const $set = $addFields;
/**
* Categorizes incoming documents into groups, called buckets, based on a specified expression and bucket boundaries.
* https://docs.mongodb.com/manual/reference/operator/aggregation/bucket/
*
* @param {*} collection
* @param {*} expr
* @param {Object} opt Pipeline options
*/
function $bucket (collection, expr, opt) {
let boundaries = expr.boundaries;
let defaultKey = expr['default'];
let lower = boundaries[0]; // inclusive
let upper = boundaries[boundaries.length - 1]; // exclusive
let outputExpr = expr.output || { 'count': { '$sum': 1 } };
assert(boundaries.length > 2, "$bucket 'boundaries' expression must have at least 3 elements");
let boundType = getType(lower);
for (let i = 0, len = boundaries.length - 1; i < len; i++) {
assert(boundType === getType(boundaries[i + 1]), "$bucket 'boundaries' must all be of the same type");
assert(boundaries[i] < boundaries[i + 1], "$bucket 'boundaries' must be sorted in ascending order");
}
!isNil(defaultKey)
&& (getType(expr.default) === getType(lower))
&& assert(lower > expr.default || upper < expr.default, "$bucket 'default' expression must be out of boundaries range");
let grouped = {};
each(boundaries, (k) => grouped[k] = []);
// add default key if provided
if (!isNil(defaultKey)) grouped[defaultKey] = [];
let iterator = false;
return Lazy(() => {
if (!iterator) {
collection.each(obj => {
let key = computeValue(obj, expr.groupBy);
if (isNil(key) || key < lower || key >= upper) {
assert(!isNil(defaultKey), '$bucket require a default for out of range values');
grouped[defaultKey].push(obj);
} else {
assert(key >= lower && key < upper, "$bucket 'groupBy' expression must resolve to a value in range of boundaries");
let index = findInsertIndex(boundaries, key);
let boundKey = boundaries[Math.max(0, index - 1)];
grouped[boundKey].push(obj);
}
});
// upper bound is exclusive so we remove it
boundaries.pop();
if (!isNil(defaultKey)) boundaries.push(defaultKey);
iterator = Lazy(boundaries).map(key => {
let acc = accumulate(grouped[key], null, outputExpr);
return Object.assign(acc, { '_id': key })
});
}
return iterator.next()
})
}
/**
* Categorizes incoming documents into a specific number of groups, called buckets,
* based on a specified expression. Bucket boundaries are automatically determined
* in an attempt to evenly distribute the documents into the specified number of buckets.
* https://docs.mongodb.com/manual/reference/operator/aggregation/bucketAuto/
*
* @param {*} collection
* @param {*} expr
* @param {*} opt Pipeline options
*/
function $bucketAuto (collection, expr, opt) {
let outputExpr = expr.output || { 'count': { '$sum': 1 } };
let groupByExpr = expr.groupBy;
let bucketCount = expr.buckets;
assert(bucketCount > 0, "The $bucketAuto 'buckets' field must be greater than 0, but found: " + bucketCount);
return collection.transform(coll => {
let approxBucketSize = Math.max(1, Math.round(coll.length / bucketCount));
let computeValueOptimized = memoize(computeValue);
let grouped = {};
let remaining = [];
let sorted = sortBy(coll, o => {
let key = computeValueOptimized(o, groupByExpr);
if (isNil(key)) {
remaining.push(o);
} else {
grouped[key] || (grouped[key] = []);
grouped[key].push(o);
}
return key
});
const ID_KEY = idKey();
let result = [];
let index = 0; // counter for sorted collection
for (let i = 0, len = sorted.length; i < bucketCount && index < len; i++) {
let boundaries = {};
let bucketItems = [];
for (let j = 0; j < approxBucketSize && index < len; j++) {
let key = computeValueOptimized(sorted[index], groupByExpr);
if (isNil(key)) key = null;
// populate current bucket with all values for current key
into(bucketItems, isNil(key) ? remaining : grouped[key]);
// increase sort index by number of items added
index += (isNil(key) ? remaining.length : grouped[key].length);
// set the min key boundary if not already present
if (!has(boundaries, 'min')) boundaries.min = key;
if (result.length > 0) {
let lastBucket = result[result.length - 1];
lastBucket[ID_KEY].max = boundaries.min;
}
}
// if is last bucket add remaining items
if (i == bucketCount - 1) {
into(bucketItems, sorted.slice(index));
}
result.push(Object.assign(accumulate(bucketItems, null, outputExpr), { '_id': boundaries }));
}
if (result.length > 0) {
result[result.length - 1][ID_KEY].max = computeValueOptimized(sorted[sorted.length - 1], groupByExpr);
}
return result
})
}
/**
* Returns a document that contains a count of the number of documents input to the stage.
*
* @param {Array} collection
* @param {String} expr
* @param {Object} opt Pipeline options
* @return {Object}
*/
function $count (collection, expr, opt) {
assert(
isString(expr) && expr.trim() !== '' && expr.indexOf('.') === -1 && expr.trim()[0] !== '$',
'Invalid expression value for $count'
);
return Lazy(() => {
let o = {};
o[expr] = collection.size();
return { value: o, done: false }
}).first()
}
/**
* Processes multiple aggregation pipelines within a single stage on the same set of input documents.
* Enables the creation of multi-faceted aggregations capable of characterizing data across multiple dimensions, or facets, in a single stage.
*/
function $facet (collection, expr, opt) {
return collection.transform(array => {
return [ objectMap(expr, pipeline => aggregate(array, pipeline)) ]
})
}
/**
* Groups documents together for the purpose of calculating aggregate values based on a collection of documents.
*
* @param collection
* @param expr
* @param opt Pipeline options
* @returns {Array}
*/
function $group (collection, expr, opt) {
// lookup key for grouping
const ID_KEY = idKey();
let id = expr[ID_KEY];
return collection.transform(coll => {
let partitions = groupBy(coll, obj => computeValue(obj, id, id));
// remove the group key
expr = clone(expr);
delete expr[ID_KEY];
let i = -1;
let size = partitions.keys.length;
return () => {
if (++i === size) return { done: true }
let value = partitions.keys[i];
let obj = {};
// exclude undefined key value
if (value !== undefined) {
obj[ID_KEY] = value;
}
// compute remaining keys in expression
each(expr, (val, key) => {
obj[key] = accumulate(partitions.groups[i], key, val);
});
return { value: obj, done: false }
}
})
}
/**
* Restricts the number of documents in an aggregation pipeline.
*
* @param collection
* @param value
* @param opt
* @returns {Object|*}
*/
function $limit (collection, value, opt) {
return collection.take(value)
}
/**
* Performs a left outer join to another collection in the same database to filter in documents from the “joined” collection for processing.
*
* @param collection
* @param expr
* @param opt
*/
function $lookup (collection, expr, opt) {
let joinColl = expr.from;
let localField = expr.localField;
let foreignField = expr.foreignField;
let asField = expr.as;
assert(isArray(joinColl) && isString(foreignField) && isString(localField) && isString(asField), '$lookup: invalid argument');
let hash = {};
each(joinColl, obj => {
let k = hashCode(resolve(obj, foreignField));
hash[k] = hash[k] || [];
hash[k].push(obj);
});
return collection.map(obj => {
let k = hashCode(resolve(obj, localField));
let newObj = clone(obj);
newObj[asField] = hash[k] || [];
return newObj
})
}
/**
* Filters the document stream, and only allows matching documents to pass into the next pipeline stage.
* $match uses standard MongoDB queries.
*
* @param collection
* @param expr
* @param opt
* @returns {Array|*}
*/
function $match (collection, expr, opt) {
let q = new Query(expr);
return collection.filter(o => q.test(o))
}
/**
* Takes the documents returned by the aggregation pipeline and writes them to a specified collection.
*
* Unlike the $out operator in MongoDB, this operator can appear in any position in the pipeline and is
* useful for collecting intermediate results of an aggregation operation.
*
* @param collection
* @param expr
* @param opt
* @returns {*}
*/
function $out (collection, expr, opt) {
assert(isArray(expr), '$out expression must be an array');
return collection.map(o => {
expr.push(o);
return o // passthrough
})
}
/**
* Reshapes a document stream.
* $project can rename, add, or remove fields as well as create computed values and sub-documents.
*
* @param collection
* @param expr
* @param opt
* @returns {Array}
*/
function $project(collection, expr, opt) {
if (isEmpty(expr)) return collection
// result collection
let expressionKeys = keys(expr);
let idOnlyExcludedExpression = false;
const ID_KEY = idKey();
// validate inclusion and exclusion
validateExpression(expr);
if (inArray(expressionKeys, ID_KEY)) {
let id = expr[ID_KEY];
if (id === 0 || id === false) {
expressionKeys = expressionKeys.filter(notInArray.bind(null, [ID_KEY]));
assert(notInArray(expressionKeys, ID_KEY), 'Must not contain collections id key');
idOnlyExcludedExpression = isEmpty(expressionKeys);
}
} else {
// if not specified the add the ID field
expressionKeys.push(ID_KEY);
}
return collection.map(obj => processObject(obj, expr, expressionKeys, idOnlyExcludedExpression))
}
/**
* Process the expression value for $project operators
*
* @param {Object} obj The object to use as context
* @param {Object} expr The experssion object of $project operator
* @param {Array} expressionKeys The key in the 'expr' object
* @param {Boolean} idOnlyExcludedExpression Boolean value indicating whether only the ID key is excluded
*/
function processObject(obj, expr, expressionKeys, idOnlyExcludedExpression) {
const ID_KEY = idKey();
let newObj = {};
let foundSlice = false;
let foundExclusion = false;
let dropKeys = [];
if (idOnlyExcludedExpression) {
dropKeys.push(ID_KEY);
}
expressionKeys.forEach(key => {
// final computed value of the key
let value;
// expression to associate with key
let subExpr = expr[key];
if (key !== ID_KEY && inArray([0, false], subExpr)) {
foundExclusion = true;
}
if (key === ID_KEY && isEmpty(subExpr)) {
// tiny optimization here to skip over id
value = obj[key];
} else if (isString(subExpr)) {
value = computeValue(obj, subExpr, key);
} else if (inArray([1, true], subExpr)) ; else if (isArray(subExpr)) {
value = subExpr.map(v => {
let r = computeValue(obj, v);
if (isNil(r)) return null
return r
});
} else if (isObject(subExpr)) {
let subExprKeys = keys(subExpr);
let operator = subExprKeys.length > 1 ? false : subExprKeys[0];
if (inArray(ops(OP_PROJECTION), operator)) {
const projectionOperators = OPERATORS[OP_PROJECTION];
// apply the projection operator on the operator expression for the key
if (operator === '$slice') {
// $slice is handled differently for aggregation and projection operations
if (ensureArray(subExpr[operator]).every(isNumber)) {
// $slice for projection operation
value = projectionOperators[operator](obj, subExpr[operator], key);
foundSlice = true;
} else {
// $slice for aggregation operation
value = computeValue(obj, subExpr, key);
}
} else {
value = projectionOperators[operator](obj, subExpr[operator], key);
}
} else {
// compute the value for the sub expression for the key
if (has(obj, key)) {
validateExpression(subExpr);
let nestedObj = obj[key];
value = isArray(nestedObj) ?
nestedObj.map(o => processObject(o, subExpr, subExprKeys, false)) :
processObject(nestedObj, subExpr, subExprKeys, false);
} else {
value = computeValue(obj, subExpr, key);
}
}
} else {
dropKeys.push(key);
return
}
// get value with object graph
let objPathValue = resolveObj(obj, key, {
preserveMissingValues: true
});
// add the value at the path
if (objPathValue !== undefined) {
merge(newObj, objPathValue, {
flatten: true
});
}
// if computed add/or remove accordingly
if (notInArray([0, 1, false, true], subExpr)) {
if (value === undefined) {
removeValue(newObj, key);
} else {
setValue(newObj, key, value);
}
}
});
// filter out all missing values preserved to support correct merging
filterMissing(newObj);
// if projection included $slice operator
// Also if exclusion fields are found or we want to exclude only the id field
// include keys that were not explicitly excluded
if (foundSlice || foundExclusion || idOnlyExcludedExpression) {
newObj = Object.assign({}, obj, newObj);
if (dropKeys.length > 0) {
newObj = cloneDeep(newObj);
each(dropKeys, k => removeValue(newObj, k));
}
}
return newObj
}
/**
* Validate inclusion and exclusion values in expression
*
* @param {Object} expr The expression given for the projection
*/
function validateExpression(expr) {
const ID_KEY = idKey();
let check = [false, false];
each(expr, (v, k) => {
if (k === ID_KEY) return
if (v === 0 || v === false) {
check[0] = true;
} else if (v === 1 || v === true) {
check[1] = true;
}
assert(!(check[0] && check[1]), 'Projection cannot have a mix of inclusion and exclusion.');
});
}
/**
* Restricts the contents of the documents based on information stored in the documents themselves.
*
* https://docs.mongodb.com/manual/reference/operator/aggregation/redact/
*/
function $redact (collection, expr, opt) {
return collection.map(obj => redactObj(cloneDeep(obj), expr))
}
/**
* Replaces a document with the specified embedded document or new one.
* The replacement document can be any valid expression that resolves to a document.
*
* https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/
*
* @param {Array} collection
* @param {Object} expr
* @param {Object} opt
* @return {*}
*/
function $replaceRoot (collection, expr, opt) {
return collection.map(obj => {
obj = computeValue(obj, expr.newRoot);
assert(isObject(obj), '$replaceRoot expression must return an object');
return obj
})
}
/**
* Randomly selects the specified number of documents from its input.
* https://docs.mongodb.com/manual/reference/operator/aggregation/sample/
*
* @param {Array} collection
* @param {Object} expr
* @param {Object} opt
* @return {*}
*/
function $sample (collection, expr, opt) {
let size = expr.size;
assert(isNumber(size), '$sample size must be a positive integer');
return collection.transform(xs => {
let len = xs.length;
let i = -1;
return () => {
if (++i === size) return { done: true }
let n = Math.floor(Math.random() * len);
return { value: xs[n], done: false }
}
})
}
/**
* Skips over a specified number of documents from the pipeline and returns the rest.
*
* @param collection
* @param value
* @param {Object} opt
* @returns {*}
*/
function $skip (collection, value, opt) {
return collection.drop(value)
}
/**
* Takes all input documents and returns them in a stream of sorted documents.
*
* @param collection
* @param sortKeys
* @param {Object} opt
* @returns {*}
*/
function $sort (collection, sortKeys, opt) {
if (isEmpty(sortKeys) || !isObject(sortKeys)) return collection
opt = opt || {};
let cmp = compare;
let collationSpec = opt['collation'];
// use collation comparator if provided
if (isObject(collationSpec) && isString(collationSpec.locale)) {
cmp = collationComparator(collationSpec);
}
return collection.transform(coll => {
let modifiers = keys(sortKeys);
each(modifiers.reverse(), key => {
let grouped = groupBy(coll, obj => resolve(obj, key));
let sortedIndex = {};
let indexKeys = sortBy(grouped.keys, (k, i) => {
sortedIndex[k] = i;
return k
}, cmp);
if (sortKeys[key] === -1) indexKeys.reverse();
coll = [];
each(indexKeys, k => into(coll, grouped.groups[sortedIndex[k]]));
});
return coll
})
}
// MongoDB collation strength to JS localeCompare sensitivity mapping.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare
const COLLATION_STRENGTH = {
// Only strings that differ in base letters compare as unequal. Examples: a ≠ b, a = á, a = A.
1: 'base',
// Only strings that differ in base letters or accents and other diacritic marks compare as unequal.
// Examples: a ≠ b, a ≠ á, a = A.
2: 'accent',
// Strings that differ in base letters, accents and other diacritic marks, or case compare as unequal.
// Other differences may also be taken into consideration. Examples: a ≠ b, a ≠ á, a ≠ A
3: 'variant',
// case - Only strings that differ in base letters or case compare as unequal. Examples: a ≠ b, a = á, a ≠ A.
};
/**
* Creates a comparator function for the given collation spec. See https://docs.mongodb.com/manual/reference/collation/
*
* @param spec {Object} The MongoDB collation spec.
* {
* locale: <string>,
* caseLevel: <boolean>,
* caseFirst: <string>,
* strength: <int>,
* numericOrdering: <boolean>,
* alternate: <string>,
* maxVariable: <string>, // unsupported
* backwards: <boolean> // unsupported
* }
*/
function collationComparator(spec) {
let localeOpt = {
sensitivity: COLLATION_STRENGTH[spec.strength || 3],
caseFirst: spec.caseFirst === 'off' ? 'false' : (spec.caseFirst || 'false'),
numeric: spec.numericOrdering || false,
ignorePunctuation: spec.alternate === 'shifted'
};
// when caseLevel is true for strength 1:base and 2:accent, bump sensitivity to the nearest that supports case comparison
if ((spec.caseLevel || false) === true) {
if (localeOpt.sensitivity === 'base') localeOpt.sensitivity = 'case';
if (localeOpt.sensitivity === 'accent') localeOpt.sensitivity = 'variant';
}
const collator = new Intl.Collator(spec.locale, localeOpt);
return (a, b) => {
// non strings
if (!isString(a) || !isString(b)) return compare(a, b)
// only for strings
let i = collator.compare(a, b);
if (i < 0) return -1
if (i > 0) return 1
return 0
}
}
/**
* Groups incoming documents based on the value of a specified expression,
* then computes the count of documents in each distinct group.
*
* https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/
*
* @param {Array} collection
* @param {Object} expr
* @param {Object} opt
* @return {*}
*/
function $sortByCount (collection, expr, opt) {
let newExpr = { count: { $sum: 1 } };
newExpr[idKey()] = expr;
return $sort(
$group(collection, newExpr),
{ count: -1 },
opt
)
}
/**
* Takes an array of documents and returns them as a stream of documents.
*
* @param collection
* @param expr
* @param {Object} opt
* @returns {Array}
*/
function $unwind(collection, expr, opt) {
if (isString(expr)) {
expr = { path: expr };
}
let field = expr.path.substr(1);
let includeArrayIndex = expr.includeArrayIndex || false;
let preserveNullAndEmptyArrays = expr.preserveNullAndEmptyArrays || false;
let format = (o, i) => {
if (includeArrayIndex !== false) o[includeArrayIndex] = i;
return o
};
let value;
return Lazy(() => {
while (true) {
// take from lazy sequence if available
if (Lazy.isIterator(value)) {
let tmp = value.next();
if (!tmp.done) return tmp
}
// fetch next object
let obj = collection.next();
if (obj.done) return obj
// unwrap value
obj = obj.value;
// get the value of the field to unwind
value = resolve(obj, field);
// throw error if value is not an array???
if (isArray(value)) {
if (value.length === 0 && preserveNullAndEmptyArrays === true) {
value = null; // reset unwind value
let tmp = cloneDeep(obj);
removeValue(tmp, field);
return { value: format(tmp, null), done: false }
} else {
// construct a lazy sequence for elements per value
value = Lazy(value).map((item, i) => {
let tmp = cloneDeep(obj);
setValue(tmp, field, item);
return format(tmp, i)
});
}
} else if (!isEmpty(value) || preserveNullAndEmptyArrays === true) {
let tmp = cloneDeep(obj);
return { value: format(tmp, null), done: false }
}
}
})
}
/**
* Pipeline Aggregation Stages. https://docs.mongodb.com/manual/reference/operator/aggregation-
*/
var pipelineOperators = /*#__PURE__*/Object.freeze({
__proto__: null,
$addFields: $addFields,
$set: $set,
$bucket: $bucket,
$bucketAuto: $bucketAuto,
$count: $count,
$facet: $facet,
$group: $group,
$limit: $limit,
$lookup: $lookup,
$match: $match,
$out: $out,
$project: $project,
$redact: $redact,
$replaceRoot: $replaceRoot,
$sample: $sample,
$skip: $skip,
$sort: $sort,
$sortByCount: $sortByCount,
$unwind: $unwind
});
/**
* Projection Operators. https://docs.mongodb.com/manual/reference/operator/projection/
*/
/**
* Projects the first element in an array that matches the query condition.
*
* @param obj
* @param field
* @param expr
*/
function $ (obj, expr, field) {
err('$ not implemented');
}
/**
* Projects only the first element from an array that matches the specified $elemMatch condition.
*
* @param obj
* @param field
* @param expr
* @returns {*}
*/
function $elemMatch$1 (obj, expr, field) {
let arr = resolve(obj, field);
let query = new Query(expr);
assert(isArray(arr), '$elemMatch: invalid argument');
for (let i = 0; i < arr.length; i++) {
if (query.test(arr[i])) return [arr[i]]
}
return undefined
}
/**
* Limits the number of elements projected from an array. Supports skip and limit slices.
*
* @param obj
* @param field
* @param expr
*/
function $slice$1 (obj, expr, field) {
let xs = resolve(obj, field);
if (!isArray(xs)) return xs
if (isArray(expr)) {
return slice(xs, expr[0], expr[1])
} else {
assert(isNumber(expr), '$slice: invalid arguments for projection');
return slice(xs, expr)
}
}
var projectionOperators = /*#__PURE__*/Object.freeze({
__proto__: null,
$: $,
$elemMatch: $elemMatch$1,
$slice: $slice$1
});
// Query and Projection Operators. https://docs.mongodb.com/manual/reference/operator/query/
function createQueryOperator(pred) {
return (selector, value) => obj => {
// value of field must be fully resolved.
let lhs = resolve(obj, selector, { meta: true });
lhs = unwrap(lhs.result, lhs.depth);
return pred(lhs, value)
}
}
const $all$1 = createQueryOperator($all);
const $elemMatch$2 = createQueryOperator($elemMatch);
const $eq$2 = createQueryOperator($eq);
const $exists$1 = createQueryOperator($exists);
const $gt$2 = createQueryOperator($gt);
const $gte$2 = createQueryOperator($gte);
const $in$2 = createQueryOperator($in$1);
const $lt$2 = createQueryOperator($lt);
const $lte$2 = createQueryOperator($lte);
const $mod$2 = createQueryOperator($mod$1);
const $ne$2 = createQueryOperator($ne);
const $nin$2 = createQueryOperator($nin);
const $regex$1 = createQueryOperator($regex);
const $size$2 = createQueryOperator($size$1);
const $type$1 = createQueryOperator($type);
/**
* Joins query clauses with a logical AND returns all documents that match the conditions of both clauses.
*
* @param selector
* @param value
* @returns {Function}
*/
function $and$1 (selector, value) {
assert(isArray(value), 'Invalid expression: $and expects value to be an Array');
let queries = [];
each(value, (expr) => queries.push(new Query(expr)));
return obj => {
for (let i = 0; i < queries.length; i++) {
if (!queries[i].test(obj)) {
return false
}
}
return true
}
}
/**
* Joins query clauses with a logical OR returns all documents that match the conditions of either clause.
*
* @param selector
* @param value
* @returns {Function}
*/
function $or$1 (selector, value) {
assert(isArray(value),'Invalid expression. $or expects value to be an Array');
let queries = [];
each(value, expr => queries.push(new Query(expr)));
return obj => {
for (let i = 0; i < queries.length; i++) {
if (queries[i].test(obj)) {
return true
}
}
return false
}
}
/**
* Joins query clauses with a logical NOR returns all documents that fail to match both clauses.
*
* @param selector
* @param value
* @returns {Function}
*/
function $nor (selector, value) {
assert(isArray(value),'Invalid expression. $nor expects value to be an Array');
let f = $or$1('$or', value);
return obj => !f(obj)
}
/**
* Inverts the effect of a query expression and returns documents that do not match the query expression.
*
* @param selector
* @param value
* @returns {Function}
*/
function $not$1 (selector, value) {
let criteria = {};
criteria[selector] = normalize(value);
let query = new Query(criteria);
return obj => !query.test(obj)
}
/**
* Matches documents that satisfy a JavaScript expression.
*
* @param selector
* @param value
* @returns {Function}
*/
function $where (selector, value) {
if (!isFunction(value)) {
value = new Function('return ' + value + ';');
}
return obj => value.call(obj) === true
}
/**
* Allows the use of aggregation expressions within the query language.
*
* @param selector
* @param value
* @returns {Function}
*/
function $expr (selector, value) {
return obj => computeValue(obj, value)
}
var queryOperators = /*#__PURE__*/Object.freeze({
__proto__: null,
$all: $all$1,
$elemMatch: $elemMatch$2,
$eq: $eq$2,
$exists: $exists$1,
$gt: $gt$2,
$gte: $gte$2,
$in: $in$2,
$lt: $lt$2,
$lte: $lte$2,
$mod: $mod$2,
$ne: $ne$2,
$nin: $nin$2,
$regex: $regex$1,
$size: $size$2,
$type: $type$1,
$and: $and$1,
$or: $or$1,
$nor: $nor,
$not: $not$1,
$where: $where,
$expr: $expr
});
// operator definitions
const OPERATORS = {};
OPERATORS[OP_EXPRESSION] = {};
OPERATORS[OP_GROUP] = {};
OPERATORS[OP_PIPELINE] = {};
OPERATORS[OP_PROJECTION] = {};
OPERATORS[OP_QUERY] = {};
const SYSTEM_OPERATORS = [
[OP_EXPRESSION, expressionOperators],
[OP_GROUP, groupOperators],
[OP_PIPELINE, pipelineOperators],
[OP_PROJECTION, projectionOperators],
[OP_QUERY, queryOperators]
];
/**
* Enables the default operators of the system
*/
function enableSystemOperators() {
each(SYSTEM_OPERATORS, arr => {
let [cls, values] = arr;
Object.assign(OPERATORS[cls], values);
});
}
/**
* Add new operators
*
* @param opClass the operator class to extend
* @param fn a function returning an object of new operators
*/
function addOperators (opClass, fn) {
const newOperators = fn(_internal());
// ensure correct type specified
assert(has(OPERATORS, opClass), `Invalid operator class ${opClass}`);
let operators = OPERATORS[opClass];
// check for existing operators
each(newOperators, (_, op) => {
assert(/^\$[a-zA-Z0-9_]*$/.test(op), `Invalid operator name ${op}`);
assert(!has(operators, op), `${op} already exists for '${opClass}' operators`);
});
let wrapped = {};
switch (opClass) {
case OP_QUERY:
each(newOperators, (fn, op) => {
fn = fn.bind(newOperators);
wrapped[op] = (selector, value) => obj => {
// value of field must be fully resolved.
let lhs = resolve(obj, selector);
let result = fn(selector, lhs, value);
assert(isBoolean(result), `${op} must return a boolean`);
return result
};
});
break
case OP_PROJECTION:
each(newOperators, (fn, op) => {
fn = fn.bind(newOperators);
wrapped[op] = (obj, expr, selector) => {
let lhs = resolve(obj, selector);
return fn(selector, lhs, expr)
};
});
break
default:
each(newOperators, (fn, op) => {
wrapped[op] = (...args) => fn.apply(newOperators, args);
});
}
// toss the operator salad :)
Object.assign(OPERATORS[opClass], wrapped);
}
/**
* Mixin for Collection types that provide a method `toJSON() -> Array[Object]`
*/
const CollectionMixin = {
/**
* Runs a query and returns a cursor to the result
* @param criteria
* @param projection
* @returns {Cursor}
*/
query (criteria, projection) {
return new Query(criteria).find(this.toJSON(), projection)
},
/**
* Runs the given aggregation operators on this collection
* @params pipeline
* @returns {Array}
*/
aggregate (pipeline) {
return new Aggregator(pipeline).run(this.toJSON())
}
};
enableSystemOperators();
const VERSION = '2.5.3';
// mingo!
var index = {
_internal,
Aggregator,
CollectionMixin,
Cursor,
Lazy,
OP_EXPRESSION,
OP_GROUP,
OP_PIPELINE,
OP_PROJECTION,
OP_QUERY,
Query,
VERSION,
addOperators,
aggregate,
find,
remove,
setup
};
export default index;