mongodb-livedata-server 0.0.4 → 0.0.6

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