feat(api): implement product caching mechanism

- Add NODE_ENV=production to .env for environment configuration
- Enhance product API route with JSON cache manager for local storage and retrieval
- Introduce cache-only mode and detailed logging for better performance and offline support
- Update UI to display cache status and storage statistics
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-09 04:30:46 -03:00
1 parent bddf19e5c8
commit c173fec668
19 files changed
+9240 -175

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
import { jsonCacheManager } from "@/lib/jsonCacheManager";
export async function POST(request: NextRequest) {
try {
const { products } = await request.json();
if (!products || !Array.isArray(products)) {
return NextResponse.json(
{ error: "Produtos inválidos" },
{ status: 400 }
);
}
// Salvar produtos no cache
const result = await jsonCacheManager.saveProducts(products);
console.log(`[Cache Sync] ${result.saved} novos produtos salvos`);
return NextResponse.json({
success: true,
saved: result.saved,
failed: result.failed,
});
} catch (error) {
console.error("Erro ao sincronizar cache:", error);
return NextResponse.json(
{ error: "Erro ao sincronizar cache" },
{ status: 500 }
);
}
}
+55 -3
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { jsonCacheManager } from "@/lib/jsonCacheManager";
const USER_AGENT = "NutriScan/1.0 (https://nutriscan.com.br)";
@@ -7,6 +8,8 @@ export async function GET(
{ params }: { params: { barcode: string } }
) {
const { barcode } = params;
const searchParams = request.nextUrl.searchParams;
const cacheOnly = searchParams.get("cache_only") === "true";
// Validar código de barras
const cleanBarcode = barcode.replace(/\D/g, "");
@@ -18,6 +21,36 @@ export async function GET(
}
try {
console.log(`[API] Iniciando busca de produto: ${cleanBarcode}`);
// Primeiro, buscar no cache JSON
console.log(`[API] Buscando produto ${cleanBarcode} no cache JSON...`);
const localProduct = await jsonCacheManager.getProductByCode(cleanBarcode);
if (localProduct) {
console.log(`Produto ${cleanBarcode} encontrado no cache JSON`);
return NextResponse.json({
...localProduct,
fromCache: true,
fromAPI: false,
});
}
// Se modo cache-only, retornar 404 se não encontrar no cache
if (cacheOnly || process.env.NEXT_PUBLIC_CACHE_ONLY_MODE === "true") {
console.log(
`[Cache-Only Mode] Produto ${cleanBarcode} não encontrado no cache JSON`
);
return NextResponse.json(
{ error: "Produto não encontrado no cache JSON" },
{ status: 404 }
);
}
// Se não encontrou no cache JSON, buscar na API externa
console.log(
`Produto ${cleanBarcode} não encontrado no cache JSON, buscando na API externa...`
);
const response = await fetch(
`https://world.openfoodfacts.org/api/v2/product/${cleanBarcode}.json`,
{
@@ -44,11 +77,30 @@ export async function GET(
);
}
return NextResponse.json(data.product);
// Salvar produto no cache JSON
if (data.product) {
console.log(`Salvando produto ${cleanBarcode} no cache JSON...`);
const saveResult = await jsonCacheManager.saveProduct(data.product);
if (saveResult) {
console.log(`Produto ${cleanBarcode} salvo no cache JSON com sucesso`);
} else {
console.log(`Falha ao salvar produto ${cleanBarcode} no cache JSON`);
}
}
return NextResponse.json({
...data.product,
fromCache: false,
fromAPI: true,
});
} catch (error) {
console.error("Erro ao buscar produto:", error);
console.error("[API] Erro ao buscar produto:", error);
return NextResponse.json(
{ error: "Erro ao buscar produto" },
{
error: "Erro interno do servidor",
fromCache: false,
fromAPI: false,
},
{ status: 500 }
);
}
+132 -18
View File
@@ -1,4 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { jsonCacheManager } from "@/lib/jsonCacheManager";
import { Product } from "@/lib/openFoodFactsApi";
import { sortByRelevance } from "@/lib/searchUtils";
const USER_AGENT = "NutriScan/1.0 (https://nutriscan.com.br)";
@@ -8,6 +11,7 @@ export async function GET(request: NextRequest) {
const country = searchParams.get("country");
const page = searchParams.get("page") || "1";
const pageSize = searchParams.get("page_size") || "20";
const cacheOnly = searchParams.get("cache_only") === "true";
if (!query) {
return NextResponse.json(
@@ -27,9 +31,9 @@ export async function GET(request: NextRequest) {
);
}
if (isNaN(pageSizeNum) || pageSizeNum < 1 || pageSizeNum > 50) {
if (isNaN(pageSizeNum) || pageSizeNum < 1 || pageSizeNum > 100) {
return NextResponse.json(
{ error: "Invalid page_size parameter (max 50)" },
{ error: "Invalid page_size parameter (max 100)" },
{ status: 400 }
);
}
@@ -180,29 +184,139 @@ export async function GET(request: NextRequest) {
}
try {
const response = await fetch(
`https://world.openfoodfacts.org/cgi/search.pl?${params.toString()}`,
{
headers: {
"User-Agent": USER_AGENT,
Accept: "application/json",
},
}
console.log(
`[API] Iniciando busca: "${query}" (país: ${
country || "todos"
}, page_size: ${pageSizeNum})`
);
if (!response.ok) {
return NextResponse.json(
{ error: "Erro ao buscar produtos" },
{ status: response.status }
// Primeiro, buscar no cache JSON
console.log(`[API] Buscando "${query}" no cache JSON...`);
const localResults = await jsonCacheManager.searchProducts(
query,
country || undefined,
pageSizeNum
);
// Se modo cache-only estiver ativo, retornar apenas resultados locais
if (cacheOnly || process.env.NEXT_PUBLIC_CACHE_ONLY_MODE === "true") {
console.log(
`[Cache-Only Mode] Retornando apenas ${localResults.products.length} produtos do cache`
);
return NextResponse.json({
products: localResults.products,
count: localResults.products.length,
page: pageNum,
page_size: pageSizeNum,
fromCache: true,
fromAPI: false,
cacheOnly: true,
});
}
const data = await response.json();
return NextResponse.json(data);
let fromAPI = false;
let apiProducts: Product[] = [];
// Se não encontrou resultados suficientes no cache JSON, buscar na API externa
if (localResults.products.length < pageSizeNum) {
console.log(
`Cache JSON retornou ${localResults.products.length} produtos, buscando na API externa...`
);
const response = await fetch(
`https://world.openfoodfacts.org/cgi/search.pl?${params.toString()}`,
{
headers: {
"User-Agent": USER_AGENT,
Accept: "application/json",
},
}
);
if (response.ok) {
const apiData = await response.json();
apiProducts = apiData.products || [];
fromAPI = true;
// Ordenar produtos da API por relevância
if (apiProducts.length > 0) {
console.log(
`Ordenando ${apiProducts.length} produtos da API por relevância...`
);
apiProducts = sortByRelevance(apiProducts, query);
console.log(
`Salvando ${apiProducts.length} produtos da API no cache JSON...`
);
const saveResult = await jsonCacheManager.saveProducts(apiProducts);
console.log(
`Cache JSON atualizado: ${saveResult.saved} salvos, ${saveResult.failed} falharam`
);
}
}
}
// Combinar resultados JSON e da API
const allProducts: Product[] = [...localResults.products];
const localProductCodes = new Set(localResults.products.map((p) => p.code));
// Adicionar apenas produtos da API que não estão no cache JSON
const newApiProducts = apiProducts.filter(
(p) => !localProductCodes.has(p.code)
);
allProducts.push(...newApiProducts);
// Reordenar a lista combinada por relevância
console.log(
`Reordenando ${allProducts.length} produtos combinados por relevância...`
);
const sortedProducts = sortByRelevance(allProducts, query);
// Limitar ao tamanho da página solicitada
const paginatedProducts = sortedProducts.slice(0, pageSizeNum);
const responseData = {
products: paginatedProducts,
count: allProducts.length,
page: pageNum,
page_size: pageSizeNum,
skip: (pageNum - 1) * pageSizeNum,
fromCache: localResults.fromCache,
fromAPI: fromAPI,
cacheStats: {
localProducts: localResults.products.length,
apiProducts: apiProducts.length,
newProducts: newApiProducts.length,
totalReturned: paginatedProducts.length,
},
};
console.log(
`Busca finalizada: ${paginatedProducts.length} produtos (${localResults.products.length} do cache JSON, ${newApiProducts.length} novos da API)`
);
return NextResponse.json(responseData);
} catch (error) {
console.error("Erro ao buscar produtos:", error);
console.error("[API] Erro ao buscar produtos:", error);
// Retornar resposta de erro estruturada em vez de quebrar
return NextResponse.json(
{ error: "Erro ao buscar produtos" },
{
products: [],
count: 0,
page: pageNum,
page_size: pageSizeNum,
skip: (pageNum - 1) * pageSizeNum,
fromCache: false,
fromAPI: false,
error: "Erro interno do servidor",
cacheStats: {
localProducts: 0,
apiProducts: 0,
newProducts: 0,
totalReturned: 0,
},
},
{ status: 500 }
);
}
+48
View File
@@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
import { jsonCacheManager } from "@/lib/jsonCacheManager";
export async function GET() {
try {
console.log("[API] Iniciando busca de estatísticas do storage...");
// Verificar se o cache JSON está inicializado
const isInitialized = await jsonCacheManager.isInitialized();
if (!isInitialized) {
console.log("[API] Cache JSON não inicializado, retornando dados vazios");
return NextResponse.json({
success: true,
data: {
totalProducts: 0,
storageSizeMB: 0,
lastUpdated: 0,
averageProductSize: 0,
},
timestamp: Date.now(),
});
}
// Obter estatísticas do storage JSON
const stats = await jsonCacheManager.getStorageStats();
console.log("[API] Estatísticas obtidas:", stats);
return NextResponse.json({
success: true,
data: stats,
timestamp: Date.now(),
});
} catch (error) {
console.error("[API] Erro ao obter estatísticas do storage:", error);
// Retornar dados vazios em caso de erro para não quebrar o frontend
return NextResponse.json({
success: true,
data: {
totalProducts: 0,
storageSizeMB: 0,
lastUpdated: 0,
averageProductSize: 0,
},
timestamp: Date.now(),
});
}
}