const axios = require('axios'); const db = require('../models'); const { Op } = require('sequelize'); class NutritionAPI { constructor(apiKey) { this.apiKey = apiKey; this.baseUrl = "https://api.api-ninjas.com/v1/nutrition"; this.headers = { 'X-Api-Key': apiKey }; this.cacheDurationDays = 30; } async searchFood(query) { // Check cache first const cached = await this._getFromCache(query); if (cached) { return cached; } // Make API request try { const response = await axios.get(this.baseUrl, { headers: this.headers, params: { query: query }, timeout: 10000 }); if (response.status === 200) { const data = response.data; // Cache the response await this._saveToCache(query, 'api_ninjas', data); // Parse and return standardized format return this._parseApiResponse(data); } else { console.error(`API Error: ${response.status}`); return []; } } catch (error) { console.error(`API Request failed: ${error.message}`); return []; } } async _getFromCache(query) { const cacheEntry = await db.APICache.findOne({ where: { query: query.toLowerCase() } }); if (cacheEntry) { // Check if cache is still valid (30 days) const age = (new Date() - new Date(cacheEntry.cached_at)) / (1000 * 60 * 60 * 24); if (age < this.cacheDurationDays) { const data = JSON.parse(cacheEntry.response_json); return this._parseApiResponse(data); } } return null; } async _saveToCache(query, source, data) { try { let cacheEntry = await db.APICache.findOne({ where: { query: query.toLowerCase() } }); if (cacheEntry) { // Update existing cache cacheEntry.response_json = JSON.stringify(data); cacheEntry.cached_at = new Date(); await cacheEntry.save(); } else { // Create new cache entry await db.APICache.create({ query: query.toLowerCase(), api_source: source, response_json: JSON.stringify(data), cached_at: new Date() }); } } catch (error) { console.error(`Cache save failed: ${error.message}`); } } _parseApiResponse(data) { return data.map(item => ({ name: (item.name || '').replace(/\b\w/g, l => l.toUpperCase()), // Title case calories: item.calories || 0, protein_g: item.protein_g || 0, carbs_g: item.carbohydrates_total_g || 0, fat_g: item.fat_total_g || 0, fiber_g: item.fiber_g || 0, sugar_g: item.sugar_g || 0, sodium_mg: item.sodium_mg || 0, serving_size_g: item.serving_size_g || 100, source: 'api_ninjas' })); } async saveFoodToDb(foodData) { try { // Check if food already exists const existing = await db.FoodItem.findOne({ where: { name: foodData.name, source: foodData.source || 'api' } }); if (existing) { return existing; } // Create new food item const food = await db.FoodItem.create({ name: foodData.name, calories: foodData.calories, protein_g: foodData.protein_g || 0, carbs_g: foodData.carbs_g || 0, fat_g: foodData.fat_g || 0, fiber_g: foodData.fiber_g || 0, sugar_g: foodData.sugar_g || 0, sodium_mg: foodData.sodium_mg || 0, serving_size_g: foodData.serving_size_g || 100, serving_description: foodData.serving_description || '1 serving', source: foodData.source || 'api', api_data: JSON.stringify(foodData) }); return food; } catch (error) { console.error(`Error saving food to DB: ${error.message}`); return null; } } } async function searchAllSources(query, apiClient) { const results = []; // 1. Search Filipino foods first const filipinoFoods = await db.FoodItem.findAll({ where: { is_filipino: true, [Op.or]: [ { name: { [Op.like]: `%${query}%` } }, { name_tagalog: { [Op.like]: `%${query}%` } } ] }, limit: 5 }); for (const food of filipinoFoods) { results.push({ id: food.id, name: food.name, name_tagalog: food.name_tagalog, calories: food.calories, protein_g: food.protein_g, carbs_g: food.carbs_g, fat_g: food.fat_g, serving_description: food.serving_description, source: 'filipino', category: food.category }); } // 2. Search other local foods const otherFoods = await db.FoodItem.findAll({ where: { is_filipino: false, name: { [Op.like]: `%${query}%` } }, limit: 5 }); for (const food of otherFoods) { results.push({ id: food.id, name: food.name, calories: food.calories, protein_g: food.protein_g, carbs_g: food.carbs_g, fat_g: food.fat_g, serving_description: food.serving_description, source: food.source }); } // 3. If not enough results, search API if (results.length < 3 && apiClient && apiClient.apiKey) { const apiResults = await apiClient.searchFood(query); for (const foodData of apiResults.slice(0, 5)) { results.push({ name: foodData.name, calories: foodData.calories, protein_g: foodData.protein_g, carbs_g: foodData.carbs_g, fat_g: foodData.fat_g, serving_size_g: foodData.serving_size_g, source: 'api_ninjas', api_data: foodData }); } } return results; } module.exports = { NutritionAPI, searchAllSources };