vanta-api 1.3.1 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +986 -220
  2. package/package.json +2 -2
  3. package/src/api-features.js +492 -241
package/README.md CHANGED
@@ -1,363 +1,1129 @@
1
- # VantaApi :: Advanced MongoDB API Utilities
1
+ # 🚀 Vanta API
2
2
 
3
- **VantaApi** is a comprehensive toolkit for building secure, performant, and flexible APIs on top of MongoDB with Mongoose. It streamlines common query operations—filtering, sorting, field selection, pagination, and population—while enforcing robust security policies and sanitization.
3
+ **Vanta API** is a lightweight, reusable API utility toolkit for **Express.js** and **Mongoose** applications.
4
+
5
+ It helps you build clean, secure, and production-friendly API endpoints with advanced filtering, search, sorting, pagination, populate, centralized error handling, and async route handling.
4
6
 
5
7
  ---
6
8
 
7
- ## 📦 Installation
9
+ ## Features
10
+
11
+ - Advanced query filtering
12
+ - Manual server-side filters
13
+ - Recursive `$and`, `$or`, `$nor` filter support
14
+ - Automatic `ObjectId` conversion
15
+ - Search using `q`
16
+ - Case-insensitive regex search
17
+ - Sorting
18
+ - Field limiting / projection
19
+ - Pagination
20
+ - Aggregation-based populate
21
+ - Nested populate support
22
+ - Role-based security limits
23
+ - Forbidden field protection
24
+ - Async route wrapper
25
+ - Centralized Express error handler
26
+ - Custom operational error class
8
27
 
9
- Install via npm or Yarn:
28
+ ---
29
+
30
+ ## 📦 Installation
10
31
 
11
32
  ```bash
12
33
  npm install vanta-api
13
- # or
14
- yarn add vanta-api
15
34
  ```
16
35
 
17
- A `postinstall` hook will scaffold a `security-config.js` file in your project root.
36
+ Required dependencies in your app:
37
+
38
+ ```bash
39
+ npm install express mongoose
40
+ ```
18
41
 
19
42
  ---
20
43
 
21
- ## ⚙️ Setup & Initialization
44
+ ## 📁 Project Files
45
+
46
+ ```txt
47
+ src/
48
+ api-features.js
49
+ catchAsync.js
50
+ config.js
51
+ errorHandler.js
52
+ handleError.js
53
+ security-default-config.js
54
+ ```
22
55
 
23
- ### 1. Importing the Package
56
+ | File | Purpose |
57
+ |---|---|
58
+ | `api-features.js` | Main API query builder for filtering, search, sorting, pagination, populate, and execution |
59
+ | `catchAsync.js` | Wraps async Express route handlers and forwards errors to `next()` |
60
+ | `errorHandler.js` | Global Express error middleware |
61
+ | `handleError.js` | Custom operational error class |
62
+ | `config.js` | Loads and merges security configuration |
63
+ | `security-default-config.js` | Default security rules and role-based limits |
24
64
 
25
- #### ECMAScript Module (ESM)
65
+ ---
66
+
67
+ # 🇬🇧 English Documentation
68
+
69
+ ## Quick Start
26
70
 
27
71
  ```js
28
- import express from 'express';
29
- import mongoose from 'mongoose';
30
- import ApiFeatures, { catchAsync, catchError, HandleERROR } from 'vanta-api';
72
+ import ApiFeatures,{catchAsync,catchError,HandleERROR} from "vanta-api";
73
+
31
74
  ```
32
75
 
33
- #### CommonJS (CJS)
76
+ Example controller:
34
77
 
35
78
  ```js
36
- const express = require('express');
37
- const mongoose = require('mongoose');
38
- const { default: ApiFeatures, catchAsync, catchError, HandleERROR } = require('vanta-api');
79
+ export const getProducts = catchAsync(async (req, res, next) => {
80
+ const result = await new ApiFeatures(Product, req.query, req.user?.role)
81
+ .filter()
82
+ .search(["name", "description"])
83
+ .sort()
84
+ .limitFields()
85
+ .paginate()
86
+ .execute();
87
+
88
+ res.status(200).json(result);
89
+ });
39
90
  ```
40
91
 
41
- ### 2. Basic Server Setup
92
+ ---
42
93
 
43
- ```js
44
- const app = express();
45
- app.use(express.json());
94
+ # ApiFeatures
46
95
 
47
- // Connect to MongoDB
48
- mongoose.connect(process.env.MONGO_URI, {
49
- useNewUrlParser: true,
50
- useUnifiedTopology: true
51
- });
96
+ `ApiFeatures` is the main class of this package. It converts request query parameters and manual backend filters into a MongoDB aggregation pipeline.
97
+
98
+ ## Constructor
99
+
100
+ ```js
101
+ new ApiFeatures(model, query, userRole)
52
102
  ```
53
103
 
54
- ### 3. Route Definition & Error Handling
104
+ | Parameter | Type | Required | Description |
105
+ |---|---|---|---|
106
+ | `model` | Mongoose Model | Yes | The Mongoose model used to run aggregation |
107
+ | `query` | Object | No | Usually `req.query` |
108
+ | `userRole` | String | No | Role name used for security rules |
109
+
110
+ Example:
55
111
 
56
112
  ```js
