MatchDocumentSource.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. "use strict";
  2. var async = require("async"),
  3. matcher = require("../../matcher/Matcher2.js");
  4. /**
  5. * A match document source built off of DocumentSource
  6. *
  7. * NOTE: THIS IS A DEVIATION FROM THE MONGO IMPLEMENTATION.
  8. * TODO: internally uses `sift` to fake it, which has bugs, so we need to reimplement this by porting the MongoDB implementation
  9. *
  10. * @class MatchDocumentSource
  11. * @namespace mungedb-aggregate.pipeline.documentSources
  12. * @module mungedb-aggregate
  13. * @constructor
  14. * @param {Object} query the match query to use
  15. * @param [ctx] {ExpressionContext}
  16. **/
  17. var MatchDocumentSource = module.exports = function MatchDocumentSource(query, ctx){
  18. if (arguments.length > 2) throw new Error("up to two args expected");
  19. if (!query) throw new Error("arg `query` is required");
  20. base.call(this, ctx);
  21. this.query = query; // save the query, so we can check it for deps later. THIS IS A DEVIATION FROM THE MONGO IMPLEMENTATION
  22. this.matcher = new matcher(query);
  23. // not supporting currently $text operator
  24. // set _isTextQuery to false.
  25. // TODO: update after we implement $text.
  26. if (klass.isTextQuery(query)) throw new Error("$text pipeline operation not supported");
  27. this._isTextQuery = false;
  28. }, klass = MatchDocumentSource, base = require("./DocumentSource"), proto = klass.prototype = Object.create(base.prototype, {constructor:{value:klass}}); //jshint ignore:line
  29. klass.matchName = "$match";
  30. proto.getSourceName = function getSourceName(){
  31. return klass.matchName;
  32. };
  33. proto.getNext = function getNext() {
  34. if (this.expCtx && this.expCtx.checkForInterrupt) this.expCtx.checkForInterrupt();
  35. // The user facing error should have been generated earlier.
  36. if (this._isTextQuery) throw new Error("Should never call getNext on a $match stage with $text clause; massert code 17309");
  37. var next;
  38. while((next = this.source.getNext())) {
  39. if (this.matcher.matches(next))
  40. return next;
  41. }
  42. // Nothing matched
  43. return null;
  44. };
  45. proto.coalesce = function coalesce(nextSource) {
  46. if (!(nextSource instanceof MatchDocumentSource))
  47. return false;
  48. this.matcher = new matcher({
  49. $and: [this.getQuery(), nextSource.getQuery()]
  50. });
  51. return true;
  52. };
  53. proto.serialize = function(explain) {
  54. var out = {};
  55. out[this.getSourceName()] = this.getQuery();
  56. return out;
  57. };
  58. klass.uassertNoDisallowedClauses = function uassertNoDisallowedClauses(query) {
  59. for(var key in query){
  60. if(query.hasOwnProperty(key)){
  61. // can't use the Matcher API because this would segfault the constructor
  62. if (key === "$where") throw new Error("code 16395; $where is not allowed inside of a $match aggregation expression");
  63. // geo breaks if it is not the first portion of the pipeline
  64. if (key === "$near") throw new Error("code 16424; $near is not allowed inside of a $match aggregation expression");
  65. if (key === "$within") throw new Error("code 16425; $within is not allowed inside of a $match aggregation expression");
  66. if (key === "$nearSphere") throw new Error("code 16426; $nearSphere is not allowed inside of a $match aggregation expression");
  67. if (query[key] instanceof Object && query[key].constructor === Object) this.uassertNoDisallowedClauses(query[key]);
  68. }
  69. }
  70. };
  71. klass.createFromJson = function createFromJson(jsonElement, ctx) {
  72. if (!(jsonElement instanceof Object) || jsonElement.constructor !== Object)
  73. throw new Error("code 15959 ; the match filter must be an expression in an object");
  74. klass.uassertNoDisallowedClauses(jsonElement);
  75. var matcher = new MatchDocumentSource(jsonElement, ctx);
  76. return matcher;
  77. };
  78. proto.isTextQuery = function isTextQuery() {
  79. return this._isTextQuery;
  80. };
  81. klass.isTextQuery = function isTextQuery(query) {
  82. for (var key in query) { //jshint ignore:line
  83. var fieldName = key;
  84. if (fieldName === "$text") return true;
  85. if (query[key] instanceof Object && query[key].constructor === Object && this.isTextQuery(query[key])) {
  86. return true;
  87. }
  88. }
  89. return false;
  90. };
  91. klass.setSource = function setSource (source) {
  92. this.setSource(source);
  93. };
  94. proto.getQuery = function getQuery() {
  95. return this.matcher._pattern;
  96. };
  97. /** Returns the portion of the match that can safely be promoted to before a $redact.
  98. * If this returns an empty BSONObj, no part of this match may safely be promoted.
  99. *
  100. * To be safe to promote, removing a field from a document to be matched must not cause
  101. * that document to be accepted when it would otherwise be rejected. As an example,
  102. * {name: {$ne: "bob smith"}} accepts documents without a name field, which means that
  103. * running this filter before a redact that would remove the name field would leak
  104. * information. On the other hand, {age: {$gt:5}} is ok because it doesn't accept documents
  105. * that have had their age field removed.
  106. */
  107. proto.redactSafePortion = function redactSafePortion() {
  108. // This block contains the functions that make up the implementation of
  109. // DocumentSourceMatch::redactSafePortion(). They will only be called after
  110. // the Match expression has been successfully parsed so they can assume that
  111. // input is well formed.
  112. var isAllDigits = function(n) {
  113. return !isNaN(n);
  114. };
  115. var isFieldnameRedactSafe = function isFieldnameRedactSafe(field) {
  116. var dotPos = field.indexOf(".");
  117. if (dotPos === -1)
  118. return !isAllDigits(field);
  119. var part = field.slice(0, dotPos),
  120. rest = field.slice(dotPos+1, field.length);
  121. return !isAllDigits(part) && isFieldnameRedactSafe(rest);
  122. };
  123. // Returns the redact-safe portion of an "inner" match expression. This is the layer like
  124. // {$gt: 5} which does not include the field name. Returns an empty document if none of the
  125. // expression can safely be promoted in front of a $redact.
  126. var redactSavePortionDollarOps = function redactSafePortionDollarOps(expr) { //jshint maxcomplexity:23
  127. var output = {},
  128. elem, i, j;
  129. var keys = Object.keys(expr);
  130. for (i = 0; i < keys.length; i++) {
  131. var field = keys[i],
  132. value = expr[field];
  133. if (field[0] !== "$")
  134. continue;
  135. // Ripped the case apart and did not implement this painful thing:
  136. // https://github.com/mongodb/mongo/blob/r2.5.4/src/mongo/db/jsobj.cpp#L286
  137. // Somebody should be taken to task for that work of art.
  138. if (field === "$type" || field === "$regex" || field === "$options" || field === "$mod") {
  139. output[field] = value;
  140. } else if (field === "$lte" || field === "$gte" || field === "$lt" || field === "$gt") {
  141. if (isTypeRedactSafeInComparison(field))
  142. output[field] = value;
  143. } else if (field === "$in") {
  144. // TODO: value/elem/field/etc may be mixed up and wrong here
  145. var allOk = true;
  146. for (j = 0; j < Object.keys(value).length; j++) {
  147. elem = Object.keys(value)[j];
  148. if (!isTypeRedactSafeInComparison(value[elem])) {
  149. allOk = false;
  150. break;
  151. }
  152. }
  153. if (allOk) {
  154. output[field] = value;
  155. }
  156. break;
  157. } else if (field === "$all") {
  158. // TODO: value/elem/field/etc may be mixed up and wrong here
  159. var matches = [];
  160. for (j = 0; j < value.length; j++) {
  161. elem = Object.keys(value)[j];
  162. if (isTypeRedactSafeInComparison(value[elem]))
  163. matches.push(value[elem]);
  164. }
  165. if (matches.length)
  166. output[field] = matches;
  167. } else if (field === "$elemMatch") {
  168. var subIn = value,
  169. subOut;
  170. if (subIn[0] === "$")
  171. subOut = redactSafePortionDollarOps(subIn);
  172. else
  173. subOut = redactSafePortionTopLevel(subIn);
  174. if (subOut && Object.keys(subOut).length)
  175. output[field] = subOut;
  176. break;
  177. } else {
  178. // never allowed:
  179. // equality, maxDist, near, ne, opSize, nin, exists, within, geoIntersects
  180. continue;
  181. }
  182. }
  183. return output;
  184. };
  185. var isTypeRedactSafeInComparison = function isTypeRedactSafeInComparison(type) {
  186. if (type instanceof Array || (type instanceof Object && type.constructor === Object) || type === null || type === undefined)
  187. return false;
  188. return true;
  189. };
  190. // Returns the redact-safe portion of an "outer" match expression. This is the layer like
  191. // {fieldName: {...}} which does include the field name. Returns an empty document if none of
  192. // the expression can safely be promoted in front of a $redact.
  193. var redactSafePortionTopLevel = function(topQuery) { //jshint maxcomplexity:18
  194. var output = {},
  195. okClauses = [],
  196. keys = topQuery ? Object.keys(topQuery) : [],
  197. j, elm, clause;
  198. for (var i = 0; i < keys.length; i++) {
  199. var field = keys[i],
  200. value = topQuery[field];
  201. if (field.length && field[0] === "$") {
  202. if (field === "$or") {
  203. okClauses = [];
  204. for (j = 0; j < Object.keys(value).length; j++) {
  205. elm = value[Object.keys(value)[j]];
  206. clause = redactSafePortionTopLevel(elm);
  207. if (!clause || Object.keys(clause).length === 0) {
  208. okClauses = [];
  209. break;
  210. }
  211. okClauses.push(clause);
  212. }
  213. if (okClauses && okClauses.length) {
  214. output.$or = okClauses;
  215. }
  216. } else if (field === "$and") {
  217. okClauses = [];
  218. for (j = 0; j < Object.keys(value).length; j++) {
  219. elm = value[Object.keys(value)[j]];
  220. clause = redactSafePortionTopLevel(elm);
  221. if (clause && Object.keys(clause).length)
  222. okClauses.push(clause);
  223. }
  224. if (okClauses.length)
  225. output.$and = okClauses;
  226. }
  227. continue;
  228. }
  229. if (!isFieldnameRedactSafe(field))
  230. continue;
  231. if (value instanceof Array || !value) {
  232. continue;
  233. } else if (value instanceof Object && value.constructor === Object) {
  234. // subobjects (not regex etc)
  235. var sub = redactSavePortionDollarOps(value);
  236. if (sub && Object.keys(sub).length)
  237. output[field] = sub;
  238. break;
  239. } else {
  240. output[field] = value;
  241. }
  242. }
  243. return output;
  244. };
  245. return redactSafePortionTopLevel(this.getQuery());
  246. };