@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/dist/utils.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
4
+
5
+ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
6
+
7
+ var replaceId = function replaceId(item) {
8
+ return item._id ? _extends({}, item, { _id: item._id.toHexString() }) : item;
9
+ };
10
+
11
+ var createSummaryQueryExecutor = function createSummaryQueryExecutor(limit) {
12
+ var queriesExecuted = 0;
13
+
14
+ return function (fn) {
15
+ return !limit || ++queriesExecuted <= limit ? fn() : Promise.resolve();
16
+ };
17
+ };
18
+
19
+ var merge = function merge(os) {
20
+ return Object.assign.apply(Object, [{}].concat(_toConsumableArray(os)));
21
+ };
22
+
23
+ var debug = function debug(id, f) {
24
+ var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
25
+
26
+ var output = options.output || console.log;
27
+ var processResult = options.processResult || function (result) {
28
+ return result;
29
+ };
30
+ var processArgs = options.processArgs || function (args) {
31
+ return args;
32
+ };
33
+ return function () {
34
+ for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
35
+ args[_key] = arguments[_key];
36
+ }
37
+
38
+ output("DEBUG(" + id + "): ", processArgs(args));
39
+ var result = f.apply(undefined, args);
40
+ output("DEBUG(" + id + "/result): ", processResult(result));
41
+ return result;
42
+ };
43
+ };
44
+
45
+ module.exports = { replaceId: replaceId, createSummaryQueryExecutor: createSummaryQueryExecutor, merge: merge, debug: debug };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ var chai = require('chai');
4
+ var assert = chai.assert;
5
+ var sinon = require('sinon');
6
+
7
+ var _require = require('./utils'),
8
+ replaceId = _require.replaceId,
9
+ createSummaryQueryExecutor = _require.createSummaryQueryExecutor;
10
+
11
+ suite('utils', function () {
12
+ suite('replaceId', function () {
13
+ test('works', function () {
14
+ assert.deepEqual(replaceId({ _id: { toHexString: function toHexString() {
15
+ return '42';
16
+ } } }), {
17
+ _id: '42'
18
+ });
19
+ });
20
+ });
21
+
22
+ suite('createSummaryQueryExecutor', function () {
23
+ test('works', function (done) {
24
+ var exec = createSummaryQueryExecutor(3);
25
+ var func = sinon.stub().resolves();
26
+ var promises = [exec(func), exec(func), exec(func), exec(func), exec(func)];
27
+
28
+ Promise.all(promises).then(function (rs) {
29
+ assert.equal(rs.length, 5);
30
+
31
+ assert.equal(func.callCount, 3);
32
+ done();
33
+ });
34
+ });
35
+ });
36
+ });
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/index');
package/options.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/options');
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@builder6/query-mongodb",
3
+ "version": "0.6.1",
4
+ "description": "Querying a MongoDB collection using DevExtreme data store load parameters",
5
+ "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "options.js",
9
+ "dist",
10
+ "src",
11
+ "LICENSE",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "prepare": "babel src --out-dir ./dist",
16
+ "build": "babel src --out-dir ./dist"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/builder6/builder6-server.git"
21
+ },
22
+ "keywords": [
23
+ "DevExtreme",
24
+ "MongoDB"
25
+ ],
26
+ "author": "Oliver Sturm <oliver@oliversturm.com>",
27
+ "license": "SEE LICENSE IN LICENSE",
28
+ "bugs": {
29
+ "url": "https://github.com/builder6/builder6-server/issues"
30
+ },
31
+ "homepage": "https://github.com/builder6/builder6-server#readme",
32
+ "engines": {
33
+ "node": ">=7.3"
34
+ },
35
+ "dependencies": {
36
+ "yup": "^1.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "babel-cli": "^6.26.0",
40
+ "babel-core": "^6.26.3",
41
+ "babel-eslint": "^10.1.0",
42
+ "babel-plugin-remove-comments": "^2.0.0",
43
+ "babel-plugin-transform-es2015-modules-umd": "^6.24.1",
44
+ "babel-plugin-transform-object-rest-spread": "^6.26.0",
45
+ "babel-polyfill": "^6.26.0",
46
+ "babel-preset-env": "^1.7.0",
47
+ "chai": "^4.3.7",
48
+ "eslint": "^8.38.0",
49
+ "eslint-plugin-promise": "^6.1.1",
50
+ "mocha": "^10.2.0",
51
+ "mongodb": "^5.2.0",
52
+ "nyc": "^15.1.0",
53
+ "qs": "^6.11.1",
54
+ "sinon": "^15.0.3"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "gitHead": "2f816481fc81096a973e75ed27ff9b8bb0638e0c"
60
+ }
package/src/index.js ADDED
@@ -0,0 +1,497 @@
1
+ const {
2
+ createGroupFieldName,
3
+ createGroupKeyPipeline,
4
+ createGroupingPipeline,
5
+ createSkipTakePipeline,
6
+ createCountPipeline,
7
+ createMatchPipeline,
8
+ createSortPipeline,
9
+ createSummaryPipeline,
10
+ createSelectProjectExpression,
11
+ createSelectPipeline,
12
+ createCompleteFilterPipeline,
13
+ createRemoveNestedFieldsPipeline,
14
+ } = require('./pipelines');
15
+ const {
16
+ replaceId,
17
+ createSummaryQueryExecutor,
18
+ merge,
19
+ debug,
20
+ } = require('./utils');
21
+ const { getOptions } = require('./options');
22
+
23
+ function createContext(contextOptions, loadOptions) {
24
+ const aggregateCall = (collection, pipeline, identifier) =>
25
+ ((aggregateOptions) => collection.aggregate(pipeline, aggregateOptions))(
26
+ contextOptions.dynamicAggregateOptions
27
+ ? filterAggregateOptions(
28
+ contextOptions.dynamicAggregateOptions(
29
+ identifier,
30
+ pipeline,
31
+ collection
32
+ )
33
+ )
34
+ : contextOptions.aggregateOptions
35
+ );
36
+
37
+ const getCount = (collection, pipeline) =>
38
+ aggregateCall(collection, pipeline, 'getCount')
39
+ .toArray()
40
+ // Strangely, the pipeline returns an empty array when the "match" part
41
+ // filters out all rows - I would expect to still see the "count" stage
42
+ // working, but it doesn't. Ask mongo.
43
+ .then((r) => (r.length > 0 ? r[0].count : 0));
44
+
45
+ const populateSummaryResults = (target, summary, summaryResults) => {
46
+ if (summary) {
47
+ target.summary = [];
48
+
49
+ for (const s of summary) {
50
+ switch (s.summaryType) {
51
+ case 'sum':
52
+ target.summary.push(summaryResults['___sum' + s.selector]);
53
+ break;
54
+ case 'avg':
55
+ target.summary.push(summaryResults['___avg' + s.selector]);
56
+ break;
57
+ case 'min':
58
+ target.summary.push(summaryResults['___min' + s.selector]);
59
+ break;
60
+ case 'max':
61
+ target.summary.push(summaryResults['___max' + s.selector]);
62
+ break;
63
+ case 'count':
64
+ target.summary.push(summaryResults.___count);
65
+ break;
66
+ default:
67
+ console.error(`Invalid summaryType ${s.summaryType}, ignoring`);
68
+ }
69
+ }
70
+ }
71
+ return target;
72
+ };
73
+
74
+ const queryGroupData = (
75
+ collection,
76
+ desc,
77
+ includeDataItems,
78
+ countSeparately,
79
+ itemProjection,
80
+ groupKeyPipeline,
81
+ sortPipeline,
82
+ filterPipelineDetails,
83
+ skipTakePipeline,
84
+ matchPipeline
85
+ ) =>
86
+ aggregateCall(
87
+ collection,
88
+ [
89
+ ...contextOptions.preProcessingPipeline,
90
+ // sort pipeline first, apparently that enables it to use indexes
91
+ ...sortPipeline,
92
+ ...filterPipelineDetails.pipeline,
93
+ ...matchPipeline,
94
+ ...createRemoveNestedFieldsPipeline(
95
+ filterPipelineDetails.nestedFields,
96
+ contextOptions
97
+ ),
98
+ ...createGroupingPipeline(
99
+ desc,
100
+ includeDataItems,
101
+ countSeparately,
102
+ groupKeyPipeline,
103
+ itemProjection,
104
+ contextOptions
105
+ ),
106
+ ...skipTakePipeline,
107
+ ],
108
+ 'queryGroupData'
109
+ )
110
+ .toArray()
111
+ .then((r) =>
112
+ includeDataItems
113
+ ? r.map((i) => ({
114
+ ...i,
115
+ items: contextOptions.replaceIds
116
+ ? i.items.map(replaceId)
117
+ : i.items,
118
+ }))
119
+ : r
120
+ );
121
+
122
+ const queryGroup = (
123
+ collection,
124
+ groupIndex,
125
+ runSummaryQuery,
126
+ filterPipelineDetails,
127
+ skipTakePipeline = [],
128
+ summaryPipeline = [],
129
+ matchPipeline = []
130
+ ) => {
131
+ const group = loadOptions.group[groupIndex];
132
+ const lastGroup = groupIndex === loadOptions.group.length - 1;
133
+ const itemDataRequired = lastGroup && group.isExpanded;
134
+ const separateCountRequired = !lastGroup;
135
+
136
+ // The current implementation of the dxDataGrid, at least, assumes that sub-group details are
137
+ // always included in the result, whether or not the group is marked isExpanded.
138
+ const subGroupsRequired = !lastGroup; // && group.isExpanded;
139
+ const summariesRequired =
140
+ loadOptions.groupSummary && loadOptions.groupSummary.length > 0;
141
+
142
+ const groupKeyPipeline = createGroupKeyPipeline(
143
+ group.selector,
144
+ group.groupInterval,
145
+ groupIndex,
146
+ contextOptions
147
+ );
148
+
149
+ const augmentWithSubGroups = (groupData) =>
150
+ subGroupsRequired
151
+ ? groupData.map((item) =>
152
+ queryGroup(
153
+ collection,
154
+ groupIndex + 1,
155
+ runSummaryQuery,
156
+ filterPipelineDetails, // used unchanged in lower levels
157
+ [], // skip/take doesn't apply on lower levels - correct?
158
+ summaryPipeline, // unmodified
159
+ // matchPipeline modified to filter down into group level
160
+ [
161
+ ...matchPipeline,
162
+ // not completely clean to include this in the match pipeline, but the field
163
+ // added in the groupKeyPipeline is required specifically for the following match
164
+ ...groupKeyPipeline,
165
+ ...createMatchPipeline(
166
+ createGroupFieldName(groupIndex),
167
+ item.key,
168
+ contextOptions
169
+ ),
170
+ ]
171
+ ).then((r) => {
172
+ item.items = r;
173
+ item.count = r.length;
174
+ return r;
175
+ })
176
+ )
177
+ : [];
178
+
179
+ const augmentWithSeparateCount = (groupData) => {
180
+ if (separateCountRequired) {
181
+ // We need to count separately because this is not the lowest level group,
182
+ // but since we didn't query details about our nested group, we can't just go
183
+ // for the length of the result array. An extra query is required in this case.
184
+ // Even though the count is a type of summary for the group, it is special - different
185
+ // from other group level summaries. The difference is that for each group, a summary
186
+ // is usually calculated with its data, even if that data isn't actually visible in the
187
+ // UI at the time. The count on the other hand is meant to represent the number of
188
+ // elements in the group, and in case these elements are sub-groups instead of data
189
+ // items, count represents a value that must not be calculated using the data items.
190
+
191
+ const nextGroup = loadOptions.group[groupIndex + 1];
192
+ const nextGroupKeyPipeline = createGroupKeyPipeline(
193
+ nextGroup.selector,
194
+ nextGroup.groupInterval,
195
+ groupIndex + 1,
196
+ contextOptions
197
+ );
198
+ return groupData.map((item) =>
199
+ getCount(collection, [
200
+ ...contextOptions.preProcessingPipeline,
201
+ ...filterPipelineDetails.pipeline,
202
+ ...groupKeyPipeline,
203
+
204
+ ...matchPipeline,
205
+ ...createMatchPipeline(
206
+ createGroupFieldName(groupIndex),
207
+ item.key,
208
+ contextOptions
209
+ ),
210
+ ...createGroupingPipeline(
211
+ nextGroup.desc,
212
+ false,
213
+ true,
214
+ nextGroupKeyPipeline,
215
+ contextOptions
216
+ ),
217
+ ...createCountPipeline(contextOptions),
218
+ ]).then((r) => {
219
+ item.count = r;
220
+ return r;
221
+ })
222
+ );
223
+ } else return [];
224
+ };
225
+
226
+ const augmentWithSummaries = (groupData) =>
227
+ summariesRequired
228
+ ? groupData.map((item) =>
229
+ runSummaryQuery(() =>
230
+ aggregateCall(
231
+ collection,
232
+ [
233
+ ...contextOptions.preProcessingPipeline,
234
+ ...filterPipelineDetails.pipeline,
235
+ ...groupKeyPipeline,
236
+ ...matchPipeline,
237
+ ...createMatchPipeline(
238
+ createGroupFieldName(groupIndex),
239
+ item.key,
240
+ contextOptions
241
+ ),
242
+ ...summaryPipeline,
243
+ ],
244
+ 'augmentWithSummaries'
245
+ ).toArray()
246
+ ).then((r) =>
247
+ populateSummaryResults(item, loadOptions.groupSummary, r[0])
248
+ )
249
+ )
250
+ : [];
251
+
252
+ return queryGroupData(
253
+ collection,
254
+ group.desc,
255
+ itemDataRequired,
256
+ separateCountRequired,
257
+ createSelectProjectExpression(loadOptions.select, true),
258
+ groupKeyPipeline,
259
+ itemDataRequired
260
+ ? createSortPipeline(loadOptions.sort, contextOptions)
261
+ : [],
262
+ filterPipelineDetails,
263
+ skipTakePipeline,
264
+ matchPipeline
265
+ ).then(
266
+ (groupData) =>
267
+ /* eslint-disable promise/no-nesting */
268
+ Promise.all([
269
+ ...augmentWithSubGroups(groupData),
270
+ ...augmentWithSeparateCount(groupData),
271
+ ...augmentWithSummaries(groupData),
272
+ ]).then(() => groupData)
273
+ /* eslint-enable promise/no-nesting */
274
+ );
275
+ };
276
+
277
+ const totalCount = (collection, completeFilterPipelineDetails) =>
278
+ loadOptions.requireTotalCount || loadOptions.totalSummary
279
+ ? contextOptions.preferMetadataCount &&
280
+ contextOptions.preProcessingPipeline.length === 0 &&
281
+ completeFilterPipelineDetails.pipeline.length <= 1
282
+ ? [
283
+ collection
284
+ .count(
285
+ completeFilterPipelineDetails.pipeline.length === 1
286
+ ? completeFilterPipelineDetails.pipeline[0]['$match']
287
+ : undefined
288
+ )
289
+ .then((r) => ({ totalCount: r })),
290
+ ]
291
+ : [
292
+ getCount(collection, [
293
+ ...contextOptions.preProcessingPipeline,
294
+ ...completeFilterPipelineDetails.pipeline,
295
+ ...createCountPipeline(contextOptions),
296
+ ]).then((r) => ({ totalCount: r })),
297
+ ]
298
+ : [];
299
+
300
+ const summary =
301
+ (collection, completeFilterPipelineDetails) => (resultObject) =>
302
+ resultObject.totalCount > 0 && loadOptions.totalSummary
303
+ ? aggregateCall(
304
+ collection,
305
+ [
306
+ ...contextOptions.preProcessingPipeline,
307
+ ...completeFilterPipelineDetails.pipeline,
308
+ ...createSummaryPipeline(
309
+ loadOptions.totalSummary,
310
+ contextOptions
311
+ ),
312
+ ],
313
+ 'summary'
314
+ )
315
+ .toArray()
316
+ .then((r) =>
317
+ populateSummaryResults(
318
+ resultObject,
319
+ loadOptions.totalSummary,
320
+ r[0]
321
+ )
322
+ )
323
+ : Promise.resolve(resultObject);
324
+
325
+ const queryGroups = (collection) => {
326
+ const completeFilterPipelineDetails = createCompleteFilterPipeline(
327
+ loadOptions.searchExpr,
328
+ loadOptions.searchOperation,
329
+ loadOptions.searchValue,
330
+ loadOptions.filter,
331
+ contextOptions
332
+ );
333
+ const summaryPipeline = createSummaryPipeline(
334
+ loadOptions.groupSummary,
335
+ contextOptions
336
+ );
337
+ const skipTakePipeline = createSkipTakePipeline(
338
+ loadOptions.skip,
339
+ loadOptions.take,
340
+ contextOptions
341
+ );
342
+
343
+ const mainQueryResult = () =>
344
+ queryGroup(
345
+ collection,
346
+ 0,
347
+ createSummaryQueryExecutor(undefined),
348
+ completeFilterPipelineDetails,
349
+ skipTakePipeline,
350
+ summaryPipeline
351
+ ).then((r) => ({ data: r }));
352
+
353
+ const groupCount = () => {
354
+ if (loadOptions.requireGroupCount) {
355
+ const group = loadOptions.group[0];
356
+
357
+ return [
358
+ getCount(collection, [
359
+ ...contextOptions.preProcessingPipeline,
360
+ ...completeFilterPipelineDetails.pipeline,
361
+ ...createGroupingPipeline(
362
+ group.desc,
363
+ false,
364
+ true,
365
+ createGroupKeyPipeline(
366
+ group.selector,
367
+ group.groupInterval,
368
+ 0,
369
+ contextOptions
370
+ ),
371
+ contextOptions
372
+ ),
373
+ ...createCountPipeline(contextOptions),
374
+ ]).then((r) => ({ groupCount: r })),
375
+ ];
376
+ } else return [];
377
+ };
378
+
379
+ return Promise.all([
380
+ mainQueryResult(),
381
+ ...groupCount(),
382
+ ...totalCount(collection, completeFilterPipelineDetails),
383
+ ])
384
+ .then(merge)
385
+ .then(summary(collection, completeFilterPipelineDetails));
386
+ };
387
+
388
+ const querySimple = (collection) => {
389
+ const completeFilterPipelineDetails = createCompleteFilterPipeline(
390
+ loadOptions.searchExpr,
391
+ loadOptions.searchOperation,
392
+ loadOptions.searchValue,
393
+ loadOptions.filter,
394
+ contextOptions
395
+ );
396
+ const sortPipeline = createSortPipeline(loadOptions.sort, contextOptions);
397
+ const skipTakePipeline = createSkipTakePipeline(
398
+ loadOptions.skip,
399
+ loadOptions.take,
400
+ contextOptions
401
+ );
402
+ const selectPipeline = createSelectPipeline(
403
+ loadOptions.select,
404
+ contextOptions
405
+ );
406
+ const removeNestedFieldsPipeline = createRemoveNestedFieldsPipeline(
407
+ completeFilterPipelineDetails.nestedFields,
408
+ contextOptions
409
+ );
410
+
411
+ const mainQueryResult = () =>
412
+ aggregateCall(
413
+ collection,
414
+ [
415
+ ...contextOptions.preProcessingPipeline,
416
+ ...completeFilterPipelineDetails.pipeline,
417
+ ...sortPipeline,
418
+ ...skipTakePipeline,
419
+ ...selectPipeline,
420
+ ...removeNestedFieldsPipeline,
421
+ ],
422
+ 'mainQueryResult'
423
+ )
424
+ .toArray()
425
+ .then((r) => (contextOptions.replaceIds ? r.map(replaceId) : r))
426
+ .then((r) => ({ data: r }));
427
+
428
+ return Promise.all([
429
+ mainQueryResult(),
430
+ ...totalCount(collection, completeFilterPipelineDetails),
431
+ ])
432
+ .then(merge)
433
+ .then(summary(collection, completeFilterPipelineDetails));
434
+ };
435
+
436
+ return { queryGroups, querySimple };
437
+ }
438
+
439
+ function filterAggregateOptions(proposedOptions) {
440
+ const acceptableAggregateOptionNames = [
441
+ 'allowDiskUse',
442
+ 'maxTimeMS',
443
+ 'readConcern',
444
+ 'collation',
445
+ 'hint',
446
+ 'comment',
447
+ ];
448
+ return Object.keys(proposedOptions).reduce(
449
+ (r, v) =>
450
+ acceptableAggregateOptionNames.includes(v)
451
+ ? { ...r, [v]: proposedOptions[v] }
452
+ : r,
453
+ {}
454
+ );
455
+ }
456
+
457
+ function query(collection, loadOptions = {}, options = {}) {
458
+ const proposedAggregateOptions = options.aggregateOptions;
459
+ delete options.aggregateOptions;
460
+
461
+ const standardContextOptions = {
462
+ replaceIds: true,
463
+ summaryQueryLimit: 100,
464
+ // timezone offset for the query, in the form returned by
465
+ // Date.getTimezoneOffset
466
+ timezoneOffset: 0,
467
+ preProcessingPipeline: [],
468
+ caseInsensitiveRegex: true,
469
+ };
470
+ const contextOptions = Object.assign(standardContextOptions, options);
471
+
472
+ if (!options.dynamicAggregateOptions && proposedAggregateOptions)
473
+ contextOptions.aggregateOptions = filterAggregateOptions(
474
+ proposedAggregateOptions
475
+ );
476
+
477
+ const context = createContext(contextOptions, loadOptions);
478
+
479
+ return loadOptions.group && loadOptions.group.length > 0
480
+ ? context.queryGroups(collection)
481
+ : context.querySimple(collection);
482
+ }
483
+
484
+ function querySimple(collection, loadOptions = {}, options = {}) {
485
+ delete loadOptions.group;
486
+ return query(collection, loadOptions, options);
487
+ }
488
+
489
+ function queryGroups(collection, loadOptions = {}, options = {}) {
490
+ return query(collection, loadOptions, options);
491
+ }
492
+
493
+ module.exports = {
494
+ queryGroups,
495
+ querySimple,
496
+ getOptions
497
+ };