// 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), }); 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 { const { data, error } = await supabase .from("planes_estudio") .select("datos") .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 data.datos; } 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_TEMPLATE_ID = Deno.env.get("CARBONE_TEMPLATE_ID"); const CARBONE_BASE_URL = Deno.env.get("CARBONE_BASE_URL") || "https://carbone.lci.ulsa.mx"; if (!CARBONE_API_TOKEN || !CARBONE_TEMPLATE_ID) { throw new HttpError( 500, "Configuración del servidor incompleta.", "MISSING_ENV", { missing: [ !CARBONE_API_TOKEN ? "CARBONE_API_TOKEN" : null, !CARBONE_TEMPLATE_ID ? "CARBONE_TEMPLATE_ID" : 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 = await loadPlanDatos(supabase, input.plan_estudio_id); const carbone = new CarboneClient(CARBONE_BASE_URL, CARBONE_API_TOKEN); const result = await carbone.render( CARBONE_TEMPLATE_ID, { data: datos, convertTo: "pdf" }, { 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 (err) { const error = err instanceof HttpError ? err : new HttpError( 500, "Error interno del servidor.", "INTERNAL_SERVER_ERROR", { original: err instanceof Error ? { message: err.message, stack: err.stack } : err, }, ); return sendError(error.status, error.message, error.code); } }); /* 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":""}' */