UNPKG

mingo

Version:

MongoDB query language for in-memory objects

675 lines (674 loc) 17.9 kB
class MingoError extends Error { } const MAX_INT = 2147483647; const MIN_INT = -2147483648; const MAX_LONG = Number.MAX_SAFE_INTEGER; const MIN_LONG = Number.MIN_SAFE_INTEGER; const MISSING = Symbol("missing"); const CYCLE_FOUND_ERROR = Object.freeze( new Error("mingo: cycle detected while processing object/array") ); const OBJECT_TAG = "[object Object]"; const OBJECT_TYPE_RE = /^\[object ([a-zA-Z0-9]+)\]$/; const DEFAULT_HASH_FUNCTION = (value) => { const s = stringify(value); let hash = 0; let i = s.length; while (i) hash = (hash << 5) - hash ^ s.charCodeAt(--i); return hash >>> 0; }; const JS_SIMPLE_TYPES = /* @__PURE__ */ new Set([ "null", "undefined", "boolean", "number", "string", "date", "regexp" ]); const SORT_ORDER_BY_TYPE = { null: 0, undefined: 0, number: 1, string: 2, object: 3, array: 4, boolean: 5, date: 6, regexp: 7, function: 8 }; const compare = (a, b) => { if (a === MISSING) a = void 0; if (b === MISSING) b = void 0; const [u, v] = [a, b].map( (n) => SORT_ORDER_BY_TYPE[getType(n).toLowerCase()] ); if (u !== v) return u - v; if (u === 1 || u === 2 || u === 6) { if (a < b) return -1; if (a > b) return 1; return 0; } if (isEqual(a, b)) return 0; if (a < b) return -1; if (a > b) return 1; return 0; }; function assert(condition, message) { if (!condition) throw new MingoError(message); } const getType = (v) => OBJECT_TYPE_RE.exec(Object.prototype.toString.call(v))[1]; const isBoolean = (v) => typeof v === "boolean"; const isString = (v) => typeof v === "string"; const isSymbol = (v) => typeof v === "symbol"; const isNumber = (v) => !isNaN(v) && typeof v === "number"; const isBigInt = (v) => !isNaN(v) && typeof v === "bigint"; const isNotNaN = (v) => !(isNaN(v) && typeof v === "number"); const isArray = Array.isArray; const isObject = (v) => { if (!v) return false; const proto = Object.getPrototypeOf(v); return (proto === Object.prototype || proto === null) && OBJECT_TAG === Object.prototype.toString.call(v); }; const isObjectLike = (v) => v === Object(v); const isDate = (v) => v instanceof Date; const isRegExp = (v) => v instanceof RegExp; const isFunction = (v) => typeof v === "function"; const isNil = (v) => v === null || v === void 0; const inArray = (arr, item) => arr.includes(item); const notInArray = (arr, item) => !inArray(arr, item); const truthy = (arg, strict = true) => !!arg || strict && arg === ""; const isEmpty = (x) => isNil(x) || isString(x) && !x || x instanceof Array && x.length === 0 || isObject(x) && Object.keys(x).length === 0; const isMissing = (v) => v === MISSING; const ensureArray = (x) => x instanceof Array ? x : [x]; const has = (obj, prop) => !!obj && Object.prototype.hasOwnProperty.call(obj, prop); const isTypedArray = (v) => typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(v); const INSTANCE_CLONE = [isDate, isRegExp, isTypedArray]; const cloneInternal = (val, refs) => { if (isNil(val)) return val; if (refs.has(val)) throw CYCLE_FOUND_ERROR; const ctor = val.constructor; if (INSTANCE_CLONE.some((f) => f(val))) return new ctor(val); try { refs.add(val); if (isArray(val)) return val.map((v) => cloneInternal(v, refs)); if (isObject(val)) { const res = {}; for (const k in val) res[k] = cloneInternal(val[k], refs); return res; } } finally { refs.delete(val); } return val; }; const cloneDeep = (obj) => cloneInternal(obj, /* @__PURE__ */ new Set()); const mergeable = (left, right) => isObject(left) && isObject(right) || isArray(left) && isArray(right); function merge(target, obj, options) { options = options || { flatten: false }; if (isMissing(target) || isNil(target)) return obj; if (isMissing(obj) || isNil(obj)) return target; if (!mergeable(target, obj)) { if (options.skipValidation) return obj || target; throw Error("mismatched types. must both be array or object"); } options.skipValidation = true; if (isArray(target)) { const result = target; const input = obj; if (options.flatten) { let i = 0; let j = 0; while (i < result.length && j < input.length) { result[i] = merge(result[i++], input[j++], options); } while (j < input.length) { result.push(obj[j++]); } } else { into(result, input); } } else { for (const k in obj) { target[k] = merge( target[k], obj[k], options ); } } return target; } function buildHashIndex(arr, hashFunction = DEFAULT_HASH_FUNCTION) { const map = /* @__PURE__ */ new Map(); arr.forEach((o, i) => { const h = hashCode(o, hashFunction); if (map.has(h)) { if (!map.get(h).some((j) => isEqual(arr[j], o))) { map.get(h).push(i); } } else { map.set(h, [i]); } }); return map; } function intersection(input, hashFunction = DEFAULT_HASH_FUNCTION) { if (input.some((arr) => arr.length == 0)) return []; if (input.length === 1) return Array.from(input); const sortedIndex = sortBy( input.map((a, i) => [i, a.length]), (a) => a[1] ); const smallest = input[sortedIndex[0][0]]; const map = buildHashIndex(smallest, hashFunction); const rmap = /* @__PURE__ */ new Map(); const results = new Array(); map.forEach((v, k) => { const lhs = v.map((j) => smallest[j]); const res = lhs.map((_) => 0); const stable = lhs.map((_) => [sortedIndex[0][0], 0]); let found = false; for (let i = 1; i < input.length; i++) { const [currIndex, _] = sortedIndex[i]; const arr = input[currIndex]; if (!rmap.has(i)) rmap.set(i, buildHashIndex(arr)); if (rmap.get(i).has(k)) { const rhs = rmap.get(i).get(k).map((j) => arr[j]); found = lhs.map( (s, n) => rhs.some((t, m) => { const p = res[n]; if (isEqual(s, t)) { res[n]++; if (currIndex < stable[n][0]) { stable[n] = [currIndex, rmap.get(i).get(k)[m]]; } } return p < res[n]; }) ).some(Boolean); } if (!found) return; } if (found) { into( results, res.map((n, i) => { return n === input.length - 1 ? [lhs[i], stable[i]] : MISSING; }).filter((n) => n !== MISSING) ); } }); return results.sort((a, b) => { const [_i, [u, m]] = a; const [_j, [v, n]] = b; const r = compare(u, v); if (r !== 0) return r; return compare(m, n); }).map((v) => v[0]); } function flatten(xs, depth = 0) { const arr = new Array(); function flatten2(ys, n) { for (let i = 0, len = ys.length; i < len; i++) { if (isArray(ys[i]) && (n > 0 || n < 0)) { flatten2(ys[i], Math.max(-1, n - 1)); } else { arr.push(ys[i]); } } } flatten2(xs, depth); return arr; } const getMembersOf = (value) => { let [proto, names] = [ Object.getPrototypeOf(value), Object.getOwnPropertyNames(value) ]; let activeProto = proto; while (!names.length && proto !== Object.prototype && proto !== Array.prototype) { activeProto = proto; names = Object.getOwnPropertyNames(proto); proto = Object.getPrototypeOf(proto); } const o = {}; names.forEach((k) => o[k] = value[k]); return [o, activeProto]; }; function isEqual(a, b) { if (a === b || Object.is(a, b)) return true; const ctor = !!a && a.constructor || a; if (a === null || b === null || a === void 0 || b === void 0 || ctor !== b.constructor || ctor === Function) { return false; } if (ctor === Array || ctor === Object) { const aKeys = Object.keys(a); const bKeys = Object.keys(b); if (aKeys.length !== bKeys.length) return false; if ((/* @__PURE__ */ new Set([...aKeys, ...bKeys])).size != aKeys.length) return false; for (const k of aKeys) if (!isEqual(a[k], b[k])) return false; return true; } const proto = Object.getPrototypeOf(a); const cmp = isTypedArray(a) || proto !== Object.prototype && proto !== Array.prototype && Object.prototype.hasOwnProperty.call(proto, "toString"); return cmp && a.toString() === b.toString(); } function unique(input, hashFunction = DEFAULT_HASH_FUNCTION) { const result = input.map((_) => MISSING); buildHashIndex(input, hashFunction).forEach((v, _) => { v.forEach((i) => result[i] = input[i]); }); return result.filter((v) => v !== MISSING); } const toString = (v, cycle) => { if (v === null) return "null"; if (v === void 0) return "undefined"; const ctor = v.constructor; switch (ctor) { case RegExp: case Number: case Boolean: case Function: case Symbol: return v.toString(); case String: return JSON.stringify(v); case Date: return v.toISOString(); } if (isTypedArray(v)) return ctor.name + "[" + v.toString() + "]"; if (cycle.has(v)) throw CYCLE_FOUND_ERROR; try { cycle.add(v); if (isArray(v)) { return "[" + v.map((s) => toString(s, cycle)).join(",") + "]"; } if (ctor === Object) { return "{" + Object.keys(v).sort().map((k) => k + ":" + toString(v[k], cycle)).join(",") + "}"; } const proto = Object.getPrototypeOf(v); if (proto !== Object.prototype && proto !== Array.prototype && Object.prototype.hasOwnProperty.call(proto, "toString")) { return ctor.name + "(" + JSON.stringify(v.toString()) + ")"; } const [members, _] = getMembersOf(v); return ctor.name + toString(members, cycle); } finally { cycle.delete(v); } }; const stringify = (value) => toString(value, /* @__PURE__ */ new Set()); function hashCode(value, hashFunction) { hashFunction = hashFunction || DEFAULT_HASH_FUNCTION; if (isNil(value)) return null; return hashFunction(value).toString(); } function sortBy(collection, keyFn, comparator = compare) { if (isEmpty(collection)) return collection; const sorted = new Array(); const result = new Array(); for (let i = 0; i < collection.length; i++) { const obj = collection[i]; const key = keyFn(obj, i); if (isNil(key)) { result.push(obj); } else { sorted.push([key, obj]); } } sorted.sort((a, b) => comparator(a[0], b[0])); return into( result, sorted.map((o) => o[1]) ); } function groupBy(collection, keyFn, hashFunction = DEFAULT_HASH_FUNCTION) { if (collection.length < 1) return /* @__PURE__ */ new Map(); const lookup = /* @__PURE__ */ new Map(); const result = /* @__PURE__ */ new Map(); for (let i = 0; i < collection.length; i++) { const obj = collection[i]; const key = keyFn(obj, i); const hash = hashCode(key, hashFunction); if (hash === null) { if (result.has(null)) { result.get(null).push(obj); } else { result.set(null, [obj]); } } else { const existingKey = lookup.has(hash) ? lookup.get(hash).find((k) => isEqual(k, key)) : null; if (isNil(existingKey)) { result.set(key, [obj]); if (lookup.has(hash)) { lookup.get(hash).push(key); } else { lookup.set(hash, [key]); } } else { result.get(existingKey).push(obj); } } } return result; } const MAX_ARRAY_PUSH = 5e4; function into(target, ...rest) { if (target instanceof Array) { return rest.reduce( (acc, arr) => { let i = Math.ceil(arr.length / MAX_ARRAY_PUSH); let begin = 0; while (i-- > 0) { Array.prototype.push.apply( acc, arr.slice(begin, begin + MAX_ARRAY_PUSH) ); begin += MAX_ARRAY_PUSH; } return acc; }, target ); } else { return rest.filter(isObjectLike).reduce((acc, item) => { Object.assign(acc, item); return acc; }, target); } } function memoize(fn, hashFunction = DEFAULT_HASH_FUNCTION) { return /* @__PURE__ */ ((memo) => { return (...args) => { const key = hashCode(args, hashFunction) || ""; if (!has(memo, key)) { memo[key] = fn.apply(this, args); } return memo[key]; }; })({ /* storage */ }); } function getValue(obj, key) { return isObjectLike(obj) ? obj[key] : void 0; } function unwrap(arr, depth) { if (depth < 1) return arr; while (depth-- && arr.length === 1) arr = arr[0]; return arr; } function resolve(obj, selector, options) { let depth = 0; function resolve2(o, path) { let value = o; for (let i = 0; i < path.length; i++) { const field = path[i]; const isText = /^\d+$/.exec(field) === null; if (isText && value instanceof Array) { if (i === 0 && depth > 0) break; depth += 1; const subpath = path.slice(i); value = value.reduce((acc, item) => { const v = resolve2(item, subpath); if (v !== void 0) acc.push(v); return acc; }, []); break; } else { value = getValue(value, field); } if (value === void 0) break; } return value; } const result = JS_SIMPLE_TYPES.has(getType(obj).toLowerCase()) ? obj : resolve2(obj, selector.split(".")); return result instanceof Array && options?.unwrapArray ? unwrap(result, depth) : result; } function resolveGraph(obj, selector, options) { const names = selector.split("."); const key = names[0]; const next = names.slice(1).join("."); const isIndex = /^\d+$/.exec(key) !== null; const hasNext = names.length > 1; let result; let value; if (obj instanceof Array) { if (isIndex) { result = getValue(obj, Number(key)); if (hasNext) { result = resolveGraph(result, next, options); } result = [result]; } else { result = []; for (const item of obj) { value = resolveGraph(item, selector, options); if (options?.preserveMissing) { if (value === void 0) { value = MISSING; } result.push(value); } else if (value !== void 0) { result.push(value); } } } } else { value = getValue(obj, key); if (hasNext) { value = resolveGraph(value, next, options); } if (value === void 0) return void 0; result = options?.preserveKeys ? { ...obj } : {}; result[key] = value; } return result; } function filterMissing(obj) { if (obj instanceof Array) { 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 (const k in obj) { if (has(obj, k)) { filterMissing(obj[k]); } } } } const NUMBER_RE = /^\d+$/; function walk(obj, selector, fn, options) { const names = selector.split("."); const key = names[0]; const next = names.slice(1).join("."); if (names.length === 1) { if (isObject(obj) || isArray(obj) && NUMBER_RE.test(key)) { fn(obj, key); } } else { if (options?.buildGraph && isNil(obj[key])) { obj[key] = {}; } const item = obj[key]; if (!item) return; const isNextArrayIndex = !!(names.length > 1 && NUMBER_RE.test(names[1])); if (item instanceof Array && options?.descendArray && !isNextArrayIndex) { item.forEach((e) => walk(e, next, fn, options)); } else { walk(item, next, fn, options); } } } function setValue(obj, selector, value) { walk( obj, selector, (item, key) => { item[key] = isFunction(value) ? value(item[key]) : value; }, { buildGraph: true } ); } function removeValue(obj, selector, options) { walk( obj, selector, (item, key) => { if (item instanceof Array) { if (/^\d+$/.test(key)) { item.splice(parseInt(key), 1); } else if (options && options.descendArray) { for (const elem of item) { if (isObject(elem)) { delete elem[key]; } } } } else if (isObject(item)) { delete item[key]; } }, options ); } const OPERATOR_NAME_PATTERN = /^\$[a-zA-Z0-9_]+$/; function isOperator(name) { return OPERATOR_NAME_PATTERN.test(name); } function normalize(expr) { if (JS_SIMPLE_TYPES.has(getType(expr).toLowerCase())) { return isRegExp(expr) ? { $regex: expr } : { $eq: expr }; } if (isObjectLike(expr)) { const exprObj = expr; if (!Object.keys(exprObj).some(isOperator)) { return { $eq: expr }; } if (has(expr, "$regex")) { const newExpr = { ...expr }; newExpr["$regex"] = new RegExp( expr["$regex"], expr["$options"] ); delete newExpr["$options"]; return newExpr; } } return expr; } function findInsertIndex(sorted, item) { let lo = 0; let hi = sorted.length - 1; while (lo <= hi) { const mid = Math.round(lo + (hi - lo) / 2); if (compare(item, sorted[mid]) < 0) { hi = mid - 1; } else if (compare(item, sorted[mid]) > 0) { lo = mid + 1; } else { return mid; } } return lo; } export { MAX_INT, MAX_LONG, MIN_INT, MIN_LONG, MingoError, assert, cloneDeep, compare, ensureArray, filterMissing, findInsertIndex, flatten, getType, groupBy, has, hashCode, inArray, intersection, into, isArray, isBigInt, isBoolean, isDate, isEmpty, isEqual, isFunction, isMissing, isNil, isNotNaN, isNumber, isObject, isObjectLike, isOperator, isRegExp, isString, isSymbol, memoize, merge, normalize, notInArray, removeValue, resolve, resolveGraph, setValue, sortBy, stringify, truthy, unique, walk };