Expression.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. var Expression = module.exports = (function(){
  2. // CONSTRUCTOR
  3. /** A base class for all pipeline expressions; Performs common expressions within an Op.
  4. | NOTE:
  5. | An object expression can take any of the following forms:
  6. | f0: {f1: ..., f2: ..., f3: ...}
  7. | f0: {$operator:[operand1, operand2, ...]}
  8. **/
  9. var klass = module.exports = Expression = function Expression(opts){
  10. if(arguments.length !== 0) throw new Error("zero args expected");
  11. }, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  12. // NESTED CLASSES
  13. /** Utility class for parseObject() below. isDocumentOk indicates that it is OK to use a Document in the current context. **/
  14. var ObjectCtx = Expression.ObjectCtx = (function(){
  15. // CONSTRUCTOR
  16. var klass = function ObjectCtx(opts /*= {isDocumentOk:..., isTopLevel:..., isInclusionOk:...}*/){
  17. if(!(opts instanceof Object && opts.constructor == Object)) throw new Error("opts is required and must be an Object containing named args");
  18. for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
  19. if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
  20. }
  21. }, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  22. proto.isDocumentOk =
  23. proto.isTopLevel =
  24. proto.isInclusionOk = undefined;
  25. return klass;
  26. })();
  27. /** Decribes how and when to create an Op instance **/
  28. var OpDesc = (function(){
  29. // CONSTRUCTOR
  30. var klass = function OpDesc(name, factory, flags, argCount){
  31. if (arguments[0] instanceof Object && arguments[0].constructor == Object) { //TODO: using this?
  32. var opts = arguments[0];
  33. for (var k in opts) { // assign all given opts to self so long as they were part of klass.prototype as undefined properties
  34. if (opts.hasOwnProperty(k) && proto.hasOwnProperty(k) && proto[k] === undefined) this[k] = opts[k];
  35. }
  36. } else {
  37. this.name = name;
  38. this.factory = factory;
  39. this.flags = flags || 0;
  40. this.argCount = argCount || 0;
  41. }
  42. }, base = Object, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  43. // STATIC MEMBERS
  44. klass.FIXED_COUNT = 1;
  45. klass.OBJECT_ARG = 2;
  46. // PROTOTYPE MEMBERS
  47. proto.name =
  48. proto.factory =
  49. proto.flags =
  50. proto.argCount = undefined;
  51. proto.cmp = function cmp(that) {
  52. return this.name < that.name ? -1 : this.name > that.name ? 1 : 0;
  53. };
  54. return klass;
  55. })();
  56. var kinds = {
  57. UNKNOWN: "UNKNOWN",
  58. OPERATOR: "OPERATOR",
  59. NOT_OPERATOR: "NOT_OPERATOR"
  60. };
  61. // STATIC MEMBERS
  62. /** Enumeration of comparison operators. These are shared between a few expression implementations, so they are factored out here. **/
  63. klass.CmpOp = {
  64. EQ: "$eq", // return true for a == b, false otherwise
  65. NE: "$ne", // return true for a != b, false otherwise
  66. GT: "$gt", // return true for a > b, false otherwise
  67. GTE: "$gte", // return true for a >= b, false otherwise
  68. LT: "$lt", // return true for a < b, false otherwise
  69. LTE: "$lte", // return true for a <= b, false otherwise
  70. CMP: "$cmp" // return -1, 0, 1 for a < b, a == b, a > b
  71. };
  72. // DEPENDENCIES (later in this file as compared to others to ensure that statics are setup first)
  73. var FieldPathExpression = require("./FieldPathExpression"),
  74. ObjectExpression = require("./ObjectExpression"),
  75. ConstantExpression = require("./ConstantExpression"),
  76. CompareExpression = require("./CompareExpression");
  77. // DEFERRED DEPENDENCIES
  78. /** Expressions, as exposed to users **/
  79. process.nextTick(function(){ // Even though `opMap` is deferred, force it to load early rather than later to prevent even *more* potential silliness
  80. Object.defineProperty(klass, "opMap", {value:klass.opMap});
  81. });
  82. Object.defineProperty(klass, "opMap", { //NOTE: deferred requires using a getter to allow circular requires (to maintain the ported API)
  83. configurable: true,
  84. /**
  85. * Autogenerated docs! Please modify if you you touch this method
  86. *
  87. * @method get
  88. **/
  89. get: function getOpMapOnce() {
  90. return Object.defineProperty(klass, "opMap", {
  91. value: [ //NOTE: rather than OpTable because it gets converted to a dict via OpDesc#name in the Array#reduce() below
  92. new OpDesc("$add", require("./AddExpression"), 0),
  93. new OpDesc("$and", require("./AndExpression"), 0),
  94. new OpDesc("$cmp", CompareExpression.bind(null, Expression.CmpOp.CMP), OpDesc.FIXED_COUNT, 2),
  95. new OpDesc("$cond", require("./CondExpression"), OpDesc.FIXED_COUNT, 3),
  96. // $const handled specially in parseExpression
  97. new OpDesc("$dayOfMonth", require("./DayOfMonthExpression"), OpDesc.FIXED_COUNT, 1),
  98. new OpDesc("$dayOfWeek", require("./DayOfWeekExpression"), OpDesc.FIXED_COUNT, 1),
  99. new OpDesc("$dayOfYear", require("./DayOfYearExpression"), OpDesc.FIXED_COUNT, 1),
  100. new OpDesc("$divide", require("./DivideExpression"), OpDesc.FIXED_COUNT, 2),
  101. new OpDesc("$eq", CompareExpression.bind(null, Expression.CmpOp.EQ), OpDesc.FIXED_COUNT, 2),
  102. new OpDesc("$gt", CompareExpression.bind(null, Expression.CmpOp.GT), OpDesc.FIXED_COUNT, 2),
  103. new OpDesc("$gte", CompareExpression.bind(null, Expression.CmpOp.GTE), OpDesc.FIXED_COUNT, 2),
  104. new OpDesc("$hour", require("./HourExpression"), OpDesc.FIXED_COUNT, 1),
  105. new OpDesc("$ifNull", require("./IfNullExpression"), OpDesc.FIXED_COUNT, 2),
  106. new OpDesc("$lt", CompareExpression.bind(null, Expression.CmpOp.LT), OpDesc.FIXED_COUNT, 2),
  107. new OpDesc("$lte", CompareExpression.bind(null, Expression.CmpOp.LTE), OpDesc.FIXED_COUNT, 2),
  108. new OpDesc("$minute", require("./MinuteExpression"), OpDesc.FIXED_COUNT, 1),
  109. new OpDesc("$mod", require("./ModExpression"), OpDesc.FIXED_COUNT, 2),
  110. new OpDesc("$month", require("./MonthExpression"), OpDesc.FIXED_COUNT, 1),
  111. new OpDesc("$multiply", require("./MultiplyExpression"), 0),
  112. new OpDesc("$ne", CompareExpression.bind(null, Expression.CmpOp.NE), OpDesc.FIXED_COUNT, 2),
  113. new OpDesc("$not", require("./NotExpression"), OpDesc.FIXED_COUNT, 1),
  114. new OpDesc("$or", require("./OrExpression"), 0),
  115. new OpDesc("$second", require("./SecondExpression"), OpDesc.FIXED_COUNT, 1),
  116. new OpDesc("$strcasecmp", require("./StrcasecmpExpression"), OpDesc.FIXED_COUNT, 2),
  117. new OpDesc("$substr", require("./SubstrExpression"), OpDesc.FIXED_COUNT, 3),
  118. new OpDesc("$subtract", require("./SubtractExpression"), OpDesc.FIXED_COUNT, 2),
  119. new OpDesc("$toLower", require("./ToLowerExpression"), OpDesc.FIXED_COUNT, 1),
  120. new OpDesc("$toUpper", require("./ToUpperExpression"), OpDesc.FIXED_COUNT, 1),
  121. new OpDesc("$week", require("./WeekExpression"), OpDesc.FIXED_COUNT, 1),
  122. new OpDesc("$year", require("./YearExpression"), OpDesc.FIXED_COUNT, 1)
  123. ].reduce(function(r,o){r[o.name]=o; return r;}, {})
  124. }).opMap;
  125. }
  126. });
  127. /**
  128. * Parse an Object. The object could represent a functional expression or a Document expression.
  129. *
  130. * @param obj the element representing the object
  131. * @param ctx a MiniCtx representing the options above
  132. * @returns the parsed Expression
  133. *
  134. * An object expression can take any of the following forms:
  135. * f0: {f1: ..., f2: ..., f3: ...}
  136. * f0: {$operator:[operand1, operand2, ...]}
  137. **/
  138. klass.parseObject = function parseObject(obj, ctx){
  139. if(!(ctx instanceof ObjectCtx)) throw new Error("ctx must be ObjectCtx");
  140. var kind = kinds.UNKNOWN,
  141. expr, // the result
  142. exprObj; // the alt result
  143. if (obj === undefined) return new ObjectExpression();
  144. var fieldNames = Object.getOwnPropertyNames(obj);
  145. for (var fc = 0, n = fieldNames.length; fc < n; ++fc) {
  146. var fn = fieldNames[fc];
  147. if (fn[0] === "$") {
  148. if (fc !== 0) throw new Error("the operator must be the only field in a pipeline object (at '" + fn + "'.; code 16410");
  149. if(ctx.isTopLevel) throw new Error("$expressions are not allowed at the top-level of $project; code 16404");
  150. kind = kinds.OPERATOR; //we've determined this "object" is an operator expression
  151. expr = Expression.parseExpression(fn, obj[fn]);
  152. } else {
  153. if (kind === kinds.OPERATOR) throw new Error("this object is already an operator expression, and can't be used as a document expression (at '" + fn + "'.; code 15990");
  154. if (!ctx.isTopLevel && fn.indexOf(".") != -1) throw new Error("dotted field names are only allowed at the top level; code 16405");
  155. if (expr === undefined) { // if it's our first time, create the document expression
  156. if (!ctx.isDocumentOk) throw new Error("document not allowed in this context"); // CW TODO error: document not allowed in this context
  157. expr = exprObj = new ObjectExpression();
  158. kind = kinds.NOT_OPERATOR; //this "object" is not an operator expression
  159. }
  160. var fv = obj[fn];
  161. switch (typeof(fv)) {
  162. case "object":
  163. // it's a nested document
  164. var subCtx = new ObjectCtx({
  165. isDocumentOk: ctx.isDocumentOk,
  166. isInclusionOk: ctx.isInclusionOk
  167. });
  168. exprObj.addField(fn, Expression.parseObject(fv, subCtx));
  169. break;
  170. case "string":
  171. // it's a renamed field // CW TODO could also be a constant
  172. var pathExpr = new FieldPathExpression(Expression.removeFieldPrefix(fv));
  173. exprObj.addField(fn, pathExpr);
  174. break;
  175. case "boolean":
  176. case "number":
  177. // it's an inclusion specification
  178. if (fv) {
  179. if (!ctx.isInclusionOk) throw new Error("field inclusion is not allowed inside of $expressions; code 16420");
  180. exprObj.includePath(fn);
  181. } else {
  182. if (!(ctx.isTopLevel && fn == "_id")) throw new Error("The top-level _id field is the only field currently supported for exclusion; code 16406");
  183. exprObj.excludeId(true);
  184. }
  185. break;
  186. default:
  187. throw new Error("disallowed field type " + (fv ? fv.constructor.name + ":" : "") + typeof(fv) + " in object expression (at '" + fn + "')");
  188. }
  189. }
  190. }
  191. return expr;
  192. };
  193. /**
  194. * Parse a BSONElement Object which has already been determined to be functional expression.
  195. *
  196. * @param opName the name of the (prefix) operator
  197. * @param obj the BSONElement to parse
  198. * @returns the parsed Expression
  199. **/
  200. klass.parseExpression = function parseExpression(opName, obj) {
  201. // look for the specified operator
  202. if (opName === "$const") return new ConstantExpression(obj); //TODO: createFromBsonElement was here, not needed since this isn't BSON?
  203. var op = klass.opMap[opName];
  204. if (!(op instanceof OpDesc)) throw new Error("invalid operator " + opName + "; code 15999");
  205. // make the expression node
  206. var IExpression = op.factory, //TODO: should this get renamed from `factory` to `ctor` or something?
  207. expr = new IExpression();
  208. // add the operands to the expression node
  209. if (op.flags & OpDesc.FIXED_COUNT && op.argCount > 1 && !(obj instanceof Array)) throw new Error("the " + op.name + " operator requires an array of " + op.argCount + " operands; code 16019");
  210. var operand; // used below
  211. if (obj.constructor === Object) { // the operator must be unary and accept an object argument
  212. if (!(op.flags & OpDesc.OBJECT_ARG)) throw new Error("the " + op.name + " operator does not accept an object as an operand");
  213. operand = Expression.parseObject(obj, new ObjectCtx({isDocumentOk: 1}));
  214. expr.addOperand(operand);
  215. } else if (obj instanceof Array) { // multiple operands - an n-ary operator
  216. if (op.flags & OpDesc.FIXED_COUNT && op.argCount !== obj.length) throw new Error("the " + op.name + " operator requires " + op.argCount + " operand(s); code 16020");
  217. for (var i = 0, n = obj.length; i < n; ++i) {
  218. operand = Expression.parseOperand(obj[i]);
  219. expr.addOperand(operand);
  220. }
  221. } else { //assume it's an atomic operand
  222. if (op.flags & OpDesc.FIXED_COUNT && op.argCount != 1) throw new Error("the " + op.name + " operator requires an array of " + op.argCount + " operands; code 16022");
  223. operand = Expression.parseOperand(obj);
  224. expr.addOperand(operand);
  225. }
  226. return expr;
  227. };
  228. /**
  229. * Parse a BSONElement which is an operand in an Expression.
  230. *
  231. * @param pBsonElement the expected operand's BSONElement
  232. * @returns the parsed operand, as an Expression
  233. **/
  234. klass.parseOperand = function parseOperand(obj){
  235. var t = typeof(obj);
  236. if (t === "string" && obj[0] == "$") { //if we got here, this is a field path expression
  237. var path = Expression.removeFieldPrefix(obj);
  238. return new FieldPathExpression(path);
  239. }
  240. else if (t === "object" && obj.constructor === Object) return Expression.parseObject(obj, new ObjectCtx({isDocumentOk: true}));
  241. else return new ConstantExpression(obj);
  242. };
  243. /**
  244. * Produce a field path string with the field prefix removed.
  245. * Throws an error if the field prefix is not present.
  246. *
  247. * @param prefixedField the prefixed field
  248. * @returns the field path with the prefix removed
  249. **/
  250. klass.removeFieldPrefix = function removeFieldPrefix(prefixedField) {
  251. if (prefixedField.indexOf("\0") != -1) throw new Error("field path must not contain embedded null characters; code 16419");
  252. if (prefixedField[0] !== "$") throw new Error("field path references must be prefixed with a '$' ('" + prefixedField + "'); code 15982");
  253. return prefixedField.substr(1);
  254. };
  255. /** @returns the sign of a number; -1, 1, or 0 **/
  256. klass.signum = function signum(i) {
  257. if (i < 0) return -1;
  258. if (i > 0) return 1;
  259. return 0;
  260. };
  261. // PROTOTYPE MEMBERS
  262. /***
  263. * Evaluate the Expression using the given document as input.
  264. *
  265. * @returns the computed value
  266. ***/
  267. proto.evaluate = function evaluate(obj) {
  268. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
  269. };
  270. /**
  271. * Optimize the Expression.
  272. *
  273. * This provides an opportunity to do constant folding, or to collapse nested
  274. * operators that have the same precedence, such as $add, $and, or $or.
  275. *
  276. * The Expression should be replaced with the return value, which may or may
  277. * not be the same object. In the case of constant folding, a computed
  278. * expression may be replaced by a constant.
  279. *
  280. * @returns the optimized Expression
  281. **/
  282. proto.optimize = function optimize() {
  283. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
  284. };
  285. /**
  286. * Add this expression's field dependencies to the set Expressions are trees, so this is often recursive.
  287. *
  288. * Top-level ExpressionObject gets pointer to empty vector.
  289. * If any other Expression is an ancestor, or in other cases where {a:1} inclusion objects aren't allowed, they get NULL.
  290. *
  291. * @param deps output parameter
  292. * @param path path to self if all ancestors are ExpressionObjects.
  293. **/
  294. proto.addDependencies = function addDependencies(deps, path) {
  295. throw new Error("WAS NOT IMPLEMENTED BY INHERITOR!");
  296. };
  297. /** simple expressions are just inclusion exclusion as supported by ExpressionObject **/
  298. proto.getIsSimple = function getIsSimple() {
  299. return false;
  300. };
  301. return klass;
  302. })();