vanta-api 1.4.4 → 1.4.5
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/package.json +1 -1
- package/src/api-features.js +510 -115
package/package.json
CHANGED
package/src/api-features.js
CHANGED
|
@@ -32,6 +32,7 @@ export class ApiFeatures {
|
|
|
32
32
|
|
|
33
33
|
filter() {
|
|
34
34
|
const queryFilters = this._parseQueryFilters();
|
|
35
|
+
|
|
35
36
|
const normalizedManualFilters = this._normalizeLogicalOperators(
|
|
36
37
|
this.manualFilters
|
|
37
38
|
);
|
|
@@ -54,6 +55,7 @@ export class ApiFeatures {
|
|
|
54
55
|
addManualFilters(filters = {}) {
|
|
55
56
|
if (filters && typeof filters === "object" && !Array.isArray(filters)) {
|
|
56
57
|
const normalizedFilters = this._normalizeLogicalOperators(filters);
|
|
58
|
+
|
|
57
59
|
this.manualFilters = this._deepMergeFilters(
|
|
58
60
|
this.manualFilters,
|
|
59
61
|
normalizedFilters
|
|
@@ -208,6 +210,7 @@ export class ApiFeatures {
|
|
|
208
210
|
});
|
|
209
211
|
|
|
210
212
|
data = [];
|
|
213
|
+
|
|
211
214
|
for await (const doc of cursor) {
|
|
212
215
|
data.push(doc);
|
|
213
216
|
}
|
|
@@ -227,40 +230,487 @@ export class ApiFeatures {
|
|
|
227
230
|
}
|
|
228
231
|
}
|
|
229
232
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
+
_addPopulateStages({
|
|
234
|
+
populateItem,
|
|
235
|
+
parentPath = "",
|
|
236
|
+
parentIsArray = false,
|
|
237
|
+
schema = null,
|
|
238
|
+
allowedPopulate = [],
|
|
239
|
+
}) {
|
|
240
|
+
if (!populateItem || !populateItem.path) return;
|
|
241
|
+
|
|
242
|
+
const path = populateItem.path;
|
|
243
|
+
const fullPath = parentPath ? `${parentPath}.${path}` : path;
|
|
244
|
+
|
|
245
|
+
if (!this._isPopulateAllowed(fullPath, allowedPopulate)) {
|
|
246
|
+
return;
|
|
233
247
|
}
|
|
234
248
|
|
|
235
|
-
|
|
249
|
+
const info = this._getPopulateInfo({
|
|
250
|
+
schema,
|
|
251
|
+
path,
|
|
252
|
+
fullPath,
|
|
253
|
+
parentPath,
|
|
254
|
+
parentIsArray,
|
|
255
|
+
populateItem,
|
|
256
|
+
});
|
|
236
257
|
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
258
|
+
if (!parentIsArray) {
|
|
259
|
+
this.pipeline.push({
|
|
260
|
+
$lookup: {
|
|
261
|
+
from: info.collection,
|
|
262
|
+
localField: info.localField,
|
|
263
|
+
foreignField: "_id",
|
|
264
|
+
as: info.as,
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
if (!info.isArray) {
|
|
269
|
+
this.pipeline.push({
|
|
270
|
+
$unwind: {
|
|
271
|
+
path: `$${info.as}`,
|
|
272
|
+
preserveNullAndEmptyArrays: true,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (populateItem.populate) {
|
|
278
|
+
const nestedList = this._normalizeNestedPopulate(populateItem.populate);
|
|
279
|
+
|
|
280
|
+
for (const nested of nestedList) {
|
|
281
|
+
this._addPopulateStages({
|
|
282
|
+
populateItem: nested,
|
|
283
|
+
parentPath: info.as,
|
|
284
|
+
parentIsArray: info.isArray,
|
|
285
|
+
schema: info.refSchema,
|
|
286
|
+
allowedPopulate,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (populateItem.select) {
|
|
292
|
+
this._applyPopulateSelect({
|
|
293
|
+
path: info.as,
|
|
294
|
+
select: populateItem.select,
|
|
295
|
+
isArray: info.isArray,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const tempLookupName = this._makeTempLookupName(fullPath);
|
|
303
|
+
|
|
304
|
+
this.pipeline.push({
|
|
305
|
+
$lookup: {
|
|
306
|
+
from: info.collection,
|
|
307
|
+
localField: info.localField,
|
|
308
|
+
foreignField: "_id",
|
|
309
|
+
as: tempLookupName,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
this.pipeline.push({
|
|
314
|
+
$set: {
|
|
315
|
+
[parentPath]: {
|
|
316
|
+
$map: {
|
|
317
|
+
input: { $ifNull: [`$${parentPath}`, []] },
|
|
318
|
+
as: "item",
|
|
319
|
+
in: {
|
|
320
|
+
$mergeObjects: [
|
|
321
|
+
"$$item",
|
|
322
|
+
{
|
|
323
|
+
[path]: {
|
|
324
|
+
$first: {
|
|
325
|
+
$filter: {
|
|
326
|
+
input: `$${tempLookupName}`,
|
|
327
|
+
as: "joined",
|
|
328
|
+
cond: {
|
|
329
|
+
$eq: ["$$joined._id", `$$item.${path}`],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
this.pipeline.push({ $unset: tempLookupName });
|
|
343
|
+
|
|
344
|
+
if (populateItem.populate) {
|
|
345
|
+
const nestedList = this._normalizeNestedPopulate(populateItem.populate);
|
|
346
|
+
|
|
347
|
+
for (const nested of nestedList) {
|
|
348
|
+
this._addNestedPopulateInsideArrayItem({
|
|
349
|
+
arrayPath: parentPath,
|
|
350
|
+
objectPath: path,
|
|
351
|
+
populateItem: nested,
|
|
352
|
+
allowedPopulate,
|
|
353
|
+
schema: info.refSchema,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (populateItem.select) {
|
|
359
|
+
this._applyNestedObjectSelectInsideArray({
|
|
360
|
+
arrayPath: parentPath,
|
|
361
|
+
objectPath: path,
|
|
362
|
+
select: populateItem.select,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_addNestedPopulateInsideArrayItem({
|
|
368
|
+
arrayPath,
|
|
369
|
+
objectPath,
|
|
370
|
+
populateItem,
|
|
371
|
+
allowedPopulate = [],
|
|
372
|
+
schema = null,
|
|
373
|
+
}) {
|
|
374
|
+
if (!populateItem || !populateItem.path) return;
|
|
375
|
+
|
|
376
|
+
const childPath = populateItem.path;
|
|
377
|
+
const fullPath = `${arrayPath}.${objectPath}.${childPath}`;
|
|
378
|
+
|
|
379
|
+
if (!this._isPopulateAllowed(fullPath, allowedPopulate)) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const info = this._getPopulateInfo({
|
|
384
|
+
schema,
|
|
385
|
+
path: childPath,
|
|
386
|
+
fullPath,
|
|
387
|
+
parentPath: `${arrayPath}.${objectPath}`,
|
|
388
|
+
parentIsArray: true,
|
|
389
|
+
populateItem,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const tempLookupName = this._makeTempLookupName(fullPath);
|
|
393
|
+
|
|
394
|
+
this.pipeline.push({
|
|
395
|
+
$lookup: {
|
|
396
|
+
from: info.collection,
|
|
397
|
+
localField: `${arrayPath}.${objectPath}.${childPath}`,
|
|
398
|
+
foreignField: "_id",
|
|
399
|
+
as: tempLookupName,
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
this.pipeline.push({
|
|
404
|
+
$set: {
|
|
405
|
+
[arrayPath]: {
|
|
406
|
+
$map: {
|
|
407
|
+
input: { $ifNull: [`$${arrayPath}`, []] },
|
|
408
|
+
as: "item",
|
|
409
|
+
in: {
|
|
410
|
+
$mergeObjects: [
|
|
411
|
+
"$$item",
|
|
412
|
+
{
|
|
413
|
+
[objectPath]: {
|
|
414
|
+
$cond: [
|
|
415
|
+
{ $ne: [`$$item.${objectPath}`, null] },
|
|
416
|
+
{
|
|
417
|
+
$mergeObjects: [
|
|
418
|
+
`$$item.${objectPath}`,
|
|
419
|
+
{
|
|
420
|
+
[childPath]: {
|
|
421
|
+
$first: {
|
|
422
|
+
$filter: {
|
|
423
|
+
input: `$${tempLookupName}`,
|
|
424
|
+
as: "joined",
|
|
425
|
+
cond: {
|
|
426
|
+
$eq: [
|
|
427
|
+
"$$joined._id",
|
|
428
|
+
`$$item.${objectPath}.${childPath}`,
|
|
429
|
+
],
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
},
|
|
437
|
+
`$$item.${objectPath}`,
|
|
438
|
+
],
|
|
439
|
+
},
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
this.pipeline.push({ $unset: tempLookupName });
|
|
449
|
+
|
|
450
|
+
if (populateItem.populate) {
|
|
451
|
+
const nestedList = this._normalizeNestedPopulate(populateItem.populate);
|
|
452
|
+
|
|
453
|
+
for (const nested of nestedList) {
|
|
454
|
+
this._addNestedPopulateInsideArrayItem({
|
|
455
|
+
arrayPath,
|
|
456
|
+
objectPath: `${objectPath}.${childPath}`,
|
|
457
|
+
populateItem: nested,
|
|
458
|
+
allowedPopulate,
|
|
459
|
+
schema: info.refSchema,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (populateItem.select) {
|
|
465
|
+
this._applyNestedObjectSelectInsideArray({
|
|
466
|
+
arrayPath,
|
|
467
|
+
objectPath: `${objectPath}.${childPath}`,
|
|
468
|
+
select: populateItem.select,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
_getPopulateInfo({
|
|
474
|
+
schema = null,
|
|
475
|
+
path,
|
|
476
|
+
fullPath,
|
|
477
|
+
parentPath = "",
|
|
478
|
+
parentIsArray = false,
|
|
479
|
+
populateItem = {},
|
|
480
|
+
}) {
|
|
481
|
+
const schemaPath = schema?.path?.(path);
|
|
482
|
+
|
|
483
|
+
const isArray =
|
|
484
|
+
populateItem.isArray === true ||
|
|
485
|
+
schemaPath?.instance === "Array" ||
|
|
486
|
+
Array.isArray(schemaPath?.options?.type);
|
|
487
|
+
|
|
488
|
+
const refModelName =
|
|
489
|
+
populateItem.ref ||
|
|
490
|
+
populateItem.modelName ||
|
|
491
|
+
schemaPath?.options?.ref ||
|
|
492
|
+
schemaPath?.caster?.options?.ref ||
|
|
493
|
+
(Array.isArray(schemaPath?.options?.type)
|
|
494
|
+
? schemaPath.options.type[0]?.ref
|
|
495
|
+
: undefined) ||
|
|
496
|
+
this._inferModelNameFromPath(path);
|
|
497
|
+
|
|
498
|
+
const collection =
|
|
499
|
+
populateItem.collection ||
|
|
500
|
+
populateItem.from ||
|
|
501
|
+
this._resolveCollectionName(refModelName);
|
|
502
|
+
|
|
503
|
+
const refSchema = this._resolveRegisteredSchema(refModelName);
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
path,
|
|
507
|
+
fullPath,
|
|
508
|
+
refModelName,
|
|
509
|
+
collection,
|
|
510
|
+
refSchema,
|
|
511
|
+
isArray,
|
|
512
|
+
localField: parentIsArray ? fullPath : fullPath,
|
|
513
|
+
as: fullPath,
|
|
514
|
+
parentPath,
|
|
515
|
+
parentIsArray,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
_applyPopulateSelect({ path, select, isArray }) {
|
|
520
|
+
const parsed = this._parseSelect(select);
|
|
521
|
+
if (!parsed.fields.length) return;
|
|
522
|
+
|
|
523
|
+
if (parsed.mode === "exclude") {
|
|
524
|
+
this.pipeline.push({
|
|
525
|
+
$unset: parsed.fields.map((field) => `${path}.${field}`),
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const includeFields = this._ensureIdField(parsed.fields);
|
|
532
|
+
|
|
533
|
+
if (isArray) {
|
|
534
|
+
const selectedObject = {};
|
|
535
|
+
|
|
536
|
+
for (const field of includeFields) {
|
|
537
|
+
selectedObject[field] = `$$item.${field}`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
this.pipeline.push({
|
|
541
|
+
$set: {
|
|
542
|
+
[path]: {
|
|
543
|
+
$map: {
|
|
544
|
+
input: { $ifNull: [`$${path}`, []] },
|
|
545
|
+
as: "item",
|
|
546
|
+
in: selectedObject,
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const selectedObject = {};
|
|
556
|
+
|
|
557
|
+
for (const field of includeFields) {
|
|
558
|
+
selectedObject[field] = `$${path}.${field}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
this.pipeline.push({
|
|
562
|
+
$set: {
|
|
563
|
+
[path]: {
|
|
564
|
+
$cond: [{ $ne: [`$${path}`, null] }, selectedObject, `$${path}`],
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_applyNestedObjectSelectInsideArray({ arrayPath, objectPath, select }) {
|
|
571
|
+
const parsed = this._parseSelect(select);
|
|
572
|
+
if (!parsed.fields.length) return;
|
|
573
|
+
|
|
574
|
+
if (parsed.mode === "exclude") {
|
|
575
|
+
this.pipeline.push({
|
|
576
|
+
$unset: parsed.fields.map(
|
|
577
|
+
(field) => `${arrayPath}.${objectPath}.${field}`
|
|
578
|
+
),
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const includeFields = this._ensureIdField(parsed.fields);
|
|
585
|
+
const selectedObject = {};
|
|
586
|
+
|
|
587
|
+
for (const field of includeFields) {
|
|
588
|
+
selectedObject[field] = `$$item.${objectPath}.${field}`;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
this.pipeline.push({
|
|
592
|
+
$set: {
|
|
593
|
+
[arrayPath]: {
|
|
594
|
+
$map: {
|
|
595
|
+
input: { $ifNull: [`$${arrayPath}`, []] },
|
|
596
|
+
as: "item",
|
|
597
|
+
in: {
|
|
598
|
+
$mergeObjects: [
|
|
599
|
+
"$$item",
|
|
600
|
+
{
|
|
601
|
+
[objectPath]: {
|
|
602
|
+
$cond: [
|
|
603
|
+
{ $ne: [`$$item.${objectPath}`, null] },
|
|
604
|
+
selectedObject,
|
|
605
|
+
`$$item.${objectPath}`,
|
|
606
|
+
],
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
],
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
_parseSelect(select = "") {
|
|
618
|
+
const fields = String(select)
|
|
619
|
+
.split(/\s+/)
|
|
620
|
+
.map((field) => field.trim())
|
|
621
|
+
.filter(Boolean)
|
|
622
|
+
.filter((field) => {
|
|
623
|
+
const cleanField = field.replace(/^-/, "");
|
|
624
|
+
return !this._isForbiddenField(cleanField);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const hasInclude = fields.some((field) => !field.startsWith("-"));
|
|
628
|
+
const hasExclude = fields.some((field) => field.startsWith("-"));
|
|
629
|
+
|
|
630
|
+
if (hasInclude && hasExclude) {
|
|
631
|
+
throw new HandleERROR(
|
|
632
|
+
"Cannot mix include and exclude in populate select",
|
|
633
|
+
400
|
|
634
|
+
);
|
|
243
635
|
}
|
|
244
636
|
|
|
637
|
+
return {
|
|
638
|
+
mode: hasExclude ? "exclude" : "include",
|
|
639
|
+
fields: fields.map((field) => field.replace(/^-/, "")),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
_ensureIdField(fields = []) {
|
|
644
|
+
return fields.includes("_id") ? fields : ["_id", ...fields];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
_sanitization() {
|
|
648
|
+
for (const key of Object.keys(this.query)) {
|
|
649
|
+
if (
|
|
650
|
+
key.startsWith("$") ||
|
|
651
|
+
["$where", "$accumulator", "$function"].includes(key)
|
|
652
|
+
) {
|
|
653
|
+
delete this.query[key];
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
["page", "limit"].forEach((field) => {
|
|
658
|
+
if (this.query[field] && !/^[0-9]+$/.test(String(this.query[field]))) {
|
|
659
|
+
throw new HandleERROR(`Invalid ${field}`, 400);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
_parseQueryFilters() {
|
|
665
|
+
const obj = { ...this.query };
|
|
666
|
+
RESERVED_QUERY_KEYS.forEach((key) => delete obj[key]);
|
|
667
|
+
|
|
245
668
|
const out = {};
|
|
246
669
|
|
|
247
|
-
for (const [
|
|
248
|
-
const
|
|
249
|
-
key === "and" ? "$and" : key === "or" ? "$or" : key === "nor" ? "$nor" : key;
|
|
670
|
+
for (const [rawKey, rawVal] of Object.entries(obj)) {
|
|
671
|
+
const bracketMatch = rawKey.match(/^(.+)\[\$?(\w+)\]$/);
|
|
250
672
|
|
|
251
|
-
|
|
673
|
+
if (bracketMatch) {
|
|
674
|
+
const [, field, op] = bracketMatch;
|
|
675
|
+
const cleanOp = op.replace(/^\$/, "");
|
|
676
|
+
|
|
677
|
+
if (securityConfig.allowedOperators?.includes(cleanOp)) {
|
|
678
|
+
out[field] = {
|
|
679
|
+
...(out[field] || {}),
|
|
680
|
+
[`$${cleanOp}`]: rawVal,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (rawVal && typeof rawVal === "object" && !Array.isArray(rawVal)) {
|
|
688
|
+
out[rawKey] = out[rawKey] || {};
|
|
689
|
+
|
|
690
|
+
for (const [op, val] of Object.entries(rawVal)) {
|
|
691
|
+
const cleanOp = op.replace(/^\$/, "");
|
|
692
|
+
|
|
693
|
+
if (securityConfig.allowedOperators?.includes(cleanOp)) {
|
|
694
|
+
out[rawKey][`$${cleanOp}`] = val;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (typeof rawVal === "string" && rawVal.includes(",")) {
|
|
702
|
+
out[rawKey] = rawVal.split(",").map((v) => v.trim());
|
|
703
|
+
} else {
|
|
704
|
+
out[rawKey] = rawVal;
|
|
705
|
+
}
|
|
252
706
|
}
|
|
253
707
|
|
|
254
708
|
return out;
|
|
255
709
|
}
|
|
256
710
|
|
|
257
711
|
_sanitizeFilters(filters = {}) {
|
|
258
|
-
const sanitizeNode = (node, key = ""
|
|
259
|
-
if (
|
|
260
|
-
node instanceof mongoose.Types.ObjectId ||
|
|
261
|
-
node instanceof ObjectId ||
|
|
262
|
-
node instanceof Date
|
|
263
|
-
) {
|
|
712
|
+
const sanitizeNode = (node, key = "") => {
|
|
713
|
+
if (node instanceof mongoose.Types.ObjectId || node instanceof ObjectId) {
|
|
264
714
|
return node;
|
|
265
715
|
}
|
|
266
716
|
|
|
@@ -269,24 +719,21 @@ export class ApiFeatures {
|
|
|
269
719
|
if (node === "false") return false;
|
|
270
720
|
|
|
271
721
|
if (Array.isArray(node)) {
|
|
272
|
-
return node.map((item) => sanitizeNode(item, key
|
|
722
|
+
return node.map((item) => sanitizeNode(item, key));
|
|
273
723
|
}
|
|
274
724
|
|
|
275
725
|
if (node && typeof node === "object") {
|
|
276
726
|
const result = {};
|
|
277
727
|
|
|
278
728
|
for (const [childKey, childVal] of Object.entries(node)) {
|
|
279
|
-
result[childKey] = sanitizeNode(childVal, childKey
|
|
729
|
+
result[childKey] = sanitizeNode(childVal, childKey);
|
|
280
730
|
}
|
|
281
731
|
|
|
282
732
|
return result;
|
|
283
733
|
}
|
|
284
734
|
|
|
285
735
|
if (typeof node === "string") {
|
|
286
|
-
if (
|
|
287
|
-
this.#isStrictObjectId(node) &&
|
|
288
|
-
this._shouldConvertToObjectId(key, parentKey)
|
|
289
|
-
) {
|
|
736
|
+
if (this.#isStrictObjectId(node) && this._shouldConvertToObjectId(key)) {
|
|
290
737
|
return new ObjectId(node);
|
|
291
738
|
}
|
|
292
739
|
|
|
@@ -303,27 +750,51 @@ export class ApiFeatures {
|
|
|
303
750
|
return sanitizeNode(filters);
|
|
304
751
|
}
|
|
305
752
|
|
|
306
|
-
_shouldConvertToObjectId(key = ""
|
|
753
|
+
_shouldConvertToObjectId(key = "") {
|
|
307
754
|
const cleanKey = String(key).replace(/^\$/, "").toLowerCase();
|
|
308
|
-
const cleanParentKey = String(parentKey).replace(/^\$/, "").toLowerCase();
|
|
309
755
|
|
|
310
|
-
|
|
756
|
+
return (
|
|
311
757
|
cleanKey === "_id" ||
|
|
312
758
|
cleanKey === "id" ||
|
|
313
759
|
cleanKey.endsWith("id") ||
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
760
|
+
cleanKey === "eq" ||
|
|
761
|
+
cleanKey === "ne" ||
|
|
762
|
+
cleanKey === "in" ||
|
|
763
|
+
cleanKey === "nin"
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
_normalizeLogicalOperators(filters = {}) {
|
|
768
|
+
if (Array.isArray(filters)) {
|
|
769
|
+
return filters.map((item) => this._normalizeLogicalOperators(item));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (
|
|
773
|
+
!filters ||
|
|
774
|
+
typeof filters !== "object" ||
|
|
775
|
+
filters instanceof mongoose.Types.ObjectId ||
|
|
776
|
+
filters instanceof ObjectId ||
|
|
777
|
+
filters instanceof Date
|
|
317
778
|
) {
|
|
318
|
-
return
|
|
779
|
+
return filters;
|
|
319
780
|
}
|
|
320
781
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
782
|
+
const out = {};
|
|
783
|
+
|
|
784
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
785
|
+
const normalizedKey =
|
|
786
|
+
key === "and"
|
|
787
|
+
? "$and"
|
|
788
|
+
: key === "or"
|
|
789
|
+
? "$or"
|
|
790
|
+
: key === "nor"
|
|
791
|
+
? "$nor"
|
|
792
|
+
: key;
|
|
793
|
+
|
|
794
|
+
out[normalizedKey] = this._normalizeLogicalOperators(value);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return out;
|
|
327
798
|
}
|
|
328
799
|
|
|
329
800
|
_deepMergeFilters(a = {}, b = {}) {
|
|
@@ -350,14 +821,6 @@ export class ApiFeatures {
|
|
|
350
821
|
|
|
351
822
|
_applySecurityFilters(filters = {}) {
|
|
352
823
|
const cleanNode = (node) => {
|
|
353
|
-
if (
|
|
354
|
-
node instanceof mongoose.Types.ObjectId ||
|
|
355
|
-
node instanceof ObjectId ||
|
|
356
|
-
node instanceof Date
|
|
357
|
-
) {
|
|
358
|
-
return node;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
824
|
if (Array.isArray(node)) {
|
|
362
825
|
return node.map(cleanNode);
|
|
363
826
|
}
|
|
@@ -379,74 +842,6 @@ export class ApiFeatures {
|
|
|
379
842
|
return cleanNode(filters);
|
|
380
843
|
}
|
|
381
844
|
|
|
382
|
-
_addPopulateStages(args) {
|
|
383
|
-
return ApiFeatures.prototype.__proto__?._addPopulateStages?.call(this, args);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
_parseQueryFilters() {
|
|
387
|
-
const obj = { ...this.query };
|
|
388
|
-
RESERVED_QUERY_KEYS.forEach((key) => delete obj[key]);
|
|
389
|
-
|
|
390
|
-
const out = {};
|
|
391
|
-
|
|
392
|
-
for (const [rawKey, rawVal] of Object.entries(obj)) {
|
|
393
|
-
const bracketMatch = rawKey.match(/^(.+)\[\$?(\w+)\]$/);
|
|
394
|
-
|
|
395
|
-
if (bracketMatch) {
|
|
396
|
-
const [, field, op] = bracketMatch;
|
|
397
|
-
const cleanOp = op.replace(/^\$/, "");
|
|
398
|
-
|
|
399
|
-
if (securityConfig.allowedOperators?.includes(cleanOp)) {
|
|
400
|
-
out[field] = {
|
|
401
|
-
...(out[field] || {}),
|
|
402
|
-
[`$${cleanOp}`]: rawVal,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
continue;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (rawVal && typeof rawVal === "object" && !Array.isArray(rawVal)) {
|
|
410
|
-
out[rawKey] = out[rawKey] || {};
|
|
411
|
-
|
|
412
|
-
for (const [op, val] of Object.entries(rawVal)) {
|
|
413
|
-
const cleanOp = op.replace(/^\$/, "");
|
|
414
|
-
|
|
415
|
-
if (securityConfig.allowedOperators?.includes(cleanOp)) {
|
|
416
|
-
out[rawKey][`$${cleanOp}`] = val;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (typeof rawVal === "string" && rawVal.includes(",")) {
|
|
424
|
-
out[rawKey] = rawVal.split(",").map((v) => v.trim());
|
|
425
|
-
} else {
|
|
426
|
-
out[rawKey] = rawVal;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return out;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
_sanitization() {
|
|
434
|
-
for (const key of Object.keys(this.query)) {
|
|
435
|
-
if (
|
|
436
|
-
key.startsWith("$") ||
|
|
437
|
-
["$where", "$accumulator", "$function"].includes(key)
|
|
438
|
-
) {
|
|
439
|
-
delete this.query[key];
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
["page", "limit"].forEach((field) => {
|
|
444
|
-
if (this.query[field] && !/^[0-9]+$/.test(String(this.query[field]))) {
|
|
445
|
-
throw new HandleERROR(`Invalid ${field}`, 400);
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
845
|
_normalizePopulateInput(input = "") {
|
|
451
846
|
const raw = [];
|
|
452
847
|
|