mongodb-livedata-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +63 -0
  2. package/dist/livedata_server.js +9 -0
  3. package/dist/meteor/binary-heap/max_heap.js +186 -0
  4. package/dist/meteor/binary-heap/min_heap.js +17 -0
  5. package/dist/meteor/binary-heap/min_max_heap.js +48 -0
  6. package/dist/meteor/callback-hook/hook.js +78 -0
  7. package/dist/meteor/ddp/crossbar.js +136 -0
  8. package/dist/meteor/ddp/heartbeat.js +77 -0
  9. package/dist/meteor/ddp/livedata_server.js +403 -0
  10. package/dist/meteor/ddp/method-invocation.js +72 -0
  11. package/dist/meteor/ddp/random-stream.js +100 -0
  12. package/dist/meteor/ddp/session-collection-view.js +106 -0
  13. package/dist/meteor/ddp/session-document-view.js +82 -0
  14. package/dist/meteor/ddp/session.js +570 -0
  15. package/dist/meteor/ddp/stream_server.js +181 -0
  16. package/dist/meteor/ddp/subscription.js +347 -0
  17. package/dist/meteor/ddp/utils.js +104 -0
  18. package/dist/meteor/ddp/writefence.js +111 -0
  19. package/dist/meteor/diff-sequence/diff.js +257 -0
  20. package/dist/meteor/ejson/ejson.js +569 -0
  21. package/dist/meteor/ejson/stringify.js +119 -0
  22. package/dist/meteor/ejson/utils.js +42 -0
  23. package/dist/meteor/id-map/id_map.js +92 -0
  24. package/dist/meteor/mongo/caching_change_observer.js +94 -0
  25. package/dist/meteor/mongo/doc_fetcher.js +53 -0
  26. package/dist/meteor/mongo/geojson_utils.js +41 -0
  27. package/dist/meteor/mongo/live_connection.js +264 -0
  28. package/dist/meteor/mongo/live_cursor.js +57 -0
  29. package/dist/meteor/mongo/minimongo_common.js +2002 -0
  30. package/dist/meteor/mongo/minimongo_matcher.js +217 -0
  31. package/dist/meteor/mongo/minimongo_sorter.js +268 -0
  32. package/dist/meteor/mongo/observe_driver_utils.js +73 -0
  33. package/dist/meteor/mongo/observe_multiplexer.js +228 -0
  34. package/dist/meteor/mongo/oplog-observe-driver.js +919 -0
  35. package/dist/meteor/mongo/oplog_tailing.js +352 -0
  36. package/dist/meteor/mongo/oplog_v2_converter.js +126 -0
  37. package/dist/meteor/mongo/polling_observe_driver.js +195 -0
  38. package/dist/meteor/mongo/synchronous-cursor.js +261 -0
  39. package/dist/meteor/mongo/synchronous-queue.js +110 -0
  40. package/dist/meteor/ordered-dict/ordered_dict.js +198 -0
  41. package/dist/meteor/random/AbstractRandomGenerator.js +92 -0
  42. package/dist/meteor/random/AleaRandomGenerator.js +90 -0
  43. package/dist/meteor/random/NodeRandomGenerator.js +42 -0
  44. package/dist/meteor/random/createAleaGenerator.js +32 -0
  45. package/dist/meteor/random/createRandom.js +22 -0
  46. package/dist/meteor/random/main.js +12 -0
  47. package/livedata_server.ts +3 -0
  48. package/meteor/LICENSE +28 -0
  49. package/meteor/binary-heap/max_heap.ts +225 -0
  50. package/meteor/binary-heap/min_heap.ts +15 -0
  51. package/meteor/binary-heap/min_max_heap.ts +53 -0
  52. package/meteor/callback-hook/hook.ts +85 -0
  53. package/meteor/ddp/crossbar.ts +148 -0
  54. package/meteor/ddp/heartbeat.ts +97 -0
  55. package/meteor/ddp/livedata_server.ts +473 -0
  56. package/meteor/ddp/method-invocation.ts +86 -0
  57. package/meteor/ddp/random-stream.ts +102 -0
  58. package/meteor/ddp/session-collection-view.ts +119 -0
  59. package/meteor/ddp/session-document-view.ts +92 -0
  60. package/meteor/ddp/session.ts +708 -0
  61. package/meteor/ddp/stream_server.ts +204 -0
  62. package/meteor/ddp/subscription.ts +392 -0
  63. package/meteor/ddp/utils.ts +119 -0
  64. package/meteor/ddp/writefence.ts +130 -0
  65. package/meteor/diff-sequence/diff.ts +295 -0
  66. package/meteor/ejson/ejson.ts +601 -0
  67. package/meteor/ejson/stringify.ts +122 -0
  68. package/meteor/ejson/utils.ts +38 -0
  69. package/meteor/id-map/id_map.ts +84 -0
  70. package/meteor/mongo/caching_change_observer.ts +120 -0
  71. package/meteor/mongo/doc_fetcher.ts +52 -0
  72. package/meteor/mongo/geojson_utils.ts +42 -0
  73. package/meteor/mongo/live_connection.ts +302 -0
  74. package/meteor/mongo/live_cursor.ts +79 -0
  75. package/meteor/mongo/minimongo_common.ts +2440 -0
  76. package/meteor/mongo/minimongo_matcher.ts +275 -0
  77. package/meteor/mongo/minimongo_sorter.ts +331 -0
  78. package/meteor/mongo/observe_driver_utils.ts +79 -0
  79. package/meteor/mongo/observe_multiplexer.ts +256 -0
  80. package/meteor/mongo/oplog-observe-driver.ts +1049 -0
  81. package/meteor/mongo/oplog_tailing.ts +414 -0
  82. package/meteor/mongo/oplog_v2_converter.ts +124 -0
  83. package/meteor/mongo/polling_observe_driver.ts +247 -0
  84. package/meteor/mongo/synchronous-cursor.ts +293 -0
  85. package/meteor/mongo/synchronous-queue.ts +119 -0
  86. package/meteor/ordered-dict/ordered_dict.ts +229 -0
  87. package/meteor/random/AbstractRandomGenerator.ts +99 -0
  88. package/meteor/random/AleaRandomGenerator.ts +96 -0
  89. package/meteor/random/NodeRandomGenerator.ts +37 -0
  90. package/meteor/random/createAleaGenerator.ts +31 -0
  91. package/meteor/random/createRandom.ts +19 -0
  92. package/meteor/random/main.ts +8 -0
  93. package/package.json +30 -0
  94. package/tsconfig.json +10 -0
