Skip to content

Commit 739b8c4

Browse files
committed
Advanced Pagination and Filtering System
1 parent 26d41c5 commit 739b8c4

7 files changed

Lines changed: 159 additions & 15 deletions

File tree

src/common/pagination/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './page.dto';
2+
export * from './page-meta.dto';
3+
export * from './page-options.dto';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { PageOptionsDto } from './page-options.dto';
2+
3+
export class PageMetaDto {
4+
readonly page: number;
5+
readonly limit: number;
6+
readonly itemCount: number;
7+
readonly pageCount: number;
8+
readonly hasPreviousPage: boolean;
9+
readonly hasNextPage: boolean;
10+
readonly nextCursor?: string;
11+
12+
constructor({
13+
pageOptionsDto,
14+
itemCount,
15+
nextCursor,
16+
}: {
17+
pageOptionsDto: PageOptionsDto;
18+
itemCount: number;
19+
nextCursor?: string;
20+
}) {
21+
this.page = pageOptionsDto.page;
22+
this.limit = pageOptionsDto.limit;
23+
this.itemCount = itemCount;
24+
this.pageCount = Math.ceil(this.itemCount / this.limit);
25+
this.hasPreviousPage = this.page > 1;
26+
this.hasNextPage = this.page < this.pageCount;
27+
this.nextCursor = nextCursor;
28+
}
29+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
2+
import { Type } from 'class-transformer';
3+
4+
export class PageOptionsDto {
5+
@Type(() => Number)
6+
@IsInt()
7+
@Min(1)
8+
@IsOptional()
9+
readonly page?: number = 1;
10+
11+
@Type(() => Number)
12+
@IsInt()
13+
@Min(1)
14+
@Max(100)
15+
@IsOptional()
16+
readonly limit?: number = 10;
17+
18+
@IsString()
19+
@IsOptional()
20+
readonly sort?: string;
21+
22+
@IsString()
23+
@IsOptional()
24+
readonly filter?: string;
25+
26+
@IsString()
27+
@IsOptional()
28+
readonly cursor?: string;
29+
}

src/common/pagination/page.dto.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { IsArray } from 'class-validator';
2+
import { PageMetaDto } from './page-meta.dto';
3+
4+
export class PageDto<T> {
5+
@IsArray()
6+
readonly data: T[];
7+
8+
readonly meta: PageMetaDto;
9+
10+
constructor(data: T[], meta: PageMetaDto) {
11+
this.data = data;
12+
this.meta = meta;
13+
}
14+
}

src/users/users.controller.ts

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1-
import { Controller, Get, Post, Body, Patch, Param, Delete, UseInterceptors, UploadedFile } from '@nestjs/common';
1+
import {
2+
Controller,
3+
Get,
4+
Post,
5+
Body,
6+
Patch,
7+
Param,
8+
Delete,
9+
UseInterceptors,
10+
UploadedFile,
11+
Query,
12+
} from '@nestjs/common';
213
import { FileInterceptor } from '@nestjs/platform-express';
314
import { fileFilter } from '../common/validators/file-upload.validator';
415
import { UsersService } from './users.service';
516
import { CreateUserDto } from './dto/create-user.dto';
617
import { UpdateUserDto } from './dto/update-user.dto';
18+
import { PageOptionsDto } from '../common/pagination';
719

820
type UploadedFile = {
921
originalname: string;
@@ -19,8 +31,8 @@ export class UsersController {
1931
}
2032

2133
@Get()
22-
findAll() {
23-
return this.usersService.findAll();
34+
findAll(@Query() pageOptionsDto: PageOptionsDto) {
35+
return this.usersService.findAll(pageOptionsDto);
2436
}
2537

2638
@Get(':id')
@@ -40,12 +52,20 @@ export class UsersController {
4052

4153
// Example: File upload with validation (avatar upload)
4254
@Post('avatar')
43-
@UseInterceptors(FileInterceptor('file', {
44-
fileFilter: fileFilter(['.png', '.jpg', '.jpeg'], ['image/png', 'image/jpeg']),
45-
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB
46-
}))
55+
@UseInterceptors(
56+
FileInterceptor('file', {
57+
fileFilter: fileFilter(
58+
['.png', '.jpg', '.jpeg'],
59+
['image/png', 'image/jpeg'],
60+
),
61+
limits: { fileSize: 2 * 1024 * 1024 }, // 2MB
62+
}),
63+
)
4764
uploadAvatar(@UploadedFile() file: UploadedFile) {
4865
// Only validation logic shown; file storage logic can be added as needed
49-
return { message: 'Avatar uploaded successfully', filename: file.originalname };
66+
return {
67+
message: 'Avatar uploaded successfully',
68+
filename: file.originalname,
69+
};
5070
}
5171
}

src/users/users.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { Module } from '@nestjs/common';
2+
import { TypeOrmModule } from '@nestjs/typeorm';
23
import { UsersService } from './users.service';
34
import { UsersController } from './users.controller';
5+
import { User } from './entities/user.entity';
46

57
@Module({
8+
imports: [TypeOrmModule.forFeature([User])],
69
controllers: [UsersController],
710
providers: [UsersService],
811
})

src/users/users.service.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,72 @@
11
import { Injectable } from '@nestjs/common';
2+
import { InjectRepository } from '@nestjs/typeorm';
3+
import { Repository } from 'typeorm';
4+
import { PageOptionsDto, PageDto, PageMetaDto } from '../../common/pagination';
25
import { CreateUserDto } from './dto/create-user.dto';
36
import { UpdateUserDto } from './dto/update-user.dto';
7+
import { User } from './entities/user.entity';
48

59
@Injectable()
610
export class UsersService {
11+
constructor(
12+
@InjectRepository(User)
13+
private readonly userRepository: Repository<User>,
14+
) {}
15+
716
create(createUserDto: CreateUserDto) {
8-
return 'This action adds a new user';
17+
const user = this.userRepository.create(createUserDto);
18+
return this.userRepository.save(user);
919
}
1020

11-
findAll() {
12-
return `This action returns all users`;
21+
async findAll(pageOptionsDto: PageOptionsDto): Promise<PageDto<User>> {
22+
const queryBuilder = this.userRepository.createQueryBuilder('user');
23+
24+
if (pageOptionsDto.filter) {
25+
queryBuilder.where('user.username LIKE :filter', {
26+
filter: `%${pageOptionsDto.filter}%`,
27+
});
28+
}
29+
30+
if (pageOptionsDto.cursor) {
31+
const cursorDate = new Date(pageOptionsDto.cursor);
32+
queryBuilder.andWhere('user.createdAt > :cursor', { cursor: cursorDate });
33+
}
34+
35+
if (pageOptionsDto.sort) {
36+
const [field, order] = pageOptionsDto.sort.split(':');
37+
queryBuilder.orderBy(`user.${field}`, order as 'ASC' | 'DESC');
38+
} else {
39+
queryBuilder.orderBy('user.createdAt', 'DESC');
40+
}
41+
42+
queryBuilder.take(pageOptionsDto.limit);
43+
44+
const itemCount = await queryBuilder.getCount();
45+
const { entities } = await queryBuilder.getRawAndEntities();
46+
47+
const nextCursor =
48+
entities.length > 0
49+
? entities[entities.length - 1].createdAt.toISOString()
50+
: null;
51+
const pageMetaDto = new PageMetaDto({
52+
itemCount,
53+
pageOptionsDto,
54+
nextCursor,
55+
});
56+
57+
return new PageDto(entities, pageMetaDto);
1358
}
1459

1560
findOne(id: string) {
16-
return `This action returns a user with id #${id}`;
61+
return this.userRepository.findOne({ where: { id } });
1762
}
1863

19-
update(id: string, updateUserDto: UpdateUserDto) {
20-
return `This action updates a user with id #${id}`;
64+
async update(id: string, updateUserDto: UpdateUserDto) {
65+
await this.userRepository.update(id, updateUserDto);
66+
return this.findOne(id);
2167
}
2268

2369
remove(id: string) {
24-
return `This action removes a user with id #${id}`;
70+
return this.userRepository.softDelete(id);
2571
}
2672
}

0 commit comments

Comments
 (0)