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).
124 lines
4.2 KiB
TypeScript
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);
|
|
}
|
|
}
|