mingo
Version:
MongoDB query language for in-memory objects
171 lines (170 loc) • 5.77 kB
JavaScript
import { ComputeOptions, Context } from "../../core/_internal";
import * as booleanOperators from "../../operators/expression/boolean";
import * as comparisonOperators from "../../operators/expression/comparison";
import * as queryOperators from "../../operators/query";
import { Query } from "../../query";
import {
assert,
cloneDeep,
isArray,
isDate,
isObject,
isRegExp,
resolve,
walk
} from "../../util/_internal";
const DEFAULT_OPTIONS = ComputeOptions.init({
context: Context.init().addQueryOps(queryOperators).addExpressionOps(booleanOperators).addExpressionOps(comparisonOperators)
}).update({ updateConfig: { cloneMode: "copy" } });
const clone = (val, opts) => {
const mode = opts?.local?.updateConfig?.cloneMode ?? "copy";
switch (mode) {
case "deep":
return cloneDeep(val);
case "copy": {
if (isDate(val)) return new Date(val);
if (isArray(val)) return val.slice();
if (isObject(val)) return Object.assign({}, val);
if (isRegExp(val)) return new RegExp(val);
return val;
}
default:
return val;
}
};
const FIRST_ONLY = "$";
const ARRAY_WIDE = "$[]";
const applyUpdate = (o, n, q, f, opts) => {
const { selector, position: c, next } = n;
if (!c) {
let b = false;
const g = (u, k) => b = Boolean(f(u, k)) || b;
walk(o, selector, g, opts);
return b;
}
const arr = resolve(o, selector);
if (!isArray(arr) || !arr.length) return false;
if (c === FIRST_ONLY) {
const i = arr.findIndex((e) => q[selector].test({ [selector]: [e] }));
if (i === -1) return false;
return next ? applyUpdate(arr[i], next, q, f, opts) : f(arr, i);
}
return arr.map((e, i) => {
if (c !== ARRAY_WIDE && q[c] && !q[c].test({ [c]: [e] })) return false;
return next ? applyUpdate(e, next, q, f, opts) : f(arr, i);
}).some(Boolean);
};
const ERR_MISSING_FIELD = "You must include the array field for '.$' as part of the query document.";
const ERR_IMMUTABLE_FIELD = (path, idKey) => `Performing an update on the path '${path}' would modify the immutable field '${idKey}'.`;
function walkExpression(expr, arrayFilters, options, callback) {
const opts = options instanceof ComputeOptions ? options : ComputeOptions.init(options);
const params = opts.local.updateParams ?? buildParams([expr], arrayFilters, opts);
const modified = [];
for (const [key, val] of Object.entries(expr)) {
const { node, queries } = params[key];
if (callback(val, node, queries)) modified.push(node.selector);
}
return modified.sort();
}
function buildParams(exprList, arrayFilters, options) {
const params = {};
arrayFilters ||= [];
const filterIndexMap = Object.fromEntries(
arrayFilters.map((o, i) => [Object.entries(o).pop()[0].split(".")[0], i])
);
const { condition } = options.local;
const queryKeys = condition && Object.keys(condition);
const conflictDetector = new Trie();
for (const expr of exprList) {
for (const selector of Object.keys(expr)) {
const identifiers = [];
const node = selector.includes("$") ? { selector: void 0 } : { selector };
if (!node.selector) {
selector.split(".").reduce((n, v) => {
if (v === FIRST_ONLY || v === ARRAY_WIDE) {
n.position = v;
} else if (v.startsWith("$[") && v.endsWith("]")) {
const id = v.slice(2, -1);
assert(
/^[a-z]+\w*$/.test(id),
`The filter <identifier> must begin with a lowercase letter and contain only alphanumeric characters. '${v}' is invalid.`
);
identifiers.push(id);
n.position = id;
} else if (!n.selector) {
n.selector = v;
} else if (!n.position) {
n.selector += "." + v;
} else {
n.next = { selector: v };
return n.next;
}
return n;
}, node);
}
const queries = {};
if (identifiers.length) {
const filters = {};
identifiers.forEach((v) => {
filters[v] = arrayFilters[filterIndexMap[v]];
});
for (const [k, c] of Object.entries(filters)) {
queries[k] = new Query(c, options);
}
}
if (node.position === FIRST_ONLY) {
const field = node.selector;
assert(queryKeys && queryKeys.length, ERR_MISSING_FIELD);
const matches = queryKeys.filter(
(k2) => k2 === field || k2.startsWith(field + ".")
);
assert(matches.length === 1, ERR_MISSING_FIELD);
const k = matches[0];
queries[field] = new Query({ [k]: condition[k] }, options);
}
const idKey = options.idKey;
assert(
node.selector !== idKey && !node.selector.startsWith(`${idKey}.`),
ERR_IMMUTABLE_FIELD(node.selector, idKey)
);
assert(
conflictDetector.add(node.selector),
`updating the path '${node.selector}' would create a conflict at '${node.selector}'`
);
params[selector] = { node, queries };
}
}
return params;
}
class Trie {
constructor() {
this.root = {
children: /* @__PURE__ */ new Map(),
isTerminal: false
};
}
add(selector) {
const parts = selector.split(".");
let current = this.root;
for (const part of parts) {
if (current.isTerminal) return false;
if (!current.children.has(part)) {
current.children.set(part, {
children: /* @__PURE__ */ new Map(),
isTerminal: false
});
}
current = current.children.get(part);
}
if (current.isTerminal || current.children.size) return false;
return current.isTerminal = true;
}
}
export {
DEFAULT_OPTIONS,
Trie,
applyUpdate,
buildParams,
clone,
walkExpression
};