@builder6/query-mongodb 0.6.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.
@@ -0,0 +1,1300 @@
1
+ /* global suite, test */
2
+
3
+ const chai = require('chai');
4
+ const assert = chai.assert;
5
+
6
+ const { ObjectId } = require('mongodb');
7
+
8
+ const pipelines = require('./pipelines');
9
+ const {
10
+ createGroupKeyPipeline,
11
+ createGroupingPipeline,
12
+ createSkipTakePipeline,
13
+ createCountPipeline,
14
+ createMatchPipeline,
15
+ createSortPipeline,
16
+ createSummaryPipeline,
17
+ createSelectProjectExpression,
18
+ createSelectPipeline,
19
+ createCompleteFilterPipeline,
20
+ createRemoveNestedFieldsPipeline,
21
+ } = pipelines;
22
+ const {
23
+ createGroupStagePipeline,
24
+ construct,
25
+ constructRegex,
26
+ parseFilter,
27
+ createFilterPipeline,
28
+ createSearchPipeline,
29
+ checkNestedField,
30
+ createAddNestedFieldsPipeline,
31
+ divInt,
32
+ subtractMod,
33
+ isAndChainWithIncompleteAnds,
34
+ fixAndChainWithIncompleteAnds,
35
+ isCorrectFilterOperatorStructure,
36
+ } = pipelines.testing;
37
+
38
+ suite('pipelines', function () {
39
+ suite('divInt', function () {
40
+ test('works', function () {
41
+ assert.deepEqual(divInt(14, 3), {
42
+ $divide: [
43
+ {
44
+ $subtract: [14, { $mod: [14, 3] }],
45
+ },
46
+ 3,
47
+ ],
48
+ });
49
+ });
50
+ });
51
+
52
+ suite('subtractMod', function () {
53
+ test('works', function () {
54
+ assert.deepEqual(subtractMod(14, 3), {
55
+ $subtract: [14, { $mod: [14, 3] }],
56
+ });
57
+ });
58
+ });
59
+
60
+ suite('createGroupKeyPipeline', function () {
61
+ test('no groupInterval', function () {
62
+ const result = createGroupKeyPipeline('sel', null, 0, 0);
63
+ const wanted = [{ $addFields: { ___group_key_0: '$sel' } }];
64
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
65
+ assert.equal(result.groupIndex, 0);
66
+ });
67
+
68
+ test('numeric groupInterval', function () {
69
+ const result = createGroupKeyPipeline('sel', 15, 0, 0);
70
+ const wanted = [
71
+ {
72
+ $addFields: {
73
+ ___group_key_0: { $subtract: ['$sel', { $mod: ['$sel', 15] }] },
74
+ },
75
+ },
76
+ ];
77
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
78
+ assert.equal(result.groupIndex, 0);
79
+ });
80
+
81
+ const basicNamedGroupIntervalTest = (name, tzo, mongoModName) => {
82
+ const result = createGroupKeyPipeline('sel', name, 0, {
83
+ timezoneOffset: tzo,
84
+ });
85
+ const wanted = [
86
+ {
87
+ $addFields: {
88
+ ___group_key_0: {
89
+ [`$${mongoModName || name}`]: {
90
+ $subtract: ['$sel', tzo * 60 * 1000],
91
+ },
92
+ },
93
+ },
94
+ },
95
+ ];
96
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
97
+ assert.equal(result.groupIndex, 0);
98
+ };
99
+
100
+ test('groupInterval year, timezoneOffset 0', function () {
101
+ basicNamedGroupIntervalTest('year', 0);
102
+ });
103
+
104
+ test('groupInterval year, timezoneOffset 60', function () {
105
+ basicNamedGroupIntervalTest('year', 60);
106
+ });
107
+
108
+ test('groupInterval quarter, timezoneOffset 60', function () {
109
+ const result = createGroupKeyPipeline('sel', 'quarter', 0, {
110
+ timezoneOffset: 60,
111
+ });
112
+ const wanted = [
113
+ {
114
+ $addFields: {
115
+ ___mp2: { $add: [{ $month: { $subtract: ['$sel', 3600000] } }, 2] },
116
+ },
117
+ },
118
+ {
119
+ $addFields: {
120
+ ___group_key_0: {
121
+ $divide: [
122
+ { $subtract: ['$___mp2', { $mod: ['$___mp2', 3] }] },
123
+ 3,
124
+ ],
125
+ },
126
+ },
127
+ },
128
+ ];
129
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
130
+ assert.equal(result.groupIndex, 0);
131
+ });
132
+
133
+ test('groupInterval month, timezoneOffset 60', function () {
134
+ basicNamedGroupIntervalTest('month', 60);
135
+ });
136
+
137
+ test('groupInterval day, timezoneOffset 60', function () {
138
+ basicNamedGroupIntervalTest('day', 60, 'dayOfMonth');
139
+ });
140
+
141
+ test('groupInterval dayOfWeek, timezoneOffset 60', function () {
142
+ const result = createGroupKeyPipeline('sel', 'dayOfWeek', 0, {
143
+ timezoneOffset: 60,
144
+ });
145
+ const wanted = [
146
+ {
147
+ $addFields: {
148
+ ___group_key_0: {
149
+ $subtract: [{ $dayOfWeek: { $subtract: ['$sel', 3600000] } }, 1],
150
+ },
151
+ },
152
+ },
153
+ ];
154
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
155
+ assert.equal(result.groupIndex, 0);
156
+ });
157
+
158
+ test('groupInterval hour, timezoneOffset 60', function () {
159
+ basicNamedGroupIntervalTest('hour', 60);
160
+ });
161
+
162
+ test('groupInterval minute, timezoneOffset 60', function () {
163
+ basicNamedGroupIntervalTest('minute', 60);
164
+ });
165
+
166
+ test('groupInterval second, timezoneOffset 60', function () {
167
+ basicNamedGroupIntervalTest('second', 60);
168
+ });
169
+
170
+ test('unknown groupInterval', function () {
171
+ const result = createGroupKeyPipeline('sel', 'non-existent name', 0, 0);
172
+ const wanted = [{ $addFields: { ___group_key_0: '$sel' } }];
173
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
174
+ assert.equal(result.groupIndex, 0);
175
+ });
176
+ });
177
+
178
+ suite('createGroupStagePipeline', function () {
179
+ test('basics', function () {
180
+ const groupKeyPipeline = ['test'];
181
+ groupKeyPipeline.groupIndex = 99;
182
+ const result = createGroupStagePipeline(
183
+ false,
184
+ true,
185
+ null,
186
+ groupKeyPipeline
187
+ );
188
+ const wanted = ['test', { $group: { _id: '$___group_key_99' } }];
189
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
190
+ assert.isUndefined(result.groupIndex);
191
+ });
192
+
193
+ test('not countingSeparately', function () {
194
+ const groupKeyPipeline = ['test'];
195
+ groupKeyPipeline.groupIndex = 99;
196
+ const result = createGroupStagePipeline(
197
+ false,
198
+ false,
199
+ null,
200
+ groupKeyPipeline
201
+ );
202
+ const wanted = [
203
+ 'test',
204
+ { $group: { _id: '$___group_key_99', count: { $sum: 1 } } },
205
+ ];
206
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
207
+ assert.isUndefined(result.groupIndex);
208
+ });
209
+
210
+ test('not countingSeparately, includeDataItems', function () {
211
+ const groupKeyPipeline = ['test'];
212
+ groupKeyPipeline.groupIndex = 99;
213
+ const result = createGroupStagePipeline(
214
+ true,
215
+ false,
216
+ 'itemProjection',
217
+ groupKeyPipeline
218
+ );
219
+ const wanted = [
220
+ 'test',
221
+ {
222
+ $group: {
223
+ _id: '$___group_key_99',
224
+ count: { $sum: 1 },
225
+ items: { $push: 'itemProjection' },
226
+ },
227
+ },
228
+ ];
229
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
230
+ assert.isUndefined(result.groupIndex);
231
+ });
232
+ });
233
+
234
+ suite('createGroupingPipeline', function () {
235
+ test('basics', function () {
236
+ const groupKeyPipeline = ['test'];
237
+ groupKeyPipeline.groupIndex = 99;
238
+ const result = createGroupingPipeline(
239
+ true,
240
+ false,
241
+ true,
242
+ groupKeyPipeline
243
+ );
244
+ const wanted = [
245
+ 'test',
246
+ { $group: { _id: '$___group_key_99' } },
247
+ { $project: { _id: 0, key: '$_id' } },
248
+ { $sort: { key: -1 } },
249
+ { $addFields: { items: null } },
250
+ ];
251
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
252
+ assert.isUndefined(result.groupIndex);
253
+ });
254
+
255
+ test('not countingSeparately', function () {
256
+ const groupKeyPipeline = ['test'];
257
+ groupKeyPipeline.groupIndex = 99;
258
+ const result = createGroupingPipeline(
259
+ true,
260
+ false,
261
+ false,
262
+ groupKeyPipeline
263
+ );
264
+ const wanted = [
265
+ 'test',
266
+ { $group: { _id: '$___group_key_99', count: { $sum: 1 } } },
267
+ { $project: { _id: 0, key: '$_id', count: 1 } },
268
+ { $sort: { key: -1 } },
269
+ { $addFields: { items: null } },
270
+ ];
271
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
272
+ assert.isUndefined(result.groupIndex);
273
+ });
274
+
275
+ test('not countingSeparately, includeDataItems', function () {
276
+ const groupKeyPipeline = ['test'];
277
+ groupKeyPipeline.groupIndex = 99;
278
+ const result = createGroupingPipeline(
279
+ true,
280
+ true,
281
+ false,
282
+ groupKeyPipeline
283
+ );
284
+ const wanted = [
285
+ 'test',
286
+ {
287
+ $group: {
288
+ _id: '$___group_key_99',
289
+ count: { $sum: 1 },
290
+ items: { $push: '$$CURRENT' },
291
+ },
292
+ },
293
+ { $project: { _id: 0, key: '$_id', count: 1, items: 1 } },
294
+ { $sort: { key: -1 } },
295
+ ];
296
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
297
+ assert.isUndefined(result.groupIndex);
298
+ });
299
+
300
+ test('not countingSeparately, includeDataItems, custom itemProjection', function () {
301
+ const groupKeyPipeline = ['test'];
302
+ groupKeyPipeline.groupIndex = 99;
303
+ const result = createGroupingPipeline(
304
+ true,
305
+ true,
306
+ false,
307
+ groupKeyPipeline,
308
+ '$$customProjection$$'
309
+ );
310
+ const wanted = [
311
+ 'test',
312
+ {
313
+ $group: {
314
+ _id: '$___group_key_99',
315
+ count: { $sum: 1 },
316
+ items: { $push: '$$customProjection$$' },
317
+ },
318
+ },
319
+ { $project: { _id: 0, key: '$_id', count: 1, items: 1 } },
320
+ { $sort: { key: -1 } },
321
+ ];
322
+ assert.equal(JSON.stringify(result), JSON.stringify(wanted));
323
+ assert.isUndefined(result.groupIndex);
324
+ });
325
+ });
326
+
327
+ suite('createSkipTakePipeline', function () {
328
+ test('no skip or take', function () {
329
+ assert.deepEqual(createSkipTakePipeline(), []);
330
+ });
331
+
332
+ test('skip, no take', function () {
333
+ assert.deepEqual(createSkipTakePipeline(33), [{ $skip: 33 }]);
334
+ });
335
+
336
+ test('no skip, take', function () {
337
+ assert.deepEqual(createSkipTakePipeline(null, 33), [{ $limit: 33 }]);
338
+ });
339
+
340
+ test('skip and take', function () {
341
+ assert.deepEqual(createSkipTakePipeline(33, 44), [
342
+ { $skip: 33 },
343
+ { $limit: 44 },
344
+ ]);
345
+ });
346
+ });
347
+
348
+ suite('createCountPipeline', function () {
349
+ test('works', function () {
350
+ assert.deepEqual(createCountPipeline(), [{ $count: 'count' }]);
351
+ });
352
+ });
353
+
354
+ suite('createMatchPipeline', function () {
355
+ test('works', function () {
356
+ assert.deepEqual(createMatchPipeline('sel', 'val'), [
357
+ { $match: { sel: 'val' } },
358
+ ]);
359
+ });
360
+ });
361
+
362
+ suite('construct', function () {
363
+ test('works', function () {
364
+ assert.deepEqual(construct('field', 'plus', 'val'), {
365
+ field: { plus: 'val' },
366
+ });
367
+ });
368
+ });
369
+
370
+ suite('constructRegex', function () {
371
+ test('works', function () {
372
+ assert.deepEqual(constructRegex('field', 'regex', true), {
373
+ field: { $regex: 'regex', $options: 'i' },
374
+ });
375
+ });
376
+ });
377
+
378
+ suite('parseFilter', function () {
379
+ const testParseFilter = (input, expectedMatch, expectedFieldList) => {
380
+ const result = parseFilter(input, { caseInsensitiveRegex: true });
381
+ const match = result && result.match;
382
+ const fieldList = result ? result.fieldList : [];
383
+ assert.deepEqual(match, expectedMatch);
384
+ assert.deepEqual(fieldList, expectedFieldList);
385
+ };
386
+
387
+ test('string element', function () {
388
+ testParseFilter(
389
+ 'thing',
390
+ {
391
+ thing: { $eq: true },
392
+ },
393
+ ['thing']
394
+ );
395
+ });
396
+
397
+ test('nested array', function () {
398
+ testParseFilter(
399
+ [[['!', 'thing']]], // wild and pointless nesting
400
+ {
401
+ $nor: [
402
+ {
403
+ thing: { $eq: true },
404
+ },
405
+ ],
406
+ },
407
+ ['thing']
408
+ );
409
+ });
410
+
411
+ test('!', function () {
412
+ testParseFilter(
413
+ ['!', 'thing'],
414
+ {
415
+ $nor: [
416
+ {
417
+ thing: { $eq: true },
418
+ },
419
+ ],
420
+ },
421
+ ['thing']
422
+ );
423
+ });
424
+
425
+ test('unknown unary', function () {
426
+ testParseFilter(['&', 'thing'], null, []);
427
+ });
428
+
429
+ test('equal', function () {
430
+ testParseFilter(
431
+ ['thing', '=', 'val'],
432
+ {
433
+ thing: { $eq: 'val' },
434
+ },
435
+ ['thing']
436
+ );
437
+ });
438
+
439
+ test('equalsObjectId', function () {
440
+ testParseFilter(
441
+ ['thing', 'equalsObjectId', '0123456789abcdef01234567'],
442
+ {
443
+ thing: {
444
+ $eq: new ObjectId('0123456789abcdef01234567'),
445
+ },
446
+ },
447
+ ['thing']
448
+ );
449
+ });
450
+
451
+ test('not equal', function () {
452
+ testParseFilter(
453
+ ['thing', '<>', 'val'],
454
+ {
455
+ thing: { $ne: 'val' },
456
+ },
457
+ ['thing']
458
+ );
459
+ });
460
+
461
+ test('greater than', function () {
462
+ testParseFilter(
463
+ ['thing', '>', 'val'],
464
+ {
465
+ thing: { $gt: 'val' },
466
+ },
467
+ ['thing']
468
+ );
469
+ });
470
+
471
+ test('greater than or equal', function () {
472
+ testParseFilter(
473
+ ['thing', '>=', 'val'],
474
+ {
475
+ thing: { $gte: 'val' },
476
+ },
477
+ ['thing']
478
+ );
479
+ });
480
+
481
+ test('lower than', function () {
482
+ testParseFilter(
483
+ ['thing', '<', 'val'],
484
+ {
485
+ thing: { $lt: 'val' },
486
+ },
487
+ ['thing']
488
+ );
489
+ });
490
+
491
+ test('lower than or equal', function () {
492
+ testParseFilter(
493
+ ['thing', '<=', 'val'],
494
+ {
495
+ thing: { $lte: 'val' },
496
+ },
497
+ ['thing']
498
+ );
499
+ });
500
+
501
+ test('startswith', function () {
502
+ testParseFilter(
503
+ ['thing', 'startswith', 'val'],
504
+ {
505
+ thing: { $regex: '^val', $options: 'i' },
506
+ },
507
+ ['thing']
508
+ );
509
+ });
510
+
511
+ test('endswith', function () {
512
+ testParseFilter(
513
+ ['thing', 'endswith', 'val'],
514
+ {
515
+ thing: { $regex: 'val$', $options: 'i' },
516
+ },
517
+ ['thing']
518
+ );
519
+ });
520
+
521
+ test('contains', function () {
522
+ testParseFilter(
523
+ ['thing', 'contains', 'val'],
524
+ {
525
+ thing: { $regex: 'val', $options: 'i' },
526
+ },
527
+ ['thing']
528
+ );
529
+ });
530
+
531
+ test('notcontains', function () {
532
+ testParseFilter(
533
+ ['thing', 'notcontains', 'val'],
534
+ {
535
+ thing: { $regex: '^((?!val).)*$', $options: 'i' },
536
+ },
537
+ ['thing']
538
+ );
539
+ });
540
+
541
+ test('unknown operator', function () {
542
+ testParseFilter(['thing', '&%&%&%&', 'val'], null, []);
543
+ });
544
+
545
+ test('even number of elements > 2', function () {
546
+ testParseFilter([1, 3, 4, 6], null, []);
547
+ });
548
+
549
+ test('not an array or a string', function () {
550
+ testParseFilter({ barg: 42 }, null, []);
551
+ });
552
+
553
+ test('odd number of elements > 3 without operator in pos 1', function () {
554
+ testParseFilter([1, 'unknown item', 3, 4, 5], null, []);
555
+ });
556
+
557
+ test('odd number of elements > 3 with non-string in pos 1', function () {
558
+ testParseFilter([1, { barg: 42 }, 3, 4, 5], null, []);
559
+ });
560
+
561
+ test('nested field', function () {
562
+ testParseFilter(
563
+ ['thing.year', '=', 'val'],
564
+ {
565
+ ___thing_year: { $eq: 'val' },
566
+ },
567
+ ['thing.year']
568
+ );
569
+ });
570
+
571
+ test('unrecognized nested field', function () {
572
+ testParseFilter(
573
+ ['thing.unknown', '=', 'val'],
574
+ {
575
+ 'thing.unknown': { $eq: 'val' },
576
+ },
577
+ ['thing.unknown']
578
+ );
579
+ });
580
+
581
+ test('correct "and" chain', function () {
582
+ testParseFilter(
583
+ [
584
+ ['field1', '=', 42],
585
+ 'and',
586
+ ['field2', '>', 10],
587
+ 'and',
588
+ ['field3', '<>', 'this thing'],
589
+ ],
590
+ {
591
+ $and: [
592
+ {
593
+ field1: { $eq: 42 },
594
+ },
595
+ {
596
+ field2: { $gt: 10 },
597
+ },
598
+ {
599
+ field3: { $ne: 'this thing' },
600
+ },
601
+ ],
602
+ },
603
+ ['field1', 'field2', 'field3']
604
+ );
605
+ });
606
+
607
+ test('short "and" chain with no "ands"', function () {
608
+ testParseFilter(
609
+ [
610
+ ['field1', '=', 42],
611
+ ['field2', '>', 10],
612
+ ],
613
+ {
614
+ $and: [
615
+ {
616
+ field1: { $eq: 42 },
617
+ },
618
+ {
619
+ field2: { $gt: 10 },
620
+ },
621
+ ],
622
+ },
623
+ ['field1', 'field2']
624
+ );
625
+ });
626
+
627
+ test('long "and" chain with no "ands"', function () {
628
+ testParseFilter(
629
+ [
630
+ ['field1', '=', 42],
631
+ ['field2', '>', 10],
632
+ ['field3', '<>', 'this thing'],
633
+ ['field4', '=', 11],
634
+ ],
635
+ {
636
+ $and: [
637
+ {
638
+ field1: { $eq: 42 },
639
+ },
640
+ {
641
+ field2: { $gt: 10 },
642
+ },
643
+ {
644
+ field3: { $ne: 'this thing' },
645
+ },
646
+ {
647
+ field4: { $eq: 11 },
648
+ },
649
+ ],
650
+ },
651
+ ['field1', 'field2', 'field3', 'field4']
652
+ );
653
+ });
654
+
655
+ test('"and" chain with incomplete "ands"', function () {
656
+ testParseFilter(
657
+ [
658
+ ['field1', '=', 42],
659
+ 'and',
660
+ ['field2', '>', 10],
661
+ ['field3', '<>', 'this thing'],
662
+ ],
663
+ {
664
+ $and: [
665
+ {
666
+ field1: { $eq: 42 },
667
+ },
668
+ {
669
+ field2: { $gt: 10 },
670
+ },
671
+ {
672
+ field3: { $ne: 'this thing' },
673
+ },
674
+ ],
675
+ },
676
+ ['field1', 'field2', 'field3']
677
+ );
678
+ });
679
+
680
+ test('correct "or" chain', function () {
681
+ testParseFilter(
682
+ [
683
+ ['field1', '=', 42],
684
+ 'or',
685
+ ['field2', '>', 10],
686
+ 'or',
687
+ ['field3', '<>', 'this thing'],
688
+ ],
689
+ {
690
+ $or: [
691
+ {
692
+ field1: { $eq: 42 },
693
+ },
694
+ {
695
+ field2: { $gt: 10 },
696
+ },
697
+ {
698
+ field3: { $ne: 'this thing' },
699
+ },
700
+ ],
701
+ },
702
+ ['field1', 'field2', 'field3']
703
+ );
704
+ });
705
+
706
+ test('incorrect operator chain', function () {
707
+ // It is unclear from documentation (https://js.devexpress.com/Documentation/17_1/Guide/Data_Layer/Data_Layer/#Reading_Data)
708
+ // (section "Group Filter Operations") whether this case
709
+ // should be allowed. There is a statement saying "operator priority"
710
+ // depends on the implementation of the underlying store" -
711
+ // this could be interpreted to mean that a construct like this
712
+ // should be supported.
713
+ // This library currently assumes that this is invalid.
714
+ testParseFilter(
715
+ [
716
+ ['field1', '=', 42],
717
+ 'and',
718
+ ['field2', '>', 10],
719
+ 'or',
720
+ ['field3', '<>', 'this thing'],
721
+ ],
722
+ null,
723
+ []
724
+ );
725
+ });
726
+
727
+ test('correct combined operator chain', function () {
728
+ testParseFilter(
729
+ [
730
+ ['field1', '=', 42],
731
+ 'and',
732
+ [['field2', '>', 10], 'or', ['field3', '<>', 'this thing']],
733
+ ],
734
+ {
735
+ $and: [
736
+ {
737
+ field1: { $eq: 42 },
738
+ },
739
+ {
740
+ $or: [
741
+ {
742
+ field2: { $gt: 10 },
743
+ },
744
+ {
745
+ field3: { $ne: 'this thing' },
746
+ },
747
+ ],
748
+ },
749
+ ],
750
+ },
751
+ ['field1', 'field2', 'field3']
752
+ );
753
+ });
754
+ });
755
+
756
+ suite('createFilterPipeline', function () {
757
+ test('works', function () {
758
+ assert.deepEqual(createFilterPipeline(['thing', '=', 42]), {
759
+ pipeline: [{ $match: { thing: { $eq: 42 } } }],
760
+ fieldList: ['thing'],
761
+ });
762
+ });
763
+
764
+ test('no filter', function () {
765
+ assert.deepEqual(createFilterPipeline(), {
766
+ pipeline: [],
767
+ fieldList: [],
768
+ });
769
+ });
770
+
771
+ test('invalid filter', function () {
772
+ assert.deepEqual(createFilterPipeline(['thing', '=']), {
773
+ pipeline: [],
774
+ fieldList: [],
775
+ });
776
+ });
777
+ });
778
+
779
+ suite('createSortPipeline', function () {
780
+ test('works', function () {
781
+ assert.deepEqual(
782
+ createSortPipeline([
783
+ { selector: 'field1', desc: true },
784
+ { selector: 'field2' },
785
+ ]),
786
+ [{ $sort: { field1: -1, field2: 1 } }]
787
+ );
788
+ });
789
+ });
790
+
791
+ suite('createSummaryPipeline', function () {
792
+ test('works', function () {
793
+ assert.deepEqual(
794
+ createSummaryPipeline([
795
+ { summaryType: 'min', selector: 'thing' },
796
+ { summaryType: 'max', selector: 'other' },
797
+ { summaryType: 'invalid', selector: 'dontknow' },
798
+ { summaryType: 'count' },
799
+ ]),
800
+ [
801
+ {
802
+ $group: {
803
+ ___minthing: { $min: '$thing' },
804
+ ___maxother: { $max: '$other' },
805
+ ___count: { $sum: 1 },
806
+ _id: null,
807
+ },
808
+ },
809
+ ]
810
+ );
811
+ });
812
+ });
813
+
814
+ suite('createSearchPipeline', function () {
815
+ test('simple values', function () {
816
+ assert.deepEqual(createSearchPipeline('thing', '=', 42), {
817
+ pipeline: [{ $match: { thing: { $eq: 42 } } }],
818
+ fieldList: ['thing'],
819
+ });
820
+ });
821
+
822
+ test('list of expr', function () {
823
+ assert.deepEqual(
824
+ createSearchPipeline(['thing', 'other', 'outlandish'], '=', 42),
825
+ {
826
+ pipeline: [
827
+ {
828
+ $match: {
829
+ $or: [
830
+ { thing: { $eq: 42 } },
831
+ { other: { $eq: 42 } },
832
+ { outlandish: { $eq: 42 } },
833
+ ],
834
+ },
835
+ },
836
+ ],
837
+ fieldList: ['thing', 'other', 'outlandish'],
838
+ }
839
+ );
840
+ });
841
+ });
842
+
843
+ suite('createSelectProjectExpression', function () {
844
+ test('basics', function () {
845
+ assert.deepEqual(createSelectProjectExpression(['field1', 'field2']), {
846
+ field1: '$field1',
847
+ field2: '$field2',
848
+ });
849
+ });
850
+
851
+ test('explicitId', function () {
852
+ assert.deepEqual(
853
+ createSelectProjectExpression(['field1', 'field2'], true),
854
+ {
855
+ field1: '$field1',
856
+ field2: '$field2',
857
+ _id: '$_id',
858
+ }
859
+ );
860
+ });
861
+ });
862
+
863
+ suite('createSelectPipeline', function () {
864
+ test('works', function () {
865
+ assert.deepEqual(createSelectPipeline(['field1', 'field2']), [
866
+ {
867
+ $project: {
868
+ field1: '$field1',
869
+ field2: '$field2',
870
+ },
871
+ },
872
+ ]);
873
+ });
874
+ });
875
+
876
+ suite('checkNestedField', function () {
877
+ test('Quarter', function () {
878
+ assert.deepEqual(checkNestedField('field.Quarter'), {
879
+ base: 'field',
880
+ nested: 'Quarter',
881
+ filterFieldName: '___field_Quarter',
882
+ });
883
+ });
884
+
885
+ test('year', function () {
886
+ assert.deepEqual(checkNestedField('field.year'), {
887
+ base: 'field',
888
+ nested: 'year',
889
+ filterFieldName: '___field_year',
890
+ });
891
+ });
892
+
893
+ test('no match', function () {
894
+ assert.isUndefined(checkNestedField('field.other'));
895
+ });
896
+ });
897
+
898
+ suite('createAddNestedFieldsPipeline', function () {
899
+ test('no recognized nested fields', function () {
900
+ assert.deepEqual(
901
+ createAddNestedFieldsPipeline(['field1', 'field2', 'field3.other'], 0),
902
+ { pipeline: [], nestedFields: [] }
903
+ );
904
+ });
905
+
906
+ test('nested fields, tzo 60', function () {
907
+ assert.deepEqual(
908
+ createAddNestedFieldsPipeline(
909
+ [
910
+ 'field1',
911
+ 'field2.year',
912
+ 'field3.quarter',
913
+ 'field4.month',
914
+ 'field3.day',
915
+ 'field3.dayofweek',
916
+ ],
917
+ { timezoneOffset: 60 }
918
+ ),
919
+ {
920
+ pipeline: [
921
+ {
922
+ $addFields: {
923
+ ___field3_mp2: {
924
+ $add: [
925
+ {
926
+ $month: {
927
+ $subtract: ['$field3', 3600000],
928
+ },
929
+ },
930
+ 2,
931
+ ],
932
+ },
933
+ },
934
+ },
935
+ {
936
+ $addFields: {
937
+ ___field2_year: {
938
+ $year: {
939
+ $subtract: ['$field2', 3600000],
940
+ },
941
+ },
942
+ ___field3_day: {
943
+ $dayOfMonth: {
944
+ $subtract: ['$field3', 3600000],
945
+ },
946
+ },
947
+ ___field3_dayofweek: {
948
+ $subtract: [
949
+ {
950
+ $dayOfWeek: {
951
+ $subtract: ['$field3', 3600000],
952
+ },
953
+ },
954
+ 1,
955
+ ],
956
+ },
957
+ ___field3_quarter: {
958
+ $divide: [
959
+ {
960
+ $subtract: [
961
+ '$___field3_mp2',
962
+ {
963
+ $mod: ['$___field3_mp2', 3],
964
+ },
965
+ ],
966
+ },
967
+ 3,
968
+ ],
969
+ },
970
+ ___field4_month: {
971
+ $month: {
972
+ $subtract: ['$field4', 3600000],
973
+ },
974
+ },
975
+ },
976
+ },
977
+ ],
978
+ nestedFields: [
979
+ '___field2_year',
980
+ '___field3_mp2',
981
+ '___field3_quarter',
982
+ '___field4_month',
983
+ '___field3_day',
984
+ '___field3_dayofweek',
985
+ ],
986
+ }
987
+ );
988
+ });
989
+ });
990
+
991
+ suite('createCompleteFilterPipeline', function () {
992
+ test('works', function () {
993
+ assert.deepEqual(
994
+ createCompleteFilterPipeline(
995
+ 'thing',
996
+ '=',
997
+ 42,
998
+ [['thing2', '>', 13], 'and', ['date.month', '<', 5]],
999
+ { timezoneOffset: 60 }
1000
+ ),
1001
+ {
1002
+ pipeline: [
1003
+ {
1004
+ $addFields: {
1005
+ ___date_month: {
1006
+ $month: {
1007
+ $subtract: ['$date', 3600000],
1008
+ },
1009
+ },
1010
+ },
1011
+ },
1012
+ {
1013
+ $match: {
1014
+ thing: {
1015
+ $eq: 42,
1016
+ },
1017
+ },
1018
+ },
1019
+ {
1020
+ $match: {
1021
+ $and: [
1022
+ {
1023
+ thing2: {
1024
+ $gt: 13,
1025
+ },
1026
+ },
1027
+ {
1028
+ ___date_month: {
1029
+ $lt: 5,
1030
+ },
1031
+ },
1032
+ ],
1033
+ },
1034
+ },
1035
+ ],
1036
+ nestedFields: ['___date_month'],
1037
+ }
1038
+ );
1039
+ });
1040
+ });
1041
+
1042
+ suite('createRemoveNestedFieldsPipeline', function () {
1043
+ test('works', function () {
1044
+ assert.deepEqual(createRemoveNestedFieldsPipeline(['field1', 'field2']), [
1045
+ { $project: { field1: 0, field2: 0 } },
1046
+ ]);
1047
+ });
1048
+ });
1049
+
1050
+ suite('correctFilterOperatorStructure', function () {
1051
+ test('detect correct structure', function () {
1052
+ assert.isTrue(
1053
+ isCorrectFilterOperatorStructure(
1054
+ [
1055
+ ['field', '=', 42],
1056
+ 'and',
1057
+ ['field2', '>', 10],
1058
+ 'and',
1059
+ ['field3', '=', 15],
1060
+ 'and',
1061
+ ['field4', '=', 11],
1062
+ 'and',
1063
+ ['field8', '>', 100],
1064
+ ],
1065
+ 'and'
1066
+ )
1067
+ );
1068
+ });
1069
+
1070
+ test('reject missing operators', function () {
1071
+ assert.isFalse(
1072
+ isCorrectFilterOperatorStructure(
1073
+ [
1074
+ ['field', '=', 42],
1075
+ 'and',
1076
+ ['field2', '>', 10],
1077
+ ['field3', '=', 15],
1078
+ 'and',
1079
+ ['field4', '=', 11],
1080
+ 'and',
1081
+ ['field8', '>', 100],
1082
+ ],
1083
+ 'and'
1084
+ )
1085
+ );
1086
+ });
1087
+
1088
+ test('reject incorrect operators', function () {
1089
+ assert.isFalse(
1090
+ isCorrectFilterOperatorStructure(
1091
+ [
1092
+ ['field', '=', 42],
1093
+ 'and',
1094
+ ['field2', '>', 10],
1095
+ 'or',
1096
+ ['field3', '=', 15],
1097
+ 'and',
1098
+ ['field4', '=', 11],
1099
+ 'and',
1100
+ ['field8', '>', 100],
1101
+ ],
1102
+ 'and'
1103
+ )
1104
+ );
1105
+ });
1106
+ });
1107
+
1108
+ suite('andChainWithIncompleteAnds', function () {
1109
+ test('detect short "and" chain with no "ands"', function () {
1110
+ assert.isTrue(
1111
+ isAndChainWithIncompleteAnds([
1112
+ ['field', '=', 42],
1113
+ ['field2', '>', 10],
1114
+ ])
1115
+ );
1116
+ });
1117
+ test('detect three element "and" chain with no "ands"', function () {
1118
+ assert.isTrue(
1119
+ isAndChainWithIncompleteAnds([
1120
+ ['field', '=', 42],
1121
+ ['field2', '>', 10],
1122
+ ['field3', '=', 15],
1123
+ ])
1124
+ );
1125
+ });
1126
+ test('detect long "and" chain with no "ands"', function () {
1127
+ assert.isTrue(
1128
+ isAndChainWithIncompleteAnds([
1129
+ ['field', '=', 42],
1130
+ ['field2', '>', 10],
1131
+ ['field3', '=', 15],
1132
+ ['field4', '=', 11],
1133
+ ['field8', '>', 100],
1134
+ ])
1135
+ );
1136
+ });
1137
+ test('detect long "and" chain with one "and"', function () {
1138
+ assert.isTrue(
1139
+ isAndChainWithIncompleteAnds([
1140
+ ['field', '=', 42],
1141
+ 'and',
1142
+ ['field2', '>', 10],
1143
+ ['field3', '=', 15],
1144
+ ['field4', '=', 11],
1145
+ ['field8', '>', 100],
1146
+ ])
1147
+ );
1148
+ });
1149
+ test('detect long "and" chain with some "ands"', function () {
1150
+ assert.isTrue(
1151
+ isAndChainWithIncompleteAnds([
1152
+ ['field', '=', 42],
1153
+ ['field2', '>', 10],
1154
+ ['field3', '=', 15],
1155
+ 'and',
1156
+ ['field4', '=', 11],
1157
+ ['field8', '>', 100],
1158
+ 'and',
1159
+ ['field5', '=', 13],
1160
+ ])
1161
+ );
1162
+ });
1163
+
1164
+ test('reject unary operator chain', function () {
1165
+ assert.isFalse(isAndChainWithIncompleteAnds(['!', ['field', '=', 10]]));
1166
+ });
1167
+
1168
+ test('reject simple criterion', function () {
1169
+ assert.isFalse(isAndChainWithIncompleteAnds(['field', '=', 10]));
1170
+ });
1171
+
1172
+ test('reject chain with invalid operators', function () {
1173
+ assert.isFalse(
1174
+ isAndChainWithIncompleteAnds([
1175
+ ['field', '=', 42],
1176
+ ['field2', '>', 10],
1177
+ ['field3', '=', 15],
1178
+ 'or',
1179
+ ['field4', '=', 11],
1180
+ ['field8', '>', 100],
1181
+ ])
1182
+ );
1183
+ });
1184
+
1185
+ test('reject chain with complete set of operators', function () {
1186
+ assert.isFalse(
1187
+ isAndChainWithIncompleteAnds([
1188
+ ['field', '=', 42],
1189
+ 'and',
1190
+ ['field2', '>', 10],
1191
+ 'and',
1192
+ ['field3', '=', 15],
1193
+ 'and',
1194
+ ['field4', '=', 11],
1195
+ 'and',
1196
+ ['field8', '>', 100],
1197
+ ])
1198
+ );
1199
+ });
1200
+
1201
+ test('fix incomplete very short "and" chain with no "ands"', function () {
1202
+ assert.deepEqual(
1203
+ fixAndChainWithIncompleteAnds([
1204
+ ['field', '=', 42],
1205
+ ['field2', '>', 10],
1206
+ ]),
1207
+ [['field', '=', 42], 'and', ['field2', '>', 10]]
1208
+ );
1209
+ });
1210
+
1211
+ test('fix incomplete short "and" chain with no "ands"', function () {
1212
+ assert.deepEqual(
1213
+ fixAndChainWithIncompleteAnds([
1214
+ ['field', '=', 42],
1215
+ ['field2', '>', 10],
1216
+ ['field3', '=', 15],
1217
+ ]),
1218
+ [
1219
+ ['field', '=', 42],
1220
+ 'and',
1221
+ ['field2', '>', 10],
1222
+ 'and',
1223
+ ['field3', '=', 15],
1224
+ ]
1225
+ );
1226
+ });
1227
+
1228
+ test('fix incomplete long "and" chain with no "ands"', function () {
1229
+ assert.deepEqual(
1230
+ fixAndChainWithIncompleteAnds([
1231
+ ['field', '=', 42],
1232
+ ['field2', '>', 10],
1233
+ ['field3', '=', 15],
1234
+ ['field4', '>', 42],
1235
+ ['field5', '=', 'something'],
1236
+ ]),
1237
+ [
1238
+ ['field', '=', 42],
1239
+ 'and',
1240
+ ['field2', '>', 10],
1241
+ 'and',
1242
+ ['field3', '=', 15],
1243
+ 'and',
1244
+ ['field4', '>', 42],
1245
+ 'and',
1246
+ ['field5', '=', 'something'],
1247
+ ]
1248
+ );
1249
+ });
1250
+
1251
+ test('fix incomplete long "and" chain with one "and"', function () {
1252
+ assert.deepEqual(
1253
+ fixAndChainWithIncompleteAnds([
1254
+ ['field', '=', 42],
1255
+ 'and',
1256
+ ['field2', '>', 10],
1257
+ ['field3', '=', 15],
1258
+ ['field4', '>', 42],
1259
+ ['field5', '=', 'something'],
1260
+ ]),
1261
+ [
1262
+ ['field', '=', 42],
1263
+ 'and',
1264
+ ['field2', '>', 10],
1265
+ 'and',
1266
+ ['field3', '=', 15],
1267
+ 'and',
1268
+ ['field4', '>', 42],
1269
+ 'and',
1270
+ ['field5', '=', 'something'],
1271
+ ]
1272
+ );
1273
+ });
1274
+
1275
+ test('fix incomplete long "and" chain with some "ands"', function () {
1276
+ assert.deepEqual(
1277
+ fixAndChainWithIncompleteAnds([
1278
+ ['field', '=', 42],
1279
+ 'and',
1280
+ ['field2', '>', 10],
1281
+ ['field3', '=', 15],
1282
+ 'and',
1283
+ ['field4', '>', 42],
1284
+ ['field5', '=', 'something'],
1285
+ ]),
1286
+ [
1287
+ ['field', '=', 42],
1288
+ 'and',
1289
+ ['field2', '>', 10],
1290
+ 'and',
1291
+ ['field3', '=', 15],
1292
+ 'and',
1293
+ ['field4', '>', 42],
1294
+ 'and',
1295
+ ['field5', '=', 'something'],
1296
+ ]
1297
+ );
1298
+ });
1299
+ });
1300
+ });