57
- // Example route with async handler
58
- app.get(
59
- '/api/v1/items',
60
- catchAsync(async (req, res, next) => {
61
- const result = await new ApiFeatures(ItemModel, req.query, req.user.role)
62
- .filter()
63
- .sort()
64
- .limitFields()
65
- .paginate()
66
- .populate()
67
- .execute();
68
- res.status(200).json(result);
69
- })
70
- );
113
+ const features = new ApiFeatures(Product, req.query, req.user?.role);
114
+ ```
71
115
 
72
- // Global error handler (after all routes)
73
- app.use(catchError);
116
+ If `userRole` is missing or invalid, the default role is `guest`.
74
117
 
75
- // Start server
76
- app.listen(3000, () => {
77
- console.log('Server running on port 3000');
78
- });
118
+ ---
119
+
120
+ ## Recommended Chain Order
121
+
122
+ ```js
123
+ const result = await new ApiFeatures(Model, req.query, req.user?.role)
124
+ .addManualFilters(serverSideFilters)
125
+ .filter()
126
+ .populate(populateOptions)
127
+ .search(["name", "description"])
128
+ .sort()
129
+ .limitFields()
130
+ .paginate()
131
+ .execute();
79
132
  ```
80
133
 
134
+ Why this order?
135
+
136
+ 1. `addManualFilters()` adds backend-controlled filters.
137
+ 2. `filter()` creates the base `$match`.
138
+ 3. `populate()` joins referenced documents.
139
+ 4. `search()` searches normal or populated fields.
140
+ 5. `sort()` sorts final results.
141
+ 6. `limitFields()` controls output fields.
142
+ 7. `paginate()` applies paging.
143
+ 8. `execute()` runs the aggregation.
144
+
81
145
  ---
82
146
 
83
- ## 🔍 API Reference
147
+ ## `filter()`
84
148
 
85
- ### 1. catchAsync(fn)
149
+ Builds a MongoDB `$match` stage from `req.query`.
150
+
151
+ ```js
152
+ new ApiFeatures(Product, req.query)
153
+ .filter()
154
+ .execute();
155
+ ```
86
156
 
87
- Wraps an async function to catch errors and pass them to Express error middleware.
157
+ ### Simple Filter
88
158
 
89
- * **Signature:** `catchAsync(fn: Function): Function`
159
+ ```txt
160
+ GET /api/products?category=phone
161
+ ```
90
162
 
91
- * **Example:**
163
+ Generated filter:
92
164
 
93
- ```js
94
- app.post(
95
- '/api/v1/users',
96
- catchAsync(async (req, res) => {
97
- // async logic
98
- })
99
- );
100
- ```
165
+ ```js
166
+ {
167
+ category: "phone"
168
+ }
169
+ ```
101
170
 
102
- ### 2. catchError(err, req, res, next)
171
+ ### Comparison Operators
103
172
 
104
- Express error-handling middleware that returns standardized JSON errors.
173
+ ```txt
174
+ GET /api/products?price[gte]=100&price[lte]=500
175
+ ```
105
176
 
106
- * **Response:**
177
+ Generated filter:
107
178
 
108
- ```json
109
- {
110
- "status": "error" | "fail",
111
- "message": "Error description",
112
- "errors": [ /* optional array of details */ ]
179
+ ```js
180
+ {
181
+ price: {
182
+ $gte: 100,
183
+ $lte: 500
113
184
  }
114
- ```
185
+ }
186
+ ```
115
187
 
116
- * **Usage:** Place after all routes
188
+ ### Boolean / Null / Number Conversion
117
189
 
118
- ### 3. HandleERROR
190
+ ```txt
191
+ GET /api/products?isActive=true&deletedAt=null&price=100
192
+ ```
119
193
 
120
- Custom `Error` subclass for operational errors.
194
+ Generated values:
121
195
 
122
- * **Constructor:** `new HandleERROR(message: string, statusCode: number)`
123
- * **Example:**
196
+ ```js
197
+ {
198
+ isActive: true,
199
+ deletedAt: null,
200
+ price: 100
201
+ }
202
+ ```
124
203
 
125
- ```js
126
- if (!user) {
127
- throw new HandleERROR('User not found', 404);
128
- }
129
- ```
204
+ Strings with leading zero are preserved:
205
+
206
+ ```txt
207
+ GET /api/users?code=0012
208
+ ```
209
+
210
+ ```js
211
+ {
212
+ code: "0012"
213
+ }
214
+ ```
215
+
216
+ ### ObjectId Conversion
217
+
218
+ Fields like `_id`, `id`, and fields ending with `id` are converted to `ObjectId` when the value is a strict MongoDB ObjectId.
219
+
220
+ ```txt
221
+ GET /api/products?_id=665f0f6f4e7d9a2e2c123456
222
+ ```
223
+
224
+ ```js
225
+ {
226
+ _id: ObjectId("665f0f6f4e7d9a2e2c123456")
227
+ }
228
+ ```
229
+
230
+ Reserved query keys are excluded from normal filtering:
231
+
232
+ ```js
233
+ ["page", "limit", "sort", "fields", "populate", "q"]
234
+ ```
235
+
236
+ ---
237
+
238
+ ## `addManualFilters(filters)`
239
+
240
+ Adds backend-controlled filters manually. This is useful when you want to enforce conditions that users should not control from the URL.
241
+
242
+ ```js
243
+ new ApiFeatures(Order, req.query)
244
+ .addManualFilters({ user: req.user._id })
245
+ .filter()
246
+ .execute();
247
+ ```
248
+
249
+ ### `$and` Example
250
+
251
+ ```js
252
+ const result = await new ApiFeatures(Product, req.query)
253
+ .addManualFilters({
254
+ $and: [
255
+ { _id: "665f0f6f4e7d9a2e2c123456" },
256
+ { isActive: true }
257
+ ]
258
+ })
259
+ .filter()
260
+ .execute();
261
+ ```
262
+
263
+ The `_id` inside `$and` is recursively converted to `ObjectId`.
264
+
265
+ ### `$or` Example
266
+
267
+ ```js
268
+ const result = await new ApiFeatures(Product, req.query)
269
+ .addManualFilters({
270
+ $or: [
271
+ { ownerId: "665f0f6f4e7d9a2e2c123456" },
272
+ { createdById: "665f0f6f4e7d9a2e2c654321" }
273
+ ]
274
+ })
275
+ .filter()
276
+ .execute();
277
+ ```
278
+
279
+ ### `$nor` Example
280
+
281
+ ```js
282
+ const result = await new ApiFeatures(Product, req.query)
283
+ .addManualFilters({
284
+ $nor: [
285
+ { status: "blocked" },
286
+ { isDeleted: true }
287
+ ]
288
+ })
289
+ .filter()
290
+ .execute();
291
+ ```
292
+
293
+ ### `$in` Example
294
+
295
+ ```js
296
+ const result = await new ApiFeatures(Product, req.query)
297
+ .addManualFilters({
298
+ _id: {
299
+ $in: [
300
+ "665f0f6f4e7d9a2e2c123456",
301
+ "665f0f6f4e7d9a2e2c654321"
302
+ ]
303
+ }
304
+ })
305
+ .filter()
306
+ .execute();
307
+ ```
308
+
309
+ ---
310
+
311
+ ## `search(fields)`
312
+
313
+ Searches using the `q` key from `req.query`. It always uses case-insensitive regex search.
314
+
315
+ ```js
316
+ .search(["name", "description"])
317
+ ```
318
+
319
+ ```txt
320
+ GET /api/products?q=iphone
321
+ ```
322
+
323
+ ```js
324
+ const result = await new ApiFeatures(Product, req.query)
325
+ .filter()
326
+ .search(["name", "description", "brand"])
327
+ .paginate()
328
+ .execute();
329
+ ```
330
+
331
+ Generated condition:
332
+
333
+ ```js
334
+ {
335
+ $or: [
336
+ { name: { $regex: "iphone", $options: "i" } },
337
+ { description: { $regex: "iphone", $options: "i" } },
338
+ { brand: { $regex: "iphone", $options: "i" } }
339
+ ]
340
+ }
341
+ ```
342
+
343
+ `q` is reserved and is not treated as a normal filter.
344
+
345
+ ---
346
+
347
+ ## `sort()`
348
+
349
+ Sorts results using the `sort` query key.
350
+
351
+ ```txt
352
+ GET /api/products?sort=-createdAt,price
353
+ ```
354
+
355
+ ```js
356
+ {
357
+ createdAt: -1,
358
+ price: 1
359
+ }
360
+ ```
361
+
362
+ Only fields existing in the model schema are accepted.
130
363
 
131
364
  ---
132
365
 
133
- ## 🚀 ApiFeatures Class
366
+ ## `limitFields(input)`
367
+
368
+ Controls returned fields using projection.
134
369
 
135
- Chainable class that translates HTTP query parameters into a secure MongoDB aggregation pipeline.
370
+ ```txt
371
+ GET /api/products?fields=name,price,category
372
+ ```
373
+
374
+ or:
136
375
 
137
376
  ```js
