@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/src/options.js ADDED
@@ -0,0 +1,573 @@
1
+ //const valueFixers = require('value-fixers');
2
+ const yup = require('yup');
3
+
4
+ var regexBool = /(true|false)/i;
5
+
6
+ function OptionError(message = '') {
7
+ this.name = 'OptionError';
8
+ this.message = message;
9
+ }
10
+ OptionError.prototype = Error.prototype;
11
+
12
+ const asBool = (v) => {
13
+ let match;
14
+ if (typeof v === 'string' && (match = v.match(regexBool))) {
15
+ return {
16
+ true: true,
17
+ false: false,
18
+ }[match[0].toLowerCase()];
19
+ } else return !!v;
20
+ };
21
+
22
+ function fixFilterAndSearch(schema) {
23
+ // schema can be
24
+ // {
25
+ // fieldName1: 'int',
26
+ // fieldName2: 'float',
27
+ // fieldName3: 'datetime'
28
+ // }
29
+
30
+ const operators = ['=', '<>', '>', '>=', '<', '<='];
31
+
32
+ function fixValue(type, value) {
33
+ return {
34
+ int: parseInt,
35
+ float: parseFloat,
36
+ datetime: (v) => new Date(v),
37
+ bool: asBool,
38
+ }[type](value);
39
+ }
40
+
41
+ function fixFilter(f) {
42
+ if (!f || !Array.isArray(f)) return f;
43
+ if (
44
+ f.length === 3 &&
45
+ typeof f[2] === 'string' &&
46
+ schema[f[0]] &&
47
+ operators.includes(f[1])
48
+ )
49
+ return [f[0], f[1], fixValue(schema[f[0]], f[2])];
50
+ else return f.map((e) => fixFilter(e));
51
+ }
52
+
53
+ // According to https://js.devexpress.com/Documentation/ApiReference/Data_Layer/DataSource/Configuration/#searchExpr
54
+ // it is possible to pass an array of field values for searchExpr: ["firstName", "lastName"]
55
+ // For "fixing" purposes, we need to assume that all such fields have the same
56
+ // type. So if an array is passed for searchExpr (se), we simply look for the
57
+ // first item in that array with a corresponding schema entry and use this
58
+ // going forward. If you have fields in this array which are defined in the
59
+ // schema to have different types, then that's your mistake.
60
+
61
+ function fixSearch(se, so, sv) {
62
+ if (!se || !so || !sv || typeof sv !== 'string') return sv;
63
+ const fieldName =
64
+ typeof se === 'string'
65
+ ? schema[se]
66
+ : Array.isArray(se)
67
+ ? se.find((e) => (schema[e] ? e : null))
68
+ : null;
69
+ return fieldName ? fixValue(schema[fieldName], sv) : sv;
70
+ }
71
+
72
+ return (qry) => {
73
+ if (!qry) return qry;
74
+ const fixedFilter = fixFilter(parse(qry.filter));
75
+ const fixedSearchValue = fixSearch(
76
+ qry.searchExpr,
77
+ qry.searchOperation,
78
+ qry.searchValue
79
+ );
80
+
81
+ return Object.assign(
82
+ {},
83
+ qry,
84
+ fixedFilter
85
+ ? {
86
+ filter: fixedFilter,
87
+ }
88
+ : {},
89
+ fixedSearchValue
90
+ ? {
91
+ searchValue: fixedSearchValue,
92
+ }
93
+ : {}
94
+ );
95
+ };
96
+ }
97
+
98
+ const wrapYupChecker = (yupChecker) => ({
99
+ validate: (o) => {
100
+ try {
101
+ yupChecker.validateSync(o, { strict: true });
102
+ return null;
103
+ } catch (e) {
104
+ return e;
105
+ }
106
+ },
107
+ });
108
+
109
+ const sortOptionsCheckerYup = yup
110
+ .object()
111
+ .shape({
112
+ desc: yup.bool().required(),
113
+ selector: yup.string().required(),
114
+ // Old note - needs rechecking.
115
+ // isExpanded doesn't make any sense with sort, but the grid seems
116
+ // to include it occasionally - probably a bug
117
+ // Btw, this has always been without type restriction - could probably
118
+ // be bool.
119
+ isExpanded: yup.mixed(),
120
+ })
121
+ .noUnknown();
122
+
123
+ const sortOptionsChecker = wrapYupChecker(sortOptionsCheckerYup);
124
+
125
+ // Based on sample code
126
+ // from https://codesandbox.io/s/example-with-or-validate-for-yup-nodelete-dgm4l?file=/src/index.js:29-497
127
+ // from https://github.com/jquense/yup/issues/743
128
+ yup.addMethod(yup.mixed, 'or', function (schemas, msg) {
129
+ return this.test({
130
+ name: 'or',
131
+ message: "Can't find valid schema" || msg,
132
+ test: (value) => {
133
+ if (!Array.isArray(schemas))
134
+ throw new OptionError('"or" requires schema array');
135
+
136
+ const results = schemas.map((schema) =>
137
+ schema.isValidSync(value, { strict: true })
138
+ );
139
+ return results.some((res) => !!res);
140
+ },
141
+ exclusive: false,
142
+ });
143
+ });
144
+
145
+ const groupOptionsCheckerYup = yup
146
+ .object()
147
+ .shape({
148
+ selector: yup.string().required(),
149
+ desc: yup.bool(),
150
+ isExpanded: yup.bool(),
151
+ groupInterval: yup
152
+ .mixed()
153
+ .or([
154
+ yup.number().integer(),
155
+ yup
156
+ .mixed()
157
+ .oneOf([
158
+ 'year',
159
+ 'quarter',
160
+ 'month',
161
+ 'day',
162
+ 'dayOfWeek',
163
+ 'hour',
164
+ 'minute',
165
+ 'second',
166
+ ]),
167
+ ]),
168
+ })
169
+ .noUnknown();
170
+
171
+ const groupOptionsChecker = wrapYupChecker(groupOptionsCheckerYup);
172
+
173
+ const summaryOptionsCheckerYup = yup
174
+ .object()
175
+ .shape({
176
+ summaryType: yup
177
+ .mixed()
178
+ .oneOf(['sum', 'avg', 'min', 'max', 'count'])
179
+ .required(),
180
+ selector: yup.string(),
181
+ })
182
+ .noUnknown();
183
+
184
+ const summaryOptionsChecker = wrapYupChecker(summaryOptionsCheckerYup);
185
+
186
+ function validateAll(list, checker, short = true) {
187
+ return list.reduce(
188
+ (r, v) => {
189
+ if (short && !r.valid) return r; // short circuiting
190
+ const newr = checker.validate(v);
191
+ if (newr) {
192
+ r.errors.push(newr);
193
+ r.valid = false;
194
+ }
195
+ return r;
196
+ },
197
+ { valid: true, errors: [] }
198
+ );
199
+ }
200
+
201
+ function parse(arg, canBeString = false) {
202
+ let ob = arg;
203
+ if (typeof arg === 'string') {
204
+ try {
205
+ ob = JSON.parse(arg);
206
+ } catch (e) {
207
+ if (!canBeString) throw new OptionError(e.message);
208
+ return arg;
209
+ }
210
+ }
211
+ return ob;
212
+ }
213
+
214
+ function representsTrue(val) {
215
+ return val === true || val === 'true';
216
+ }
217
+
218
+ function wrapLoadOptions(lo) {
219
+ return {
220
+ loadOptions: lo,
221
+ };
222
+ }
223
+
224
+ function wrapProcessingOptions(po) {
225
+ return {
226
+ processingOptions: po,
227
+ };
228
+ }
229
+
230
+ function check(
231
+ qry,
232
+ onames,
233
+ checker,
234
+ converter = (v /*, vname*/) => v,
235
+ defaultValue = {},
236
+ wrapper = wrapLoadOptions
237
+ ) {
238
+ const options = typeof onames === 'string' ? [onames] : onames;
239
+ const allFound = qry && options.reduce((r, v) => r && !!qry[v], true);
240
+
241
+ if (!allFound) return defaultValue;
242
+ try {
243
+ const vals = options.map((o) => converter(qry[o], o));
244
+
245
+ const checkResult = checker(...vals);
246
+
247
+ // It's currently not possible to return per-value errors
248
+ // If something goes wrong, all tested options will be highlighted
249
+ // as errors at the same time.
250
+ return checkResult
251
+ ? wrapper(checkResult)
252
+ : {
253
+ errors: options.map((o) => `Invalid '${o}': ${qry[o]}`),
254
+ };
255
+ } catch (err) {
256
+ //console.log('Error caught in check: ' + JSON.stringify(err));
257
+ //console.log('Error message in check: ' + err.message);
258
+ return {
259
+ errors: [err],
260
+ };
261
+ }
262
+ }
263
+
264
+ function takeOptions(qry) {
265
+ return check(
266
+ qry,
267
+ 'take',
268
+ (take) =>
269
+ take >= 0
270
+ ? {
271
+ take,
272
+ }
273
+ : null,
274
+ (take) => parseInt(take)
275
+ );
276
+ }
277
+
278
+ function skipOptions(qry) {
279
+ return check(
280
+ qry,
281
+ 'skip',
282
+ (skip) =>
283
+ skip >= 0
284
+ ? {
285
+ skip,
286
+ }
287
+ : null,
288
+ (skip) => parseInt(skip)
289
+ );
290
+ }
291
+
292
+ function totalCountOptions(qry) {
293
+ return check(
294
+ qry,
295
+ 'requireTotalCount',
296
+ (requireTotalCount) => ({
297
+ requireTotalCount,
298
+ }),
299
+ (requireTotalCount) => representsTrue(requireTotalCount)
300
+ );
301
+ }
302
+
303
+ function sortOptions(qry) {
304
+ return check(
305
+ qry,
306
+ 'sort',
307
+ (sort) => {
308
+ const sortOptions = parse(sort);
309
+ if (Array.isArray(sortOptions) && sortOptions.length > 0) {
310
+ const vr = validateAll(sortOptions, sortOptionsChecker);
311
+ if (vr.valid)
312
+ return {
313
+ sort: sortOptions,
314
+ };
315
+ else {
316
+ //console.log('Error string generated in sortOptions: ' + JSON.stringify(vr.errors));
317
+ throw new OptionError(
318
+ `Sort parameter validation errors: ${JSON.stringify(vr.errors)}`
319
+ );
320
+ }
321
+ } else return null;
322
+ },
323
+ (sort) => {
324
+ const sortOptions = parse(sort);
325
+ if (Array.isArray(sortOptions)) {
326
+ return sortOptions.map((s) => ({ ...s, desc: representsTrue(s.desc) }));
327
+ } else return sort;
328
+ }
329
+ );
330
+ }
331
+
332
+ function groupOptions(qry) {
333
+ return check(
334
+ qry,
335
+ 'group',
336
+ (group) => {
337
+ const groupOptions = parse(group);
338
+ if (Array.isArray(groupOptions)) {
339
+ if (groupOptions.length > 0) {
340
+ const vr = validateAll(groupOptions, groupOptionsChecker);
341
+ if (vr.valid)
342
+ return mergeResults([
343
+ wrapLoadOptions({
344
+ group: groupOptions,
345
+ }),
346
+ check(
347
+ qry,
348
+ 'requireGroupCount',
349
+ (requireGroupCount) => ({
350
+ requireGroupCount,
351
+ }),
352
+ (requireGroupCount) => representsTrue(requireGroupCount)
353
+ ),
354
+ check(qry, 'groupSummary', (groupSummary) => {
355
+ const gsOptions = parse(groupSummary);
356
+ if (Array.isArray(gsOptions)) {
357
+ if (gsOptions.length > 0) {
358
+ const vr = validateAll(gsOptions, summaryOptionsChecker);
359
+ if (vr.valid)
360
+ return {
361
+ groupSummary: gsOptions,
362
+ };
363
+ else
364
+ throw new OptionError(
365
+ `Group summary parameter validation errors: ${JSON.stringify(
366
+ vr.errors
367
+ )}`
368
+ );
369
+ } else return {}; // ignore empty array
370
+ } else return null;
371
+ }),
372
+ ]);
373
+ else
374
+ throw new OptionError(
375
+ `Group parameter validation errors: ${JSON.stringify(vr.errors)}`
376
+ );
377
+ } else return {}; // ignore empty array
378
+ } else return null;
379
+ },
380
+ (group) => {
381
+ const groupOptions = parse(group);
382
+ if (Array.isArray(groupOptions)) {
383
+ return groupOptions.map((g) => ({
384
+ ...g,
385
+ isExpanded: representsTrue(g.isExpanded),
386
+ }));
387
+ } else return group;
388
+ },
389
+ undefined,
390
+ (o) => o /* deactivate wrapper for the result */
391
+ );
392
+ }
393
+
394
+ function totalSummaryOptions(qry) {
395
+ return check(qry, 'totalSummary', (totalSummary) => {
396
+ const tsOptions = parse(totalSummary);
397
+ if (Array.isArray(tsOptions)) {
398
+ if (tsOptions.length > 0) {
399
+ const vr = validateAll(tsOptions, summaryOptionsChecker);
400
+ if (vr.valid)
401
+ return {
402
+ totalSummary: tsOptions,
403
+ };
404
+ else
405
+ throw new OptionError(
406
+ `Total summary parameter validation errors: ${JSON.stringify(
407
+ vr.errors
408
+ )}`
409
+ );
410
+ } else return {}; // ignore empty array
411
+ } else return null;
412
+ });
413
+ }
414
+
415
+ function filterOptions(qry) {
416
+ return check(qry, 'filter', (filter) => {
417
+ const filterOptions = parse(filter, true);
418
+ if (typeof filterOptions === 'string' || Array.isArray(filterOptions))
419
+ return {
420
+ filter: filterOptions,
421
+ };
422
+ else return null;
423
+ });
424
+ }
425
+
426
+ function searchOptions(qry) {
427
+ return check(
428
+ qry,
429
+ ['searchExpr', 'searchOperation', 'searchValue'],
430
+ (se, so, sv) => {
431
+ if (typeof se === 'string' || Array.isArray(se))
432
+ return {
433
+ searchExpr: se,
434
+ searchOperation: so,
435
+ searchValue: sv,
436
+ };
437
+ else return null;
438
+ }
439
+ );
440
+ }
441
+
442
+ function selectOptions(qry) {
443
+ return check(qry, 'select', (select) => {
444
+ const selectOptions = parse(select, true);
445
+ if (typeof selectOptions === 'string')
446
+ return {
447
+ select: [selectOptions],
448
+ };
449
+ else if (Array.isArray(selectOptions)) {
450
+ if (selectOptions.length > 0) {
451
+ if (selectOptions.reduce((r, v) => r && typeof v === 'string'))
452
+ return {
453
+ select: selectOptions,
454
+ };
455
+ else
456
+ throw new OptionError(
457
+ `Select array parameter has invalid content: ${JSON.stringify(
458
+ selectOptions
459
+ )}`
460
+ );
461
+ } else return {}; // ignore empty array
462
+ } else return null;
463
+ });
464
+ }
465
+
466
+ function timezoneOptions(qry) {
467
+ return check(
468
+ qry,
469
+ 'tzOffset',
470
+ (tzOffset) => ({
471
+ timezoneOffset: parseInt(tzOffset) || 0,
472
+ }),
473
+ (v) => v,
474
+ {
475
+ timezoneOffset: 0,
476
+ },
477
+ wrapProcessingOptions
478
+ );
479
+ }
480
+
481
+ function caseInsensitiveRegexOptions(qry) {
482
+ return check(
483
+ qry,
484
+ 'caseInsensitiveRegex',
485
+ (caseInsensitiveRegex) => ({
486
+ caseInsensitiveRegex,
487
+ }),
488
+ (caseInsensitiveRegex) => representsTrue(caseInsensitiveRegex),
489
+ { caseInsensitiveRegex: true },
490
+ wrapProcessingOptions
491
+ );
492
+ }
493
+
494
+ function summaryQueryLimitOptions(qry) {
495
+ return check(
496
+ qry,
497
+ 'summaryQueryLimit',
498
+ (sql) =>
499
+ sql >= 0
500
+ ? {
501
+ summaryQueryLimit: sql,
502
+ }
503
+ : {},
504
+ (sql) => parseInt(sql),
505
+ {},
506
+ wrapProcessingOptions
507
+ );
508
+ }
509
+
510
+ function mergeResults(results) {
511
+ return results.reduce(
512
+ (r, v) => ({
513
+ loadOptions: {
514
+ ...(r.loadOptions || {}),
515
+ ...(v.loadOptions || {}),
516
+ },
517
+ processingOptions: {
518
+ ...(r.processingOptions || {}),
519
+ ...(v.processingOptions || {}),
520
+ },
521
+ errors: [...(r.errors || []), ...(v.errors || [])],
522
+ }),
523
+ {}
524
+ );
525
+ }
526
+
527
+ function getOptions(qry, schema) {
528
+ if (!qry) return undefined;
529
+
530
+ const fixedQry = schema ? fixFilterAndSearch(schema)(qry) : qry;
531
+
532
+ // console.log('Fixed query: ', JSON.stringify(fixedQry, null, 2));
533
+
534
+ return mergeResults(
535
+ [
536
+ takeOptions,
537
+ skipOptions,
538
+ totalCountOptions,
539
+ sortOptions,
540
+ groupOptions,
541
+ totalSummaryOptions,
542
+ filterOptions,
543
+ searchOptions,
544
+ selectOptions,
545
+ timezoneOptions,
546
+ summaryQueryLimitOptions,
547
+ caseInsensitiveRegexOptions,
548
+ ].map((f) => f(fixedQry))
549
+ );
550
+ }
551
+
552
+ module.exports = {
553
+ getOptions,
554
+ private: {
555
+ fixFilterAndSearch,
556
+ validateAll,
557
+ check,
558
+ takeOptions,
559
+ skipOptions,
560
+ totalCountOptions,
561
+ sortOptions,
562
+ groupOptions,
563
+ totalSummaryOptions,
564
+ filterOptions,
565
+ searchOptions,
566
+ selectOptions,
567
+ sortOptionsChecker,
568
+ groupOptionsChecker,
569
+ summaryOptionsChecker,
570
+ asBool,
571
+ parse
572
+ },
573
+ };