@hedhog/pagination 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@hedhog/pagination",
3
+ "version": "0.0.1",
4
+ "private": false,
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc && npm version patch",
8
+ "prod": "npm run build && npm publish --access public"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "MIT",
13
+ "description": "",
14
+ "peerDependencies": {
15
+ "@prisma/client": "^5.17.0",
16
+ "@hedhog/prisma": "latest",
17
+ "class-transformer": "^0.5.1",
18
+ "class-validator": "^0.14.1"
19
+ }
20
+ }
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_PAGE = 1;
2
+ export const DEFAULT_PAGE_SIZE = 20;
@@ -0,0 +1,62 @@
1
+ import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2
+ import {
3
+ DEFAULT_PAGE,
4
+ DEFAULT_PAGE_SIZE,
5
+ } from '../constants/pagination.constants';
6
+ import { PageOrderDirection, PaginationField } from '../enums/patination.enums';
7
+ import { PaginationType } from '../types/pagination.types';
8
+
9
+ export const Pagination = createParamDecorator(
10
+ (data: PaginationField, ctx: ExecutionContext): PaginationType => {
11
+ const request = ctx.switchToHttp().getRequest();
12
+
13
+ const defaultOptions: PaginationType = {
14
+ page: DEFAULT_PAGE,
15
+ pageSize: DEFAULT_PAGE_SIZE,
16
+ search: '',
17
+ sortField: 'id',
18
+ sortOrder: PageOrderDirection.Asc,
19
+ fields: '',
20
+ };
21
+
22
+ const requestData = {
23
+ ...defaultOptions,
24
+ ...(request.body || {}),
25
+ ...(request.query || {}),
26
+ };
27
+
28
+ const {
29
+ page = defaultOptions.page,
30
+ pageSize = defaultOptions.pageSize,
31
+ search = defaultOptions.search,
32
+ sortField = defaultOptions.sortField,
33
+ sortOrder = defaultOptions.sortOrder,
34
+ fields = defaultOptions.fields,
35
+ } = requestData;
36
+
37
+ const validSortOrder = Object.values(PageOrderDirection).includes(sortOrder)
38
+ ? sortOrder
39
+ : defaultOptions.sortOrder;
40
+
41
+ if (data) {
42
+ switch (data) {
43
+ case PaginationField.Page:
44
+ case PaginationField.PageSize:
45
+ return requestData[data] ? +requestData[data] : defaultOptions[data];
46
+ case PaginationField.OrderDirection:
47
+ return requestData[data] || defaultOptions[data];
48
+ default:
49
+ return requestData[data];
50
+ }
51
+ }
52
+
53
+ return {
54
+ page: +page,
55
+ pageSize: +pageSize,
56
+ search,
57
+ sortField,
58
+ sortOrder: validSortOrder,
59
+ fields,
60
+ };
61
+ },
62
+ );
@@ -0,0 +1,31 @@
1
+ import { Transform } from 'class-transformer';
2
+ import { IsEnum, IsInt, IsOptional, IsString } from 'class-validator';
3
+ import { PageOrderDirection } from '../enums/patination.enums';
4
+ export class PaginationDTO {
5
+ @IsOptional()
6
+ @Transform((value) => Number(value))
7
+ @IsInt({ message: 'page must be an integer' })
8
+ page: number;
9
+
10
+ @IsOptional()
11
+ @Transform((value) => Number(value))
12
+ @IsInt({ message: 'pageSize must be an integer' })
13
+ pageSize: number;
14
+
15
+ @IsOptional()
16
+ @IsString({ message: 'search must be a string' })
17
+ search: string;
18
+
19
+ @IsOptional()
20
+ @IsString({ message: 'field must be a string' })
21
+ sortField: string;
22
+
23
+ @IsOptional()
24
+ @IsString({ message: 'sortOrder must be a string' })
25
+ @IsEnum(PageOrderDirection, { message: 'sortOrder is not valid' })
26
+ sortOrder: PageOrderDirection;
27
+
28
+ @IsOptional()
29
+ @IsString({ message: 'fields must be a string' })
30
+ fields: string;
31
+ }
@@ -0,0 +1,12 @@
1
+ export enum PageOrderDirection {
2
+ Asc = 'asc',
3
+ Desc = 'desc',
4
+ }
5
+
6
+ export enum PaginationField {
7
+ Page = 'page',
8
+ PageSize = 'pageSize',
9
+ OrderField = 'orderField',
10
+ OrderDirection = 'orderDirection',
11
+ Search = 'search',
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from './constants/pagination.constants';
2
+ export * from './decorator/pagination.decorator';
3
+ export * from './dto/pagination.dto';
4
+ export * from './enums/patination.enums';
5
+ export * from './pagination.module';
6
+ export * from './pagination.service';
7
+ export type * from './types/pagination.types';
@@ -0,0 +1,9 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { PaginationService } from './pagination.service';
3
+
4
+ @Module({
5
+ imports: [],
6
+ providers: [PaginationService],
7
+ exports: [PaginationService],
8
+ })
9
+ export class PaginationModule {}
@@ -0,0 +1,18 @@
1
+ import { Test, TestingModule } from '@nestjs/testing';
2
+ import { PaginationService } from './pagination.service';
3
+
4
+ describe('PaginationService', () => {
5
+ let service: PaginationService;
6
+
7
+ beforeEach(async () => {
8
+ const module: TestingModule = await Test.createTestingModule({
9
+ providers: [PaginationService],
10
+ }).compile();
11
+
12
+ service = module.get<PaginationService>(PaginationService);
13
+ });
14
+
15
+ it('should be defined', () => {
16
+ expect(service).toBeDefined();
17
+ });
18
+ });
@@ -0,0 +1,128 @@
1
+ import { BadRequestException, Injectable, Logger } from '@nestjs/common';
2
+ import {
3
+ DEFAULT_PAGE,
4
+ DEFAULT_PAGE_SIZE,
5
+ } from './constants/pagination.constants';
6
+ import { PageOrderDirection } from './enums/patination.enums';
7
+ import type {
8
+ BaseModel,
9
+ FindManyArgs,
10
+ PaginatedResult,
11
+ PaginationParams,
12
+ } from './types/pagination.types';
13
+
14
+ @Injectable()
15
+ export class PaginationService {
16
+ private readonly logger = new Logger(PaginationService.name);
17
+
18
+ async paginate<T, M extends BaseModel>(
19
+ model: M,
20
+ paginationParams: PaginationParams,
21
+ customQuery?: FindManyArgs<M>,
22
+ ): Promise<PaginatedResult<T>> {
23
+ try {
24
+ const page = Number(paginationParams.page || DEFAULT_PAGE);
25
+ const pageSize = Number(paginationParams.pageSize || DEFAULT_PAGE_SIZE);
26
+ const search = paginationParams.search || null;
27
+ const sortField = paginationParams.sortField || null;
28
+ const sortOrder = paginationParams.sortOrder || PageOrderDirection.Asc;
29
+ const fields = paginationParams.fields
30
+ ? paginationParams.fields.split(',')
31
+ : null;
32
+
33
+ if (page < 1 || pageSize < 1) {
34
+ throw new BadRequestException(
35
+ 'Page and pageSize must be greater than 0',
36
+ );
37
+ }
38
+
39
+ let selectCondition = undefined;
40
+ let sortOrderCondition: any = {
41
+ id: paginationParams.sortOrder || PageOrderDirection.Asc,
42
+ };
43
+
44
+ if (search && sortField) {
45
+ const fieldNames = this.extractFieldNames(model);
46
+
47
+ if (!fieldNames.includes(sortField)) {
48
+ throw new BadRequestException(
49
+ `Invalid field: ${sortField}. Valid columns are: ${fieldNames.join(', ')}`,
50
+ );
51
+ }
52
+
53
+ if (typeof sortField !== 'string') {
54
+ throw new BadRequestException('Field must be a string');
55
+ }
56
+ }
57
+
58
+ if (sortField) {
59
+ sortOrderCondition = { [sortField]: sortOrder };
60
+ }
61
+
62
+ if (fields) {
63
+ const fieldNames = this.extractFieldNames(model);
64
+ const invalidFields = fields.filter(
65
+ (field) => !fieldNames.includes(field),
66
+ );
67
+ if (invalidFields.length > 0) {
68
+ throw new BadRequestException(
69
+ `Invalid fields: ${invalidFields.join(', ')}. Valid columns are: ${fieldNames.join(', ')}`,
70
+ );
71
+ }
72
+ selectCondition = fields.reduce((acc, field) => {
73
+ acc[field] = true;
74
+ return acc;
75
+ }, {});
76
+ }
77
+
78
+ const skip = page > 0 ? pageSize * (page - 1) : 0;
79
+
80
+ const query = {
81
+ select: selectCondition,
82
+ where: customQuery?.where || {},
83
+ orderBy: sortOrderCondition,
84
+ take: pageSize,
85
+ skip,
86
+ };
87
+
88
+ const [total, data] = await Promise.all([
89
+ model.count({ where: customQuery?.where || {} }),
90
+ model.findMany(query),
91
+ ]);
92
+
93
+ const lastPage = Math.ceil(total / pageSize);
94
+
95
+ return {
96
+ total,
97
+ lastPage,
98
+ currentPage: page,
99
+ pageSize,
100
+ prev: page > 1 ? page - 1 : null,
101
+ next: page < lastPage ? page + 1 : null,
102
+ data,
103
+ };
104
+ } catch (error) {
105
+ this.logger.error('Pagination Error:', error);
106
+
107
+ if (error instanceof BadRequestException) {
108
+ throw error;
109
+ }
110
+
111
+ throw new BadRequestException(`Failed to paginate: ${error.message}`);
112
+ }
113
+ }
114
+
115
+ extractFieldNames(model: Record<string, any>): string[] {
116
+ const fieldNames: string[] = [];
117
+
118
+ const fields = model.fields;
119
+
120
+ for (const key in fields) {
121
+ if (fields.hasOwnProperty(key)) {
122
+ fieldNames.push(key);
123
+ }
124
+ }
125
+
126
+ return fieldNames;
127
+ }
128
+ }
@@ -0,0 +1,43 @@
1
+ import { PageOrderDirection } from '../enums/patination.enums';
2
+
3
+ export type PaginatedResult<T> = {
4
+ total: number;
5
+ lastPage: number;
6
+ currentPage: number;
7
+ pageSize: number;
8
+ prev: number | null;
9
+ next: number | null;
10
+ data: T[];
11
+ };
12
+
13
+ export type PaginationType = string | number | PaginationParams;
14
+
15
+ export type PaginateFunction = <K, T>(
16
+ model: any,
17
+ args?: K,
18
+ options?: PaginateOptions,
19
+ ) => Promise<PaginatedResult<T>>;
20
+
21
+ export type PaginationParams = {
22
+ page?: number;
23
+ pageSize?: number;
24
+ search?: string;
25
+ sortField?: string;
26
+ sortOrder?: PageOrderDirection;
27
+ fields: string;
28
+ };
29
+
30
+ export type PaginateOptions = {
31
+ page?: number | string;
32
+ pageSize?: number | string;
33
+ };
34
+
35
+ export type BaseModel = {
36
+ findMany: (args: any) => Promise<any[]>;
37
+ count: (args: any) => Promise<number>;
38
+ fields?: Record<string, any>;
39
+ };
40
+
41
+ export type FindManyArgs<M> = M extends { findMany: (args: infer A) => any }
42
+ ? A
43
+ : never;
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "outDir": "../../dist/libs/pagination"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
9
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "experimentalDecorators": true,
4
+ "target": "es2017",
5
+ "module": "commonjs",
6
+ "lib": ["es2017", "es7", "es6"],
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "sourceMap": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "strict": true,
13
+ "noImplicitAny": false,
14
+ "strictNullChecks": false,
15
+ "allowSyntheticDefaultImports": true,
16
+ "esModuleInterop": true,
17
+ "emitDecoratorMetadata": true
18
+ },
19
+ "exclude": ["node_modules", "dist"]
20
+ }