/**
* Standardized pagination utilities for API routes
*/
import type { PaginationMetadata } from "../../types/pagination";
/**
* Default pagination configuration
*/
export const PAGINATION_CONFIG = {
DEFAULT_PAGE: 1,
DEFAULT_PAGE_SIZE: 100,
MIN_PAGE_SIZE: 10,
MAX_PAGE_SIZE: 500,
} as const;
/**
* Parsed and validated pagination parameters
*/
export interface PaginationParams {
page: number;
pageSize: number;
offset: number;
}
/**
* Result containing pagination metadata and validation
*/
export interface PaginationResult {
params: PaginationParams;
metadata: Omit<PaginationMetadata, 'totalPages' | 'totalItems' | 'hasNextPage'>;
}
/**
* Error result for invalid pagination parameters
*/
export interface PaginationError {
error: string;
field: string;
}
/**
* Parses and validates pagination parameters from query strings
*/
export function parsePaginationParams(
pageParam?: string,
pageSizeParam?: string,
customMinPageSize?: number,
customMaxPageSize?: number
): PaginationParams | PaginationError {
const minPageSize = customMinPageSize ?? PAGINATION_CONFIG.MIN_PAGE_SIZE;
const maxPageSize = customMaxPageSize ?? PAGINATION_CONFIG.MAX_PAGE_SIZE;
// Parse page parameter
const page = parseInt(pageParam || String(PAGINATION_CONFIG.DEFAULT_PAGE), 10);
if (isNaN(page) || page < 1) {
return {
error: "Invalid page parameter: must be a positive integer",
field: "page"
};
}
// Parse pageSize parameter
const pageSize = parseInt(pageSizeParam || String(PAGINATION_CONFIG.DEFAULT_PAGE_SIZE), 10);
if (isNaN(pageSize)) {
return {
error: "Invalid pageSize parameter: must be a number",
field: "pageSize"
};
}
// Validate pageSize limits
if (pageSize < minPageSize || pageSize > maxPageSize) {
return {
error: `Invalid pageSize parameter: must be between ${minPageSize} and ${maxPageSize}`,
field: "pageSize"
};
}
// Calculate offset
const offset = (page - 1) * pageSize;
return {
page,
pageSize,
offset
};
}
/**
* Creates complete pagination metadata with total counts
*/
export function createPaginationMetadata(
params: PaginationParams,
totalItems: number
): PaginationMetadata {
const totalPages = Math.ceil(totalItems / params.pageSize);
return {
currentPage: params.page,
pageSize: params.pageSize,
totalPages,
totalItems,
hasNextPage: params.page < totalPages,
hasPreviousPage: params.page > 1
};
}
/**
* Convenience function that combines parsing and metadata creation
*/
export function processPagination(
pageParam?: string,
pageSizeParam?: string,
totalItems?: number,
customMinPageSize?: number,
customMaxPageSize?: number
): {
params: PaginationParams;
metadata?: PaginationMetadata | undefined;
error?: PaginationError
} {
const parseResult = parsePaginationParams(
pageParam,
pageSizeParam,
customMinPageSize,
customMaxPageSize
);
if ('error' in parseResult) {
return {
params: {
page: PAGINATION_CONFIG.DEFAULT_PAGE,
pageSize: PAGINATION_CONFIG.DEFAULT_PAGE_SIZE,
offset: 0
},
error: parseResult
};
}
const metadata = totalItems !== undefined
? createPaginationMetadata(parseResult, totalItems)
: undefined;
return {
params: parseResult,
metadata
};
}
/**
* Validates that a page number is within valid range for total items
*/
export function validatePageRange(page: number, pageSize: number, totalItems: number): boolean {
if (totalItems === 0) return page === 1;
const maxPage = Math.ceil(totalItems / pageSize);
return page >= 1 && page <= maxPage;
}
/**
* Creates a standardized pagination response error
*/
export function createPaginationError(error: PaginationError): {
error: string;
details: string;
} {
return {
error: "Invalid pagination parameters",
details: error.error
};
}