5fac762678
- Implemented buscar-bibliografia function in index.ts - Added Deno import map for dependencies in deno.json - Included request validation and error handling - Integrated Google Books API to fetch bibliographic data based on search terms
193 lines
5.2 KiB
TypeScript
193 lines
5.2 KiB
TypeScript
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<string, unknown>;
|
|
|
|
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<typeof BodySchema>;
|
|
|
|
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, unknown>,
|
|
): 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<Response> => {
|
|
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"}'
|
|
|
|
*/
|