Se creó la edge function que sirve de wrapper a carbone y ya permite descargar el pdf del plan de estudios
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
This commit is contained in:
+13
-21
@@ -489,7 +489,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 6,
|
||||
"id": "caf8bc20",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
@@ -500,13 +500,13 @@
|
||||
" success: \u001b[33mtrue\u001b[39m,\n",
|
||||
" data: {\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",
|
||||
" }\n",
|
||||
"}"
|
||||
]
|
||||
},
|
||||
"execution_count": 19,
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
@@ -514,13 +514,13 @@
|
||||
"source": [
|
||||
"await carbone.patchTemplate(templateID, {\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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 7,
|
||||
"id": "929883b9",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@@ -570,7 +570,7 @@
|
||||
" { download: true },\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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "4528a917",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"await carbone.list"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
@@ -661,7 +651,7 @@
|
||||
" type: \"docx\",\n",
|
||||
" name: \"Anexo 1\",\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",
|
||||
" origin: 1\n",
|
||||
" }\n",
|
||||
@@ -674,10 +664,10 @@
|
||||
"source": [
|
||||
"const versions = await carbone.listTemplates({\n",
|
||||
" id: \"1373945625988937236\",\n",
|
||||
" includeVersions: true\n",
|
||||
" includeVersions: true,\n",
|
||||
"});\n",
|
||||
"\n",
|
||||
"console.log(versions);"
|
||||
"console.log(versions);\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -746,8 +736,10 @@
|
||||
"console.log(result);\n",
|
||||
"\n",
|
||||
"// const file = await carbone.downloadRender(render.data.renderId);\n",
|
||||
"const file = await carbone.downloadRender(\"X1Bx6Stre95Sm54jHxIwpQYW5leG8xLnBkZg.pdf\");\n",
|
||||
"await Deno.writeFile(\"dist/anexo1.pdf\", file.buffer);"
|
||||
"const file = await carbone.downloadRender(\n",
|
||||
" \"X1Bx6Stre95Sm54jHxIwpQYW5leG8xLnBkZg.pdf\",\n",
|
||||
");\n",
|
||||
"await Deno.writeFile(\"dist/anexo1.pdf\", file.buffer);\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -76,3 +76,14 @@ entrypoint = "./functions/buscar-bibliografia/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/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" ]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type Json =
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
@@ -7,10 +7,30 @@ export type Json =
|
||||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instantiate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: "14.1"
|
||||
graphql_public: {
|
||||
Tables: {
|
||||
[_ in never]: never
|
||||
}
|
||||
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: {
|
||||
Tables: {
|
||||
@@ -1548,6 +1568,9 @@ export type CompositeTypes<
|
||||
: never
|
||||
|
||||
export const Constants = {
|
||||
graphql_public: {
|
||||
Enums: {},
|
||||
},
|
||||
public: {
|
||||
Enums: {
|
||||
estado_asignatura: ["borrador", "revisada", "aprobada", "generando"],
|
||||
@@ -1607,3 +1630,4 @@ export const Constants = {
|
||||
},
|
||||
},
|
||||
} 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,230 @@
|
||||
// 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>"}'
|
||||
|
||||
*/
|
||||
Reference in New Issue
Block a user