avocavo
Version:
Avocavo CLI - Nutrition analysis made simple. Get accurate USDA nutrition data with secure authentication.
262 lines (226 loc) • 8.47 kB
JavaScript
const axios = require('axios');
const chalk = require('chalk');
class ApiError extends Error {
constructor(message, statusCode = null, response = null) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.response = response;
}
}
class NutritionAPI {
constructor(apiKey, baseUrl = 'https://app.avocavo.app', timeout = 30000, authManager = null) {
this.apiKey = apiKey;
this.baseUrl = baseUrl.replace(/\/$/, '');
this.timeout = timeout;
this.authManager = authManager;
// Create axios instance
this.client = axios.create({
baseURL: this.baseUrl,
timeout: this.timeout,
headers: {
'Content-Type': 'application/json',
'User-Agent': 'avocavo-nutrition-cli/1.8.0'
}
});
// Add API key to requests if provided
if (this.apiKey) {
this.client.defaults.headers['X-API-Key'] = this.apiKey;
}
// Add request interceptor to handle authentication
this.client.interceptors.request.use(async (config) => {
// Determine authentication method based on endpoint
const endpoint = config.url;
if (process.env.NODE_ENV === 'development' || process.env.AVOCAVO_DEBUG) {
console.log(`[DEBUG] Request to ${endpoint} with API key: ${this.apiKey ? '[REDACTED]' : 'none'}`);
// Sanitize headers before logging
const sanitizedHeaders = { ...config.headers };
if (sanitizedHeaders['X-API-Key']) sanitizedHeaders['X-API-Key'] = '[REDACTED]';
if (sanitizedHeaders['Authorization']) sanitizedHeaders['Authorization'] = '[REDACTED]';
console.log(`[DEBUG] Request headers:`, JSON.stringify(sanitizedHeaders, null, 2));
}
if (endpoint.startsWith('/api/auth/')) {
// JWT authentication for key management endpoints
if (this.authManager) {
const jwt = await this.authManager.getJwtToken();
if (jwt) {
config.headers['Authorization'] = `Bearer ${jwt}`;
// Remove API key header for JWT endpoints
delete config.headers['X-API-Key'];
}
}
} else if (endpoint.startsWith('/api/v2/nutrition/') || endpoint.startsWith('/api/v1/nutrition/')) {
// API key authentication for nutrition endpoints
if (this.apiKey) {
// Use API key from constructor (command line or direct)
config.headers['X-API-Key'] = this.apiKey;
} else if (this.authManager) {
// Try to get selected API key from auth manager
const apiKey = await this.authManager.getApiKey();
if (apiKey) {
config.headers['X-API-Key'] = apiKey;
}
}
}
return config;
});
// Response interceptor for error handling
this.client.interceptors.response.use(
response => response,
error => {
if (error.response) {
const status = error.response.status;
const data = error.response.data;
let message = data?.error || `HTTP ${status}`;
if (status === 401) {
message = 'Invalid API key or authentication required';
if (process.env.NODE_ENV === 'development' || process.env.AVOCAVO_DEBUG) {
console.log(`[DEBUG] 401 Response:`, JSON.stringify(data, null, 2));
}
} else if (status === 402) {
message = 'Trial expired or payment required';
} else if (status === 403) {
message = 'Feature not available on your plan';
} else if (status === 429) {
message = 'Rate limit exceeded';
} else if (status >= 500) {
message = 'Server error - please try again later';
}
throw new ApiError(message, status, data);
} else if (error.request) {
throw new ApiError('Connection error. Check your internet connection.');
} else {
throw new ApiError(`Request failed: ${error.message}`);
}
}
);
}
async analyze(input, servings = null) {
try {
// Smart routing to proper structured endpoints based on input type
if (typeof input === 'string') {
// Single ingredient - use bulletproof V2 ingredient endpoint
return await this.analyzeIngredient(input);
} else if (Array.isArray(input)) {
if (servings && servings > 1) {
// Recipe with servings - use bulletproof V2 recipe endpoint
return await this.analyzeRecipe(input, servings);
} else {
// Multiple ingredients without servings - use bulletproof V2 batch endpoint
return await this.analyzeBatch(input);
}
} else {
throw new Error('Input must be a string (ingredient) or array (recipe/batch)');
}
} catch (error) {
throw error;
}
}
async analyzeIngredient(ingredient, includeVerification = false, verbose = false) {
try {
const requestData = {
ingredient: ingredient
};
// Add verbose parameter if requested
if (verbose) {
requestData.verbose = true;
}
const response = await this.client.post('/api/v2/nutrition/ingredient', requestData);
const result = response.data;
// For backward compatibility, ensure verification URL is included if requested
if (includeVerification && result.success && result.nutrition?.fdc_id) {
result.verification_url = `https://fdc.nal.usda.gov/fdc-app.html#/food-details/${result.nutrition.fdc_id}`;
}
return result;
} catch (error) {
throw error;
}
}
async analyzeRecipe(ingredients, servings = 1, verbose = false) {
try {
const requestData = {
ingredients: ingredients,
servings: servings
};
// Add verbose parameter if requested
if (verbose) {
requestData.verbose = true;
}
const response = await this.client.post('/api/v2/nutrition/recipe', requestData);
return response.data;
} catch (error) {
throw error;
}
}
async analyzeBatch(ingredients, verbose = false) {
try {
// The batch endpoint now supports both array of strings and array of objects
const requestData = {
ingredients: ingredients
};
// Add verbose parameter if requested
if (verbose) {
requestData.verbose = true;
}
const response = await this.client.post('/api/v2/nutrition/batch', requestData);
return response.data;
} catch (error) {
throw error;
}
}
async getAccountUsage() {
try {
const response = await this.client.get('/api/v2/nutrition/account/usage');
// V2 endpoint returns correct format
const data = response.data;
return {
email: data.account?.email || 'Unknown',
api_tier: data.account?.api_tier || 'Unknown',
subscription_status: data.account?.subscription_status || 'Unknown',
usage: data.usage || {
current_month: 0,
monthly_limit: 1000,
remaining: 1000,
percentage_used: 0,
reset_date: new Date().toISOString(),
days_until_reset: 30
},
// Include detailed credit information
credits: {
total: data.usage?.total_credits || 0,
trial: data.usage?.trial_credits || 0,
monthly: data.usage?.monthly_credits || 0,
paid: data.usage?.paid_credits || 0
}
};
} catch (error) {
throw error;
}
}
async verifyFdcId(fdcId) {
try {
const response = await this.client.get(`/api/v2/nutrition/nutrition/verify/${fdcId}`);
return response.data;
} catch (error) {
throw error;
}
}
async healthCheck() {
try {
// Basic health check (no auth required)
const tempHeaders = { ...this.client.defaults.headers };
delete this.client.defaults.headers['X-API-Key'];
const response = await this.client.get('/health');
// Restore headers
this.client.defaults.headers = tempHeaders;
return response.data;
} catch (error) {
// Restore headers even on error
if (this.apiKey) {
this.client.defaults.headers['X-API-Key'] = this.apiKey;
}
throw error;
}
}
}
module.exports = { NutritionAPI, ApiError };