qmemory
Version:
A comprehensive production-ready Node.js utility library with MongoDB document operations, user ownership enforcement, Express.js HTTP utilities, environment-aware logging, and in-memory storage. Features 96%+ test coverage with comprehensive error handli
503 lines (420 loc) • 19.8 kB
JavaScript
/**
* Advanced Pagination Demo
* Comprehensive demonstration of enhanced pagination utilities
*
* This demo showcases the complete pagination feature set including:
* - Traditional offset-based pagination for simple use cases
* - Cursor-based pagination for large datasets and real-time data
* - Advanced sorting with security validation
* - Integration with existing HTTP and storage utilities
*
* Real-world scenarios demonstrated:
* 1. Product catalog pagination with sorting
* 2. User activity feed with cursor-based navigation
* 3. Search results with filtering and pagination
* 4. Performance comparison between pagination approaches
*/
const {
validatePagination,
createPaginationMeta,
createPaginatedResponse,
validateCursorPagination,
createCursor,
createCursorPaginationMeta,
createCursorPaginatedResponse,
validateSorting
} = require('../lib/pagination-utils');
// Sample dataset simulating a real application database
const sampleProducts = [
{ id: 1, name: 'Wireless Headphones', price: 99.99, category: 'Electronics', createdAt: '2024-01-15T10:30:00Z' },
{ id: 2, name: 'Coffee Maker', price: 149.99, category: 'Appliances', createdAt: '2024-01-16T14:20:00Z' },
{ id: 3, name: 'Smartphone', price: 799.99, category: 'Electronics', createdAt: '2024-01-17T09:15:00Z' },
{ id: 4, name: 'Running Shoes', price: 129.99, category: 'Sports', createdAt: '2024-01-18T16:45:00Z' },
{ id: 5, name: 'Laptop', price: 999.99, category: 'Electronics', createdAt: '2024-01-19T11:30:00Z' },
{ id: 6, name: 'Desk Chair', price: 199.99, category: 'Furniture', createdAt: '2024-01-20T13:10:00Z' },
{ id: 7, name: 'Tablet', price: 299.99, category: 'Electronics', createdAt: '2024-01-21T08:20:00Z' },
{ id: 8, name: 'Blender', price: 79.99, category: 'Appliances', createdAt: '2024-01-22T15:30:00Z' },
{ id: 9, name: 'Monitor', price: 249.99, category: 'Electronics', createdAt: '2024-01-23T12:45:00Z' },
{ id: 10, name: 'Yoga Mat', price: 29.99, category: 'Sports', createdAt: '2024-01-24T10:15:00Z' }
];
const sampleUsers = [
{ id: 101, username: 'alice_dev', email: 'alice@example.com', lastActive: '2024-01-24T16:30:00Z', score: 850 },
{ id: 102, username: 'bob_designer', email: 'bob@example.com', lastActive: '2024-01-24T15:45:00Z', score: 720 },
{ id: 103, username: 'charlie_pm', email: 'charlie@example.com', lastActive: '2024-01-24T14:20:00Z', score: 960 },
{ id: 104, username: 'diana_qa', email: 'diana@example.com', lastActive: '2024-01-24T13:10:00Z', score: 680 },
{ id: 105, username: 'eve_ops', email: 'eve@example.com', lastActive: '2024-01-24T12:30:00Z', score: 780 }
];
// Helper function to create mock Express request/response objects
function createMockExpressObjects(query = {}) {
const req = { query };
const res = {
status: function(code) {
this._statusCode = code;
return this;
},
json: function(data) {
this._jsonData = data;
return this;
},
_statusCode: 200,
_jsonData: null
};
return { req, res };
}
// Simulation functions for database queries
function simulateOffsetQuery(data, pagination, sortConfig = null) {
let sortedData = [...data];
// Apply sorting if provided
if (sortConfig && sortConfig.length > 0) {
sortedData.sort((a, b) => {
for (const sort of sortConfig) {
const aVal = a[sort.field];
const bVal = b[sort.field];
let comparison = 0;
if (aVal < bVal) comparison = -1;
else if (aVal > bVal) comparison = 1;
if (comparison !== 0) {
return sort.direction === 'desc' ? -comparison : comparison;
}
}
return 0;
});
}
// Apply pagination
const startIndex = pagination.skip;
const endIndex = startIndex + pagination.limit;
return {
data: sortedData.slice(startIndex, endIndex),
totalRecords: sortedData.length
};
}
function simulateCursorQuery(data, pagination, sortField = 'id') {
let sortedData = [...data];
// Sort data based on the sort field
const isDescending = pagination.sort.startsWith('-');
const actualSortField = pagination.sort.replace(/^[+-]/, '');
sortedData.sort((a, b) => {
const aVal = a[actualSortField];
const bVal = b[actualSortField];
let comparison = 0;
if (aVal < bVal) comparison = -1;
else if (aVal > bVal) comparison = 1;
return isDescending ? -comparison : comparison;
});
// Apply cursor filtering if provided
let startIndex = 0;
if (pagination.cursor) {
const cursorValue = pagination.cursor[actualSortField];
const cursorId = pagination.cursor.id;
startIndex = sortedData.findIndex(item => {
if (pagination.direction === 'next') {
return isDescending ?
(item[actualSortField] < cursorValue || (item[actualSortField] === cursorValue && item.id > cursorId)) :
(item[actualSortField] > cursorValue || (item[actualSortField] === cursorValue && item.id > cursorId));
} else {
return isDescending ?
(item[actualSortField] > cursorValue || (item[actualSortField] === cursorValue && item.id < cursorId)) :
(item[actualSortField] < cursorValue || (item[actualSortField] === cursorValue && item.id < cursorId));
}
});
if (startIndex === -1) startIndex = sortedData.length;
}
// Get page of results
const endIndex = startIndex + pagination.limit;
const pageData = sortedData.slice(startIndex, endIndex);
// Check if there are more records
const hasMore = endIndex < sortedData.length;
return { data: pageData, hasMore };
}
async function demo1_basicOffsetPagination() {
console.log('\n=== Demo 1: Basic Offset Pagination ===');
console.log('Demonstrating traditional page-based pagination for product catalog');
// Simulate first page request
const { req: req1, res: res1 } = createMockExpressObjects({ page: '1', limit: '3' });
const pagination1 = validatePagination(req1, res1);
if (pagination1) {
const result1 = simulateOffsetQuery(sampleProducts, pagination1);
const response1 = createPaginatedResponse(result1.data, pagination1.page, pagination1.limit, result1.totalRecords);
console.log(`Page 1 Results (${result1.data.length} of ${result1.totalRecords} total):`);
result1.data.forEach(product => {
console.log(` - ${product.name} ($${product.price}) [${product.category}]`);
});
console.log(`Navigation: Page ${response1.pagination.currentPage} of ${response1.pagination.totalPages}`);
console.log(`Has Next: ${response1.pagination.hasNextPage}, Has Previous: ${response1.pagination.hasPrevPage}`);
}
// Simulate middle page request
const { req: req2, res: res2 } = createMockExpressObjects({ page: '3', limit: '3' });
const pagination2 = validatePagination(req2, res2);
if (pagination2) {
const result2 = simulateOffsetQuery(sampleProducts, pagination2);
const response2 = createPaginatedResponse(result2.data, pagination2.page, pagination2.limit, result2.totalRecords);
console.log(`\nPage 3 Results (${result2.data.length} of ${result2.totalRecords} total):`);
result2.data.forEach(product => {
console.log(` - ${product.name} ($${product.price}) [${product.category}]`);
});
console.log(`Navigation: Page ${response2.pagination.currentPage} of ${response2.pagination.totalPages}`);
}
}
async function demo2_cursorBasedPagination() {
console.log('\n=== Demo 2: Cursor-Based Pagination ===');
console.log('Demonstrating cursor navigation for large datasets with real-time updates');
// First page with cursor pagination
const { req: req1, res: res1 } = createMockExpressObjects({
limit: '3',
direction: 'next',
sort: '-score' // Sort by score descending
});
const pagination1 = validateCursorPagination(req1, res1);
if (pagination1) {
const result1 = simulateCursorQuery(sampleUsers, pagination1, 'score');
const response1 = createCursorPaginatedResponse(result1.data, pagination1, result1.hasMore, 'score');
console.log(`First Page Results (sorted by score descending):`);
result1.data.forEach(user => {
console.log(` - ${user.username} (Score: ${user.score}) - Last Active: ${user.lastActive}`);
});
console.log(`Has More: ${response1.pagination.hasMore}`);
console.log(`Next Cursor: ${response1.pagination.cursors.next ? 'Available' : 'None'}`);
// Use cursor for next page
if (response1.pagination.cursors.next) {
const { req: req2, res: res2 } = createMockExpressObjects({
limit: '2',
direction: 'next',
sort: '-score',
cursor: response1.pagination.cursors.next
});
const pagination2 = validateCursorPagination(req2, res2);
if (pagination2) {
const result2 = simulateCursorQuery(sampleUsers, pagination2, 'score');
const response2 = createCursorPaginatedResponse(result2.data, pagination2, result2.hasMore, 'score');
console.log(`\nNext Page Results (using cursor):`);
result2.data.forEach(user => {
console.log(` - ${user.username} (Score: ${user.score}) - Last Active: ${user.lastActive}`);
});
console.log(`Has More: ${response2.pagination.hasMore}`);
}
}
}
}
async function demo3_advancedSorting() {
console.log('\n=== Demo 3: Advanced Sorting with Security Validation ===');
console.log('Demonstrating multi-field sorting with security constraints');
// Valid multi-field sorting
const { req: req1, res: res1 } = createMockExpressObjects({
sort: 'category,-price,name',
page: '1',
limit: '5'
});
const allowedFields = ['name', 'price', 'category', 'createdAt'];
const sortConfig = validateSorting(req1, res1, { allowedFields });
if (sortConfig) {
console.log('Valid Sort Configuration:');
sortConfig.sortConfig.forEach((sort, index) => {
console.log(` ${index + 1}. ${sort.field} (${sort.direction})`);
});
const pagination1 = validatePagination(req1, res1);
if (pagination1) {
const result1 = simulateOffsetQuery(sampleProducts, pagination1, sortConfig.sortConfig);
console.log('\nSorted Results (Category ASC, Price DESC, Name ASC):');
result1.data.forEach(product => {
console.log(` - [${product.category}] ${product.name} - $${product.price}`);
});
}
}
// Demonstrate security validation - invalid field
console.log('\n--- Security Validation Test ---');
const { req: req2, res: res2 } = createMockExpressObjects({ sort: 'password,admin_level' });
const invalidSort = validateSorting(req2, res2, { allowedFields });
if (!invalidSort) {
console.log('Security validation working: Rejected unauthorized sort fields');
console.log('This prevents potential data exposure or injection attacks');
}
}
async function demo4_performanceComparison() {
console.log('\n=== Demo 4: Performance Comparison ===');
console.log('Comparing offset vs cursor pagination performance characteristics');
// Simulate large dataset
const largeDataset = Array.from({ length: 100000 }, (_, i) => ({
id: i + 1,
name: `Item ${i + 1}`,
timestamp: new Date(2024, 0, 1 + (i % 365)).toISOString(),
value: Math.floor(Math.random() * 1000)
}));
console.log(`Dataset Size: ${largeDataset.length.toLocaleString()} records`);
// Offset pagination performance simulation
console.log('\nOffset Pagination:');
const offsetStart = process.hrtime.bigint();
// Simulate deep page access (page 1000)
const { req: offsetReq, res: offsetRes } = createMockExpressObjects({ page: '1000', limit: '50' });
const offsetPagination = validatePagination(offsetReq, offsetRes);
if (offsetPagination) {
// In real database, this would require scanning 49,950 records before returning results
const offsetSkipped = offsetPagination.skip;
console.log(` - Deep page access requires skipping ${offsetSkipped.toLocaleString()} records`);
console.log(` - Performance degrades linearly with page depth`);
console.log(` - Suitable for: Small to medium datasets, jump-to-page navigation`);
}
const offsetEnd = process.hrtime.bigint();
const offsetTime = Number(offsetEnd - offsetStart) / 1000000; // Convert to milliseconds
// Cursor pagination performance simulation
console.log('\nCursor Pagination:');
const cursorStart = process.hrtime.bigint();
const { req: cursorReq, res: cursorRes } = createMockExpressObjects({
limit: '50',
direction: 'next',
sort: 'timestamp'
});
const cursorPagination = validateCursorPagination(cursorReq, cursorRes);
if (cursorPagination) {
console.log(` - No record skipping required, uses indexed seeks`);
console.log(` - Consistent performance regardless of position in dataset`);
console.log(` - Suitable for: Large datasets, real-time feeds, mobile apps`);
}
const cursorEnd = process.hrtime.bigint();
const cursorTime = Number(cursorEnd - cursorStart) / 1000000;
console.log(`\nValidation Performance Comparison:`);
console.log(` - Offset validation: ${offsetTime.toFixed(3)}ms`);
console.log(` - Cursor validation: ${cursorTime.toFixed(3)}ms`);
console.log(` - Both validations are extremely fast and scale well`);
}
async function demo5_realWorldIntegration() {
console.log('\n=== Demo 5: Real-World Integration Patterns ===');
console.log('Demonstrating pagination integration with filtering and search');
// E-commerce product search with pagination
console.log('E-commerce Search Results:');
const searchQuery = 'Electronics';
const filteredProducts = sampleProducts.filter(p => p.category === searchQuery);
const { req, res } = createMockExpressObjects({
page: '1',
limit: '3',
sort: '-price',
category: searchQuery
});
// Validate sorting with appropriate security restrictions
const sortConfig = validateSorting(req, res, {
allowedFields: ['name', 'price', 'createdAt'],
defaultSort: '-createdAt'
});
const pagination = validatePagination(req, res);
if (pagination && sortConfig) {
const result = simulateOffsetQuery(filteredProducts, pagination, sortConfig.sortConfig);
const response = createPaginatedResponse(result.data, pagination.page, pagination.limit, result.totalRecords);
console.log(`Search: "${searchQuery}" - ${result.totalRecords} results found`);
console.log(`Page ${response.pagination.currentPage} of ${response.pagination.totalPages}:`);
result.data.forEach((product, index) => {
console.log(` ${pagination.skip + index + 1}. ${product.name} - $${product.price}`);
});
console.log(`\nNavigation Options:`);
if (response.pagination.hasPrevPage) {
console.log(` ← Previous: Page ${response.pagination.prevPage}`);
}
if (response.pagination.hasNextPage) {
console.log(` → Next: Page ${response.pagination.nextPage}`);
}
console.log(`\nAPI Response Structure:`);
console.log(` - data: Array of ${result.data.length} products`);
console.log(` - pagination: Complete navigation metadata`);
console.log(` - timestamp: ${response.timestamp}`);
}
}
async function demo6_errorHandlingShowcase() {
console.log('\n=== Demo 6: Error Handling and Validation ===');
console.log('Demonstrating comprehensive input validation and error responses');
const errorCases = [
{
query: { page: '0', limit: '10' },
description: 'Invalid page number (too low)'
},
{
query: { page: '1', limit: '500' },
description: 'Limit exceeds maximum allowed'
},
{
query: { page: 'abc', limit: '10' },
description: 'Non-numeric page parameter'
},
{
query: { cursor: 'invalid-cursor-data', limit: '10' },
description: 'Malformed cursor'
},
{
query: { direction: 'sideways', limit: '10' },
description: 'Invalid direction parameter'
}
];
console.log('Testing Input Validation:');
errorCases.forEach((testCase, index) => {
console.log(`\n${index + 1}. ${testCase.description}:`);
const { req, res } = createMockExpressObjects(testCase.query);
// Test offset pagination validation
if (testCase.query.page !== undefined) {
const result = validatePagination(req, res);
if (!result) {
console.log(' ✓ Offset pagination correctly rejected invalid input');
}
}
// Test cursor pagination validation
if (testCase.query.cursor !== undefined || testCase.query.direction !== undefined) {
const result = validateCursorPagination(req, res);
if (!result) {
console.log(' ✓ Cursor pagination correctly rejected invalid input');
}
}
// Mock response should have been called with error details
if (res.status.mock && res.status.mock.calls.length > 0) {
const statusCode = res.status.mock.calls[res.status.mock.calls.length - 1][0];
console.log(` → HTTP ${statusCode} error response generated`);
}
});
console.log('\nError handling provides:');
console.log(' ✓ Consistent HTTP status codes');
console.log(' ✓ Detailed error messages for debugging');
console.log(' ✓ Timestamp information');
console.log(' ✓ Security-conscious error responses');
}
async function runAllAdvancedDemos() {
console.log('🚀 Advanced Pagination Utilities Comprehensive Demo');
console.log('====================================================');
console.log('\nThis demonstration showcases the enhanced pagination features');
console.log('integrated with your existing qmemory library utilities.');
console.log('\nFeatures demonstrated:');
console.log(' • Offset-based pagination for traditional use cases');
console.log(' • Cursor-based pagination for large datasets');
console.log(' • Advanced sorting with security validation');
console.log(' • Performance characteristics comparison');
console.log(' • Real-world integration patterns');
console.log(' • Comprehensive error handling');
try {
await demo1_basicOffsetPagination();
await demo2_cursorBasedPagination();
await demo3_advancedSorting();
await demo4_performanceComparison();
await demo5_realWorldIntegration();
await demo6_errorHandlingShowcase();
console.log('\n====================================================');
console.log('✅ All Advanced Pagination Demos Completed Successfully!');
console.log('\nKey Benefits Demonstrated:');
console.log(' ✓ Enhanced functionality while maintaining existing API compatibility');
console.log(' ✓ Security-first approach with input validation and field restrictions');
console.log(' ✓ Performance optimization for different use cases');
console.log(' ✓ Comprehensive error handling and user-friendly responses');
console.log(' ✓ Integration with existing HTTP utilities for consistency');
console.log('\nThe enhanced pagination utilities are ready for production use!');
} catch (error) {
console.error('\n❌ Demo execution error:', error.message);
console.error('Stack trace:', error.stack);
}
}
// Export for use in other modules or direct execution
module.exports = {
runAllAdvancedDemos,
demo1_basicOffsetPagination,
demo2_cursorBasedPagination,
demo3_advancedSorting,
demo4_performanceComparison,
demo5_realWorldIntegration,
demo6_errorHandlingShowcase
};
// Run demos if this file is executed directly
if (require.main === module) {
runAllAdvancedDemos().catch(console.error);
}