138
- const features = new ApiFeatures(
139
- Model, // Mongoose model
140
- req.query, // HTTP query object
141
- req.user.role // User role for security (guest|user|admin|superAdmin)
142
- )
143
- .filter() // Filtering
144
- .sort() // Sorting
145
- .limitFields() // Field limiting
146
- .paginate() // Pagination
147
- .populate() // Population
148
- .addManualFilters({ isActive: true }) // Manual filters
149
- ;
150
- const result = await features.execute({ allowDiskUse: true });
377
+ .limitFields("name,price")
151
378
  ```
152
379
 
153
- ### Constructor
380
+ Include mode:
154
381
 
155
- ```ts
156
- new ApiFeatures(
157
- model: mongoose.Model,
158
- queryParams: Record<string, any> = {},
159
- userRole: string = 'guest'
160
- )
382
+ ```js
383
+ { name: 1, price: 1 }
161
384
  ```
162
385
 
163
- * **model**: Mongoose model.
164
- * **queryParams**: Typically `req.query`.
165
- * **userRole**: Role key for security rules.
386
+ Exclude mode:
387
+
388
+ ```txt
389
+ GET /api/products?fields=-description
390
+ ```
391
+
392
+ ```js
393
+ { description: 0 }
394
+ ```
166
395
 
