import "@supabase/functions-js/edge-runtime.d.ts"; import { z } from "zod"; import { corsHeaders } from "../_shared/cors.ts"; import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts"; console.log("Starting buscar-bibliografia function"); type GoogleBooksVolume = Record; interface GoogleBooksVolumesListResponse { kind?: string; totalItems?: number; items?: GoogleBooksVolume[]; } const SearchTermsSchema = z .object({ q: z.string().min(1, "q es requerido"), maxResults: z.number().int().min(0).max(40), orderBy: z.enum(["newest", "relevance"]).optional(), }) .passthrough(); const BodySchema = z .object({ searchTerms: SearchTermsSchema, }) .strict(); type BuscarBibliografiaRequest = z.infer; function formatZodIssues(issues: z.ZodIssue[]): string { return issues .map((issue, i) => { const path = issue.path.length ? issue.path.join(".") : "(root)"; return `${i + 1}. ${path}: ${issue.message}`; }) .join("\n"); } function buildUrlWithSearchTerms( baseUrl: string, searchTerms: Record, ): string { const url = new URL(baseUrl); for (const [key, value] of Object.entries(searchTerms)) { if (value === undefined || value === null) continue; if (Array.isArray(value)) { for (const v of value) { if (v === undefined || v === null) continue; url.searchParams.append(key, String(v)); } continue; } url.searchParams.set(key, String(value)); } return url.toString(); } Deno.serve(async (req: Request): Promise => { const url = new URL(req.url); const functionName = url.pathname.split("/").pop(); console.log( `[${new Date().toISOString()}][${functionName}]: Request received`, ); if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: corsHeaders }); } try { if (req.method !== "POST") { throw new HttpError(405, "Método no permitido.", "METHOD_NOT_ALLOWED", { method: req.method, }); } const contentType = (req.headers.get("content-type") || "").toLowerCase(); if (!contentType.includes("application/json")) { throw new HttpError( 415, "Content-Type no soportado.", "UNSUPPORTED_MEDIA_TYPE", { contentType, expected: "application/json" }, ); } let rawBody: unknown; try { rawBody = await req.json(); } catch (e) { throw new HttpError(400, "Body JSON inválido.", "INVALID_JSON", { cause: e, }); } const parsed = BodySchema.safeParse(rawBody); if (!parsed.success) { throw new HttpError( 422, formatZodIssues(parsed.error.issues), "VALIDATION_ERROR", parsed.error, ); } const body: BuscarBibliografiaRequest = parsed.data; const GOOGLE_API_KEY = Deno.env.get("GOOGLE_API_KEY"); if (!GOOGLE_API_KEY) { throw new HttpError( 500, "Configuración del servidor incompleta.", "MISSING_ENV", { missing: ["GOOGLE_API_KEY"] }, ); } const baseUrl = "https://www.googleapis.com/books/v1/volumes"; const requestUrl = buildUrlWithSearchTerms(baseUrl, { ...body.searchTerms, key: GOOGLE_API_KEY, }); const googleResp = await fetch(requestUrl, { method: "GET", headers: { Accept: "application/json", }, }); if (!googleResp.ok) { const text = await googleResp.text().catch(() => ""); throw new HttpError( 502, "Error al consultar Google Books.", "GOOGLE_BOOKS_REQUEST_FAILED", { status: googleResp.status, statusText: googleResp.statusText, body: text || null, }, ); } const data = (await googleResp.json()) as GoogleBooksVolumesListResponse; const items = Array.isArray(data?.items) ? data.items : []; return sendSuccess(items); } catch (error) { if (error instanceof HttpError) { console.error( `[${new Date().toISOString()}][${functionName}] ⚠️ Handled Error:`, { message: error.message, code: error.code, internalDetails: error.internalDetails || "N/A", }, ); return sendError(error.status, error.message, error.code); } const unexpectedError = error instanceof Error ? error : new Error(String(error)); console.error( `[${ new Date().toISOString() }][${functionName}] 💥 CRITICAL UNHANDLED ERROR:`, unexpectedError.stack || unexpectedError.message, ); return sendError( 500, "Ocurrió un error inesperado en el servidor.", "INTERNAL_SERVER_ERROR", ); } }); /* To invoke locally: 1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start) 2. Make an HTTP request: curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/buscar-bibliografia' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \ --header 'Content-Type: application/json' \ --data '{"name":"Functions"}' */