Creada edge function wrapper para carbone.io del lci con un método (descargar reporte) #80
+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