Files
nutriscan/app/api/search/route.ts
T
evandro bf688257ef refactor(api): extrair montagem e paginação da busca para lib/offSearch
GET /api/search passa de CC 41 para <= 10. Testes de caracterização
(sem rede: fetch e cache JSON temporário) cobrem validação, filtros,
ordenação, paginação, modo cache-only e erros.

Também: parameter property de NutriScanError virou campo explícito e
imports só de tipo usam import type, para o type stripping nativo do
Node; o script de teste não usa mais --experimental-transform-types
(removida no Node 26).
2026-10-10 04:14:48 -03:00

124 lines
4.2 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { jsonCacheManager } from "@/lib/jsonCacheManager";
import { buildOffSearchParams, fetchOffProducts, parsePagination } from "@/lib/offSearch";
import type { Product } from "@/lib/openFoodFactsApi";
import { sortByRelevance } from "@/lib/searchUtils";
type LocalResults = Awaited<ReturnType<typeof jsonCacheManager.searchProducts>>;
function isCacheOnly(searchParams: URLSearchParams): boolean {
return searchParams.get("cache_only") === "true" || process.env.NEXT_PUBLIC_CACHE_ONLY_MODE === "true";
}
/** Complementa o cache com a API externa quando ele não alcança o tamanho da página. */
async function loadApiProducts(
query: string,
params: URLSearchParams,
localCount: number,
pageSize: number
): Promise<Product[]> {
if (localCount >= pageSize) return [];
console.log(`Cache JSON retornou ${localCount} produtos, buscando na API externa...`);
const fetched = await fetchOffProducts(params, pageSize - localCount);
if (fetched.length === 0) return [];
const apiProducts = sortByRelevance(fetched, query);
const saveResult = await jsonCacheManager.saveProducts(apiProducts);
console.log(`Cache JSON atualizado: ${saveResult.saved} salvos, ${saveResult.failed} falharam`);
return apiProducts;
}
function buildSearchResponse(
query: string,
localResults: LocalResults,
apiProducts: Product[],
page: number,
pageSize: number
) {
const localCodes = new Set(localResults.products.map((p) => p.code));
const newApiProducts = apiProducts.filter((p) => !localCodes.has(p.code));
const allProducts = [...localResults.products, ...newApiProducts];
const paginated = sortByRelevance(allProducts, query).slice(0, pageSize);
console.log(
`Busca finalizada: ${paginated.length} produtos (${localResults.products.length} do cache JSON, ${newApiProducts.length} novos da API)`
);
return {
products: paginated,
count: allProducts.length,
page,
page_size: pageSize,
skip: (page - 1) * pageSize,
fromCache: localResults.fromCache,
fromAPI: apiProducts.length > 0,
cacheStats: {
localProducts: localResults.products.length,
apiProducts: apiProducts.length,
newProducts: newApiProducts.length,
totalReturned: paginated.length,
},
};
}
function errorResponse(page: number, pageSize: number) {
return NextResponse.json(
{
products: [],
count: 0,
page,
page_size: pageSize,
skip: (page - 1) * pageSize,
fromCache: false,
fromAPI: false,
error: "Erro interno do servidor",
cacheStats: { localProducts: 0, apiProducts: 0, newProducts: 0, totalReturned: 0 },
},
{ status: 500 }
);
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get("q");
const country = searchParams.get("country");
if (!query) {
return NextResponse.json({ error: "Query parameter is required" }, { status: 400 });
}
const pagination = parsePagination(searchParams);
if ("error" in pagination) {
return NextResponse.json({ error: pagination.error }, { status: 400 });
}
const { page, pageSize, pageParam } = pagination;
try {
console.log(`[API] Iniciando busca: "${query}" (país: ${country || "todos"}, page_size: ${pageSize})`);
const localResults = await jsonCacheManager.searchProducts(query, country || undefined, pageSize);
if (isCacheOnly(searchParams)) {
console.log(`[Cache-Only Mode] Retornando apenas ${localResults.products.length} produtos do cache`);
return NextResponse.json({
products: localResults.products,
count: localResults.products.length,
page,
page_size: pageSize,
fromCache: true,
fromAPI: false,
cacheOnly: true,
});
}
const params = buildOffSearchParams(searchParams, query, country, pageParam);
const apiProducts = await loadApiProducts(query, params, localResults.products.length, pageSize);
return NextResponse.json(buildSearchResponse(query, localResults, apiProducts, page, pageSize));
} catch (error) {
console.error("[API] Erro ao buscar produtos:", error);
return errorResponse(page, pageSize);
}
}