Creada edge function wrapper para carbone.io del lci con un método (descargar reporte) #80

Merged
Guillermo.Arrieta merged 3 commits from issue/76-crear-edge-function-para-plantillas-de-carbone into main 2026-03-20 23:47:30 +00:00
8 changed files with 826 additions and 26 deletions
+13 -21
View File
@@ -489,7 +489,7 @@
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 6,
"id": "caf8bc20", "id": "caf8bc20",
"metadata": {}, "metadata": {},
"outputs": [ "outputs": [
@@ -500,13 +500,13 @@
" success: \u001b[33mtrue\u001b[39m,\n", " success: \u001b[33mtrue\u001b[39m,\n",
" data: {\n", " data: {\n",
" category: \u001b[32m\"Planes de estudio\"\u001b[39m,\n", " category: \u001b[32m\"Planes de estudio\"\u001b[39m,\n",
" comment: \u001b[32m\"Plantilla de anexo 1 SEP RVOE Acuerdo 17-11-17\"\u001b[39m,\n", " comment: \u001b[32m\"Plantillaaa de anexo 1 SEP RVOE Acuerdo 17-11-17\"\u001b[39m,\n",
" versionId: \u001b[32m\"1990f3351b4313779ae3ef3caeaea14dea51c3a98a691fb90a347dec53f875e6\"\u001b[39m\n", " versionId: \u001b[32m\"1990f3351b4313779ae3ef3caeaea14dea51c3a98a691fb90a347dec53f875e6\"\u001b[39m\n",
" }\n", " }\n",
"}" "}"
] ]
}, },
"execution_count": 19, "execution_count": 6,
"metadata": {}, "metadata": {},
"output_type": "execute_result" "output_type": "execute_result"
} }
@@ -514,13 +514,13 @@
"source": [ "source": [
"await carbone.patchTemplate(templateID, {\n", "await carbone.patchTemplate(templateID, {\n",
" category: \"Planes de estudio\",\n", " category: \"Planes de estudio\",\n",
" comment: \"Plantilla de anexo 1 SEP RVOE Acuerdo 17-11-17\",\n", " comment: \"Plantillaaa de anexo 1 SEP RVOE Acuerdo 17-11-17\",\n",
"});\n" "});\n"
] ]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": 7,
"id": "929883b9", "id": "929883b9",
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
@@ -570,7 +570,7 @@
" { download: true },\n", " { download: true },\n",
");\n", ");\n",
"\n", "\n",
"await Deno.writeFile(\"../dist/anexo1.docx\", result.buffer);\n" "await Deno.writeFile(\"../anexo1.docx\", result.buffer);\n"
] ]
}, },
{ {
@@ -628,16 +628,6 @@
"await Deno.writeFile(\"../dist/anexo1.pdf\", result.buffer);\n" "await Deno.writeFile(\"../dist/anexo1.pdf\", result.buffer);\n"
] ]
}, },
{
"cell_type": "code",
"execution_count": null,
"id": "4528a917",
"metadata": {},
"outputs": [],
"source": [
"await carbone.list"
]
},
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
@@ -661,7 +651,7 @@
" type: \"docx\",\n", " type: \"docx\",\n",
" name: \"Anexo 1\",\n", " name: \"Anexo 1\",\n",
" category: \"Planes de estudio\",\n", " category: \"Planes de estudio\",\n",
" comment: \"Plantilla de anexo 1 SEP RVOE Acuerdo 17-11-17\",\n", " comment: \"Plantillaaa de anexo 1 SEP RVOE Acuerdo 17-11-17\",\n",
" tags: [],\n", " tags: [],\n",
" origin: 1\n", " origin: 1\n",
" }\n", " }\n",
@@ -674,10 +664,10 @@
"source": [ "source": [
"const versions = await carbone.listTemplates({\n", "const versions = await carbone.listTemplates({\n",
" id: \"1373945625988937236\",\n", " id: \"1373945625988937236\",\n",
" includeVersions: true\n", " includeVersions: true,\n",
"});\n", "});\n",
"\n", "\n",
"console.log(versions);" "console.log(versions);\n"
] ]
}, },
{ {
@@ -746,8 +736,10 @@
"console.log(result);\n", "console.log(result);\n",
"\n", "\n",
"// const file = await carbone.downloadRender(render.data.renderId);\n", "// const file = await carbone.downloadRender(render.data.renderId);\n",
"const file = await carbone.downloadRender(\"X1Bx6Stre95Sm54jHxIwpQYW5leG8xLnBkZg.pdf\");\n", "const file = await carbone.downloadRender(\n",
"await Deno.writeFile(\"dist/anexo1.pdf\", file.buffer);" " \"X1Bx6Stre95Sm54jHxIwpQYW5leG8xLnBkZg.pdf\",\n",
");\n",
"await Deno.writeFile(\"dist/anexo1.pdf\", file.buffer);\n"
] ]
}, },
{ {
+11
View File
@@ -76,3 +76,14 @@ entrypoint = "./functions/buscar-bibliografia/index.ts"
# Specifies static files to be bundled with the function. Supports glob patterns. # Specifies static files to be bundled with the function. Supports glob patterns.
# For example, if you want to serve static HTML pages in your function: # For example, if you want to serve static HTML pages in your function:
# static_files = [ "./functions/buscar-bibliografia/*.html" ] # static_files = [ "./functions/buscar-bibliografia/*.html" ]
[functions.carbone-io-wrapper]
enabled = true
verify_jwt = true
import_map = "./functions/carbone-io-wrapper/deno.json"
# Uncomment to specify a custom file path to the entrypoint.
# Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
entrypoint = "./functions/carbone-io-wrapper/index.ts"
# Specifies static files to be bundled with the function. Supports glob patterns.
# For example, if you want to serve static HTML pages in your function:
# static_files = [ "./functions/carbone-io-wrapper/*.html" ]
+29 -5
View File
@@ -1,4 +1,4 @@
export type Json = export type Json =
| string | string
| number | number
| boolean | boolean
@@ -7,10 +7,30 @@ export type Json =
| Json[] | Json[]
export type Database = { export type Database = {
// Allows to automatically instantiate createClient with right options graphql_public: {
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY) Tables: {
__InternalSupabase: { [_ in never]: never
PostgrestVersion: "14.1" }
Views: {
[_ in never]: never
}
Functions: {
graphql: {
Args: {
extensions?: Json
operationName?: string
query?: string
variables?: Json
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
} }
public: { public: {
Tables: { Tables: {
@@ -1548,6 +1568,9 @@ export type CompositeTypes<
: never : never
export const Constants = { export const Constants = {
graphql_public: {
Enums: {},
},
public: { public: {
Enums: { Enums: {
estado_asignatura: ["borrador", "revisada", "aprobada", "generando"], estado_asignatura: ["borrador", "revisada", "aprobada", "generando"],
@@ -1607,3 +1630,4 @@ export const Constants = {
}, },
}, },
} as const } as const
@@ -0,0 +1,3 @@
# Configuration for private npm package dependencies
# For more information on using private registries with Edge Functions, see:
# https://supabase.com/docs/guides/functions/import-maps#importing-from-private-registries
@@ -0,0 +1,331 @@
const CARBONE_VERSION = "5";
export type CarboneSuccess<T> = {
success: true;
data: T;
hasMore?: boolean;
nextCursor?: string;
};
export type CarboneError = {
success: false;
error: string;
};
export type CarboneResponse<T> = CarboneSuccess<T> | CarboneError;
export type CarboneDownloadResult = {
buffer: Uint8Array;
contentType: string | null;
contentDisposition: string | null;
};
export class CarboneClient {
constructor(
private baseUrl: string,
private apiToken: string,
) {}
private buildHeaders(extra?: Record<string, string>) {
return {
Authorization: `Bearer ${this.apiToken}`,
"carbone-version": CARBONE_VERSION,
...extra,
};
}
private async parseResponse<T>(res: Response): Promise<T> {
const contentType = res.headers.get("content-type") || "";
if (!res.ok) {
let message = `Carbone request failed: ${res.status}`;
try {
const err = await res.json();
message = err?.error || err?.message || message;
} catch {
// ignore json parse errors
}
throw new Error(message);
}
if (contentType.includes("application/json")) {
return res.json() as Promise<T>;
}
return res as unknown as T;
}
private toQuery(params?: Record<string, unknown>) {
const qs = new URLSearchParams();
if (!params) return "";
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === null) continue;
qs.set(key, String(value));
}
const s = qs.toString();
return s ? `?${s}` : "";
}
async listTemplates(params?: {
id?: string;
versionId?: string;
category?: string;
origin?: 0 | 1;
includeVersions?: boolean;
search?: string;
limit?: number;
cursor?: string;
}) {
const res = await fetch(
`${this.baseUrl}/templates${this.toQuery(params)}`,
{
method: "GET",
headers: this.buildHeaders(),
},
);
return this.parseResponse<
CarboneResponse<
Array<{
id: string;
versionId: string;
deployedAt: number;
createdAt: number;
expireAt?: number;
size: number;
type: string;
name?: string;
category?: string;
comment?: string;
tags?: string[];
origin?: number;
}>
>
>(res);
}
async listCategories() {
const res = await fetch(`${this.baseUrl}/templates/categories`, {
method: "GET",
headers: this.buildHeaders(),
});
return this.parseResponse<CarboneResponse<Array<{ name: string }>>>(res);
}
async listTags() {
const res = await fetch(`${this.baseUrl}/templates/tags`, {
method: "GET",
headers: this.buildHeaders(),
});
return this.parseResponse<CarboneResponse<Array<{ name: string }>>>(res);
}
async uploadTemplateJson(body: {
template: string;
versioning?: boolean;
id?: string;
name?: string;
comment?: string;
tags?: string[];
category?: string;
sample?: unknown[];
deployedAt?: number;
expireAt?: number;
}) {
const res = await fetch(`${this.baseUrl}/template`, {
method: "POST",
headers: this.buildHeaders({
"Content-Type": "application/json",
}),
body: JSON.stringify(body),
});
return this.parseResponse<
CarboneResponse<{
id?: string;
versionId?: string;
templateId?: string;
type?: string;
size?: number;
createdAt?: number;
}>
>(res);
}
async uploadTemplateFile(input: {
file: Blob;
filename: string;
versioning?: boolean;
id?: string;
name?: string;
comment?: string;
tags?: string[];
category?: string;
sample?: unknown[];
deployedAt?: number;
expireAt?: number;
}) {
const form = new FormData();
if (input.versioning !== undefined) {
form.append("versioning", String(input.versioning));
}
if (input.id) form.append("id", input.id);
if (input.name) form.append("name", input.name);
if (input.comment) form.append("comment", input.comment);
if (input.category) form.append("category", input.category);
if (input.tags) {
for (const tag of input.tags) form.append("tags[]", tag);
}
if (input.sample) form.append("sample", JSON.stringify(input.sample));
if (input.deployedAt !== undefined) {
form.append("deployedAt", String(input.deployedAt));
}
if (input.expireAt !== undefined) {
form.append("expireAt", String(input.expireAt));
}
// template should be appended last
form.append("template", input.file, input.filename);
const res = await fetch(`${this.baseUrl}/template`, {
method: "POST",
headers: this.buildHeaders(),
body: form,
});
return this.parseResponse<
CarboneResponse<{
id?: string;
versionId?: string;
templateId?: string;
type?: string;
size?: number;
createdAt?: number;
}>
>(res);
}
async patchTemplate(
id: string,
body: {
name?: string;
comment?: string;
tags?: string[];
category?: string;
deployedAt?: number;
expireAt?: number;
},
) {
const res = await fetch(`${this.baseUrl}/template/${id}`, {
method: "PATCH",
headers: this.buildHeaders({
"Content-Type": "application/json",
}),
body: JSON.stringify(body),
});
return this.parseResponse<CarboneResponse<true>>(res);
}
async deleteTemplate(id: string) {
const res = await fetch(`${this.baseUrl}/template/${id}`, {
method: "DELETE",
headers: this.buildHeaders(),
});
return this.parseResponse<CarboneResponse<true>>(res);
}
async downloadTemplate(id: string): Promise<CarboneDownloadResult> {
const res = await fetch(`${this.baseUrl}/template/${id}`, {
method: "GET",
headers: this.buildHeaders(),
});
if (!res.ok) {
const err = await res.json().catch(() => null);
throw new Error(
err?.error || `Failed to download template: ${res.status}`,
);
}
return {
buffer: new Uint8Array(await res.arrayBuffer()),
contentType: res.headers.get("content-type"),
contentDisposition: res.headers.get("content-disposition"),
};
}
async render(
templateIdOrVersionId: string,
body: Record<string, unknown>,
opts?: { download?: boolean },
) {
const query = opts?.download ? "?download=true" : "";
const res = await fetch(
`${this.baseUrl}/render/${templateIdOrVersionId}${query}`,
{
method: "POST",
headers: this.buildHeaders({
"Content-Type": "application/json",
}),
body: JSON.stringify(body),
},
);
if (opts?.download) {
if (!res.ok) {
const err = await res.json().catch(() => null);
throw new Error(err?.error || `Render failed: ${res.status}`);
}
return {
buffer: new Uint8Array(await res.arrayBuffer()),
contentType: res.headers.get("content-type"),
contentDisposition: res.headers.get("content-disposition"),
} satisfies CarboneDownloadResult;
}
return this.parseResponse<CarboneResponse<{ renderId: string }>>(res);
}
async renderFromTemplate(
body: Record<string, unknown>,
opts?: { download?: boolean },
) {
const query = opts?.download ? "?download=true" : "";
const res = await fetch(`${this.baseUrl}/render/template${query}`, {
method: "POST",
headers: this.buildHeaders({
"Content-Type": "application/json",
}),
body: JSON.stringify(body),
});
if (opts?.download) {
if (!res.ok) {
const err = await res.json().catch(() => null);
throw new Error(err?.error || `Render failed: ${res.status}`);
}
return {
buffer: new Uint8Array(await res.arrayBuffer()),
contentType: res.headers.get("content-type"),
contentDisposition: res.headers.get("content-disposition"),
} satisfies CarboneDownloadResult;
}
return this.parseResponse<CarboneResponse<{ renderId: string }>>(res);
}
async downloadRender(renderId: string): Promise<CarboneDownloadResult> {
const res = await fetch(`${this.baseUrl}/render/${renderId}`, {
method: "GET",
});
if (!res.ok) {
const err = await res.json().catch(() => null);
throw new Error(err?.error || `Failed to download render: ${res.status}`);
}
return {
buffer: new Uint8Array(await res.arrayBuffer()),
contentType: res.headers.get("content-type"),
contentDisposition: res.headers.get("content-disposition"),
};
}
}
@@ -0,0 +1,8 @@
{
"imports": {
"zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
"@supabase/supabase-js": "npm:@supabase/supabase-js@2",
"@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts",
"@supabase/functions-js": "jsr:@supabase/functions-js@^2"
}
}
@@ -0,0 +1,252 @@
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 } from "../_shared/utils.ts";
import { CarboneClient } from "./carbone.ts";
const DownloadReportBodySchema = z.record(z.unknown()).optional().default({});
const DownloadReportPlanSchema = z
.object({
action: z.literal("downloadReport"),
plan_estudio_id: z.string().min(1),
body: DownloadReportBodySchema,
})
.strict();
const DownloadReportAsignaturaSchema = z
.object({
action: z.literal("downloadReport"),
asignatura_id: z.string().min(1),
body: DownloadReportBodySchema,
})
.strict();
export const DownloadReportSchema = z.union([
DownloadReportPlanSchema,
DownloadReportAsignaturaSchema,
]);
export type DownloadReportInput = z.infer<typeof DownloadReportSchema>;
type SupabaseClient = ReturnType<typeof createClient<Database>>;
type CarboneDownload = {
buffer: Uint8Array;
contentType: string | null;
contentDisposition: string | null;
};
async function loadPlanDatos(
supabase: SupabaseClient,
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: SupabaseClient,
estructuraId: string,
): Promise<string> {
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;
}
async function loadTemplateIdForAsignatura(
supabase: SupabaseClient,
asignaturaId: string,
): Promise<string> {
const { data, error } = await supabase
.from("plantilla_asignatura")
.select("template_id")
.eq("asignatura_id", asignaturaId)
.maybeSingle();
if (error) {
throw new HttpError(
500,
"Error consultando la plantilla de la asignatura.",
"DB_ERROR",
{
message: error.message,
details: error.details,
hint: error.hint,
},
);
}
if (!data) {
throw new HttpError(404, "Asignatura no encontrada.", "NOT_FOUND", {
asignatura_id: asignaturaId,
});
}
if (!data.template_id) {
throw new HttpError(
409,
"La asignatura no tiene template_id configurado.",
"MISSING_TEMPLATE_ID",
{ asignatura_id: asignaturaId },
);
}
return data.template_id;
}
function ensureCarboneDownload(result: unknown): CarboneDownload {
if (!(result && typeof result === "object" && "buffer" in result)) {
throw new HttpError(
502,
"Respuesta inválida de Carbone.",
"UPSTREAM_INVALID_RESPONSE",
);
}
const download = result as CarboneDownload;
if (!(download.buffer instanceof Uint8Array)) {
throw new HttpError(
502,
"Respuesta inválida de Carbone.",
"UPSTREAM_INVALID_RESPONSE",
);
}
return download;
}
function downloadToResponse(download: CarboneDownload): Response {
const headers = new Headers({
...corsHeaders,
"Content-Type": download.contentType === "application/pdf"
? "application/pdf"
: "application/octet-stream",
Connection: "keep-alive",
});
if (download.contentDisposition) {
headers.set("Content-Disposition", download.contentDisposition);
}
const body = new Uint8Array(
download.buffer.buffer as ArrayBuffer,
download.buffer.byteOffset,
download.buffer.byteLength,
);
return new Response(body, { status: 200, headers });
}
export async function handleDownloadReportAction(args: {
bodyUnknown: unknown;
supabase: SupabaseClient;
carboneBaseUrl: string;
carboneApiToken: string;
}): Promise<Response> {
const input: DownloadReportInput = DownloadReportSchema.parse(
args.bodyUnknown,
);
const carbone = new CarboneClient(args.carboneBaseUrl, args.carboneApiToken);
if ("plan_estudio_id" in input) {
const { datos, estructura_id } = await loadPlanDatos(
args.supabase,
input.plan_estudio_id,
);
const templateId = await loadTemplateIdForEstructura(
args.supabase,
estructura_id,
);
const { data: _ignoredData, ...extraBody } = input.body;
const result = await carbone.render(
templateId,
{ data: datos, ...extraBody },
{ download: true },
);
return downloadToResponse(ensureCarboneDownload(result));
}
const templateId = await loadTemplateIdForAsignatura(
args.supabase,
input.asignatura_id,
);
const hasData = Object.prototype.hasOwnProperty.call(input.body, "data");
if (!hasData) {
throw new HttpError(
400,
"Para asignatura_id se requiere body.data.",
"MISSING_DATA",
{ asignatura_id: input.asignatura_id },
);
}
// console.log("body:", input.body);
const { data, ...extraBody } = input.body as Record<string, unknown>;
const result = await carbone.render(
templateId,
{ data, ...extraBody },
{ download: true },
);
return downloadToResponse(ensureCarboneDownload(result));
}
@@ -0,0 +1,179 @@
// 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 } from "../_shared/database.types.ts";
import { HttpError, sendError } from "../_shared/utils.ts";
import { handleDownloadReportAction } from "./download-report.ts";
const ActionSchema = z.object({
action: z.string().min(1),
});
function getAuthHeader(req: Request): string | null {
return req.headers.get("Authorization") ?? req.headers.get("authorization");
}
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_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<Database>(SUPABASE_URL, SERVICE_ROLE_KEY, {
auth: { persistSession: false },
});
switch (action) {
case "downloadReport": {
const response = await handleDownloadReportAction({
bodyUnknown,
supabase,
carboneBaseUrl: CARBONE_BASE_URL,
carboneApiToken: CARBONE_API_TOKEN,
});
console.log(
`[${
new Date().toISOString()
}][${functionName}]: Request processed successfully`,
);
return response;
}
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
# 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>","body":{}}'
# Or for asignaturas (must include body.data):
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","asignatura_id":"<uuid>","body":{"data":{}}}'
*/