// Follow this setup guide to integrate the Deno language server with your editor: // https://deno.land/manual/getting_started/setup_your_environment // This enables autocomplete, go to definition, etc. // Setup type definitions for built-in Supabase Runtime APIs import "@supabase/functions-js/edge-runtime.d.ts"; import { createClient } from "@supabase/supabase-js"; import { z } from "zod"; import { corsHeaders } from "../_shared/cors.ts"; import type { Database, Json } from "../_shared/database.types.ts"; import { HttpError, sendError } from "../_shared/utils.ts"; import { CarboneClient } from "./carbone.ts"; const ActionSchema = z.object({ action: z.string().min(1), }); const DownloadReportSchema = z.object({ action: z.literal("downloadReport"), plan_estudio_id: z.string().min(1), body: z.record(z.unknown()), }); type DownloadReportInput = z.infer; function getAuthHeader(req: Request): string | null { return req.headers.get("Authorization") ?? req.headers.get("authorization"); } async function loadPlanDatos( supabase: ReturnType>, planEstudioId: string, ): Promise<{ datos: Json; estructura_id: string }> { const { data, error } = await supabase .from("planes_estudio") .select("datos, estructura_id") .eq("id", planEstudioId) .maybeSingle(); if (error) { throw new HttpError( 500, "Error consultando el plan de estudios.", "DB_ERROR", { message: error.message, details: error.details, hint: error.hint }, ); } if (!data) { throw new HttpError( 404, "Plan de estudios no encontrado.", "NOT_FOUND", { plan_estudio_id: planEstudioId }, ); } return { datos: data.datos, estructura_id: data.estructura_id, }; } async function loadTemplateIdForEstructura( supabase: ReturnType>, estructuraId: string, ): Promise { const { data, error } = await supabase .from("estructuras_plan") .select("template_id") .eq("id", estructuraId) .maybeSingle(); if (error) { throw new HttpError( 500, "Error consultando la estructura del plan.", "DB_ERROR", { message: error.message, details: error.details, hint: error.hint }, ); } if (!data) { throw new HttpError( 404, "Estructura del plan no encontrada.", "NOT_FOUND", { estructura_id: estructuraId }, ); } if (!data.template_id) { throw new HttpError( 409, "La estructura del plan no tiene template_id configurado.", "MISSING_TEMPLATE_ID", { estructura_id: estructuraId }, ); } return data.template_id; } 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 authHeader = getAuthHeader(req); if (!authHeader) { throw new HttpError(401, "No autorizado.", "UNAUTHORIZED", { reason: "missing_authorization_header", }); } const contentType = (req.headers.get("content-type") || "").toLowerCase(); if (!contentType.startsWith("application/json")) { throw new HttpError( 415, "Content-Type no soportado.", "UNSUPPORTED_MEDIA_TYPE", { contentType }, ); } const SUPABASE_URL = Deno.env.get("SUPABASE_URL"); const SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); if (!SUPABASE_URL || !SERVICE_ROLE_KEY) { throw new HttpError( 500, "Configuración del servidor incompleta.", "MISSING_ENV", { missing: [ !SUPABASE_URL ? "SUPABASE_URL" : null, !SERVICE_ROLE_KEY ? "SUPABASE_SERVICE_ROLE_KEY" : null, ].filter(Boolean), }, ); } const CARBONE_API_TOKEN = Deno.env.get("CARBONE_API_TOKEN"); const CARBONE_BASE_URL = Deno.env.get("CARBONE_BASE_URL") || "https://carbone.lci.ulsa.mx"; if (!CARBONE_API_TOKEN) { throw new HttpError( 500, "Configuración del servidor incompleta.", "MISSING_ENV", { missing: [ !CARBONE_API_TOKEN ? "CARBONE_API_TOKEN" : null, ].filter(Boolean), }, ); } const bodyUnknown: unknown = await req.json(); const { action } = ActionSchema.parse(bodyUnknown); const supabase = createClient(SUPABASE_URL, SERVICE_ROLE_KEY, { auth: { persistSession: false }, }); switch (action) { case "downloadReport": { const input: DownloadReportInput = DownloadReportSchema.parse( bodyUnknown, ); const { datos, estructura_id } = await loadPlanDatos( supabase, input.plan_estudio_id, ); const templateId = await loadTemplateIdForEstructura( supabase, estructura_id, ); const carbone = new CarboneClient(CARBONE_BASE_URL, CARBONE_API_TOKEN); const { data: _ignoredData, ...extraBody } = input.body; console.log("body:", extraBody); const result = await carbone.render( templateId, { data: datos, ...extraBody }, { download: true }, ); if (!(result && typeof result === "object" && "buffer" in result)) { throw new HttpError( 502, "Respuesta inválida de Carbone.", "UPSTREAM_INVALID_RESPONSE", ); } const download = result as { buffer: Uint8Array; contentType: string | null; contentDisposition: string | null; }; const bytes = download.buffer; const arrayBuffer = bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength, ) as ArrayBuffer; const blob = new Blob([arrayBuffer], { type: download.contentType ?? "application/octet-stream", }); const headers = new Headers({ ...corsHeaders, "Content-Type": download.contentType ?? "application/octet-stream", Connection: "keep-alive", }); if (download.contentDisposition) { headers.set("Content-Disposition", download.contentDisposition); } console.log( `[${ new Date().toISOString() }][${functionName}]: Request processed successfully`, ); return new Response(blob, { status: 200, headers }); } default: throw new HttpError( 400, "Acción no soportada.", "UNSUPPORTED_ACTION", { action }, ); } } 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: # Requires secrets: # - CARBONE_API_TOKEN # - CARBONE_TEMPLATE_ID # Optional: # - CARBONE_BASE_URL (defaults to https://carbone.lci.ulsa.mx) curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/carbone-io-wrapper' \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{"action":"downloadReport","plan_estudio_id":""}' */