@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
JavaScript
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();