vanta-api 1.4.1 → 1.4.2
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 +476 -92
package/package.json
CHANGED
package/src/api-features.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import mongoose from "mongoose";
|
|
2
2
|
import winston from "winston";
|
|
3
|
+
import pluralize from "pluralize";
|
|
3
4
|
import HandleERROR from "./handleError.js";
|
|
4
5
|
import { securityConfig } from "./config.js";
|
|
5
6
|
import { ObjectId } from "bson";
|
|
@@ -22,7 +23,6 @@ export class ApiFeatures {
|
|
|
22
23
|
this.query = { ...query };
|
|
23
24
|
this.pipeline = [];
|
|
24
25
|
this.manualFilters = {};
|
|
25
|
-
this.populateOptions = [];
|
|
26
26
|
this.useCursor = false;
|
|
27
27
|
|
|
28
28
|
this.userRole =
|
|
@@ -161,11 +161,15 @@ export class ApiFeatures {
|
|
|
161
161
|
const allowedPopulate =
|
|
162
162
|
securityConfig.accessLevels?.[this.userRole]?.allowedPopulate || [];
|
|
163
163
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
164
|
+
for (const populateItem of populateList) {
|
|
165
|
+
this._addPopulateStages({
|
|
166
|
+
populateItem,
|
|
167
|
+
parentPath: "",
|
|
168
|
+
parentIsArray: false,
|
|
169
|
+
schema: this.model.schema,
|
|
170
|
+
allowedPopulate,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
169
173
|
|
|
170
174
|
return this;
|
|
171
175
|
}
|
|
@@ -176,10 +180,9 @@ export class ApiFeatures {
|
|
|
176
180
|
|
|
177
181
|
if (options.debug) {
|
|
178
182
|
logger.info("Pipeline:", this.pipeline);
|
|
179
|
-
logger.info("Populate:", this.populateOptions);
|
|
180
183
|
}
|
|
181
184
|
|
|
182
|
-
if (this.pipeline.length > (securityConfig.maxPipelineStages ||
|
|
185
|
+
if (this.pipeline.length > (securityConfig.maxPipelineStages || 80)) {
|
|
183
186
|
throw new HandleERROR("Too many pipeline stages", 400);
|
|
184
187
|
}
|
|
185
188
|
|
|
@@ -212,10 +215,6 @@ export class ApiFeatures {
|
|
|
212
215
|
.readConcern(options.readConcern || "majority");
|
|
213
216
|
}
|
|
214
217
|
|
|
215
|
-
if (this.populateOptions.length) {
|
|
216
|
-
data = await this.model.populate(data, this.populateOptions);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
218
|
return {
|
|
220
219
|
success: true,
|
|
221
220
|
count: countResult?.total || 0,
|
|
@@ -226,6 +225,420 @@ export class ApiFeatures {
|
|
|
226
225
|
}
|
|
227
226
|
}
|
|
228
227
|
|
|
228
|
+
_addPopulateStages({
|
|
229
|
+
populateItem,
|
|
230
|
+
parentPath = "",
|
|
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;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const info = this._getPopulateInfo({
|
|
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 });
|
|
444
|
+
|
|
445
|
+
if (populateItem.populate) {
|
|
446
|
+
const nestedList = this._normalizeNestedPopulate(populateItem.populate);
|
|
447
|
+
|
|
448
|
+
for (const nested of nestedList) {
|
|
449
|
+
this._addNestedPopulateInsideArrayItem({
|
|
450
|
+
arrayPath,
|
|
451
|
+
objectPath: `${objectPath}.${childPath}`,
|
|
452
|
+
populateItem: nested,
|
|
453
|
+
allowedPopulate,
|
|
454
|
+
schema: info.refSchema,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (populateItem.select) {
|
|
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
|
+
});
|
|
621
|
+
|
|
622
|
+
const hasInclude = fields.some((field) => !field.startsWith("-"));
|
|
623
|
+
const hasExclude = fields.some((field) => field.startsWith("-"));
|
|
624
|
+
|
|
625
|
+
if (hasInclude && hasExclude) {
|
|
626
|
+
throw new HandleERROR(
|
|
627
|
+
"Cannot mix include and exclude in populate select",
|
|
628
|
+
400
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
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];
|
|
640
|
+
}
|
|
641
|
+
|
|
229
642
|
_sanitization() {
|
|
230
643
|
for (const key of Object.keys(this.query)) {
|
|
231
644
|
if (
|
|
@@ -443,7 +856,7 @@ export class ApiFeatures {
|
|
|
443
856
|
}
|
|
444
857
|
|
|
445
858
|
if (typeof normalized.select === "string") {
|
|
446
|
-
normalized.select =
|
|
859
|
+
normalized.select = normalized.select.trim();
|
|
447
860
|
}
|
|
448
861
|
|
|
449
862
|
if (normalized.populate) {
|
|
@@ -454,7 +867,7 @@ export class ApiFeatures {
|
|
|
454
867
|
}
|
|
455
868
|
|
|
456
869
|
_normalizeNestedPopulate(input) {
|
|
457
|
-
if (!input) return
|
|
870
|
+
if (!input) return [];
|
|
458
871
|
|
|
459
872
|
if (typeof input === "string") {
|
|
460
873
|
return this._normalizePopulateInput(input);
|
|
@@ -473,14 +886,17 @@ export class ApiFeatures {
|
|
|
473
886
|
}
|
|
474
887
|
|
|
475
888
|
if (typeof input === "object" && input.path) {
|
|
476
|
-
return this._normalizePopulateObject(input);
|
|
889
|
+
return [this._normalizePopulateObject(input)];
|
|
477
890
|
}
|
|
478
891
|
|
|
479
|
-
return
|
|
892
|
+
return [];
|
|
480
893
|
}
|
|
481
894
|
|
|
482
895
|
_dotPathToPopulate(path) {
|
|
483
|
-
const parts = path
|
|
896
|
+
const parts = path
|
|
897
|
+
.split(".")
|
|
898
|
+
.map((part) => part.trim())
|
|
899
|
+
.filter(Boolean);
|
|
484
900
|
|
|
485
901
|
const root = { path: parts[0] };
|
|
486
902
|
let current = root;
|
|
@@ -515,96 +931,60 @@ export class ApiFeatures {
|
|
|
515
931
|
const merged = { ...a, ...b };
|
|
516
932
|
|
|
517
933
|
if (a.populate || b.populate) {
|
|
518
|
-
const aList =
|
|
519
|
-
|
|
934
|
+
const aList = Array.isArray(a.populate)
|
|
935
|
+
? a.populate
|
|
936
|
+
: a.populate
|
|
937
|
+
? [a.populate]
|
|
938
|
+
: [];
|
|
939
|
+
|
|
940
|
+
const bList = Array.isArray(b.populate)
|
|
941
|
+
? b.populate
|
|
942
|
+
: b.populate
|
|
943
|
+
? [b.populate]
|
|
944
|
+
: [];
|
|
945
|
+
|
|
520
946
|
merged.populate = this._dedupePopulate([...aList, ...bList]);
|
|
521
947
|
}
|
|
522
948
|
|
|
523
949
|
return merged;
|
|
524
950
|
}
|
|
525
951
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
952
|
+
_isPopulateAllowed(path, allowedPopulate = []) {
|
|
953
|
+
return (
|
|
954
|
+
allowedPopulate.includes("*") ||
|
|
955
|
+
allowedPopulate.includes(path) ||
|
|
956
|
+
allowedPopulate.includes(path.split(".")[0])
|
|
957
|
+
);
|
|
529
958
|
}
|
|
530
959
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const fullPath = parentPath ? `${parentPath}.${item.path}` : item.path;
|
|
535
|
-
|
|
536
|
-
if (!this._isPopulateAllowed(fullPath, allowedPopulate)) {
|
|
537
|
-
return null;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const sanitized = {
|
|
541
|
-
path: item.path,
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
if (item.select) {
|
|
545
|
-
sanitized.select = this._sanitizeSelectString(item.select);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (item.match && typeof item.match === "object") {
|
|
549
|
-
sanitized.match = this._applySecurityFilters(
|
|
550
|
-
this._sanitizeFilters(item.match)
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
if (item.options && typeof item.options === "object") {
|
|
555
|
-
sanitized.options = item.options;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
if (item.model) {
|
|
559
|
-
sanitized.model = item.model;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (item.populate) {
|
|
563
|
-
const nested = this._populateToArray(item.populate)
|
|
564
|
-
.map((child) =>
|
|
565
|
-
this._sanitizePopulateOption(child, fullPath, allowedPopulate)
|
|
566
|
-
)
|
|
567
|
-
.filter(Boolean);
|
|
960
|
+
_resolveCollectionName(refModelName = "") {
|
|
961
|
+
return pluralize(String(refModelName).toLowerCase());
|
|
962
|
+
}
|
|
568
963
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
}
|
|
964
|
+
_resolveRegisteredSchema(refModelName = "") {
|
|
965
|
+
const connection = this.model?.db || mongoose.connection;
|
|
966
|
+
const registeredModel =
|
|
967
|
+
connection.models?.[refModelName] || mongoose.models?.[refModelName];
|
|
575
968
|
|
|
576
|
-
return
|
|
969
|
+
return registeredModel?.schema || null;
|
|
577
970
|
}
|
|
578
971
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
.split(/\s+/)
|
|
582
|
-
.map((field) => field.trim())
|
|
583
|
-
.filter(Boolean)
|
|
584
|
-
.filter((field) => {
|
|
585
|
-
const cleanField = field.replace(/^-/, "");
|
|
586
|
-
return !this._isForbiddenField(cleanField);
|
|
587
|
-
});
|
|
972
|
+
_inferModelNameFromPath(path = "") {
|
|
973
|
+
let clean = String(path).split(".").pop();
|
|
588
974
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (hasInclude && hasExclude) {
|
|
593
|
-
throw new HandleERROR(
|
|
594
|
-
"Cannot mix include and exclude in populate select",
|
|
595
|
-
400
|
|
596
|
-
);
|
|
597
|
-
}
|
|
975
|
+
clean = clean.replace(/Ids$/i, "");
|
|
976
|
+
clean = clean.replace(/Id$/i, "");
|
|
977
|
+
clean = pluralize.singular(clean);
|
|
598
978
|
|
|
599
|
-
return
|
|
979
|
+
return clean
|
|
980
|
+
.split(/[_-\s]+/)
|
|
981
|
+
.filter(Boolean)
|
|
982
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
983
|
+
.join("");
|
|
600
984
|
}
|
|
601
985
|
|
|
602
|
-
|
|
603
|
-
return (
|
|
604
|
-
allowedPopulate.includes("*") ||
|
|
605
|
-
allowedPopulate.includes(path) ||
|
|
606
|
-
allowedPopulate.includes(path.split(".")[0])
|
|
607
|
-
);
|
|
986
|
+
_makeTempLookupName(path = "") {
|
|
987
|
+
return `__vanta_lookup_${String(path).replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
608
988
|
}
|
|
609
989
|
|
|
610
990
|
_isForbiddenField(field) {
|
|
@@ -617,7 +997,11 @@ export class ApiFeatures {
|
|
|
617
997
|
"$skip" in stage ||
|
|
618
998
|
"$limit" in stage ||
|
|
619
999
|
"$sort" in stage ||
|
|
620
|
-
"$project" in stage
|
|
1000
|
+
"$project" in stage ||
|
|
1001
|
+
"$lookup" in stage ||
|
|
1002
|
+
"$unwind" in stage ||
|
|
1003
|
+
"$set" in stage ||
|
|
1004
|
+
"$unset" in stage
|
|
621
1005
|
);
|
|
622
1006
|
});
|
|
623
1007
|
}
|