FieldPathExpression.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. "use strict";
  2. /**
  3. * Create a field path expression. Evaluation will extract the value associated with the given field path from the source document.
  4. * @class FieldPathExpression
  5. * @namespace mungedb-aggregate.pipeline.expressions
  6. * @module mungedb-aggregate
  7. * @extends mungedb-aggregate.pipeline.expressions.Expression
  8. * @constructor
  9. * @param {String} fieldPath the field path string, without any leading document indicator
  10. **/
  11. var Expression = require("./Expression"),
  12. Variables = require("./Variables"),
  13. Value = require("../Value"),
  14. FieldPath = require("../FieldPath");
  15. var FieldPathExpression = module.exports = function FieldPathExpression(path, variableId){
  16. if (arguments.length > 2) throw new Error("args expected: path[, vps]");
  17. this.path = new FieldPath(path);
  18. if(arguments.length == 2) {
  19. this.variable = variableId;
  20. } else {
  21. this.variable = Variables.ROOT_ID;
  22. }
  23. }, klass = FieldPathExpression, base = require("./Expression"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}});
  24. klass.create = function create(path) {
  25. return new FieldPathExpression("CURRENT."+path, Variables.ROOT_ID);
  26. };
  27. // PROTOTYPE MEMBERS
  28. proto.evaluateInternal = function evaluateInternal(vars){
  29. if(this.path.fields.length === 1) {
  30. return vars.getValue(this.variable);
  31. }
  32. if(this.variable === Variables.ROOT_ID) {
  33. return this.evaluatePath(1, vars.getRoot());
  34. }
  35. var vari = vars.getValue(this.variable);
  36. if(vari instanceof Array) {
  37. return this.evaluatePathArray(1,vari);
  38. } else if (vari instanceof Object) {
  39. return this.evaluatePath(1, vari);
  40. } else {
  41. return undefined;
  42. }
  43. };
  44. /**
  45. * Parses a fieldpath using the mongo 2.5 spec with optional variables
  46. *
  47. * @param raw raw string fieldpath
  48. * @param vps variablesParseState
  49. * @returns a new FieldPathExpression
  50. **/
  51. klass.parse = function parse(raw, vps) {
  52. if(raw[0] !== "$") {
  53. throw new Error("FieldPath: '" + raw + "' doesn't start with a $");
  54. }
  55. if(raw.length === 1) {
  56. throw new Error("'$' by itself is not a valid FieldPath");
  57. }
  58. if(raw[1] === "$") {
  59. var firstPeriod = raw.indexOf('.');
  60. var varname = (firstPeriod === -1 ? raw.slice(2) : raw.slice(2,firstPeriod));
  61. Variables.uassertValidNameForUserRead(varname);
  62. return new FieldPathExpression(raw.slice(2), vps.getVariableName(varname));
  63. } else {
  64. return new FieldPathExpression("CURRENT." + raw.slice(1), vps.getVariable("CURRENT"));
  65. }
  66. };
  67. /**
  68. * Parses a fieldpath using the mongo 2.5 spec with optional variables
  69. *
  70. * @param raw raw string fieldpath
  71. * @param vps variablesParseState
  72. * @returns a new FieldPathExpression
  73. **/
  74. proto.optimize = function optimize() {
  75. return this;
  76. };
  77. /**
  78. * Internal implementation of evaluate(), used recursively.
  79. *
  80. * The internal implementation doesn't just use a loop because of the
  81. * possibility that we need to skip over an array. If the path is "a.b.c",
  82. * and a is an array, then we fan out from there, and traverse "b.c" for each
  83. * element of a:[...]. This requires that a be an array of objects in order
  84. * to navigate more deeply.
  85. *
  86. * @param index current path field index to extract
  87. * @param pathLength maximum number of fields on field path
  88. * @param pDocument current document traversed to (not the top-level one)
  89. * @returns the field found; could be an array
  90. **/
  91. proto._evaluatePath = function _evaluatePath(obj, i, len){
  92. var fieldName = this.path.fields[i],
  93. field = obj[fieldName]; // It is possible we won't have an obj (document) and we need to not fail if that is the case
  94. // if the field doesn't exist, quit with an undefined value
  95. if (field === undefined) return undefined;
  96. // if we've hit the end of the path, stop
  97. if (++i >= len) return field;
  98. // We're diving deeper. If the value was null, return null
  99. if(field === null) return undefined;
  100. if (field.constructor === Object) {
  101. return this._evaluatePath(field, i, len);
  102. } else if (Array.isArray(field)) {
  103. var results = [];
  104. for (var i2 = 0, l2 = field.length; i2 < l2; i2++) {
  105. var subObj = field[i2],
  106. subObjType = typeof(subObj);
  107. if (subObjType === "undefined" || subObj === null) {
  108. results.push(subObj);
  109. } else if (subObj.constructor === Object) {
  110. results.push(this._evaluatePath(subObj, i, len));
  111. } else {
  112. throw new Error("the element '" + fieldName + "' along the dotted path '" + this.path.getPath() + "' is not an object, and cannot be navigated.; code 16014");
  113. }
  114. }
  115. return results;
  116. }
  117. return undefined;
  118. };
  119. proto.evaluatePathArray = function evaluatePathArray(index, input) {
  120. if(!(input instanceof Array)) {
  121. throw new Error("evaluatePathArray called on non-array");
  122. }
  123. var result = [];
  124. for(var ii = 0; ii < input.length; ii++) {
  125. if(input[ii] instanceof Object) {
  126. var nested = this.evaluatePath(index, input[ii]);
  127. if(nested) {
  128. result.push(nested);
  129. }
  130. }
  131. }
  132. return result;
  133. };
  134. proto.evaluatePath = function(index, input) {
  135. if(index === this.path.fields.length -1) {
  136. return input[this.path.fields[index]];
  137. }
  138. var val = input[this.path.fields[index]];
  139. if(val instanceof Array) {
  140. return this.evaluatePathArray(index+1, val);
  141. } else if (val instanceof Object) {
  142. return this.evaluatePath(index+1, val);
  143. } else {
  144. return undefined;
  145. }
  146. };
  147. proto.optimize = function(){
  148. return this;
  149. };
  150. proto.addDependencies = function addDependencies(deps){
  151. if(this.path.fields[0] === "CURRENT" || this.path.fields[0] === "ROOT") {
  152. if(this.path.fields.length === 1) {
  153. deps[""] = 1;
  154. } else {
  155. deps[this.path.tail().getPath(false)] = 1;
  156. }
  157. }
  158. };
  159. // renamed write to get because there are no streams
  160. proto.getFieldPath = function getFieldPath(usePrefix){
  161. return this.path.getPath(usePrefix);
  162. };
  163. proto.serialize = function toJSON(){
  164. if(this.path.fields[0] === "CURRENT" && this.path.fields.length > 1) {
  165. return "$" + this.path.tail().getPath(false);
  166. } else {
  167. return "$$" + this.path.getPath(false);
  168. }
  169. };
  170. //TODO: proto.addToBsonObj = ...?
  171. //TODO: proto.addToBsonArray = ...?
  172. //proto.writeFieldPath = ...? use #getFieldPath instead