vanta-api 1.2.6 → 1.2.8

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 +97 -68
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanta-api",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Advanced API features and security configuration for Node.js/MongoDB.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -62,21 +62,46 @@ export class ApiFeatures {
62
62
  return this;
63
63
  }
64
64
 
65
- limitFields() {
66
- if (!this.query.fields) return this;
67
- const validFields = Object.keys(this.model.schema.paths).filter(
68
- (f) => !securityConfig.forbiddenFields.includes(f)
69
- );
70
- const project = {};
65
+ limitFields(input = "") {
66
+ const rawFields = [input, this.query.fields].filter(Boolean).join(",");
67
+ if (!rawFields) return this;
68
+
69
+ const validFields = Object.keys(this.model.schema.paths).filter(
70
+ (f) => !securityConfig.forbiddenFields.includes(f)
71
+ );
72
+
73
+ const fieldsArray = rawFields
74
+ .split(",")
75
+ .map((f) => f.trim())
76
+ .filter(Boolean);
77
+
78
+ const includeFields = new Set();
79
+ const excludeFields = new Set();
80
+
81
+ fieldsArray.forEach((f) => {
82
+ if (f.startsWith("-")) excludeFields.add(f.slice(1));
83
+ else includeFields.add(f);
84
+ });
85
+
86
+ const project = {};
71
87
 
72
- this.query.fields.split(",").forEach((f) => {
88
+ if (includeFields.size > 0) {
89
+ includeFields.forEach((f) => {
73
90
  if (validFields.includes(f)) project[f] = 1;
74
91
  });
92
+ } else if (excludeFields.size > 0) {
93
+ validFields.forEach((f) => {
94
+ if (!excludeFields.has(f)) project[f] = 1;
95
+ });
96
+ }
75
97
 
76
- if (Object.keys(project).length) this.pipeline.push({ $project: project });
77
- return this;
98
+ if (Object.keys(project).length) {
99
+ this.pipeline.push({ $project: project });
78
100
  }
79
101
 
102
+ return this;
103
+ }
104
+
80
105
  paginate() {
81
106
  const { maxLimit } = securityConfig.accessLevels[this.userRole] || {
82
107
  maxLimit: 100,
@@ -91,70 +116,74 @@ export class ApiFeatures {
91
116
  return this;
92
117
  }
93
118
 
94
- populate(input = "") {
95
- let list = [];
96
- const raw = Array.isArray(input) ? input : [input];
97
- if (this.query.populate) raw.push(...this.query.populate.split(","));
119
+ populate(input = "") {
120
+ let list = [];
121
+ const raw = Array.isArray(input) ? input : [input];
122
+ if (this.query.populate) raw.push(...this.query.populate.split(","));
98
123
 
99
- raw.forEach((item) => {
100
- if (typeof item === "string" && item.trim()) list.push(item.trim());
101
- else if (item?.path) list.push(item);
102
- });
124
+ raw.forEach((item) => {
125
+ if (typeof item === "string" && item.trim()) list.push(item.trim());
126
+ else if (item?.path) list.push(item);
127
+ });
103
128
 
104
- const map = new Map();
105
- list.forEach((opt) => {
106
- const key = typeof opt === "string" ? opt : opt.path;
107
- map.set(key, opt);
108
- });
129
+ const map = new Map();
130
+ list.forEach((opt) => {
131
+ const key = typeof opt === "string" ? opt : opt.path;
132
+ map.set(key, opt);
133
+ });
109
134
 
110
- const allowed =
111
- securityConfig.accessLevels[this.userRole]?.allowedPopulate || [];
112
- const final = [];
113
- map.forEach((opt, key) => {
114
- if (allowed.includes("*") || allowed.includes(key)) final.push(opt);
115
- });
135
+ const allowed =
136
+ securityConfig.accessLevels[this.userRole]?.allowedPopulate || [];
137
+ const final = [];
138
+ map.forEach((opt, key) => {
139
+ if (allowed.includes("*") || allowed.includes(key)) final.push(opt);
140
+ });
116
141
 
117
- for (const opt of final) {
118
- const field = typeof opt === "string" ? opt : opt.path;
119
- const proj =
120
- typeof opt === "object" && opt.select
121
- ? opt.select.split(" ").reduce((a, f) => {
122
- a[f] = 1;
123
- return a;
124
- }, {})
125
- : {};
126
-
127
- const { collection, isArray } = this._getCollectionInfo(field);
128
-
129
- const lookup =
130
- proj && Object.keys(proj).length
131
- ? {
132
- from: collection,
133
- let: { id: `$${field}` },
134
- pipeline: [
135
- { $match: { $expr: { $eq: ["$_id", "$$id"] } } },
136
- { $project: proj },
137
- ],
138
- as: field,
139
- }
140
- : {
141
- from: collection,
142
- localField: field,
143
- foreignField: "_id",
144
- as: field,
145
- };
146
-
147
- this.pipeline.push({ $lookup: lookup });
148
-
149
- if (!isArray) {
150
- this.pipeline.push({
151
- $unwind: { path: `$${field}`, preserveNullAndEmptyArrays: true },
152
- });
142
+ for (const opt of final) {
143
+ const field = typeof opt === "string" ? opt : opt.path;
144
+ const proj =
145
+ typeof opt === "object" && opt.select
146
+ ? opt.select.split(" ").reduce(
147
+ (a, f) => {
148
+ a[f] = 1;
149
+ return a;
150
+ },
151
+ { _id: 1 }
152
+ )
153
+ : {};
154
+
155
+ const { collection, isArray } = this._getCollectionInfo(field);
156
+
157
+ const matchStage = isArray
158
+ ? { $match: { $expr: { $in: ["$_id", "$$id"] } } }
159
+ : { $match: { $expr: { $eq: ["$_id", "$$id"] } } };
160
+
161
+ const lookup =
162
+ proj && Object.keys(proj).length
163
+ ? {
164
+ from: collection,
165
+ let: { id: `$${field}` },
166
+ pipeline: [matchStage, { $project: proj }],
167
+ as: field,
168
+ }
169
+ : {
170
+ from: collection,
171
+ localField: field,
172
+ foreignField: "_id",
173
+ as: field,
174
+ };
175
+
176
+ this.pipeline.push({ $lookup: lookup });
177
+
178
+ if (!isArray) {
179
+ this.pipeline.push({
180
+ $unwind: { path: `$${field}`, preserveNullAndEmptyArrays: true },
181
+ });
182
+ }
153
183
  }
154
- }
155
184
 
156
- return this;
157
- }
185
+ return this;
186
+ }
158
187
 
159
188
  addManualFilters(filters) {
160
189
  if (filters) this.manualFilters = { ...this.manualFilters, ...filters };
@@ -285,7 +314,7 @@ populate(input = "") {
285
314
  resultObj[keyObj] = false;
286
315
  return;
287
316
  }
288
-
317
+
289
318
  if (typeof val === "string" && /^[0-9]+$/.test(val)) {
290
319
  resultObj[keyObj] = parseInt(val, 10);
291
320
  return;