ObjectExpression.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. var ObjectExpression = module.exports = (function(){
  2. // CONSTRUCTOR
  3. /**
  4. * Create an empty expression. Until fields are added, this will evaluate to an empty document (object).
  5. *
  6. * @class ObjectExpression
  7. * @namespace mungedb.aggregate.pipeline.expressions
  8. * @module mungedb-aggregate
  9. * @extends munge.pipeline.expressions.Expression
  10. * @constructor
  11. **/
  12. var klass = function ObjectExpression(){
  13. if(arguments.length !== 0) throw new Error("zero args expected");
  14. this.excludeId = false; /// <Boolean> for if _id is to be excluded
  15. this._expressions = {}; /// <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
  16. this._order = []; /// <Array<String>> this is used to maintain order for generated fields not in the source document
  17. }, Expression = require("./Expression"), base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  18. // DEPENDENCIES
  19. var Document = require("../Document"),
  20. FieldPath = require("../FieldPath");
  21. // INSTANCE VARIABLES
  22. /**
  23. * <Boolean> for if _id is to be excluded
  24. *
  25. * @property excludeId
  26. **/
  27. proto.excludeId = undefined;
  28. /**
  29. * <Object<Expression>> mapping from fieldname to Expression to generate the value NULL expression means include from source document
  30. **/
  31. proto._expressions = undefined;
  32. //TODO: might be able to completely ditch _order everywhere in here since `Object`s are mostly ordered anyhow but need to come back and revisit that later
  33. /**
  34. * <Array<String>> this is used to maintain order for generated fields not in the source document
  35. **/
  36. proto._order = [];
  37. // PROTOTYPE MEMBERS
  38. /**
  39. * evaluate(), but return a Document instead of a Value-wrapped Document.
  40. *
  41. * @method evaluateDocument
  42. * @param pDocument the input Document
  43. * @returns the result document
  44. **/
  45. proto.evaluateDocument = function evaluateDocument(doc) {
  46. // create and populate the result
  47. var pResult = {};
  48. this.addToDocument(pResult, pResult, doc); // No inclusion field matching.
  49. return pResult;
  50. };
  51. proto.evaluate = function evaluate(doc) { //TODO: collapse with #evaluateDocument()?
  52. return this.evaluateDocument(doc);
  53. };
  54. proto.optimize = function optimize(){
  55. for (var key in this._expressions) {
  56. var expr = this._expressions[key];
  57. if (expr !== undefined && expr !== null) this._expressions[key] = expr.optimize();
  58. }
  59. return this;
  60. };
  61. proto.getIsSimple = function getIsSimple(){
  62. for (var key in this._expressions) {
  63. var expr = this._expressions[key];
  64. if (expr !== undefined && expr !== null && !expr.getIsSimple()) return false;
  65. }
  66. return true;
  67. };
  68. proto.addDependencies = function addDependencies(deps, path){
  69. var depsSet = {};
  70. var pathStr = "";
  71. if (path instanceof Array) {
  72. if (path.length === 0) {
  73. // we are in the top level of a projection so _id is implicit
  74. if (!this.excludeId) depsSet[Document.ID_PROPERTY_NAME] = 1;
  75. } else {
  76. pathStr = new FieldPath(path).getPath() + ".";
  77. }
  78. } else {
  79. if (this.excludeId) throw new Error("excludeId is true!");
  80. }
  81. for (var key in this._expressions) {
  82. var expr = this._expressions[key];
  83. if (expr !== undefined && expr !== null) {
  84. if (path instanceof Array) path.push(key);
  85. expr.addDependencies(deps, path);
  86. if (path instanceof Array) path.pop();
  87. } else { // inclusion
  88. if (path === undefined || path === null) throw new Error("inclusion not supported in objects nested in $expressions; uassert code 16407");
  89. depsSet[pathStr + key] = 1;
  90. }
  91. }
  92. Array.prototype.push.apply(deps, Object.getOwnPropertyNames(depsSet));
  93. return deps; // NOTE: added to munge as a convenience
  94. };
  95. /**
  96. * evaluate(), but add the evaluated fields to a given document instead of creating a new one.
  97. *
  98. * @method addToDocument
  99. * @param pResult the Document to add the evaluated expressions to
  100. * @param pDocument the input Document for this level
  101. * @param rootDoc the root of the whole input document
  102. **/
  103. proto.addToDocument = function addToDocument(pResult, pDocument, rootDoc){
  104. var atRoot = (pDocument === rootDoc);
  105. var doneFields = {}; // This is used to mark fields we've done so that we can add the ones we haven't
  106. for(var fieldName in pDocument){
  107. if (!pDocument.hasOwnProperty(fieldName)) continue;
  108. var fieldValue = pDocument[fieldName];
  109. // This field is not supposed to be in the output (unless it is _id)
  110. if (!this._expressions.hasOwnProperty(fieldName)) {
  111. if (!this.excludeId && atRoot && fieldName == Document.ID_PROPERTY_NAME) {
  112. // _id from the root doc is always included (until exclusion is supported)
  113. // not updating doneFields since "_id" isn't in _expressions
  114. pResult[fieldName] = fieldValue;
  115. }
  116. continue;
  117. }
  118. // make sure we don't add this field again
  119. doneFields[fieldName] = true;
  120. // This means pull the matching field from the input document
  121. var expr = this._expressions[fieldName];
  122. if (!(expr instanceof Expression)) {
  123. pResult[fieldName] = fieldValue;
  124. continue;
  125. }
  126. // Check if this expression replaces the whole field
  127. if ((fieldValue.constructor !== Object && fieldValue.constructor !== Array) || !(expr instanceof ObjectExpression)) {
  128. var pValue = expr.evaluate(rootDoc);
  129. // don't add field if nothing was found in the subobject
  130. if (expr instanceof ObjectExpression && pValue instanceof Object && Object.getOwnPropertyNames(pValue).length === 0) continue;
  131. // Don't add non-existent values (note: different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
  132. // TODO make missing distinct from Undefined
  133. if (pValue !== undefined) pResult[fieldName] = pValue;
  134. continue;
  135. }
  136. // Check on the type of the input value. If it's an object, just walk down into that recursively, and add it to the result.
  137. if (fieldValue.constructor === Object) {
  138. pResult[fieldName] = expr.addToDocument({}, fieldValue, rootDoc); //TODO: pretty sure this is broken;
  139. } else if (fieldValue.constructor == Array) {
  140. // If it's an array, we have to do the same thing, but to each array element. Then, add the array of results to the current document.
  141. var result = [];
  142. for(var fvi = 0, fvl = fieldValue.length; fvi < fvl; fvi++){
  143. var subValue = fieldValue[fvi];
  144. if (subValue.constructor !== Object) continue; // can't look for a subfield in a non-object value.
  145. result.push(expr.addToDocument({}, subValue, rootDoc));
  146. }
  147. pResult[fieldName] = result;
  148. } else {
  149. throw new Error("should never happen"); //verify( false );
  150. }
  151. }
  152. if (Object.getOwnPropertyNames(doneFields).length == Object.getOwnPropertyNames(this._expressions).length) return pResult; //NOTE: munge returns result as a convenience
  153. // add any remaining fields we haven't already taken care of
  154. for(var i = 0, l = this._order.length; i < l; i++){
  155. var fieldName2 = this._order[i];
  156. var expr2 = this._expressions[fieldName2];
  157. // if we've already dealt with this field, above, do nothing
  158. if (doneFields.hasOwnProperty(fieldName2)) continue;
  159. // this is a missing inclusion field
  160. if (!expr2) continue;
  161. var value = expr2.evaluate(rootDoc);
  162. // Don't add non-existent values (note: different from NULL); this is consistent with existing selection syntax which doesn't force the appearnance of non-existent fields.
  163. if (value === undefined) continue;
  164. // don't add field if nothing was found in the subobject
  165. if (expr2 instanceof ObjectExpression && value && value instanceof Object && Object.getOwnPropertyNames(value).length === 0) continue;
  166. pResult[fieldName2] = value;
  167. }
  168. return pResult; //NOTE: munge returns result as a convenience
  169. };
  170. /**
  171. * estimated number of fields that will be output
  172. *
  173. * @method getSizeHint
  174. **/
  175. proto.getSizeHint = function getSizeHint(){
  176. // Note: this can overestimate, but that is better than underestimating
  177. return Object.getOwnPropertyNames(this._expressions).length + (this.excludeId ? 0 : 1);
  178. };
  179. /**
  180. * Add a field to the document expression.
  181. *
  182. * @method addField
  183. * @param fieldPath the path the evaluated expression will have in the result Document
  184. * @param pExpression the expression to evaluate obtain this field's Value in the result Document
  185. **/
  186. proto.addField = function addField(fieldPath, pExpression){
  187. if(!(fieldPath instanceof FieldPath)) fieldPath = new FieldPath(fieldPath);
  188. var fieldPart = fieldPath.fields[0],
  189. haveExpr = this._expressions.hasOwnProperty(fieldPart),
  190. subObj = this._expressions[fieldPart]; // inserts if !haveExpr //NOTE: not in munge & JS it doesn't, handled manually below
  191. if (!haveExpr) {
  192. this._order.push(fieldPart);
  193. } else { // we already have an expression or inclusion for this field
  194. if (fieldPath.getPathLength() == 1) { // This expression is for right here
  195. if (!(subObj instanceof ObjectExpression && typeof pExpression == "object" && pExpression instanceof ObjectExpression)) throw new Error("can't add an expression for field `" + fieldPart + "` because there is already an expression for that field or one of its sub-fields; uassert code 16400"); // we can merge them
  196. // Copy everything from the newSubObj to the existing subObj
  197. // This is for cases like { $project:{ 'b.c':1, b:{ a:1 } } }
  198. for(var key in pExpression._expressions){
  199. if(pExpression._expressions.hasOwnProperty(key)){
  200. // asserts if any fields are dupes
  201. subObj.addField(key, pExpression._expressions[key]);
  202. }
  203. }
  204. return;
  205. } else { // This expression is for a subfield
  206. if(!subObj) throw new Error("can't add an expression for a subfield of `" + fieldPart + "` because there is already an expression that applies to the whole field; uassert code 16401");
  207. }
  208. }
  209. if (fieldPath.getPathLength() == 1) {
  210. if(haveExpr) throw new Error("Internal error."); // haveExpr case handled above.
  211. this._expressions[fieldPart] = pExpression;
  212. return;
  213. }
  214. if (!haveExpr) subObj = this._expressions[fieldPart] = new ObjectExpression();
  215. subObj.addField(fieldPath.tail(), pExpression);
  216. };
  217. /**
  218. * Add a field path to the set of those to be included.
  219. *
  220. * Note that including a nested field implies including everything on the path leading down to it.
  221. *
  222. * @method includePath
  223. * @param fieldPath the name of the field to be included
  224. **/
  225. proto.includePath = function includePath(path){
  226. this.addField(path, undefined);
  227. };
  228. /**
  229. * Get a count of the added fields.
  230. *
  231. * @method getFieldCount
  232. * @returns how many fields have been added
  233. **/
  234. proto.getFieldCount = function getFieldCount(){
  235. return Object.getOwnPropertyNames(this._expressions).length;
  236. };
  237. ///**
  238. //* Specialized BSON conversion that allows for writing out a $project specification.
  239. //* This creates a standalone object, which must be added to a containing object with a name
  240. //*
  241. //* @param pBuilder where to write the object to
  242. //* @param requireExpression see Expression::addToBsonObj
  243. //**/
  244. //TODO: proto.documentToBson = ...?
  245. //TODO: proto.addToBsonObj = ...?
  246. //TODO: proto.addToBsonArray = ...?
  247. //NOTE: in `munge` we're not passing the `Object`s in and allowing `toJson` (was `documentToBson`) to modify it directly and are instead building and returning a new `Object` since that's the way it's actually used
  248. proto.toJson = function toJson(requireExpression){ //TODO: requireExpression doesn't seem to really get used in the mongo code; remove it?
  249. var o = {};
  250. if(this.excludeId)
  251. o[Document.ID_PROPERTY_NAME] = false;
  252. for(var i = 0, l = this._order.length; i < l; i++){
  253. var fieldName = this._order[i];
  254. if(!this._expressions.hasOwnProperty(fieldName)) throw new Error("internal error: fieldName from _ordered list not found in _expressions");
  255. var fieldValue = this._expressions[fieldName];
  256. if(fieldValue === undefined){
  257. // this is inclusion, not an expression
  258. o[fieldName] = true;
  259. }else{
  260. o[fieldName] = fieldValue.toJson(requireExpression);
  261. }
  262. }
  263. return o;
  264. };
  265. //TODO: where's toJson? or is that what documentToBson really is up above?
  266. return klass;
  267. })();