343 lines
9.3 KiB
JavaScript
343 lines
9.3 KiB
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);
|
|
}
|
|
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
|
|
};
|