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.
- package/README.md +986 -220
- package/package.json +2 -2
- package/src/api-features.js +492 -241
package/README.md
CHANGED
|
@@ -1,363 +1,1129 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🚀 Vanta API
|
|
2
2
|
|
|
3
|
-
**
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
Required dependencies in your app:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install express mongoose
|
|
40
|
+
```
|
|
18
41
|
|
|
19
42
|
---
|
|
20
43
|
|
|
21
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# 🇬🇧 English Documentation
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
26
70
|
|
|
27
71
|
```js
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
import ApiFeatures, { catchAsync, catchError, HandleERROR } from 'vanta-api';
|
|
72
|
+
import ApiFeatures,{catchAsync,catchError,HandleERROR} from "vanta-api";
|
|
73
|
+
|
|
31
74
|
```
|
|
32
75
|
|
|
33
|
-
|
|
76
|
+
Example controller:
|
|
34
77
|
|
|
35
78
|
```js
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
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
|
-
|
|
92
|
+
---
|
|
42
93
|
|
|
43
|
-
|
|
44
|
-
const app = express();
|
|
45
|
-
app.use(express.json());
|
|
94
|
+
# ApiFeatures
|
|
46
95
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
73
|
-
app.use(catchError);
|
|
116
|
+
If `userRole` is missing or invalid, the default role is `guest`.
|
|
74
117
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
##
|
|
147
|
+
## `filter()`
|
|
84
148
|
|
|
85
|
-
|
|
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
|
-
|
|
157
|
+
### Simple Filter
|
|
88
158
|
|
|
89
|
-
|
|
159
|
+
```txt
|
|
160
|
+
GET /api/products?category=phone
|
|
161
|
+
```
|
|
90
162
|
|
|
91
|
-
|
|
163
|
+
Generated filter:
|
|
92
164
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
})
|
|
99
|
-
);
|
|
100
|
-
```
|
|
165
|
+
```js
|
|
166
|
+
{
|
|
167
|
+
category: "phone"
|
|
168
|
+
}
|
|
169
|
+
```
|
|
101
170
|
|
|
102
|
-
###
|
|
171
|
+
### Comparison Operators
|
|
103
172
|
|
|
104
|
-
|
|
173
|
+
```txt
|
|
174
|
+
GET /api/products?price[gte]=100&price[lte]=500
|
|
175
|
+
```
|
|
105
176
|
|
|
106
|
-
|
|
177
|
+
Generated filter:
|
|
107
178
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
179
|
+
```js
|
|
180
|
+
{
|
|
181
|
+
price: {
|
|
182
|
+
$gte: 100,
|
|
183
|
+
$lte: 500
|
|
113
184
|
}
|
|
114
|
-
|
|
185
|
+
}
|
|
186
|
+
```
|
|
115
187
|
|
|
116
|
-
|
|
188
|
+
### Boolean / Null / Number Conversion
|
|
117
189
|
|
|
118
|
-
|
|
190
|
+
```txt
|
|
191
|
+
GET /api/products?isActive=true&deletedAt=null&price=100
|
|
192
|
+
```
|
|
119
193
|
|
|
120
|
-
|
|
194
|
+
Generated values:
|
|
121
195
|
|
|
122
|
-
|
|
123
|
-
|
|
196
|
+
```js
|
|
197
|
+
{
|
|
198
|
+
isActive: true,
|
|
199
|
+
deletedAt: null,
|
|
200
|
+
price: 100
|
|
201
|
+
}
|
|
202
|
+
```
|
|
124
203
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
##
|
|
366
|
+
## `limitFields(input)`
|
|
367
|
+
|
|
368
|
+
Controls returned fields using projection.
|
|
134
369
|
|
|
135
|
-
|
|
370
|
+
```txt
|
|
371
|
+
GET /api/products?fields=name,price,category
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
or:
|
|
136
375
|
|
|
137
376
|
```js
|
|
138
|
-
|
|
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
|
-
|
|
380
|
+
Include mode:
|
|
154
381
|
|
|
155
|
-
```
|
|
156
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
386
|
+
Exclude mode:
|
|
387
|
+
|
|
388
|
+
```txt
|
|
389
|
+
GET /api/products?fields=-description
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
```js
|
|
393
|
+
{ description: 0 }
|
|
394
|
+
```
|
|
166
395
|
|
|
167
|
-
|
|
396
|
+
Mixed include/exclude is not allowed:
|
|
168
397
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
402
|
+
Throws:
|
|
180
403
|
|
|
181
|
-
|
|
404
|
+
```txt
|
|
405
|
+
Cannot mix include and exclude fields
|
|
406
|
+
```
|
|
182
407
|
|
|
183
|
-
|
|
408
|
+
---
|
|
184
409
|
|
|
185
|
-
|
|
410
|
+
## `paginate()`
|
|
186
411
|
|
|
187
|
-
|
|
188
|
-
.populate('author,comments')
|
|
189
|
-
```
|
|
190
|
-
2. **Array of strings**
|
|
412
|
+
Adds `$skip` and `$limit`.
|
|
191
413
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
3. **Dot notation for nested paths**
|
|
414
|
+
```txt
|
|
415
|
+
GET /api/products?page=2&limit=10
|
|
416
|
+
```
|
|
196
417
|
|
|
197
|
-
|
|
198
|
-
// Populating nested field 'comments.author'
|
|
199
|
-
.populate('comments.author')
|
|
200
|
-
```
|
|
201
|
-
4. **Object with options**
|
|
418
|
+
Pipeline:
|
|
202
419
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
})
|
|
210
|
-
```
|
|
211
|
-
5. **Array of objects**
|
|
420
|
+
```js
|
|
421
|
+
[
|
|
422
|
+
{ $skip: 10 },
|
|
423
|
+
{ $limit: 10 }
|
|
424
|
+
]
|
|
425
|
+
```
|
|
212
426
|
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
(options)
|
|
445
|
+
Multiple populate paths:
|
|
226
446
|
|
|
227
|
-
|
|
447
|
+
```js
|
|
448
|
+
.populate(["user", "category"])
|
|
449
|
+
```
|
|
228
450
|
|
|
229
|
-
|
|
451
|
+
Nested populate:
|
|
230
452
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
639
|
+
# Security Configuration
|
|
640
|
+
|
|
641
|
+
Default security settings are stored in:
|
|
642
|
+
|
|
643
|
+
```txt
|
|
644
|
+
src/security-default-config.js
|
|
645
|
+
```
|
|
256
646
|
|
|
257
|
-
|
|
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
|
-
|
|
652
|
+
"eq",
|
|
653
|
+
"ne",
|
|
654
|
+
"gt",
|
|
655
|
+
"gte",
|
|
656
|
+
"lt",
|
|
657
|
+
"lte",
|
|
658
|
+
"in",
|
|
659
|
+
"nin",
|
|
660
|
+
"regex",
|
|
661
|
+
"exists"
|
|
264
662
|
],
|
|
265
|
-
|
|
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: [
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
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
|
-
|
|
732
|
+
# ApiFeatures چیست؟
|
|
733
|
+
|
|
734
|
+
`ApiFeatures` کلاس اصلی پکیج است. این کلاس از روی `req.query` و فیلترهای دستی سمت سرور، یک MongoDB aggregation pipeline میسازد.
|
|
292
735
|
|
|
293
|
-
|
|
736
|
+
## Constructor
|
|
294
737
|
|
|
295
|
-
```
|
|
296
|
-
|
|
738
|
+
```js
|
|
739
|
+
new ApiFeatures(model, query, userRole)
|
|
297
740
|
```
|
|
298
741
|
|
|
299
|
-
|
|
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
|
|
303
|
-
.addManualFilters(
|
|
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
|
-
.
|
|
762
|
+
.paginate()
|
|
763
|
+
.execute();
|
|
307
764
|
```
|
|
308
765
|
|
|
309
|
-
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
## `filter()`
|
|
310
769
|
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
782
|
+
operatorهای مقایسهای:
|
|
783
|
+
|
|
784
|
+
```txt
|
|
785
|
+
GET /api/products?price[gte]=100&price[lte]=500
|
|
786
|
+
```
|
|
316
787
|
|
|
317
788
|
```js
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
##
|
|
874
|
+
## `search(fields)`
|
|
337
875
|
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
937
|
+
حالت mixed مجاز نیست:
|
|
938
|
+
|
|
939
|
+
```txt
|
|
940
|
+
GET /api/products?fields=name,-password
|
|
941
|
+
```
|
|
343
942
|
|
|
344
943
|
---
|
|
345
944
|
|
|
346
|
-
##
|
|
945
|
+
## `paginate()`
|
|
946
|
+
|
|
947
|
+
برای صفحهبندی استفاده میشود.
|
|
347
948
|
|
|
348
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
1127
|
+
# License
|
|
361
1128
|
|
|
362
|
-
|
|
363
|
-
* LinkedIn: [alireza-aghaee-mern-dev](https://www.linkedin.com/in/alireza-aghaee-mern-dev)
|
|
1129
|
+
MIT
|