auto-builder-sdk
Version:
SDK for building Auto Builder workflow plugins
294 lines (293 loc) • 12.3 kB
JavaScript
/**
* Standard Pagination Utilities for Auto-Builder SDK
*
* This module provides utilities to enforce consistent pagination across all nodes
* and plugins. It implements the StandardPaginationParams interface and provides
* helper functions for converting between different pagination formats.
*/
import { log } from './logger.js';
import { PAGINATION_CONFIG } from './pagination.config.js';
/**
* Safely convert any value to a number with fallback
*/
function safeNumber(value, fallback) {
if (value === null || value === undefined)
return fallback;
if (typeof value === 'number') {
if (isNaN(value) || !isFinite(value))
return fallback;
return Math.floor(value); // Handle floating point numbers
}
if (typeof value === 'string') {
const trimmed = value.trim();
if (trimmed === '')
return fallback;
const parsed = parseFloat(trimmed);
if (isNaN(parsed) || !isFinite(parsed))
return fallback;
return Math.floor(parsed);
}
if (typeof value === 'boolean')
return fallback;
if (typeof value === 'object' || typeof value === 'function')
return fallback;
return fallback;
}
/**
* Safely extract nested property from object
*/
function safeGet(obj, path) {
if (!obj || typeof obj !== 'object')
return undefined;
return path.split('.').reduce((current, key) => {
return current && typeof current === 'object' ? current[key] : undefined;
}, obj);
}
/**
* Convert page/pageSize to limit/offset for database queries
*/
export function convertToLimitOffset(page = PAGINATION_CONFIG.DEFAULT_PAGE, pageSize = PAGINATION_CONFIG.DEFAULT_PAGE_SIZE) {
const safePage = safeNumber(page, PAGINATION_CONFIG.DEFAULT_PAGE);
const safePageSize = safeNumber(pageSize, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE);
// Handle mathematical overflow for very large numbers
let offset = 0;
if (safePage > PAGINATION_CONFIG.MIN_PAGE) {
const calculatedOffset = (safePage - 1) * safePageSize;
// Handle overflow by capping at MAX_SAFE_INTEGER
offset = calculatedOffset > PAGINATION_CONFIG.MAX_SAFE_INTEGER ? PAGINATION_CONFIG.MAX_SAFE_INTEGER : calculatedOffset;
}
return {
limit: safePageSize,
offset
};
}
/**
* Calculate pagination metadata
*/
export function calculatePaginationMeta(page = PAGINATION_CONFIG.DEFAULT_PAGE, pageSize = PAGINATION_CONFIG.DEFAULT_PAGE_SIZE, totalRecords) {
const safePage = safeNumber(page, PAGINATION_CONFIG.DEFAULT_PAGE);
const safePageSize = safeNumber(pageSize, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE);
const safeTotalRecords = safeNumber(totalRecords, 0);
// Handle division by zero - return Infinity for totalPages
const totalPages = safePageSize > 0 ? Math.ceil(safeTotalRecords / safePageSize) : Infinity;
return {
page: safePage,
pageSize: safePageSize,
totalRecords: safeTotalRecords,
totalPages,
hasNextPage: safePage < totalPages,
hasPreviousPage: safePage > PAGINATION_CONFIG.MIN_PAGE
};
}
/**
* Validate and apply defaults to pagination parameters
*/
export function validatePaginationParams(params) {
const page = safeNumber(params.page, PAGINATION_CONFIG.DEFAULT_PAGE);
const pageSize = Math.min(PAGINATION_CONFIG.MAX_PAGE_SIZE, Math.max(PAGINATION_CONFIG.MIN_PAGE_SIZE, safeNumber(params.pageSize, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE)));
return {
page: Math.max(PAGINATION_CONFIG.MIN_PAGE, page),
pageSize,
totalRecords: params.totalRecords
};
}
/**
* Create a standardized pagination response
*/
export function createPaginationResponse(data, page = PAGINATION_CONFIG.DEFAULT_PAGE, pageSize = PAGINATION_CONFIG.DEFAULT_PAGE_SIZE, totalRecords) {
if (!Array.isArray(data)) {
throw new Error(PAGINATION_CONFIG.ERROR_MESSAGES.DATA_MUST_BE_ARRAY);
}
const safePage = safeNumber(page, PAGINATION_CONFIG.DEFAULT_PAGE);
const safePageSize = safeNumber(pageSize, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE);
const safeTotalRecords = totalRecords !== undefined ? safeNumber(totalRecords, data.length) : data.length;
return {
data,
pagination: calculatePaginationMeta(safePage, safePageSize, safeTotalRecords)
};
}
/**
* Extract pagination parameters from node parameters
*/
export function extractPaginationParams(params) {
if (!params || typeof params !== 'object') {
return {
page: PAGINATION_CONFIG.DEFAULT_PAGE,
pageSize: PAGINATION_CONFIG.DEFAULT_PAGE_SIZE,
totalRecords: undefined
};
}
// Handle legacy parameter names with proper precedence
const page = params.page !== undefined ? safeNumber(params.page, PAGINATION_CONFIG.DEFAULT_PAGE) :
params.pageIndex !== undefined ? safeNumber(params.pageIndex, PAGINATION_CONFIG.DEFAULT_PAGE) :
params.pageNumber !== undefined ? safeNumber(params.pageNumber, PAGINATION_CONFIG.DEFAULT_PAGE) : PAGINATION_CONFIG.DEFAULT_PAGE;
const pageSize = params.pageSize !== undefined ? safeNumber(params.pageSize, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE) :
params.limit !== undefined ? safeNumber(params.limit, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE) :
params.pageLimit !== undefined ? safeNumber(params.pageLimit, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE) : PAGINATION_CONFIG.DEFAULT_PAGE_SIZE;
return {
page,
pageSize,
totalRecords: params.totalRecords || params.totalCount || params.total
};
}
/**
* Convert legacy pagination parameter names to standard format
*/
export function convertLegacyPagination(params) {
if (!params || typeof params !== 'object') {
return {
page: PAGINATION_CONFIG.DEFAULT_PAGE,
pageSize: PAGINATION_CONFIG.DEFAULT_PAGE_SIZE,
totalRecords: undefined
};
}
// Handle very long string numbers that might exceed Number.MAX_SAFE_INTEGER
const parseLargeNumber = (value, fallback) => {
if (typeof value === 'string') {
const trimmed = value.trim();
if (trimmed === '')
return fallback;
// For very long numbers, check if they exceed safe integer limits
if (trimmed.length > PAGINATION_CONFIG.MAX_STRING_LENGTH) {
return fallback; // Too large, use fallback
}
const parsed = parseFloat(trimmed);
if (isNaN(parsed) || !isFinite(parsed) || parsed > PAGINATION_CONFIG.MAX_SAFE_INTEGER) {
return fallback;
}
return Math.floor(parsed);
}
return safeNumber(value, fallback);
};
return {
page: parseLargeNumber(params.pageIndex, parseLargeNumber(params.pageNumber, parseLargeNumber(params.page, PAGINATION_CONFIG.DEFAULT_PAGE))),
pageSize: parseLargeNumber(params.pageLimit, parseLargeNumber(params.limit, parseLargeNumber(params.pageSize, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE))),
totalRecords: params.totalCount || params.total || params.totalRecords
};
}
/**
* Build API-specific pagination parameters
*/
export function buildApiPaginationParams(params, apiType = 'rest') {
const validatedParams = validatePaginationParams(params);
const { page, pageSize } = validatedParams;
switch (apiType) {
case 'graphql':
return { first: pageSize, offset: (page - 1) * pageSize };
case 'odata':
return { '$top': pageSize, '$skip': (page - 1) * pageSize };
case 'firebase':
return { startAt: (page - 1) * pageSize, maxResults: pageSize };
case 'mongo':
return { top: pageSize, skip: (page - 1) * pageSize };
case 'rest':
default:
return { page, pageSize, limit: pageSize, offset: (page - 1) * pageSize };
}
}
/**
* Parse API response and extract pagination metadata
*/
export function parseApiPaginationResponse(response, dataPath = 'data', paginationPath = 'pagination') {
if (!response || typeof response !== 'object') {
return {
data: [],
pagination: {
page: PAGINATION_CONFIG.DEFAULT_PAGE,
pageSize: PAGINATION_CONFIG.DEFAULT_PAGE_SIZE,
totalRecords: 0,
totalPages: 0,
hasNextPage: false,
hasPreviousPage: false
}
};
}
// Safely extract data using the provided path
const data = safeGet(response, dataPath) || response.items || response.results || [];
const pagination = safeGet(response, paginationPath) || response.meta || {};
const pageSize = safeNumber(pagination.pageSize || pagination.per_page || pagination.limit, PAGINATION_CONFIG.DEFAULT_PAGE_SIZE);
const totalRecords = safeNumber(pagination.totalRecords || pagination.total || pagination.total_count, Array.isArray(data) ? data.length : 0);
return {
data: Array.isArray(data) ? data : [],
pagination: {
page: safeNumber(pagination.page || pagination.current_page, PAGINATION_CONFIG.DEFAULT_PAGE),
pageSize,
totalRecords,
totalPages: pagination.totalPages || pagination.total_pages || (pageSize > 0 ? Math.ceil(totalRecords / pageSize) : 0),
hasNextPage: Boolean(pagination.hasNextPage || pagination.has_next_page),
hasPreviousPage: Boolean(pagination.hasPreviousPage || pagination.has_previous_page)
}
};
}
/**
* Standard pagination node definition for easy integration
*/
export const PAGINATION_NODE_DEFINITION = {
displayName: 'Page',
name: 'page',
type: 'number',
default: PAGINATION_CONFIG.DEFAULT_PAGE,
description: `Page number (${PAGINATION_CONFIG.MIN_PAGE}-based)`,
displayOptions: {
show: {
resource: ['*'],
operation: ['getAll', 'list', 'search', 'query']
}
}
};
/**
* Standard page size node definition
*/
export const PAGINATION_SIZE_NODE_DEFINITION = {
displayName: 'Page Size',
name: 'pageSize',
type: 'number',
default: PAGINATION_CONFIG.DEFAULT_PAGE_SIZE,
description: `Number of records per page (max ${PAGINATION_CONFIG.MAX_PAGE_SIZE})`,
displayOptions: {
show: {
resource: ['*'],
operation: ['getAll', 'list', 'search', 'query']
}
}
};
/**
* Helper function to add standard pagination to node properties
*/
export function addStandardPagination(properties) {
if (!Array.isArray(properties)) {
throw new Error(PAGINATION_CONFIG.ERROR_MESSAGES.PROPERTIES_MUST_BE_ARRAY);
}
return [
...properties,
PAGINATION_NODE_DEFINITION,
PAGINATION_SIZE_NODE_DEFINITION
];
}
/**
* Helper function to validate pagination in node execution
*/
export function validateNodePagination(items, resource, operation) {
try {
const nodeParams = this.getNodeParameter('', 0, {});
const params = extractPaginationParams(nodeParams);
const validatedParams = validatePaginationParams(params);
const { page, pageSize } = validatedParams;
// Check validation rules and throw errors if violated
if (pageSize > PAGINATION_CONFIG.MAX_PAGE_SIZE) {
throw new Error(PAGINATION_CONFIG.ERROR_MESSAGES.PAGE_SIZE_EXCEEDED(resource, operation, PAGINATION_CONFIG.MAX_PAGE_SIZE));
}
if (page < PAGINATION_CONFIG.MIN_PAGE) {
throw new Error(PAGINATION_CONFIG.ERROR_MESSAGES.PAGE_NUMBER_INVALID(resource, operation, PAGINATION_CONFIG.MIN_PAGE));
}
log.info(`Executing ${resource}.${operation} with pagination: page=${page}, pageSize=${pageSize}`);
}
catch (error) {
// Re-throw validation errors
if (error instanceof Error && (error.message.includes('Page size cannot exceed') || error.message.includes('Page number must be'))) {
throw error;
}
// For other errors (like getNodeParameter failing), use defaults
log.info(`Executing ${resource}.${operation} with default pagination: page=${PAGINATION_CONFIG.DEFAULT_PAGE}, pageSize=${PAGINATION_CONFIG.DEFAULT_PAGE_SIZE}`);
}
}