UNPKG

@ideal-photography/shared

Version:

Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.

379 lines (322 loc) 12.1 kB
import { models } from '../mongoDB/index.js'; import { cache } from '../utils/caching.js'; class ProductService { constructor() { this.cache = cache; this.cacheKey = 'products'; this.cacheTTL = 3600; // 1 hour } /** * Get all products with caching * @param {Object} filters - Filter options * @param {string} filters.category - Product category * @param {string} filters.subcategory - Product subcategory * @param {boolean} filters.isActive - Active status * @param {number} filters.limit - Limit results * @param {number} filters.skip - Skip results * @returns {Promise<Object>} Products and pagination info */ async getProducts(filters = {}) { const cacheKey = `${this.cacheKey}:list:${JSON.stringify(filters)}`; try { // Try cache first const cached = await this.cache.get(cacheKey); if (cached) { return cached; } // Build query const query = {}; if (filters.category) query.category = filters.category; if (filters.subcategory) query.subcategory = filters.subcategory; if (filters.isActive !== undefined) query.isActive = filters.isActive; // Execute query const [products, total] = await Promise.all([ models.Product.find(query) .sort({ createdAt: -1 }) .skip(filters.skip || 0) .limit(filters.limit || 50) .lean(), models.Product.countDocuments(query) ]); const result = { products, pagination: { total, page: Math.floor((filters.skip || 0) / (filters.limit || 50)) + 1, limit: filters.limit || 50, pages: Math.ceil(total / (filters.limit || 50)) } }; // Cache result await this.cache.set(cacheKey, result, this.cacheTTL); return result; } catch (error) { console.error('Error fetching products:', error); throw new Error('Failed to fetch products'); } } /** * Get product by ID with caching * @param {string} productId - Product ID * @returns {Promise<Object>} Product data */ async getProductById(productId) { const cacheKey = `${this.cacheKey}:${productId}`; try { // Try cache first const cached = await this.cache.get(cacheKey); if (cached) { return cached; } // Fetch from database const product = await models.Product.findById(productId).lean(); if (!product) { throw new Error('Product not found'); } // Cache result await this.cache.set(cacheKey, product, this.cacheTTL); return product; } catch (error) { console.error('Error fetching product:', error); throw error; } } /** * Get products by category with caching * @param {string} category - Product category * @param {Object} options - Additional options * @returns {Promise<Array>} Products array */ async getProductsByCategory(category, options = {}) { const cacheKey = `${this.cacheKey}:category:${category}:${JSON.stringify(options)}`; try { // Try cache first const cached = await this.cache.get(cacheKey); if (cached) { return cached; } // Build query const query = { category, isActive: true }; if (options.subcategory) { query.subcategory = options.subcategory; } // Execute query const products = await models.Product.find(query) .sort({ createdAt: -1 }) .limit(options.limit || 20) .lean(); // Cache result await this.cache.set(cacheKey, products, this.cacheTTL); return products; } catch (error) { console.error('Error fetching products by category:', error); throw new Error('Failed to fetch products by category'); } } /** * Search products with caching * @param {string} searchTerm - Search term * @param {Object} options - Search options * @returns {Promise<Array>} Search results */ async searchProducts(searchTerm, options = {}) { const cacheKey = `${this.cacheKey}:search:${searchTerm}:${JSON.stringify(options)}`; try { // Try cache first const cached = await this.cache.get(cacheKey); if (cached) { return cached; } // Build search query const query = { $text: { $search: searchTerm }, isActive: true }; if (options.category) { query.category = options.category; } // Execute search const products = await models.Product.find(query, { score: { $meta: 'textScore' } }) .sort({ score: { $meta: 'textScore' } }) .limit(options.limit || 20) .lean(); // Cache result await this.cache.set(cacheKey, products, this.cacheTTL); return products; } catch (error) { console.error('Error searching products:', error); throw new Error('Failed to search products'); } } /** * Create new product * @param {Object} productData - Product data * @returns {Promise<Object>} Created product */ async createProduct(productData) { try { const product = await models.Product.create(productData); // Clear related caches await this.clearProductCaches(); return product; } catch (error) { console.error('Error creating product:', error); throw new Error('Failed to create product'); } } /** * Update product * @param {string} productId - Product ID * @param {Object} updateData - Update data * @returns {Promise<Object>} Updated product */ async updateProduct(productId, updateData) { try { const product = await models.Product.findByIdAndUpdate( productId, { ...updateData, updatedAt: new Date() }, { new: true, runValidators: true } ); if (!product) { throw new Error('Product not found'); } // Clear related caches await this.clearProductCaches(productId); return product; } catch (error) { console.error('Error updating product:', error); throw error; } } /** * Delete product * @param {string} productId - Product ID * @returns {Promise<boolean>} Success status */ async deleteProduct(productId) { try { const result = await models.Product.findByIdAndDelete(productId); if (!result) { throw new Error('Product not found'); } // Clear related caches await this.clearProductCaches(productId); return true; } catch (error) { console.error('Error deleting product:', error); throw error; } } /** * Check product availability * @param {string} productId - Product ID * @param {number} quantity - Required quantity * @returns {Promise<boolean>} Availability status */ async checkAvailability(productId, quantity = 1) { try { const product = await this.getProductById(productId); return product.isActive && product.stock >= quantity; } catch (error) { console.error('Error checking product availability:', error); return false; } } /** * Update product stock * @param {string} productId - Product ID * @param {number} quantity - Quantity change (positive or negative) * @returns {Promise<Object>} Updated product */ async updateStock(productId, quantity) { try { const product = await models.Product.findByIdAndUpdate( productId, { $inc: { stock: quantity }, updatedAt: new Date() }, { new: true, runValidators: true } ); if (!product) { throw new Error('Product not found'); } // Clear related caches await this.clearProductCaches(productId); return product; } catch (error) { console.error('Error updating product stock:', error); throw error; } } /** * Get product categories * @returns {Promise<Array>} Categories array */ async getCategories() { const cacheKey = `${this.cacheKey}:categories`; try { // Try cache first const cached = await this.cache.get(cacheKey); if (cached) { return cached; } // Get distinct categories const categories = await models.Product.distinct('category', { isActive: true }); // Cache result await this.cache.set(cacheKey, categories, this.cacheTTL); return categories; } catch (error) { console.error('Error fetching categories:', error); throw new Error('Failed to fetch categories'); } } /** * Get product subcategories * @param {string} category - Category to get subcategories for * @returns {Promise<Array>} Subcategories array */ async getSubcategories(category) { const cacheKey = `${this.cacheKey}:subcategories:${category}`; try { // Try cache first const cached = await this.cache.get(cacheKey); if (cached) { return cached; } // Get distinct subcategories for category const subcategories = await models.Product.distinct('subcategory', { category, isActive: true }); // Cache result await this.cache.set(cacheKey, subcategories, this.cacheTTL); return subcategories; } catch (error) { console.error('Error fetching subcategories:', error); throw new Error('Failed to fetch subcategories'); } } /** * Clear product-related caches * @param {string} productId - Specific product ID to clear (optional) */ async clearProductCaches(productId = null) { try { const patterns = [ `${this.cacheKey}:list:*`, `${this.cacheKey}:category:*`, `${this.cacheKey}:search:*`, `${this.cacheKey}:categories`, `${this.cacheKey}:subcategories:*` ]; if (productId) { patterns.push(`${this.cacheKey}:${productId}`); } // Clear all matching cache keys for (const pattern of patterns) { await this.cache.del(pattern); } } catch (error) { console.error('Error clearing product caches:', error); } } } export default new ProductService();