93c4e46e62
Por hacer: - permitir descargar los otros dos anexos tanto en pdf como en word/excel segun el caso - quitar el hardcode de convertir a pdf de downloadReport - Extraer el template id no de una variable de entorno, sino de la columna correspondiente tanto en la tabla de la estructura del plan como en la tabla de la estructura de la asignatura. En alguna parte de la BDD se tendrá que poner el template id del anexo del mapa curricular - Permitir otros métodos
231 lines
6.7 KiB
TypeScript
231 lines
6.7 KiB
TypeScript
// 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<typeof DownloadReportSchema>;
|
|
|
|
function getAuthHeader(req: Request): string | null {
|
|
return req.headers.get("Authorization") ?? req.headers.get("authorization");
|
|
}
|
|
|
|
async function loadPlanDatos(
|
|
supabase: ReturnType<typeof createClient<Database>>,
|
|
planEstudioId: string,
|
|
): Promise<Json> {
|
|
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<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 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<Database>(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 <JWT>' \
|
|
--header 'Content-Type: application/json' \
|
|
--data '{"action":"downloadReport","plan_estudio_id":"<uuid>"}'
|
|
|
|
*/
|