UNPKG

auto-builder-sdk

Version:

SDK for building Auto Builder workflow plugins

294 lines (293 loc) 12.3 kB
/** * 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}`); } }