@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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSearchCondition = buildSearchCondition;
|
|
4
|
+
function buildSearchCondition(search, searchableColumns) {
|
|
5
|
+
if (!search || !searchableColumns || searchableColumns.length === 0) {
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
return {
|
|
9
|
+
OR: searchableColumns.map((column) => ({
|
|
10
|
+
[column]: { contains: search, mode: 'insensitive' },
|
|
11
|
+
})),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { SortOrder } from '../interfaces/filter-operator.type';
|
|
2
|
+
export declare function validateSortColumns(sortBy: [string, SortOrder][] | undefined, sortableColumns: string[]): void;
|
|
3
|
+
export declare function buildOrderBy(sortBy: [string, SortOrder][] | undefined, nullSort?: 'first' | 'last'): Record<string, any>[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateSortColumns = validateSortColumns;
|
|
4
|
+
exports.buildOrderBy = buildOrderBy;
|
|
5
|
+
const invalid_sort_column_error_1 = require("../errors/invalid-sort-column.error");
|
|
6
|
+
function validateSortColumns(sortBy, sortableColumns) {
|
|
7
|
+
if (!sortBy)
|
|
8
|
+
return;
|
|
9
|
+
for (const [column] of sortBy) {
|
|
10
|
+
if (!sortableColumns.includes(column)) {
|
|
11
|
+
throw new invalid_sort_column_error_1.InvalidSortColumnError(column, sortableColumns);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function buildOrderBy(sortBy, nullSort) {
|
|
16
|
+
if (!sortBy || sortBy.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
return sortBy.map(([column, order]) => {
|
|
19
|
+
const direction = order.toLowerCase();
|
|
20
|
+
if (nullSort) {
|
|
21
|
+
return { [column]: { sort: direction, nulls: nullSort } };
|
|
22
|
+
}
|
|
23
|
+
return { [column]: direction };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PaginateQuery } from '../interfaces/paginate-query.interface';
|
|
2
|
+
export declare function buildOffsetLinks(query: PaginateQuery, currentPage: number, limit: number, totalPages: number): {
|
|
3
|
+
first: string;
|
|
4
|
+
previous: string | null;
|
|
5
|
+
current: string;
|
|
6
|
+
next: string | null;
|
|
7
|
+
last: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function buildCursorLinks(query: PaginateQuery, limit: number, endCursor: string | null, startCursor: string | null, hasNextPage: boolean, hasPreviousPage: boolean): {
|
|
10
|
+
current: string;
|
|
11
|
+
next: string | null;
|
|
12
|
+
previous: string | null;
|
|
13
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildOffsetLinks = buildOffsetLinks;
|
|
4
|
+
exports.buildCursorLinks = buildCursorLinks;
|
|
5
|
+
function buildQueryString(path, query, overrides) {
|
|
6
|
+
const params = new URLSearchParams();
|
|
7
|
+
// Pagination params (from overrides)
|
|
8
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
9
|
+
if (value !== undefined) {
|
|
10
|
+
params.set(key, value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
// Preserve sortBy
|
|
14
|
+
if (query.sortBy) {
|
|
15
|
+
for (const [col, order] of query.sortBy) {
|
|
16
|
+
params.append('sortBy', `${col}:${order}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Preserve search
|
|
20
|
+
if (query.search) {
|
|
21
|
+
params.set('search', query.search);
|
|
22
|
+
}
|
|
23
|
+
// Preserve filters
|
|
24
|
+
if (query.filter) {
|
|
25
|
+
for (const [col, value] of Object.entries(query.filter)) {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
for (const v of value) {
|
|
28
|
+
params.append(`filter.${col}`, v);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
params.set(`filter.${col}`, value);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return `${path}?${params.toString()}`;
|
|
37
|
+
}
|
|
38
|
+
function buildOffsetLinks(query, currentPage, limit, totalPages) {
|
|
39
|
+
const base = { limit: String(limit) };
|
|
40
|
+
return {
|
|
41
|
+
first: buildQueryString(query.path, query, { page: '1', ...base }),
|
|
42
|
+
previous: currentPage > 1
|
|
43
|
+
? buildQueryString(query.path, query, { page: String(currentPage - 1), ...base })
|
|
44
|
+
: null,
|
|
45
|
+
current: buildQueryString(query.path, query, { page: String(currentPage), ...base }),
|
|
46
|
+
next: currentPage < totalPages
|
|
47
|
+
? buildQueryString(query.path, query, { page: String(currentPage + 1), ...base })
|
|
48
|
+
: null,
|
|
49
|
+
last: buildQueryString(query.path, query, { page: String(totalPages), ...base }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function buildCursorLinks(query, limit, endCursor, startCursor, hasNextPage, hasPreviousPage) {
|
|
53
|
+
const base = { limit: String(limit) };
|
|
54
|
+
// current link: reflect the original request's cursor
|
|
55
|
+
let currentOverrides = { ...base };
|
|
56
|
+
if (query.after) {
|
|
57
|
+
currentOverrides.after = query.after;
|
|
58
|
+
}
|
|
59
|
+
else if (query.before) {
|
|
60
|
+
currentOverrides.before = query.before;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
current: buildQueryString(query.path, query, currentOverrides),
|
|
64
|
+
next: hasNextPage && endCursor
|
|
65
|
+
? buildQueryString(query.path, query, { ...base, after: endCursor })
|
|
66
|
+
: null,
|
|
67
|
+
previous: hasPreviousPage && startCursor
|
|
68
|
+
? buildQueryString(query.path, query, { ...base, before: startCursor })
|
|
69
|
+
: null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function coerceFilterValue(value: string): string | number | boolean | null;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coerceFilterValue = coerceFilterValue;
|
|
4
|
+
function coerceFilterValue(value) {
|
|
5
|
+
if (value === 'true')
|
|
6
|
+
return true;
|
|
7
|
+
if (value === 'false')
|
|
8
|
+
return false;
|
|
9
|
+
if (value === 'null')
|
|
10
|
+
return null;
|
|
11
|
+
if (value !== '' && !isNaN(Number(value)) && value.trim() === value && String(Number(value)) === value) {
|
|
12
|
+
return Number(value);
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { PaginationModule } from './pagination.module';
|
|
2
|
+
export { PaginationModuleOptions, PaginationModuleAsyncOptions, } from './interfaces/pagination-options.interface';
|
|
3
|
+
export { paginate } from './paginate';
|
|
4
|
+
export { PaginateService } from './paginate.service';
|
|
5
|
+
export { PaginateQuery } from './interfaces/paginate-query.interface';
|
|
6
|
+
export { PaginateConfig } from './interfaces/paginate-config.interface';
|
|
7
|
+
export { Paginated, CursorPaginated } from './interfaces/paginated.interface';
|
|
8
|
+
export { FilterOperator, SortOrder } from './interfaces/filter-operator.type';
|
|
9
|
+
export { Paginate } from './decorators/paginate.decorator';
|
|
10
|
+
export { PaginateDefaults, PAGINATE_DEFAULTS_KEY, PaginateDefaultsOptions } from './decorators/paginate-defaults.decorator';
|
|
11
|
+
export { ApiPaginatedResponse, ApiCursorPaginatedResponse, } from './decorators/api-paginated-response.decorator';
|
|
12
|
+
export { InvalidSortColumnError } from './errors/invalid-sort-column.error';
|
|
13
|
+
export { InvalidFilterColumnError } from './errors/invalid-filter-column.error';
|
|
14
|
+
export { InvalidCursorError } from './errors/invalid-cursor.error';
|
|
15
|
+
export { PAGINATION_MODULE_OPTIONS } from './pagination.constants';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PAGINATION_MODULE_OPTIONS = exports.InvalidCursorError = exports.InvalidFilterColumnError = exports.InvalidSortColumnError = exports.ApiCursorPaginatedResponse = exports.ApiPaginatedResponse = exports.PAGINATE_DEFAULTS_KEY = exports.PaginateDefaults = exports.Paginate = exports.PaginateService = exports.paginate = exports.PaginationModule = void 0;
|
|
4
|
+
// Core Module
|
|
5
|
+
var pagination_module_1 = require("./pagination.module");
|
|
6
|
+
Object.defineProperty(exports, "PaginationModule", { enumerable: true, get: function () { return pagination_module_1.PaginationModule; } });
|
|
7
|
+
// Core Function
|
|
8
|
+
var paginate_1 = require("./paginate");
|
|
9
|
+
Object.defineProperty(exports, "paginate", { enumerable: true, get: function () { return paginate_1.paginate; } });
|
|
10
|
+
// Service
|
|
11
|
+
var paginate_service_1 = require("./paginate.service");
|
|
12
|
+
Object.defineProperty(exports, "PaginateService", { enumerable: true, get: function () { return paginate_service_1.PaginateService; } });
|
|
13
|
+
// Decorators
|
|
14
|
+
var paginate_decorator_1 = require("./decorators/paginate.decorator");
|
|
15
|
+
Object.defineProperty(exports, "Paginate", { enumerable: true, get: function () { return paginate_decorator_1.Paginate; } });
|
|
16
|
+
var paginate_defaults_decorator_1 = require("./decorators/paginate-defaults.decorator");
|
|
17
|
+
Object.defineProperty(exports, "PaginateDefaults", { enumerable: true, get: function () { return paginate_defaults_decorator_1.PaginateDefaults; } });
|
|
18
|
+
Object.defineProperty(exports, "PAGINATE_DEFAULTS_KEY", { enumerable: true, get: function () { return paginate_defaults_decorator_1.PAGINATE_DEFAULTS_KEY; } });
|
|
19
|
+
var api_paginated_response_decorator_1 = require("./decorators/api-paginated-response.decorator");
|
|
20
|
+
Object.defineProperty(exports, "ApiPaginatedResponse", { enumerable: true, get: function () { return api_paginated_response_decorator_1.ApiPaginatedResponse; } });
|
|
21
|
+
Object.defineProperty(exports, "ApiCursorPaginatedResponse", { enumerable: true, get: function () { return api_paginated_response_decorator_1.ApiCursorPaginatedResponse; } });
|
|
22
|
+
// Errors
|
|
23
|
+
var invalid_sort_column_error_1 = require("./errors/invalid-sort-column.error");
|
|
24
|
+
Object.defineProperty(exports, "InvalidSortColumnError", { enumerable: true, get: function () { return invalid_sort_column_error_1.InvalidSortColumnError; } });
|
|
25
|
+
var invalid_filter_column_error_1 = require("./errors/invalid-filter-column.error");
|
|
26
|
+
Object.defineProperty(exports, "InvalidFilterColumnError", { enumerable: true, get: function () { return invalid_filter_column_error_1.InvalidFilterColumnError; } });
|
|
27
|
+
var invalid_cursor_error_1 = require("./errors/invalid-cursor.error");
|
|
28
|
+
Object.defineProperty(exports, "InvalidCursorError", { enumerable: true, get: function () { return invalid_cursor_error_1.InvalidCursorError; } });
|
|
29
|
+
// Constants
|
|
30
|
+
var pagination_constants_1 = require("./pagination.constants");
|
|
31
|
+
Object.defineProperty(exports, "PAGINATION_MODULE_OPTIONS", { enumerable: true, get: function () { return pagination_constants_1.PAGINATION_MODULE_OPTIONS; } });
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FilterOperator, SortOrder } from './filter-operator.type';
|
|
2
|
+
export interface PaginateConfig<T = any> {
|
|
3
|
+
sortableColumns: (keyof T & string)[];
|
|
4
|
+
defaultSortBy?: [keyof T & string, SortOrder][];
|
|
5
|
+
nullSort?: 'first' | 'last';
|
|
6
|
+
searchableColumns?: (keyof T & string)[];
|
|
7
|
+
filterableColumns?: {
|
|
8
|
+
[K in keyof T & string]?: FilterOperator[];
|
|
9
|
+
};
|
|
10
|
+
relations?: Record<string, boolean | object>;
|
|
11
|
+
select?: (keyof T & string)[];
|
|
12
|
+
paginationType?: 'offset' | 'cursor';
|
|
13
|
+
/**
|
|
14
|
+
* Column used as cursor for cursor-based pagination. Defaults to 'id'.
|
|
15
|
+
*
|
|
16
|
+
* Requirements:
|
|
17
|
+
* - Must be included in `sortableColumns`
|
|
18
|
+
* - Should have unique, sequential values (e.g., auto-increment ID, UUID v7, timestamp)
|
|
19
|
+
* - Non-unique cursor columns may produce inconsistent results across pages
|
|
20
|
+
*
|
|
21
|
+
* @see https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination
|
|
22
|
+
*/
|
|
23
|
+
cursorColumn?: keyof T & string;
|
|
24
|
+
defaultLimit?: number;
|
|
25
|
+
maxLimit?: number;
|
|
26
|
+
withTotalCount?: boolean;
|
|
27
|
+
where?: object;
|
|
28
|
+
allowWithDeleted?: boolean;
|
|
29
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { SortOrder } from './filter-operator.type';
|
|
2
|
+
export interface PaginateQuery {
|
|
3
|
+
limit?: number;
|
|
4
|
+
sortBy?: [string, SortOrder][];
|
|
5
|
+
search?: string;
|
|
6
|
+
filter?: Record<string, string | string[]>;
|
|
7
|
+
path: string;
|
|
8
|
+
page?: number;
|
|
9
|
+
after?: string;
|
|
10
|
+
before?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { SortOrder } from './filter-operator.type';
|
|
2
|
+
export interface Paginated<T> {
|
|
3
|
+
data: T[];
|
|
4
|
+
meta: {
|
|
5
|
+
itemsPerPage: number;
|
|
6
|
+
totalItems: number;
|
|
7
|
+
currentPage: number;
|
|
8
|
+
totalPages: number;
|
|
9
|
+
sortBy: [string, SortOrder][];
|
|
10
|
+
search?: string;
|
|
11
|
+
filter?: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
links: {
|
|
14
|
+
first: string;
|
|
15
|
+
previous: string | null;
|
|
16
|
+
current: string;
|
|
17
|
+
next: string | null;
|
|
18
|
+
last: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export interface CursorPaginated<T> {
|
|
22
|
+
data: T[];
|
|
23
|
+
meta: {
|
|
24
|
+
itemsPerPage: number;
|
|
25
|
+
hasNextPage: boolean;
|
|
26
|
+
hasPreviousPage: boolean;
|
|
27
|
+
startCursor: string | null;
|
|
28
|
+
endCursor: string | null;
|
|
29
|
+
sortBy: [string, SortOrder][];
|
|
30
|
+
search?: string;
|
|
31
|
+
filter?: Record<string, string>;
|
|
32
|
+
totalItems?: number;
|
|
33
|
+
};
|
|
34
|
+
links: {
|
|
35
|
+
current: string;
|
|
36
|
+
next: string | null;
|
|
37
|
+
previous: string | null;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
import { SortOrder } from './filter-operator.type';
|
|
3
|
+
export interface PaginationModuleOptions {
|
|
4
|
+
defaultLimit?: number;
|
|
5
|
+
maxLimit?: number;
|
|
6
|
+
defaultPaginationType?: 'offset' | 'cursor';
|
|
7
|
+
defaultSortBy?: [string, SortOrder][];
|
|
8
|
+
withLinks?: boolean;
|
|
9
|
+
withTotalCount?: boolean;
|
|
10
|
+
fieldNamingStrategy?: 'camelCase' | 'snake_case';
|
|
11
|
+
}
|
|
12
|
+
export interface PaginationModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
|
|
13
|
+
useFactory: (...args: any[]) => Promise<PaginationModuleOptions> | PaginationModuleOptions;
|
|
14
|
+
inject?: any[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { PaginateQuery } from './interfaces/paginate-query.interface';
|
|
2
|
+
import { PaginateConfig } from './interfaces/paginate-config.interface';
|
|
3
|
+
import { Paginated, CursorPaginated } from './interfaces/paginated.interface';
|
|
4
|
+
export declare function paginate<T>(query: PaginateQuery, delegate: {
|
|
5
|
+
findMany: (args: any) => Promise<T[]>;
|
|
6
|
+
count: (args: any) => Promise<number>;
|
|
7
|
+
}, config: PaginateConfig<T>): Promise<Paginated<T> | CursorPaginated<T>>;
|
package/dist/paginate.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.paginate = paginate;
|
|
4
|
+
const pagination_constants_1 = require("./pagination.constants");
|
|
5
|
+
const filter_parser_1 = require("./filter/filter-parser");
|
|
6
|
+
const sort_builder_1 = require("./filter/sort-builder");
|
|
7
|
+
const search_builder_1 = require("./filter/search-builder");
|
|
8
|
+
const link_builder_1 = require("./helpers/link-builder");
|
|
9
|
+
const cursor_encoder_1 = require("./cursor/cursor.encoder");
|
|
10
|
+
async function paginate(query, delegate, config) {
|
|
11
|
+
const isCursorMode = config.paginationType === 'cursor' ||
|
|
12
|
+
query.after !== undefined ||
|
|
13
|
+
query.before !== undefined;
|
|
14
|
+
if (isCursorMode) {
|
|
15
|
+
return paginateCursor(query, delegate, config);
|
|
16
|
+
}
|
|
17
|
+
return paginateOffset(query, delegate, config);
|
|
18
|
+
}
|
|
19
|
+
async function paginateOffset(query, delegate, config) {
|
|
20
|
+
const limit = resolveLimit(query.limit, config);
|
|
21
|
+
const page = query.page ?? pagination_constants_1.DEFAULT_PAGE;
|
|
22
|
+
const sortBy = query.sortBy ?? config.defaultSortBy;
|
|
23
|
+
if (sortBy) {
|
|
24
|
+
(0, sort_builder_1.validateSortColumns)(sortBy, config.sortableColumns);
|
|
25
|
+
}
|
|
26
|
+
const orderBy = (0, sort_builder_1.buildOrderBy)(sortBy, config.nullSort);
|
|
27
|
+
const where = buildWhere(query, config);
|
|
28
|
+
const findManyArgs = {
|
|
29
|
+
where,
|
|
30
|
+
orderBy,
|
|
31
|
+
skip: (page - 1) * limit,
|
|
32
|
+
take: limit,
|
|
33
|
+
};
|
|
34
|
+
applySelectAndRelations(findManyArgs, config);
|
|
35
|
+
const [data, totalItems] = await Promise.all([
|
|
36
|
+
delegate.findMany(findManyArgs),
|
|
37
|
+
delegate.count({ where }),
|
|
38
|
+
]);
|
|
39
|
+
const totalPages = Math.max(Math.ceil(totalItems / limit), 1);
|
|
40
|
+
return {
|
|
41
|
+
data,
|
|
42
|
+
meta: {
|
|
43
|
+
itemsPerPage: limit,
|
|
44
|
+
totalItems,
|
|
45
|
+
currentPage: page,
|
|
46
|
+
totalPages,
|
|
47
|
+
sortBy: sortBy ?? [],
|
|
48
|
+
...(query.search && { search: query.search }),
|
|
49
|
+
...(query.filter && { filter: flattenFilter(query.filter) }),
|
|
50
|
+
},
|
|
51
|
+
links: (0, link_builder_1.buildOffsetLinks)(query, page, limit, totalPages),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function paginateCursor(query, delegate, config) {
|
|
55
|
+
const limit = resolveLimit(query.limit, config);
|
|
56
|
+
const cursorColumn = (config.cursorColumn ?? 'id');
|
|
57
|
+
const sortBy = query.sortBy ?? config.defaultSortBy;
|
|
58
|
+
if (sortBy) {
|
|
59
|
+
(0, sort_builder_1.validateSortColumns)(sortBy, config.sortableColumns);
|
|
60
|
+
}
|
|
61
|
+
const orderBy = (0, sort_builder_1.buildOrderBy)(sortBy, config.nullSort);
|
|
62
|
+
const where = buildWhere(query, config);
|
|
63
|
+
const findManyArgs = {
|
|
64
|
+
where,
|
|
65
|
+
orderBy,
|
|
66
|
+
take: limit + 1,
|
|
67
|
+
};
|
|
68
|
+
if (query.after) {
|
|
69
|
+
const cursorValue = (0, cursor_encoder_1.decodeCursor)(query.after);
|
|
70
|
+
findManyArgs.cursor = cursorValue;
|
|
71
|
+
findManyArgs.skip = 1;
|
|
72
|
+
}
|
|
73
|
+
else if (query.before) {
|
|
74
|
+
const cursorValue = (0, cursor_encoder_1.decodeCursor)(query.before);
|
|
75
|
+
findManyArgs.cursor = cursorValue;
|
|
76
|
+
findManyArgs.skip = 1;
|
|
77
|
+
findManyArgs.take = -(limit + 1);
|
|
78
|
+
}
|
|
79
|
+
applySelectAndRelations(findManyArgs, config);
|
|
80
|
+
let data = await delegate.findMany(findManyArgs);
|
|
81
|
+
let hasPreviousPage;
|
|
82
|
+
let hasNextPage;
|
|
83
|
+
if (query.before) {
|
|
84
|
+
// Navigating backward — we fetched limit+1 items backward
|
|
85
|
+
hasNextPage = true; // We came from a forward page, so there's always a next
|
|
86
|
+
hasPreviousPage = data.length > limit;
|
|
87
|
+
if (hasPreviousPage) {
|
|
88
|
+
data = data.slice(data.length - limit);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (query.after) {
|
|
92
|
+
// Navigating forward from a cursor — there's always a previous
|
|
93
|
+
hasPreviousPage = true;
|
|
94
|
+
hasNextPage = data.length > limit;
|
|
95
|
+
if (hasNextPage) {
|
|
96
|
+
data.pop();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// First page (no cursor) — no previous
|
|
101
|
+
hasPreviousPage = false;
|
|
102
|
+
hasNextPage = data.length > limit;
|
|
103
|
+
if (hasNextPage) {
|
|
104
|
+
data.pop();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const startCursor = data.length > 0 ? (0, cursor_encoder_1.encodeCursor)(data[0], cursorColumn) : null;
|
|
108
|
+
const endCursor = data.length > 0 ? (0, cursor_encoder_1.encodeCursor)(data[data.length - 1], cursorColumn) : null;
|
|
109
|
+
const meta = {
|
|
110
|
+
itemsPerPage: limit,
|
|
111
|
+
hasNextPage,
|
|
112
|
+
hasPreviousPage,
|
|
113
|
+
startCursor,
|
|
114
|
+
endCursor,
|
|
115
|
+
sortBy: sortBy ?? [],
|
|
116
|
+
...(query.search && { search: query.search }),
|
|
117
|
+
...(query.filter && { filter: flattenFilter(query.filter) }),
|
|
118
|
+
};
|
|
119
|
+
if (config.withTotalCount) {
|
|
120
|
+
meta.totalItems = await delegate.count({ where });
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
data,
|
|
124
|
+
meta,
|
|
125
|
+
links: (0, link_builder_1.buildCursorLinks)(query, limit, endCursor, startCursor, hasNextPage, hasPreviousPage),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function buildWhere(query, config) {
|
|
129
|
+
const conditions = [];
|
|
130
|
+
if (config.where && Object.keys(config.where).length > 0) {
|
|
131
|
+
conditions.push(config.where);
|
|
132
|
+
}
|
|
133
|
+
if (query.filter && config.filterableColumns) {
|
|
134
|
+
const filterWhere = (0, filter_parser_1.parseFilters)(query.filter, config.filterableColumns);
|
|
135
|
+
if (Object.keys(filterWhere).length > 0) {
|
|
136
|
+
conditions.push(filterWhere);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (query.search && config.searchableColumns) {
|
|
140
|
+
const searchWhere = (0, search_builder_1.buildSearchCondition)(query.search, config.searchableColumns);
|
|
141
|
+
if (Object.keys(searchWhere).length > 0) {
|
|
142
|
+
conditions.push(searchWhere);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (conditions.length === 0)
|
|
146
|
+
return {};
|
|
147
|
+
if (conditions.length === 1)
|
|
148
|
+
return conditions[0];
|
|
149
|
+
return { AND: conditions };
|
|
150
|
+
}
|
|
151
|
+
function applySelectAndRelations(findManyArgs, config) {
|
|
152
|
+
if (config.select) {
|
|
153
|
+
// Build select object from column list
|
|
154
|
+
const selectObj = Object.fromEntries(config.select.map((col) => [col, true]));
|
|
155
|
+
// Merge relations into select if both are present
|
|
156
|
+
if (config.relations) {
|
|
157
|
+
for (const [key, value] of Object.entries(config.relations)) {
|
|
158
|
+
selectObj[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
findManyArgs.select = selectObj;
|
|
162
|
+
}
|
|
163
|
+
else if (config.relations) {
|
|
164
|
+
findManyArgs.include = config.relations;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function resolveLimit(queryLimit, config) {
|
|
168
|
+
const defaultLimit = config.defaultLimit ?? pagination_constants_1.DEFAULT_LIMIT;
|
|
169
|
+
const maxLimit = config.maxLimit ?? pagination_constants_1.DEFAULT_MAX_LIMIT;
|
|
170
|
+
const limit = queryLimit ?? defaultLimit;
|
|
171
|
+
return Math.min(Math.max(limit, 1), maxLimit);
|
|
172
|
+
}
|
|
173
|
+
function flattenFilter(filter) {
|
|
174
|
+
const flat = {};
|
|
175
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
176
|
+
flat[key] = Array.isArray(value) ? value.join(',') : value;
|
|
177
|
+
}
|
|
178
|
+
return flat;
|
|
179
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Reflector } from '@nestjs/core';
|
|
2
|
+
import { PaginationModuleOptions } from './interfaces/pagination-options.interface';
|
|
3
|
+
import { PaginateQuery } from './interfaces/paginate-query.interface';
|
|
4
|
+
import { PaginateConfig } from './interfaces/paginate-config.interface';
|
|
5
|
+
import { Paginated, CursorPaginated } from './interfaces/paginated.interface';
|
|
6
|
+
export declare class PaginateService {
|
|
7
|
+
private readonly moduleOptions;
|
|
8
|
+
private readonly reflector;
|
|
9
|
+
constructor(moduleOptions: PaginationModuleOptions | undefined, reflector: Reflector);
|
|
10
|
+
paginate<T>(query: PaginateQuery, delegate: {
|
|
11
|
+
findMany: (args: any) => Promise<T[]>;
|
|
12
|
+
count: (args: any) => Promise<number>;
|
|
13
|
+
}, config: PaginateConfig<T>, handler?: Function): Promise<Paginated<T> | CursorPaginated<T>>;
|
|
14
|
+
private mergeConfig;
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.PaginateService = void 0;
|
|
16
|
+
const common_1 = require("@nestjs/common");
|
|
17
|
+
const core_1 = require("@nestjs/core");
|
|
18
|
+
const pagination_constants_1 = require("./pagination.constants");
|
|
19
|
+
const paginate_1 = require("./paginate");
|
|
20
|
+
const paginate_defaults_decorator_1 = require("./decorators/paginate-defaults.decorator");
|
|
21
|
+
let PaginateService = class PaginateService {
|
|
22
|
+
constructor(moduleOptions = {}, reflector) {
|
|
23
|
+
this.moduleOptions = moduleOptions;
|
|
24
|
+
this.reflector = reflector;
|
|
25
|
+
}
|
|
26
|
+
async paginate(query, delegate, config, handler) {
|
|
27
|
+
const mergedConfig = this.mergeConfig(config, handler);
|
|
28
|
+
return (0, paginate_1.paginate)(query, delegate, mergedConfig);
|
|
29
|
+
}
|
|
30
|
+
mergeConfig(config, handler) {
|
|
31
|
+
// Read @PaginateDefaults metadata from handler
|
|
32
|
+
const defaults = handler
|
|
33
|
+
? this.reflector.get(paginate_defaults_decorator_1.PAGINATE_DEFAULTS_KEY, handler)
|
|
34
|
+
: undefined;
|
|
35
|
+
// Priority: config (highest) > @PaginateDefaults (medium) > module options (lowest)
|
|
36
|
+
return {
|
|
37
|
+
...config,
|
|
38
|
+
defaultLimit: config.defaultLimit ??
|
|
39
|
+
defaults?.defaultLimit ??
|
|
40
|
+
this.moduleOptions.defaultLimit,
|
|
41
|
+
maxLimit: config.maxLimit ??
|
|
42
|
+
defaults?.maxLimit ??
|
|
43
|
+
this.moduleOptions.maxLimit,
|
|
44
|
+
defaultSortBy: config.defaultSortBy ??
|
|
45
|
+
defaults?.defaultSortBy ??
|
|
46
|
+
this.moduleOptions.defaultSortBy,
|
|
47
|
+
paginationType: config.paginationType ??
|
|
48
|
+
defaults?.paginationType ??
|
|
49
|
+
this.moduleOptions.defaultPaginationType,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
exports.PaginateService = PaginateService;
|
|
54
|
+
exports.PaginateService = PaginateService = __decorate([
|
|
55
|
+
(0, common_1.Injectable)(),
|
|
56
|
+
__param(0, (0, common_1.Optional)()),
|
|
57
|
+
__param(0, (0, common_1.Inject)(pagination_constants_1.PAGINATION_MODULE_OPTIONS)),
|
|
58
|
+
__metadata("design:paramtypes", [Object, core_1.Reflector])
|
|
59
|
+
], PaginateService);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PAGINATION_TYPE = exports.DEFAULT_PAGE = exports.DEFAULT_MAX_LIMIT = exports.DEFAULT_LIMIT = exports.PAGINATION_MODULE_OPTIONS = void 0;
|
|
4
|
+
exports.PAGINATION_MODULE_OPTIONS = 'PAGINATION_MODULE_OPTIONS';
|
|
5
|
+
exports.DEFAULT_LIMIT = 20;
|
|
6
|
+
exports.DEFAULT_MAX_LIMIT = 100;
|
|
7
|
+
exports.DEFAULT_PAGE = 1;
|
|
8
|
+
exports.DEFAULT_PAGINATION_TYPE = 'offset';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DynamicModule } from '@nestjs/common';
|
|
2
|
+
import { PaginationModuleOptions, PaginationModuleAsyncOptions } from './interfaces/pagination-options.interface';
|
|
3
|
+
export declare class PaginationModule {
|
|
4
|
+
static forRoot(options?: PaginationModuleOptions): DynamicModule;
|
|
5
|
+
static forRootAsync(options: PaginationModuleAsyncOptions): DynamicModule;
|
|
6
|
+
}
|