From 2b6775898ae018951acfa7874f1d7cf5740b7d1c Mon Sep 17 00:00:00 2001 From: DevALVIN-24 Date: Thu, 27 Aug 2026 13:35:23 +0100 Subject: [PATCH] fix(search): enforce pagination bounds on search list endpoints --- src/search/dto/pagination.dto.ts | 20 ++++++++++++++++++++ src/search/search.controller.ts | 12 ++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 src/search/dto/pagination.dto.ts diff --git a/src/search/dto/pagination.dto.ts b/src/search/dto/pagination.dto.ts new file mode 100644 index 00000000..a7ffafec --- /dev/null +++ b/src/search/dto/pagination.dto.ts @@ -0,0 +1,20 @@ +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, Max, Min } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export class PaginationDto { + @ApiPropertyOptional({ description: 'The page number', default: 1, minimum: 1 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page?: number = 1; + + @ApiPropertyOptional({ description: 'The maximum number of results (max 100)', default: 20, minimum: 1, maximum: 100 }) + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + limit?: number = 20; +} diff --git a/src/search/search.controller.ts b/src/search/search.controller.ts index a262eefd..6871ac8e 100644 --- a/src/search/search.controller.ts +++ b/src/search/search.controller.ts @@ -3,6 +3,7 @@ import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Throttle } from '@nestjs/throttler'; import { THROTTLE } from '../common/constants/throttle.constants'; import { SearchService } from './search.service'; +import { PaginationDto } from './dto/pagination.dto'; @ApiTags('Search') @Throttle({ default: THROTTLE.SEARCH }) @@ -29,8 +30,8 @@ export class SearchController { example: '{"category":"programming","level":"beginner"}', }) @ApiQuery({ name: 'sort', required: false, example: 'relevance' }) - @ApiQuery({ name: 'page', required: false, example: 1 }) - @ApiQuery({ name: 'limit', required: false, example: 20 }) + @ApiQuery({ name: 'page', required: false, description: 'The page number. Default is 1.', example: 1, type: Number }) + @ApiQuery({ name: 'limit', required: false, description: 'The maximum number of results. Default is 20. Max is 100.', example: 20, type: Number }) @ApiResponse({ status: 200, description: 'Search results', @@ -49,10 +50,9 @@ export class SearchController { @ApiResponse({ status: 503, description: 'Search is temporarily unavailable' }) async search( @Query('q') query: string, + @Query() paginationDto: PaginationDto, @Query('filters') filters?: string, @Query('sort') sort?: string, - @Query('page') page?: string, - @Query('limit') limit?: string, ): Promise { let parsedFilters: Record = {}; if (filters) { @@ -63,8 +63,8 @@ export class SearchController { } } - const pageNum = page ? parseInt(page, 10) : 1; - const limitNum = limit ? parseInt(limit, 10) : 20; + const pageNum = paginationDto.page ?? 1; + const limitNum = paginationDto.limit ?? 20; return this.searchService.search(query, parsedFilters, sort, pageNum, limitNum); }