@@ -0,0 +1,2002 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports._compileProjection = exports._pathsElidingNumericKeys = exports.combineImportantPathsIntoProjection = exports._modify = exports._checkSupportedProjection = exports._f = exports.regexpElementMatcher = exports.projectionDetails = exports.populateDocumentWithQueryFields = exports.pathsToTree = exports.nothingMatcher = exports.makeLookupFunction = exports.isOperatorObject = exports.isNumericKey = exports.isIndexable = exports.expandArraysInBranches = exports.equalityElementMatcher = exports.compileDocumentSelector = exports.ELEMENT_OPERATORS = exports.hasOwn = void 0;
7
+ const mongodb_1 = require("mongodb");
8
+ const ejson_1 = require("../ejson/ejson");
9
+ const geojson_utils_1 = require("./geojson_utils");
10
+ const minimongo_matcher_1 = require("./minimongo_matcher");
11
+ const minimongo_sorter_1 = __importDefault(require("./minimongo_sorter"));
12
+ exports.hasOwn = Object.prototype.hasOwnProperty;
13
+ // Each element selector contains:
14
+ // - compileElementSelector, a function with args:
15
+ // - operand - the "right hand side" of the operator
16
+ // - valueSelector - the "context" for the operator (so that $regex can find
17
+ // $options)
18
+ // - matcher - the Matcher this is going into (so that $elemMatch can compile
19
+ // more things)
20
+ // returning a function mapping a single value to bool.
21
+ // - dontExpandLeafArrays, a bool which prevents expandArraysInBranches from
22
+ // being called
23
+ // - dontIncludeLeafArrays, a bool which causes an argument to be passed to
24
+ // expandArraysInBranches if it is called
25
+ exports.ELEMENT_OPERATORS = {
26
+ $lt: makeInequality(cmpValue => cmpValue < 0),
27
+ $gt: makeInequality(cmpValue => cmpValue > 0),
28
+ $lte: makeInequality(cmpValue => cmpValue <= 0),
29
+ $gte: makeInequality(cmpValue => cmpValue >= 0),
30
+ $mod: {
31
+ compileElementSelector(operand) {
32
+ if (!(Array.isArray(operand) && operand.length === 2
33
+ && typeof operand[0] === 'number'
34
+ && typeof operand[1] === 'number')) {
35
+ throw Error('argument to $mod must be an array of two numbers');
36
+ }
37
+ // XXX could require to be ints or round or something
38
+ const divisor = operand[0];
39
+ const remainder = operand[1];
40
+ return value => (typeof value === 'number' && value % divisor === remainder);
41
+ },
42
+ },
43
+ $in: {
44
+ compileElementSelector(operand) {
45
+ if (!Array.isArray(operand)) {
46
+ throw Error('$in needs an array');
47
+ }
48
+ const elementMatchers = operand.map(option => {
49
+ if (option instanceof RegExp) {
50
+ return regexpElementMatcher(option);
51
+ }
52
+ if (isOperatorObject(option)) {
53
+ throw Error('cannot nest $ under $in');
54
+ }
55
+ return equalityElementMatcher(option);
56
+ });
57
+ return value => {
58
+ // Allow {a: {$in: [null]}} to match when 'a' does not exist.
59
+ if (value === undefined) {
60
+ value = null;
61
+ }
62
+ return elementMatchers.some(matcher => matcher(value));
63
+ };
64
+ },
65
+ },
66
+ $size: {
67
+ // {a: [[5, 5]]} must match {a: {$size: 1}} but not {a: {$size: 2}}, so we
68
+ // don't want to consider the element [5,5] in the leaf array [[5,5]] as a
69
+ // possible value.
70
+ dontExpandLeafArrays: true,
71
+ compileElementSelector(operand) {
72
+ if (typeof operand === 'string') {
73
+ // Don't ask me why, but by experimentation, this seems to be what Mongo
74
+ // does.
75
+ operand = 0;
76
+ }
77
+ else if (typeof operand !== 'number') {
78
+ throw Error('$size needs a number');
79
+ }
80
+ return value => Array.isArray(value) && value.length === operand;
81
+ },
82
+ },
83
+ $type: {
84
+ // {a: [5]} must not match {a: {$type: 4}} (4 means array), but it should
85
+ // match {a: {$type: 1}} (1 means number), and {a: [[5]]} must match {$a:
86
+ // {$type: 4}}. Thus, when we see a leaf array, we *should* expand it but
87
+ // should *not* include it itself.
88
+ dontIncludeLeafArrays: true,
89
+ compileElementSelector(operand) {
90
+ if (typeof operand === 'string') {
91
+ const operandAliasMap = {
92
+ 'double': 1,
93
+ 'string': 2,
94
+ 'object': 3,
95
+ 'array': 4,
96
+ 'binData': 5,
97
+ 'undefined': 6,
98
+ 'objectId': 7,
99
+ 'bool': 8,
100
+ 'date': 9,
101
+ 'null': 10,
102
+ 'regex': 11,
103
+ 'dbPointer': 12,
104
+ 'javascript': 13,
105
+ 'symbol': 14,
106
+ 'javascriptWithScope': 15,
107
+ 'int': 16,
108
+ 'timestamp': 17,
109
+ 'long': 18,
110
+ 'decimal': 19,
111
+ 'minKey': -1,
112
+ 'maxKey': 127,
113
+ };
114
+ if (!exports.hasOwn.call(operandAliasMap, operand)) {
115
+ throw Error(`unknown string alias for $type: ${operand}`);
116
+ }
117
+ operand = operandAliasMap[operand];
118
+ }
119
+ else if (typeof operand === 'number') {
120
+ if (operand === 0 || operand < -1
121
+ || (operand > 19 && operand !== 127)) {
122
+ throw Error(`Invalid numerical $type code: ${operand}`);
123
+ }
124
+ }
125
+ else {
126
+ throw Error('argument to $type is not a number or a string');
127
+ }
128
+ return value => (value !== undefined && exports._f._type(value) === operand);
129
+ },
130
+ },
131
+ $bitsAllSet: {
132
+ compileElementSelector(operand) {
133
+ const mask = getOperandBitmask(operand, '$bitsAllSet');
134
+ return value => {
135
+ const bitmask = getValueBitmask(value, mask.length);
136
+ return bitmask && mask.every((byte, i) => (bitmask[i] & byte) === byte);
137
+ };
138
+ },
139
+ },
140
+ $bitsAnySet: {
141
+ compileElementSelector(operand) {
142
+ const mask = getOperandBitmask(operand, '$bitsAnySet');
143
+ return value => {
144
+ const bitmask = getValueBitmask(value, mask.length);
145
+ return bitmask && mask.some((byte, i) => (~bitmask[i] & byte) !== byte);
146
+ };
147
+ },
148
+ },
149
+ $bitsAllClear: {
150
+ compileElementSelector(operand) {
151
+ const mask = getOperandBitmask(operand, '$bitsAllClear');
152
+ return value => {
153
+ const bitmask = getValueBitmask(value, mask.length);
154
+ return bitmask && mask.every((byte, i) => !(bitmask[i] & byte));
155
+ };
156
+ },
157
+ },
158
+ $bitsAnyClear: {
159
+ compileElementSelector(operand) {
160
+ const mask = getOperandBitmask(operand, '$bitsAnyClear');
161
+ return value => {
162
+ const bitmask = getValueBitmask(value, mask.length);
163
+ return bitmask && mask.some((byte, i) => (bitmask[i] & byte) !== byte);
164
+ };
165
+ },
166
+ },
167
+ $regex: {
168
+ compileElementSelector(operand, valueSelector) {
169
+ if (!(typeof operand === 'string' || operand instanceof RegExp)) {
170
+ throw Error('$regex has to be a string or RegExp');
171
+ }
172
+ let regexp;
173
+ if (valueSelector.$options !== undefined) {
174
+ // Options passed in $options (even the empty string) always overrides
175
+ // options in the RegExp object itself.
176
+ // Be clear that we only support the JS-supported options, not extended
177
+ // ones (eg, Mongo supports x and s). Ideally we would implement x and s
178
+ // by transforming the regexp, but not today...
179
+ if (/[^gim]/.test(valueSelector.$options)) {
180
+ throw new Error('Only the i, m, and g regexp options are supported');
181
+ }
182
+ const source = operand instanceof RegExp ? operand.source : operand;
183
+ regexp = new RegExp(source, valueSelector.$options);
184
+ }
185
+ else if (operand instanceof RegExp) {
186
+ regexp = operand;
187
+ }
188
+ else {
189
+ regexp = new RegExp(operand);
190
+ }
191
+ return regexpElementMatcher(regexp);
192
+ },
193
+ },
194
+ $elemMatch: {
195
+ dontExpandLeafArrays: true,
196
+ compileElementSelector(operand, valueSelector, matcher) {
197
+ if (!_isPlainObject(operand)) {
198
+ throw Error('$elemMatch need an object');
199
+ }
200
+ const isDocMatcher = !isOperatorObject(Object.keys(operand)
201
+ .filter(key => !exports.hasOwn.call(LOGICAL_OPERATORS, key))
202
+ .reduce((a, b) => Object.assign(a, { [b]: operand[b] }), {}), true);
203
+ let subMatcher;
204
+ if (isDocMatcher) {
205
+ // This is NOT the same as compileValueSelector(operand), and not just
206
+ // because of the slightly different calling convention.
207
+ // {$elemMatch: {x: 3}} means "an element has a field x:3", not
208
+ // "consists only of a field x:3". Also, regexps and sub-$ are allowed.
209
+ subMatcher =
210
+ compileDocumentSelector(operand, matcher, { inElemMatch: true });
211
+ }
212
+ else {
213
+ subMatcher = compileValueSelector(operand, matcher);
214
+ }
215
+ return value => {
216
+ if (!Array.isArray(value)) {
217
+ return false;
218
+ }
219
+ for (let i = 0; i < value.length; ++i) {
220
+ const arrayElement = value[i];
221
+ let arg;
222
+ if (isDocMatcher) {
223
+ // We can only match {$elemMatch: {b: 3}} against objects.
224
+ // (We can also match against arrays, if there's numeric indices,
225
+ // eg {$elemMatch: {'0.b': 3}} or {$elemMatch: {0: 3}}.)
226
+ if (!isIndexable(arrayElement)) {
227
+ return false;
228
+ }
229
+ arg = arrayElement;
230
+ }
231
+ else {
232
+ // dontIterate ensures that {a: {$elemMatch: {$gt: 5}}} matches
233
+ // {a: [8]} but not {a: [[8]]}
234
+ arg = [{ value: arrayElement, dontIterate: true }];
235
+ }
236
+ // XXX support $near in $elemMatch by propagating $distance?
237
+ if (subMatcher(arg).result) {
238
+ return i; // specially understood to mean "use as arrayIndices"
239
+ }
240
+ }
241
+ return false;
242
+ };
243
+ },
244
+ },
245
+ };
246
+ // Operators that appear at the top level of a document selector.
247
+ const LOGICAL_OPERATORS = {
248
+ $and(subSelector, matcher, inElemMatch) {
249
+ return andDocumentMatchers(compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch));
250
+ },
251
+ $or(subSelector, matcher, inElemMatch) {
252
+ const matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch);
253
+ // Special case: if there is only one matcher, use it directly, *preserving*
254
+ // any arrayIndices it returns.
255
+ if (matchers.length === 1) {
256
+ return matchers[0];
257
+ }
258
+ return doc => {
259
+ const result = matchers.some(fn => fn(doc).result);
260
+ // $or does NOT set arrayIndices when it has multiple
261
+ // sub-expressions. (Tested against MongoDB.)
262
+ return { result };
263
+ };
264
+ },
265
+ $nor(subSelector, matcher, inElemMatch) {
266
+ const matchers = compileArrayOfDocumentSelectors(subSelector, matcher, inElemMatch);
267
+ return doc => {
268
+ const result = matchers.every(fn => !fn(doc).result);
269
+ // Never set arrayIndices, because we only match if nothing in particular
270
+ // 'matched' (and because this is consistent with MongoDB).
271
+ return { result };
272
+ };
273
+ },
274
+ $where(selectorValue, matcher) {
275
+ // Record that *any* path may be used.
276
+ matcher._recordPathUsed('');
277
+ matcher._hasWhere = true;
278
+ if (!(selectorValue instanceof Function)) {
279
+ // XXX MongoDB seems to have more complex logic to decide where or or not
280
+ // to add 'return'; not sure exactly what it is.
281
+ selectorValue = Function('obj', `return ${selectorValue}`);
282
+ }
283
+ // We make the document available as both `this` and `obj`.
284
+ // // XXX not sure what we should do if this throws
285
+ return doc => ({ result: selectorValue.call(doc, doc) });
286
+ },
287
+ // This is just used as a comment in the query (in MongoDB, it also ends up in
288
+ // query logs); it has no effect on the actual selection.
289
+ $comment() {
290
+ return () => ({ result: true });
291
+ },
292
+ };
293
+ // Operators that (unlike LOGICAL_OPERATORS) pertain to individual paths in a
294
+ // document, but (unlike ELEMENT_OPERATORS) do not have a simple definition as
295
+ // "match each branched value independently and combine with
296
+ // convertElementMatcherToBranchedMatcher".
297
+ const VALUE_OPERATORS = {
298
+ $eq(operand) {
299
+ return convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand));
300
+ },
301
+ $not(operand, valueSelector, matcher) {
302
+ return invertBranchedMatcher(compileValueSelector(operand, matcher));
303
+ },
304
+ $ne(operand) {
305
+ return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(equalityElementMatcher(operand)));
306
+ },
307
+ $nin(operand) {
308
+ return invertBranchedMatcher(convertElementMatcherToBranchedMatcher(exports.ELEMENT_OPERATORS.$in.compileElementSelector(operand)));
309
+ },
310
+ $exists(operand) {
311
+ const exists = convertElementMatcherToBranchedMatcher(value => value !== undefined);
312
+ return operand ? exists : invertBranchedMatcher(exists);
313
+ },
314
+ // $options just provides options for $regex; its logic is inside $regex
315
+ $options(operand, valueSelector) {
316
+ if (!exports.hasOwn.call(valueSelector, '$regex')) {
317
+ throw Error('$options needs a $regex');
318
+ }
319
+ return everythingMatcher;
320
+ },
321
+ // $maxDistance is basically an argument to $near
322
+ $maxDistance(operand, valueSelector) {
323
+ if (!valueSelector.$near) {
324
+ throw Error('$maxDistance needs a $near');
325
+ }
326
+ return everythingMatcher;
327
+ },
328
+ $all(operand, valueSelector, matcher) {
329
+ if (!Array.isArray(operand)) {
330
+ throw Error('$all requires array');
331
+ }
332
+ // Not sure why, but this seems to be what MongoDB does.
333
+ if (operand.length === 0) {
334
+ return nothingMatcher;
335
+ }
336
+ const branchedMatchers = operand.map(criterion => {
337
+ // XXX handle $all/$elemMatch combination
338
+ if (isOperatorObject(criterion)) {
339
+ throw Error('no $ expressions in $all');
340
+ }
341
+ // This is always a regexp or equality selector.
342
+ return compileValueSelector(criterion, matcher);
343
+ });
344
+ // andBranchedMatchers does NOT require all selectors to return true on the
345
+ // SAME branch.
346
+ return andBranchedMatchers(branchedMatchers);
347
+ },
348
+ $near(operand, valueSelector, matcher, isRoot) {
349
+ if (!isRoot) {
350
+ throw Error('$near can\'t be inside another $ operator');
351
+ }
352
+ matcher._hasGeoQuery = true;
353
+ // There are two kinds of geodata in MongoDB: legacy coordinate pairs and
354
+ // GeoJSON. They use different distance metrics, too. GeoJSON queries are
355
+ // marked with a $geometry property, though legacy coordinates can be
356
+ // matched using $geometry.
357
+ let maxDistance, point, distance;
358
+ if (_isPlainObject(operand) && exports.hasOwn.call(operand, '$geometry')) {
359
+ // GeoJSON "2dsphere" mode.
360
+ maxDistance = operand.$maxDistance;
361
+ point = operand.$geometry;
362
+ distance = value => {
363
+ // XXX: for now, we don't calculate the actual distance between, say,
364
+ // polygon and circle. If people care about this use-case it will get
365
+ // a priority.
366
+ if (!value) {
367
+ return null;
368
+ }
369
+ if (!value.type) {
370
+ return (0, geojson_utils_1.pointDistance)(point, { type: 'Point', coordinates: pointToArray(value) });
371
+ }
372
+ if (value.type === 'Point') {
373
+ return (0, geojson_utils_1.pointDistance)(point, value);
374
+ }
375
+ return (0, geojson_utils_1.geometryWithinRadius)(value, point, maxDistance)
376
+ ? 0
377
+ : maxDistance + 1;
378
+ };
379
+ }
380
+ else {
381
+ maxDistance = valueSelector.$maxDistance;
382
+ if (!isIndexable(operand)) {
383
+ throw Error('$near argument must be coordinate pair or GeoJSON');
384
+ }
385
+ point = pointToArray(operand);
386
+ distance = value => {
387
+ if (!isIndexable(value)) {
388
+ return null;
389
+ }
390
+ return distanceCoordinatePairs(point, value);
391
+ };
392
+ }
393
+ return branchedValues => {
394
+ // There might be multiple points in the document that match the given
395
+ // field. Only one of them needs to be within $maxDistance, but we need to
396
+ // evaluate all of them and use the nearest one for the implicit sort
397
+ // specifier. (That's why we can't just use ELEMENT_OPERATORS here.)
398
+ //
399
+ // Note: This differs from MongoDB's implementation, where a document will
400
+ // actually show up *multiple times* in the result set, with one entry for
401
+ // each within-$maxDistance branching point.
402
+ const result = { result: false, distance: undefined, arrayIndices: undefined };
403
+ expandArraysInBranches(branchedValues).every(branch => {
404
+ // if operation is an update, don't skip branches, just return the first
405
+ // one (#3599)
406
+ let curDistance;
407
+ if (!matcher._isUpdate) {
408
+ if (!(typeof branch.value === 'object')) {
409
+ return true;
410
+ }
411
+ curDistance = distance(branch.value);
412
+ // Skip branches that aren't real points or are too far away.
413
+ if (curDistance === null || curDistance > maxDistance) {
414
+ return true;
415
+ }
416
+ // Skip anything that's a tie.
417
+ if (result.distance !== undefined && result.distance <= curDistance) {
418
+ return true;
419
+ }
420
+ }
421
+ result.result = true;
422
+ result.distance = curDistance;
423
+ if (branch.arrayIndices) {
424
+ result.arrayIndices = branch.arrayIndices;
425
+ }
426
+ else {
427
+ delete result.arrayIndices;
428
+ }
429
+ return !matcher._isUpdate;
430
+ });
431
+ return result;
432
+ };
433
+ },
434
+ };
435
+ // NB: We are cheating and using this function to implement 'AND' for both
436
+ // 'document matchers' and 'branched matchers'. They both return result objects
437
+ // but the argument is different: for the former it's a whole doc, whereas for
438
+ // the latter it's an array of 'branched values'.
439
+ function andSomeMatchers(subMatchers) {
440
+ if (subMatchers.length === 0) {
441
+ return everythingMatcher;
442
+ }
443
+ if (subMatchers.length === 1) {
444
+ return subMatchers[0];
445
+ }
446
+ return docOrBranches => {
447
+ const match = {};
448
+ match.result = subMatchers.every(fn => {
449
+ const subResult = fn(docOrBranches);
450
+ // Copy a 'distance' number out of the first sub-matcher that has
451
+ // one. Yes, this means that if there are multiple $near fields in a
452
+ // query, something arbitrary happens; this appears to be consistent with
453
+ // Mongo.
454
+ if (subResult.result &&
455
+ subResult.distance !== undefined &&
456
+ match.distance === undefined) {
457
+ match.distance = subResult.distance;
458
+ }
459
+ // Similarly, propagate arrayIndices from sub-matchers... but to match
460
+ // MongoDB behavior, this time the *last* sub-matcher with arrayIndices
461
+ // wins.
462
+ if (subResult.result && subResult.arrayIndices) {
463
+ match.arrayIndices = subResult.arrayIndices;
464
+ }
465
+ return subResult.result;
466
+ });
467
+ // If we didn't actually match, forget any extra metadata we came up with.
468
+ if (!match.result) {
469
+ delete match.distance;
470
+ delete match.arrayIndices;
471
+ }
472
+ return match;
473
+ };
474
+ }
475
+ const andDocumentMatchers = andSomeMatchers;
476
+ const andBranchedMatchers = andSomeMatchers;
477
+ function compileArrayOfDocumentSelectors(selectors, matcher, inElemMatch) {
478
+ if (!Array.isArray(selectors) || selectors.length === 0) {
479
+ throw Error('$and/$or/$nor must be nonempty array');
480
+ }
481
+ return selectors.map(subSelector => {
482
+ if (!_isPlainObject(subSelector)) {
483
+ throw Error('$or/$and/$nor entries need to be full objects');
484
+ }
485
+ return compileDocumentSelector(subSelector, matcher, { inElemMatch });
486
+ });
487
+ }
488
+ // Takes in a selector that could match a full document (eg, the original
489
+ // selector). Returns a function mapping document->result object.
490
+ //
491
+ // matcher is the Matcher object we are compiling.
492
+ //
493
+ // If this is the root document selector (ie, not wrapped in $and or the like),
494
+ // then isRoot is true. (This is used by $near.)
495
+ function compileDocumentSelector(docSelector, matcher, options = {}) {
496
+ const docMatchers = Object.keys(docSelector).map(key => {
497
+ const subSelector = docSelector[key];
498
+ if (key.substr(0, 1) === '$') {
499
+ // Outer operators are either logical operators (they recurse back into
500
+ // this function), or $where.
501
+ if (!exports.hasOwn.call(LOGICAL_OPERATORS, key)) {
502
+ throw new Error(`Unrecognized logical operator: ${key}`);
503
+ }
504
+ matcher._isSimple = false;
505
+ return LOGICAL_OPERATORS[key](subSelector, matcher, options.inElemMatch);
506
+ }
507
+ // Record this path, but only if we aren't in an elemMatcher, since in an
508
+ // elemMatch this is a path inside an object in an array, not in the doc
509
+ // root.
510
+ if (!options.inElemMatch) {
511
+ matcher._recordPathUsed(key);
512
+ }
513
+ // Don't add a matcher if subSelector is a function -- this is to match
514
+ // the behavior of Meteor on the server (inherited from the node mongodb
515
+ // driver), which is to ignore any part of a selector which is a function.
516
+ if (typeof subSelector === 'function') {
517
+ return undefined;
518
+ }
519
+ const lookUpByIndex = makeLookupFunction(key);
520
+ const valueMatcher = compileValueSelector(subSelector, matcher, options.isRoot);
521
+ return doc => valueMatcher(lookUpByIndex(doc));
522
+ }).filter(Boolean);
523
+ return andDocumentMatchers(docMatchers);
524
+ }
525
+ exports.compileDocumentSelector = compileDocumentSelector;
526
+ // Takes in a selector that could match a key-indexed value in a document; eg,
527
+ // {$gt: 5, $lt: 9}, or a regular expression, or any non-expression object (to
528
+ // indicate equality). Returns a branched matcher: a function mapping
529
+ // [branched value]->result object.
530
+ function compileValueSelector(valueSelector, matcher, isRoot) {
531
+ if (valueSelector instanceof RegExp) {
532
+ matcher._isSimple = false;
533
+ return convertElementMatcherToBranchedMatcher(regexpElementMatcher(valueSelector));
534
+ }
535
+ if (isOperatorObject(valueSelector)) {
536
+ return operatorBranchedMatcher(valueSelector, matcher, isRoot);
537
+ }
538
+ return convertElementMatcherToBranchedMatcher(equalityElementMatcher(valueSelector));
539
+ }
540
+ // Given an element matcher (which evaluates a single value), returns a branched
541
+ // value (which evaluates the element matcher on all the branches and returns a
542
+ // more structured return value possibly including arrayIndices).
543
+ function convertElementMatcherToBranchedMatcher(elementMatcher, options = {}) {
544
+ return (branches) => {
545
+ const expanded = options.dontExpandLeafArrays
546
+ ? branches
547
+ : expandArraysInBranches(branches, options.dontIncludeLeafArrays);
548
+ const match = { result: false, arrayIndices: [] };
549
+ match.result = expanded.some(element => {
550
+ let matched = elementMatcher(element.value);
551
+ // Special case for $elemMatch: it means "true, and use this as an array
552
+ // index if I didn't already have one".
553
+ if (typeof matched === 'number') {
554
+ // XXX This code dates from when we only stored a single array index
555
+ // (for the outermost array). Should we be also including deeper array
556
+ // indices from the $elemMatch match?
557
+ if (!element.arrayIndices) {
558
+ element.arrayIndices = [matched];
559
+ }
560
+ matched = true;
561
+ }
562
+ // If some element matched, and it's tagged with array indices, include
563
+ // those indices in our result object.
564
+ if (matched && element.arrayIndices) {
565
+ match.arrayIndices = element.arrayIndices;
566
+ }
567
+ return matched;
568
+ });
569
+ return match;
570
+ };
571
+ }
572
+ // Helpers for $near.
573
+ function distanceCoordinatePairs(a, b) {
574
+ const pointA = pointToArray(a);
575
+ const pointB = pointToArray(b);
576
+ return Math.hypot(pointA[0] - pointB[0], pointA[1] - pointB[1]);
577
+ }
578
+ // Takes something that is not an operator object and returns an element matcher
579
+ // for equality with that thing.
580
+ function equalityElementMatcher(elementSelector) {
581
+ if (isOperatorObject(elementSelector)) {
582
+ throw Error('Can\'t create equalityValueSelector for operator object');
583
+ }
584
+ // Special-case: null and undefined are equal (if you got undefined in there
585
+ // somewhere, or if you got it due to some branch being non-existent in the
586
+ // weird special case), even though they aren't with EJSON.equals.
587
+ // undefined or null
588
+ if (elementSelector == null) {
589
+ return value => value == null;
590
+ }
591
+ return value => exports._f._equal(elementSelector, value);
592
+ }
593
+ exports.equalityElementMatcher = equalityElementMatcher;
594
+ function everythingMatcher(docOrBranchedValues) {
595
+ return { result: true };
596
+ }
597
+ function expandArraysInBranches(branches, skipTheArrays) {
598
+ const branchesOut = [];
599
+ branches.forEach(branch => {
600
+ const thisIsArray = Array.isArray(branch.value);
601
+ // We include the branch itself, *UNLESS* we it's an array that we're going
602
+ // to iterate and we're told to skip arrays. (That's right, we include some
603
+ // arrays even skipTheArrays is true: these are arrays that were found via
604
+ // explicit numerical indices.)
605
+ if (!(skipTheArrays && thisIsArray && !branch.dontIterate)) {
606
+ branchesOut.push({ arrayIndices: branch.arrayIndices, value: branch.value });
607
+ }
608
+ if (thisIsArray && !branch.dontIterate) {
609
+ branch.value.forEach((value, i) => {
610
+ branchesOut.push({
611
+ arrayIndices: (branch.arrayIndices || []).concat(i),
612
+ value
613
+ });
614
+ });
615
+ }
616
+ });
617
+ return branchesOut;
618
+ }
619
+ exports.expandArraysInBranches = expandArraysInBranches;
620
+ // Helpers for $bitsAllSet/$bitsAnySet/$bitsAllClear/$bitsAnyClear.
621
+ function getOperandBitmask(operand, selector) {
622
+ // numeric bitmask
623
+ // You can provide a numeric bitmask to be matched against the operand field.
624
+ // It must be representable as a non-negative 32-bit signed integer.
625
+ // Otherwise, $bitsAllSet will return an error.
626
+ if (Number.isInteger(operand) && operand >= 0) {
627
+ return new Uint8Array(new Int32Array([operand]).buffer);
628
+ }
629
+ // bindata bitmask
630
+ // You can also use an arbitrarily large BinData instance as a bitmask.
631
+ if ((0, ejson_1.isBinary)(operand)) {
632
+ return new Uint8Array(operand.buffer);
633
+ }
634
+ // position list
635
+ // If querying a list of bit positions, each <position> must be a non-negative
636
+ // integer. Bit positions start at 0 from the least significant bit.
637
+ if (Array.isArray(operand) &&
638
+ operand.every(x => Number.isInteger(x) && x >= 0)) {
639
+ const buffer = new ArrayBuffer((Math.max(...operand) >> 3) + 1);
640
+ const view = new Uint8Array(buffer);
641
+ operand.forEach(x => {
642
+ view[x >> 3] |= 1 << (x & 0x7);
643
+ });
644
+ return view;
645
+ }
646
+ // bad operand
647
+ throw Error(`operand to ${selector} must be a numeric bitmask (representable as a ` +
648
+ 'non-negative 32-bit signed integer), a bindata bitmask or an array with ' +
649
+ 'bit positions (non-negative integers)');
650
+ }
651
+ function getValueBitmask(value, length) {
652
+ // The field value must be either numerical or a BinData instance. Otherwise,
653
+ // $bits... will not match the current document.
654
+ // numerical
655
+ if (Number.isSafeInteger(value)) {
656
+ // $bits... will not match numerical values that cannot be represented as a
657
+ // signed 64-bit integer. This can be the case if a value is either too
658
+ // large or small to fit in a signed 64-bit integer, or if it has a
659
+ // fractional component.
660
+ const buffer = new ArrayBuffer(Math.max(length, 2 * Uint32Array.BYTES_PER_ELEMENT));
661
+ let view32 = new Uint32Array(buffer, 0, 2);
662
+ view32[0] = value % ((1 << 16) * (1 << 16)) | 0;
663
+ view32[1] = value / ((1 << 16) * (1 << 16)) | 0;
664
+ // sign extension
665
+ if (value < 0) {
666
+ const view8 = new Uint8Array(buffer, 2);
667
+ view8.forEach((byte, i) => {
668
+ view8[i] = 0xff;
669
+ });
670
+ }
671
+ return new Uint8Array(buffer);
672
+ }
673
+ // bindata
674
+ if ((0, ejson_1.isBinary)(value)) {
675
+ return new Uint8Array(value.buffer);
676
+ }
677
+ // no match
678
+ return false;
679
+ }
680
+ // Actually inserts a key value into the selector document
681
+ // However, this checks there is no ambiguity in setting
682
+ // the value for the given key, throws otherwise
683
+ function insertIntoDocument(document, key, value) {
684
+ Object.keys(document).forEach(existingKey => {
685
+ if ((existingKey.length > key.length && existingKey.indexOf(`${key}.`) === 0) ||
686
+ (key.length > existingKey.length && key.indexOf(`${existingKey}.`) === 0)) {
687
+ throw new Error(`cannot infer query fields to set, both paths '${existingKey}' and ` +
688
+ `'${key}' are matched`);
689
+ }
690
+ else if (existingKey === key) {
691
+ throw new Error(`cannot infer query fields to set, path '${key}' is matched twice`);
692
+ }
693
+ });
694
+ document[key] = value;
695
+ }
696
+ // Returns a branched matcher that matches iff the given matcher does not.
697
+ // Note that this implicitly "deMorganizes" the wrapped function. ie, it
698
+ // means that ALL branch values need to fail to match innerBranchedMatcher.
699
+ function invertBranchedMatcher(branchedMatcher) {
700
+ return branchValues => {
701
+ // We explicitly choose to strip arrayIndices here: it doesn't make sense to
702
+ // say "update the array element that does not match something", at least
703
+ // in mongo-land.
704
+ return { result: !branchedMatcher(branchValues).result };
705
+ };
706
+ }
707
+ function isIndexable(obj) {
708
+ return Array.isArray(obj) || _isPlainObject(obj);
709
+ }
710
+ exports.isIndexable = isIndexable;
711
+ function isNumericKey(s) {
712
+ return /^[0-9]+$/.test(s);
713
+ }
714
+ exports.isNumericKey = isNumericKey;
715
+ // Returns true if this is an object with at least one key and all keys begin
716
+ // with $. Unless inconsistentOK is set, throws if some keys begin with $ and
717
+ // others don't.
718
+ function isOperatorObject(valueSelector, inconsistentOK) {
719
+ if (!_isPlainObject(valueSelector)) {
720
+ return false;
721
+ }
722
+ let theseAreOperators = undefined;
723
+ Object.keys(valueSelector).forEach(selKey => {
724
+ const thisIsOperator = selKey.substr(0, 1) === '$' || selKey === 'diff';
725
+ if (theseAreOperators === undefined) {
726
+ theseAreOperators = thisIsOperator;
727
+ }
728
+ else if (theseAreOperators !== thisIsOperator) {
729
+ if (!inconsistentOK) {
730
+ throw new Error(`Inconsistent operator: ${JSON.stringify(valueSelector)}`);
731
+ }
732
+ theseAreOperators = false;
733
+ }
734
+ });
735
+ return !!theseAreOperators; // {} has no operators
736
+ }
737
+ exports.isOperatorObject = isOperatorObject;
738
+ // Helper for $lt/$gt/$lte/$gte.
739
+ function makeInequality(cmpValueComparator) {
740
+ return {
741
+ compileElementSelector(operand) {
742
+ // Arrays never compare false with non-arrays for any inequality.
743
+ // XXX This was behavior we observed in pre-release MongoDB 2.5, but
744
+ // it seems to have been reverted.
745
+ // See https://jira.mongodb.org/browse/SERVER-11444
746
+ if (Array.isArray(operand)) {
747
+ return () => false;
748
+ }
749
+ // Special case: consider undefined and null the same (so true with
750
+ // $gte/$lte).
751
+ if (operand === undefined) {
752
+ operand = null;
753
+ }
754
+ const operandType = exports._f._type(operand);
755
+ return value => {
756
+ if (value === undefined) {
757
+ value = null;
758
+ }
759
+ // Comparisons are never true among things of different type (except
760
+ // null vs undefined).
761
+ if (exports._f._type(value) !== operandType) {
762
+ return false;
763
+ }
764
+ return cmpValueComparator(exports._f._cmp(value, operand));
765
+ };
766
+ },
767
+ };
768
+ }
769
+ // makeLookupFunction(key) returns a lookup function.
770
+ //
771
+ // A lookup function takes in a document and returns an array of matching
772
+ // branches. If no arrays are found while looking up the key, this array will
773
+ // have exactly one branches (possibly 'undefined', if some segment of the key
774
+ // was not found).
775
+ //
776
+ // If arrays are found in the middle, this can have more than one element, since
777
+ // we 'branch'. When we 'branch', if there are more key segments to look up,
778
+ // then we only pursue branches that are plain objects (not arrays or scalars).
779
+ // This means we can actually end up with no branches!
780
+ //
781
+ // We do *NOT* branch on arrays that are found at the end (ie, at the last
782
+ // dotted member of the key). We just return that array; if you want to
783
+ // effectively 'branch' over the array's values, post-process the lookup
784
+ // function with expandArraysInBranches.
785
+ //
786
+ // Each branch is an object with keys:
787
+ // - value: the value at the branch
788
+ // - dontIterate: an optional bool; if true, it means that 'value' is an array
789
+ // that expandArraysInBranches should NOT expand. This specifically happens
790
+ // when there is a numeric index in the key, and ensures the
791
+ // perhaps-surprising MongoDB behavior where {'a.0': 5} does NOT
792
+ // match {a: [[5]]}.
793
+ // - arrayIndices: if any array indexing was done during lookup (either due to
794
+ // explicit numeric indices or implicit branching), this will be an array of
795
+ // the array indices used, from outermost to innermost; it is falsey or
796
+ // absent if no array index is used. If an explicit numeric index is used,
797
+ // the index will be followed in arrayIndices by the string 'x'.
798
+ //
799
+ // Note: arrayIndices is used for two purposes. First, it is used to
800
+ // implement the '$' modifier feature, which only ever looks at its first
801
+ // element.
802
+ //
803
+ // Second, it is used for sort key generation, which needs to be able to tell
804
+ // the difference between different paths. Moreover, it needs to
805
+ // differentiate between explicit and implicit branching, which is why
806
+ // there's the somewhat hacky 'x' entry: this means that explicit and
807
+ // implicit array lookups will have different full arrayIndices paths. (That
808
+ // code only requires that different paths have different arrayIndices; it
809
+ // doesn't actually 'parse' arrayIndices. As an alternative, arrayIndices
810
+ // could contain objects with flags like 'implicit', but I think that only
811
+ // makes the code surrounding them more complex.)
812
+ //
813
+ // (By the way, this field ends up getting passed around a lot without
814
+ // cloning, so never mutate any arrayIndices field/var in this package!)
815
+ //
816
+ //
817
+ // At the top level, you may only pass in a plain object or array.
818
+ //
819
+ // See the test 'minimongo - lookup' for some examples of what lookup functions
820
+ // return.
821
+ function makeLookupFunction(key, options = {}) {
822
+ const parts = key.split('.');
823
+ const firstPart = parts.length ? parts[0] : '';
824
+ const lookupRest = (parts.length > 1 &&
825
+ makeLookupFunction(parts.slice(1).join('.'), options));
826
+ const omitUnnecessaryFields = result => {
827
+ if (!result.dontIterate) {
828
+ delete result.dontIterate;
829
+ }
830
+ if (result.arrayIndices && !result.arrayIndices.length) {
831
+ delete result.arrayIndices;
832
+ }
833
+ return result;
834
+ };
835
+ // Doc will always be a plain object or an array.
836
+ // apply an explicit numeric index, an array.
837
+ return (doc, arrayIndices = []) => {
838
+ if (Array.isArray(doc)) {
839
+ // If we're being asked to do an invalid lookup into an array (non-integer
840
+ // or out-of-bounds), return no results (which is different from returning
841
+ // a single undefined result, in that `null` equality checks won't match).
842
+ if (!(isNumericKey(firstPart) && firstPart < doc.length)) {
843
+ return [];
844
+ }
845
+ // Remember that we used this array index. Include an 'x' to indicate that
846
+ // the previous index came from being considered as an explicit array
847
+ // index (not branching).
848
+ arrayIndices = arrayIndices.concat(+firstPart, 'x');
849
+ }
850
+ // Do our first lookup.
851
+ const firstLevel = doc[firstPart];
852
+ // If there is no deeper to dig, return what we found.
853
+ //
854
+ // If what we found is an array, most value selectors will choose to treat
855
+ // the elements of the array as matchable values in their own right, but
856
+ // that's done outside of the lookup function. (Exceptions to this are $size
857
+ // and stuff relating to $elemMatch. eg, {a: {$size: 2}} does not match {a:
858
+ // [[1, 2]]}.)
859
+ //
860
+ // That said, if we just did an *explicit* array lookup (on doc) to find
861
+ // firstLevel, and firstLevel is an array too, we do NOT want value
862
+ // selectors to iterate over it. eg, {'a.0': 5} does not match {a: [[5]]}.
863
+ // So in that case, we mark the return value as 'don't iterate'.
864
+ if (!lookupRest) {
865
+ return [omitUnnecessaryFields({
866
+ arrayIndices,
867
+ dontIterate: Array.isArray(doc) && Array.isArray(firstLevel),
868
+ value: firstLevel
869
+ })];
870
+ }
871
+ // We need to dig deeper. But if we can't, because what we've found is not
872
+ // an array or plain object, we're done. If we just did a numeric index into
873
+ // an array, we return nothing here (this is a change in Mongo 2.5 from
874
+ // Mongo 2.4, where {'a.0.b': null} stopped matching {a: [5]}). Otherwise,
875
+ // return a single `undefined` (which can, for example, match via equality
876
+ // with `null`).
877
+ if (!isIndexable(firstLevel)) {
878
+ if (Array.isArray(doc)) {
879
+ return [];
880
+ }
881
+ return [omitUnnecessaryFields({ arrayIndices, value: undefined })];
882
+ }
883
+ const result = [];
884
+ const appendToResult = more => {
885
+ result.push(...more);
886
+ };
887
+ // Dig deeper: look up the rest of the parts on whatever we've found.
888
+ // (lookupRest is smart enough to not try to do invalid lookups into
889
+ // firstLevel if it's an array.)
890
+ appendToResult(lookupRest(firstLevel, arrayIndices));
891
+ // If we found an array, then in *addition* to potentially treating the next
892
+ // part as a literal integer lookup, we should also 'branch': try to look up
893
+ // the rest of the parts on each array element in parallel.
894
+ //
895
+ // In this case, we *only* dig deeper into array elements that are plain
896
+ // objects. (Recall that we only got this far if we have further to dig.)
897
+ // This makes sense: we certainly don't dig deeper into non-indexable
898
+ // objects. And it would be weird to dig into an array: it's simpler to have
899
+ // a rule that explicit integer indexes only apply to an outer array, not to
900
+ // an array you find after a branching search.
901
+ //
902
+ // In the special case of a numeric part in a *sort selector* (not a query
903
+ // selector), we skip the branching: we ONLY allow the numeric part to mean
904
+ // 'look up this index' in that case, not 'also look up this index in all
905
+ // the elements of the array'.
906
+ if (Array.isArray(firstLevel) &&
907
+ !(isNumericKey(parts[1]) && options.forSort)) {
908
+ firstLevel.forEach((branch, arrayIndex) => {
909
+ if (_isPlainObject(branch)) {
910
+ appendToResult(lookupRest(branch, arrayIndices.concat(arrayIndex)));
911
+ }
912
+ });
913
+ }
914
+ return result;
915
+ };
916
+ }
917
+ exports.makeLookupFunction = makeLookupFunction;
918
+ function MinimongoError(message, options = {}) {
919
+ if (typeof message === 'string' && options.field) {
920
+ message += ` for field '${options.field}'`;
921
+ }
922
+ const error = new Error(message);
923
+ error.name = 'MinimongoError';
924
+ return error;
925
+ }
926
+ function nothingMatcher(docOrBranchedValues) {
927
+ return { result: false };
928
+ }
929
+ exports.nothingMatcher = nothingMatcher;
930
+ // Takes an operator object (an object with $ keys) and returns a branched
931
+ // matcher for it.
932
+ function operatorBranchedMatcher(valueSelector, matcher, isRoot) {
933
+ // Each valueSelector works separately on the various branches. So one
934
+ // operator can match one branch and another can match another branch. This
935
+ // is OK.
936
+ const operatorMatchers = Object.keys(valueSelector).map(operator => {
937
+ const operand = valueSelector[operator];
938
+ const simpleRange = (['$lt', '$lte', '$gt', '$gte'].includes(operator) &&
939
+ typeof operand === 'number');
940
+ const simpleEquality = (['$ne', '$eq'].includes(operator) &&
941
+ operand !== Object(operand));
942
+ const simpleInclusion = (['$in', '$nin'].includes(operator)
943
+ && Array.isArray(operand)
944
+ && !operand.some(x => x === Object(x)));
945
+ if (!(simpleRange || simpleInclusion || simpleEquality)) {
946
+ matcher._isSimple = false;
947
+ }
948
+ if (exports.hasOwn.call(VALUE_OPERATORS, operator)) {
949
+ return VALUE_OPERATORS[operator](operand, valueSelector, matcher, isRoot);
950
+ }
951
+ if (exports.hasOwn.call(exports.ELEMENT_OPERATORS, operator)) {
952
+ const options = exports.ELEMENT_OPERATORS[operator];
953
+ return convertElementMatcherToBranchedMatcher(options.compileElementSelector(operand, valueSelector, matcher), options);
954
+ }
955
+ throw new Error(`Unrecognized operator: ${operator}`);
956
+ });
957
+ return andBranchedMatchers(operatorMatchers);
958
+ }
959
+ // paths - Array: list of mongo style paths
960
+ // newLeafFn - Function: of form function(path) should return a scalar value to
961
+ // put into list created for that path
962
+ // conflictFn - Function: of form function(node, path, fullPath) is called
963
+ // when building a tree path for 'fullPath' node on
964
+ // 'path' was already a leaf with a value. Must return a
965
+ // conflict resolution.
966
+ // initial tree - Optional Object: starting tree.
967
+ // @returns - Object: tree represented as a set of nested objects
968
+ function pathsToTree(paths, newLeafFn, conflictFn, root = {}) {
969
+ paths.forEach(path => {
970
+ const pathArray = path.split('.');
971
+ let tree = root;
972
+ // use .every just for iteration with break
973
+ const success = pathArray.slice(0, -1).every((key, i) => {
974
+ if (!exports.hasOwn.call(tree, key)) {
975
+ tree[key] = {};
976
+ }
977
+ else if (tree[key] !== Object(tree[key])) {
978
+ tree[key] = conflictFn(tree[key], pathArray.slice(0, i + 1).join('.'), path);
979
+ // break out of loop if we are failing for this path
980
+ if (tree[key] !== Object(tree[key])) {
981
+ return false;
982
+ }
983
+ }
984
+ tree = tree[key];
985
+ return true;
986
+ });
987
+ if (success) {
988
+ const lastKey = pathArray[pathArray.length - 1];
989
+ if (exports.hasOwn.call(tree, lastKey)) {
990
+ tree[lastKey] = conflictFn(tree[lastKey], path, path);
991
+ }
992
+ else {
993
+ tree[lastKey] = newLeafFn(path);
994
+ }
995
+ }
996
+ });
997
+ return root;
998
+ }
999
+ exports.pathsToTree = pathsToTree;
1000
+ // Makes sure we get 2 elements array and assume the first one to be x and
1001
+ // the second one to y no matter what user passes.
1002
+ // In case user passes { lon: x, lat: y } returns [x, y]
1003
+ function pointToArray(point) {
1004
+ return Array.isArray(point) ? point.slice() : [point.x, point.y];
1005
+ }
1006
+ // Creating a document from an upsert is quite tricky.
1007
+ // E.g. this selector: {"$or": [{"b.foo": {"$all": ["bar"]}}]}, should result
1008
+ // in: {"b.foo": "bar"}
1009
+ // But this selector: {"$or": [{"b": {"foo": {"$all": ["bar"]}}}]} should throw
1010
+ // an error
1011
+ // Some rules (found mainly with trial & error, so there might be more):
1012
+ // - handle all childs of $and (or implicit $and)
1013
+ // - handle $or nodes with exactly 1 child
1014
+ // - ignore $or nodes with more than 1 child
1015
+ // - ignore $nor and $not nodes
1016
+ // - throw when a value can not be set unambiguously
1017
+ // - every value for $all should be dealt with as separate $eq-s
1018
+ // - threat all children of $all as $eq setters (=> set if $all.length === 1,
1019
+ // otherwise throw error)
1020
+ // - you can not mix '$'-prefixed keys and non-'$'-prefixed keys
1021
+ // - you can only have dotted keys on a root-level
1022
+ // - you can not have '$'-prefixed keys more than one-level deep in an object
1023
+ // Handles one key/value pair to put in the selector document
1024
+ function populateDocumentWithKeyValue(document, key, value) {
1025
+ if (value && Object.getPrototypeOf(value) === Object.prototype) {
1026
+ populateDocumentWithObject(document, key, value);
1027
+ }
1028
+ else if (!(value instanceof RegExp)) {
1029
+ insertIntoDocument(document, key, value);
1030
+ }
1031
+ }
1032
+ // Handles a key, value pair to put in the selector document
1033
+ // if the value is an object
1034
+ function populateDocumentWithObject(document, key, value) {
1035
+ const keys = Object.keys(value);
1036
+ const unprefixedKeys = keys.filter(op => op[0] !== '$');
1037
+ if (unprefixedKeys.length > 0 || !keys.length) {
1038
+ // Literal (possibly empty) object ( or empty object )
1039
+ // Don't allow mixing '$'-prefixed with non-'$'-prefixed fields
1040
+ if (keys.length !== unprefixedKeys.length) {
1041
+ throw new Error(`unknown operator: ${unprefixedKeys[0]}`);
1042
+ }
1043
+ validateObject(value, key);
1044
+ insertIntoDocument(document, key, value);
1045
+ }
1046
+ else {
1047
+ Object.keys(value).forEach(op => {
1048
+ const object = value[op];
1049
+ if (op === '$eq') {
1050
+ populateDocumentWithKeyValue(document, key, object);
1051
+ }
1052
+ else if (op === '$all') {
1053
+ // every value for $all should be dealt with as separate $eq-s
1054
+ object.forEach(element => populateDocumentWithKeyValue(document, key, element));
1055
+ }
1056
+ });
1057
+ }
1058
+ }
1059
+ // Fills a document with certain fields from an upsert selector
1060
+ function populateDocumentWithQueryFields(query, document = {}) {
1061
+ if (Object.getPrototypeOf(query) === Object.prototype) {
1062
+ // handle implicit $and
1063
+ Object.keys(query).forEach(key => {
1064
+ const value = query[key];
1065
+ if (key === '$and') {
1066
+ // handle explicit $and
1067
+ value.forEach(element => populateDocumentWithQueryFields(element, document));
1068
+ }
1069
+ else if (key === '$or') {
1070
+ // handle $or nodes with exactly 1 child
1071
+ if (value.length === 1) {
1072
+ populateDocumentWithQueryFields(value[0], document);
1073
+ }
1074
+ }
1075
+ else if (key[0] !== '$') {
1076
+ // Ignore other '$'-prefixed logical selectors
1077
+ populateDocumentWithKeyValue(document, key, value);
1078
+ }
1079
+ });
1080
+ }
1081
+ return document;
1082
+ }
1083
+ exports.populateDocumentWithQueryFields = populateDocumentWithQueryFields;
1084
+ // Traverses the keys of passed projection and constructs a tree where all
1085
+ // leaves are either all True or all False
1086
+ // @returns Object:
1087
+ // - tree - Object - tree representation of keys involved in projection
1088
+ // (exception for '_id' as it is a special case handled separately)
1089
+ // - including - Boolean - "take only certain fields" type of projection
1090
+ function projectionDetails(fields) {
1091
+ // Find the non-_id keys (_id is handled specially because it is included
1092
+ // unless explicitly excluded). Sort the keys, so that our code to detect
1093
+ // overlaps like 'foo' and 'foo.bar' can assume that 'foo' comes first.
1094
+ let fieldsKeys = Object.keys(fields).sort();
1095
+ // If _id is the only field in the projection, do not remove it, since it is
1096
+ // required to determine if this is an exclusion or exclusion. Also keep an
1097
+ // inclusive _id, since inclusive _id follows the normal rules about mixing
1098
+ // inclusive and exclusive fields. If _id is not the only field in the
1099
+ // projection and is exclusive, remove it so it can be handled later by a
1100
+ // special case, since exclusive _id is always allowed.
1101
+ if (!(fieldsKeys.length === 1 && fieldsKeys[0] === '_id') &&
1102
+ !(fieldsKeys.includes('_id') && fields._id)) {
1103
+ fieldsKeys = fieldsKeys.filter(key => key !== '_id');
1104
+ }
1105
+ let including = null; // Unknown
1106
+ fieldsKeys.forEach(keyPath => {
1107
+ const rule = !!fields[keyPath];
1108
+ if (including === null) {
1109
+ including = rule;
1110
+ }
1111
+ // This error message is copied from MongoDB shell
1112
+ if (including !== rule) {
1113
+ throw MinimongoError('You cannot currently mix including and excluding fields.');
1114
+ }
1115
+ });
1116
+ const projectionRulesTree = pathsToTree(fieldsKeys, path => including, (node, path, fullPath) => {
1117
+ // Check passed projection fields' keys: If you have two rules such as
1118
+ // 'foo.bar' and 'foo.bar.baz', then the result becomes ambiguous. If
1119
+ // that happens, there is a probability you are doing something wrong,
1120
+ // framework should notify you about such mistake earlier on cursor
1121
+ // compilation step than later during runtime. Note, that real mongo
1122
+ // doesn't do anything about it and the later rule appears in projection
1123
+ // project, more priority it takes.
1124
+ //
1125
+ // Example, assume following in mongo shell:
1126
+ // > db.coll.insert({ a: { b: 23, c: 44 } })
1127
+ // > db.coll.find({}, { 'a': 1, 'a.b': 1 })
1128
+ // {"_id": ObjectId("520bfe456024608e8ef24af3"), "a": {"b": 23}}
1129
+ // > db.coll.find({}, { 'a.b': 1, 'a': 1 })
1130
+ // {"_id": ObjectId("520bfe456024608e8ef24af3"), "a": {"b": 23, "c": 44}}
1131
+ //
1132
+ // Note, how second time the return set of keys is different.
1133
+ const currentPath = fullPath;
1134
+ const anotherPath = path;
1135
+ throw MinimongoError(`both ${currentPath} and ${anotherPath} found in fields option, ` +
1136
+ 'using both of them may trigger unexpected behavior. Did you mean to ' +
1137
+ 'use only one of them?');
1138
+ });
1139
+ return { including, tree: projectionRulesTree };
1140
+ }
1141
+ exports.projectionDetails = projectionDetails;
1142
+ // Takes a RegExp object and returns an element matcher.
1143
+ function regexpElementMatcher(regexp) {
1144
+ return value => {
1145
+ if (value instanceof RegExp) {
1146
+ return value.toString() === regexp.toString();
1147
+ }
1148
+ // Regexps only work against strings.
1149
+ if (typeof value !== 'string') {
1150
+ return false;
1151
+ }
1152
+ // Reset regexp's state to avoid inconsistent matching for objects with the
1153
+ // same value on consecutive calls of regexp.test. This happens only if the
1154
+ // regexp has the 'g' flag. Also note that ES6 introduces a new flag 'y' for
1155
+ // which we should *not* change the lastIndex but MongoDB doesn't support
1156
+ // either of these flags.
1157
+ regexp.lastIndex = 0;
1158
+ return regexp.test(value);
1159
+ };
1160
+ }
1161
+ exports.regexpElementMatcher = regexpElementMatcher;
1162
+ // Validates the key in a path.
1163
+ // Objects that are nested more then 1 level cannot have dotted fields
1164
+ // or fields starting with '$'
1165
+ function validateKeyInPath(key, path) {
1166
+ if (key.includes('.')) {
1167
+ throw new Error(`The dotted field '${key}' in '${path}.${key} is not valid for storage.`);
1168
+ }
1169
+ if (key[0] === '$') {
1170
+ throw new Error(`The dollar ($) prefixed field '${path}.${key} is not valid for storage.`);
1171
+ }
1172
+ }
1173
+ // Recursively validates an object that is nested more than one level deep
1174
+ function validateObject(object, path) {
1175
+ if (object && Object.getPrototypeOf(object) === Object.prototype) {
1176
+ Object.keys(object).forEach(key => {
1177
+ validateKeyInPath(key, path);
1178
+ validateObject(object[key], path + '.' + key);
1179
+ });
1180
+ }
1181
+ }
1182
+ function _isPlainObject(x) {
1183
+ return x && exports._f._type(x) === 3;
1184
+ }
1185
+ // helpers used by compiled selector code
1186
+ exports._f = {
1187
+ // XXX for _all and _in, consider building 'inquery' at compile time..
1188
+ _type(v) {
1189
+ if (typeof v === 'number') {
1190
+ return 1;
1191
+ }
1192
+ if (typeof v === 'string') {
1193
+ return 2;
1194
+ }
1195
+ if (typeof v === 'boolean') {
1196
+ return 8;
1197
+ }
1198
+ if (Array.isArray(v)) {
1199
+ return 4;
1200
+ }
1201
+ if (v === null) {
1202
+ return 10;
1203
+ }
1204
+ // note that typeof(/x/) === "object"
1205
+ if (v instanceof RegExp) {
1206
+ return 11;
1207
+ }
1208
+ if (typeof v === 'function') {
1209
+ return 13;
1210
+ }
1211
+ if (v instanceof Date) {
1212
+ return 9;
1213
+ }
1214
+ if ((0, ejson_1.isBinary)(v)) {
1215
+ return 5;
1216
+ }
1217
+ if (v instanceof mongodb_1.ObjectId) {
1218
+ return 7;
1219
+ }
1220
+ if (v instanceof mongodb_1.Decimal128) {
1221
+ return 1;
1222
+ }
1223
+ // object
1224
+ return 3;
1225
+ // XXX support some/all of these:
1226
+ // 14, symbol
1227
+ // 15, javascript code with scope
1228
+ // 16, 18: 32-bit/64-bit integer
1229
+ // 17, timestamp
1230
+ // 255, minkey
1231
+ // 127, maxkey
1232
+ },
1233
+ // deep equality test: use for literal document and array matches
1234
+ _equal(a, b) {
1235
+ return (0, ejson_1.equals)(a, b, { keyOrderSensitive: true });
1236
+ },
1237
+ // maps a type code to a value that can be used to sort values of different
1238
+ // types
1239
+ _typeorder(t) {
1240
+ // http://www.mongodb.org/display/DOCS/What+is+the+Compare+Order+for+BSON+Types
1241
+ // XXX what is the correct sort position for Javascript code?
1242
+ // ('100' in the matrix below)
1243
+ // XXX minkey/maxkey
1244
+ return [
1245
+ -1,
1246
+ 1,
1247
+ 2,
1248
+ 3,
1249
+ 4,
1250
+ 5,
1251
+ -1,
1252
+ 6,
1253
+ 7,
1254
+ 8,
1255
+ 0,
1256
+ 9,
1257
+ -1,
1258
+ 100,
1259
+ 2,
1260
+ 100,
1261
+ 1,
1262
+ 8,
1263
+ 1 // 64-bit int
1264
+ ][t];
1265
+ },
1266
+ // compare two values of unknown type according to BSON ordering
1267
+ // semantics. (as an extension, consider 'undefined' to be less than
1268
+ // any other value.) return negative if a is less, positive if b is
1269
+ // less, or 0 if equal
1270
+ _cmp(a, b) {
1271
+ if (a === undefined) {
1272
+ return b === undefined ? 0 : -1;
1273
+ }
1274
+ if (b === undefined) {
1275
+ return 1;
1276
+ }
1277
+ let ta = exports._f._type(a);
1278
+ let tb = exports._f._type(b);
1279
+ const oa = exports._f._typeorder(ta);
1280
+ const ob = exports._f._typeorder(tb);
1281
+ if (oa !== ob) {
1282
+ return oa < ob ? -1 : 1;
1283
+ }
1284
+ // XXX need to implement this if we implement Symbol or integers, or
1285
+ // Timestamp
1286
+ if (ta !== tb) {
1287
+ throw Error('Missing type coercion logic in _cmp');
1288
+ }
1289
+ if (ta === 7) { // ObjectID
1290
+ // Convert to string.
1291
+ ta = tb = 2;
1292
+ a = a.toHexString();
1293
+ b = b.toHexString();
1294
+ }
1295
+ if (ta === 9) { // Date
1296
+ // Convert to millis.
1297
+ ta = tb = 1;
1298
+ a = isNaN(a) ? 0 : a.getTime();
1299
+ b = isNaN(b) ? 0 : b.getTime();
1300
+ }
1301
+ if (ta === 1) { // double
1302
+ if (a instanceof mongodb_1.Decimal128) {
1303
+ return mongodb_1.Decimal128.fromString((BigInt(a.toString()) - BigInt(b.toString())).toString());
1304
+ }
1305
+ else {
1306
+ return a - b;
1307
+ }
1308
+ }
1309
+ if (tb === 2) // string
1310
+ return a < b ? -1 : a === b ? 0 : 1;
1311
+ if (ta === 3) { // Object
1312
+ // this could be much more efficient in the expected case ...
1313
+ const toArray = object => {
1314
+ const result = [];
1315
+ Object.keys(object).forEach(key => {
1316
+ result.push(key, object[key]);
1317
+ });
1318
+ return result;
1319
+ };
1320
+ return exports._f._cmp(toArray(a), toArray(b));
1321
+ }
1322
+ if (ta === 4) { // Array
1323
+ for (let i = 0;; i++) {
1324
+ if (i === a.length) {
1325
+ return i === b.length ? 0 : -1;
1326
+ }
1327
+ if (i === b.length) {
1328
+ return 1;
1329
+ }
1330
+ const s = exports._f._cmp(a[i], b[i]);
1331
+ if (s !== 0) {
1332
+ return s;
1333
+ }
1334
+ }
1335
+ }
1336
+ if (ta === 5) { // binary
1337
+ // Surprisingly, a small binary blob is always less than a large one in
1338
+ // Mongo.
1339
+ if (a.length !== b.length) {
1340
+ return a.length - b.length;
1341
+ }
1342
+ for (let i = 0; i < a.length; i++) {
1343
+ if (a[i] < b[i]) {
1344
+ return -1;
1345
+ }
1346
+ if (a[i] > b[i]) {
1347
+ return 1;
1348
+ }
1349
+ }
1350
+ return 0;
1351
+ }
1352
+ if (ta === 8) { // boolean
1353
+ if (a) {
1354
+ return b ? 0 : 1;
1355
+ }
1356
+ return b ? -1 : 0;
1357
+ }
1358
+ if (ta === 10) // null
1359
+ return 0;
1360
+ if (ta === 11) // regexp
1361
+ throw Error('Sorting not supported on regular expression'); // XXX
1362
+ // 13: javascript code
1363
+ // 14: symbol
1364
+ // 15: javascript code with scope
1365
+ // 16: 32-bit integer
1366
+ // 17: timestamp
1367
+ // 18: 64-bit integer
1368
+ // 255: minkey
1369
+ // 127: maxkey
1370
+ if (ta === 13) // javascript code
1371
+ throw Error('Sorting not supported on Javascript code'); // XXX
1372
+ throw Error('Unknown type to sort');
1373
+ },
1374
+ };
1375
+ function _checkSupportedProjection(fields) {
1376
+ if (fields !== Object(fields) || Array.isArray(fields)) {
1377
+ throw MinimongoError('fields option must be an object');
1378
+ }
1379
+ Object.keys(fields).forEach(keyPath => {
1380
+ if (keyPath.split('.').includes('$')) {
1381
+ throw MinimongoError('Minimongo doesn\'t support $ operator in projections yet.');
1382
+ }
1383
+ const value = fields[keyPath];
1384
+ if (typeof value === 'object' &&
1385
+ ['$elemMatch', '$meta', '$slice'].some(key => exports.hasOwn.call(value, key))) {
1386
+ throw MinimongoError('Minimongo doesn\'t support operators in projections yet.');
1387
+ }
1388
+ if (![1, 0, true, false].includes(value)) {
1389
+ throw MinimongoError('Projection values should be one of 1, 0, true, or false');
1390
+ }
1391
+ });
1392
+ }
1393
+ exports._checkSupportedProjection = _checkSupportedProjection;
1394
+ // XXX need a strategy for passing the binding of $ into this
1395
+ // function, from the compiled selector
1396
+ //
1397
+ // maybe just {key.up.to.just.before.dollarsign: array_index}
1398
+ //
1399
+ // XXX atomicity: if one modification fails, do we roll back the whole
1400
+ // change?
1401
+ //
1402
+ // options:
1403
+ // - isInsert is set when _modify is being called to compute the document to
1404
+ // insert as part of an upsert operation. We use this primarily to figure
1405
+ // out when to set the fields in $setOnInsert, if present.
1406
+ function _modify(doc, modifier, options = {}) {
1407
+ if (!_isPlainObject(modifier)) {
1408
+ throw MinimongoError('Modifier must be an object');
1409
+ }
1410
+ // Make sure the caller can't mutate our data structures.
1411
+ modifier = (0, ejson_1.clone)(modifier);
1412
+ const isModifier = isOperatorObject(modifier);
1413
+ const newDoc = isModifier ? (0, ejson_1.clone)(doc) : modifier;
1414
+ if (isModifier) {
1415
+ // apply modifiers to the doc.
1416
+ Object.keys(modifier).forEach(operator => {
1417
+ // Treat $setOnInsert as $set if this is an insert.
1418
+ const setOnInsert = options.isInsert && operator === '$setOnInsert';
1419
+ const modFunc = MODIFIERS[setOnInsert ? '$set' : operator];
1420
+ const operand = modifier[operator];
1421
+ if (!modFunc) {
1422
+ throw MinimongoError(`Invalid modifier specified ${operator}`);
1423
+ }
1424
+ Object.keys(operand).forEach(keypath => {
1425
+ const arg = operand[keypath];
1426
+ if (keypath === '') {
1427
+ throw MinimongoError('An empty update path is not valid.');
1428
+ }
1429
+ const keyparts = keypath.split('.');
1430
+ if (!keyparts.every(Boolean)) {
1431
+ throw MinimongoError(`The update path '${keypath}' contains an empty field name, ` +
1432
+ 'which is not allowed.');
1433
+ }
1434
+ const target = findModTarget(newDoc, keyparts, {
1435
+ arrayIndices: options.arrayIndices,
1436
+ forbidArray: operator === '$rename',
1437
+ noCreate: NO_CREATE_MODIFIERS[operator]
1438
+ });
1439
+ modFunc(target, keyparts.pop(), arg, keypath, newDoc);
1440
+ });
1441
+ });
1442
+ if (doc._id && !(0, ejson_1.equals)(doc._id, newDoc._id)) {
1443
+ throw MinimongoError(`After applying the update to the document {_id: "${doc._id}", ...},` +
1444
+ ' the (immutable) field \'_id\' was found to have been altered to ' +
1445
+ `_id: "${newDoc._id}"`);
1446
+ }
1447
+ }
1448
+ else {
1449
+ if (doc._id && modifier._id && !(0, ejson_1.equals)(doc._id, modifier._id)) {
1450
+ throw MinimongoError(`The _id field cannot be changed from {_id: "${doc._id}"} to ` +
1451
+ `{_id: "${modifier._id}"}`);
1452
+ }
1453
+ // replace the whole document
1454
+ assertHasValidFieldNames(modifier);
1455
+ }
1456
+ // move new document into place.
1457
+ Object.keys(doc).forEach(key => {
1458
+ // Note: this used to be for (var key in doc) however, this does not
1459
+ // work right in Opera. Deleting from a doc while iterating over it
1460
+ // would sometimes cause opera to skip some keys.
1461
+ if (key !== '_id') {
1462
+ delete doc[key];
1463
+ }
1464
+ });
1465
+ Object.keys(newDoc).forEach(key => {
1466
+ doc[key] = newDoc[key];
1467
+ });
1468
+ }
1469
+ exports._modify = _modify;
1470
+ ;
1471
+ // checks if all field names in an object are valid
1472
+ function assertHasValidFieldNames(doc) {
1473
+ if (doc && typeof doc === 'object') {
1474
+ JSON.stringify(doc, (key, value) => {
1475
+ assertIsValidFieldName(key);
1476
+ return value;
1477
+ });
1478
+ }
1479
+ }
1480
+ const MODIFIERS = {
1481
+ $currentDate(target, field, arg) {
1482
+ if (typeof arg === 'object' && exports.hasOwn.call(arg, '$type')) {
1483
+ if (arg.$type !== 'date') {
1484
+ throw MinimongoError('Minimongo does currently only support the date type in ' +
1485
+ '$currentDate modifiers', { field });
1486
+ }
1487
+ }
1488
+ else if (arg !== true) {
1489
+ throw MinimongoError('Invalid $currentDate modifier', { field });
1490
+ }
1491
+ target[field] = new Date();
1492
+ },
1493
+ $min(target, field, arg) {
1494
+ if (typeof arg !== 'number') {
1495
+ throw MinimongoError('Modifier $min allowed for numbers only', { field });
1496
+ }
1497
+ if (field in target) {
1498
+ if (typeof target[field] !== 'number') {
1499
+ throw MinimongoError('Cannot apply $min modifier to non-number', { field });
1500
+ }
1501
+ if (target[field] > arg) {
1502
+ target[field] = arg;
1503
+ }
1504
+ }
1505
+ else {
1506
+ target[field] = arg;
1507
+ }
1508
+ },
1509
+ $max(target, field, arg) {
1510
+ if (typeof arg !== 'number') {
1511
+ throw MinimongoError('Modifier $max allowed for numbers only', { field });
1512
+ }
1513
+ if (field in target) {
1514
+ if (typeof target[field] !== 'number') {
1515
+ throw MinimongoError('Cannot apply $max modifier to non-number', { field });
1516
+ }
1517
+ if (target[field] < arg) {
1518
+ target[field] = arg;
1519
+ }
1520
+ }
1521
+ else {
1522
+ target[field] = arg;
1523
+ }
1524
+ },
1525
+ $inc(target, field, arg) {
1526
+ if (typeof arg !== 'number') {
1527
+ throw MinimongoError('Modifier $inc allowed for numbers only', { field });
1528
+ }
1529
+ if (field in target) {
1530
+ if (typeof target[field] !== 'number') {
1531
+ throw MinimongoError('Cannot apply $inc modifier to non-number', { field });
1532
+ }
1533
+ target[field] += arg;
1534
+ }
1535
+ else {
1536
+ target[field] = arg;
1537
+ }
1538
+ },
1539
+ $set(target, field, arg) {
1540
+ if (target !== Object(target)) { // not an array or an object
1541
+ const error = MinimongoError('Cannot set property on non-object field', { field });
1542
+ error.setPropertyError = true;
1543
+ throw error;
1544
+ }
1545
+ if (target === null) {
1546
+ const error = MinimongoError('Cannot set property on null', { field });
1547
+ error.setPropertyError = true;
1548
+ throw error;
1549
+ }
1550
+ assertHasValidFieldNames(arg);
1551
+ target[field] = arg;
1552
+ },
1553
+ $setOnInsert(target, field, arg) {
1554
+ // converted to `$set` in `_modify`
1555
+ },
1556
+ $unset(target, field, arg) {
1557
+ if (target !== undefined) {
1558
+ if (target instanceof Array) {
1559
+ if (field in target) {
1560
+ target[field] = null;
1561
+ }
1562
+ }
1563
+ else {
1564
+ delete target[field];
1565
+ }
1566
+ }
1567
+ },
1568
+ $push(target, field, arg) {
1569
+ if (target[field] === undefined) {
1570
+ target[field] = [];
1571
+ }
1572
+ if (!(target[field] instanceof Array)) {
1573
+ throw MinimongoError('Cannot apply $push modifier to non-array', { field });
1574
+ }
1575
+ if (!(arg && arg.$each)) {
1576
+ // Simple mode: not $each
1577
+ assertHasValidFieldNames(arg);
1578
+ target[field].push(arg);
1579
+ return;
1580
+ }
1581
+ // Fancy mode: $each (and maybe $slice and $sort and $position)
1582
+ const toPush = arg.$each;
1583
+ if (!(toPush instanceof Array)) {
1584
+ throw MinimongoError('$each must be an array', { field });
1585
+ }
1586
+ assertHasValidFieldNames(toPush);
1587
+ // Parse $position
1588
+ let position = undefined;
1589
+ if ('$position' in arg) {
1590
+ if (typeof arg.$position !== 'number') {
1591
+ throw MinimongoError('$position must be a numeric value', { field });
1592
+ }
1593
+ // XXX should check to make sure integer
1594
+ if (arg.$position < 0) {
1595
+ throw MinimongoError('$position in $push must be zero or positive', { field });
1596
+ }
1597
+ position = arg.$position;
1598
+ }
1599
+ // Parse $slice.
1600
+ let slice = undefined;
1601
+ if ('$slice' in arg) {
1602
+ if (typeof arg.$slice !== 'number') {
1603
+ throw MinimongoError('$slice must be a numeric value', { field });
1604
+ }
1605
+ // XXX should check to make sure integer
1606
+ slice = arg.$slice;
1607
+ }
1608
+ // Parse $sort.
1609
+ let sortFunction = undefined;
1610
+ if (arg.$sort) {
1611
+ if (slice === undefined) {
1612
+ throw MinimongoError('$sort requires $slice to be present', { field });
1613
+ }
1614
+ // XXX this allows us to use a $sort whose value is an array, but that's
1615
+ // actually an extension of the Node driver, so it won't work
1616
+ // server-side. Could be confusing!
1617
+ // XXX is it correct that we don't do geo-stuff here?
1618
+ sortFunction = new minimongo_sorter_1.default(arg.$sort).getComparator();
1619
+ toPush.forEach(element => {
1620
+ if (exports._f._type(element) !== 3) {
1621
+ throw MinimongoError('$push like modifiers using $sort require all elements to be ' +
1622
+ 'objects', { field });
1623
+ }
1624
+ });
1625
+ }
1626
+ // Actually push.
1627
+ if (position === undefined) {
1628
+ toPush.forEach(element => {
1629
+ target[field].push(element);
1630
+ });
1631
+ }
1632
+ else {
1633
+ const spliceArguments = [position, 0];
1634
+ toPush.forEach(element => {
1635
+ spliceArguments.push(element);
1636
+ });
1637
+ target[field].splice(...spliceArguments);
1638
+ }
1639
+ // Actually sort.
1640
+ if (sortFunction) {
1641
+ target[field].sort(sortFunction);
1642
+ }
1643
+ // Actually slice.
1644
+ if (slice !== undefined) {
1645
+ if (slice === 0) {
1646
+ target[field] = []; // differs from Array.slice!
1647
+ }
1648
+ else if (slice < 0) {
1649
+ target[field] = target[field].slice(slice);
1650
+ }
1651
+ else {
1652
+ target[field] = target[field].slice(0, slice);
1653
+ }
1654
+ }
1655
+ },
1656
+ $pushAll(target, field, arg) {
1657
+ if (!(typeof arg === 'object' && arg instanceof Array)) {
1658
+ throw MinimongoError('Modifier $pushAll/pullAll allowed for arrays only');
1659
+ }
1660
+ assertHasValidFieldNames(arg);
1661
+ const toPush = target[field];
1662
+ if (toPush === undefined) {
1663
+ target[field] = arg;
1664
+ }
1665
+ else if (!(toPush instanceof Array)) {
1666
+ throw MinimongoError('Cannot apply $pushAll modifier to non-array', { field });
1667
+ }
1668
+ else {
1669
+ toPush.push(...arg);
1670
+ }
1671
+ },
1672
+ $addToSet(target, field, arg) {
1673
+ let isEach = false;
1674
+ if (typeof arg === 'object') {
1675
+ // check if first key is '$each'
1676
+ const keys = Object.keys(arg);
1677
+ if (keys[0] === '$each') {
1678
+ isEach = true;
1679
+ }
1680
+ }
1681
+ const values = isEach ? arg.$each : [arg];
1682
+ assertHasValidFieldNames(values);
1683
+ const toAdd = target[field];
1684
+ if (toAdd === undefined) {
1685
+ target[field] = values;
1686
+ }
1687
+ else if (!(toAdd instanceof Array)) {
1688
+ throw MinimongoError('Cannot apply $addToSet modifier to non-array', { field });
1689
+ }
1690
+ else {
1691
+ values.forEach(value => {
1692
+ if (toAdd.some(element => exports._f._equal(value, element))) {
1693
+ return;
1694
+ }
1695
+ toAdd.push(value);
1696
+ });
1697
+ }
1698
+ },
1699
+ $pop(target, field, arg) {
1700
+ if (target === undefined) {
1701
+ return;
1702
+ }
1703
+ const toPop = target[field];
1704
+ if (toPop === undefined) {
1705
+ return;
1706
+ }
1707
+ if (!(toPop instanceof Array)) {
1708
+ throw MinimongoError('Cannot apply $pop modifier to non-array', { field });
1709
+ }
1710
+ if (typeof arg === 'number' && arg < 0) {
1711
+ toPop.splice(0, 1);
1712
+ }
1713
+ else {
1714
+ toPop.pop();
1715
+ }
1716
+ },
1717
+ $pull(target, field, arg) {
1718
+ if (target === undefined) {
1719
+ return;
1720
+ }
1721
+ const toPull = target[field];
1722
+ if (toPull === undefined) {
1723
+ return;
1724
+ }
1725
+ if (!(toPull instanceof Array)) {
1726
+ throw MinimongoError('Cannot apply $pull/pullAll modifier to non-array', { field });
1727
+ }
1728
+ let out;
1729
+ if (arg != null && typeof arg === 'object' && !(arg instanceof Array)) {
1730
+ // XXX would be much nicer to compile this once, rather than
1731
+ // for each document we modify.. but usually we're not
1732
+ // modifying that many documents, so we'll let it slide for
1733
+ // now
1734
+ // XXX Minimongo.Matcher isn't up for the job, because we need
1735
+ // to permit stuff like {$pull: {a: {$gt: 4}}}.. something
1736
+ // like {$gt: 4} is not normally a complete selector.
1737
+ // same issue as $elemMatch possibly?
1738
+ const matcher = new minimongo_matcher_1.MinimongoMatcher(arg);
1739
+ out = toPull.filter(element => !matcher.documentMatches(element).result);
1740
+ }
1741
+ else {
1742
+ out = toPull.filter(element => !exports._f._equal(element, arg));
1743
+ }
1744
+ target[field] = out;
1745
+ },
1746
+ $pullAll(target, field, arg) {
1747
+ if (!(typeof arg === 'object' && arg instanceof Array)) {
1748
+ throw MinimongoError('Modifier $pushAll/pullAll allowed for arrays only', { field });
1749
+ }
1750
+ if (target === undefined) {
1751
+ return;
1752
+ }
1753
+ const toPull = target[field];
1754
+ if (toPull === undefined) {
1755
+ return;
1756
+ }
1757
+ if (!(toPull instanceof Array)) {
1758
+ throw MinimongoError('Cannot apply $pull/pullAll modifier to non-array', { field });
1759
+ }
1760
+ target[field] = toPull.filter(object => !arg.some(element => exports._f._equal(object, element)));
1761
+ },
1762
+ $rename(target, field, arg, keypath, doc) {
1763
+ // no idea why mongo has this restriction..
1764
+ if (keypath === arg) {
1765
+ throw MinimongoError('$rename source must differ from target', { field });
1766
+ }
1767
+ if (target === null) {
1768
+ throw MinimongoError('$rename source field invalid', { field });
1769
+ }
1770
+ if (typeof arg !== 'string') {
1771
+ throw MinimongoError('$rename target must be a string', { field });
1772
+ }
1773
+ if (arg.includes('\0')) {
1774
+ // Null bytes are not allowed in Mongo field names
1775
+ // https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names
1776
+ throw MinimongoError('The \'to\' field for $rename cannot contain an embedded null byte', { field });
1777
+ }
1778
+ if (target === undefined) {
1779
+ return;
1780
+ }
1781
+ const object = target[field];
1782
+ delete target[field];
1783
+ const keyparts = arg.split('.');
1784
+ const target2 = findModTarget(doc, keyparts, { forbidArray: true });
1785
+ if (target2 === null) {
1786
+ throw MinimongoError('$rename target field invalid', { field });
1787
+ }
1788
+ target2[keyparts.pop()] = object;
1789
+ },
1790
+ $bit(target, field, arg) {
1791
+ // XXX mongo only supports $bit on integers, and we only support
1792
+ // native javascript numbers (doubles) so far, so we can't support $bit
1793
+ throw MinimongoError('$bit is not supported', { field });
1794
+ },
1795
+ $v() {
1796
+ // As discussed in https://github.com/meteor/meteor/issues/9623,
1797
+ // the `$v` operator is not needed by Meteor, but problems can occur if
1798
+ // it's not at least callable (as of Mongo >= 3.6). It's defined here as
1799
+ // a no-op to work around these problems.
1800
+ }
1801
+ };
1802
+ const NO_CREATE_MODIFIERS = {
1803
+ $pop: true,
1804
+ $pull: true,
1805
+ $pullAll: true,
1806
+ $rename: true,
1807
+ $unset: true
1808
+ };
1809
+ const invalidCharMsg = {
1810
+ $: 'start with \'$\'',
1811
+ '.': 'contain \'.\'',
1812
+ '\0': 'contain null bytes'
1813
+ };
1814
+ function assertIsValidFieldName(key) {
1815
+ let match;
1816
+ if (typeof key === 'string' && (match = key.match(/^\$|\.|\0/))) {
1817
+ throw MinimongoError(`Key ${key} must not ${invalidCharMsg[match[0]]}`);
1818
+ }
1819
+ }
1820
+ // for a.b.c.2.d.e, keyparts should be ['a', 'b', 'c', '2', 'd', 'e'],
1821
+ // and then you would operate on the 'e' property of the returned
1822
+ // object.
1823
+ //
1824
+ // if options.noCreate is falsey, creates intermediate levels of
1825
+ // structure as necessary, like mkdir -p (and raises an exception if
1826
+ // that would mean giving a non-numeric property to an array.) if
1827
+ // options.noCreate is true, return undefined instead.
1828
+ //
1829
+ // may modify the last element of keyparts to signal to the caller that it needs
1830
+ // to use a different value to index into the returned object (for example,
1831
+ // ['a', '01'] -> ['a', 1]).
1832
+ //
1833
+ // if forbidArray is true, return null if the keypath goes through an array.
1834
+ //
1835
+ // if options.arrayIndices is set, use its first element for the (first) '$' in
1836
+ // the path.
1837
+ function findModTarget(doc, keyparts, options = {}) {
1838
+ let usedArrayIndex = false;
1839
+ for (let i = 0; i < keyparts.length; i++) {
1840
+ const last = i === keyparts.length - 1;
1841
+ let keypart = keyparts[i];
1842
+ if (!isIndexable(doc)) {
1843
+ if (options.noCreate) {
1844
+ return undefined;
1845
+ }
1846
+ const error = MinimongoError(`cannot use the part '${keypart}' to traverse ${doc}`);
1847
+ error.setPropertyError = true;
1848
+ throw error;
1849
+ }
1850
+ if (doc instanceof Array) {
1851
+ if (options.forbidArray) {
1852
+ return null;
1853
+ }
1854
+ if (keypart === '$') {
1855
+ if (usedArrayIndex) {
1856
+ throw MinimongoError('Too many positional (i.e. \'$\') elements');
1857
+ }
1858
+ if (!options.arrayIndices || !options.arrayIndices.length) {
1859
+ throw MinimongoError('The positional operator did not find the match needed from the ' +
1860
+ 'query');
1861
+ }
1862
+ keypart = options.arrayIndices[0];
1863
+ usedArrayIndex = true;
1864
+ }
1865
+ else if (isNumericKey(keypart)) {
1866
+ keypart = parseInt(keypart);
1867
+ }
1868
+ else {
1869
+ if (options.noCreate) {
1870
+ return undefined;
1871
+ }
1872
+ throw MinimongoError(`can't append to array using string field name [${keypart}]`);
1873
+ }
1874
+ if (last) {
1875
+ keyparts[i] = keypart; // handle 'a.01'
1876
+ }
1877
+ if (options.noCreate && keypart >= doc.length) {
1878
+ return undefined;
1879
+ }
1880
+ while (doc.length < keypart) {
1881
+ doc.push(null);
1882
+ }
1883
+ if (!last) {
1884
+ if (doc.length === keypart) {
1885
+ doc.push({});
1886
+ }
1887
+ else if (typeof doc[keypart] !== 'object') {
1888
+ throw MinimongoError(`can't modify field '${keyparts[i + 1]}' of list value ` +
1889
+ JSON.stringify(doc[keypart]));
1890
+ }
1891
+ }
1892
+ }
1893
+ else {
1894
+ assertIsValidFieldName(keypart);
1895
+ if (!(keypart in doc)) {
1896
+ if (options.noCreate) {
1897
+ return undefined;
1898
+ }
1899
+ if (!last) {
1900
+ doc[keypart] = {};
1901
+ }
1902
+ }
1903
+ }
1904
+ if (last) {
1905
+ return doc;
1906
+ }
1907
+ doc = doc[keypart];
1908
+ }
1909
+ // notreached
1910
+ }
1911
+ function combineImportantPathsIntoProjection(paths, projection) {
1912
+ const details = projectionDetails(projection);
1913
+ // merge the paths to include
1914
+ const tree = pathsToTree(paths, path => true, (node, path, fullPath) => true, details.tree);
1915
+ const mergedProjection = treeToPaths(tree);
1916
+ if (details.including) {
1917
+ // both selector and projection are pointing on fields to include
1918
+ // so we can just return the merged tree
1919
+ return mergedProjection;
1920
+ }
1921
+ // selector is pointing at fields to include
1922
+ // projection is pointing at fields to exclude
1923
+ // make sure we don't exclude important paths
1924
+ const mergedExclProjection = {};
1925
+ Object.keys(mergedProjection).forEach(path => {
1926
+ if (!mergedProjection[path]) {
1927
+ mergedExclProjection[path] = false;
1928
+ }
1929
+ });
1930
+ return mergedExclProjection;
1931
+ }
1932
+ exports.combineImportantPathsIntoProjection = combineImportantPathsIntoProjection;
1933
+ // Returns a set of key paths similar to
1934
+ // { 'foo.bar': 1, 'a.b.c': 1 }
1935
+ function treeToPaths(tree, prefix = '') {
1936
+ const result = {};
1937
+ Object.keys(tree).forEach(key => {
1938
+ const value = tree[key];
1939
+ if (value === Object(value)) {
1940
+ Object.assign(result, treeToPaths(value, `${prefix + key}.`));
1941
+ }
1942
+ else {
1943
+ result[prefix + key] = value;
1944
+ }
1945
+ });
1946
+ return result;
1947
+ }
1948
+ function _pathsElidingNumericKeys(paths) {
1949
+ return paths.map(path => path.split('.').filter(part => !isNumericKey(part)).join('.'));
1950
+ }
1951
+ exports._pathsElidingNumericKeys = _pathsElidingNumericKeys;
1952
+ // Knows how to compile a fields projection to a predicate function.
1953
+ // @returns - Function: a closure that filters out an object according to the
1954
+ // fields projection rules:
1955
+ // @param obj - Object: MongoDB-styled document
1956
+ // @returns - Object: a document with the fields filtered out
1957
+ // according to projection rules. Doesn't retain subfields
1958
+ // of passed argument.
1959
+ function _compileProjection(fields) {
1960
+ _checkSupportedProjection(fields);
1961
+ const _idProjection = fields._id === undefined ? true : fields._id;
1962
+ const details = projectionDetails(fields);
1963
+ // returns transformed doc according to ruleTree
1964
+ const transform = (doc, ruleTree) => {
1965
+ // Special case for "sets"
1966
+ if (Array.isArray(doc)) {
1967
+ return doc.map(subdoc => transform(subdoc, ruleTree));
1968
+ }
1969
+ const result = details.including ? {} : (0, ejson_1.clone)(doc);
1970
+ Object.keys(ruleTree).forEach(key => {
1971
+ if (doc == null || !exports.hasOwn.call(doc, key)) {
1972
+ return;
1973
+ }
1974
+ const rule = ruleTree[key];
1975
+ if (rule === Object(rule)) {
1976
+ // For sub-objects/subsets we branch
1977
+ if (doc[key] === Object(doc[key])) {
1978
+ result[key] = transform(doc[key], rule);
1979
+ }
1980
+ }
1981
+ else if (details.including) {
1982
+ // Otherwise we don't even touch this subfield
1983
+ result[key] = (0, ejson_1.clone)(doc[key]);
1984
+ }
1985
+ else {
1986
+ delete result[key];
1987
+ }
1988
+ });
1989
+ return doc != null ? result : doc;
1990
+ };
1991
+ return doc => {
1992
+ const result = transform(doc, details.tree);
1993
+ if (_idProjection && exports.hasOwn.call(doc, '_id')) {
1994
+ result._id = doc._id;
1995
+ }
1996
+ if (!_idProjection && exports.hasOwn.call(result, '_id')) {
1997
+ delete result._id;
1998
+ }
1999
+ return result;
2000
+ };
2001
+ }
2002
+ exports._compileProjection = _compileProjection;