UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

305 lines 10.5 kB
/** * PaginationParameterExtractor - Intelligent extraction of pagination parameters * * Handles all possible ways AI agents might send pagination: * - Nested vs flat structures * - Different naming conventions (page/pageNumber/pageNum/offset) * - Different size parameters (page_size/pageSize/limit/perPage/count) * - Skip/take patterns * - Offset/limit patterns * - 0-based vs 1-based page numbers * * Created: January 8, 2025 */ import { getLogger } from '../logging/Logger.js'; const logger = getLogger(); export class PaginationParameterExtractor { // Common page number field names (in priority order) static PAGE_FIELDS = [ 'page', 'pageNumber', 'page_number', 'pageNum', 'page_num', 'currentPage', 'current_page', 'pageIndex', 'page_index', 'p' ]; // Common page size field names (in priority order) static SIZE_FIELDS = [ 'page_size', 'pageSize', 'size', 'limit', 'perPage', 'per_page', 'count', 'take', 'pageLimit', 'page_limit', 'records_per_page', 'recordsPerPage', 'items_per_page', 'itemsPerPage', 'l' ]; // Common offset field names static OFFSET_FIELDS = [ 'offset', 'skip', 'start', 'startIndex', 'start_index', 'from' ]; /** * Extract pagination parameters from various input formats */ static extract(input, defaults = {}) { const result = { page: defaults.page || 1, pageSize: defaults.pageSize || 10, offset: 0, warnings: [] }; if (!input || typeof input !== 'object') { return result; } // Try multiple extraction strategies (order matters!) const strategies = [ () => this.extractFromNestedPagination(input, result), () => this.extractFromOffsetLimit(input, result), // Check offset BEFORE flat () => this.extractFromSkipTake(input, result), // Check skip BEFORE flat () => this.extractFromFlatStructure(input, result), () => this.extractFromRootLevel(input, result), () => this.extractFromOptions(input, result), () => this.extractFromMixedFormats(input, result) ]; let extracted = false; for (const strategy of strategies) { if (strategy()) { extracted = true; break; } } // Validate and correct values this.validateAndCorrect(result); // Calculate offset from page result.offset = (result.page - 1) * result.pageSize; logger.info(`🎯 PAGINATION-EXTRACT: Detected format="${result.detectedFormat}", page=${result.page}, pageSize=${result.pageSize}, offset=${result.offset}`); return result; } /** * Extract from nested pagination object: { options: { pagination: { page, page_size } } } */ static extractFromNestedPagination(input, result) { const pagination = input.options?.pagination || input.pagination; if (!pagination) return false; const page = this.findFieldValue(pagination, this.PAGE_FIELDS); const size = this.findFieldValue(pagination, this.SIZE_FIELDS); if (page !== undefined || size !== undefined) { if (page !== undefined) result.page = page; if (size !== undefined) result.pageSize = size; result.detectedFormat = 'nested_pagination'; return true; } return false; } /** * Extract from flat options structure: { options: { page, page_size } } */ static extractFromFlatStructure(input, result) { const options = input.options; if (!options || typeof options !== 'object') return false; const page = this.findFieldValue(options, this.PAGE_FIELDS); const size = this.findFieldValue(options, this.SIZE_FIELDS); if (page !== undefined || size !== undefined) { if (page !== undefined) result.page = page; if (size !== undefined) result.pageSize = size; result.detectedFormat = 'flat_options'; return true; } return false; } /** * Extract from root level: { page, limit } */ static extractFromRootLevel(input, result) { const page = this.findFieldValue(input, this.PAGE_FIELDS); const size = this.findFieldValue(input, this.SIZE_FIELDS); if (page !== undefined || size !== undefined) { if (page !== undefined) result.page = page; if (size !== undefined) result.pageSize = size; result.detectedFormat = 'root_level'; return true; } return false; } /** * Extract from offset/limit pattern: { offset: 20, limit: 10 } */ static extractFromOffsetLimit(input, result) { const searchIn = [input, input.options, input.options?.pagination]; for (const obj of searchIn) { if (!obj) continue; const offset = this.findFieldValue(obj, this.OFFSET_FIELDS); const limit = this.findFieldValue(obj, this.SIZE_FIELDS); if (offset !== undefined) { // Calculate page from offset result.pageSize = limit || result.pageSize; result.page = Math.floor(offset / result.pageSize) + 1; result.detectedFormat = 'offset_limit'; return true; } } return false; } /** * Extract from skip/take pattern: { skip: 20, take: 10 } */ static extractFromSkipTake(input, result) { const searchIn = [input, input.options, input.options?.pagination]; for (const obj of searchIn) { if (!obj) continue; const skip = obj.skip !== undefined ? Number(obj.skip) : undefined; const take = obj.take !== undefined ? Number(obj.take) : undefined; if (skip !== undefined || take !== undefined) { if (take !== undefined) result.pageSize = take; if (skip !== undefined) { result.page = Math.floor(skip / result.pageSize) + 1; } result.detectedFormat = 'skip_take'; return true; } } return false; } /** * Extract from various "options" locations */ static extractFromOptions(input, result) { const optionsPaths = [ input.queryOptions, input.query_options, input.params, input.parameters, input.config, input.settings ]; for (const options of optionsPaths) { if (!options) continue; const page = this.findFieldValue(options, this.PAGE_FIELDS); const size = this.findFieldValue(options, this.SIZE_FIELDS); if (page !== undefined || size !== undefined) { if (page !== undefined) result.page = page; if (size !== undefined) result.pageSize = size; result.detectedFormat = 'alternative_options'; return true; } } return false; } /** * Handle mixed formats where page and size are in different places */ static extractFromMixedFormats(input, result) { let foundPage = false; let foundSize = false; // Search everywhere for page const allObjects = [ input, input.options, input.options?.pagination, input.pagination, input.params, input.query ]; for (const obj of allObjects) { if (!obj) continue; if (!foundPage) { const page = this.findFieldValue(obj, this.PAGE_FIELDS); if (page !== undefined) { result.page = page; foundPage = true; } } if (!foundSize) { const size = this.findFieldValue(obj, this.SIZE_FIELDS); if (size !== undefined) { result.pageSize = size; foundSize = true; } } if (foundPage && foundSize) break; } if (foundPage || foundSize) { result.detectedFormat = 'mixed_format'; return true; } return false; } /** * Find a field value using multiple possible field names */ static findFieldValue(obj, fieldNames) { for (const field of fieldNames) { if (obj[field] !== undefined) { const value = Number(obj[field]); if (!isNaN(value)) { return value; } } } return undefined; } /** * Validate and correct pagination values */ static validateAndCorrect(result) { // Correct page number if (result.page < 1) { result.warnings?.push(`Page number ${result.page} corrected to 1 (pages are 1-based)`); result.page = 1; } // Handle 0-based page index if (result.detectedFormat?.includes('index') && result.page === 0) { result.warnings?.push('Detected 0-based page index, converting to 1-based'); result.page = 1; } // Correct page size if (result.pageSize < 1) { result.warnings?.push(`Invalid page size ${result.pageSize} corrected to 10`); result.pageSize = 10; } else if (result.pageSize > 1000) { result.warnings?.push(`Page size ${result.pageSize} limited to 1000`); result.pageSize = 1000; } // Round to integers result.page = Math.floor(result.page); result.pageSize = Math.floor(result.pageSize); } } // Export convenience function export function extractPagination(input, defaults) { return PaginationParameterExtractor.extract(input, defaults); } //# sourceMappingURL=PaginationParameterExtractor.js.map