From e72951d32d9f0282d56c52a102e0d5d7f9d94b4a Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Mon, 9 Mar 2026 17:00:29 -0600 Subject: [PATCH] Enhance buscar-bibliografia function with Open Library integration and parameter handling --- .../functions/buscar-bibliografia/index.ts | 153 ++++++++++++++++-- 1 file changed, 141 insertions(+), 12 deletions(-) diff --git a/supabase/functions/buscar-bibliografia/index.ts b/supabase/functions/buscar-bibliografia/index.ts index 9b47e34..f45b5a5 100644 --- a/supabase/functions/buscar-bibliografia/index.ts +++ b/supabase/functions/buscar-bibliografia/index.ts @@ -6,6 +6,11 @@ import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts"; console.log("Starting buscar-bibliografia function"); type GoogleBooksVolume = Record; +type OpenLibraryDoc = Record; + +type EndpointResult = + | { endpoint: "google"; item: GoogleBooksVolume } + | { endpoint: "open_library"; item: OpenLibraryDoc }; interface GoogleBooksVolumesListResponse { kind?: string; @@ -13,17 +18,54 @@ interface GoogleBooksVolumesListResponse { items?: GoogleBooksVolume[]; } +interface OpenLibrarySearchResponse { + num_found?: number; + start?: number; + docs?: OpenLibraryDoc[]; +} + +const ISO6391 = z.string().regex( + /^[a-z]{2}$/i, + "Debe ser ISO 639-1 (2 letras)", +); +const ISO6392 = z.string().regex( + /^[a-z]{3}$/i, + "Debe ser ISO 639-2 (3 letras)", +); + 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(); + .strict(); + +const GoogleParamsSchema = z + .object({ + orderBy: z.enum(["newest", "relevance"]).optional(), + langRestrict: ISO6391.optional(), + startIndex: z.number().int().min(0).optional(), + }) + .passthrough() + .optional() + .default({}); + +const OpenLibraryParamsSchema = z + .object({ + language: ISO6392.optional(), + page: z.number().int().min(1).optional(), + sort: z.string().optional(), + }) + .passthrough() + .optional() + .default({}); const BodySchema = z .object({ searchTerms: SearchTermsSchema, + + // Parámetros por endpoint + google: GoogleParamsSchema, + openLibrary: OpenLibraryParamsSchema, }) .strict(); @@ -57,6 +99,40 @@ function buildUrlWithSearchTerms( return url.toString(); } +function pickSerializableParams( + input: Record, +): Record< + string, + string | number | boolean | Array +> { + const out: Record< + string, + string | number | boolean | Array + > = {}; + + for (const [key, value] of Object.entries(input)) { + if (value === undefined || value === null) continue; + + if (Array.isArray(value)) { + const arr = value + .filter((v) => v !== undefined && v !== null) + .filter((v) => + ["string", "number", "boolean"].includes(typeof v) + ) as Array< + string | number | boolean + >; + if (arr.length) out[key] = arr; + continue; + } + + if (["string", "number", "boolean"].includes(typeof value)) { + out[key] = value as string | number | boolean; + } + } + + return out; +} + Deno.serve(async (req: Request): Promise => { const url = new URL(req.url); const functionName = url.pathname.split("/").pop(); @@ -117,17 +193,40 @@ Deno.serve(async (req: Request): Promise => { } const baseUrl = "https://www.googleapis.com/books/v1/volumes"; - const requestUrl = buildUrlWithSearchTerms(baseUrl, { + const googleParams = pickSerializableParams({ + ...body.google, ...body.searchTerms, key: GOOGLE_API_KEY, + maxResults: 20, + startIndex: (body.google as Record)?.startIndex ?? 0, }); + // No mandar parámetros exclusivos de Open Library a Google + delete (googleParams as Record)["page"]; + delete (googleParams as Record)["language"]; - const googleResp = await fetch(requestUrl, { - method: "GET", - headers: { - Accept: "application/json", - }, + const googleUrl = buildUrlWithSearchTerms(baseUrl, googleParams); + + const openLibraryBaseUrl = "https://openlibrary.org/search.json"; + const openLibraryParams = pickSerializableParams({ + ...body.openLibrary, + q: body.searchTerms.q, + limit: 20, + page: (body.openLibrary as Record)?.page ?? 1, }); + // No mandar parámetros exclusivos de Google a Open Library + delete (openLibraryParams as Record)["langRestrict"]; + delete (openLibraryParams as Record)["orderBy"]; + delete (openLibraryParams as Record)["startIndex"]; + + const openLibraryUrl = buildUrlWithSearchTerms( + openLibraryBaseUrl, + openLibraryParams, + ); + + const [googleResp, openLibraryResp] = await Promise.all([ + fetch(googleUrl, { headers: { Accept: "application/json" } }), + fetch(openLibraryUrl, { headers: { Accept: "application/json" } }), + ]); if (!googleResp.ok) { const text = await googleResp.text().catch(() => ""); @@ -143,10 +242,40 @@ Deno.serve(async (req: Request): Promise => { ); } - const data = (await googleResp.json()) as GoogleBooksVolumesListResponse; - const items = Array.isArray(data?.items) ? data.items : []; + if (!openLibraryResp.ok) { + const text = await openLibraryResp.text().catch(() => ""); + throw new HttpError( + 502, + "Error al consultar Open Library.", + "OPEN_LIBRARY_REQUEST_FAILED", + { + status: openLibraryResp.status, + statusText: openLibraryResp.statusText, + body: text || null, + }, + ); + } - return sendSuccess(items); + const googleData = + (await googleResp.json()) as GoogleBooksVolumesListResponse; + const openLibraryData = + (await openLibraryResp.json()) as OpenLibrarySearchResponse; + + const googleItems = Array.isArray(googleData?.items) + ? googleData.items.slice(0, 20) + : []; + const openLibraryDocs = Array.isArray(openLibraryData?.docs) + ? openLibraryData.docs.slice(0, 20) + : []; + + const results: EndpointResult[] = [ + ...googleItems.map((item) => ({ endpoint: "google", item } as const)), + ...openLibraryDocs.map(( + item, + ) => ({ endpoint: "open_library", item } as const)), + ]; + + return sendSuccess(results); } catch (error) { if (error instanceof HttpError) { console.error( -- 2.52.0