ObjectExpression.js 13 KB

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