167
- ### Chainable Methods
396
+ Mixed include/exclude is not allowed:
168
397
 
169
- | Method | Description |
170
- | ------------------------ | ---------------------------------------------------------------------------------------------------- |
171
- | `.filter()` | Applies MongoDB operators. Supported operators: |
172
- | | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `regex` (e.g. `?price[gt]=100&name[regex]=Book`). |
173
- | `.sort()` | Sorting (e.g. `?sort=price,-name`). |
174
- | `.limitFields()` | Field selection (e.g. `?fields=name,price`). |
175
- | `.paginate()` | Pagination (e.g. `?page=2&limit=10`). |
176
- | `.populate(paths?)` | Populate referenced documents. Accepts various input types (see below). |
177
- | `.addManualFilters(obj)` | Add programmatic filters (e.g. `.addManualFilters({ isActive: true })`). |
398
+ ```txt
399
+ GET /api/products?fields=name,-password
400
+ ```
178
401
 
179
- > All methods return `this` for chaining.
402
+ Throws:
180
403
 
181
- #### Populate Input Formats
404
+ ```txt
405
+ Cannot mix include and exclude fields
406
+ ```
182
407
 
183
- The `.populate()` method supports multiple ways to specify which paths to populate:
408
+ ---
184
409
 
185
- 1. **String (comma-separated)**
410
+ ## `paginate()`
186
411
 
187
- ```js
188
- .populate('author,comments')
189
- ```
190
- 2. **Array of strings**
412
+ Adds `$skip` and `$limit`.
191
413
 
192
- ```js
193
- .populate(['author', 'comments'])
194
- ```
195
- 3. **Dot notation for nested paths**
414
+ ```txt
415
+ GET /api/products?page=2&limit=10
416
+ ```
196
417
 
197
- ```js
198
- // Populating nested field 'comments.author'
199
- .populate('comments.author')
200
- ```
201
- 4. **Object with options**
418
+ Pipeline:
202
419
 
203
- ```js
204
- .populate({
205
- path: 'author', // field to populate
206
- select: 'name email', // include only name and email
207
- match: { isActive: true}, // only active authors
208
- options: { limit: 5 } // limit populated docs
209
- })
210
- ```
211
- 5. **Array of objects**
420
+ ```js
421
+ [
422
+ { $skip: 10 },
423
+ { $limit: 10 }
424
+ ]
425
+ ```
212
426
 
213
- ```js
214
- .populate([
215
- { path: 'author', select: 'name' },
216
- { path: 'comments', match: { flagged: false } }
217
- ])
218
- ```
427
+ Limits are capped by role.
219
428
 
220
429
  ---
221
430
 
222
- ### execute(options)
431
+ ## `populate(input)`
432
+
433
+ Performs aggregation-based populate using `$lookup`.
434
+
435
+ ```js
436
+ .populate("user")
437
+ ```
438
+
439
+ From query:
440
+
441
+ ```txt
442
+ GET /api/posts?populate=user
443
+ ```
223
444
 
224
- Executes aggregation pipeline.
225
- (options)
445
+ Multiple populate paths:
226
446
 
227
- Executes aggregation pipeline.
447
+ ```js
448
+ .populate(["user", "category"])
449
+ ```
228
450
 
229
- * **Signature:**
451
+ Nested populate:
230
452
 
