mingo
Version:
MongoDB query language for in-memory objects
348 lines (347 loc) • 9.56 kB
JavaScript
import {
assert,
has,
isArray,
isFunction,
isNil,
isObject,
isOperator,
isString,
resolve
} from "./util";
var ProcessingMode = /* @__PURE__ */ ((ProcessingMode2) => {
ProcessingMode2[ProcessingMode2["CLONE_OFF"] = 0] = "CLONE_OFF";
ProcessingMode2[ProcessingMode2["CLONE_INPUT"] = 1] = "CLONE_INPUT";
ProcessingMode2[ProcessingMode2["CLONE_OUTPUT"] = 2] = "CLONE_OUTPUT";
ProcessingMode2[ProcessingMode2["CLONE_ALL"] = 3] = "CLONE_ALL";
return ProcessingMode2;
})(ProcessingMode || {});
class ComputeOptions {
#options;
/** Reference to the root object when processing subgraphs of the object. */
#root;
#local;
constructor(options, root, local) {
this.#options = options;
this.update(root, local);
}
/**
* Initialize new ComputeOptions.
* @returns {ComputeOptions}
*/
static init(options, root, local) {
return !(options instanceof ComputeOptions) ? new ComputeOptions(options, root, local) : new ComputeOptions(options.#options, options.root ?? root, {
...options.#local,
...local,
variables: Object.assign(
{},
options.#local?.variables,
local?.variables
)
});
}
/**
* Updates the internal state.
*
* @param root The new root context for this object.
* @param local The new local state to merge into current if it exists.
* @returns
*/
update(root, local) {
this.#root = root;
const variables = Object.assign(
{},
this.#local?.variables,
local?.variables
);
if (Object.keys(variables).length) {
this.#local = { ...local, variables };
} else {
this.#local = local ?? {};
}
return this;
}
getOptions() {
return Object.freeze({
...this.#options,
context: Context.from(this.#options.context)
});
}
get root() {
return this.#root;
}
get local() {
return this.#local;
}
get idKey() {
return this.#options.idKey;
}
get collation() {
return this.#options?.collation;
}
get processingMode() {
return this.#options?.processingMode || 0 /* CLONE_OFF */;
}
get useStrictMode() {
return this.#options?.useStrictMode;
}
get scriptEnabled() {
return this.#options?.scriptEnabled;
}
get useGlobalContext() {
return this.#options?.useGlobalContext;
}
get hashFunction() {
return this.#options?.hashFunction;
}
get collectionResolver() {
return this.#options?.collectionResolver;
}
get jsonSchemaValidator() {
return this.#options?.jsonSchemaValidator;
}
get variables() {
return this.#options?.variables;
}
get context() {
return this.#options?.context;
}
}
function initOptions(options) {
return options instanceof ComputeOptions ? options.getOptions() : Object.freeze({
idKey: "_id",
scriptEnabled: true,
useStrictMode: true,
useGlobalContext: true,
processingMode: 0 /* CLONE_OFF */,
...options,
context: options?.context ? Context.from(options?.context) : Context.init()
});
}
var OperatorType = /* @__PURE__ */ ((OperatorType2) => {
OperatorType2["ACCUMULATOR"] = "accumulator";
OperatorType2["EXPRESSION"] = "expression";
OperatorType2["PIPELINE"] = "pipeline";
OperatorType2["PROJECTION"] = "projection";
OperatorType2["QUERY"] = "query";
OperatorType2["WINDOW"] = "window";
return OperatorType2;
})(OperatorType || {});
class Context {
#operators = /* @__PURE__ */ new Map();
constructor() {
}
static init() {
return new Context();
}
static from(ctx) {
const instance = Context.init();
if (isNil(ctx)) return instance;
ctx.#operators.forEach((v, k) => instance.addOperators(k, v));
return instance;
}
addOperators(type, operators) {
if (!this.#operators.has(type)) this.#operators.set(type, {});
for (const [name, fn] of Object.entries(operators)) {
if (!this.getOperator(type, name)) {
this.#operators.get(type)[name] = fn;
}
}
return this;
}
getOperator(type, name) {
const ops = this.#operators.get(type) ?? {};
return ops[name] ?? null;
}
addAccumulatorOps(ops) {
return this.addOperators("accumulator", ops);
}
addExpressionOps(ops) {
return this.addOperators("expression", ops);
}
addQueryOps(ops) {
return this.addOperators("query", ops);
}
addPipelineOps(ops) {
return this.addOperators("pipeline", ops);
}
addProjectionOps(ops) {
return this.addOperators("projection", ops);
}
addWindowOps(ops) {
return this.addOperators("window", ops);
}
}
const GLOBAL_CONTEXT = Context.init();
function useOperators(type, operators) {
for (const [name, fn] of Object.entries(operators)) {
assert(
isFunction(fn) && isOperator(name),
`'${name}' is not a valid operator`
);
const currentFn = getOperator(type, name, null);
assert(
!currentFn || fn === currentFn,
`${name} already exists for '${type}' operators. Cannot change operator function once registered.`
);
}
switch (type) {
case "accumulator":
GLOBAL_CONTEXT.addAccumulatorOps(operators);
break;
case "expression":
GLOBAL_CONTEXT.addExpressionOps(operators);
break;
case "pipeline":
GLOBAL_CONTEXT.addPipelineOps(operators);
break;
case "projection":
GLOBAL_CONTEXT.addProjectionOps(operators);
break;
case "query":
GLOBAL_CONTEXT.addQueryOps(operators);
break;
case "window":
GLOBAL_CONTEXT.addWindowOps(operators);
break;
}
}
function getOperator(type, name, options) {
const { context: ctx, useGlobalContext: fallback } = options || {};
const fn = ctx ? ctx.getOperator(type, name) : null;
return !fn && fallback ? GLOBAL_CONTEXT.getOperator(type, name) : fn;
}
function computeValue(obj, expr, operator, options) {
const copts = ComputeOptions.init(options, obj);
return !!operator && isOperator(operator) ? computeOperator(obj, expr, operator, copts) : computeExpression(obj, expr, copts);
}
const SYSTEM_VARS = ["$$ROOT", "$$CURRENT", "$$REMOVE", "$$NOW"];
function computeExpression(obj, expr, options) {
if (isString(expr) && expr.length > 0 && expr[0] === "$") {
if (REDACT_ACTIONS.includes(expr)) return expr;
let ctx = options.root;
const arr = expr.split(".");
if (SYSTEM_VARS.includes(arr[0])) {
switch (arr[0]) {
case "$$ROOT":
break;
case "$$CURRENT":
ctx = obj;
break;
case "$$REMOVE":
ctx = void 0;
break;
case "$$NOW":
ctx = /* @__PURE__ */ new Date();
break;
}
expr = expr.slice(arr[0].length + 1);
} else if (arr[0].slice(0, 2) === "$$") {
ctx = Object.assign(
{},
// global vars
options.variables,
// current item is added before local variables because the binding may be changed.
{ this: obj },
// local vars
options?.local?.variables
);
const name = arr[0].slice(2);
assert(has(ctx, name), `Use of undefined variable: ${name}`);
expr = expr.slice(2);
} else {
expr = expr.slice(1);
}
return expr === "" ? ctx : resolve(ctx, expr);
}
if (isArray(expr)) {
return expr.map((item) => computeExpression(obj, item, options));
}
if (isObject(expr)) {
const result = {};
const elems = Object.entries(expr);
for (const [key, val] of elems) {
if (isOperator(key)) {
assert(elems.length == 1, "expression must have single operator.");
return computeOperator(obj, val, key, options);
}
result[key] = computeExpression(obj, val, options);
}
return result;
}
return expr;
}
function computeOperator(obj, expr, operator, options) {
const callExpression = getOperator(
"expression",
operator,
options
);
if (callExpression) return callExpression(obj, expr, options);
const callAccumulator = getOperator(
"accumulator",
operator,
options
);
assert(!!callAccumulator, `accumulator '${operator}' is not registered.`);
if (!isArray(obj)) {
obj = computeExpression(obj, expr, options);
expr = null;
}
assert(isArray(obj), `arguments must resolve to array for ${operator}.`);
return callAccumulator(
obj,
expr,
options.update(null, options.local)
// reset the root object.
);
}
const REDACT_ACTIONS = ["$$KEEP", "$$PRUNE", "$$DESCEND"];
function redact(obj, expr, options) {
const action = computeValue(obj, expr, null, options);
switch (action) {
case "$$KEEP":
return obj;
case "$$PRUNE":
return void 0;
case "$$DESCEND": {
if (!has(expr, "$cond")) return obj;
const output = {};
for (const [key, value] of Object.entries(obj)) {
if (isArray(value)) {
const res = new Array();
for (let elem of value) {
if (isObject(elem)) {
elem = redact(elem, expr, options.update(elem));
}
if (!isNil(elem)) res.push(elem);
}
output[key] = res;
} else if (isObject(value)) {
const res = redact(
value,
expr,
options.update(value)
);
if (!isNil(res)) output[key] = res;
} else {
output[key] = value;
}
}
return output;
}
default:
return action;
}
}
export {
ComputeOptions,
Context,
OperatorType,
ProcessingMode,
computeValue,
getOperator,
initOptions,
redact,
useOperators
};