const { getCache, setCache, deleteCache } = require('../config/redis'); const logger = require('../config/logger'); /** * Cache middleware for GET requests * @param {number} ttl - Time to live in seconds (default: 300 = 5 minutes) * @param {function} keyGenerator - Function to generate cache key from req */ const cacheMiddleware = (ttl = 300, keyGenerator = null) => { return async (req, res, next) => { // Only cache GET requests if (req.method !== 'GET') { return next(); } try { // Generate cache key const cacheKey = keyGenerator ? keyGenerator(req) : `cache:${req.originalUrl}`; // Try to get from cache const cachedData = await getCache(cacheKey); if (cachedData) { logger.debug(`Cache hit for: ${cacheKey}`); return res.status(200).json(cachedData); } // Cache miss - store original json method const originalJson = res.json.bind(res); // Override json method to cache response res.json = function(data) { // Only cache successful responses if (res.statusCode === 200) { setCache(cacheKey, data, ttl).catch(err => { logger.error('Failed to cache response:', err); }); } // Call original json method return originalJson(data); }; next(); } catch (error) { logger.error('Cache middleware error:', error); next(); // Continue even if cache fails } }; }; /** * Cache categories (rarely change) */ const cacheCategories = cacheMiddleware(3600, (req) => { // Cache for 1 hour return 'cache:categories:list'; }); /** * Cache single category */ const cacheSingleCategory = cacheMiddleware(3600, (req) => { return `cache:category:${req.params.id}`; }); /** * Cache guest settings (rarely change) */ const cacheGuestSettings = cacheMiddleware(1800, (req) => { // Cache for 30 minutes return 'cache:guest:settings'; }); /** * Cache system statistics (update frequently) */ const cacheStatistics = cacheMiddleware(300, (req) => { // Cache for 5 minutes return 'cache:admin:statistics'; }); /** * Cache guest analytics */ const cacheGuestAnalytics = cacheMiddleware(600, (req) => { // Cache for 10 minutes return 'cache:admin:guest-analytics'; }); /** * Cache user dashboard */ const cacheUserDashboard = cacheMiddleware(300, (req) => { // Cache for 5 minutes const userId = req.params.userId || req.user?.id; return `cache:user:${userId}:dashboard`; }); /** * Cache questions list (with filters) */ const cacheQuestions = cacheMiddleware(600, (req) => { // Cache for 10 minutes const { categoryId, difficulty, questionType, visibility } = req.query; const filters = [categoryId, difficulty, questionType, visibility] .filter(Boolean) .join(':'); return `cache:questions:${filters || 'all'}`; }); /** * Cache single question */ const cacheSingleQuestion = cacheMiddleware(1800, (req) => { return `cache:question:${req.params.id}`; }); /** * Cache user bookmarks */ const cacheUserBookmarks = cacheMiddleware(300, (req) => { const userId = req.params.userId || req.user?.id; return `cache:user:${userId}:bookmarks`; }); /** * Cache user history */ const cacheUserHistory = cacheMiddleware(300, (req) => { const userId = req.params.userId || req.user?.id; const page = req.query.page || 1; return `cache:user:${userId}:history:page:${page}`; }); /** * Invalidate cache patterns */ const invalidateCache = { /** * Invalidate user-related cache */ user: async (userId) => { await deleteCache(`cache:user:${userId}:*`); logger.debug(`Invalidated cache for user ${userId}`); }, /** * Invalidate category cache */ category: async (categoryId = null) => { if (categoryId) { await deleteCache(`cache:category:${categoryId}`); } await deleteCache('cache:categories:*'); logger.debug('Invalidated category cache'); }, /** * Invalidate question cache */ question: async (questionId = null) => { if (questionId) { await deleteCache(`cache:question:${questionId}`); } await deleteCache('cache:questions:*'); logger.debug('Invalidated question cache'); }, /** * Invalidate statistics cache */ statistics: async () => { await deleteCache('cache:admin:statistics'); await deleteCache('cache:admin:guest-analytics'); logger.debug('Invalidated statistics cache'); }, /** * Invalidate guest settings cache */ guestSettings: async () => { await deleteCache('cache:guest:settings'); logger.debug('Invalidated guest settings cache'); }, /** * Invalidate all quiz-related cache */ quiz: async (userId = null, guestId = null) => { if (userId) { await deleteCache(`cache:user:${userId}:*`); } if (guestId) { await deleteCache(`cache:guest:${guestId}:*`); } await invalidateCache.statistics(); logger.debug('Invalidated quiz cache'); } }; /** * Middleware to invalidate cache after mutations */ const invalidateCacheMiddleware = (pattern) => { return async (req, res, next) => { // Store original json method const originalJson = res.json.bind(res); // Override json method res.json = async function(data) { // Only invalidate on successful mutations if (res.statusCode >= 200 && res.statusCode < 300) { try { if (typeof pattern === 'function') { await pattern(req); } else { await deleteCache(pattern); } } catch (error) { logger.error('Cache invalidation error:', error); } } // Call original json method return originalJson(data); }; next(); }; }; /** * Cache warming - preload frequently accessed data */ const warmCache = async () => { try { logger.info('Warming cache...'); // This would typically fetch and cache common data // For now, we'll just log the intent // In a real scenario, you'd fetch categories, popular questions, etc. logger.info('Cache warming complete'); } catch (error) { logger.error('Cache warming error:', error); } }; module.exports = { cacheMiddleware, cacheCategories, cacheSingleCategory, cacheGuestSettings, cacheStatistics, cacheGuestAnalytics, cacheUserDashboard, cacheQuestions, cacheSingleQuestion, cacheUserBookmarks, cacheUserHistory, invalidateCache, invalidateCacheMiddleware, warmCache };