FieldPathExpression.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. "use strict";
  2. var Expression = require("./Expression"),
  3. Variables = require("./Variables"),
  4. FieldPath = require("../FieldPath");
  5. /**
  6. * Create a field path expression.
  7. *
  8. * Evaluation will extract the value associated with the given field
  9. * path from the source document.
  10. *
  11. * @class FieldPathExpression
  12. * @namespace mungedb-aggregate.pipeline.expressions
  13. * @module mungedb-aggregate
  14. * @extends mungedb-aggregate.pipeline.expressions.Expression
  15. * @constructor
  16. * @param {String} theFieldPath the field path string, without any leading document indicator
  17. */
  18. var FieldPathExpression = module.exports = function FieldPathExpression(theFieldPath, variable) {
  19. if (arguments.length !== 2) throw new Error(klass.name + ": expected args: theFieldPath[, variable]");
  20. this._fieldPath = new FieldPath(theFieldPath);
  21. this._variable = variable;
  22. }, klass = FieldPathExpression, base = Expression, proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}}); //jshint ignore:line
  23. /**
  24. * Create a field path expression using old semantics (rooted off of CURRENT).
  25. *
  26. * // NOTE: this method is deprecated and only used by tests
  27. * // TODO remove this method in favor of parse()
  28. *
  29. * Evaluation will extract the value associated with the given field
  30. * path from the source document.
  31. *
  32. * @param fieldPath the field path string, without any leading document
  33. * indicator
  34. * @returns the newly created field path expression
  35. */
  36. klass.create = function create(fieldPath) {
  37. return new FieldPathExpression("CURRENT." + fieldPath, Variables.ROOT_ID);
  38. };
  39. // this is the new version that supports every syntax
  40. /**
  41. * Like create(), but works with the raw string from the user with the "$" prefixes.
  42. * @param raw raw string fieldpath
  43. * @param vps variablesParseState
  44. * @returns a new FieldPathExpression
  45. */
  46. klass.parse = function parse(raw, vps) {
  47. if (raw[0] !== "$") // raw[0] is always a valid reference
  48. throw new Error("FieldPath: '" + raw + "' doesn't start with a $; uassert code 16873");
  49. if (raw.length < 2) // need at least "$" and either "$" or a field name
  50. throw new Error("'$' by itself is not a valid FieldPath; uassert code 16872");
  51. if (raw[1] === "$") {
  52. var fieldPath = raw.substr(2), // strip off $$
  53. dotIndex = fieldPath.indexOf("."),
  54. varName = fieldPath.substr(0, dotIndex !== -1 ? dotIndex : fieldPath.length);
  55. Variables.uassertValidNameForUserRead(varName);
  56. return new FieldPathExpression(fieldPath, vps.getVariable(varName));
  57. } else {
  58. return new FieldPathExpression("CURRENT." + raw.substr(1), // strip the "$" prefix
  59. vps.getVariable("CURRENT"));
  60. }
  61. };
  62. proto.optimize = function optimize() {
  63. // nothing can be done for these
  64. return this;
  65. };
  66. proto.addDependencies = function addDependencies(deps) {
  67. if (this._variable === Variables.ROOT_ID) {
  68. if (this._fieldPath.fieldNames.length === 1) {
  69. deps.needWholeDocument = true; // need full doc if just "$$ROOT"
  70. } else {
  71. deps.fields[this._fieldPath.tail().getPath(false)] = 1;
  72. }
  73. }
  74. };
  75. /**
  76. * Helper for evaluatePath to handle Array case
  77. */
  78. proto._evaluatePathArray = function _evaluatePathArray(index, input) {
  79. if (!(input instanceof Array)) throw new Error("must be array; dassert");
  80. // Check for remaining path in each element of array
  81. var result = [];
  82. for (var i = 0, l = input.length; i < l; i++) {
  83. if (!(input[i] instanceof Object))
  84. continue;
  85. var nested = this._evaluatePath(index, input[i]);
  86. if (nested !== undefined)
  87. result.push(nested);
  88. }
  89. return result;
  90. };
  91. /**
  92. * Internal implementation of evaluateInternal(), used recursively.
  93. *
  94. * The internal implementation doesn't just use a loop because of
  95. * the possibility that we need to skip over an array. If the path
  96. * is "a.b.c", and a is an array, then we fan out from there, and
  97. * traverse "b.c" for each element of a:[...]. This requires that
  98. * a be an array of objects in order to navigate more deeply.
  99. *
  100. * @param index current path field index to extract
  101. * @param input current document traversed to (not the top-level one)
  102. * @returns the field found; could be an array
  103. */
  104. proto._evaluatePath = function _evaluatePath(index, input) {
  105. // Note this function is very hot so it is important that is is well optimized.
  106. // In particular, all return paths should support RVO.
  107. // if we've hit the end of the path, stop
  108. if (index === this._fieldPath.fieldNames.length - 1)
  109. return input[this._fieldPath.fieldNames[index]];
  110. // Try to dive deeper
  111. var val = input[this._fieldPath.fieldNames[index]];
  112. if (val instanceof Object && val.constructor === Object) {
  113. return this._evaluatePath(index + 1, val);
  114. } else if (val instanceof Array) {
  115. return this._evaluatePathArray(index + 1, val);
  116. } else {
  117. return undefined;
  118. }
  119. };
  120. proto.evaluateInternal = function evaluateInternal(vars) {
  121. if (this._fieldPath.fieldNames.length === 1) // get the whole variable
  122. return vars.getValue(this._variable);
  123. if (this._variable === Variables.ROOT_ID) {
  124. // ROOT is always a document so use optimized code path
  125. return this._evaluatePath(1, vars.getRoot());
  126. }
  127. var val = vars.getValue(this._variable);
  128. if (val instanceof Object && val.constructor === Object) {
  129. return this._evaluatePath(1, val);
  130. } else if(val instanceof Array) {
  131. return this._evaluatePathArray(1,val);
  132. } else {
  133. return undefined;
  134. }
  135. };
  136. proto.serialize = function serialize(){
  137. if(this._fieldPath.fieldNames[0] === "CURRENT" && this._fieldPath.fieldNames.length > 1) {
  138. // use short form for "$$CURRENT.foo" but not just "$$CURRENT"
  139. return "$" + this._fieldPath.tail().getPath(false);
  140. } else {
  141. return "$$" + this._fieldPath.getPath(false);
  142. }
  143. };
  144. proto.getFieldPath = function getFieldPath(){
  145. return this._fieldPath;
  146. };