UNPKG

mingo

Version:

MongoDB query language for in-memory objects

589 lines (588 loc) 17.4 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var util_exports = {}; __export(util_exports, { HashMap: () => HashMap, MingoError: () => MingoError, assert: () => assert, cloneDeep: () => cloneDeep, compare: () => compare, ensureArray: () => ensureArray, filterMissing: () => filterMissing, findInsertIndex: () => findInsertIndex, flatten: () => flatten, groupBy: () => groupBy, has: () => has, hashCode: () => hashCode, intersection: () => intersection, isArray: () => isArray, isBoolean: () => isBoolean, isDate: () => isDate, isEmpty: () => isEmpty, isEqual: () => isEqual, isFunction: () => isFunction, isNil: () => isNil, isNumber: () => isNumber, isObject: () => isObject, isObjectLike: () => isObjectLike, isOperator: () => isOperator, isRegExp: () => isRegExp, isString: () => isString, isSymbol: () => isSymbol, merge: () => merge, normalize: () => normalize, removeValue: () => removeValue, resolve: () => resolve, resolveGraph: () => resolveGraph, setValue: () => setValue, stringify: () => stringify, truthy: () => truthy, typeOf: () => typeOf, unique: () => unique, walk: () => walk }); module.exports = __toCommonJS(util_exports); class MingoError extends Error { } const MISSING = Symbol("missing"); const ERR_CYCLE_FOUND = "mingo: cycle detected while processing object/array"; 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 isPrimitive = (v) => typeof v !== "object" && typeof v !== "function" || v === null; const isScalar = (v) => isPrimitive(v) || isDate(v) || isRegExp(v); const SORT_ORDER = { undefined: 1, null: 2, number: 3, string: 4, symbol: 5, object: 6, array: 7, arraybuffer: 8, boolean: 9, date: 10, regexp: 11, function: 12 }; function compare(a, b) { if (a === MISSING) a = void 0; if (b === MISSING) b = void 0; const customOrder = 100; const [u, v] = [a, b].map( (n) => SORT_ORDER[isTypedArray(n) ? "arraybuffer" : typeOf(n)] ?? customOrder /*custom objects have highest sort order*/ ); if (u !== v) return u - v; if (u === customOrder) { a = stringify(a); b = stringify(b); } return isEqual(a, b) ? 0 : a < b ? -1 : 1; } class HashMap extends Map { // The hash function #hashFn = DEFAULT_HASH_FUNCTION; // maps the hashcode to key set #keyMap = /* @__PURE__ */ new Map(); // returns a tuple of [<masterKey>, <hash>]. Expects an object key. #unpack = (key) => { const hash = this.#hashFn(key); return [(this.#keyMap.get(hash) || []).find((k) => isEqual(k, key)), hash]; }; constructor() { super(); } /** * Returns a new {@link HashMap} object. * @param fn An optional custom hash function */ static init(fn) { const m = new HashMap(); if (fn) m.#hashFn = fn; return m; } clear() { super.clear(); this.#keyMap.clear(); } /** * @returns true if an element in the Map existed and has been removed, or false if the element does not exist. */ delete(key) { if (isPrimitive(key)) return super.delete(key); const [masterKey, hash] = this.#unpack(key); if (!super.delete(masterKey)) return false; this.#keyMap.set( hash, this.#keyMap.get(hash).filter((k) => !isEqual(k, masterKey)) ); return true; } /** * Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map. * @returns Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned. */ get(key) { if (isPrimitive(key)) return super.get(key); const [masterKey, _] = this.#unpack(key); return super.get(masterKey); } /** * @returns boolean indicating whether an element with the specified key exists or not. */ has(key) { if (isPrimitive(key)) return super.has(key); const [masterKey, _] = this.#unpack(key); return super.has(masterKey); } /** * Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated. */ set(key, value) { if (isPrimitive(key)) return super.set(key, value); const [masterKey, hash] = this.#unpack(key); if (super.has(masterKey)) { super.set(masterKey, value); } else { super.set(key, value); const keys = this.#keyMap.get(hash) || []; keys.push(key); this.#keyMap.set(hash, keys); } return this; } /** * @returns the number of elements in the Map. */ get size() { return super.size; } } function assert(condition, message) { if (!condition) throw new MingoError(message); } function typeOf(v) { if (v === null) return "null"; const t = typeof v; if (t !== "object" && t in SORT_ORDER) return t; if (isArray(v)) return "array"; if (isDate(v)) return "date"; if (isRegExp(v)) return "regexp"; const s = Object.prototype.toString.call(v); return s === "[object Object]" ? v?.constructor?.name?.toLowerCase() ?? "object" : s.slice(8, -1).toLowerCase(); } 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 isArray = Array.isArray; const isObject = (v) => typeOf(v) === "object"; const isObjectLike = (v) => !isPrimitive(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 truthy = (arg, strict = true) => !!arg || strict && arg === ""; const isEmpty = (x) => isNil(x) || isString(x) && !x || isArray(x) && x.length === 0 || isObject(x) && Object.keys(x).length === 0; const ensureArray = (x) => isArray(x) ? x : [x]; const has = (obj, prop) => !!obj && Object.prototype.hasOwnProperty.call(obj, prop); const isTypedArray = (v) => typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(v); const cloneDeep = (v, refs) => { if (isNil(v) || isBoolean(v) || isNumber(v) || isString(v)) return v; if (isDate(v)) return new Date(v); if (isRegExp(v)) return new RegExp(v); if (isTypedArray(v)) { const ctor = v.constructor; return new ctor(v); } if (!(refs instanceof Set)) refs = /* @__PURE__ */ new Set(); if (refs.has(v)) throw new Error(ERR_CYCLE_FOUND); refs.add(v); try { if (isArray(v)) { const arr = new Array(v.length); for (let i = 0; i < v.length; i++) arr[i] = cloneDeep(v[i], refs); return arr; } if (isObject(v)) { const obj = {}; for (const k of Object.keys(v)) obj[k] = cloneDeep(v[k], refs); return obj; } } finally { refs.delete(v); } return v; }; const isMissing = (v) => v === MISSING; function merge(target, input) { if (isMissing(target) || isNil(target)) return input; if (isMissing(input) || isNil(input)) return target; const t = typeOf(target); if (t !== typeOf(input)) return input; if (isArray(target) && isArray(input)) { for (let i = 0; i < input.length; i++) { if (i < target.length) { target[i] = merge(target[i], input[i]); } else { target.push(input[i]); } } } else if (t === "object") { for (const k of Object.keys(input)) { target[k] = merge(target[k], input[k]); } } return target; } function intersection(input, hashFunc = DEFAULT_HASH_FUNCTION) { const vmaps = [HashMap.init(hashFunc), HashMap.init(hashFunc)]; if (input.length === 0) return []; if (input.some((arr) => arr.length === 0)) return []; if (input.length === 1) return [...input[0]]; input[input.length - 1].forEach((v) => vmaps[0].set(v, true)); for (let i = input.length - 2; i > -1; i--) { input[i].forEach((v) => { if (vmaps[0].has(v)) vmaps[1].set(v, true); }); if (vmaps[1].size === 0) return []; vmaps.reverse(); vmaps[1].clear(); } return Array.from(vmaps[0].keys()); } function flatten(xs, depth = 1) { 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; } function getMembersOf(o) { const props = {}; while (o) { for (const k of Object.getOwnPropertyNames(o)) if (!(k in props)) props[k] = o[k]; o = Object.getPrototypeOf(o); } return props; } const hasCustomString = (o) => o !== null && o !== void 0 && o["toString"] !== Object.prototype.toString; function isEqual(a, b) { if (a === b || Object.is(a, b)) return true; if (a === null || b === null) return false; if (typeof a !== typeof b) return false; if (typeof a !== "object") return false; if (isDate(a)) return isDate(b) && +a === +b; if (isRegExp(a)) return isRegExp(b) && a.toString() === b.toString(); const t = typeOf(a); if (t !== typeOf(b)) return false; switch (t) { case "array": if (a.length !== b.length) return false; for (let i = 0, size = a.length; i < size; i++) { if (!isEqual(a[i], b[i])) return false; } return true; case "object": { const ka = Object.keys(a); const kb = Object.keys(b); if (ka.length !== kb.length) return false; for (const k of ka) { if (!(k in b && isEqual(a[k], b[k]))) return false; } return true; } default: return hasCustomString(a) && a.toString() === b.toString(); } } function unique(input, hashFunc = DEFAULT_HASH_FUNCTION) { const m = HashMap.init(hashFunc); input.forEach((v) => m.set(v, true)); return Array.from(m.keys()); } function stringify(v, refs) { if (v === null) return "null"; if (v === void 0) return "undefined"; if (isString(v) || isNumber(v) || isBoolean(v)) return JSON.stringify(v); if (isDate(v)) return v.toISOString(); if (isRegExp(v) || isSymbol(v) || isFunction(v)) return v.toString(); if (!(refs instanceof Set)) refs = /* @__PURE__ */ new Set(); if (refs.has(v)) throw new Error(ERR_CYCLE_FOUND); try { refs.add(v); if (isArray(v)) return "[" + v.map((s2) => stringify(s2, refs)).join(",") + "]"; if (isObject(v)) { const keys = Object.keys(v).sort(); return "{" + keys.map((k) => `${k}:${stringify(v[k], refs)}`).join() + "}"; } const s = hasCustomString(v) ? v.toString() : stringify(getMembersOf(v), refs); return typeOf(v) + "(" + s + ")"; } finally { refs.delete(v); } } function hashCode(value, hashFunc) { if (isNil(value)) return null; hashFunc = hashFunc || DEFAULT_HASH_FUNCTION; return hashFunc(value); } function groupBy(collection, keyFunc, hashFunc = DEFAULT_HASH_FUNCTION) { if (collection.length < 1) return /* @__PURE__ */ new Map(); const result = HashMap.init(hashFunc); for (let i = 0; i < collection.length; i++) { const obj = collection[i]; const key = keyFunc(obj, i) ?? null; let a = result.get(key); if (!a) { a = [obj]; result.set(key, a); } else { a.push(obj); } } return result; } 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 && isArray(arr[0])) 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 && isArray(value)) { 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 res = isScalar(obj) ? obj : resolve2(obj, selector.split(".")); return isArray(res) && options?.unwrapArray ? unwrap(res, depth) : res; } function resolveGraph(obj, selector, options) { const sep = selector.indexOf("."); const key = sep == -1 ? selector : selector.substring(0, sep); const next = selector.substring(sep + 1); const hasNext = sep != -1; if (isArray(obj)) { const isIndex = /^\d+$/.test(key); const arr = isIndex && options?.preserveIndex ? [...obj] : []; if (isIndex) { const index = parseInt(key); let value2 = getValue(obj, index); if (hasNext) { value2 = resolveGraph(value2, next, options); } if (options?.preserveIndex) { arr[index] = value2; } else { arr.push(value2); } } else { for (const item of obj) { const value2 = resolveGraph(item, selector, options); if (options?.preserveMissing) { arr.push(value2 == void 0 ? MISSING : value2); } else if (value2 != void 0 || options?.preserveIndex) { arr.push(value2); } } } return arr; } const res = options?.preserveKeys ? { ...obj } : {}; let value = getValue(obj, key); if (hasNext) { value = resolveGraph(value, next, options); } if (value === void 0) return void 0; res[key] = value; return res; } 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 (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 (isArray(item) && 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 (isArray(item)) { item.splice(parseInt(key), 1); } else if (isObject(item)) { delete item[key]; } }), options ); } const isOperator = (name) => name && name[0] === "$" && /^\$[a-zA-Z0-9_]+$/.test(name); function normalize(expr) { if (isScalar(expr)) { return isRegExp(expr) ? { $regex: expr } : { $eq: expr }; } if (isObjectLike(expr)) { if (!Object.keys(expr).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, comparator = compare) { let lo = 0; let hi = sorted.length - 1; while (lo <= hi) { const mid = Math.round(lo + (hi - lo) / 2); if (comparator(item, sorted[mid]) < 0) { hi = mid - 1; } else if (comparator(item, sorted[mid]) > 0) { lo = mid + 1; } else { return mid; } } return lo; } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { HashMap, MingoError, assert, cloneDeep, compare, ensureArray, filterMissing, findInsertIndex, flatten, groupBy, has, hashCode, intersection, isArray, isBoolean, isDate, isEmpty, isEqual, isFunction, isNil, isNumber, isObject, isObjectLike, isOperator, isRegExp, isString, isSymbol, merge, normalize, removeValue, resolve, resolveGraph, setValue, stringify, truthy, typeOf, unique, walk });