mingo
Version:
MongoDB query language for in-memory objects
589 lines (588 loc) • 17.4 kB
JavaScript
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
});