Files
Tasks/backend/middleware/cache.js
2025-11-12 23:06:27 +02:00

268 lines
6.4 KiB
JavaScript

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
};