filter-kit 2.0.0 → 2.0.1
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 +291 -139
- package/package.json +46 -9
package/README.md
CHANGED
|
@@ -1,30 +1,90 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://www.typescriptlang.org/)
|
|
3
|
+
# filter-kit
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
**The last Mongoose query builder you'll ever need.**
|
|
6
|
+
One function. Full REST API filtering — search, paginate, sort, populate, date ranges, operator filters, soft-delete — all from URL query params.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/filter-kit)
|
|
9
|
+
[](https://www.npmjs.com/package/filter-kit)
|
|
10
|
+
[](https://bundlephobia.com/package/filter-kit)
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[](https://www.typescriptlang.org/)
|
|
13
|
+
[](https://mongoosejs.com/)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Why filter-kit?
|
|
20
|
+
|
|
21
|
+
- **Zero boilerplate** — one call replaces 50+ lines of hand-written filter logic
|
|
22
|
+
- **URL-driven** — every option is controllable via `req.query`
|
|
23
|
+
- **Smart search** — regex for strings, `$eq` for numbers, exact match mode
|
|
24
|
+
- **Deep populate** — dot-notation nesting up to 4 levels (`user.role.department.company`)
|
|
25
|
+
- **Operator filters** — `_gte`, `_lte`, `_gt`, `_lt`, `_ne`, `_in`, `_nin`, `_exists`, `_regex` via URL
|
|
26
|
+
- **Multi-sort** — `?sort=createdAt,name&order=desc,asc`
|
|
27
|
+
- **Soft-delete guard** — auto-exclude deleted documents with one option
|
|
28
|
+
- **Security gate** — unknown filter keys are silently dropped
|
|
29
|
+
- **Dual CJS + ESM** — works in Node.js `require` and bundler `import`
|
|
30
|
+
- **Full TypeScript** — generic types, zero `@types/` install needed
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Demo
|
|
35
|
+
|
|
36
|
+
> **Record a quick GIF** of your terminal or API response and drop it here.
|
|
37
|
+
> Upload to `assets/demo.gif` in your GitHub repo, then replace this line:
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
**Example — what one URL does:**
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
GET /users?search=john&role=admin&age_gte=18&sort=createdAt,name&order=desc,asc&page=2&limit=10&populate=user.role&from=2024-01-01
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"data": [
|
|
50
|
+
{ "_id": "...", "name": "John Admin", "role": { "name": "Administrator" }, "age": 28 }
|
|
51
|
+
],
|
|
52
|
+
"pagination": {
|
|
53
|
+
"total": 47,
|
|
54
|
+
"page": 2,
|
|
55
|
+
"limit": 10,
|
|
56
|
+
"pages": 5,
|
|
57
|
+
"hasNext": true,
|
|
58
|
+
"hasPrev": true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
8
64
|
|
|
9
65
|
## Install
|
|
10
66
|
|
|
11
67
|
```bash
|
|
12
|
-
npm install
|
|
68
|
+
npm install filter-kit
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
yarn add filter-kit
|
|
13
73
|
```
|
|
14
74
|
|
|
15
|
-
Mongoose 6.
|
|
75
|
+
Mongoose `>=6.0.0` required as a peer dependency:
|
|
16
76
|
|
|
17
77
|
```bash
|
|
18
78
|
npm install mongoose
|
|
19
79
|
```
|
|
20
80
|
|
|
21
|
-
|
|
81
|
+
---
|
|
22
82
|
|
|
23
|
-
|
|
83
|
+
## Quick Start
|
|
24
84
|
|
|
85
|
+
**CommonJS**
|
|
25
86
|
```javascript
|
|
26
|
-
const { queryBuilder } = require('
|
|
27
|
-
const User = require('./models/User');
|
|
87
|
+
const { queryBuilder } = require('filter-kit');
|
|
28
88
|
|
|
29
89
|
app.get('/users', async (req, res) => {
|
|
30
90
|
const result = await queryBuilder({
|
|
@@ -38,11 +98,9 @@ app.get('/users', async (req, res) => {
|
|
|
38
98
|
});
|
|
39
99
|
```
|
|
40
100
|
|
|
41
|
-
**ESM
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
import { queryBuilder } from 'mongoose-filter-kit';
|
|
45
|
-
import User from './models/User.js';
|
|
101
|
+
**ESM / TypeScript**
|
|
102
|
+
```typescript
|
|
103
|
+
import { queryBuilder } from 'filter-kit';
|
|
46
104
|
|
|
47
105
|
app.get('/users', async (req, res) => {
|
|
48
106
|
const result = await queryBuilder({
|
|
@@ -50,97 +108,200 @@ app.get('/users', async (req, res) => {
|
|
|
50
108
|
query: req.query,
|
|
51
109
|
searchFields: ['name', 'email'],
|
|
52
110
|
allowedFilters: ['role', 'status'],
|
|
53
|
-
|
|
111
|
+
defaultSort: 'createdAt',
|
|
112
|
+
defaultOrder: 'desc',
|
|
113
|
+
populate: 'role',
|
|
114
|
+
dateField: 'createdAt',
|
|
115
|
+
softDelete: { field: 'isDeleted' },
|
|
54
116
|
});
|
|
55
117
|
res.json(result);
|
|
56
118
|
});
|
|
57
119
|
```
|
|
58
120
|
|
|
59
|
-
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Features
|
|
124
|
+
|
|
125
|
+
### Smart Search — strings, numbers, exact
|
|
60
126
|
|
|
61
127
|
```typescript
|
|
62
|
-
|
|
63
|
-
|
|
128
|
+
searchFields: [
|
|
129
|
+
'name', // string → case-insensitive regex
|
|
130
|
+
'email', // string → case-insensitive regex
|
|
131
|
+
{ field: 'age', type: 'number' }, // number → $eq (skips non-numeric search)
|
|
132
|
+
{ field: 'code', type: 'exact' }, // string → exact match, no regex
|
|
133
|
+
]
|
|
134
|
+
```
|
|
64
135
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
query: req.query,
|
|
70
|
-
searchFields: ['name', 'email'],
|
|
71
|
-
allowedFilters: ['role', 'status'],
|
|
72
|
-
defaultSort: 'createdAt',
|
|
73
|
-
defaultOrder: 'desc',
|
|
74
|
-
defaultLimit: 10,
|
|
75
|
-
maxLimit: 100,
|
|
76
|
-
populate: 'role',
|
|
77
|
-
dateField: 'createdAt',
|
|
78
|
-
customFilter: { isDeleted: false },
|
|
79
|
-
});
|
|
136
|
+
```
|
|
137
|
+
?search=john → { $or: [ { name: /john/i }, { email: /john/i } ] }
|
|
138
|
+
?search=42 → { $or: [ { name: /42/i }, { age: { $eq: 42 } } ] }
|
|
139
|
+
```
|
|
80
140
|
|
|
81
|
-
|
|
82
|
-
|
|
141
|
+
**AND mode** — every space-separated word must match at least one field:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
searchMode: 'and'
|
|
145
|
+
// ?search=john admin
|
|
146
|
+
// → { $and: [
|
|
147
|
+
// { $or: [{ name: /john/i }, { email: /john/i }] },
|
|
148
|
+
// { $or: [{ name: /admin/i }, { email: /admin/i }] }
|
|
149
|
+
// ]}
|
|
83
150
|
```
|
|
84
151
|
|
|
85
|
-
|
|
152
|
+
---
|
|
86
153
|
|
|
87
|
-
|
|
154
|
+
### Deep Populate — up to 4 levels
|
|
155
|
+
|
|
156
|
+
Use dot-notation in the `?populate=` query param or the `populate` option:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
?populate=user.role.department.company
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
// Produces:
|
|
88
164
|
{
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
"pages": 5,
|
|
97
|
-
"hasNext": true,
|
|
98
|
-
"hasPrev": false
|
|
165
|
+
path: 'user',
|
|
166
|
+
populate: {
|
|
167
|
+
path: 'role',
|
|
168
|
+
populate: {
|
|
169
|
+
path: 'department',
|
|
170
|
+
populate: { path: 'company' }
|
|
171
|
+
}
|
|
99
172
|
}
|
|
100
173
|
}
|
|
101
174
|
```
|
|
102
175
|
|
|
103
|
-
|
|
176
|
+
Mix flat and deep in one request:
|
|
177
|
+
```
|
|
178
|
+
?populate=company,user.role
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Operator Filters — via URL
|
|
184
|
+
|
|
185
|
+
Append a suffix to any allowed field name in the URL:
|
|
186
|
+
|
|
187
|
+
| Suffix | MongoDB Op | Example URL | Result |
|
|
188
|
+
|--------|-----------|-------------|--------|
|
|
189
|
+
| `_gte` | `$gte` | `?age_gte=18` | `{ age: { $gte: 18 } }` |
|
|
190
|
+
| `_lte` | `$lte` | `?age_lte=65` | `{ age: { $lte: 65 } }` |
|
|
191
|
+
| `_gt` | `$gt` | `?price_gt=100` | `{ price: { $gt: 100 } }` |
|
|
192
|
+
| `_lt` | `$lt` | `?price_lt=500` | `{ price: { $lt: 500 } }` |
|
|
193
|
+
| `_ne` | `$ne` | `?status_ne=deleted` | `{ status: { $ne: 'deleted' } }` |
|
|
194
|
+
| `_in` | `$in` | `?role_in=admin,editor` | `{ role: { $in: ['admin','editor'] } }` |
|
|
195
|
+
| `_nin` | `$nin` | `?role_nin=banned,guest` | `{ role: { $nin: ['banned','guest'] } }` |
|
|
196
|
+
| `_exists` | `$exists` | `?avatar_exists=false` | `{ avatar: { $exists: false } }` |
|
|
197
|
+
| `_regex` | `$regex` | `?name_regex=^john` | `{ name: { $regex: /^john/i } }` |
|
|
198
|
+
|
|
199
|
+
Multiple operators on the same field are **merged automatically**:
|
|
200
|
+
```
|
|
201
|
+
?age_gte=18&age_lte=65 → { age: { $gte: 18, $lte: 65 } }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### Multi-Sort
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
?sort=createdAt,name&order=desc,asc
|
|
210
|
+
→ { createdAt: -1, name: 1 }
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
### Multiple Date Ranges
|
|
216
|
+
|
|
217
|
+
Filter different date fields with custom param names:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
dateFields: [
|
|
221
|
+
{ field: 'createdAt', fromParam: 'createdFrom', toParam: 'createdTo' },
|
|
222
|
+
{ field: 'updatedAt', fromParam: 'updatedFrom', toParam: 'updatedTo' },
|
|
223
|
+
]
|
|
224
|
+
// ?createdFrom=2024-01-01&updatedFrom=2024-06-01
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### Soft Delete Guard
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
softDelete: { field: 'isDeleted' } // excludes { isDeleted: true }
|
|
233
|
+
softDelete: { field: 'deletedAt', value: null } // excludes { deletedAt: null }
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Auto-merges into every query via `$and`. Cannot be bypassed by URL params.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### Skip Count for Performance
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
withCount: false // skips countDocuments — faster for infinite scroll / cursor UI
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Full Options Reference
|
|
104
249
|
|
|
105
250
|
| Option | Type | Default | Description |
|
|
106
|
-
|
|
107
|
-
| `model` | `Model<T>` | **required** |
|
|
108
|
-
| `query` | `Record<string, any>` | **required** | Parsed query string
|
|
109
|
-
| `searchFields` | `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
251
|
+
|--------|------|---------|-------------|
|
|
252
|
+
| `model` | `Model<T>` | **required** | Mongoose model to query. |
|
|
253
|
+
| `query` | `Record<string, any>` | **required** | Parsed query string (`req.query`). |
|
|
254
|
+
| `searchFields` | `SearchField[]` | `undefined` | Fields for `?search=`. Strings = regex, `{ type: 'number' }` = `$eq`, `{ type: 'exact' }` = literal. |
|
|
255
|
+
| `searchMode` | `'or' \| 'and'` | `'or'` | `'or'` — any field matches. `'and'` — every space-split term must match. |
|
|
256
|
+
| `allowedFilters` | `string[]` | `undefined` | Extra keys allowed as dynamic filters beyond schema paths. |
|
|
257
|
+
| `defaultSort` | `string` | `'createdAt'` | Sort field when `?sort=` absent. |
|
|
258
|
+
| `defaultOrder` | `'asc' \| 'desc'` | `'desc'` | Sort direction when `?order=` absent. |
|
|
259
|
+
| `defaultLimit` | `number` | `10` | Page size when `?limit=` absent. |
|
|
260
|
+
| `maxLimit` | `number` | `100` | Hard cap on page size. |
|
|
261
|
+
| `populate` | `string \| string[] \| PopulateOptions \| PopulateOptions[]` | `undefined` | Populate when `?populate=` absent. |
|
|
262
|
+
| `select` | `ProjectionType<T> \| string` | `undefined` | Field projection when `?select=` absent. |
|
|
263
|
+
| `dateField` | `string` | `undefined` | Single date field for `?from=` / `?to=`. |
|
|
264
|
+
| `dateFields` | `DateFieldConfig[]` | `undefined` | Multiple date fields with custom param names. |
|
|
265
|
+
| `lean` | `boolean` | `false` | Return plain JS objects (faster reads). |
|
|
266
|
+
| `customFilter` | `Record<string, any>` | `undefined` | Always-on filter, merged via `$and`. Cannot be bypassed by URL. |
|
|
267
|
+
| `softDelete` | `SoftDeleteConfig` | `undefined` | Auto-exclude soft-deleted docs (`{ field, value? }`). |
|
|
268
|
+
| `withCount` | `boolean` | `true` | Set `false` to skip `countDocuments` for perf. |
|
|
269
|
+
|
|
270
|
+
---
|
|
120
271
|
|
|
121
272
|
## Query Parameters Reference
|
|
122
273
|
|
|
123
|
-
| Param |
|
|
124
|
-
|
|
125
|
-
| `search` |
|
|
126
|
-
| `page` |
|
|
127
|
-
| `limit` |
|
|
128
|
-
| `sort` |
|
|
129
|
-
| `order` |
|
|
130
|
-
| `from` |
|
|
131
|
-
| `to` |
|
|
132
|
-
| `populate` |
|
|
133
|
-
| `select` |
|
|
134
|
-
| `<field>` |
|
|
274
|
+
| Param | Example | Description |
|
|
275
|
+
|-------|---------|-------------|
|
|
276
|
+
| `search` | `?search=john` | Case-insensitive search across `searchFields`. |
|
|
277
|
+
| `page` | `?page=2` | Page number (1-based). |
|
|
278
|
+
| `limit` | `?limit=25` | Docs per page (clamped to `maxLimit`). |
|
|
279
|
+
| `sort` | `?sort=name,age` | Comma-separated sort fields. |
|
|
280
|
+
| `order` | `?order=asc,desc` | Per-field order, matches `sort` positions. |
|
|
281
|
+
| `from` | `?from=2024-01-01` | Date range start (`$gte`) on `dateField`. |
|
|
282
|
+
| `to` | `?to=2024-12-31` | Date range end (`$lte`, end of day). |
|
|
283
|
+
| `populate` | `?populate=user.role,company` | Comma-separated paths. Dot = nested populate. |
|
|
284
|
+
| `select` | `?select=name,email` | Comma-separated fields to include. |
|
|
285
|
+
| `<field>` | `?status=active` | Dynamic filter (schema path or `allowedFilters`). |
|
|
286
|
+
| `<field>_gte` | `?age_gte=18` | Greater-than-or-equal operator filter. |
|
|
287
|
+
| `<field>_lte` | `?age_lte=65` | Less-than-or-equal operator filter. |
|
|
288
|
+
| `<field>_gt` | `?price_gt=99` | Greater-than operator filter. |
|
|
289
|
+
| `<field>_lt` | `?price_lt=500` | Less-than operator filter. |
|
|
290
|
+
| `<field>_ne` | `?status_ne=deleted` | Not-equal operator filter. |
|
|
291
|
+
| `<field>_in` | `?role_in=admin,mod` | `$in` array filter. |
|
|
292
|
+
| `<field>_nin` | `?role_nin=banned` | `$nin` array filter. |
|
|
293
|
+
| `<field>_exists` | `?photo_exists=false` | Field existence filter. |
|
|
294
|
+
| `<field>_regex` | `?name_regex=^A` | Regex filter (case-insensitive). |
|
|
295
|
+
|
|
296
|
+
---
|
|
135
297
|
|
|
136
298
|
## Examples
|
|
137
299
|
|
|
138
|
-
###
|
|
300
|
+
### Express
|
|
139
301
|
|
|
140
302
|
```typescript
|
|
141
303
|
import express from 'express';
|
|
142
|
-
import
|
|
143
|
-
import { queryBuilder } from 'mongoose-filter-kit';
|
|
304
|
+
import { queryBuilder } from 'filter-kit';
|
|
144
305
|
import Product from './models/Product';
|
|
145
306
|
|
|
146
307
|
const app = express();
|
|
@@ -150,7 +311,11 @@ app.get('/products', async (req, res, next) => {
|
|
|
150
311
|
const result = await queryBuilder({
|
|
151
312
|
model: Product,
|
|
152
313
|
query: req.query,
|
|
153
|
-
searchFields: [
|
|
314
|
+
searchFields: [
|
|
315
|
+
'name',
|
|
316
|
+
'description',
|
|
317
|
+
{ field: 'price', type: 'number' },
|
|
318
|
+
],
|
|
154
319
|
allowedFilters: ['category', 'brand', 'inStock'],
|
|
155
320
|
defaultSort: 'createdAt',
|
|
156
321
|
defaultOrder: 'desc',
|
|
@@ -159,6 +324,7 @@ app.get('/products', async (req, res, next) => {
|
|
|
159
324
|
dateField: 'createdAt',
|
|
160
325
|
populate: [{ path: 'category', select: 'name slug' }],
|
|
161
326
|
customFilter: { isActive: true },
|
|
327
|
+
softDelete: { field: 'isDeleted' },
|
|
162
328
|
lean: true,
|
|
163
329
|
});
|
|
164
330
|
|
|
@@ -169,13 +335,22 @@ app.get('/products', async (req, res, next) => {
|
|
|
169
335
|
});
|
|
170
336
|
```
|
|
171
337
|
|
|
172
|
-
|
|
338
|
+
Client calls:
|
|
339
|
+
```
|
|
340
|
+
GET /products?search=shoe&price_gte=50&price_lte=200&brand=nike,adidas&sort=price,name&order=asc,asc&page=1&limit=20
|
|
341
|
+
GET /products?category=electronics&inStock=true&createdAt_exists=true
|
|
342
|
+
GET /products?name_regex=^air&sort=createdAt&order=desc
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
### NestJS
|
|
173
348
|
|
|
174
349
|
```typescript
|
|
175
350
|
import { Controller, Get, Query } from '@nestjs/common';
|
|
176
351
|
import { InjectModel } from '@nestjs/mongoose';
|
|
177
352
|
import { Model } from 'mongoose';
|
|
178
|
-
import { queryBuilder, QueryBuilderResult } from '
|
|
353
|
+
import { queryBuilder, QueryBuilderResult } from 'filter-kit';
|
|
179
354
|
import { User, UserDocument } from './user.schema';
|
|
180
355
|
|
|
181
356
|
@Controller('users')
|
|
@@ -190,20 +365,19 @@ export class UsersController {
|
|
|
190
365
|
model: this.userModel,
|
|
191
366
|
query,
|
|
192
367
|
searchFields: ['name', 'email'],
|
|
368
|
+
searchMode: 'and',
|
|
193
369
|
allowedFilters: ['role', 'status', 'department'],
|
|
194
|
-
|
|
195
|
-
defaultOrder: 'desc',
|
|
196
|
-
defaultLimit: 10,
|
|
197
|
-
maxLimit: 100,
|
|
198
|
-
populate: 'role',
|
|
370
|
+
populate: 'role.permissions', // level 2 deep
|
|
199
371
|
dateField: 'createdAt',
|
|
200
|
-
|
|
372
|
+
softDelete: { field: 'isDeleted' },
|
|
201
373
|
});
|
|
202
374
|
}
|
|
203
375
|
}
|
|
204
376
|
```
|
|
205
377
|
|
|
206
|
-
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### Advanced — Orders with multiple date ranges
|
|
207
381
|
|
|
208
382
|
```typescript
|
|
209
383
|
const result = await queryBuilder({
|
|
@@ -211,64 +385,39 @@ const result = await queryBuilder({
|
|
|
211
385
|
query: req.query,
|
|
212
386
|
searchFields: ['orderNumber', 'customerName'],
|
|
213
387
|
allowedFilters: ['status', 'paymentMethod'],
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
dateField: 'placedAt',
|
|
388
|
+
dateFields: [
|
|
389
|
+
{ field: 'placedAt', fromParam: 'placedFrom', toParam: 'placedTo' },
|
|
390
|
+
{ field: 'shippedAt', fromParam: 'shippedFrom', toParam: 'shippedTo' },
|
|
391
|
+
],
|
|
219
392
|
populate: [
|
|
220
393
|
{ path: 'customer', select: 'name email phone' },
|
|
221
394
|
{ path: 'items.product', select: 'name sku price' },
|
|
222
395
|
],
|
|
223
|
-
|
|
224
|
-
|
|
396
|
+
softDelete: { field: 'isArchived', value: true },
|
|
397
|
+
withCount: false, // infinite scroll — skip the count query
|
|
225
398
|
lean: true,
|
|
226
399
|
});
|
|
227
400
|
```
|
|
228
401
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
```bash
|
|
232
|
-
GET /orders?from=2024-01-01&to=2024-03-31&status=pending,processing&isPaid=false&page=1&limit=25
|
|
402
|
+
Client call:
|
|
233
403
|
```
|
|
234
|
-
|
|
235
|
-
```typescript
|
|
236
|
-
const result = await queryBuilder({
|
|
237
|
-
model: Order,
|
|
238
|
-
query: req.query, // { from, to, status, isPaid, page, limit }
|
|
239
|
-
allowedFilters: ['isPaid'],
|
|
240
|
-
dateField: 'placedAt',
|
|
241
|
-
// status is also allowed because it exists on the schema
|
|
242
|
-
});
|
|
243
|
-
// Produces: { $and: [
|
|
244
|
-
// { placedAt: { $gte: ..., $lte: ... } },
|
|
245
|
-
// { status: { $in: ['pending', 'processing'] }, isPaid: false }
|
|
246
|
-
// ]}
|
|
404
|
+
GET /orders?placedFrom=2024-01-01&placedTo=2024-03-31&status=pending,processing&paymentMethod_ne=cash&sort=placedAt&order=desc
|
|
247
405
|
```
|
|
248
406
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Dynamic filters let clients filter by any model field directly via the URL — but with a security gate to prevent arbitrary injection:
|
|
407
|
+
---
|
|
252
408
|
|
|
253
|
-
|
|
409
|
+
## Security
|
|
254
410
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
411
|
+
- **Reserved params** (`page`, `limit`, `sort`, `order`, `search`, `from`, `to`, `populate`, `select`) are never treated as field filters.
|
|
412
|
+
- **Unknown keys** are silently dropped — not in `allowedFilters` and not in `model.schema.paths` = ignored.
|
|
413
|
+
- **`customFilter` and `softDelete`** are always applied server-side — URL params cannot override them.
|
|
414
|
+
- Operator suffixes (`_gte`, etc.) are only applied when the **base field** passes the security gate.
|
|
258
415
|
|
|
259
|
-
|
|
416
|
+
---
|
|
260
417
|
|
|
261
|
-
|
|
262
|
-
- `?status=active,inactive` → `{ status: { $in: ['active', 'inactive'] } }`
|
|
263
|
-
- `?isVerified=true` → `{ isVerified: true }` (boolean)
|
|
264
|
-
- `?isVerified=false` → `{ isVerified: false }` (boolean)
|
|
265
|
-
- Everything else is passed through as-is.
|
|
418
|
+
## TypeScript
|
|
266
419
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
## TypeScript Support
|
|
270
|
-
|
|
271
|
-
Full types ship with the package — no `@types/` install needed. All types are exported:
|
|
420
|
+
Full types ship with the package — no `@types/` install needed.
|
|
272
421
|
|
|
273
422
|
```typescript
|
|
274
423
|
import {
|
|
@@ -276,19 +425,22 @@ import {
|
|
|
276
425
|
QueryBuilderOptions,
|
|
277
426
|
QueryBuilderResult,
|
|
278
427
|
PaginationMeta,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
428
|
+
SearchField,
|
|
429
|
+
SearchFieldConfig,
|
|
430
|
+
SearchFieldType,
|
|
431
|
+
DateFieldConfig,
|
|
432
|
+
SoftDeleteConfig,
|
|
433
|
+
} from 'filter-kit';
|
|
434
|
+
|
|
435
|
+
// Generic — result.data is UserDocument[]
|
|
285
436
|
const result = await queryBuilder<UserDocument>({
|
|
286
437
|
model: User,
|
|
287
438
|
query: req.query,
|
|
288
439
|
});
|
|
289
|
-
// result.data is UserDocument[]
|
|
290
440
|
```
|
|
291
441
|
|
|
442
|
+
---
|
|
443
|
+
|
|
292
444
|
## License
|
|
293
445
|
|
|
294
|
-
MIT
|
|
446
|
+
[MIT](https://opensource.org/licenses/MIT) © [parthpatel2597](https://www.npmjs.com/~parthpatel2597)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "filter-kit",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "Powerful Mongoose query builder — smart search (string + number), deep populate (4 levels), multi-sort, operator filters, date ranges, soft-delete, pagination",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Powerful Mongoose query builder — smart search (string + number), deep populate (4 levels), multi-sort, operator filters (_gte _lte _ne _in _regex), date ranges, soft-delete, pagination. One function for full REST API filtering from URL query params.",
|
|
5
5
|
"main": "dist/cjs/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
7
7
|
"types": "dist/cjs/index.d.ts",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
-
"files": ["dist"],
|
|
20
|
+
"files": ["dist", "README.md"],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
23
23
|
"build": "npm run clean && npm run build:cjs && npm run build:esm && node scripts/postbuild.js",
|
|
@@ -31,14 +31,51 @@
|
|
|
31
31
|
"prepublishOnly": "npm run build"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
34
|
+
"filter-kit",
|
|
35
|
+
"mongoose",
|
|
36
|
+
"mongodb",
|
|
37
|
+
"query-builder",
|
|
38
|
+
"mongoose-query-builder",
|
|
39
|
+
"mongodb-query-builder",
|
|
40
|
+
"filter",
|
|
41
|
+
"pagination",
|
|
42
|
+
"mongoose-pagination",
|
|
43
|
+
"mongodb-pagination",
|
|
44
|
+
"search",
|
|
45
|
+
"mongoose-search",
|
|
46
|
+
"sorting",
|
|
47
|
+
"multi-sort",
|
|
48
|
+
"populate",
|
|
49
|
+
"deep-populate",
|
|
50
|
+
"date-filter",
|
|
51
|
+
"dynamic-filter",
|
|
52
|
+
"operator-filter",
|
|
53
|
+
"soft-delete",
|
|
54
|
+
"rest-api",
|
|
55
|
+
"express",
|
|
56
|
+
"nestjs",
|
|
57
|
+
"typescript",
|
|
58
|
+
"mongoose-utils",
|
|
59
|
+
"api-query",
|
|
60
|
+
"url-filter",
|
|
61
|
+
"mongodb-filter"
|
|
39
62
|
],
|
|
40
|
-
"author":
|
|
63
|
+
"author": {
|
|
64
|
+
"name": "parthpatel2597",
|
|
65
|
+
"url": "https://www.npmjs.com/~parthpatel2597"
|
|
66
|
+
},
|
|
41
67
|
"license": "MIT",
|
|
68
|
+
"homepage": "https://www.npmjs.com/package/filter-kit",
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/parthpatel2597/filter-kit"
|
|
72
|
+
},
|
|
73
|
+
"bugs": {
|
|
74
|
+
"url": "https://github.com/parthpatel2597/filter-kit/issues"
|
|
75
|
+
},
|
|
76
|
+
"engines": {
|
|
77
|
+
"node": ">=16.0.0"
|
|
78
|
+
},
|
|
42
79
|
"peerDependencies": { "mongoose": ">=6.0.0" },
|
|
43
80
|
"devDependencies": {
|
|
44
81
|
"@types/jest": "^29.5.0",
|