vanta-api 1.4.3 → 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 +134 -495
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,440 +227,164 @@ 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;
|
|
230
|
+
_normalizeLogicalOperators(filters = {}) {
|
|
231
|
+
if (Array.isArray(filters)) {
|
|
232
|
+
return filters.map((item) => this._normalizeLogicalOperators(item));
|
|
242
233
|
}
|
|
243
234
|
|
|
244
|
-
|
|
245
|
-
schema,
|
|
246
|
-
path,
|
|
247
|
-
fullPath,
|
|
248
|
-
parentPath,
|
|
249
|
-
parentIsArray,
|
|
250
|
-
populateItem,
|
|
251
|
-
});
|
|
235
|
+
if (!filters || typeof filters !== "object") return filters;
|
|
252
236
|
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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;
|
|
237
|
+
if (
|
|
238
|
+
filters instanceof mongoose.Types.ObjectId ||
|
|
239
|
+
filters instanceof ObjectId ||
|
|
240
|
+
filters instanceof Date
|
|
241
|
+
) {
|
|
242
|
+
return filters;
|
|
295
243
|
}
|
|
296
244
|
|
|
297
|
-
const
|
|
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 });
|
|
245
|
+
const out = {};
|
|
338
246
|
|
|
339
|
-
|
|
340
|
-
const
|
|
247
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
248
|
+
const normalizedKey =
|
|
249
|
+
key === "and" ? "$and" : key === "or" ? "$or" : key === "nor" ? "$nor" : key;
|
|
341
250
|
|
|
342
|
-
|
|
343
|
-
this._addNestedPopulateInsideArrayItem({
|
|
344
|
-
arrayPath: parentPath,
|
|
345
|
-
objectPath: path,
|
|
346
|
-
populateItem: nested,
|
|
347
|
-
allowedPopulate,
|
|
348
|
-
schema: info.refSchema,
|
|
349
|
-
});
|
|
350
|
-
}
|
|
251
|
+
out[normalizedKey] = this._normalizeLogicalOperators(value);
|
|
351
252
|
}
|
|
352
253
|
|
|
353
|
-
|
|
354
|
-
this._applyNestedObjectSelectInsideArray({
|
|
355
|
-
arrayPath: parentPath,
|
|
356
|
-
objectPath: path,
|
|
357
|
-
select: populateItem.select,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
254
|
+
return out;
|
|
360
255
|
}
|
|
361
256
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
const childPath = populateItem.path;
|
|
372
|
-
const fullPath = `${arrayPath}.${objectPath}.${childPath}`;
|
|
373
|
-
|
|
374
|
-
if (!this._isPopulateAllowed(fullPath, allowedPopulate)) {
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
257
|
+
_sanitizeFilters(filters = {}) {
|
|
258
|
+
const sanitizeNode = (node, key = "", parentKey = "") => {
|
|
259
|
+
if (
|
|
260
|
+
node instanceof mongoose.Types.ObjectId ||
|
|
261
|
+
node instanceof ObjectId ||
|
|
262
|
+
node instanceof Date
|
|
263
|
+
) {
|
|
264
|
+
return node;
|
|
265
|
+
}
|
|
377
266
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
fullPath,
|
|
382
|
-
parentPath: `${arrayPath}.${objectPath}`,
|
|
383
|
-
parentIsArray: true,
|
|
384
|
-
populateItem,
|
|
385
|
-
});
|
|
267
|
+
if (node === null || node === "null") return null;
|
|
268
|
+
if (node === "true") return true;
|
|
269
|
+
if (node === "false") return false;
|
|
386
270
|
|
|
387
|
-
|
|
271
|
+
if (Array.isArray(node)) {
|
|
272
|
+
return node.map((item) => sanitizeNode(item, key, parentKey));
|
|
273
|
+
}
|
|
388
274
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
from: info.collection,
|
|
392
|
-
localField: `${arrayPath}.${objectPath}.${childPath}`,
|
|
393
|
-
foreignField: "_id",
|
|
394
|
-
as: tempLookupName,
|
|
395
|
-
},
|
|
396
|
-
});
|
|
275
|
+
if (node && typeof node === "object") {
|
|
276
|
+
const result = {};
|
|
397
277
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
});
|
|
278
|
+
for (const [childKey, childVal] of Object.entries(node)) {
|
|
279
|
+
result[childKey] = sanitizeNode(childVal, childKey, key);
|
|
280
|
+
}
|
|
442
281
|
|
|
443
|
-
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
444
284
|
|
|
445
|
-
|
|
446
|
-
|
|
285
|
+
if (typeof node === "string") {
|
|
286
|
+
if (
|
|
287
|
+
this.#isStrictObjectId(node) &&
|
|
288
|
+
this._shouldConvertToObjectId(key, parentKey)
|
|
289
|
+
) {
|
|
290
|
+
return new ObjectId(node);
|
|
291
|
+
}
|
|
447
292
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
allowedPopulate,
|
|
454
|
-
schema: info.refSchema,
|
|
455
|
-
});
|
|
293
|
+
if (/^[0-9]+$/.test(node)) {
|
|
294
|
+
return node.length > 1 && node.startsWith("0")
|
|
295
|
+
? node
|
|
296
|
+
: parseInt(node, 10);
|
|
297
|
+
}
|
|
456
298
|
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (populateItem.select) {
|
|
460
|
-
this._applyNestedObjectSelectInsideArray({
|
|
461
|
-
arrayPath,
|
|
462
|
-
objectPath: `${objectPath}.${childPath}`,
|
|
463
|
-
select: populateItem.select,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
299
|
|
|
468
|
-
|
|
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,
|
|
300
|
+
return node;
|
|
511
301
|
};
|
|
512
|
-
}
|
|
513
302
|
|
|
514
|
-
|
|
515
|
-
|
|
303
|
+
return sanitizeNode(filters);
|
|
304
|
+
}
|
|
516
305
|
|
|
517
|
-
|
|
306
|
+
_shouldConvertToObjectId(key = "", parentKey = "") {
|
|
307
|
+
const cleanKey = String(key).replace(/^\$/, "").toLowerCase();
|
|
308
|
+
const cleanParentKey = String(parentKey).replace(/^\$/, "").toLowerCase();
|
|
518
309
|
|
|
519
|
-
if (
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
310
|
+
if (
|
|
311
|
+
cleanKey === "_id" ||
|
|
312
|
+
cleanKey === "id" ||
|
|
313
|
+
cleanKey.endsWith("id") ||
|
|
314
|
+
cleanParentKey === "_id" ||
|
|
315
|
+
cleanParentKey === "id" ||
|
|
316
|
+
cleanParentKey.endsWith("id")
|
|
317
|
+
) {
|
|
318
|
+
return true;
|
|
524
319
|
}
|
|
525
320
|
|
|
526
|
-
|
|
321
|
+
return (
|
|
322
|
+
["eq", "ne", "in", "nin"].includes(cleanKey) &&
|
|
323
|
+
(cleanParentKey === "_id" ||
|
|
324
|
+
cleanParentKey === "id" ||
|
|
325
|
+
cleanParentKey.endsWith("id"))
|
|
326
|
+
);
|
|
327
|
+
}
|
|
527
328
|
|
|
528
|
-
|
|
529
|
-
|
|
329
|
+
_deepMergeFilters(a = {}, b = {}) {
|
|
330
|
+
const out = { ...a };
|
|
530
331
|
|
|
531
|
-
|
|
532
|
-
|
|
332
|
+
for (const [key, value] of Object.entries(b)) {
|
|
333
|
+
if (
|
|
334
|
+
value &&
|
|
335
|
+
typeof value === "object" &&
|
|
336
|
+
!Array.isArray(value) &&
|
|
337
|
+
out[key] &&
|
|
338
|
+
typeof out[key] === "object" &&
|
|
339
|
+
!Array.isArray(out[key]) &&
|
|
340
|
+
!LOGICAL_OPERATORS.includes(key)
|
|
341
|
+
) {
|
|
342
|
+
out[key] = this._deepMergeFilters(out[key], value);
|
|
343
|
+
} else {
|
|
344
|
+
out[key] = value;
|
|
533
345
|
}
|
|
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
346
|
}
|
|
549
347
|
|
|
550
|
-
|
|
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
|
-
});
|
|
348
|
+
return out;
|
|
563
349
|
}
|
|
564
350
|
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
}
|
|
351
|
+
_applySecurityFilters(filters = {}) {
|
|
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
|
+
}
|
|
585
360
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
}
|
|
361
|
+
if (Array.isArray(node)) {
|
|
362
|
+
return node.map(cleanNode);
|
|
363
|
+
}
|
|
611
364
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
.map((field) => field.trim())
|
|
616
|
-
.filter(Boolean)
|
|
617
|
-
.filter((field) => {
|
|
618
|
-
const cleanField = field.replace(/^-/, "");
|
|
619
|
-
return !this._isForbiddenField(cleanField);
|
|
620
|
-
});
|
|
365
|
+
if (!node || typeof node !== "object") {
|
|
366
|
+
return node;
|
|
367
|
+
}
|
|
621
368
|
|
|
622
|
-
|
|
623
|
-
const hasExclude = fields.some((field) => field.startsWith("-"));
|
|
369
|
+
const result = {};
|
|
624
370
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
);
|
|
630
|
-
}
|
|
371
|
+
for (const [key, value] of Object.entries(node)) {
|
|
372
|
+
if (this._isForbiddenField(key)) continue;
|
|
373
|
+
result[key] = cleanNode(value);
|
|
374
|
+
}
|
|
631
375
|
|
|
632
|
-
|
|
633
|
-
mode: hasExclude ? "exclude" : "include",
|
|
634
|
-
fields: fields.map((field) => field.replace(/^-/, "")),
|
|
376
|
+
return result;
|
|
635
377
|
};
|
|
636
|
-
}
|
|
637
378
|
|
|
638
|
-
|
|
639
|
-
return fields.includes("_id") ? fields : ["_id", ...fields];
|
|
379
|
+
return cleanNode(filters);
|
|
640
380
|
}
|
|
641
381
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (
|
|
645
|
-
key.startsWith("$") ||
|
|
646
|
-
["$where", "$accumulator", "$function"].includes(key)
|
|
647
|
-
) {
|
|
648
|
-
delete this.query[key];
|
|
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
|
-
});
|
|
382
|
+
_addPopulateStages(args) {
|
|
383
|
+
return ApiFeatures.prototype.__proto__?._addPopulateStages?.call(this, args);
|
|
657
384
|
}
|
|
658
385
|
|
|
659
386
|
_parseQueryFilters() {
|
|
660
387
|
const obj = { ...this.query };
|
|
661
|
-
|
|
662
388
|
RESERVED_QUERY_KEYS.forEach((key) => delete obj[key]);
|
|
663
389
|
|
|
664
390
|
const out = {};
|
|
@@ -704,109 +430,21 @@ export class ApiFeatures {
|
|
|
704
430
|
return out;
|
|
705
431
|
}
|
|
706
432
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
node instanceof mongoose.Types.ObjectId ||
|
|
711
|
-
node instanceof ObjectId
|
|
712
|
-
) {
|
|
713
|
-
return node;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
if (node === null || node === "null") return null;
|
|
717
|
-
if (node === "true") return true;
|
|
718
|
-
if (node === "false") return false;
|
|
719
|
-
|
|
720
|
-
if (Array.isArray(node)) {
|
|
721
|
-
return node.map((item) => sanitizeNode(item, key));
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (node && typeof node === "object") {
|
|
725
|
-
const result = {};
|
|
726
|
-
|
|
727
|
-
for (const [childKey, childVal] of Object.entries(node)) {
|
|
728
|
-
result[childKey] = sanitizeNode(childVal, childKey);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
return result;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (typeof node === "string") {
|
|
735
|
-
if (this.#isStrictObjectId(node) && this._shouldConvertToObjectId(key)) {
|
|
736
|
-
return new ObjectId(node);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
if (/^[0-9]+$/.test(node)) {
|
|
740
|
-
return node.length > 1 && node.startsWith("0")
|
|
741
|
-
? node
|
|
742
|
-
: parseInt(node, 10);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
return node;
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
return sanitizeNode(filters);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
_shouldConvertToObjectId(key = "") {
|
|
753
|
-
const cleanKey = String(key).replace(/^\$/, "").toLowerCase();
|
|
754
|
-
|
|
755
|
-
return (
|
|
756
|
-
cleanKey === "_id" ||
|
|
757
|
-
cleanKey === "id" ||
|
|
758
|
-
cleanKey.endsWith("id") ||
|
|
759
|
-
cleanKey === "eq" ||
|
|
760
|
-
cleanKey === "ne" ||
|
|
761
|
-
cleanKey === "in" ||
|
|
762
|
-
cleanKey === "nin"
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
_deepMergeFilters(a = {}, b = {}) {
|
|
767
|
-
const out = { ...a };
|
|
768
|
-
|
|
769
|
-
for (const [key, value] of Object.entries(b)) {
|
|
433
|
+
_sanitization() {
|
|
434
|
+
for (const key of Object.keys(this.query)) {
|
|
770
435
|
if (
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
!Array.isArray(value) &&
|
|
774
|
-
out[key] &&
|
|
775
|
-
typeof out[key] === "object" &&
|
|
776
|
-
!Array.isArray(out[key]) &&
|
|
777
|
-
!LOGICAL_OPERATORS.includes(key)
|
|
436
|
+
key.startsWith("$") ||
|
|
437
|
+
["$where", "$accumulator", "$function"].includes(key)
|
|
778
438
|
) {
|
|
779
|
-
|
|
780
|
-
} else {
|
|
781
|
-
out[key] = value;
|
|
439
|
+
delete this.query[key];
|
|
782
440
|
}
|
|
783
441
|
}
|
|
784
442
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
_applySecurityFilters(filters = {}) {
|
|
789
|
-
const cleanNode = (node) => {
|
|
790
|
-
if (Array.isArray(node)) {
|
|
791
|
-
return node.map(cleanNode);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
if (!node || typeof node !== "object") {
|
|
795
|
-
return node;
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const result = {};
|
|
799
|
-
|
|
800
|
-
for (const [key, value] of Object.entries(node)) {
|
|
801
|
-
if (this._isForbiddenField(key)) continue;
|
|
802
|
-
|
|
803
|
-
result[key] = cleanNode(value);
|
|
443
|
+
["page", "limit"].forEach((field) => {
|
|
444
|
+
if (this.query[field] && !/^[0-9]+$/.test(String(this.query[field]))) {
|
|
445
|
+
throw new HandleERROR(`Invalid ${field}`, 400);
|
|
804
446
|
}
|
|
805
|
-
|
|
806
|
-
return result;
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
return cleanNode(filters);
|
|
447
|
+
});
|
|
810
448
|
}
|
|
811
449
|
|
|
812
450
|
_normalizePopulateInput(input = "") {
|
|
@@ -828,7 +466,6 @@ _sanitizeFilters(filters = {}) {
|
|
|
828
466
|
|
|
829
467
|
if (typeof item === "string") {
|
|
830
468
|
const trimmed = item.trim();
|
|
831
|
-
|
|
832
469
|
if (!trimmed) return;
|
|
833
470
|
|
|
834
471
|
if (trimmed.includes(".")) {
|
|
@@ -884,9 +521,11 @@ _sanitizeFilters(filters = {}) {
|
|
|
884
521
|
return input
|
|
885
522
|
.flatMap((item) => {
|
|
886
523
|
if (typeof item === "string") return this._normalizePopulateInput(item);
|
|
524
|
+
|
|
887
525
|
if (item && typeof item === "object" && item.path) {
|
|
888
526
|
return [this._normalizePopulateObject(item)];
|
|
889
527
|
}
|
|
528
|
+
|
|
890
529
|
return [];
|
|
891
530
|
})
|
|
892
531
|
.filter(Boolean);
|