ObjectExpression.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. "use strict";
  2. /**
  3. * Create an empty expression. Until fields are added, this will evaluateInternal to an empty document (object).
  4. * @class ObjectExpression
  5. * @namespace mungedb-aggregate.pipeline.expressions
  6. * @module mungedb-aggregate
  7. * @extends mungedb-aggregate.pipeline.expressions.Expression
  8. * @constructor
  9. */
  10. var ObjectExpression = module.exports = function ObjectExpression(atRoot) {
  11. if (arguments.length !== 1) throw new Error(klass.name + ": expected args: atRoot");
  12. this.excludeId = false;
  13. this._atRoot = atRoot;
  14. this._expressions = {};
  15. this._order = [];
  16. }, klass = ObjectExpression, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}}); //jshint ignore:line
  17. var Document = require("../Document"),
  18. Value = require("../Value"),
  19. FieldPath = require("../FieldPath"),
  20. ConstantExpression = require("./ConstantExpression");
  21. /**
  22. * Create an empty expression.
  23. * Until fields are added, this will evaluate to an empty document.
  24. * @method create
  25. * @static
  26. */
  27. klass.create = function create() {
  28. return new ObjectExpression(false);
  29. };
  30. /**
  31. * Like create but uses special handling of _id for root object of $project.
  32. * @method createRoot
  33. * @static
  34. */
  35. klass.createRoot = function createRoot() {
  36. return new ObjectExpression(true);
  37. };
  38. proto.optimize = function optimize() {
  39. for (var key in this._expressions) { //jshint ignore:line
  40. if (!this._expressions.hasOwnProperty(key)) continue;
  41. var expr = this._expressions[key];
  42. if (expr)
  43. this._expressions[key] = expr.optimize();
  44. }
  45. return this;
  46. };
  47. proto.isSimple = function isSimple() {
  48. for (var key in this._expressions) { //jshint ignore:line
  49. if (!this._expressions.hasOwnProperty(key)) continue;
  50. var expr = this._expressions[key];
  51. if (expr && !expr.isSimple())
  52. return false;
  53. }
  54. return true;
  55. };
  56. proto.addDependencies = function addDependencies(deps, path) {
  57. var pathStr = "";
  58. if (path) {
  59. if (path.length === 0) {
  60. // we are in the top level of a projection so _id is implicit
  61. if (!this.excludeId)
  62. deps.fields[Document.ID_PROPERTY_NAME] = 1;
  63. } else {
  64. var f = new FieldPath(path);
  65. pathStr = f.getPath(false);
  66. pathStr += ".";
  67. }
  68. } else {
  69. if (this.excludeId) throw new Error("Assertion error");
  70. }
  71. for (var key in this._expressions) { //jshint ignore:line
  72. var expr = this._expressions[key];
  73. if (expr instanceof Expression) {
  74. if (path) path.push(key);
  75. expr.addDependencies(deps, path);
  76. if (path) path.pop();
  77. } else { // inclusion
  78. if (!path) throw new Error("inclusion not supported in objects nested in $expressions; uassert code 16407");
  79. deps.fields[pathStr + key] = 1;
  80. }
  81. }
  82. return deps; // NOTE: added to munge as a convenience
  83. };
  84. /**
  85. * evaluateInternal(), but add the evaluated fields to a given document instead of creating a new one.
  86. * @method addToDocument
  87. * @param pResult the Document to add the evaluated expressions to
  88. * @param currentDoc the input Document for this level
  89. * @param vars the root of the whole input document
  90. */
  91. proto.addToDocument = function addToDocument(out, currentDoc, vars) { //jshint maxcomplexity:22
  92. var doneFields = {}; // This is used to mark fields we've done so that we can add the ones we haven't
  93. for (var fieldName in currentDoc) { //jshint ignore:line
  94. if (!currentDoc.hasOwnProperty(fieldName)) continue;
  95. var fieldValue = currentDoc[fieldName];
  96. // This field is not supposed to be in the output (unless it is _id)
  97. if (!this._expressions.hasOwnProperty(fieldName)) {
  98. if (!this.excludeId && this._atRoot && fieldName === Document.ID_PROPERTY_NAME) {
  99. // _id from the root doc is always included (until exclusion is supported)
  100. // not updating doneFields since "_id" isn't in _expressions
  101. out[fieldName] = fieldValue;
  102. }
  103. continue;
  104. }
  105. // make sure we don't add this field again
  106. doneFields[fieldName] = true;
  107. var expr = this._expressions[fieldName];
  108. if (!(expr instanceof Expression)) expr = undefined;
  109. if (!expr) {
  110. // This means pull the matching field from the input document
  111. out[fieldName] = fieldValue;
  112. continue;
  113. }
  114. var objExpr = expr instanceof ObjectExpression ? expr : undefined,
  115. valueType = Value.getType(fieldValue);
  116. if ((valueType !== "Object" && valueType !== "Array") || !objExpr) {
  117. // This expression replace the whole field
  118. var pValue = expr.evaluateInternal(vars);
  119. // don't add field if nothing was found in the subobject
  120. if (objExpr && Object.getOwnPropertyNames(pValue).length === 0)
  121. continue;
  122. /*
  123. * Don't add non-existent values (note: different from NULL or Undefined);
  124. * this is consistent with existing selection syntax which doesn't
  125. * force the appearance of non-existent fields.
  126. */
  127. // if (pValue !== undefined)
  128. out[fieldName] = pValue; //NOTE: DEVIATION FROM MONGO: we want to keep these in JS
  129. continue;
  130. }
  131. /*
  132. * Check on the type of the input value. If it's an
  133. * object, just walk down into that recursively, and
  134. * add it to the result.
  135. */
  136. if (valueType === "Object") {
  137. var sub = {};
  138. objExpr.addToDocument(sub, fieldValue, vars);
  139. out[fieldName] = sub;
  140. } else if (valueType === "Array") {
  141. /*
  142. * If it's an array, we have to do the same thing,
  143. * but to each array element. Then, add the array
  144. * of results to the current document.
  145. */
  146. var result = [],
  147. input = fieldValue;
  148. for (var fvi = 0, fvl = input.length; fvi < fvl; fvi++) {
  149. // can't look for a subfield in a non-object value.
  150. if (Value.getType(input[fvi]) !== "Object")
  151. continue;
  152. var doc = {};
  153. objExpr.addToDocument(doc, input[fvi], vars);
  154. result.push(doc);
  155. }
  156. out[fieldName] = result;
  157. } else {
  158. throw new Error("Assertion failure");
  159. }
  160. }
  161. if (Object.getOwnPropertyNames(doneFields).length === Object.getOwnPropertyNames(this._expressions).length)
  162. return out; //NOTE: munge returns result as a convenience
  163. // add any remaining fields we haven't already taken care of
  164. for (var i = 0, l = this._order.length; i < l; i++) {
  165. var fieldName2 = this._order[i],
  166. expr2 = this._expressions[fieldName2];
  167. // if we've already dealt with this field, above, do nothing
  168. if (doneFields.hasOwnProperty(fieldName2))
  169. continue;
  170. // this is a missing inclusion field
  171. if (expr2 === null || expr2 === undefined)
  172. continue;
  173. var value = expr2.evaluateInternal(vars);
  174. /*
  175. * Don't add non-existent values (note: different from NULL or Undefined);
  176. * this is consistent with existing selection syntax which doesn't
  177. * force the appearnance of non-existent fields.
  178. */
  179. if (value === undefined && !(expr2 instanceof ConstantExpression)) //NOTE: DEVIATION FROM MONGO: only if not {$const:undefined}
  180. continue;
  181. // don't add field if nothing was found in the subobject
  182. if (expr2 instanceof ObjectExpression && Object.getOwnPropertyNames(value).length === 0)
  183. continue;
  184. out[fieldName2] = value;
  185. }
  186. return out; //NOTE: munge returns result as a convenience
  187. };
  188. /**
  189. * estimated number of fields that will be output
  190. * @method getSizeHint
  191. */
  192. proto.getSizeHint = function getSizeHint() {
  193. // Note: this can overestimate, but that is better than underestimating
  194. return Object.getOwnPropertyNames(this._expressions).length + (this.excludeId ? 0 : 1);
  195. };
  196. /**
  197. * evaluateInternal(), but return a Document instead of a Value-wrapped Document.
  198. * @method evaluateDocument
  199. * @param currentDoc the input Document
  200. * @returns the result document
  201. */
  202. proto.evaluateDocument = function evaluateDocument(vars) {
  203. // create and populate the result
  204. var out = {};
  205. this.addToDocument(out, {}, vars); // No inclusion field matching.
  206. return out;
  207. };
  208. proto.evaluateInternal = function evaluateInternal(vars) {
  209. return this.evaluateDocument(vars);
  210. };
  211. /**
  212. * Add a field to the document expression.
  213. * @method addField
  214. * @param fieldPath the path the evaluated expression will have in the result Document
  215. * @param pExpression the expression to evaluateInternal obtain this field's Value in the result Document
  216. */
  217. proto.addField = function addField(fieldPath, pExpression) {
  218. if (!(fieldPath instanceof FieldPath)) fieldPath = new FieldPath(fieldPath);
  219. var fieldPart = fieldPath.getFieldName(0),
  220. haveExpr = this._expressions.hasOwnProperty(fieldPart),
  221. expr = this._expressions[fieldPart],
  222. subObj = expr instanceof ObjectExpression ? expr : undefined; // inserts if !haveExpr
  223. if (!haveExpr) {
  224. this._order.push(fieldPart);
  225. } else { // we already have an expression or inclusion for this field
  226. if (fieldPath.getPathLength() === 1) {
  227. // This expression is for right here
  228. var newSubObj = pExpression instanceof ObjectExpression ? pExpression : undefined;
  229. if (!(subObj && newSubObj))
  230. throw new Error("can't add an expression for field " + fieldPart +
  231. " because there is already an expression for that field" +
  232. " or one of its sub-fields; uassert code 16400"); // we can merge them
  233. // Copy everything from the newSubObj to the existing subObj
  234. // This is for cases like { $project:{ 'b.c':1, b:{ a:1 } } }
  235. for (var i = 0, l = newSubObj._order.length; i < l; ++i) {
  236. var key = newSubObj._order[i];
  237. // asserts if any fields are dupes
  238. subObj.addField(key, newSubObj._expressions[key]);
  239. }
  240. return;
  241. } else {
  242. // This expression is for a subfield
  243. if (!subObj)
  244. throw new Error("can't add an expression for a subfield of " + fieldPart +
  245. " because there is already an expression that applies to" +
  246. " the whole field; uassert code 16401");
  247. }
  248. }
  249. if (fieldPath.getPathLength() === 1) {
  250. if (haveExpr) throw new Error("Assertion error."); // haveExpr case handled above.
  251. this._expressions[fieldPart] = pExpression;
  252. return;
  253. }
  254. if (!haveExpr)
  255. this._expressions[fieldPart] = subObj = ObjectExpression.create();
  256. subObj.addField(fieldPath.tail(), pExpression);
  257. };
  258. /**
  259. * Add a field path to the set of those to be included.
  260. *
  261. * Note that including a nested field implies including everything on the path leading down to it.
  262. *
  263. * @method includePath
  264. * @param fieldPath the name of the field to be included
  265. */
  266. proto.includePath = function includePath(theFieldPath) {
  267. this.addField(theFieldPath, null);
  268. };
  269. proto.serialize = function serialize(explain) {
  270. var valBuilder = {};
  271. if (this.excludeId)
  272. valBuilder[Document.ID_PROPERTY_NAME] = false;
  273. for (var i = 0, l = this._order.length; i < l; ++i) {
  274. var fieldName = this._order[i];
  275. if (!this._expressions.hasOwnProperty(fieldName)) throw new Error("Assertion failure");
  276. var expr = this._expressions[fieldName];
  277. if (!expr) {
  278. valBuilder[fieldName] = true;
  279. } else {
  280. valBuilder[fieldName] = expr.serialize(explain);
  281. }
  282. }
  283. return valBuilder;
  284. };
  285. /**
  286. * Get a count of the added fields.
  287. * @method getFieldCount
  288. * @returns how many fields have been added
  289. */
  290. proto.getFieldCount = function getFieldCount() {
  291. return Object.getOwnPropertyNames(this._expressions).length;
  292. };