| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336 |
- "use strict";
- /**
- * A base class for all pipeline expressions; Performs common expressions within an Op.
- *
- * NOTE: An object expression can take any of the following forms:
- *
- * f0: {f1: ..., f2: ..., f3: ...}
- * f0: {$operator:[operand1, operand2, ...]}
- *
- * @class Expression
- * @namespace mungedb-aggregate.pipeline.expressions
- * @module mungedb-aggregate
- * @constructor
- */
- var Expression = module.exports = function Expression() {
- if (arguments.length !== 0) throw new Error("zero args expected");
- }, klass = Expression, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
- var Value = require("../Value"),
- Document = require("../Document"),
- Variables = require("./Variables");
- /**
- * Reference to the `mungedb-aggregate.pipeline.expressions.Expression.ObjectCtx` class
- * @static
- * @property ObjectCtx
- */
- var ObjectCtx = Expression.ObjectCtx = (function() {
- // CONSTRUCTOR
- /**
- * Utility class for parseObject() below. isDocumentOk indicates that it is OK to use a Document in the current context.
- *
- * NOTE: deviation from Mongo code: accepts an `Object` of settings rather than a bitmask to help simplify the interface a little bit
- *
- * @class ObjectCtx
- * @namespace mungedb-aggregate.pipeline.expressions.Expression
- * @module mungedb-aggregate
- * @constructor
- * @param opts
- * @param [opts.isDocumentOk] {Boolean}
- * @param [opts.isTopLevel] {Boolean}
- * @param [opts.isInclusionOk] {Boolean}
- */
- var klass = function ObjectCtx(opts /*= {isDocumentOk:..., isTopLevel:..., isInclusionOk:...}*/ ) {
- if (!(opts instanceof Object && opts.constructor === Object)) throw new Error("opts is required and must be an Object containing named args");
- for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
- if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
- }
- }, base = Object,
- proto = klass.prototype = Object.create(base.prototype, {
- constructor: {
- value: klass
- }
- });
- // PROTOTYPE MEMBERS
- proto.isDocumentOk =
- proto.isTopLevel =
- proto.isInclusionOk = undefined;
- return klass;
- })();
- //
- // Diagram of relationship between parse functions when parsing a $op:
- //
- // { someFieldOrArrayIndex: { $op: [ARGS] } }
- // ^ parseExpression on inner $op BSONElement
- // ^ parseObject on BSONObject
- // ^ parseOperand on outer BSONElement wrapping the $op Object
- //
- /**
- * Parses a JSON Object that could represent a functional expression or a Document expression.
- * @method parseObject
- * @static
- * @param obj the element representing the object
- * @param ctx a MiniCtx representing the options above
- * @param vps Variables Parse State
- * @returns the parsed Expression
- */
- klass.parseObject = function parseObject(obj, ctx, vps) {
- if (!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
- /*
- An object expression can take any of the following forms:
- f0: {f1: ..., f2: ..., f3: ...}
- f0: {$operator:[operand1, operand2, ...]}
- */
- var expression, // the result
- expressionObject, // the alt result
- UNKNOWN = 0,
- NOTOPERATOR = 1,
- OPERATOR = 2,
- kind = UNKNOWN;
- if (obj === undefined || obj === null || (obj instanceof Object && Object.keys(obj).length === 0)) return new ObjectExpression();
- var fieldNames = Object.keys(obj);
- for (var fieldCount = 0, n = fieldNames.length; fieldCount < n; ++fieldCount) {
- var fieldName = fieldNames[fieldCount];
- if (fieldName[0] === "$") {
- if (fieldCount !== 0)
- throw new Error("the operator must be the only field in a pipeline object (at '" + fieldName + "'.; uassert code 15983");
- if (ctx.isTopLevel)
- throw new Error("$expressions are not allowed at the top-level of $project; uassert code 16404");
- // we've determined this "object" is an operator expression
- kind = OPERATOR;
- expression = Expression.parseExpression(fieldName, obj[fieldName], vps); //NOTE: DEVIATION FROM MONGO: c++ code uses 2 arguments. See #parseExpression
- } else {
- if (kind === OPERATOR)
- throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + fieldName + "'.; uassert code 15990");
- if (!ctx.isTopLevel && fieldName.indexOf(".") !== -1)
- throw new Error("dotted field names are only allowed at the top level; uassert code 16405");
- // if it's our first time, create the document expression
- if (expression === undefined) {
- if (!ctx.isDocumentOk) throw new Error("Assertion failure");
- // CW TODO error: document not allowed in this context
- expressionObject = ctx.isTopLevel ? ObjectExpression.createRoot() : ObjectExpression.create();
- expression = expressionObject;
- // this "object" is not an operator expression
- kind = NOTOPERATOR;
- }
- var fieldValue = obj[fieldName];
- switch (typeof(fieldValue)) {
- case "object":
- // it's a nested document
- var subCtx = new ObjectCtx({
- isDocumentOk: ctx.isDocumentOk,
- isInclusionOk: ctx.isInclusionOk
- });
- expressionObject.addField(fieldName, Expression.parseObject(fieldValue, subCtx, vps));
- break;
- case "string":
- // it's a renamed field
- // CW TODO could also be a constant
- expressionObject.addField(fieldName, FieldPathExpression.parse(fieldValue, vps));
- break;
- case "boolean":
- case "number":
- // it's an inclusion specification
- if (fieldValue) {
- if (!ctx.isInclusionOk)
- throw new Error("field inclusion is not allowed inside of $expressions; uassert code 16420");
- expressionObject.includePath(fieldName);
- } else {
- if (!(ctx.isTopLevel && fieldName === Document.ID_PROPERTY_NAME))
- throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; uassert code 16406");
- expressionObject.excludeId = true;
- }
- break;
- default:
- throw new Error("disallowed field type " + Value.getType(fieldValue) + " in object expression (at '" + fieldName + "') uassert code 15992");
- }
- }
- }
- return expression;
- };
- klass.expressionParserMap = {};
- /**
- * Registers an ExpressionParser so it can be called from parseExpression and friends.
- * As an example, if your expression looks like {"$foo": [1,2,3]} you would add this line:
- * REGISTER_EXPRESSION("$foo", ExpressionFoo::parse);
- */
- klass.registerExpression = function registerExpression(key, parserFunc) {
- if (key in klass.expressionParserMap) {
- throw new Error("Duplicate expression (" + key + ") detected; massert code 17064");
- }
- klass.expressionParserMap[key] = parserFunc;
- return 1;
- };
- //NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3. //TODO: could easily fix this inconsistency
- /**
- * Parses a BSONElement which has already been determined to be functional expression.
- * @static
- * @method parseExpression
- * @param exprElement should be the only element inside the expression object.
- * That is the field name should be the $op for the expression.
- * @param vps the variable parse state
- * @returns the parsed Expression
- */
- klass.parseExpression = function parseExpression(exprElementKey, exprElementValue, vps) {
- var opName = exprElementKey,
- op = Expression.expressionParserMap[opName];
- if (!op) throw new Error("invalid operator : " + exprElementKey + "; uassert code 15999");
- // make the expression node
- return op(exprElementValue, vps);
- };
- /**
- * Parses a BSONElement which is an operand in an Expression.
- *
- * This is the most generic parser and can parse ExpressionFieldPath, a literal, or a $op.
- * If it is a $op, exprElement should be the outer element whose value is an Object
- * containing the $op.
- *
- * @method parseOperand
- * @static
- * @param exprElement should be the only element inside the expression object.
- * That is the field name should be the $op for the expression.
- * @param vps the variable parse state
- * @returns the parsed operand, as an Expression
- */
- klass.parseOperand = function parseOperand(exprElement, vps) {
- var t = typeof(exprElement);
- if (t === "string" && exprElement[0] === "$") {
- //if we got here, this is a field path expression
- return FieldPathExpression.parse(exprElement, vps);
- } else if (t === "object" && exprElement && exprElement.constructor === Object) {
- var oCtx = new ObjectCtx({
- isDocumentOk: true
- });
- return Expression.parseObject(exprElement, oCtx, vps);
- } else {
- return ConstantExpression.parse(exprElement, vps);
- }
- };
- /**
- * Optimize the Expression.
- *
- * This provides an opportunity to do constant folding, or to collapse nested
- * operators that have the same precedence, such as $add, $and, or $or.
- *
- * The Expression should be replaced with the return value, which may or may
- * not be the same object. In the case of constant folding, a computed
- * expression may be replaced by a constant.
- *
- * @method optimize
- * @returns the optimized Expression
- */
- proto.optimize = function optimize() {
- return this;
- };
- /**
- * Add this expression's field dependencies to the set.
- * Expressions are trees, so this is often recursive.
- *
- * @method addDependencies
- * @param deps Fully qualified paths to depended-on fields are added to this set.
- * Empty string means need full document.
- * @param path path to self if all ancestors are ExpressionObjects.
- * Top-level ExpressionObject gets pointer to empty vector.
- * If any other Expression is an ancestor, or in other cases
- * where {a:1} inclusion objects aren't allowed, they get
- * NULL.
- */
- proto.addDependencies = function addDependencies(deps, path) {
- throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
- };
- /**
- * simple expressions are just inclusion exclusion as supported by ExpressionObject
- * @method isSimple
- */
- proto.isSimple = function isSimple() {
- return false;
- };
- /**
- * Serialize the Expression tree recursively.
- * If explain is false, returns a Value parsable by parseOperand().
- * @method serialize
- */
- proto.serialize = function serialize(explain) {
- throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
- };
- /**
- * Evaluate expression with specified inputs and return result.
- *
- * While vars is non-const, if properly constructed, subexpressions modifications to it
- * should not effect outer expressions due to unique variable Ids.
- *
- * @method evaluate
- * @param vars
- */
- proto.evaluate = function evaluate(vars) {
- if (!(vars instanceof Variables)) vars = new Variables(0, vars); /// Evaluate expression with specified inputs and return result. (only used by tests)
- return this.evaluateInternal(vars);
- };
- /**
- * Produce a field path string with the field prefix removed.
- * Throws an error if the field prefix is not present.
- * @method removeFieldPrefix
- * @static
- * @param prefixedField the prefixed field
- * @returns the field path with the prefix removed
- */
- klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
- if (prefixedField.indexOf("\0") !== -1) throw new Error("field path must not contain embedded null characters; uassert code 16419");
- if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
- return prefixedField.substr(1);
- };
- /**
- * Evaluate the subclass Expression using the given Variables as context and return result.
- * @method evaluate
- * @returns the computed value
- */
- proto.evaluateInternal = function evaluateInternal(vars) {
- throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
- };
- var ObjectExpression = require("./ObjectExpression"),
- FieldPathExpression = require("./FieldPathExpression"),
- ConstantExpression = require("./ConstantExpression");
|