MatchDocumentSource.js 8.6 KB

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