vanta-api 1.0.0

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/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 AlirezaAghaee1996
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,370 @@
1
+
2
+
3
+ # VantaApi: Advanced API Features & Security Config for MongoDB
4
+
5
+ This repository provides a robust, feature-rich, and secure solution for building, customizing, and optimizing your Node.js APIs powered by MongoDB. **VantaApi** processes incoming query parameters and builds an aggregation pipeline step by step, offering powerful features such as advanced filtering, sorting, field selection, pagination, and document population with comprehensive security controls.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Installation & Setup](#installation--setup)
12
+ 2. [Overview](#overview)
13
+ 3. [VantaApi Class Methods](#VantaApi-class-methods)
14
+ - [filter()](#filter)
15
+ - [sort()](#sort)
16
+ - [limitFields()](#limitfields)
17
+ - [paginate()](#paginate)
18
+ - [populate()](#populate)
19
+ - [addManualFilters()](#addmanualfilters)
20
+ - [execute()](#execute)
21
+ 4. [Input Types & Supported Operators](#input-types--supported-operators)
22
+ 5. [Additional Conditions](#additional-conditions)
23
+ 6. [Security Configuration](#security-configuration)
24
+ 7. [Security & Performance Enhancements](#security--performance-enhancements)
25
+ 8. [Error Handling Middleware](#error-handling-middleware)
26
+ 9. [Full Examples](#full-examples)
27
+ 10. [Summary](#summary)
28
+
29
+ ---
30
+
31
+ ## Installation & Setup
32
+
33
+ ### Prerequisites
34
+
35
+ - Node.js 16+
36
+ - MongoDB 5+
37
+ - Mongoose 7+
38
+
39
+ ### Install Dependencies
40
+
41
+ Install core dependencies:
42
+
43
+ ```bash
44
+ npm install mongoose lodash dotenv winston
45
+ ```
46
+
47
+ For testing purposes, install Jest:
48
+
49
+ ```bash
50
+ npm install --save-dev jest
51
+ ```
52
+
53
+ ---
54
+
55
+ ## Overview
56
+
57
+ The **VantaApi** class processes incoming query parameters and progressively builds an aggregation pipeline. The package supports:
58
+
59
+ - **Advanced filtering, sorting, and field selection.**
60
+ - **Pagination with defaults** (defaults to page 1 and limit 10 if not provided), with the maximum limit based on the user's role.
61
+ - **Document population:** Supports joining related documents, including nested population.
62
+ - **Automatic Conditions:** If the model includes an `isActive` field and the user is not an admin, `isActive: true` is added automatically.
63
+ - **Input Sanitization & Validation:** Sanitizes inputs, validates numeric fields, and enforces security through a dedicated configuration.
64
+ - **Enhanced Logging & Error Handling:** Uses winston for logging and employs a custom error class for centralized error management.
65
+ - **Performance Improvements:** Features aggregation cursor support and optimized pipeline ordering.
66
+ - **Error Middleware:** Provides a centralized error handling middleware for consistent API error responses.
67
+
68
+ ---
69
+
70
+ ## VantaApi Class Methods
71
+
72
+ ### filter()
73
+ - **Description:**
74
+ Parses query parameters, merges them with manually added filters (if provided), and applies security filters. If the model includes an `isActive` field and the user is not "admin," it automatically adds `isActive: true`.
75
+ - **Usage Example:**
76
+
77
+ ```javascript
78
+ // URL: /api/products?status=active&price[gte]=100
79
+ const features = new VantaApi(Product, req.query);
80
+ features.filter();
81
+ // Pipeline adds: { $match: { status: "active", price: { $gte: 100 }, isActive: true } }
82
+ ```
83
+
84
+ ### sort()
85
+ - **Description:**
86
+ Converts a comma-separated list of sorting fields into a `$sort` object. A "-" prefix indicates descending order.
87
+ - **Usage Example:**
88
+
89
+ ```javascript
90
+ // URL: /api/products?sort=-price,createdAt
91
+ const features = new VantaApi(Product, req.query);
92
+ features.sort();
93
+ // Pipeline adds: { $sort: { price: -1, createdAt: 1 } }
94
+ ```
95
+
96
+ ### limitFields()
97
+ - **Description:**
98
+ Uses `$project` to return only the specified fields while excluding forbidden fields (such as "password").
99
+ - **Usage Example:**
100
+
101
+ ```javascript
102
+ // URL: /api/products?fields=name,price,category,password
103
+ const features = new VantaApi(Product, req.query);
104
+ features.limitFields();
105
+ // Pipeline adds: { $project: { name: 1, price: 1, category: 1 } }
106
+ ```
107
+
108
+ ### paginate()
109
+ - **Description:**
110
+ Determines the page and limit values, applying pagination with defaults (page 1 and limit 10 if not provided). The maximum limit is based on the user's role as defined in the security configuration.
111
+ - **Usage Example:**
112
+
113
+ ```javascript
114
+ // URL: /api/products?page=2&limit=20
115
+ const features = new VantaApi(Product, req.query, "user");
116
+ features.paginate();
117
+ // Pipeline adds: { $skip: 20 } and { $limit: 20 }
118
+ ```
119
+
120
+ ### populate()
121
+ - **Description:**
122
+ Joins related documents using `$lookup` and `$unwind`. It supports:
123
+ - **String Input:** A comma-separated list of field names.
124
+ - **Object Input:** An object with properties `path` (required) and `select` (optional).
125
+ - **Array Input:** An array of strings or objects for multiple or nested population.
126
+ - **Usage Examples:**
127
+ - **String Input:**
128
+
129
+ ```javascript
130
+ // URL: /api/products?populate=category,brand
131
+ const features = new VantaApi(Product, req.query);
132
+ features.populate();
133
+ ```
134
+
135
+ - **Object Input:**
136
+
137
+ ```javascript
138
+ const populateOptions = { path: "category", select: "name description" };
139
+ const features = new VantaApi(Product, req.query);
140
+ features.populate(populateOptions);
141
+ ```
142
+
143
+ - **Array Input:**
144
+
145
+ ```javascript
146
+ const populateArray = [
147
+ "brand",
148
+ { path: "category", select: "name description" },
149
+ { path: "category", select: "name", populate: { path: "subCategory", select: "title" } }
150
+ ];
151
+ const features = new VantaApi(Product, req.query, "admin");
152
+ features.populate(populateArray);
153
+ ```
154
+
155
+ ### addManualFilters()
156
+ - **Description:**
157
+ Merges additional manual filters with the parsed query filters. **Note:** Call `addManualFilters()` before `filter()` to incorporate the manual filters properly.
158
+ - **Usage Example:**
159
+
160
+ ```javascript
161
+ const manualFilter = { category: "electronics" };
162
+ const features = new VantaApi(Product, { status: "active" });
163
+ features.addManualFilters(manualFilter).filter();
164
+ ```
165
+
166
+ ### execute()
167
+ - **Description:**
168
+ Executes the built aggregation pipeline using Mongoose and returns an object containing a success flag, total document count, and result data. Supports aggregation cursor for handling large datasets.
169
+ - **Usage Example:**
170
+
171
+ ```javascript
172
+ const features = new VantaApi(Product, req.query);
173
+ const result = await features
174
+ .filter()
175
+ .sort()
176
+ .limitFields()
177
+ .paginate()
178
+ .populate()
179
+ .execute();
180
+ console.log(result);
181
+ ```
182
+
183
+ ---
184
+
185
+ ## Input Types & Supported Operators
186
+
187
+ ### Filtering Operators
188
+
189
+ The class translates query parameters into MongoDB operators:
190
+
191
+ | Operator | Query Example | Description |
192
+ |----------|---------------------------|----------------------------|
193
+ | eq | `?age=25` | Equal to |
194
+ | ne | `?status[ne]=inactive` | Not equal to |
195
+ | gt | `?price[gt]=100` | Greater than |
196
+ | gte | `?stock[gte]=50` | Greater than or equal to |
197
+ | lt | `?weight[lt]=500` | Less than |
198
+ | lte | `?rating[lte]=3` | Less than or equal to |
199
+ | in | `?colors[in]=red,blue` | In the list |
200
+ | nin | `?size[nin]=xl` | Not in the list |
201
+ | regex | `?name[regex]=^A` | Regex search |
202
+ | exists | `?discount[exists]=true` | Field existence |
203
+
204
+ Additional aspects like sorting, projection, and pagination are handled via `$sort`, `$project`, `$skip`, and `$limit`.
205
+
206
+ ---
207
+
208
+ ## Additional Conditions
209
+
210
+ - **isActive Condition:**
211
+ If the model includes an `isActive` field and the user is not an admin, `filter()` automatically adds `isActive: true`.
212
+ - **Default Pagination:**
213
+ Defaults are applied (page 1 and limit 10) when no pagination parameters are provided.
214
+ - **Numeric Validation:**
215
+ Validates that fields such as `page` and `limit` contain only numeric values.
216
+ - **Removal of Dangerous Operators:**
217
+ Operators such as `$where`, `$accumulator`, and `$function` are removed from the query.
218
+ - **Ordering of Manual Filters:**
219
+ Ensure that `addManualFilters()` is called before `filter()` so that manual filters are merged correctly.
220
+
221
+ ---
222
+
223
+ ## Security Configuration
224
+
225
+ Security settings enforce allowed operators, exclude forbidden fields (e.g., "password"), and apply role-based access limits:
226
+
227
+ ```javascript
228
+ export const securityConfig = {
229
+ allowedOperators: [
230
+ "eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "regex", "exists", "size", "or", "and"
231
+ ],
232
+ forbiddenFields: ["password"],
233
+ accessLevels: {
234
+ guest: { maxLimit: 50, allowedPopulate: ["*"] },
235
+ user: { maxLimit: 100, allowedPopulate: ["*"] },
236
+ admin: { maxLimit: 1000, allowedPopulate: ["*"] },
237
+ superAdmin: { maxLimit: 1000, allowedPopulate: ["*"] }
238
+ }
239
+ };
240
+ ```
241
+
242
+ These configurations are applied automatically in methods such as `filter()`, `paginate()`, and `limitFields()`.
243
+
244
+ ---
245
+
246
+ ## Security & Performance Enhancements
247
+
248
+ - **Enhanced Logging:**
249
+ Uses winston to log events and errors with detailed timestamps and stack traces.
250
+ - **Improved Error Handling:**
251
+ A custom error class (HandleERROR) coupled with dedicated error middleware centralizes error management.
252
+ - **Dynamic Configuration:**
253
+ Security settings are maintained in a separate config file (`config.js`) for easy modifications without changing core code.
254
+ - **Performance Optimization:**
255
+ Features aggregation cursor support in `execute()` and optimized pipeline ordering for resource-efficient execution.
256
+
257
+ ---
258
+
259
+ ## Error Handling Middleware
260
+
261
+ A dedicated error-handling middleware is available to centralize error responses:
262
+
263
+ ```javascript
264
+ import catchError from "./errorHandler.js";
265
+
266
+ // In your Express app:
267
+ // app.use(catchError);
268
+ ```
269
+
270
+ This middleware sets the appropriate HTTP status code and JSON error message when an error occurs.
271
+
272
+ ---
273
+
274
+ ## Full Examples
275
+
276
+ ### Example 1: Basic Query
277
+
278
+ ```javascript
279
+ import VantaApi from "./api-features.js";
280
+ import Product from "./models/product.js";
281
+
282
+ // URL: /api/products?status=active&price[gte]=100&sort=-price,createdAt&fields=name,price,category&page=1&limit=10&populate=category,brand
283
+ const features = new VantaApi(Product, req.query, "user");
284
+ const result = await features
285
+ .filter()
286
+ .sort()
287
+ .limitFields()
288
+ .paginate()
289
+ .populate()
290
+ .execute();
291
+ console.log(result);
292
+ ```
293
+
294
+ ### Example 2: Query with Manual Filters
295
+
296
+ *(Call `addManualFilters()` before `filter()`)*
297
+
298
+ ```javascript
299
+ const query = { status: "active" };
300
+ const manualFilter = { category: "electronics" };
301
+ const features = new VantaApi(Product, query, "user");
302
+ features.addManualFilters(manualFilter).filter();
303
+ const result = await features.execute();
304
+ console.log(result);
305
+ ```
306
+
307
+ ### Example 3: Advanced Nested Populate with Array Input
308
+
309
+ ```javascript
310
+ const populateArray = [
311
+ "brand", // Simple string input
312
+ { path: "category", select: "name description" },
313
+ { path: "category", select: "name", populate: { path: "subCategory", select: "title" } }
314
+ ];
315
+ const features = new VantaApi(Product, req.query, "admin");
316
+ const result = await features.populate(populateArray).execute();
317
+ console.log(result);
318
+ ```
319
+
320
+ ### Example 4: Full Advanced Query Example
321
+
322
+ ```http
323
+ GET /api/products?
324
+ page=1&
325
+ limit=10&
326
+ sort=-createdAt,price&
327
+ fields=name,price,category&
328
+ populate=category,brand&
329
+ price[gte]=1000&
330
+ category[in]=electronics,phones&
331
+ name[regex]=^Samsung
332
+ ```
333
+
334
+ ### Example 5: Using Default Pagination (when page and limit are not provided)
335
+
336
+ ```javascript
337
+ // URL: /api/products?status=active
338
+ const features = new VantaApi(Product, req.query);
339
+ const result = await features
340
+ .filter() // Defaults to page 1 and limit 10
341
+ .execute();
342
+ console.log(result);
343
+ ```
344
+
345
+ ---
346
+
347
+ ## Summary
348
+
349
+ - **Filtering:**
350
+ Combines query parameters with manual filters and applies a safe `$match` with an automatic `isActive: true` condition for non-admin users.
351
+ - **Sorting:**
352
+ Converts a comma-separated string into a proper `$sort` object.
353
+ - **Field Selection:**
354
+ Uses `$project` to include only permitted fields while excluding forbidden fields.
355
+ - **Pagination:**
356
+ Applies `$skip` and `$limit` with default values (page 1, limit 10) and role-based limits.
357
+ - **Populate:**
358
+ Joins related documents using `$lookup` and `$unwind`, supporting nested and varied input types.
359
+ - **Security:**
360
+ Enforces allowed operators, sanitizes inputs, validates numeric fields, and removes dangerous operators via the security configuration.
361
+ - **Logging & Error Handling:**
362
+ Integrated advanced logging using winston and centralized error handling with a custom error class and middleware.
363
+ - **Performance Optimizations:**
364
+ Supports aggregation cursor for large datasets and optimizes aggregation pipelines for efficient resource usage.
365
+
366
+ ---
367
+
368
+ VantaApi provides a complete solution for integrating powerful, secure, and customizable query capabilities into any Node.js/MongoDB project.
369
+
370
+ ---
@@ -0,0 +1,30 @@
1
+ // bin/create-security-config.js
2
+ import { existsSync, writeFileSync } from 'fs';
3
+ import { join } from 'path';
4
+
5
+ // Define the target path for the security-config file.
6
+ // This example creates it in the current working directory.
7
+ const configPath = join(process.cwd(), 'security-config.js');
8
+
9
+ // If the file does not exist, create it with default content.
10
+ if (!existsSync(configPath)) {
11
+ const defaultConfig = `// Default Security Configuration
12
+ export const securityConfig = {
13
+ allowedOperators: [
14
+ "eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "regex", "exists", "size", "or", "and"
15
+ ],
16
+ forbiddenFields: ["password"],
17
+ accessLevels: {
18
+ guest: { maxLimit: 50, allowedPopulate: ["*"] },
19
+ user: { maxLimit: 100, allowedPopulate: ["*"] },
20
+ admin: { maxLimit: 1000, allowedPopulate: ["*"] },
21
+ superAdmin: { maxLimit: 1000, allowedPopulate: ["*"] }
22
+ }
23
+ };
24
+ `;
25
+
26
+ writeFileSync(configPath, defaultConfig);
27
+ console.log('Default security-config.js created in your project root. You can customize it as needed.');
28
+ } else {
29
+ console.log('security-config.js already exists. Using custom configuration.');
30
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "vanta-api",
3
+ "version": "1.0.0",
4
+ "description": "Advanced API features and security configuration for Node.js/MongoDB.",
5
+ "main": "src/api-features.js",
6
+ "scripts": {
7
+ "postinstall": "node bin/create-security-config.js",
8
+ "test": "jest",
9
+ "start": "node src/api-features.js"
10
+ },
11
+ "keywords": [
12
+ "api",
13
+ "mongodb",
14
+ "mongoose",
15
+ "aggregation",
16
+ "security",
17
+ "nodejs"
18
+ ],
19
+ "author": "Alireza Aghaee",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "mongoose": "^7.0.0",
23
+ "winston": "^3.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "jest": "^29.0.0"
27
+ },
28
+ "type": "module"
29
+ }
@@ -0,0 +1,306 @@
1
+ // api-features.js
2
+ import mongoose from "mongoose";
3
+ import winston from "winston";
4
+ import { securityConfig } from "./config.js";
5
+ import HandleERROR from "./handleError.js";
6
+
7
+ // تنظیم logger با winston
8
+ const logger = winston.createLogger({
9
+ level: "info",
10
+ format: winston.format.combine(
11
+ winston.format.timestamp(),
12
+ winston.format.json()
13
+ ),
14
+ transports: [new winston.transports.Console()]
15
+ });
16
+
17
+ export class ApiFeatures {
18
+ constructor(model, query, userRole = "guest") {
19
+ this.Model = model;
20
+ this.query = { ...query };
21
+ this.userRole = userRole;
22
+ this.pipeline = [];
23
+ this.countPipeline = [];
24
+ this.manualFilters = {};
25
+ // انتخاب استفاده از cursor برای پردازش داده‌های حجیم
26
+ this.useCursor = false;
27
+ this.#initialSanitization();
28
+ }
29
+
30
+ // ---------- Core Methods ----------
31
+ filter() {
32
+ const queryFilters = this.#parseQueryFilters();
33
+ const mergedFilters = { ...queryFilters, ...this.manualFilters };
34
+ const safeFilters = this.#applySecurityFilters(mergedFilters);
35
+
36
+ if (Object.keys(safeFilters).length > 0) {
37
+ // اضافه کردن فیلتر به ابتدای pipeline جهت بهبود عملکرد
38
+ this.pipeline.push({ $match: safeFilters });
39
+ this.countPipeline.push({ $match: safeFilters });
40
+ }
41
+ return this;
42
+ }
43
+
44
+ sort() {
45
+ if (this.query.sort) {
46
+ const sortObject = this.query.sort.split(",").reduce((acc, field) => {
47
+ const [key, order] = field.startsWith("-")
48
+ ? [field.slice(1), -1]
49
+ : [field, 1];
50
+ acc[key] = order;
51
+ return acc;
52
+ }, {});
53
+ this.pipeline.push({ $sort: sortObject });
54
+ }
55
+ return this;
56
+ }
57
+
58
+ limitFields() {
59
+ if (this.query.fields) {
60
+ const allowedFields = this.query.fields
61
+ .split(",")
62
+ .filter(f => !securityConfig.forbiddenFields.includes(f))
63
+ .reduce((acc, curr) => ({ ...acc, [curr]: 1 }), {});
64
+
65
+ this.pipeline.push({ $project: allowedFields });
66
+ }
67
+ return this;
68
+ }
69
+
70
+ paginate() {
71
+ const { maxLimit } = securityConfig.accessLevels[this.userRole] || { maxLimit: 100 };
72
+ const page = Math.max(parseInt(this.query.page, 10) || 1, 1);
73
+ const limit = Math.min(
74
+ parseInt(this.query.limit, 10) || 10,
75
+ maxLimit
76
+ );
77
+
78
+ this.pipeline.push(
79
+ { $skip: (page - 1) * limit },
80
+ { $limit: limit }
81
+ );
82
+ return this;
83
+ }
84
+
85
+ populate(input = "") {
86
+ let populateOptions = [];
87
+
88
+ if (Array.isArray(input)) {
89
+ input.forEach(item => {
90
+ if (typeof item === "object" && item.path) {
91
+ populateOptions.push(item);
92
+ } else if (typeof item === "string") {
93
+ populateOptions.push(item);
94
+ }
95
+ });
96
+ } else if (typeof input === "object" && input.path) {
97
+ populateOptions.push(input);
98
+ } else if (typeof input === "string" && input.trim().length > 0) {
99
+ input.split(",").filter(Boolean).forEach(item => {
100
+ populateOptions.push(item.trim());
101
+ });
102
+ }
103
+
104
+ if (this.query.populate) {
105
+ this.query.populate.split(",").filter(Boolean).forEach(item => {
106
+ populateOptions.push(item.trim());
107
+ });
108
+ }
109
+
110
+ const uniqueMap = new Map();
111
+ populateOptions.forEach(item => {
112
+ if (typeof item === "object" && item.path) {
113
+ uniqueMap.set(item.path, item);
114
+ } else if (typeof item === "string") {
115
+ uniqueMap.set(item, item);
116
+ }
117
+ });
118
+ const uniquePopulateOptions = Array.from(uniqueMap.values());
119
+
120
+ uniquePopulateOptions.forEach(option => {
121
+ let field, projection = {};
122
+ if (typeof option === "object") {
123
+ field = option.path;
124
+ if (option.select) {
125
+ option.select.split(" ").forEach(fieldName => {
126
+ if (fieldName) projection[fieldName.trim()] = 1;
127
+ });
128
+ }
129
+ } else if (typeof option === "string") {
130
+ field = option;
131
+ }
132
+
133
+ field = field.trim();
134
+ const { collection, isArray } = this.#getCollectionInfo(field);
135
+
136
+ let lookupStage = {};
137
+ if (Object.keys(projection).length > 0) {
138
+ lookupStage = {
139
+ $lookup: {
140
+ from: collection,
141
+ let: { localField: `$${field}` },
142
+ pipeline: [
143
+ {
144
+ $match: {
145
+ $expr: { $eq: ["$_id", "$$localField"] }
146
+ }
147
+ },
148
+ { $project: projection }
149
+ ],
150
+ as: field
151
+ }
152
+ };
153
+ } else {
154
+ lookupStage = {
155
+ $lookup: {
156
+ from: collection,
157
+ localField: field,
158
+ foreignField: "_id",
159
+ as: field
160
+ }
161
+ };
162
+ }
163
+
164
+ this.pipeline.push(lookupStage);
165
+ this.pipeline.push({
166
+ $unwind: {
167
+ path: `$${field}`,
168
+ preserveNullAndEmptyArrays: true
169
+ }
170
+ });
171
+ });
172
+
173
+ // پشتیبانی از nested populate: در صورت نیاز، منطق تو در تو را می‌توانید اینجا اضافه کنید.
174
+
175
+ return this;
176
+ }
177
+
178
+ addManualFilters(filters) {
179
+ if (filters) {
180
+ this.manualFilters = { ...this.manualFilters, ...filters };
181
+ }
182
+ return this;
183
+ }
184
+
185
+ async execute(options = {}) {
186
+ try {
187
+ // انتخاب حالت cursor در مواقع پردازش داده‌های حجیم
188
+ if (options.useCursor === true) {
189
+ this.useCursor = true;
190
+ }
191
+ // اجرای موازی pipeline‌های شمارش و داده
192
+ const [countResult, dataResult] = await Promise.all([
193
+ this.Model.aggregate([...this.countPipeline, { $count: "total" }]),
194
+ (this.useCursor
195
+ ? this.Model.aggregate(this.pipeline).cursor({ batchSize: 100 }).exec()
196
+ : this.Model.aggregate(this.pipeline)
197
+ .allowDiskUse(options.allowDiskUse || false)
198
+ .readConcern("majority")
199
+ )
200
+ ]);
201
+
202
+ const count = countResult[0]?.total || 0;
203
+ let data = [];
204
+ if (this.useCursor) {
205
+ const cursor = dataResult;
206
+ for await (const doc of cursor) {
207
+ data.push(doc);
208
+ }
209
+ } else {
210
+ data = dataResult;
211
+ }
212
+
213
+ return {
214
+ success: true,
215
+ count,
216
+ data
217
+ };
218
+ } catch (error) {
219
+ this.#handleError(error);
220
+ }
221
+ }
222
+
223
+ // ---------- Security and Sanitization Methods ----------
224
+ #initialSanitization() {
225
+ ["$where", "$accumulator", "$function"].forEach(op => {
226
+ delete this.query[op];
227
+ delete this.manualFilters[op];
228
+ });
229
+ ["page", "limit"].forEach(field => {
230
+ if (this.query[field] && !/^\d+$/.test(this.query[field])) {
231
+ throw new HandleERROR(`Invalid value for ${field}`, 400);
232
+ }
233
+ });
234
+ }
235
+
236
+ #parseQueryFilters() {
237
+ const queryObj = { ...this.query };
238
+ ["page", "limit", "sort", "fields", "populate"].forEach(el => delete queryObj[el]);
239
+
240
+ return JSON.parse(
241
+ JSON.stringify(queryObj)
242
+ .replace(/\b(gte|gt|lte|lt|in|nin|eq|ne|regex|exists|size)\b/g, "$$$&")
243
+ );
244
+ }
245
+
246
+ #applySecurityFilters(filters) {
247
+ let result = { ...filters };
248
+
249
+ securityConfig.forbiddenFields.forEach(field => delete result[field]);
250
+
251
+ if (this.userRole !== "admin" && this.Model.schema.path("isActive")) {
252
+ result.isActive = true;
253
+ result = this.#sanitizeNestedObjects(result);
254
+ }
255
+
256
+ return result;
257
+ }
258
+
259
+ #sanitizeNestedObjects(obj) {
260
+ return Object.entries(obj).reduce((acc, [key, value]) => {
261
+ if (typeof value === "object" && !Array.isArray(value)) {
262
+ acc[key] = this.#sanitizeNestedObjects(value);
263
+ } else {
264
+ acc[key] = this.#sanitizeValue(key, value);
265
+ }
266
+ return acc;
267
+ }, {});
268
+ }
269
+
270
+ #sanitizeValue(key, value) {
271
+ if (key.endsWith("Id") && mongoose.isValidObjectId(value)) {
272
+ return new mongoose.Types.ObjectId(value);
273
+ }
274
+ if (typeof value === "string") {
275
+ if (value === "true") return true;
276
+ if (value === "false") return false;
277
+ if (/^\d+$/.test(value)) return parseInt(value);
278
+ }
279
+ return value;
280
+ }
281
+
282
+ #getCollectionInfo(field) {
283
+ const schemaPath = this.Model.schema.path(field);
284
+ if (!schemaPath?.options?.ref) {
285
+ throw new HandleERROR(`Invalid populate field: ${field}`, 400);
286
+ }
287
+
288
+ const refModel = mongoose.model(schemaPath.options.ref);
289
+ if (refModel.schema.options.restricted && this.userRole !== "admin") {
290
+ throw new HandleERROR(`Unauthorized to populate ${field}`, 403);
291
+ }
292
+
293
+ return {
294
+ collection: refModel.collection.name,
295
+ isArray: schemaPath.instance === "Array"
296
+ };
297
+ }
298
+
299
+ #handleError(error) {
300
+ // ثبت خطا در logger همراه با stack trace
301
+ logger.error(`[API Features Error]: ${error.message}`, { stack: error.stack });
302
+ throw error;
303
+ }
304
+ }
305
+
306
+ export default ApiFeatures;
package/src/config.js ADDED
@@ -0,0 +1,31 @@
1
+ export const securityConfig = {
2
+ allowedOperators: [
3
+ "eq", "ne", "gt", "gte",
4
+ "lt", "lte", "in", "nin",
5
+ "regex", "exists", "size","or","and"
6
+ ],
7
+
8
+ forbiddenFields: [
9
+ "password",
10
+ ],
11
+
12
+ accessLevels: {
13
+ guest: {
14
+ maxLimit: 50,
15
+ allowedPopulate: ["*"]
16
+ },
17
+ user: {
18
+ maxLimit: 100,
19
+ allowedPopulate: ["*"]
20
+ },
21
+ admin: {
22
+ maxLimit: 1000,
23
+ allowedPopulate: ["*"]
24
+ },
25
+ superAdmin: {
26
+ maxLimit: 1000,
27
+ allowedPopulate: ["*"]
28
+ },
29
+
30
+ }
31
+ };
@@ -0,0 +1,9 @@
1
+ const catchError=(err,req,res,next)=>{
2
+ err.statusCode=err.statusCode || 500
3
+ err.status=err.status||'error'
4
+ res.status(err.statusCode).json({
5
+ status:err.status,
6
+ message:err.message
7
+ })
8
+ }
9
+ export default catchError
@@ -0,0 +1,10 @@
1
+ class HandleERROR extends Error{
2
+ constructor(message,statusCode){
3
+ super(message)
4
+ this.statusCode=statusCode
5
+ this.status=`${statusCode}`.startsWith('4')?'fail':'error'
6
+ this.isOperational=true
7
+ Error.captureStackTrace(this,this.constructor)
8
+ }
9
+ }
10
+ export default HandleERROR
File without changes