Expression.js 16 KB

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