@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.
- package/LICENSE +201 -0
- package/README.md +37 -0
- package/dist/index.js +272 -0
- package/dist/index.test.js +1941 -0
- package/dist/options.js +420 -0
- package/dist/options.test.js +960 -0
- package/dist/pipelines.js +751 -0
- package/dist/pipelines.test.js +782 -0
- package/dist/utils.js +45 -0
- package/dist/utils.test.js +36 -0
- package/index.js +1 -0
- package/options.js +1 -0
- package/package.json +60 -0
- package/src/index.js +497 -0
- package/src/index.test.js +2333 -0
- package/src/options.js +573 -0
- package/src/options.test.js +1112 -0
- package/src/pipelines.js +760 -0
- package/src/pipelines.test.js +1300 -0
- package/src/utils.js +33 -0
- package/src/utils.test.js +40 -0
package/src/pipelines.js
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
const { ObjectId } = require('mongodb');
|
|
2
|
+
|
|
3
|
+
const createGroupFieldName = (groupIndex) => '___group_key_' + groupIndex;
|
|
4
|
+
|
|
5
|
+
// much more complicated than it should be because braindead mongo
|
|
6
|
+
// doesn't support integer division by itself
|
|
7
|
+
// so I'm doing (dividend - (dividend MOD divisor)) / divisor
|
|
8
|
+
const divInt = (dividend, divisor) => ({
|
|
9
|
+
$divide: [subtractMod(dividend, divisor), divisor],
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const subtractMod = (a, b) => ({
|
|
13
|
+
$subtract: [
|
|
14
|
+
a,
|
|
15
|
+
{
|
|
16
|
+
$mod: [a, b],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const createGroupKeyPipeline = (
|
|
22
|
+
selector,
|
|
23
|
+
groupInterval,
|
|
24
|
+
groupIndex,
|
|
25
|
+
contextOptions
|
|
26
|
+
) => {
|
|
27
|
+
const { timezoneOffset } = contextOptions;
|
|
28
|
+
|
|
29
|
+
const wrapGroupKey = (keyExpr) => ({
|
|
30
|
+
$addFields: { [createGroupFieldName(groupIndex)]: keyExpr },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const prefix = (s) => '$' + s;
|
|
34
|
+
|
|
35
|
+
const pipe = (...args) => {
|
|
36
|
+
let result = Array.from(args);
|
|
37
|
+
result.groupIndex = groupIndex;
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
if (groupInterval) {
|
|
42
|
+
const numericInterval = parseInt(Number(groupInterval));
|
|
43
|
+
if (numericInterval) {
|
|
44
|
+
return pipe(wrapGroupKey(subtractMod(prefix(selector), numericInterval)));
|
|
45
|
+
} else {
|
|
46
|
+
// timezone adjusted field
|
|
47
|
+
const tafield = {
|
|
48
|
+
$subtract: [prefix(selector), timezoneOffset * 60 * 1000],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
switch (groupInterval) {
|
|
52
|
+
case 'year':
|
|
53
|
+
return pipe(
|
|
54
|
+
wrapGroupKey({
|
|
55
|
+
$year: tafield,
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
case 'quarter':
|
|
59
|
+
return pipe(
|
|
60
|
+
{
|
|
61
|
+
// need to pre-calculate month(date)+2, because the divInt logic
|
|
62
|
+
// will reuse the field and we don't want to calculate it multiple
|
|
63
|
+
// times
|
|
64
|
+
$addFields: {
|
|
65
|
+
___mp2: {
|
|
66
|
+
$add: [
|
|
67
|
+
{
|
|
68
|
+
$month: tafield,
|
|
69
|
+
},
|
|
70
|
+
2,
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
wrapGroupKey(divInt('$___mp2', 3))
|
|
76
|
+
);
|
|
77
|
+
case 'month':
|
|
78
|
+
return pipe(
|
|
79
|
+
wrapGroupKey({
|
|
80
|
+
$month: tafield,
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
case 'day':
|
|
84
|
+
return pipe(
|
|
85
|
+
wrapGroupKey({
|
|
86
|
+
$dayOfMonth: tafield,
|
|
87
|
+
})
|
|
88
|
+
);
|
|
89
|
+
case 'dayOfWeek':
|
|
90
|
+
return pipe(
|
|
91
|
+
wrapGroupKey({
|
|
92
|
+
$subtract: [
|
|
93
|
+
{
|
|
94
|
+
$dayOfWeek: tafield, // correct in that it's sunday to saturday, but it's 1-7 (must be 0-6)
|
|
95
|
+
},
|
|
96
|
+
1,
|
|
97
|
+
],
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
case 'hour':
|
|
101
|
+
return pipe(
|
|
102
|
+
wrapGroupKey({
|
|
103
|
+
$hour: tafield,
|
|
104
|
+
})
|
|
105
|
+
);
|
|
106
|
+
case 'minute':
|
|
107
|
+
return pipe(
|
|
108
|
+
wrapGroupKey({
|
|
109
|
+
$minute: tafield,
|
|
110
|
+
})
|
|
111
|
+
);
|
|
112
|
+
case 'second':
|
|
113
|
+
return pipe(
|
|
114
|
+
wrapGroupKey({
|
|
115
|
+
$second: tafield,
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
default:
|
|
119
|
+
// unknown grouping operator, ignoring
|
|
120
|
+
return pipe(wrapGroupKey(prefix(selector)));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
return pipe(wrapGroupKey(prefix(selector)));
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const createGroupStagePipeline = (
|
|
129
|
+
includeDataItems,
|
|
130
|
+
countingSeparately,
|
|
131
|
+
itemProjection,
|
|
132
|
+
groupKeyPipeline
|
|
133
|
+
) => {
|
|
134
|
+
let result = {
|
|
135
|
+
$group: {
|
|
136
|
+
// must use _id at this point for the group key
|
|
137
|
+
_id: '$' + createGroupFieldName(groupKeyPipeline.groupIndex),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
if (!countingSeparately) {
|
|
141
|
+
// this method of counting results in the number of data items in the group
|
|
142
|
+
// if the group has sub-groups, it can't be used
|
|
143
|
+
result.$group.count = {
|
|
144
|
+
$sum: 1,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (includeDataItems) {
|
|
148
|
+
// include items directly if we're expected to do so, and if this is the
|
|
149
|
+
// most deeply nested group in case there are several
|
|
150
|
+
result.$group.items = {
|
|
151
|
+
$push: itemProjection,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return groupKeyPipeline.concat([result]);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const createGroupingPipeline = (
|
|
159
|
+
desc,
|
|
160
|
+
includeDataItems,
|
|
161
|
+
countingSeparately,
|
|
162
|
+
groupKeyPipeline,
|
|
163
|
+
itemProjection = '$$CURRENT'
|
|
164
|
+
) => {
|
|
165
|
+
let projectStage = {
|
|
166
|
+
$project: {
|
|
167
|
+
// rename _id to key
|
|
168
|
+
_id: 0,
|
|
169
|
+
key: '$_id',
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
let sortStage = {
|
|
173
|
+
$sort: {
|
|
174
|
+
key: desc ? -1 : 1,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
let pipeline = createGroupStagePipeline(
|
|
179
|
+
includeDataItems,
|
|
180
|
+
countingSeparately,
|
|
181
|
+
itemProjection,
|
|
182
|
+
groupKeyPipeline
|
|
183
|
+
).concat([projectStage, sortStage]);
|
|
184
|
+
|
|
185
|
+
if (!countingSeparately) {
|
|
186
|
+
// this method of counting results in the number of data items in the group
|
|
187
|
+
// if the group has sub-groups, it can't be used
|
|
188
|
+
projectStage.$project.count = 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (includeDataItems) {
|
|
192
|
+
// include items directly if we're expected to do so, and if this is the
|
|
193
|
+
// most deeply nested group in case there are several
|
|
194
|
+
projectStage.$project.items = 1;
|
|
195
|
+
} else {
|
|
196
|
+
// add null items field otherwise
|
|
197
|
+
pipeline.push({
|
|
198
|
+
$addFields: {
|
|
199
|
+
items: null, // only null works, not [] or leaving out items altogether
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return pipeline;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const createSkipTakePipeline = (skip, take) => {
|
|
208
|
+
let pipeline = [];
|
|
209
|
+
|
|
210
|
+
if (skip)
|
|
211
|
+
pipeline.push({
|
|
212
|
+
$skip: skip,
|
|
213
|
+
});
|
|
214
|
+
if (take)
|
|
215
|
+
pipeline.push({
|
|
216
|
+
$limit: take,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return pipeline;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const createCountPipeline = () => {
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
$count: 'count',
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const createMatchPipeline = (selector, value) => [
|
|
231
|
+
{ $match: { [selector]: value } },
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
const construct = (fieldName, operator, compValue) => ({
|
|
235
|
+
[fieldName]: { [operator]: compValue },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const constructRegex = (fieldName, regex, caseInsensitive) => ({
|
|
239
|
+
[fieldName]: { $regex: regex, $options: caseInsensitive ? 'i' : '' },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const isCorrectFilterOperatorStructure = (element, operator) =>
|
|
243
|
+
element.reduce(
|
|
244
|
+
(r, v) => {
|
|
245
|
+
if (r.previous) return { ok: r.ok, previous: false };
|
|
246
|
+
else
|
|
247
|
+
return {
|
|
248
|
+
ok: r.ok && typeof v === 'string' && v.toLowerCase() === operator,
|
|
249
|
+
previous: true,
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
{ ok: true, previous: true }
|
|
253
|
+
).ok;
|
|
254
|
+
|
|
255
|
+
const isAndChainWithIncompleteAnds = (element) => {
|
|
256
|
+
if (!Array.isArray(element)) return false;
|
|
257
|
+
if (element.length < 2) return false;
|
|
258
|
+
if (!Array.isArray(element[0])) return false;
|
|
259
|
+
// this is important to prevent endless recursion
|
|
260
|
+
if (isCorrectFilterOperatorStructure(element, 'and')) return false;
|
|
261
|
+
return element.reduce(
|
|
262
|
+
(r, v) =>
|
|
263
|
+
r &&
|
|
264
|
+
((typeof v === 'string' && v.toLowerCase() === 'and') ||
|
|
265
|
+
Array.isArray(v)),
|
|
266
|
+
true
|
|
267
|
+
);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
function* _fixAndChainWithIncompleteAnds(chain) {
|
|
271
|
+
// the function assumes that
|
|
272
|
+
// isAndChainWithIncompleteAnds(chain) === true
|
|
273
|
+
let firstDone = false;
|
|
274
|
+
let expectAnd = true;
|
|
275
|
+
for (const item of chain) {
|
|
276
|
+
if (!firstDone) {
|
|
277
|
+
yield item;
|
|
278
|
+
firstDone = true;
|
|
279
|
+
} else {
|
|
280
|
+
if (expectAnd) {
|
|
281
|
+
if (typeof item === 'string') {
|
|
282
|
+
yield 'and';
|
|
283
|
+
expectAnd = false;
|
|
284
|
+
} else {
|
|
285
|
+
yield 'and';
|
|
286
|
+
yield item;
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
if (typeof item !== 'string') {
|
|
290
|
+
yield item;
|
|
291
|
+
expectAnd = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const fixAndChainWithIncompleteAnds = (element) =>
|
|
299
|
+
Array.from(_fixAndChainWithIncompleteAnds(element));
|
|
300
|
+
|
|
301
|
+
// eslint-disable-next-line complexity
|
|
302
|
+
const parseFilter = (element, contextOptions = {}) => {
|
|
303
|
+
// Element can be a string denoting a field name - I don't know if that's a case
|
|
304
|
+
// supported by the widgets in any way, but it seems conceivable that somebody constructs
|
|
305
|
+
// an expression like [ "!", "boolValueField" ]
|
|
306
|
+
// In the string case, I return a truth-checking filter.
|
|
307
|
+
//
|
|
308
|
+
// Element can be an array with two items
|
|
309
|
+
// For two items:
|
|
310
|
+
// 0: unary operator
|
|
311
|
+
// 1: operand
|
|
312
|
+
//
|
|
313
|
+
// Element can be an array with an odd number of items
|
|
314
|
+
// For three items:
|
|
315
|
+
// 0: operand 1 - this is described as the "getter" in the docs - i.e. field name -
|
|
316
|
+
// but in the cases of "and" and "or" it could be another nested element
|
|
317
|
+
// 1: operator
|
|
318
|
+
// 2: operand 2 - the value for comparison - for "and" and "or" can be a nested element
|
|
319
|
+
//
|
|
320
|
+
// For more than three items, it's assumed that this is a chain of "or" or "and" -
|
|
321
|
+
// either one or the other, no combinations
|
|
322
|
+
// 0: operand 1
|
|
323
|
+
// 1: "or" or "and"
|
|
324
|
+
// 2: operand 2
|
|
325
|
+
// 3: "or" or "and" - must be the same as (1) - see comment in test "incorrect operator chain"
|
|
326
|
+
// 4: operand 3
|
|
327
|
+
// .... etc
|
|
328
|
+
//
|
|
329
|
+
|
|
330
|
+
const { caseInsensitiveRegex } = contextOptions;
|
|
331
|
+
|
|
332
|
+
const rval = (match, fieldList) => ({ match, fieldList });
|
|
333
|
+
|
|
334
|
+
if (typeof element === 'string') {
|
|
335
|
+
const nf = checkNestedField(element);
|
|
336
|
+
const fieldName = nf ? nf.filterFieldName : element;
|
|
337
|
+
return rval(construct(fieldName, '$eq', true), [element]);
|
|
338
|
+
} else if (Array.isArray(element)) {
|
|
339
|
+
if (element.length === 1 && Array.isArray(element[0])) {
|
|
340
|
+
// assuming a nested array in this case:
|
|
341
|
+
// the pivot grid sometimes does this
|
|
342
|
+
// [ [ "field", "=", 5 ] ]
|
|
343
|
+
return parseFilter(element[0], contextOptions);
|
|
344
|
+
} else if (element.length === 2) {
|
|
345
|
+
// unary operator - only one supported
|
|
346
|
+
if (element[0] === '!') {
|
|
347
|
+
const { match, fieldList } = parseFilter(element[1], contextOptions);
|
|
348
|
+
if (match)
|
|
349
|
+
return rval(
|
|
350
|
+
{
|
|
351
|
+
$nor: [match],
|
|
352
|
+
},
|
|
353
|
+
fieldList
|
|
354
|
+
);
|
|
355
|
+
else return null;
|
|
356
|
+
} else if (isAndChainWithIncompleteAnds(element))
|
|
357
|
+
return parseFilter(
|
|
358
|
+
fixAndChainWithIncompleteAnds(element),
|
|
359
|
+
contextOptions
|
|
360
|
+
);
|
|
361
|
+
else return null;
|
|
362
|
+
} else {
|
|
363
|
+
if (isAndChainWithIncompleteAnds(element))
|
|
364
|
+
return parseFilter(
|
|
365
|
+
fixAndChainWithIncompleteAnds(element),
|
|
366
|
+
contextOptions
|
|
367
|
+
);
|
|
368
|
+
else if (element.length % 2 === 1) {
|
|
369
|
+
// odd number of elements - let's see what the operator is
|
|
370
|
+
const operator = String(element[1]).toLowerCase();
|
|
371
|
+
|
|
372
|
+
if (['and', 'or'].includes(operator)) {
|
|
373
|
+
if (isCorrectFilterOperatorStructure(element, operator)) {
|
|
374
|
+
// all operators are the same - build a list of conditions from the nested
|
|
375
|
+
// items, combine with the operator
|
|
376
|
+
let result = element.reduce(
|
|
377
|
+
(r, v) => {
|
|
378
|
+
if (r.previous) return { ...r, previous: false };
|
|
379
|
+
else {
|
|
380
|
+
const nestedResult = parseFilter(v, contextOptions);
|
|
381
|
+
const nestedFilter = nestedResult && nestedResult.match;
|
|
382
|
+
const fieldList = nestedResult ? nestedResult.fieldList : [];
|
|
383
|
+
if (nestedFilter) r.list.push(nestedFilter);
|
|
384
|
+
return {
|
|
385
|
+
list: r.list,
|
|
386
|
+
fieldList: r.fieldList.concat(fieldList),
|
|
387
|
+
previous: true,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
{ list: [], fieldList: [], previous: false }
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
return rval({ ['$' + operator]: result.list }, result.fieldList);
|
|
395
|
+
} else return null;
|
|
396
|
+
} else {
|
|
397
|
+
if (element.length === 3) {
|
|
398
|
+
const nf = checkNestedField(element[0]);
|
|
399
|
+
const fieldName = nf ? nf.filterFieldName : element[0];
|
|
400
|
+
|
|
401
|
+
switch (operator) {
|
|
402
|
+
case '=':
|
|
403
|
+
return rval(construct(fieldName, '$eq', element[2]), [
|
|
404
|
+
element[0],
|
|
405
|
+
]);
|
|
406
|
+
case '<>':
|
|
407
|
+
return rval(construct(fieldName, '$ne', element[2]), [
|
|
408
|
+
element[0],
|
|
409
|
+
]);
|
|
410
|
+
case '>':
|
|
411
|
+
return rval(construct(fieldName, '$gt', element[2]), [
|
|
412
|
+
element[0],
|
|
413
|
+
]);
|
|
414
|
+
case '>=':
|
|
415
|
+
return rval(construct(fieldName, '$gte', element[2]), [
|
|
416
|
+
element[0],
|
|
417
|
+
]);
|
|
418
|
+
case '<':
|
|
419
|
+
return rval(construct(fieldName, '$lt', element[2]), [
|
|
420
|
+
element[0],
|
|
421
|
+
]);
|
|
422
|
+
case '<=':
|
|
423
|
+
return rval(construct(fieldName, '$lte', element[2]), [
|
|
424
|
+
element[0],
|
|
425
|
+
]);
|
|
426
|
+
case 'startswith':
|
|
427
|
+
return rval(
|
|
428
|
+
constructRegex(
|
|
429
|
+
fieldName,
|
|
430
|
+
'^' + element[2],
|
|
431
|
+
caseInsensitiveRegex
|
|
432
|
+
),
|
|
433
|
+
[element[0]]
|
|
434
|
+
);
|
|
435
|
+
case 'endswith':
|
|
436
|
+
return rval(
|
|
437
|
+
constructRegex(
|
|
438
|
+
fieldName,
|
|
439
|
+
element[2] + '$',
|
|
440
|
+
caseInsensitiveRegex
|
|
441
|
+
),
|
|
442
|
+
[element[0]]
|
|
443
|
+
);
|
|
444
|
+
case 'contains':
|
|
445
|
+
return rval(
|
|
446
|
+
constructRegex(fieldName, element[2], caseInsensitiveRegex),
|
|
447
|
+
[element[0]]
|
|
448
|
+
);
|
|
449
|
+
case 'notcontains':
|
|
450
|
+
return rval(
|
|
451
|
+
constructRegex(
|
|
452
|
+
fieldName,
|
|
453
|
+
'^((?!' + element[2] + ').)*$',
|
|
454
|
+
caseInsensitiveRegex
|
|
455
|
+
),
|
|
456
|
+
[element[0]]
|
|
457
|
+
);
|
|
458
|
+
case 'equalsobjectid':
|
|
459
|
+
return rval(
|
|
460
|
+
construct(fieldName, '$eq', new ObjectId(element[2])),
|
|
461
|
+
[element[0]]
|
|
462
|
+
);
|
|
463
|
+
default:
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
} else return null;
|
|
467
|
+
}
|
|
468
|
+
} else return null;
|
|
469
|
+
}
|
|
470
|
+
} else return null;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const createFilterPipeline = (filter, contextOptions) => {
|
|
474
|
+
const dummy = {
|
|
475
|
+
pipeline: [],
|
|
476
|
+
fieldList: [],
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
if (filter) {
|
|
480
|
+
const result = parseFilter(filter, contextOptions);
|
|
481
|
+
const match = result && result.match;
|
|
482
|
+
const fieldList = result ? result.fieldList : [];
|
|
483
|
+
if (match)
|
|
484
|
+
return {
|
|
485
|
+
pipeline: [
|
|
486
|
+
{
|
|
487
|
+
$match: match,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
fieldList: fieldList,
|
|
491
|
+
};
|
|
492
|
+
else return dummy;
|
|
493
|
+
} else return dummy;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const createSortPipeline = (sort) =>
|
|
497
|
+
sort
|
|
498
|
+
? [
|
|
499
|
+
{
|
|
500
|
+
$sort: sort.reduce(
|
|
501
|
+
(r, v) => ({ ...r, [v.selector]: v.desc ? -1 : 1 }),
|
|
502
|
+
{}
|
|
503
|
+
),
|
|
504
|
+
},
|
|
505
|
+
]
|
|
506
|
+
: [];
|
|
507
|
+
|
|
508
|
+
const createSummaryPipeline = (summary) => {
|
|
509
|
+
if (summary) {
|
|
510
|
+
let gc = { _id: null };
|
|
511
|
+
for (const s of summary) {
|
|
512
|
+
switch (s.summaryType) {
|
|
513
|
+
case 'sum':
|
|
514
|
+
case 'avg':
|
|
515
|
+
case 'min':
|
|
516
|
+
case 'max':
|
|
517
|
+
gc[`___${s.summaryType}${s.selector}`] = {
|
|
518
|
+
[`$${s.summaryType}`]: `$${s.selector}`,
|
|
519
|
+
};
|
|
520
|
+
break;
|
|
521
|
+
case 'count':
|
|
522
|
+
gc.___count = { $sum: 1 };
|
|
523
|
+
break;
|
|
524
|
+
default:
|
|
525
|
+
console.error(`Invalid summary type '${s.summaryType}', ignoring`);
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return [
|
|
530
|
+
{
|
|
531
|
+
$group: gc,
|
|
532
|
+
},
|
|
533
|
+
];
|
|
534
|
+
} else return [];
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
const createSearchPipeline = (expr, op, val, contextOptions) => {
|
|
538
|
+
const dummy = {
|
|
539
|
+
pipeline: [],
|
|
540
|
+
fieldList: [],
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
if (!expr || !op || !val) return dummy;
|
|
544
|
+
|
|
545
|
+
let criteria;
|
|
546
|
+
if (typeof expr === 'string') criteria = [expr, op, val];
|
|
547
|
+
else if (expr.length > 0) {
|
|
548
|
+
criteria = [];
|
|
549
|
+
for (const exprItem of expr) {
|
|
550
|
+
if (criteria.length) criteria.push('or');
|
|
551
|
+
criteria.push([exprItem, op, val]);
|
|
552
|
+
}
|
|
553
|
+
} else return dummy;
|
|
554
|
+
|
|
555
|
+
return createFilterPipeline(criteria, contextOptions);
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const createSelectProjectExpression = (fields, explicitId = false) => {
|
|
559
|
+
if (fields && fields.length > 0) {
|
|
560
|
+
let project = {};
|
|
561
|
+
if (explicitId) project._id = '$_id';
|
|
562
|
+
for (const field of fields) project[field] = '$' + field;
|
|
563
|
+
return project;
|
|
564
|
+
} else return undefined;
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const createSelectPipeline = (fields, contextOptions) => {
|
|
568
|
+
if (fields && fields.length > 0) {
|
|
569
|
+
return [
|
|
570
|
+
{
|
|
571
|
+
$project: createSelectProjectExpression(fields, contextOptions),
|
|
572
|
+
},
|
|
573
|
+
];
|
|
574
|
+
} else return [];
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// check whether any of the fields have a path structure ("field.Nested")
|
|
578
|
+
// and a recognized element for the nested part
|
|
579
|
+
// if so, I need to add the nested fields to the pipeline for filtering
|
|
580
|
+
const nestedFieldRegex = /^([^.]+)\.(year|quarter|month|dayofweek|day)$/i;
|
|
581
|
+
|
|
582
|
+
const checkNestedField = (fieldName) => {
|
|
583
|
+
const match = nestedFieldRegex.exec(fieldName);
|
|
584
|
+
if (!match) return undefined;
|
|
585
|
+
return {
|
|
586
|
+
base: match[1],
|
|
587
|
+
nested: match[2],
|
|
588
|
+
filterFieldName: `___${match[1]}_${match[2]}`,
|
|
589
|
+
};
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const createAddNestedFieldsPipeline = (fieldNames, contextOptions) => {
|
|
593
|
+
const { timezoneOffset } = contextOptions;
|
|
594
|
+
// constructing a pipeline that potentially has two parts, because the
|
|
595
|
+
// quarter calculation has two steps
|
|
596
|
+
// both parts will be wrapped in { $addFields: PART } after this
|
|
597
|
+
// reduce call completes
|
|
598
|
+
const pr = fieldNames.reduce(
|
|
599
|
+
(r, v) => {
|
|
600
|
+
const nf = checkNestedField(v);
|
|
601
|
+
|
|
602
|
+
if (nf) {
|
|
603
|
+
// ignore all unknown cases - perhaps people have actual db fields
|
|
604
|
+
// with . in them
|
|
605
|
+
const nestedFunction = nf.nested.toLowerCase();
|
|
606
|
+
if (
|
|
607
|
+
['year', 'quarter', 'month', 'day', 'dayofweek'].includes(
|
|
608
|
+
nestedFunction
|
|
609
|
+
)
|
|
610
|
+
) {
|
|
611
|
+
// timezone adjusted field
|
|
612
|
+
const tafield = {
|
|
613
|
+
$subtract: ['$' + nf.base, timezoneOffset * 60 * 1000],
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
switch (nestedFunction) {
|
|
617
|
+
case 'year':
|
|
618
|
+
r.pipeline[1][nf.filterFieldName] = {
|
|
619
|
+
$year: tafield,
|
|
620
|
+
};
|
|
621
|
+
r.nestedFields.push(nf.filterFieldName);
|
|
622
|
+
break;
|
|
623
|
+
case 'quarter': {
|
|
624
|
+
const tempField = `___${nf.base}_mp2`;
|
|
625
|
+
r.pipeline[0][tempField] = {
|
|
626
|
+
$add: [
|
|
627
|
+
{
|
|
628
|
+
$month: tafield,
|
|
629
|
+
},
|
|
630
|
+
2,
|
|
631
|
+
],
|
|
632
|
+
};
|
|
633
|
+
r.nestedFields.push(tempField);
|
|
634
|
+
r.pipeline[1][nf.filterFieldName] = divInt('$' + tempField, 3);
|
|
635
|
+
r.nestedFields.push(nf.filterFieldName);
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
case 'month':
|
|
639
|
+
r.pipeline[1][nf.filterFieldName] = {
|
|
640
|
+
$month: tafield,
|
|
641
|
+
};
|
|
642
|
+
r.nestedFields.push(nf.filterFieldName);
|
|
643
|
+
break;
|
|
644
|
+
case 'day':
|
|
645
|
+
r.pipeline[1][nf.filterFieldName] = {
|
|
646
|
+
$dayOfMonth: tafield,
|
|
647
|
+
};
|
|
648
|
+
r.nestedFields.push(nf.filterFieldName);
|
|
649
|
+
break;
|
|
650
|
+
case 'dayofweek':
|
|
651
|
+
r.pipeline[1][nf.filterFieldName] = {
|
|
652
|
+
$subtract: [
|
|
653
|
+
{
|
|
654
|
+
$dayOfWeek: tafield,
|
|
655
|
+
},
|
|
656
|
+
1,
|
|
657
|
+
],
|
|
658
|
+
};
|
|
659
|
+
r.nestedFields.push(nf.filterFieldName);
|
|
660
|
+
break;
|
|
661
|
+
default:
|
|
662
|
+
console.error('Hit a completely impossible default case');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return r;
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
pipeline: [{}, {}],
|
|
670
|
+
nestedFields: [],
|
|
671
|
+
}
|
|
672
|
+
);
|
|
673
|
+
[1, 0].forEach((i) => {
|
|
674
|
+
if (Object.getOwnPropertyNames(pr.pipeline[i]).length === 0) {
|
|
675
|
+
pr.pipeline.splice(i, 1); // nothing in this part, remove
|
|
676
|
+
} else {
|
|
677
|
+
pr.pipeline[i] = {
|
|
678
|
+
$addFields: pr.pipeline[i],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
return pr;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
const createCompleteFilterPipeline = (
|
|
687
|
+
searchExpr,
|
|
688
|
+
searchOperation,
|
|
689
|
+
searchValue,
|
|
690
|
+
filter,
|
|
691
|
+
contextOptions
|
|
692
|
+
) => {
|
|
693
|
+
// this pipeline has the search options, the function also returns
|
|
694
|
+
// a list of fields that are being accessed
|
|
695
|
+
const searchPipeline = createSearchPipeline(
|
|
696
|
+
searchExpr,
|
|
697
|
+
searchOperation,
|
|
698
|
+
searchValue,
|
|
699
|
+
contextOptions
|
|
700
|
+
);
|
|
701
|
+
// and the same for the filter option
|
|
702
|
+
const filterPipeline = createFilterPipeline(filter, contextOptions);
|
|
703
|
+
|
|
704
|
+
// this pipeline adds fields in case there are nested elements:
|
|
705
|
+
// dateField.Month
|
|
706
|
+
const addNestedFieldsPipelineDetails = createAddNestedFieldsPipeline(
|
|
707
|
+
searchPipeline.fieldList.concat(filterPipeline.fieldList),
|
|
708
|
+
contextOptions
|
|
709
|
+
);
|
|
710
|
+
|
|
711
|
+
return {
|
|
712
|
+
pipeline: addNestedFieldsPipelineDetails.pipeline.concat(
|
|
713
|
+
searchPipeline.pipeline,
|
|
714
|
+
filterPipeline.pipeline
|
|
715
|
+
),
|
|
716
|
+
nestedFields: addNestedFieldsPipelineDetails.nestedFields,
|
|
717
|
+
};
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const createRemoveNestedFieldsPipeline = (nestedFields) => {
|
|
721
|
+
if (nestedFields.length === 0) return [];
|
|
722
|
+
|
|
723
|
+
let pd = {};
|
|
724
|
+
for (const f of nestedFields) pd[f] = 0;
|
|
725
|
+
return [
|
|
726
|
+
{
|
|
727
|
+
$project: pd,
|
|
728
|
+
},
|
|
729
|
+
];
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
module.exports = {
|
|
733
|
+
createGroupFieldName,
|
|
734
|
+
createGroupKeyPipeline,
|
|
735
|
+
createGroupingPipeline,
|
|
736
|
+
createSkipTakePipeline,
|
|
737
|
+
createCountPipeline,
|
|
738
|
+
createMatchPipeline,
|
|
739
|
+
createSortPipeline,
|
|
740
|
+
createSummaryPipeline,
|
|
741
|
+
createSelectProjectExpression,
|
|
742
|
+
createSelectPipeline,
|
|
743
|
+
createCompleteFilterPipeline,
|
|
744
|
+
createRemoveNestedFieldsPipeline,
|
|
745
|
+
testing: {
|
|
746
|
+
divInt,
|
|
747
|
+
subtractMod,
|
|
748
|
+
createGroupStagePipeline,
|
|
749
|
+
construct,
|
|
750
|
+
constructRegex,
|
|
751
|
+
parseFilter,
|
|
752
|
+
createFilterPipeline,
|
|
753
|
+
createSearchPipeline,
|
|
754
|
+
checkNestedField,
|
|
755
|
+
createAddNestedFieldsPipeline,
|
|
756
|
+
isAndChainWithIncompleteAnds,
|
|
757
|
+
fixAndChainWithIncompleteAnds,
|
|
758
|
+
isCorrectFilterOperatorStructure,
|
|
759
|
+
},
|
|
760
|
+
};
|