Expression.js 11 KB

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