@simonecoelhosfo/optimizely-mcp-server
Version:
Optimizely MCP Server for AI assistants with integrated CLI tools
305 lines • 10.5 kB
JavaScript
/**
* 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