vanta-api 1.4.3 → 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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/api-features.js +81 -47
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanta-api",
3
- "version": "1.4.3",
3
+ "version": "1.4.5",
4
4
  "description": "Advanced API features and security configuration for Node.js/MongoDB.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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,16 @@ export class ApiFeatures {
33
32
 
34
33
  filter() {
35
34
  const queryFilters = this._parseQueryFilters();
35
+
36
+ const normalizedManualFilters = this._normalizeLogicalOperators(
37
+ this.manualFilters
38
+ );
39
+
36
40
  const mergedFilters = this._deepMergeFilters(
37
41
  queryFilters,
38
- this.manualFilters
42
+ normalizedManualFilters
39
43
  );
44
+
40
45
  const sanitizedFilters = this._sanitizeFilters(mergedFilters);
41
46
  const safeFilters = this._applySecurityFilters(sanitizedFilters);
42
47
 
@@ -49,7 +54,12 @@ export class ApiFeatures {
49
54
 
50
55
  addManualFilters(filters = {}) {
51
56
  if (filters && typeof filters === "object" && !Array.isArray(filters)) {
52
- this.manualFilters = this._deepMergeFilters(this.manualFilters, filters);
57
+ const normalizedFilters = this._normalizeLogicalOperators(filters);
58
+
59
+ this.manualFilters = this._deepMergeFilters(
60
+ this.manualFilters,
61
+ normalizedFilters
62
+ );
53
63
  }
54
64
 
55
65
  return this;
@@ -57,11 +67,9 @@ export class ApiFeatures {
57
67
 
58
68
  search(fields = []) {
59
69
  const q = this.query.q;
60
-
61
70
  if (!q || !Array.isArray(fields) || !fields.length) return this;
62
71
 
63
72
  const safeQ = this._escapeRegex(String(q).trim());
64
-
65
73
  if (!safeQ) return this;
66
74
 
67
75
  const conditions = fields
@@ -109,7 +117,6 @@ export class ApiFeatures {
109
117
 
110
118
  limitFields(input = "") {
111
119
  const rawFields = [input, this.query.fields].filter(Boolean).join(",");
112
-
113
120
  if (!rawFields) return this;
114
121
 
115
122
  const fields = rawFields
@@ -128,9 +135,7 @@ export class ApiFeatures {
128
135
 
129
136
  for (const field of fields) {
130
137
  const cleanField = field.replace(/^-/, "");
131
-
132
138
  if (this._isForbiddenField(cleanField)) continue;
133
-
134
139
  project[cleanField] = field.startsWith("-") ? 0 : 1;
135
140
  }
136
141
 
@@ -513,13 +518,13 @@ export class ApiFeatures {
513
518
 
514
519
  _applyPopulateSelect({ path, select, isArray }) {
515
520
  const parsed = this._parseSelect(select);
516
-
517
521
  if (!parsed.fields.length) return;
518
522
 
519
523
  if (parsed.mode === "exclude") {
520
524
  this.pipeline.push({
521
525
  $unset: parsed.fields.map((field) => `${path}.${field}`),
522
526
  });
527
+
523
528
  return;
524
529
  }
525
530
 
@@ -564,7 +569,6 @@ export class ApiFeatures {
564
569
 
565
570
  _applyNestedObjectSelectInsideArray({ arrayPath, objectPath, select }) {
566
571
  const parsed = this._parseSelect(select);
567
-
568
572
  if (!parsed.fields.length) return;
569
573
 
570
574
  if (parsed.mode === "exclude") {
@@ -573,6 +577,7 @@ export class ApiFeatures {
573
577
  (field) => `${arrayPath}.${objectPath}.${field}`
574
578
  ),
575
579
  });
580
+
576
581
  return;
577
582
  }
578
583
 
@@ -658,7 +663,6 @@ export class ApiFeatures {
658
663
 
659
664
  _parseQueryFilters() {
660
665
  const obj = { ...this.query };
661
-
662
666
  RESERVED_QUERY_KEYS.forEach((key) => delete obj[key]);
663
667
 
664
668
  const out = {};
@@ -704,50 +708,47 @@ export class ApiFeatures {
704
708
  return out;
705
709
  }
706
710
 
707
- _sanitizeFilters(filters = {}) {
708
- const sanitizeNode = (node, key = "") => {
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
- }
711
+ _sanitizeFilters(filters = {}) {
712
+ const sanitizeNode = (node, key = "") => {
713
+ if (node instanceof mongoose.Types.ObjectId || node instanceof ObjectId) {
714
+ return node;
715
+ }
723
716
 
724
- if (node && typeof node === "object") {
725
- const result = {};
717
+ if (node === null || node === "null") return null;
718
+ if (node === "true") return true;
719
+ if (node === "false") return false;
726
720
 
727
- for (const [childKey, childVal] of Object.entries(node)) {
728
- result[childKey] = sanitizeNode(childVal, childKey);
721
+ if (Array.isArray(node)) {
722
+ return node.map((item) => sanitizeNode(item, key));
729
723
  }
730
724
 
731
- return result;
732
- }
725
+ if (node && typeof node === "object") {
726
+ const result = {};
727
+
728
+ for (const [childKey, childVal] of Object.entries(node)) {
729
+ result[childKey] = sanitizeNode(childVal, childKey);
730
+ }
733
731
 
734
- if (typeof node === "string") {
735
- if (this.#isStrictObjectId(node) && this._shouldConvertToObjectId(key)) {
736
- return new ObjectId(node);
732
+ return result;
737
733
  }
738
734
 
739
- if (/^[0-9]+$/.test(node)) {
740
- return node.length > 1 && node.startsWith("0")
741
- ? node
742
- : parseInt(node, 10);
735
+ if (typeof node === "string") {
736
+ if (this.#isStrictObjectId(node) && this._shouldConvertToObjectId(key)) {
737
+ return new ObjectId(node);
738
+ }
739
+
740
+ if (/^[0-9]+$/.test(node)) {
741
+ return node.length > 1 && node.startsWith("0")
742
+ ? node
743
+ : parseInt(node, 10);
744
+ }
743
745
  }
744
- }
745
746
 
746
- return node;
747
- };
747
+ return node;
748
+ };
748
749
 
749
- return sanitizeNode(filters);
750
- }
750
+ return sanitizeNode(filters);
751
+ }
751
752
 
752
753
  _shouldConvertToObjectId(key = "") {
753
754
  const cleanKey = String(key).replace(/^\$/, "").toLowerCase();
@@ -763,6 +764,39 @@ _sanitizeFilters(filters = {}) {
763
764
  );
764
765
  }
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
778
+ ) {
779
+ return filters;
780
+ }
781
+
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;
798
+ }
799
+
766
800
  _deepMergeFilters(a = {}, b = {}) {
767
801
  const out = { ...a };
768
802
 
@@ -799,7 +833,6 @@ _sanitizeFilters(filters = {}) {
799
833
 
800
834
  for (const [key, value] of Object.entries(node)) {
801
835
  if (this._isForbiddenField(key)) continue;
802
-
803
836
  result[key] = cleanNode(value);
804
837
  }
805
838
 
@@ -828,7 +861,6 @@ _sanitizeFilters(filters = {}) {
828
861
 
829
862
  if (typeof item === "string") {
830
863
  const trimmed = item.trim();
831
-
832
864
  if (!trimmed) return;
833
865
 
834
866
  if (trimmed.includes(".")) {
@@ -884,9 +916,11 @@ _sanitizeFilters(filters = {}) {
884
916
  return input
885
917
  .flatMap((item) => {
886
918
  if (typeof item === "string") return this._normalizePopulateInput(item);
919
+
887
920
  if (item && typeof item === "object" && item.path) {
888
921
  return [this._normalizePopulateObject(item)];
889
922
  }
923
+
890
924
  return [];
891
925
  })
892
926
  .filter(Boolean);