231
- ````ts
232
- async execute(options?: {
233
- useCursor?: boolean; // return cursor if true
234
- allowDiskUse?: boolean; // enable disk use
235
- projection?: Record<string, any>; // manual projection map
236
- }): Promise<{
237
- success: boolean;
238
- count: number;
239
- data: any[];
240
- }>```
241
-
242
- ````
243
- * **Example Response:**
244
-
245
- ```json
246
- {
247
- "success": true,
248
- "count": 50,
249
- "data": [ /* documents */ ]
453
+ ```js
454
+ .populate({
455
+ path: "user",
456
+ populate: {
457
+ path: "company",
458
+ populate: {
459
+ path: "country"
460
+ }
250
461
  }
251
- ```
462
+ })
463
+ ```
464
+
465
+ Dot notation:
466
+
467
+ ```js
468
+ .populate("user.company.country")
469
+ ```
470
+
471
+ Populate with select:
472
+
473
+ ```js
474
+ .populate({
475
+ path: "user",
476
+ select: "name email"
477
+ })
478
+ ```
479
+
480
+ Exclude fields:
481
+
482
+ ```js
483
+ .populate({
484
+ path: "user",
485
+ select: "-password"
486
+ })
487
+ ```
488
+
489
+ Mixed include/exclude is not allowed.
490
+
491
+ ---
492
+
493
+ ## `execute(options)`
494
+
495
+ Runs the aggregation pipeline.
496
+
497
+ ```js
498
+ const result = await features.execute();
499
+ ```
500
+
501
+ Returns:
502
+
503
+ ```js
504
+ {
505
+ success: true,
506
+ count: 25,
507
+ data: [...]
508
+ }
509
+ ```
510
+
511
+ Options:
512
+
513
+ ```js
514
+ .execute({
515
+ debug: true,
516
+ useCursor: false,
517
+ batchSize: 100,
518
+ maxTimeMS: 10000,
519
+ allowDiskUse: true,
520
+ readConcern: "majority"
521
+ })
522
+ ```
523
+
524
+ | Option | Type | Default | Description |
525
+ |---|---|---|---|
526
+ | `debug` | Boolean | `false` | Logs the pipeline |
527
+ | `useCursor` | Boolean | `false` | Uses aggregation cursor |
528
+ | `batchSize` | Number | `100` | Cursor batch size |
529
+ | `maxTimeMS` | Number | `10000` | Max execution time |
530
+ | `allowDiskUse` | Boolean | `false` | Allows disk usage |
531
+ | `readConcern` | String | `majority` | MongoDB read concern |
532
+
533
+ ---
534
+
535
+ # Full Controller Example
536
+
537
+ ```js
538
+ import ApiFeatures,{catchAsync} from "vanta-api";
539
+ import Product from "../models/productModel.js";
540
+
541
+ export const getProducts = catchAsync(async (req, res, next) => {
542
+ const result = await new ApiFeatures(Product, req.query, req.user?.role)
543
+ .addManualFilters({ isDeleted: false })
544
+ .filter()
545
+ .populate([
546
+ {
547
+ path: "category",
548
+ select: "name slug"
549
+ },
550
+ {
551
+ path: "createdBy",
552
+ select: "name email"
553
+ }
554
+ ])
555
+ .search(["name", "description", "category.name"])
556
+ .sort()
557
+ .limitFields()
558
+ .paginate()
559
+ .execute();
560
+
561
+ res.status(200).json(result);
562
+ });
563
+ ```
564
+
565
+ ---
566
+
567
+ # Error Handling
568
+
569
+ ## `HandleERROR`
570
+
571
+ Custom operational error class.
572
+
573
+ ```js
574
+ import {HandleERROR} from "vanta-api";
575
+
576
+ throw new HandleERROR("Product not found", 404);
577
+ ```
578
+
579
+ Example properties:
580
+
581
+ ```js
582
+ {
583
+ message: "Product not found",
584
+ statusCode: 404,
585
+ status: "fail",
586
+ isOperational: true
587
+ }
588
+ ```
589
+
590
+ For `4xx` errors, `status` is `fail`. For `5xx` errors, `status` is `error`.
591
+
592
+ ## `catchAsync`
593
+
594
+ Wraps async Express handlers and removes repetitive `try/catch`.
595
+
596
+ ```js
597
+ import {catchAsync} from "vanta-api";
598
+
599
+ app.get(
600
+ "/products",
601
+ catchAsync(async (req, res, next) => {
602
+ const products = await Product.find();
603
+ res.json(products);
604
+ })
605
+ );
606
+ ```
607
+
608
+ Any rejected promise is forwarded to Express `next()`.
609
+
610
+ ## `errorHandler`
611
+
612
+ Global Express error middleware.
613
+
614
+ ```js
615
+ import {catchError} from "vanta-api";
616
+
617
+ app.use(catchError);
618
+ ```
619
+
620
+ Example response:
621
+
622
+ ```json
623
+ {
624
+ "status": "fail",
625
+ "success": false,
626
+ "message": "Product not found"
627
+ }
628
+ ```
629
+
630
+ Use it after all routes:
631
+
632
+ ```js
633
+ app.use("/api/products", productRouter);
634
+ app.use(catchError);
635
+ ```
252
636
 
253
637
  ---
254
638
 
255
- ## 🔐 Security Configuration
639
+ # Security Configuration
640
+
641
+ Default security settings are stored in:
642
+
643
+ ```txt
644
+ src/security-default-config.js
645
+ ```
256
646
 
257
- Customize rules in `security-config.js` at your project root (auto-generated):
647
+ You can override them by creating a `security-config.js` file in your project root.
258
648
 
259
649
  ```js
