Expression.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  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. function fn(){
  25. return;
  26. }
  27. // NESTED CLASSES
  28. /**
  29. * Reference to the `mungedb-aggregate.pipeline.expressions.Expression.ObjectCtx` class
  30. * @static
  31. * @property ObjectCtx
  32. **/
  33. var ObjectCtx = Expression.ObjectCtx = (function() {
  34. // CONSTRUCTOR
  35. /**
  36. * Utility class for parseObject() below. isDocumentOk indicates that it is OK to use a Document in the current context.
  37. *
  38. * NOTE: deviation from Mongo code: accepts an `Object` of settings rather than a bitmask to help simplify the interface a little bit
  39. *
  40. * @class ObjectCtx
  41. * @namespace mungedb-aggregate.pipeline.expressions.Expression
  42. * @module mungedb-aggregate
  43. * @constructor
  44. * @param opts
  45. * @param [opts.isDocumentOk] {Boolean}
  46. * @param [opts.isTopLevel] {Boolean}
  47. * @param [opts.isInclusionOk] {Boolean}
  48. **/
  49. var klass = function ObjectCtx(opts /*= {isDocumentOk:..., isTopLevel:..., isInclusionOk:...}*/ ) {
  50. if (!(opts instanceof Object && opts.constructor == Object)) throw new Error("opts is required and must be an Object containing named args");
  51. for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
  52. if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
  53. }
  54. }, base = Object,
  55. proto = klass.prototype = Object.create(base.prototype, {
  56. constructor: {
  57. value: klass
  58. }
  59. });
  60. // PROTOTYPE MEMBERS
  61. proto.isDocumentOk =
  62. proto.isTopLevel =
  63. proto.isInclusionOk = undefined;
  64. return klass;
  65. })();
  66. proto.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
  67. if (prefixedField.indexOf("\0") !== -1) {
  68. // field path must not contain embedded null characters - 16419
  69. }
  70. if (prefixedField[0] !== '$') {
  71. // "field path references must be prefixed with a '$'"
  72. }
  73. return prefixedField.slice(1);
  74. };
  75. var KIND_UNKNOWN = 0,
  76. KIND_NOTOPERATOR = 1,
  77. KIND_OPERATOR = 2;
  78. /**
  79. * Parse an Object. The object could represent a functional expression or a Document expression.
  80. *
  81. * An object expression can take any of the following forms:
  82. *
  83. * f0: {f1: ..., f2: ..., f3: ...}
  84. * f0: {$operator:[operand1, operand2, ...]}
  85. *
  86. * @static
  87. * @method parseObject
  88. * @param obj the element representing the object
  89. * @param ctx a MiniCtx representing the options above
  90. * @returns the parsed Expression
  91. **/
  92. klass.parseObject = function parseObject(obj, ctx, vps) {
  93. if (!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
  94. var kind = KIND_UNKNOWN,
  95. pExpression, // the result
  96. pExpressionObject; // the alt result
  97. if (obj === undefined || obj == {}) return new ObjectExpression();
  98. var fieldNames = Object.keys(obj);
  99. if (fieldNames.length === 0) { //NOTE: Added this for mongo 2.5 port of document sources. Should reconsider when porting the expressions themselves
  100. return new ObjectExpression();
  101. }
  102. for (var fieldCount = 0, n = fieldNames.length; fieldCount < n; ++fieldCount) {
  103. var pFieldName = fieldNames[fieldCount];
  104. if (pFieldName[0] === "$") {
  105. if (fieldCount !== 0)
  106. throw new Error("the operator must be the only field in a pipeline object (at '" + pFieldName + "'.; code 16410");
  107. if (ctx.isTopLevel)
  108. throw new Error("$expressions are not allowed at the top-level of $project; code 16404");
  109. kind = KIND_OPERATOR; //we've determined this "object" is an operator expression
  110. pExpression = Expression.parseExpression(pFieldName, obj[pFieldName], vps);
  111. } else {
  112. if (kind === KIND_OPERATOR)
  113. throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + pFieldName + "'.; code 15990");
  114. if (!ctx.isTopLevel && pFieldName.indexOf(".") != -1)
  115. throw new Error("dotted field names are only allowed at the top level; code 16405");
  116. if (pExpression === undefined) { // if it's our first time, create the document expression
  117. if (!ctx.isDocumentOk)
  118. throw new Error("document not allowed in this context"); // CW TODO error: document not allowed in this context
  119. pExpression = pExpressionObject = new ObjectExpression(); //check for top level?
  120. kind = KIND_NOTOPERATOR; //this "object" is not an operator expression
  121. }
  122. var fieldValue = obj[pFieldName];
  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. pExpressionObject.addField(pFieldName, Expression.parseObject(fieldValue, subCtx, vps));
  131. break;
  132. case "string":
  133. // it's a renamed field // CW TODO could also be a constant
  134. var pathExpr = new FieldPathExpression.parse(fieldValue);
  135. pExpressionObject.addField(pFieldName, pathExpr);
  136. break;
  137. case "boolean":
  138. case "number":
  139. // it's an inclusion specification
  140. if (fieldValue) {
  141. if (!ctx.isInclusionOk)
  142. throw new Error("field inclusion is not allowed inside of $expressions; code 16420");
  143. pExpressionObject.includePath(pFieldName);
  144. } else {
  145. if (!(ctx.isTopLevel && fn == Document.ID_PROPERTY_NAME))
  146. throw new Error("The top-level " + Document.ID_PROPERTY_NAME + " field is the only field currently supported for exclusion; code 16406");
  147. pExpressionObject.excludeId = true;
  148. }
  149. break;
  150. default:
  151. throw new Error("disallowed field type " + (fieldValue ? fieldValue.constructor.name + ":" : "") + typeof(fieldValue) + " in object expression (at '" + pFieldName + "')");
  152. }
  153. }
  154. }
  155. return pExpression;
  156. };
  157. klass.expressionParserMap = {};
  158. klass.registerExpression = function registerExpression(key, parserFunc) {
  159. if (key in klass.expressionParserMap) {
  160. throw new Error("Duplicate expression registrarion for " + key);
  161. }
  162. klass.expressionParserMap[key] = parserFunc;
  163. return 0; // Should
  164. };
  165. /**
  166. * Parse a BSONElement Object which has already been determined to be functional expression.
  167. *
  168. * @static
  169. * @method parseExpression
  170. * @param opName the name of the (prefix) operator
  171. * @param obj the BSONElement to parse
  172. * @returns the parsed Expression
  173. **/
  174. klass.parseExpression = function parseExpression(exprKey, exprValue, vps) {
  175. if (!(exprKey in Expression.expressionParserMap)) {
  176. throw new Error("Invalid operator : " + exprKey);
  177. }
  178. return Expression.expressionParserMap[exprKey](exprValue, vps);
  179. };
  180. /**
  181. * Parse a BSONElement which is an operand in an Expression.
  182. *
  183. * @static
  184. * @param pBsonElement the expected operand's BSONElement
  185. * @returns the parsed operand, as an Expression
  186. **/
  187. klass.parseOperand = function parseOperand(exprElement, vps) {
  188. var t = typeof(exprElement);
  189. if (t === "string" && exprElement[0] == "$") { //if we got here, this is a field path expression
  190. return new FieldPathExpression.parse(exprElement, vps);
  191. } else
  192. if (t === "object" && exprElement && exprElement.constructor === Object)
  193. return Expression.parseObject(exprElement, new ObjectCtx({
  194. isDocumentOk: true
  195. }), vps);
  196. else return ConstantExpression.parse(exprElement, vps);
  197. };
  198. /**
  199. * Produce a field path string with the field prefix removed.
  200. * Throws an error if the field prefix is not present.
  201. *
  202. * @static
  203. * @param prefixedField the prefixed field
  204. * @returns the field path with the prefix removed
  205. **/
  206. klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
  207. if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; code 16419");
  208. if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); code 15982");
  209. return prefixedField.substr(1);
  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. * Optimize the Expression.
  223. *
  224. * This provides an opportunity to do constant folding, or to collapse nested
  225. * operators that have the same precedence, such as $add, $and, or $or.
  226. *
  227. * The Expression should be replaced with the return value, which may or may
  228. * not be the same object. In the case of constant folding, a computed
  229. * expression may be replaced by a constant.
  230. *
  231. * @method optimize
  232. * @returns the optimized Expression
  233. **/
  234. proto.optimize = function optimize() {
  235. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
  236. };
  237. /**
  238. * Add this expression's field dependencies to the set Expressions are trees, so this is often recursive.
  239. *
  240. * Top-level ExpressionObject gets pointer to empty vector.
  241. * If any other Expression is an ancestor, or in other cases where {a:1} inclusion objects aren't allowed, they get NULL.
  242. *
  243. * @method addDependencies
  244. * @param deps output parameter
  245. * @param path path to self if all ancestors are ExpressionObjects.
  246. **/
  247. proto.addDependencies = function addDependencies(deps, path) {
  248. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
  249. };
  250. /**
  251. * simple expressions are just inclusion exclusion as supported by ExpressionObject
  252. * @method getIsSimple
  253. **/
  254. proto.getIsSimple = function getIsSimple() {
  255. return false;
  256. };
  257. proto.toMatcherBson = function toMatcherBson() {
  258. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!"); //verify(false && "Expression::toMatcherBson()");
  259. };
  260. // DEPENDENCIES
  261. var Document = require("../Document");
  262. var ObjectExpression = require("./ObjectExpression");
  263. var FieldPathExpression = require("./FieldPathExpression");
  264. var ConstantExpression = require("./ConstantExpression");