UNPKG

mingo

Version:

MongoDB query language for in-memory objects

171 lines (170 loc) 5.77 kB
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 };