260
- // security-config.js
261
650
  export const securityConfig = {
262
651
  allowedOperators: [
263
- 'eq','ne','gt','gte','lt','lte','in','nin','regex'
652
+ "eq",
653
+ "ne",
654
+ "gt",
655
+ "gte",
656
+ "lt",
657
+ "lte",
658
+ "in",
659
+ "nin",
660
+ "regex",
661
+ "exists"
264
662
  ],
265
- forbiddenFields: ['password','__v'],
663
+
664
+ forbiddenFields: ["password", "refreshToken", "resetPasswordToken"],
665
+
666
+ maxPipelineStages: 50,
667
+
266
668
  accessLevels: {
267
669
  guest: {
268
670
  maxLimit: 20,
269
- allowedPopulate: []
671
+ allowedPopulate: ["category"]
270
672
  },
271
673
  user: {
272
674
  maxLimit: 100,
273
- allowedPopulate: ['orders','profile']
675
+ allowedPopulate: ["category", "createdBy"]
274
676
  },
275
677
  admin: {
276
678
  maxLimit: 1000,
277
- allowedPopulate: ['*']
679
+ allowedPopulate: ["*"]
278
680
  }
279
681
  }
280
682
  };
281
683
  ```
282
684
 
283
- * **allowedOperators**: Operators users can use in queries.
284
- * **forbiddenFields**: Fields excluded from results.
285
- * **accessLevels**: Per-role limits and populate permissions.
685
+ ---
686
+
687
+ # Express Setup Example
688
+
689
+ ```js
690
+ import express from "express";
691
+ import {catchError} from "vanta-api";
692
+ import productRouter from "./routes/productRoutes.js";
693
+
694
+ const app = express();
695
+
696
+ app.use(express.json());
697
+ app.use("/api/products", productRouter);
698
+ app.use(catchError);
699
+
700
+ export default app;
701
+ ```
702
+
703
+ ---
704
+
705
+ # 🇮🇷 مستندات فارسی
706
+
707
+ ## شروع سریع
708
+
709
+ ```js
710
+ import ApiFeatures,{catchAsync,catchError,HandleERROR} from "vanta-api";
711
+
712
+ ```
713
+
714
+ مثال controller:
286
715
 
287
- > If you omit `security-config.js`, defaults are applied from the package.
716
+ ```js
717
+ export const getProducts = catchAsync(async (req, res, next) => {
718
+ const result = await new ApiFeatures(Product, req.query, req.user?.role)
719
+ .filter()
720
+ .search(["name", "description"])
721
+ .sort()
722
+ .limitFields()
723
+ .paginate()
724
+ .execute();
725
+
726
+ res.status(200).json(result);
727
+ });
728
+ ```
288
729
 
289
730
  ---
290
731
 
291
- ## 🔍 Examples
732
+ # ApiFeatures چیست؟
733
+
734
+ `ApiFeatures` کلاس اصلی پکیج است. این کلاس از روی `req.query` و فیلترهای دستی سمت سرور، یک MongoDB aggregation pipeline می‌سازد.
292
735
 
293
- ### Filter with Multiple Operators
736
+ ## Constructor
294
737
 
295
- ```http
296
- GET /api/v1/products?price[gte]=50&price[lte]=200&category[in]=["books","electronics"]
738
+ ```js
739
+ new ApiFeatures(model, query, userRole)
297
740
  ```
298
741
 
299
- ### Manual Filter & Projection
742
+ | ورودی | نوع | اجباری | توضیح |
743
+ |---|---|---|---|
744
+ | `model` | Mongoose Model | بله | مدلی که aggregation روی آن اجرا می‌شود |
745
+ | `query` | Object | خیر | معمولاً همان `req.query` |
746
+ | `userRole` | String | خیر | نقش کاربر برای قوانین امنیتی |
747
+
748
+ اگر `userRole` داده نشود یا معتبر نباشد، role پیش‌فرض `guest` استفاده می‌شود.
749
+
750
+ ---
751
+
752
+ ## ترتیب پیشنهادی استفاده
300
753
 
301
754
  ```js
302
- const features = new ApiFeatures(Product, req.query, 'user')
303
- .addManualFilters({ isActive: true })
755
+ const result = await new ApiFeatures(Model, req.query, req.user?.role)
756
+ .addManualFilters(serverSideFilters)
304
757
  .filter()
758
+ .populate(populateOptions)
759
+ .search(["name", "description"])
760
+ .sort()
305
761
  .limitFields()
306
- .execute({ projection: { name: 1, price: 1 } });
762
+ .paginate()
763
+ .execute();
307
764
  ```
308
765
 
309
- ### Populate Relations
766
+ ---
767
+
768
+ ## `filter()`
310
769
 
311
- ```http
312
- GET /api/v1/posts?populate=author,comments
770
+ از روی `req.query` مرحله‌ی `$match` می‌سازد.
771
+
772
+ ```txt
773
+ GET /api/products?category=phone
774
+ ```
775
+
776
+ ```js
777
+ {
778
+ category: "phone"
779
+ }
313
780
  ```
314
781
 
315
- ### Complete Controller Example
782
+ operatorهای مقایسه‌ای:
783
+
784
+ ```txt
785
+ GET /api/products?price[gte]=100&price[lte]=500
786
+ ```
316
787
 
317
788
  ```js
