From ba4bf2d2f8609705e59d74197252d46ca808cb32 Mon Sep 17 00:00:00 2001 From: yunusabdul38 Date: Fri, 27 Mar 2026 10:19:23 +0100 Subject: [PATCH] feat: implement pagination validation and error handling for creator list --- src/modules/creator/creator.controller.ts | 44 ++++++++++-- src/modules/creators/creators.schemas.ts | 8 ++- src/modules/creators/creators.utils.ts | 28 ++++++++ src/utils/pagination-guard.utils.ts | 87 +++++++++++++++++++++++ 4 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 src/utils/pagination-guard.utils.ts diff --git a/src/modules/creator/creator.controller.ts b/src/modules/creator/creator.controller.ts index bdde209..cea5acb 100644 --- a/src/modules/creator/creator.controller.ts +++ b/src/modules/creator/creator.controller.ts @@ -1,31 +1,61 @@ // src/modules/creator/creator.controller.ts import { Request, Response } from 'express'; -import { sendPaginatedSuccess, sendError, ErrorCode } from '../../utils/api-response.utils'; +import { + sendPaginatedSuccess, + sendError, + ErrorCode, +} from '../../utils/api-response.utils'; import { getPaginatedCreators } from './creator.service'; import { parseCreatorSortOptions } from './creator.utils'; +import { + validatePageSize, + PageSizeExceededError, +} from '../../utils/pagination-guard.utils'; export async function listCreators(req: Request, res: Response) { try { const page = parseInt(req.query.page as string) || 1; - const limit = parseInt(req.query.limit as string) || 10; + const limitInput = parseInt(req.query.limit as string) || 10; const sortBy = req.query.sortBy as string; const sortOrder = req.query.sortOrder as string; - if (page < 1 || limit < 1) { - return sendError(res, 400, ErrorCode.VALIDATION_ERROR, 'Invalid pagination parameters'); + if (page < 1) { + return sendError( + res, + 400, + ErrorCode.VALIDATION_ERROR, + 'Invalid pagination parameters' + ); } + // Validate page size using the reusable guard + const limit = validatePageSize(limitInput); + const sort = parseCreatorSortOptions(sortBy, sortOrder); - + const { creators, meta } = await getPaginatedCreators({ page, limit, sort, }); - return sendPaginatedSuccess(res, creators, meta, 200, 'Creators retrieved successfully'); + return sendPaginatedSuccess( + res, + creators, + meta, + 200, + 'Creators retrieved successfully' + ); } catch (error) { + if (error instanceof PageSizeExceededError) { + return sendError(res, 400, ErrorCode.VALIDATION_ERROR, error.message); + } console.error('Error listing creators:', error); - return sendError(res, 500, ErrorCode.INTERNAL_ERROR, 'Failed to retrieve creators'); + return sendError( + res, + 500, + ErrorCode.INTERNAL_ERROR, + 'Failed to retrieve creators' + ); } } diff --git a/src/modules/creators/creators.schemas.ts b/src/modules/creators/creators.schemas.ts index 57ca5ef..a78b462 100644 --- a/src/modules/creators/creators.schemas.ts +++ b/src/modules/creators/creators.schemas.ts @@ -3,6 +3,10 @@ import { CREATOR_LIST_SORT_OPTIONS, CREATOR_LIST_SORT_ORDERS, } from './creators.sort'; +import { + MAX_PAGE_SIZE, + MIN_PAGE_SIZE, +} from '../../constants/pagination.constants'; /** * Validation schema for creator list query parameters. @@ -20,8 +24,8 @@ export const CreatorListQuerySchema = z.object({ .optional() .default('20') .transform(val => parseInt(val, 10)) - .refine(val => val > 0 && val <= 100, { - message: 'Limit must be between 1 and 100', + .refine(val => val >= MIN_PAGE_SIZE && val <= MAX_PAGE_SIZE, { + message: `Limit must be between ${MIN_PAGE_SIZE} and ${MAX_PAGE_SIZE}`, }), offset: z .string() diff --git a/src/modules/creators/creators.utils.ts b/src/modules/creators/creators.utils.ts index dac7b96..5bafe79 100644 --- a/src/modules/creators/creators.utils.ts +++ b/src/modules/creators/creators.utils.ts @@ -2,6 +2,7 @@ import { prisma } from '../../utils/prisma.utils'; import { CreatorProfile } from '../../types/profile.types'; import { CreatorListQueryType } from './creators.schemas'; import { mapCreatorListSort } from './creators.sort'; +import { CreatorListResponse } from './creators.serializers'; type CreatorListWhere = { isVerified?: boolean; @@ -51,3 +52,30 @@ export async function fetchCreatorList( return [creators as CreatorProfile[], total]; } + +/** + * Creates a consistent empty response for creator list endpoints. + * + * Ensures empty list responses maintain the same shape as paginated responses, + * allowing clients to rely on consistent structure even when no data exists. + * + * @param query - Validated query parameters used for the request + * @returns Empty creator list response with proper pagination metadata + * + * @example + * const emptyResponse = createEmptyCreatorListResponse(validatedQuery); + * // Returns: { creators: [], pagination: { limit, offset, total: 0, hasMore: false } } + */ +export function createEmptyCreatorListResponse( + query: CreatorListQueryType +): CreatorListResponse { + return { + creators: [], + pagination: { + limit: query.limit, + offset: query.offset, + total: 0, + hasMore: false, + }, + }; +} diff --git a/src/utils/pagination-guard.utils.ts b/src/utils/pagination-guard.utils.ts new file mode 100644 index 0000000..140e02b --- /dev/null +++ b/src/utils/pagination-guard.utils.ts @@ -0,0 +1,87 @@ +import { MAX_PAGE_SIZE, MIN_PAGE_SIZE } from '../constants/pagination.constants'; + +/** + * Error thrown when page size exceeds the maximum allowed limit. + */ +export class PageSizeExceededError extends Error { + constructor(limit: number, maxLimit: number = MAX_PAGE_SIZE) { + super(`Page size limit (${limit}) exceeds maximum allowed (${maxLimit})`); + this.name = 'PageSizeExceededError'; + } +} + +/** + * Error thrown when page size is below the minimum allowed limit. + */ +export class PageSizeTooSmallError extends Error { + constructor(limit: number, minLimit: number = MIN_PAGE_SIZE) { + super(`Page size limit (${limit}) is below minimum allowed (${minLimit})`); + this.name = 'PageSizeTooSmallError'; + } +} + +/** + * Validates that a page size limit is within allowed bounds. + * + * @param limit - The page size limit to validate + * @param options - Optional configuration for custom min/max values + * @returns The validated limit (unchanged) + * @throws {PageSizeExceededError} If limit exceeds maximum + * @throws {PageSizeTooSmallError} If limit is below minimum + * + * @example + * // Basic usage with default bounds + * validatePageSize(50); // returns 50 + * validatePageSize(150); // throws PageSizeExceededError + * + * @example + * // Custom bounds + * validatePageSize(30, { max: 50 }); // returns 30 + */ +export function validatePageSize( + limit: number, + options: { min?: number; max?: number } = {} +): number { + const min = options.min ?? MIN_PAGE_SIZE; + const max = options.max ?? MAX_PAGE_SIZE; + + if (limit < min) { + throw new PageSizeTooSmallError(limit, min); + } + + if (limit > max) { + throw new PageSizeExceededError(limit, max); + } + + return limit; +} + +/** + * Clamps a page size limit to the allowed range. + * + * Unlike validatePageSize, this function will not throw errors. + * Instead, it returns a clamped value within the valid range. + * + * @param limit - The page size limit to clamp + * @param options - Optional configuration for custom min/max values + * @returns The clamped limit within valid range + * + * @example + * // Basic usage + * clampPageSize(150); // returns 100 + * clampPageSize(0); // returns 1 + * clampPageSize(50); // returns 50 + * + * @example + * // Custom bounds + * clampPageSize(200, { max: 50 }); // returns 50 + */ +export function clampPageSize( + limit: number, + options: { min?: number; max?: number } = {} +): number { + const min = options.min ?? MIN_PAGE_SIZE; + const max = options.max ?? MAX_PAGE_SIZE; + + return Math.max(min, Math.min(limit, max)); +}