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