318
- app.get(
319
- '/api/v1/orders',
320
- catchAsync(async (req, res) => {
321
- const result = await new ApiFeatures(Order, req.query, req.user.role)
322
- .filter()
323
- .sort()
324
- .limitFields()
325
- .paginate()
326
- .populate()
327
- .execute();
328
-
329
- res.json(result);
789
+ {
790
+ price: {
791
+ $gte: 100,
792
+ $lte: 500
793
+ }
794
+ }
795
+ ```
796
+
797
+ تبدیل‌های خودکار:
798
+
799
+ ```txt
800
+ GET /api/products?isActive=true&deletedAt=null&price=100
801
+ ```
802
+
803
+ ```js
804
+ {
805
+ isActive: true,
806
+ deletedAt: null,
807
+ price: 100
808
+ }
809
+ ```
810
+
811
+ کلیدهایی مثل `_id`, `id` و کلیدهایی که به `id` ختم می‌شوند، اگر مقدارشان ObjectId معتبر باشد، به `ObjectId` تبدیل می‌شوند.
812
+
813
+ ---
814
+
815
+ ## `addManualFilters(filters)`
816
+
817
+ برای اضافه کردن فیلترهای دستی سمت سرور استفاده می‌شود.
818
+
819
+ ```js
820
+ new ApiFeatures(Order, req.query)
821
+ .addManualFilters({ user: req.user._id })
822
+ .filter()
823
+ .execute();
824
+ ```
825
+
826
+ استفاده از `$and`:
827
+
828
+ ```js
829
+ const result = await new ApiFeatures(Product, req.query)
830
+ .addManualFilters({
831
+ $and: [
832
+ { _id: "665f0f6f4e7d9a2e2c123456" },
833
+ { isActive: true }
834
+ ]
330
835
  })
331
- );
836
+ .filter()
837
+ .execute();
838
+ ```
839
+
840
+ در این حالت `_id` داخل `$and` هم به صورت recursive به `ObjectId` تبدیل می‌شود.
841
+
842
+ استفاده از `$or`:
843
+
844
+ ```js
845
+ const result = await new ApiFeatures(Product, req.query)
846
+ .addManualFilters({
847
+ $or: [
848
+ { ownerId: "665f0f6f4e7d9a2e2c123456" },
849
+ { createdById: "665f0f6f4e7d9a2e2c654321" }
850
+ ]
851
+ })
852
+ .filter()
853
+ .execute();
854
+ ```
855
+
856
+ استفاده از `$in`:
857
+
858
+ ```js
859
+ const result = await new ApiFeatures(Product, req.query)
860
+ .addManualFilters({
861
+ _id: {
862
+ $in: [
863
+ "665f0f6f4e7d9a2e2c123456",
864
+ "665f0f6f4e7d9a2e2c654321"
865
+ ]
866
+ }
867
+ })
868
+ .filter()
869
+ .execute();
332
870
  ```
333
871
 
334
872
  ---
335
873
 
336
- ## 🧪 Testing
874
+ ## `search(fields)`
337
875
 
338
- ```bash
339
- npm test
876
+ از کلید `q` داخل `req.query` مقدار را می‌گیرد و با regex جستجو می‌کند. جستجو همیشه case-insensitive است.
877
+
878
+ ```txt
879
+ GET /api/products?q=iphone
880
+ ```
881
+
882
+ ```js
883
+ const result = await new ApiFeatures(Product, req.query)
884
+ .filter()
885
+ .search(["name", "description", "brand"])
886
+ .paginate()
887
+ .execute();
888
+ ```
889
+
890
+ نکته: `q` جزو کلیدهای رزرو شده است و وارد filter معمولی نمی‌شود.
891
+
892
+ ---
893
+
894
+ ## `sort()`
895
+
896
+ از `sort` داخل query برای مرتب‌سازی استفاده می‌کند.
897
+
898
+ ```txt
899
+ GET /api/products?sort=-createdAt,price
900
+ ```
901
+
902
+ ```js
903
+ {
904
+ createdAt: -1,
905
+ price: 1
906
+ }
907
+ ```
908
+
909
+ ---
910
+
911
+ ## `limitFields(input)`
912
+
913
+ برای کنترل فیلدهای خروجی استفاده می‌شود.
914
+
915
+ ```txt
916
+ GET /api/products?fields=name,price,category
917
+ ```
918
+
919
+ یا مستقیم:
920
+
921
+ ```js
922
+ .limitFields("name,price")
923
+ ```
924
+
925
+ حالت include:
926
+
927
+ ```js
928
+ { name: 1, price: 1 }
929
+ ```
930
+
931
+ حالت exclude:
932
+
933
+ ```js
934
+ { description: 0 }
340
935
  ```
341
936
 
342
- Tests use Jest. Add tests for your controllers and ApiFeatures behaviors.
937
+ حالت mixed مجاز نیست:
938
+
939
+ ```txt
940
+ GET /api/products?fields=name,-password
941
+ ```
343
942
 
344
943
  ---
345
944
 
346
- ## 📜 License
945
+ ## `paginate()`
946
+
947
+ برای صفحه‌بندی استفاده می‌شود.
347
948
 
348
- MIT © [Alireza Aghaee](https://github.com/alirezaaghaee)
949
+ ```txt
950
+ GET /api/products?page=2&limit=10
951
+ ```
952
+
953
+ ```js
954
+ [
955
+ { $skip: 10 },
956
+ { $limit: 10 }
957
+ ]
958
+ ```
349
959
 
350
960
  ---
351
961
 
352
- ## 📜 License
962
+ ## `populate(input)`
963
+
964
+ با استفاده از aggregation و `$lookup` داده‌های مرتبط را join می‌کند.
965
+
966
+ ```js
967
+ .populate("user")
968
+ ```
969
+
970
+ از query:
971
+
972
+ ```txt
973
+ GET /api/posts?populate=user
974
+ ```
975
+
976
+ چند populate:
977
+
978
+ ```js
979
+ .populate(["user", "category"])
980
+ ```
981
+
982
+ nested populate:
983
+
984
+ ```js
985
+ .populate({
986
+ path: "user",
987
+ populate: {
988
+ path: "company",
989
+ populate: {
990
+ path: "country"
991
+ }
992
+ }
993
+ })
994
+ ```
995
+
996
+ یا با dot notation:
997
+
998
+ ```js
999
+ .populate("user.company.country")
1000
+ ```
1001
+
1002
+ select در populate:
353
1003
 
354
- MIT © 2024 Alireza Aghaee
1004
+ ```js
1005
+ .populate({
1006
+ path: "user",
1007
+ select: "name email"
1008
+ })
1009
+ ```
1010
+
1011
+ exclude:
1012
+
1013
+ ```js
1014
+ .populate({
1015
+ path: "user",
1016
+ select: "-password"
1017
+ })
1018
+ ```
355
1019
 
356
1020
  ---
357
1021
 
358
- ## ✒️ Author
1022
+ ## `execute(options)`
1023
+
1024
+ pipeline را اجرا می‌کند.
1025
+
1026
+ ```js
1027
+ const result = await features.execute();
1028
+ ```
1029
+
1030
+ خروجی:
1031
+
1032
+ ```js
1033
+ {
1034
+ success: true,
1035
+ count: 25,
1036
+ data: [...]
1037
+ }
1038
+ ```
1039
+
1040
+ ---
1041
+
1042
+ # مدیریت خطاها
1043
+
1044
+ ## `HandleERROR`
1045
+
1046
+ کلاس خطای اختصاصی.
1047
+
1048
+ ```js
1049
+ throw new HandleERROR("Product not found", 404);
1050
+ ```
1051
+
1052
+ ```js
1053
+ {
1054
+ message: "Product not found",
1055
+ statusCode: 404,
1056
+ status: "fail",
1057
+ isOperational: true
1058
+ }
1059
+ ```
1060
+
1061
+ ## `catchAsync`
1062
+
1063
+ برای حذف `try/catch` تکراری در route handlerهای async.
1064
+
1065
+ ```js
1066
+ app.get(
1067
+ "/products",
1068
+ catchAsync(async (req, res, next) => {
1069
+ const products = await Product.find();
1070
+ res.json(products);
1071
+ })
1072
+ );
1073
+ ```
1074
+
1075
+ ## `errorHandler`
1076
+
1077
+ middleware مرکزی خطاها:
1078
+
1079
+ ```js
1080
+ app.use(catchError);
1081
+ ```
1082
+
1083
+ نمونه خروجی:
1084
+
1085
+ ```json
1086
+ {
1087
+ "status": "fail",
1088
+ "success": false,
1089
+ "message": "Product not found"
1090
+ }
1091
+ ```
1092
+
1093
+ ---
1094
+
1095
+ # تنظیمات امنیتی
1096
+
1097
+ برای override کردن تنظیمات پیش‌فرض، در root پروژه فایل زیر را بسازید:
1098
+
1099
+ ```txt
1100
+ security-config.js
1101
+ ```
1102
+
1103
+ ```js
1104
+ export const securityConfig = {
1105
+ allowedOperators: ["eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "regex", "exists"],
1106
+ forbiddenFields: ["password", "refreshToken", "resetPasswordToken"],
1107
+ maxPipelineStages: 50,
1108
+ accessLevels: {
1109
+ guest: {
1110
+ maxLimit: 20,
1111
+ allowedPopulate: ["category"]
1112
+ },
1113
+ user: {
1114
+ maxLimit: 100,
1115
+ allowedPopulate: ["category", "createdBy"]
1116
+ },
1117
+ admin: {
1118
+ maxLimit: 1000,
1119
+ allowedPopulate: ["*"]
1120
+ }
1121
+ }
1122
+ };
1123
+ ```
1124
+
1125
+ ---
359
1126
 
360
- **Alireza Aghaee**
1127
+ # License
361
1128
 
362
- * GitHub: [AlirezaAghaee1996](https://github.com/AlirezaAghaee1996)
363
- * LinkedIn: [alireza-aghaee-mern-dev](https://www.linkedin.com/in/alireza-aghaee-mern-dev)
1129
+ MIT