@nestarc/pagination 0.1.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/LICENSE +21 -0
- package/README.md +334 -0
- package/dist/cursor/cursor.encoder.d.ts +2 -0
- package/dist/cursor/cursor.encoder.js +25 -0
- package/dist/decorators/api-paginated-response.decorator.d.ts +3 -0
- package/dist/decorators/api-paginated-response.decorator.js +91 -0
- package/dist/decorators/paginate-defaults.decorator.d.ts +9 -0
- package/dist/decorators/paginate-defaults.decorator.js +7 -0
- package/dist/decorators/paginate.decorator.d.ts +1 -0
- package/dist/decorators/paginate.decorator.js +9 -0
- package/dist/errors/invalid-cursor.error.d.ts +4 -0
- package/dist/errors/invalid-cursor.error.js +11 -0
- package/dist/errors/invalid-filter-column.error.d.ts +4 -0
- package/dist/errors/invalid-filter-column.error.js +14 -0
- package/dist/errors/invalid-sort-column.error.d.ts +4 -0
- package/dist/errors/invalid-sort-column.error.js +11 -0
- package/dist/filter/filter-parser.d.ts +2 -0
- package/dist/filter/filter-parser.js +89 -0
- package/dist/filter/search-builder.d.ts +1 -0
- package/dist/filter/search-builder.js +13 -0
- package/dist/filter/sort-builder.d.ts +3 -0
- package/dist/filter/sort-builder.js +25 -0
- package/dist/helpers/link-builder.d.ts +13 -0
- package/dist/helpers/link-builder.js +71 -0
- package/dist/helpers/type-coercion.d.ts +1 -0
- package/dist/helpers/type-coercion.js +15 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +31 -0
- package/dist/interfaces/filter-operator.type.d.ts +2 -0
- package/dist/interfaces/filter-operator.type.js +2 -0
- package/dist/interfaces/paginate-config.interface.d.ts +29 -0
- package/dist/interfaces/paginate-config.interface.js +2 -0
- package/dist/interfaces/paginate-query.interface.d.ts +11 -0
- package/dist/interfaces/paginate-query.interface.js +2 -0
- package/dist/interfaces/paginated.interface.d.ts +39 -0
- package/dist/interfaces/paginated.interface.js +2 -0
- package/dist/interfaces/pagination-options.interface.d.ts +15 -0
- package/dist/interfaces/pagination-options.interface.js +2 -0
- package/dist/paginate.d.ts +7 -0
- package/dist/paginate.js +179 -0
- package/dist/paginate.service.d.ts +15 -0
- package/dist/paginate.service.js +59 -0
- package/dist/pagination.constants.d.ts +5 -0
- package/dist/pagination.constants.js +8 -0
- package/dist/pagination.module.d.ts +6 -0
- package/dist/pagination.module.js +52 -0
- package/dist/pipes/paginate-query.pipe.d.ts +2 -0
- package/dist/pipes/paginate-query.pipe.js +43 -0
- package/dist/testing/create-paginate-query.d.ts +2 -0
- package/dist/testing/create-paginate-query.js +9 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +7 -0
- package/dist/testing/test-pagination.module.d.ts +5 -0
- package/dist/testing/test-pagination.module.js +30 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nestarc
|
|
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,334 @@
|
|
|
1
|
+
# @nestarc/pagination
|
|
2
|
+
|
|
3
|
+
Prisma cursor & offset pagination for NestJS with filtering, sorting, search, and Swagger auto-documentation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Offset + cursor** pagination in a single API
|
|
8
|
+
- **12 filter operators**: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$ilike`, `$btw`, `$null`, `$not:null`
|
|
9
|
+
- **Multi-column sorting** with null positioning
|
|
10
|
+
- **Full-text search** across multiple columns
|
|
11
|
+
- **Column/operator whitelisting** for security
|
|
12
|
+
- **Swagger** auto-documentation (optional)
|
|
13
|
+
- **Standalone** `paginate()` function — works without NestJS
|
|
14
|
+
- Compatible with `@nestarc/tenancy` (RLS) and `@nestarc/soft-delete` via Prisma extension chain
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @nestarc/pagination
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Peer dependencies: `@nestjs/common`, `@nestjs/core`, `@prisma/client`, `reflect-metadata`, `rxjs`
|
|
25
|
+
|
|
26
|
+
### 1. Register the module
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { PaginationModule } from '@nestarc/pagination';
|
|
30
|
+
|
|
31
|
+
@Module({
|
|
32
|
+
imports: [
|
|
33
|
+
PaginationModule.forRoot({
|
|
34
|
+
defaultLimit: 20,
|
|
35
|
+
maxLimit: 100,
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
export class AppModule {}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Use in a controller
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { Paginate, PaginateQuery, ApiPaginatedResponse } from '@nestarc/pagination';
|
|
46
|
+
|
|
47
|
+
@Controller('users')
|
|
48
|
+
export class UserController {
|
|
49
|
+
constructor(private readonly userService: UserService) {}
|
|
50
|
+
|
|
51
|
+
@Get()
|
|
52
|
+
@ApiPaginatedResponse(UserDto)
|
|
53
|
+
async findAll(@Paginate() query: PaginateQuery) {
|
|
54
|
+
return this.userService.findAll(query);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 3. Use in a service
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { paginate, PaginateQuery, PaginateConfig, Paginated } from '@nestarc/pagination';
|
|
63
|
+
|
|
64
|
+
@Injectable()
|
|
65
|
+
export class UserService {
|
|
66
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
67
|
+
|
|
68
|
+
async findAll(query: PaginateQuery): Promise<Paginated<User>> {
|
|
69
|
+
return paginate(query, this.prisma.user, {
|
|
70
|
+
sortableColumns: ['id', 'name', 'email', 'createdAt'],
|
|
71
|
+
defaultSortBy: [['createdAt', 'DESC']],
|
|
72
|
+
searchableColumns: ['name', 'email'],
|
|
73
|
+
filterableColumns: {
|
|
74
|
+
role: ['$eq', '$in'],
|
|
75
|
+
createdAt: ['$gte', '$lte'],
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Query Parameters
|
|
83
|
+
|
|
84
|
+
### Offset
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
GET /users?page=2&limit=20&sortBy=createdAt:DESC&search=john&filter.role=$eq:admin
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
| Param | Description | Example |
|
|
91
|
+
|-------|-------------|---------|
|
|
92
|
+
| `page` | Page number (1-based) | `2` |
|
|
93
|
+
| `limit` | Items per page | `20` |
|
|
94
|
+
| `sortBy` | Sort (multi allowed) | `createdAt:DESC` |
|
|
95
|
+
| `search` | Full-text search | `john` |
|
|
96
|
+
| `filter.{col}` | Filter by column | `filter.role=$eq:admin` |
|
|
97
|
+
|
|
98
|
+
### Cursor
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
GET /users?limit=20&after=eyJpZCI6IjEwIn0&sortBy=createdAt:DESC
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| Param | Description | Example |
|
|
105
|
+
|-------|-------------|---------|
|
|
106
|
+
| `limit` | Items per page | `20` |
|
|
107
|
+
| `after` | Forward cursor (Base64url) | `eyJpZCI6IjEwIn0` |
|
|
108
|
+
| `before` | Backward cursor | `eyJpZCI6NX0` |
|
|
109
|
+
| `sortBy` | Sort | `createdAt:DESC` |
|
|
110
|
+
|
|
111
|
+
Cursor mode activates automatically when `after`/`before` is present or `paginationType: 'cursor'` is set.
|
|
112
|
+
|
|
113
|
+
## Filter Operators
|
|
114
|
+
|
|
115
|
+
| Operator | Prisma | Example |
|
|
116
|
+
|----------|--------|---------|
|
|
117
|
+
| `$eq` | `{ equals }` | `filter.role=$eq:admin` |
|
|
118
|
+
| `$ne` | `{ not }` | `filter.status=$ne:deleted` |
|
|
119
|
+
| `$gt` | `{ gt }` | `filter.age=$gt:18` |
|
|
120
|
+
| `$gte` | `{ gte }` | `filter.age=$gte:18` |
|
|
121
|
+
| `$lt` | `{ lt }` | `filter.price=$lt:100` |
|
|
122
|
+
| `$lte` | `{ lte }` | `filter.price=$lte:100` |
|
|
123
|
+
| `$in` | `{ in }` | `filter.role=$in:admin,user` |
|
|
124
|
+
| `$nin` | `{ notIn }` | `filter.role=$nin:banned` |
|
|
125
|
+
| `$ilike` | `{ contains, mode: 'insensitive' }` | `filter.name=$ilike:john` |
|
|
126
|
+
| `$btw` | `{ gte, lte }` | `filter.price=$btw:10,100` |
|
|
127
|
+
| `$null` | `null` | `filter.deletedAt=$null` |
|
|
128
|
+
| `$not:null` | `{ not: null }` | `filter.verifiedAt=$not:null` |
|
|
129
|
+
|
|
130
|
+
## Response Format
|
|
131
|
+
|
|
132
|
+
### Offset
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"data": [{ "id": "1", "name": "Alice" }],
|
|
137
|
+
"meta": {
|
|
138
|
+
"itemsPerPage": 20,
|
|
139
|
+
"totalItems": 500,
|
|
140
|
+
"currentPage": 1,
|
|
141
|
+
"totalPages": 25,
|
|
142
|
+
"sortBy": [["createdAt", "DESC"]]
|
|
143
|
+
},
|
|
144
|
+
"links": {
|
|
145
|
+
"first": "/users?page=1&limit=20&sortBy=createdAt%3ADESC",
|
|
146
|
+
"previous": null,
|
|
147
|
+
"current": "/users?page=1&limit=20&sortBy=createdAt%3ADESC",
|
|
148
|
+
"next": "/users?page=2&limit=20&sortBy=createdAt%3ADESC",
|
|
149
|
+
"last": "/users?page=25&limit=20&sortBy=createdAt%3ADESC"
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Cursor
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"data": [{ "id": "10", "name": "Bob" }],
|
|
159
|
+
"meta": {
|
|
160
|
+
"itemsPerPage": 20,
|
|
161
|
+
"hasNextPage": true,
|
|
162
|
+
"hasPreviousPage": true,
|
|
163
|
+
"startCursor": "eyJpZCI6IjEwIn0",
|
|
164
|
+
"endCursor": "eyJpZCI6IjI5In0",
|
|
165
|
+
"sortBy": [["createdAt", "DESC"]]
|
|
166
|
+
},
|
|
167
|
+
"links": {
|
|
168
|
+
"current": "/users?limit=20&after=eyJpZCI6IjEwIn0",
|
|
169
|
+
"next": "/users?limit=20&after=eyJpZCI6IjI5In0",
|
|
170
|
+
"previous": "/users?limit=20&before=eyJpZCI6IjEwIn0"
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## PaginateConfig
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const config: PaginateConfig<User> = {
|
|
179
|
+
// Required
|
|
180
|
+
sortableColumns: ['id', 'name', 'email', 'createdAt'],
|
|
181
|
+
|
|
182
|
+
// Sorting
|
|
183
|
+
defaultSortBy: [['createdAt', 'DESC']],
|
|
184
|
+
nullSort: 'last',
|
|
185
|
+
|
|
186
|
+
// Search
|
|
187
|
+
searchableColumns: ['name', 'email'],
|
|
188
|
+
|
|
189
|
+
// Filtering
|
|
190
|
+
filterableColumns: {
|
|
191
|
+
role: ['$eq', '$in'],
|
|
192
|
+
age: ['$gt', '$gte', '$lt', '$lte'],
|
|
193
|
+
createdAt: ['$gte', '$lte', '$btw'],
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Relations (Prisma include)
|
|
197
|
+
relations: { profile: true },
|
|
198
|
+
|
|
199
|
+
// Column selection (Prisma select)
|
|
200
|
+
select: ['id', 'name', 'email'],
|
|
201
|
+
|
|
202
|
+
// Pagination
|
|
203
|
+
paginationType: 'offset', // 'offset' | 'cursor'
|
|
204
|
+
cursorColumn: 'id', // default: 'id'
|
|
205
|
+
defaultLimit: 20,
|
|
206
|
+
maxLimit: 100,
|
|
207
|
+
withTotalCount: false, // cursor mode: include total count
|
|
208
|
+
|
|
209
|
+
// Base where condition
|
|
210
|
+
where: { isActive: true },
|
|
211
|
+
};
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
> When both `select` and `relations` are set, relations are merged into the select object to avoid Prisma's include/select conflict.
|
|
215
|
+
|
|
216
|
+
## Module Options
|
|
217
|
+
|
|
218
|
+
### forRoot
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
PaginationModule.forRoot({
|
|
222
|
+
defaultLimit: 20,
|
|
223
|
+
maxLimit: 100,
|
|
224
|
+
defaultPaginationType: 'offset',
|
|
225
|
+
defaultSortBy: [['createdAt', 'DESC']],
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### forRootAsync
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
PaginationModule.forRootAsync({
|
|
233
|
+
imports: [ConfigModule],
|
|
234
|
+
useFactory: (config: ConfigService) => ({
|
|
235
|
+
defaultLimit: config.get('PAGINATION_DEFAULT_LIMIT', 20),
|
|
236
|
+
maxLimit: config.get('PAGINATION_MAX_LIMIT', 100),
|
|
237
|
+
}),
|
|
238
|
+
inject: [ConfigService],
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## PaginateService
|
|
243
|
+
|
|
244
|
+
`PaginateService` merges module options, `@PaginateDefaults` metadata, and per-endpoint config (highest priority wins):
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
@Controller('users')
|
|
248
|
+
export class UserController {
|
|
249
|
+
constructor(
|
|
250
|
+
private readonly prisma: PrismaService,
|
|
251
|
+
private readonly paginateService: PaginateService,
|
|
252
|
+
) {}
|
|
253
|
+
|
|
254
|
+
@Get()
|
|
255
|
+
@PaginateDefaults({ defaultLimit: 10, maxLimit: 50 })
|
|
256
|
+
async findAll(@Paginate() query: PaginateQuery) {
|
|
257
|
+
return this.paginateService.paginate(
|
|
258
|
+
query,
|
|
259
|
+
this.prisma.user,
|
|
260
|
+
{ sortableColumns: ['id', 'name', 'createdAt'] },
|
|
261
|
+
this.findAll,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Priority: `config` (per-endpoint) > `@PaginateDefaults` (per-handler) > `forRoot()` (global)
|
|
268
|
+
|
|
269
|
+
## Swagger
|
|
270
|
+
|
|
271
|
+
Install `@nestjs/swagger` (optional peer dependency) for auto-documentation:
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
@Get()
|
|
275
|
+
@ApiPaginatedResponse(UserDto) // offset response schema
|
|
276
|
+
async findAll(@Paginate() query: PaginateQuery) { ... }
|
|
277
|
+
|
|
278
|
+
@Get('stream')
|
|
279
|
+
@ApiCursorPaginatedResponse(UserDto) // cursor response schema
|
|
280
|
+
async findAllCursor(@Paginate() query: PaginateQuery) { ... }
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
If `@nestjs/swagger` is not installed, decorators are no-ops.
|
|
284
|
+
|
|
285
|
+
## Standalone Usage
|
|
286
|
+
|
|
287
|
+
`paginate()` works without NestJS:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import { paginate } from '@nestarc/pagination';
|
|
291
|
+
import { PrismaClient } from '@prisma/client';
|
|
292
|
+
|
|
293
|
+
const prisma = new PrismaClient();
|
|
294
|
+
|
|
295
|
+
const result = await paginate(
|
|
296
|
+
{ page: 1, limit: 20, path: '/users' },
|
|
297
|
+
prisma.user,
|
|
298
|
+
{ sortableColumns: ['id', 'name', 'createdAt'] },
|
|
299
|
+
);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Testing Utilities
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { createPaginateQuery, TestPaginationModule } from '@nestarc/pagination/testing';
|
|
306
|
+
|
|
307
|
+
// Test module
|
|
308
|
+
const module = await Test.createTestingModule({
|
|
309
|
+
imports: [TestPaginationModule.register({ defaultLimit: 10 })],
|
|
310
|
+
providers: [UserService],
|
|
311
|
+
}).compile();
|
|
312
|
+
|
|
313
|
+
// Query factory
|
|
314
|
+
const query = createPaginateQuery({
|
|
315
|
+
page: 1,
|
|
316
|
+
limit: 10,
|
|
317
|
+
sortBy: [['createdAt', 'DESC']],
|
|
318
|
+
path: '/users',
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Error Handling
|
|
323
|
+
|
|
324
|
+
| Error | Status | When |
|
|
325
|
+
|-------|--------|------|
|
|
326
|
+
| `InvalidSortColumnError` | 400 | Sort column not in `sortableColumns` |
|
|
327
|
+
| `InvalidFilterColumnError` | 400 | Filter column not in `filterableColumns` or operator not allowed |
|
|
328
|
+
| `InvalidCursorError` | 400 | Invalid Base64url cursor |
|
|
329
|
+
|
|
330
|
+
Unknown sort/filter columns throw errors (not silently ignored) to prevent clients from trusting incorrect results.
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodeCursor = encodeCursor;
|
|
4
|
+
exports.decodeCursor = decodeCursor;
|
|
5
|
+
const invalid_cursor_error_1 = require("../errors/invalid-cursor.error");
|
|
6
|
+
function encodeCursor(record, cursorColumn) {
|
|
7
|
+
const value = record[cursorColumn];
|
|
8
|
+
return Buffer.from(JSON.stringify({ [cursorColumn]: value })).toString('base64url');
|
|
9
|
+
}
|
|
10
|
+
function decodeCursor(cursor) {
|
|
11
|
+
if (!cursor) {
|
|
12
|
+
throw new invalid_cursor_error_1.InvalidCursorError(cursor);
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const json = Buffer.from(cursor, 'base64url').toString('utf-8');
|
|
16
|
+
const parsed = JSON.parse(json);
|
|
17
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
18
|
+
throw new Error('Not an object');
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new invalid_cursor_error_1.InvalidCursorError(cursor);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiPaginatedResponse = ApiPaginatedResponse;
|
|
4
|
+
exports.ApiCursorPaginatedResponse = ApiCursorPaginatedResponse;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
let swagger;
|
|
7
|
+
try {
|
|
8
|
+
swagger = require('@nestjs/swagger');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// @nestjs/swagger not installed — decorators become no-ops
|
|
12
|
+
}
|
|
13
|
+
function ApiPaginatedResponse(dataDto) {
|
|
14
|
+
if (!swagger) {
|
|
15
|
+
return (0, common_1.applyDecorators)();
|
|
16
|
+
}
|
|
17
|
+
const { ApiOkResponse, ApiQuery, getSchemaPath } = swagger;
|
|
18
|
+
return (0, common_1.applyDecorators)(ApiOkResponse({
|
|
19
|
+
schema: {
|
|
20
|
+
allOf: [
|
|
21
|
+
{
|
|
22
|
+
properties: {
|
|
23
|
+
data: {
|
|
24
|
+
type: 'array',
|
|
25
|
+
items: { $ref: getSchemaPath(dataDto) },
|
|
26
|
+
},
|
|
27
|
+
meta: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
itemsPerPage: { type: 'number', example: 20 },
|
|
31
|
+
totalItems: { type: 'number', example: 500 },
|
|
32
|
+
currentPage: { type: 'number', example: 1 },
|
|
33
|
+
totalPages: { type: 'number', example: 25 },
|
|
34
|
+
sortBy: { type: 'array', example: [['createdAt', 'DESC']] },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
links: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
first: { type: 'string' },
|
|
41
|
+
previous: { type: 'string', nullable: true },
|
|
42
|
+
current: { type: 'string' },
|
|
43
|
+
next: { type: 'string', nullable: true },
|
|
44
|
+
last: { type: 'string' },
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
}), ApiQuery({ name: 'page', required: false, type: Number }), ApiQuery({ name: 'limit', required: false, type: Number }), ApiQuery({ name: 'sortBy', required: false, type: String, isArray: true }), ApiQuery({ name: 'search', required: false, type: String }));
|
|
52
|
+
}
|
|
53
|
+
function ApiCursorPaginatedResponse(dataDto) {
|
|
54
|
+
if (!swagger) {
|
|
55
|
+
return (0, common_1.applyDecorators)();
|
|
56
|
+
}
|
|
57
|
+
const { ApiOkResponse, ApiQuery, getSchemaPath } = swagger;
|
|
58
|
+
return (0, common_1.applyDecorators)(ApiOkResponse({
|
|
59
|
+
schema: {
|
|
60
|
+
allOf: [
|
|
61
|
+
{
|
|
62
|
+
properties: {
|
|
63
|
+
data: {
|
|
64
|
+
type: 'array',
|
|
65
|
+
items: { $ref: getSchemaPath(dataDto) },
|
|
66
|
+
},
|
|
67
|
+
meta: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
itemsPerPage: { type: 'number', example: 20 },
|
|
71
|
+
hasNextPage: { type: 'boolean', example: true },
|
|
72
|
+
hasPreviousPage: { type: 'boolean', example: false },
|
|
73
|
+
startCursor: { type: 'string', nullable: true },
|
|
74
|
+
endCursor: { type: 'string', nullable: true },
|
|
75
|
+
sortBy: { type: 'array', example: [['createdAt', 'DESC']] },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
links: {
|
|
79
|
+
type: 'object',
|
|
80
|
+
properties: {
|
|
81
|
+
current: { type: 'string' },
|
|
82
|
+
next: { type: 'string', nullable: true },
|
|
83
|
+
previous: { type: 'string', nullable: true },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
}), ApiQuery({ name: 'limit', required: false, type: Number }), ApiQuery({ name: 'after', required: false, type: String }), ApiQuery({ name: 'before', required: false, type: String }), ApiQuery({ name: 'sortBy', required: false, type: String, isArray: true }), ApiQuery({ name: 'search', required: false, type: String }));
|
|
91
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SortOrder } from '../interfaces/filter-operator.type';
|
|
2
|
+
export declare const PAGINATE_DEFAULTS_KEY = "PAGINATE_DEFAULTS";
|
|
3
|
+
export interface PaginateDefaultsOptions {
|
|
4
|
+
defaultLimit?: number;
|
|
5
|
+
maxLimit?: number;
|
|
6
|
+
defaultSortBy?: [string, SortOrder][];
|
|
7
|
+
paginationType?: 'offset' | 'cursor';
|
|
8
|
+
}
|
|
9
|
+
export declare const PaginateDefaults: (defaults: PaginateDefaultsOptions) => import("@nestjs/common").CustomDecorator<string>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PaginateDefaults = exports.PAGINATE_DEFAULTS_KEY = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
exports.PAGINATE_DEFAULTS_KEY = 'PAGINATE_DEFAULTS';
|
|
6
|
+
const PaginateDefaults = (defaults) => (0, common_1.SetMetadata)(exports.PAGINATE_DEFAULTS_KEY, defaults);
|
|
7
|
+
exports.PaginateDefaults = PaginateDefaults;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const Paginate: (...dataOrPipes: unknown[]) => ParameterDecorator;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Paginate = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
const paginate_query_pipe_1 = require("../pipes/paginate-query.pipe");
|
|
6
|
+
exports.Paginate = (0, common_1.createParamDecorator)((_data, ctx) => {
|
|
7
|
+
const request = ctx.switchToHttp().getRequest();
|
|
8
|
+
return (0, paginate_query_pipe_1.parsePaginateQuery)(request);
|
|
9
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InvalidCursorError = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
class InvalidCursorError extends common_1.BadRequestException {
|
|
6
|
+
constructor(cursor) {
|
|
7
|
+
super(`Invalid cursor: '${cursor}'`);
|
|
8
|
+
this.name = 'InvalidCursor';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.InvalidCursorError = InvalidCursorError;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InvalidFilterColumnError = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
class InvalidFilterColumnError extends common_1.BadRequestException {
|
|
6
|
+
constructor(column, filterableColumns, operator, allowedOperators) {
|
|
7
|
+
const message = operator && allowedOperators
|
|
8
|
+
? `Operator '${operator}' is not allowed for column '${column}'. Allowed operators: ${allowedOperators.join(', ')}`
|
|
9
|
+
: `Column '${column}' is not filterable. Filterable columns: ${filterableColumns.join(', ')}`;
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'InvalidFilterColumn';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.InvalidFilterColumnError = InvalidFilterColumnError;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InvalidSortColumnError = void 0;
|
|
4
|
+
const common_1 = require("@nestjs/common");
|
|
5
|
+
class InvalidSortColumnError extends common_1.BadRequestException {
|
|
6
|
+
constructor(column, sortableColumns) {
|
|
7
|
+
super(`Column '${column}' is not sortable. Sortable columns: ${sortableColumns.join(', ')}`);
|
|
8
|
+
this.name = 'InvalidSortColumn';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.InvalidSortColumnError = InvalidSortColumnError;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseFilters = parseFilters;
|
|
4
|
+
const invalid_filter_column_error_1 = require("../errors/invalid-filter-column.error");
|
|
5
|
+
const type_coercion_1 = require("../helpers/type-coercion");
|
|
6
|
+
function parseFilters(filters, filterableColumns) {
|
|
7
|
+
if (!filters)
|
|
8
|
+
return {};
|
|
9
|
+
const where = {};
|
|
10
|
+
for (const [column, rawValue] of Object.entries(filters)) {
|
|
11
|
+
const allowedOperators = filterableColumns[column];
|
|
12
|
+
if (!allowedOperators) {
|
|
13
|
+
throw new invalid_filter_column_error_1.InvalidFilterColumnError(column, Object.keys(filterableColumns));
|
|
14
|
+
}
|
|
15
|
+
const values = Array.isArray(rawValue) ? rawValue : [rawValue];
|
|
16
|
+
for (const value of values) {
|
|
17
|
+
const parsed = parseSingleFilter(value, column, allowedOperators);
|
|
18
|
+
where[column] = where[column]
|
|
19
|
+
? { ...where[column], ...toObject(parsed) }
|
|
20
|
+
: parsed;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return where;
|
|
24
|
+
}
|
|
25
|
+
function toObject(value) {
|
|
26
|
+
if (value === null || typeof value !== 'object')
|
|
27
|
+
return {};
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function parseSingleFilter(raw, column, allowedOperators) {
|
|
31
|
+
if (raw === '$null') {
|
|
32
|
+
validateOperator('$null', column, allowedOperators);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (raw === '$not:null') {
|
|
36
|
+
validateOperator('$not:null', column, allowedOperators);
|
|
37
|
+
return { not: null };
|
|
38
|
+
}
|
|
39
|
+
const colonIndex = raw.indexOf(':');
|
|
40
|
+
if (colonIndex === -1) {
|
|
41
|
+
throw new invalid_filter_column_error_1.InvalidFilterColumnError(column, [], raw, allowedOperators);
|
|
42
|
+
}
|
|
43
|
+
let operator;
|
|
44
|
+
let value;
|
|
45
|
+
if (raw.startsWith('$not:null')) {
|
|
46
|
+
operator = '$not:null';
|
|
47
|
+
value = '';
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
operator = raw.substring(0, colonIndex);
|
|
51
|
+
value = raw.substring(colonIndex + 1);
|
|
52
|
+
}
|
|
53
|
+
validateOperator(operator, column, allowedOperators);
|
|
54
|
+
switch (operator) {
|
|
55
|
+
case '$eq':
|
|
56
|
+
return { equals: (0, type_coercion_1.coerceFilterValue)(value) };
|
|
57
|
+
case '$ne':
|
|
58
|
+
return { not: (0, type_coercion_1.coerceFilterValue)(value) };
|
|
59
|
+
case '$gt':
|
|
60
|
+
return { gt: (0, type_coercion_1.coerceFilterValue)(value) };
|
|
61
|
+
case '$gte':
|
|
62
|
+
return { gte: (0, type_coercion_1.coerceFilterValue)(value) };
|
|
63
|
+
case '$lt':
|
|
64
|
+
return { lt: (0, type_coercion_1.coerceFilterValue)(value) };
|
|
65
|
+
case '$lte':
|
|
66
|
+
return { lte: (0, type_coercion_1.coerceFilterValue)(value) };
|
|
67
|
+
case '$in': {
|
|
68
|
+
const items = value.split(',').map(type_coercion_1.coerceFilterValue);
|
|
69
|
+
return { in: items };
|
|
70
|
+
}
|
|
71
|
+
case '$nin': {
|
|
72
|
+
const items = value.split(',').map(type_coercion_1.coerceFilterValue);
|
|
73
|
+
return { notIn: items };
|
|
74
|
+
}
|
|
75
|
+
case '$ilike':
|
|
76
|
+
return { contains: value, mode: 'insensitive' };
|
|
77
|
+
case '$btw': {
|
|
78
|
+
const [min, max] = value.split(',');
|
|
79
|
+
return { gte: (0, type_coercion_1.coerceFilterValue)(min), lte: (0, type_coercion_1.coerceFilterValue)(max) };
|
|
80
|
+
}
|
|
81
|
+
default:
|
|
82
|
+
throw new invalid_filter_column_error_1.InvalidFilterColumnError(column, [], operator, allowedOperators);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function validateOperator(operator, column, allowedOperators) {
|
|
86
|
+
if (!allowedOperators.includes(operator)) {
|
|
87
|
+
throw new invalid_filter_column_error_1.InvalidFilterColumnError(column, [], operator, allowedOperators);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildSearchCondition(search: string | undefined, searchableColumns: string[] | undefined): Record<string, any>;
|