@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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +334 -0
  3. package/dist/cursor/cursor.encoder.d.ts +2 -0
  4. package/dist/cursor/cursor.encoder.js +25 -0
  5. package/dist/decorators/api-paginated-response.decorator.d.ts +3 -0
  6. package/dist/decorators/api-paginated-response.decorator.js +91 -0
  7. package/dist/decorators/paginate-defaults.decorator.d.ts +9 -0
  8. package/dist/decorators/paginate-defaults.decorator.js +7 -0
  9. package/dist/decorators/paginate.decorator.d.ts +1 -0
  10. package/dist/decorators/paginate.decorator.js +9 -0
  11. package/dist/errors/invalid-cursor.error.d.ts +4 -0
  12. package/dist/errors/invalid-cursor.error.js +11 -0
  13. package/dist/errors/invalid-filter-column.error.d.ts +4 -0
  14. package/dist/errors/invalid-filter-column.error.js +14 -0
  15. package/dist/errors/invalid-sort-column.error.d.ts +4 -0
  16. package/dist/errors/invalid-sort-column.error.js +11 -0
  17. package/dist/filter/filter-parser.d.ts +2 -0
  18. package/dist/filter/filter-parser.js +89 -0
  19. package/dist/filter/search-builder.d.ts +1 -0
  20. package/dist/filter/search-builder.js +13 -0
  21. package/dist/filter/sort-builder.d.ts +3 -0
  22. package/dist/filter/sort-builder.js +25 -0
  23. package/dist/helpers/link-builder.d.ts +13 -0
  24. package/dist/helpers/link-builder.js +71 -0
  25. package/dist/helpers/type-coercion.d.ts +1 -0
  26. package/dist/helpers/type-coercion.js +15 -0
  27. package/dist/index.d.ts +15 -0
  28. package/dist/index.js +31 -0
  29. package/dist/interfaces/filter-operator.type.d.ts +2 -0
  30. package/dist/interfaces/filter-operator.type.js +2 -0
  31. package/dist/interfaces/paginate-config.interface.d.ts +29 -0
  32. package/dist/interfaces/paginate-config.interface.js +2 -0
  33. package/dist/interfaces/paginate-query.interface.d.ts +11 -0
  34. package/dist/interfaces/paginate-query.interface.js +2 -0
  35. package/dist/interfaces/paginated.interface.d.ts +39 -0
  36. package/dist/interfaces/paginated.interface.js +2 -0
  37. package/dist/interfaces/pagination-options.interface.d.ts +15 -0
  38. package/dist/interfaces/pagination-options.interface.js +2 -0
  39. package/dist/paginate.d.ts +7 -0
  40. package/dist/paginate.js +179 -0
  41. package/dist/paginate.service.d.ts +15 -0
  42. package/dist/paginate.service.js +59 -0
  43. package/dist/pagination.constants.d.ts +5 -0
  44. package/dist/pagination.constants.js +8 -0
  45. package/dist/pagination.module.d.ts +6 -0
  46. package/dist/pagination.module.js +52 -0
  47. package/dist/pipes/paginate-query.pipe.d.ts +2 -0
  48. package/dist/pipes/paginate-query.pipe.js +43 -0
  49. package/dist/testing/create-paginate-query.d.ts +2 -0
  50. package/dist/testing/create-paginate-query.js +9 -0
  51. package/dist/testing/index.d.ts +2 -0
  52. package/dist/testing/index.js +7 -0
  53. package/dist/testing/test-pagination.module.d.ts +5 -0
  54. package/dist/testing/test-pagination.module.js +30 -0
  55. 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,2 @@
1
+ export declare function encodeCursor(record: Record<string, any>, cursorColumn: string): string;
2
+ export declare function decodeCursor(cursor: string): Record<string, any>;
@@ -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,3 @@
1
+ import { Type } from '@nestjs/common';
2
+ export declare function ApiPaginatedResponse(dataDto: Type): MethodDecorator;
3
+ export declare function ApiCursorPaginatedResponse(dataDto: Type): MethodDecorator;
@@ -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,4 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ export declare class InvalidCursorError extends BadRequestException {
3
+ constructor(cursor: string);
4
+ }
@@ -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,4 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ export declare class InvalidFilterColumnError extends BadRequestException {
3
+ constructor(column: string, filterableColumns: string[], operator?: string, allowedOperators?: string[]);
4
+ }
@@ -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,4 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ export declare class InvalidSortColumnError extends BadRequestException {
3
+ constructor(column: string, sortableColumns: string[]);
4
+ }
@@ -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,2 @@
1
+ import { FilterOperator } from '../interfaces/filter-operator.type';
2
+ export declare function parseFilters(filters: Record<string, string | string[]> | undefined, filterableColumns: Record<string, FilterOperator[]>): Record<string, any>;
@@ -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>;