Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions src/modules/creator/creator.controller.ts
Original file line number Diff line number Diff line change
@@ -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'
);
}
}
8 changes: 6 additions & 2 deletions src/modules/creators/creators.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
Expand Down
28 changes: 28 additions & 0 deletions src/modules/creators/creators.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
},
};
}
87 changes: 87 additions & 0 deletions src/utils/pagination-guard.utils.ts
Original file line number Diff line number Diff line change
@@ -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));
}
Loading