Expression.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. "use strict";
  2. /**
  3. * A base class for all pipeline expressions; Performs common expressions within an Op.
  4. * @class Expression
  5. * @namespace mungedb-aggregate.pipeline.expressions
  6. * @module mungedb-aggregate
  7. * @constructor
  8. */
  9. var Expression = module.exports = function Expression() {
  10. if (arguments.length !== 0) throw new Error("zero args expected");
  11. }, klass = Expression, proto = klass.prototype;
  12. var Value = require("../Value"),
  13. Document = require("../Document"),
  14. Variables = require("./Variables");
  15. /**
  16. * Reference to the `mungedb-aggregate.pipeline.expressions.Expression.ObjectCtx` class
  17. * @static
  18. * @property ObjectCtx
  19. */
  20. var ObjectCtx = Expression.ObjectCtx = (function() {
  21. // CONSTRUCTOR
  22. /**
  23. * Utility class for parseObject() below. isDocumentOk indicates that it is OK to use a Document in the current context.
  24. *
  25. * NOTE: deviation from Mongo code: accepts an `Object` of settings rather than a bitmask to help simplify the interface a little bit
  26. *
  27. * @class ObjectCtx
  28. * @namespace mungedb-aggregate.pipeline.expressions.Expression
  29. * @module mungedb-aggregate
  30. * @constructor
  31. * @param opts
  32. * @param [opts.isDocumentOk] {Boolean}
  33. * @param [opts.isTopLevel] {Boolean}
  34. * @param [opts.isInclusionOk] {Boolean}
  35. */
  36. var klass = function ObjectCtx(opts) {
  37. if (Value.getType(opts) !== "Object") throw new Error("opts is required and must be an Object containing named args");
  38. for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
  39. if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
  40. }
  41. }, proto = klass.prototype;
  42. // PROTOTYPE MEMBERS
  43. proto.isDocumentOk =
  44. proto.isTopLevel =
  45. proto.isInclusionOk = undefined;
  46. return klass;
  47. })();
  48. //
  49. // Diagram of relationship between parse functions when parsing a $op:
  50. //
  51. // { someFieldOrArrayIndex: { $op: [ARGS] } }
  52. // ^ parseExpression on inner $op BSONElement
  53. // ^ parseObject on BSONObject
  54. // ^ parseOperand on outer BSONElement wrapping the $op Object
  55. //
  56. /**
  57. * Parses a JSON Object that could represent a functional expression or a Document expression.
  58. * @method parseObject
  59. * @static
  60. * @param obj the element representing the object
  61. * @param ctx a MiniCtx representing the options above
  62. * @param vps Variables Parse State
  63. * @returns the parsed Expression
  64. */
  65. klass.parseObject = function parseObject(obj, ctx, vps) { //jshint maxcomplexity:21
  66. if (!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
  67. /**
  68. * An object expression can take any of the following forms:
  69. *
  70. * f0: {f1: ..., f2: ..., f3: ...}
  71. * f0: {$operator:[operand1, operand2, ...]}
  72. */
  73. var expression, // the result
  74. expressionObject, // the alt result
  75. UNKNOWN = 0,
  76. NOTOPERATOR = 1,
  77. OPERATOR = 2,
  78. kind = UNKNOWN;
  79. if (obj === undefined || obj === null || (obj instanceof Object && Object.keys(obj).length === 0)) return new ObjectExpression({});
  80. var fieldNames = Object.keys(obj);
  81. for (var fieldCount = 0, n = fieldNames.length; fieldCount < n; ++fieldCount) {
  82. var fieldName = fieldNames[fieldCount];
  83. if (fieldName[0] === "$") {
  84. if (fieldCount !== 0)
  85. throw new Error("the operator must be the only field in a pipeline object (at '" + fieldName + "'.; uassert code 15983");
  86. if (ctx.isTopLevel)
  87. throw new Error("$expressions are not allowed at the top-level of $project; uassert code 16404");
  88. // we've determined this "object" is an operator expression
  89. kind = OPERATOR;
  90. expression = Expression.parseExpression(fieldName, obj[fieldName], vps); //NOTE: DEVIATION FROM MONGO: see #parseExpression api
  91. } else {
  92. if (kind === OPERATOR)
  93. throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" +
  94. fieldName + "'.; uassert code 15990");
  95. if (!ctx.isTopLevel && fieldName.indexOf(".") !== -1)
  96. throw new Error("dotted field names are only allowed at the top level; uassert code 16405");
  97. // if it's our first time, create the document expression
  98. if (expression === undefined) {
  99. if (!ctx.isDocumentOk) throw new Error("Assertion failure");
  100. // CW TODO error: document not allowed in this context
  101. expressionObject = ctx.isTopLevel ? ObjectExpression.createRoot() : ObjectExpression.create();
  102. expression = expressionObject;
  103. // this "object" is not an operator expression
  104. kind = NOTOPERATOR;
  105. }
  106. var fieldValue = obj[fieldName];
  107. switch (typeof(fieldValue)) {
  108. case "object":
  109. // it's a nested document
  110. var subCtx = new ObjectCtx({
  111. isDocumentOk: ctx.isDocumentOk,
  112. isInclusionOk: ctx.isInclusionOk
  113. });
  114. expressionObject.addField(fieldName, Expression.parseObject(fieldValue, subCtx, vps));
  115. break;
  116. case "string":
  117. // it's a renamed field
  118. // CW TODO could also be a constant
  119. expressionObject.addField(fieldName, FieldPathExpression.parse(fieldValue, vps));
  120. break;
  121. case "boolean":
  122. case "number":
  123. // it's an inclusion specification
  124. if (fieldValue) {
  125. if (!ctx.isInclusionOk)
  126. throw new Error("field inclusion is not allowed inside of $expressions; uassert code 16420");
  127. expressionObject.includePath(fieldName);
  128. } else {
  129. if (!(ctx.isTopLevel && fieldName === Document.ID_PROPERTY_NAME))
  130. throw new Error("The top-level " + Document.ID_PROPERTY_NAME +
  131. " field is the only field currently supported for exclusion; uassert code 16406");
  132. expressionObject.excludeId = true;
  133. }
  134. break;
  135. default:
  136. throw new Error("disallowed field type " + Value.getType(fieldValue) +
  137. " in object expression (at '" + fieldName + "') uassert code 15992");
  138. }
  139. }
  140. }
  141. return expression;
  142. };
  143. klass.expressionParserMap = {};
  144. /**
  145. * Registers an ExpressionParser so it can be called from parseExpression and friends.
  146. * As an example, if your expression looks like {"$foo": [1,2,3]} you would add this line:
  147. * REGISTER_EXPRESSION("$foo", ExpressionFoo::parse);
  148. */
  149. klass.registerExpression = function registerExpression(key, parserFunc) {
  150. if (key in klass.expressionParserMap)
  151. throw new Error("Duplicate expression (" + key + ") detected; massert code 17064");
  152. klass.expressionParserMap[key] = parserFunc;
  153. return 1;
  154. };
  155. //NOTE: DEVIATION FROM MONGO: the c++ version has 2 arguments, not 3. //TODO: could easily fix this inconsistency
  156. /**
  157. * Parses a BSONElement which has already been determined to be functional expression.
  158. * @static
  159. * @method parseExpression
  160. * @param exprElement should be the only element inside the expression object.
  161. * That is the field name should be the $op for the expression.
  162. * @param vps the variable parse state
  163. * @returns the parsed Expression
  164. */
  165. klass.parseExpression = function parseExpression(exprElementKey, exprElementValue, vps) {
  166. var opName = exprElementKey,
  167. op = Expression.expressionParserMap[opName];
  168. if (!op) throw new Error("invalid operator : " + exprElementKey + "; uassert code 15999");
  169. // make the expression node
  170. return op(exprElementValue, vps);
  171. };
  172. /**
  173. * Parses a BSONElement which is an operand in an Expression.
  174. *
  175. * This is the most generic parser and can parse ExpressionFieldPath, a literal, or a $op.
  176. * If it is a $op, exprElement should be the outer element whose value is an Object
  177. * containing the $op.
  178. *
  179. * @method parseOperand
  180. * @static
  181. * @param exprElement should be the only element inside the expression object.
  182. * That is the field name should be the $op for the expression.
  183. * @param vps the variable parse state
  184. * @returns the parsed operand, as an Expression
  185. */
  186. klass.parseOperand = function parseOperand(exprElement, vps) {
  187. var t = typeof(exprElement);
  188. if (t === "string" && exprElement[0] === "$") {
  189. //if we got here, this is a field path expression
  190. return FieldPathExpression.parse(exprElement, vps);
  191. } else if (t === "object" && exprElement && exprElement.constructor === Object) {
  192. var oCtx = new ObjectCtx({
  193. isDocumentOk: true
  194. });
  195. return Expression.parseObject(exprElement, oCtx, vps);
  196. } else {
  197. return ConstantExpression.parse(exprElement, vps);
  198. }
  199. };
  200. /**
  201. * Optimize the Expression.
  202. *
  203. * This provides an opportunity to do constant folding, or to collapse nested
  204. * operators that have the same precedence, such as $add, $and, or $or.
  205. *
  206. * The Expression should be replaced with the return value, which may or may
  207. * not be the same object. In the case of constant folding, a computed
  208. * expression may be replaced by a constant.
  209. *
  210. * @method optimize
  211. * @returns the optimized Expression
  212. */
  213. proto.optimize = function optimize() {
  214. return this;
  215. };
  216. /**
  217. * Add this expression's field dependencies to the set.
  218. * Expressions are trees, so this is often recursive.
  219. *
  220. * @method addDependencies
  221. * @param deps Fully qualified paths to depended-on fields are added to this set.
  222. * Empty string means need full document.
  223. * @param path path to self if all ancestors are ExpressionObjects.
  224. * Top-level ExpressionObject gets pointer to empty vector.
  225. * If any other Expression is an ancestor, or in other cases
  226. * where {a:1} inclusion objects aren't allowed, they get
  227. * NULL.
  228. */
  229. proto.addDependencies = function addDependencies(deps, path) { //jshint ignore:line
  230. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
  231. };
  232. /**
  233. * simple expressions are just inclusion exclusion as supported by ExpressionObject
  234. * @method isSimple
  235. */
  236. proto.isSimple = function isSimple() {
  237. return false;
  238. };
  239. /**
  240. * Serialize the Expression tree recursively.
  241. * If explain is false, returns a Value parsable by parseOperand().
  242. * @method serialize
  243. */
  244. proto.serialize = function serialize(explain) { //jshint ignore:line
  245. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
  246. };
  247. /**
  248. * Evaluate expression with specified inputs and return result.
  249. *
  250. * While vars is non-const, if properly constructed, subexpressions modifications to it
  251. * should not effect outer expressions due to unique variable Ids.
  252. *
  253. * @method evaluate
  254. * @param vars
  255. */
  256. proto.evaluate = function evaluate(vars) {
  257. if (vars instanceof Object && vars.constructor === Object) {
  258. /// Evaluate expression with specified inputs and return result. (only used by tests)
  259. vars = new Variables(0, vars);
  260. }
  261. return this.evaluateInternal(vars);
  262. };
  263. /**
  264. * Produce a field path string with the field prefix removed.
  265. * Throws an error if the field prefix is not present.
  266. * @method removeFieldPrefix
  267. * @static
  268. * @param prefixedField the prefixed field
  269. * @returns the field path with the prefix removed
  270. */
  271. klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
  272. if (prefixedField.indexOf("\0") !== -1)
  273. throw new Error("field path must not contain embedded null characters; uassert code 16419");
  274. if (prefixedField[0] !== "$")
  275. throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); uassert code 15982");
  276. return prefixedField.substr(1);
  277. };
  278. /**
  279. * Evaluate the subclass Expression using the given Variables as context and return result.
  280. * @method evaluate
  281. * @returns the computed value
  282. */
  283. proto.evaluateInternal = function evaluateInternal(vars) { //jshint ignore:line
  284. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
  285. };
  286. //NOTE: these must be located after klass statics have been setup
  287. var ObjectExpression = require("./ObjectExpression"), //jshint ignore:line
  288. FieldPathExpression = require("./FieldPathExpression"), //jshint ignore:line
  289. ConstantExpression = require("./ConstantExpression"); //jshint ignore:line