← back to Watches
database/services/WatchService.js
303 lines
/**
* Watch Service Layer
* Business logic layer with caching integration
*/
import WatchRepository from '../repositories/WatchRepository.js';
import PriceHistoryRepository from '../repositories/PriceHistoryRepository.js';
import cache from '../cache/RedisCache.js';
class WatchService {
constructor() {
this.watchRepo = WatchRepository;
this.priceRepo = PriceHistoryRepository;
this.cache = cache;
}
/**
* Get all watches with caching
*/
async getAllWatches(options = {}) {
const cacheKey = this.cache.generateKey(
'watches:all',
JSON.stringify(options)
);
return await this.cache.getOrSet(
cacheKey,
async () => await this.watchRepo.findAll(options),
this.cache.TTL.MEDIUM
);
}
/**
* Get watch by ID with full details and caching
*/
async getWatchById(watchId, includeHistory = false) {
const cacheKey = this.cache.generateKey(
'watch:full',
`${watchId}:${includeHistory}`
);
const result = await this.cache.getOrSet(
cacheKey,
async () => {
if (includeHistory) {
return await this.watchRepo.findByIdWithPriceHistory(watchId);
}
return await this.watchRepo.findById(watchId);
},
this.cache.TTL.LONG
);
// Track view asynchronously (fire and forget)
if (result.value) {
this.watchRepo.trackView(result.value.id, {
sessionId: null, // Would come from request
userAgent: null,
ipAddress: null
}).catch(err => console.error('View tracking failed:', err));
}
return result.value;
}
/**
* Search watches with caching
*/
async searchWatches(searchTerm, options = {}) {
const cacheKey = this.cache.generateKey(
'search',
`${searchTerm}:${JSON.stringify(options)}`
);
return await this.cache.getOrSet(
cacheKey,
async () => await this.watchRepo.search(searchTerm, options),
this.cache.TTL.SHORT
);
}
/**
* Get watch statistics with caching
*/
async getWatchStatistics(watchId) {
const cacheKey = this.cache.generateKey('watch:stats', watchId);
return await this.cache.getOrSet(
cacheKey,
async () => await this.watchRepo.getStatistics(watchId),
this.cache.TTL.LONG
);
}
/**
* Get all statistics (leaderboard)
*/
async getAllStatistics(options = {}) {
const cacheKey = this.cache.generateKey(
'stats:all',
JSON.stringify(options)
);
return await this.cache.getOrSet(
cacheKey,
async () => await this.watchRepo.getAllStatistics(options),
this.cache.TTL.LONG
);
}
/**
* Get trending watches with caching
*/
async getTrendingWatches(daysBack = 7, limit = 10) {
const cacheKey = this.cache.generateKey('trending', `${daysBack}:${limit}`);
return await this.cache.getOrSet(
cacheKey,
async () => await this.watchRepo.getTrending(daysBack, limit),
this.cache.TTL.SHORT // Shorter TTL for trending data
);
}
/**
* Get watches by series with caching
*/
async getWatchesBySeries(series) {
const cacheKey = this.cache.generateKey('series', series);
return await this.cache.getOrSet(
cacheKey,
async () => await this.watchRepo.findBySeries(series),
this.cache.TTL.LONG
);
}
/**
* Get price history with caching
*/
async getPriceHistory(watchId, options = {}) {
const cacheKey = this.cache.generateKey(
'price-history',
`${watchId}:${JSON.stringify(options)}`
);
return await this.cache.getOrSet(
cacheKey,
async () => await this.priceRepo.findByWatchId(watchId, options),
this.cache.TTL.LONG
);
}
/**
* Calculate appreciation with caching
*/
async calculateAppreciation(watchId, startYear, endYear) {
const cacheKey = this.cache.generateKey(
'appreciation',
`${watchId}:${startYear}:${endYear}`
);
return await this.cache.getOrSet(
cacheKey,
async () => await this.priceRepo.calculateAppreciation(watchId, startYear, endYear),
this.cache.TTL.VERY_LONG
);
}
/**
* Get price trends with caching
*/
async getPriceTrends(options = {}) {
const cacheKey = this.cache.generateKey('trends', JSON.stringify(options));
return await this.cache.getOrSet(
cacheKey,
async () => await this.priceRepo.getPriceTrends(options),
this.cache.TTL.LONG
);
}
/**
* Get series comparison with caching
*/
async getSeriesComparison(series, startYear, endYear) {
const cacheKey = this.cache.generateKey(
'comparison',
`${series}:${startYear}:${endYear}`
);
return await this.cache.getOrSet(
cacheKey,
async () => await this.priceRepo.getSeriesComparison(series, startYear, endYear),
this.cache.TTL.VERY_LONG
);
}
/**
* Create watch and invalidate cache
*/
async createWatch(watchData) {
const watch = await this.watchRepo.create(watchData);
// Invalidate relevant caches
await this.cache.deletePattern('omega:watches:*');
await this.cache.deletePattern('omega:stats:*');
return watch;
}
/**
* Update watch and invalidate cache
*/
async updateWatch(watchId, updateData) {
const watch = await this.watchRepo.update(watchId, updateData);
// Invalidate watch-specific and list caches
await this.cache.invalidateWatch(watchId);
await this.cache.deletePattern('omega:watches:*');
await this.cache.deletePattern('omega:stats:*');
return watch;
}
/**
* Add price point and invalidate cache
*/
async addPricePoint(watchId, priceData) {
const price = await this.priceRepo.create({
watchId,
...priceData
});
// Invalidate caches
await this.cache.invalidateWatch(watchId);
await this.cache.deletePattern('omega:stats:*');
await this.cache.deletePattern('omega:trends:*');
return price;
}
/**
* Refresh statistics materialized view
*/
async refreshStatistics() {
await this.watchRepo.refreshStatistics();
await this.cache.deletePattern('omega:stats:*');
console.log('✅ Statistics refreshed and cache invalidated');
}
/**
* Warm cache with frequently accessed data
*/
async warmCache() {
console.log('🔥 Warming cache...');
try {
// Warm all watches
const watches = await this.watchRepo.findAll({ limit: 100 });
const watchData = {};
for (const watch of watches) {
const cacheKey = this.cache.generateKey('watch:full', watch.id);
watchData[cacheKey] = watch;
}
await this.cache.warmCache(watchData, 'watch', this.cache.TTL.LONG);
// Warm statistics
const stats = await this.watchRepo.getAllStatistics({ limit: 100 });
await this.cache.set(
this.cache.generateKey('stats:all', 'default'),
stats,
this.cache.TTL.LONG
);
console.log('✅ Cache warmed successfully');
} catch (error) {
console.error('❌ Cache warming failed:', error);
}
}
/**
* Get service health
*/
async getHealth() {
const dbHealth = await import('../connection.js').then(mod =>
mod.checkDatabaseHealth()
);
const cacheHealth = await this.cache.healthCheck();
return {
database: dbHealth,
cache: {
healthy: cacheHealth,
connected: cacheHealth
}
};
}
}
export default new WatchService();