initial commit

This commit is contained in:
Jp
2026-01-30 23:32:43 +08:00
commit 3df16ee995
20 changed files with 6405 additions and 0 deletions

213
utils/api_client.js Normal file
View File

@@ -0,0 +1,213 @@
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 };