5c9fde1a9e
Deploy Function / deploy (push) Successful in 24s
close #6: - Ya se pueden generar asignaturas con IA. Específicamente los datos generales. - Se actualizaron los tipos de supabase - Se arreglaron los imports - TODO: generar contenido temático de la asignatura
650 lines
50 KiB
Plaintext
650 lines
50 KiB
Plaintext
diff --git a/deno.json b/deno.json
|
|
index 60f816d..2128fe4 100644
|
|
--- a/deno.json
|
|
+++ b/deno.json
|
|
@@ -7,6 +7,10 @@
|
|
"imports": {
|
|
"@openai/openai": "jsr:@openai/openai@^6.16.0",
|
|
"@zod/zod": "jsr:@zod/zod@^4.3.5",
|
|
- "@david/dax": "jsr:@david/dax@^0.44.2"
|
|
+ "@david/dax": "jsr:@david/dax@^0.44.2",
|
|
+ "zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
|
+ "openai": "npm:openai@6.16.0",
|
|
+ "@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
|
+ "@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
|
}
|
|
}
|
|
diff --git a/diff.txt b/diff.txt
|
|
index b2704fc..e69de29 100644
|
|
Binary files a/diff.txt and b/diff.txt differ
|
|
diff --git a/supabase/functions/_shared/database.types.ts b/supabase/functions/_shared/database.types.ts
|
|
index fa3ef8e..bf4b960 100644
|
|
--- a/supabase/functions/_shared/database.types.ts
|
|
+++ b/supabase/functions/_shared/database.types.ts
|
|
@@ -4,7 +4,7 @@
|
|
| boolean
|
|
| null
|
|
| { [key: string]: Json | undefined }
|
|
- | Json[];
|
|
+ | Array<Json>;
|
|
|
|
export type Database = {
|
|
// Allows to automatically instantiate createClient with right options
|
|
@@ -90,6 +90,7 @@ export type Database = {
|
|
Row: {
|
|
actualizado_en: string;
|
|
actualizado_por: string | null;
|
|
+ asignatura_hash: string | null;
|
|
codigo: string | null;
|
|
contenido_tematico: Json;
|
|
creado_en: string;
|
|
@@ -97,8 +98,8 @@ export type Database = {
|
|
creditos: number;
|
|
datos: Json;
|
|
estructura_id: string | null;
|
|
- facultad_propietaria_id: string | null;
|
|
- horas_semana: number | null;
|
|
+ horas_academicas: number | null;
|
|
+ horas_independientes: number | null;
|
|
id: string;
|
|
linea_plan_id: string | null;
|
|
meta_origen: Json;
|
|
@@ -114,6 +115,7 @@ export type Database = {
|
|
Insert: {
|
|
actualizado_en?: string;
|
|
actualizado_por?: string | null;
|
|
+ asignatura_hash?: string | null;
|
|
codigo?: string | null;
|
|
contenido_tematico?: Json;
|
|
creado_en?: string;
|
|
@@ -121,8 +123,8 @@ export type Database = {
|
|
creditos: number;
|
|
datos?: Json;
|
|
estructura_id?: string | null;
|
|
- facultad_propietaria_id?: string | null;
|
|
- horas_semana?: number | null;
|
|
+ horas_academicas?: number | null;
|
|
+ horas_independientes?: number | null;
|
|
id?: string;
|
|
linea_plan_id?: string | null;
|
|
meta_origen?: Json;
|
|
@@ -138,6 +140,7 @@ export type Database = {
|
|
Update: {
|
|
actualizado_en?: string;
|
|
actualizado_por?: string | null;
|
|
+ asignatura_hash?: string | null;
|
|
codigo?: string | null;
|
|
contenido_tematico?: Json;
|
|
creado_en?: string;
|
|
@@ -145,8 +148,8 @@ export type Database = {
|
|
creditos?: number;
|
|
datos?: Json;
|
|
estructura_id?: string | null;
|
|
- facultad_propietaria_id?: string | null;
|
|
- horas_semana?: number | null;
|
|
+ horas_academicas?: number | null;
|
|
+ horas_independientes?: number | null;
|
|
id?: string;
|
|
linea_plan_id?: string | null;
|
|
meta_origen?: Json;
|
|
@@ -181,14 +184,6 @@ export type Database = {
|
|
referencedRelation: "estructuras_asignatura";
|
|
referencedColumns: ["id"];
|
|
},
|
|
- {
|
|
- foreignKeyName:
|
|
- "asignaturas_facultad_propietaria_id_fkey";
|
|
- columns: ["facultad_propietaria_id"];
|
|
- isOneToOne: false;
|
|
- referencedRelation: "facultades";
|
|
- referencedColumns: ["id"];
|
|
- },
|
|
{
|
|
foreignKeyName: "asignaturas_linea_plan_fk_compuesta";
|
|
columns: ["linea_plan_id", "plan_estudio_id"];
|
|
@@ -332,8 +327,8 @@ export type Database = {
|
|
cambiado_por: string | null;
|
|
campo: string | null;
|
|
id: string;
|
|
- interaccion_ia_id: string | null;
|
|
plan_estudio_id: string;
|
|
+ response_id: string | null;
|
|
tipo: Database["public"]["Enums"]["tipo_cambio"];
|
|
valor_anterior: Json | null;
|
|
valor_nuevo: Json | null;
|
|
@@ -343,8 +338,8 @@ export type Database = {
|
|
cambiado_por?: string | null;
|
|
campo?: string | null;
|
|
id?: string;
|
|
- interaccion_ia_id?: string | null;
|
|
plan_estudio_id: string;
|
|
+ response_id?: string | null;
|
|
tipo: Database["public"]["Enums"]["tipo_cambio"];
|
|
valor_anterior?: Json | null;
|
|
valor_nuevo?: Json | null;
|
|
@@ -354,8 +349,8 @@ export type Database = {
|
|
cambiado_por?: string | null;
|
|
campo?: string | null;
|
|
id?: string;
|
|
- interaccion_ia_id?: string | null;
|
|
plan_estudio_id?: string;
|
|
+ response_id?: string | null;
|
|
tipo?: Database["public"]["Enums"]["tipo_cambio"];
|
|
valor_anterior?: Json | null;
|
|
valor_nuevo?: Json | null;
|
|
@@ -368,20 +363,6 @@ export type Database = {
|
|
referencedRelation: "usuarios_app";
|
|
referencedColumns: ["id"];
|
|
},
|
|
- {
|
|
- foreignKeyName: "cambios_plan_plan_estudio_id_fkey";
|
|
- columns: ["plan_estudio_id"];
|
|
- isOneToOne: false;
|
|
- referencedRelation: "planes_estudio";
|
|
- referencedColumns: ["id"];
|
|
- },
|
|
- {
|
|
- foreignKeyName: "cambios_plan_plan_estudio_id_fkey";
|
|
- columns: ["plan_estudio_id"];
|
|
- isOneToOne: false;
|
|
- referencedRelation: "plantilla_plan";
|
|
- referencedColumns: ["plan_estudio_id"];
|
|
- },
|
|
];
|
|
};
|
|
carreras: {
|
|
@@ -708,6 +689,7 @@ export type Database = {
|
|
actualizado_en: string;
|
|
actualizado_por: string | null;
|
|
carrera_id: string;
|
|
+ conversation_id: string | null;
|
|
creado_en: string;
|
|
creado_por: string | null;
|
|
datos: Json;
|
|
@@ -719,6 +701,7 @@ export type Database = {
|
|
nombre: string;
|
|
nombre_search: string | null;
|
|
numero_ciclos: number;
|
|
+ plan_hash: string | null;
|
|
tipo_ciclo: Database["public"]["Enums"]["tipo_ciclo"];
|
|
tipo_origen:
|
|
| Database["public"]["Enums"]["tipo_origen"]
|
|
@@ -729,6 +712,7 @@ export type Database = {
|
|
actualizado_en?: string;
|
|
actualizado_por?: string | null;
|
|
carrera_id: string;
|
|
+ conversation_id?: string | null;
|
|
creado_en?: string;
|
|
creado_por?: string | null;
|
|
datos?: Json;
|
|
@@ -740,6 +724,7 @@ export type Database = {
|
|
nombre: string;
|
|
nombre_search?: string | null;
|
|
numero_ciclos: number;
|
|
+ plan_hash?: string | null;
|
|
tipo_ciclo: Database["public"]["Enums"]["tipo_ciclo"];
|
|
tipo_origen?:
|
|
| Database["public"]["Enums"]["tipo_origen"]
|
|
@@ -750,6 +735,7 @@ export type Database = {
|
|
actualizado_en?: string;
|
|
actualizado_por?: string | null;
|
|
carrera_id?: string;
|
|
+ conversation_id?: string | null;
|
|
creado_en?: string;
|
|
creado_por?: string | null;
|
|
datos?: Json;
|
|
@@ -761,6 +747,7 @@ export type Database = {
|
|
nombre?: string;
|
|
nombre_search?: string | null;
|
|
numero_ciclos?: number;
|
|
+ plan_hash?: string | null;
|
|
tipo_ciclo?: Database["public"]["Enums"]["tipo_ciclo"];
|
|
tipo_origen?:
|
|
| Database["public"]["Enums"]["tipo_origen"]
|
|
@@ -1140,7 +1127,7 @@ export type Database = {
|
|
fuente_cambio: "HUMANO" | "IA";
|
|
nivel_plan_estudio:
|
|
| "Licenciatura"
|
|
- | "MaestrÔö£┬ía"
|
|
+ | "Maestría"
|
|
| "Doctorado"
|
|
| "Especialidad"
|
|
| "Diplomado"
|
|
@@ -1163,7 +1150,8 @@ export type Database = {
|
|
| "ACTUALIZACION_MAPA"
|
|
| "TRANSICION_ESTADO"
|
|
| "OTRO"
|
|
- | "CREACION";
|
|
+ | "CREACION"
|
|
+ | "ACTUALIZACION";
|
|
tipo_ciclo: "Semestre" | "Cuatrimestre" | "Trimestre" | "Otro";
|
|
tipo_estructura_plan: "CURRICULAR" | "NO_CURRICULAR";
|
|
tipo_fuente_bibliografia: "MANUAL" | "BIBLIOTECA";
|
|
@@ -1343,7 +1331,7 @@ export const Constants = {
|
|
fuente_cambio: ["HUMANO", "IA"],
|
|
nivel_plan_estudio: [
|
|
"Licenciatura",
|
|
- "MaestrÔö£┬ía",
|
|
+ "Maestría",
|
|
"Doctorado",
|
|
"Especialidad",
|
|
"Diplomado",
|
|
@@ -1370,6 +1358,7 @@ export const Constants = {
|
|
"TRANSICION_ESTADO",
|
|
"OTRO",
|
|
"CREACION",
|
|
+ "ACTUALIZACION",
|
|
],
|
|
tipo_ciclo: ["Semestre", "Cuatrimestre", "Trimestre", "Otro"],
|
|
tipo_estructura_plan: ["CURRICULAR", "NO_CURRICULAR"],
|
|
diff --git a/supabase/functions/ai-generate-plan/index.ts b/supabase/functions/ai-generate-plan/index.ts
|
|
index 0f3185b..1db2eef 100644
|
|
--- a/supabase/functions/ai-generate-plan/index.ts
|
|
+++ b/supabase/functions/ai-generate-plan/index.ts
|
|
@@ -3,10 +3,10 @@
|
|
// This enables autocomplete, go to definition, etc.
|
|
|
|
// Setup type definitions for built-in Supabase Runtime APIs
|
|
-import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
+import "@supabase/functions-js/edge-runtime.d.ts";
|
|
import { corsHeaders } from "../_shared/cors.ts";
|
|
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
|
|
-import { createClient } from "npm:@supabase/supabase-js@2";
|
|
+import { createClient } from "@supabase/supabase-js";
|
|
import type { Database, Json } from "../_shared/database.types.ts";
|
|
import type { AIGeneratePlanInput } from "./types.ts";
|
|
import { z } from "zod";
|
|
diff --git a/supabase/functions/ai-generate-subject/deno.json b/supabase/functions/ai-generate-subject/deno.json
|
|
index f6ca845..8b204ca 100644
|
|
--- a/supabase/functions/ai-generate-subject/deno.json
|
|
+++ b/supabase/functions/ai-generate-subject/deno.json
|
|
@@ -1,3 +1,8 @@
|
|
{
|
|
- "imports": {}
|
|
+ "imports": {
|
|
+ "zod": "https://deno.land/x/zod@v3.22.4/mod.ts",
|
|
+ "openai": "npm:openai@6.16.0",
|
|
+ "@supabase/supabase-js": "npm:@supabase/supabase-js@2",
|
|
+ "@supabase/functions-js/edge-runtime.d.ts": "jsr:@supabase/functions-js@^2/edge-runtime.d.ts"
|
|
+ }
|
|
}
|
|
diff --git a/supabase/functions/ai-generate-subject/index.ts b/supabase/functions/ai-generate-subject/index.ts
|
|
index e974ea3..9045854 100644
|
|
--- a/supabase/functions/ai-generate-subject/index.ts
|
|
+++ b/supabase/functions/ai-generate-subject/index.ts
|
|
@@ -1,12 +1,15 @@
|
|
-import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
+import "@supabase/functions-js/edge-runtime.d.ts";
|
|
import { corsHeaders } from "../_shared/cors.ts";
|
|
import { HttpError, sendError, sendSuccess } from "../_shared/utils.ts";
|
|
-import { createClient } from "npm:@supabase/supabase-js@2";
|
|
+import { createClient } from "@supabase/supabase-js";
|
|
import type { Database, Json } from "../_shared/database.types.ts";
|
|
+import { z } from "zod";
|
|
+import {
|
|
+ OpenAIService,
|
|
+ type StructuredResponseOptions,
|
|
+} from "../_shared/openai-service.ts";
|
|
|
|
-console.log("Hello from Functions!");
|
|
-
|
|
-Deno.serve(async (req) => {
|
|
+Deno.serve(async (req: Request): Promise<Response> => {
|
|
const url = new URL(req.url);
|
|
const functionName = url.pathname.split("/").pop();
|
|
console.log(
|
|
@@ -18,6 +21,52 @@ Deno.serve(async (req) => {
|
|
}
|
|
|
|
try {
|
|
+ const method = req.method;
|
|
+ if (method !== "POST") {
|
|
+ console.error(
|
|
+ `[${
|
|
+ new Date().toISOString()
|
|
+ }][${functionName}]: Invalid method: ${method}`,
|
|
+ );
|
|
+ throw new HttpError(
|
|
+ 405,
|
|
+ "M├®todo no permitido.",
|
|
+ "METHOD_NOT_ALLOWED",
|
|
+ { method },
|
|
+ );
|
|
+ }
|
|
+
|
|
+ const authHeaderRaw = req.headers.get("Authorization") ??
|
|
+ req.headers.get("authorization");
|
|
+ if (!authHeaderRaw) {
|
|
+ console.error(
|
|
+ `[${
|
|
+ new Date().toISOString()
|
|
+ }][${functionName}]: Missing Authorization header`,
|
|
+ );
|
|
+ throw new HttpError(
|
|
+ 401,
|
|
+ "No autorizado.",
|
|
+ "UNAUTHORIZED",
|
|
+ { reason: "missing_authorization_header" },
|
|
+ );
|
|
+ }
|
|
+
|
|
+ const contentType = (req.headers.get("content-type") || "").toLowerCase();
|
|
+ if (!contentType.startsWith("multipart/form-data")) {
|
|
+ console.error(
|
|
+ `[${
|
|
+ new Date().toISOString()
|
|
+ }][${functionName}]: Unsupported content type: ${contentType}`,
|
|
+ );
|
|
+ throw new HttpError(
|
|
+ 415,
|
|
+ "Content-Type no soportado.",
|
|
+ "UNSUPPORTED_MEDIA_TYPE",
|
|
+ { contentType },
|
|
+ );
|
|
+ }
|
|
+
|
|
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
|
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
|
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
@@ -59,20 +108,207 @@ Deno.serve(async (req) => {
|
|
SERVICE_ROLE_KEY,
|
|
);
|
|
|
|
- // INICIO DE CODIGO PARA DEBBUGGING
|
|
+ const formData = await req.formData();
|
|
+ const validation = parseAndValidate(formData);
|
|
+ if (!validation.success) {
|
|
+ console.error(
|
|
+ `[${new Date().toISOString()}][${functionName}]: Validation errors:`,
|
|
+ validation.errors,
|
|
+ );
|
|
+ const message = validation.errors
|
|
+ .map((e, i) => `${i + 1}. ${e}`)
|
|
+ .join("\n");
|
|
+ throw new HttpError(
|
|
+ 422,
|
|
+ message,
|
|
+ "VALIDATION_ERROR",
|
|
+ { errors: validation.errors },
|
|
+ );
|
|
+ }
|
|
+
|
|
+ const payload = validation.data;
|
|
+
|
|
+ const { data: estructura, error: estructuraError } = await supabaseService
|
|
+ .from("estructuras_asignatura")
|
|
+ .select("id,nombre,definicion,version")
|
|
+ .eq("id", payload.datosBasicos.estructuraId)
|
|
+ .single();
|
|
+ if (estructuraError) {
|
|
+ const maybeCode = (estructuraError as { code?: string }).code;
|
|
+ if (maybeCode === "PGRST116") {
|
|
+ throw new HttpError(
|
|
+ 404,
|
|
+ "No se encontr├│ la estructura de la asignatura.",
|
|
+ "NOT_FOUND",
|
|
+ {
|
|
+ table: "estructuras_asignatura",
|
|
+ id: payload.datosBasicos.estructuraId,
|
|
+ },
|
|
+ );
|
|
+ }
|
|
+ throw new HttpError(
|
|
+ 500,
|
|
+ "No se pudo obtener la estructura de la asignatura.",
|
|
+ "SUPABASE_QUERY_FAILED",
|
|
+ estructuraError,
|
|
+ );
|
|
+ }
|
|
+
|
|
+ const systemPrompt =
|
|
+ "Eres un asistente experto en diseño curricular. Responde únicamente con JSON válido que cumpla estrictamente el JSON Schema proporcionado.";
|
|
+
|
|
+ const userPrompt =
|
|
+ `Genera un borrador completo completo de una ASIGNATURA con base en lo siguiente:\n` +
|
|
+ `- Nombre de la asignatura: ${payload.datosBasicos.nombre}\n` +
|
|
+ `- C├│digo (clave de la asignatura): ${
|
|
+ payload.datosBasicos.codigo ?? "(no especificado)"
|
|
+ }\n` +
|
|
+ `- Tipo: ${payload.datosBasicos.tipo ?? "(no especificado)"}\n` +
|
|
+ `- N├║mero de cr├®ditos: ${payload.datosBasicos.creditos}\n` +
|
|
+ `- Horas acad├®micas: ${
|
|
+ payload.datosBasicos.horasAcademicas ?? "(no especificado)"
|
|
+ }\n` +
|
|
+ `- Horas independientes: ${
|
|
+ payload.datosBasicos.horasIndependientes ?? "(no especificado)"
|
|
+ }\n` +
|
|
+ `- Descripci├│n del enfoque acad├®mico (sobre el contenido de la respuesta generada): ${
|
|
+ payload.iaConfig?.descripcionEnfoqueAcademico ?? "(ninguna)"
|
|
+ }\n` +
|
|
+ `- Instrucciones adicionales (sobre el formato de la respuesta generada): ${
|
|
+ payload.iaConfig?.instruccionesAdicionalesIA ?? "(ninguna)"
|
|
+ }`;
|
|
+
|
|
+ const schemaDef: Record<string, unknown> =
|
|
+ typeof estructura?.definicion === "object" &&
|
|
+ estructura?.definicion !== null
|
|
+ ? (estructura.definicion as Record<string, unknown>)
|
|
+ : {};
|
|
+
|
|
+ const aiStructuredPayload: StructuredResponseOptions = {
|
|
+ model: "gpt-5-nano",
|
|
+ input: [
|
|
+ { role: "system", content: systemPrompt },
|
|
+ { role: "user", content: userPrompt },
|
|
+ ],
|
|
+ text: {
|
|
+ format: {
|
|
+ type: "json_schema",
|
|
+ name: "asignatura_contenido_tematico",
|
|
+ schema: schemaDef,
|
|
+ strict: true,
|
|
+ },
|
|
+ },
|
|
+ };
|
|
+
|
|
+ const svc = OpenAIService.fromEnv();
|
|
+ if (!(svc instanceof OpenAIService)) {
|
|
+ throw new HttpError(
|
|
+ 500,
|
|
+ "Configuraci├│n del servidor incompleta.",
|
|
+ "OPENAI_MISCONFIGURED",
|
|
+ svc,
|
|
+ );
|
|
+ }
|
|
+
|
|
+ const aiResult = await svc.createStructuredResponse(
|
|
+ aiStructuredPayload,
|
|
+ payload.archivosAdjuntos,
|
|
+ );
|
|
+ if (!aiResult.ok) {
|
|
+ const status = aiResult.code === "MissingEnv" ? 500 : 502;
|
|
+ throw new HttpError(
|
|
+ status,
|
|
+ "No se pudo generar la asignatura con IA.",
|
|
+ "OPENAI_REQUEST_FAILED",
|
|
+ aiResult,
|
|
+ );
|
|
+ }
|
|
+
|
|
+ let aiOutput = aiResult.output ?? null;
|
|
+ if (aiOutput == null && aiResult.outputText) {
|
|
+ try {
|
|
+ aiOutput = JSON.parse(aiResult.outputText);
|
|
+ } catch {
|
|
+ throw new HttpError(
|
|
+ 502,
|
|
+ "La respuesta de la IA no es JSON válido.",
|
|
+ "OPENAI_INVALID_JSON",
|
|
+ { outputText: aiResult.outputText },
|
|
+ );
|
|
+ }
|
|
+ }
|
|
+ if (!aiOutput) {
|
|
+ throw new HttpError(
|
|
+ 502,
|
|
+ "La respuesta de la IA no contiene salida estructurada.",
|
|
+ "OPENAI_MISSING_STRUCTURED_OUTPUT",
|
|
+ { outputText: aiResult.outputText ?? null },
|
|
+ );
|
|
+ }
|
|
+
|
|
+ const aiOutputJson: Json = aiOutput as unknown as Json;
|
|
+
|
|
+ const asignaturaInsert:
|
|
+ Database["public"]["Tables"]["asignaturas"]["Insert"] = {
|
|
+ plan_estudio_id: payload.plan_estudio_id,
|
|
+ estructura_id: payload.datosBasicos.estructuraId,
|
|
+ nombre: payload.datosBasicos.nombre,
|
|
+ codigo: payload.datosBasicos.codigo ?? null,
|
|
+ creditos: payload.datosBasicos.creditos,
|
|
+ horas_academicas: payload.datosBasicos.horasAcademicas ?? null,
|
|
+ horas_independientes: payload.datosBasicos.horasIndependientes ?? null,
|
|
+ tipo: (payload.datosBasicos.tipo ?? undefined) as Database["public"][
|
|
+ "Tables"
|
|
+ ]["asignaturas"]["Insert"]["tipo"],
|
|
+ tipo_origen: "IA",
|
|
+ datos: aiOutputJson,
|
|
+ meta_origen: {
|
|
+ generado_por: "ai_generate_subject",
|
|
+ ai: {
|
|
+ responseId: aiResult.responseId ?? null,
|
|
+ conversationId: aiResult.conversationId ?? null,
|
|
+ model: aiResult.model,
|
|
+ usage: aiResult.usage ?? null,
|
|
+ },
|
|
+ referencias: {
|
|
+ archivosReferenciaIds: payload.iaConfig?.archivosReferencia ?? null,
|
|
+ repositoriosReferencia: payload.iaConfig?.repositoriosReferencia ??
|
|
+ null,
|
|
+ },
|
|
+ iaConfig: {
|
|
+ descripcionEnfoqueAcademico:
|
|
+ payload.iaConfig?.descripcionEnfoqueAcademico ?? null,
|
|
+ instruccionesAdicionalesIA:
|
|
+ payload.iaConfig?.instruccionesAdicionalesIA ?? null,
|
|
+ },
|
|
+ } as unknown as Json,
|
|
+ };
|
|
+
|
|
+ const { data: asignatura, error: asignaturaError } = await supabaseService
|
|
+ .from("asignaturas")
|
|
+ .insert(asignaturaInsert)
|
|
+ .select(
|
|
+ "id,nombre,codigo,tipo,creditos,horas_academicas,horas_independientes,estructura_id,plan_estudio_id,contenido_tematico,meta_origen,creado_en,actualizado_en",
|
|
+ )
|
|
+ .single();
|
|
+
|
|
+ if (asignaturaError) {
|
|
+ const maybeCode = (asignaturaError as { code?: string }).code;
|
|
+ const status = maybeCode ? 409 : 500;
|
|
+ throw new HttpError(
|
|
+ status,
|
|
+ "No se pudo guardar la asignatura.",
|
|
+ "SUPABASE_INSERT_FAILED",
|
|
+ { ...asignaturaError, code: maybeCode },
|
|
+ );
|
|
+ }
|
|
+
|
|
console.log(
|
|
`[${
|
|
new Date().toISOString()
|
|
}][${functionName}]: Request processed successfully`,
|
|
);
|
|
- const { data: asignatura_debug, error: asignatura_error } =
|
|
- await supabaseService
|
|
- .from("asignaturas")
|
|
- .select("*")
|
|
- .eq("id", "9d4dda6a-488f-428a-8a07-38081592a641")
|
|
- .single();
|
|
- return sendSuccess(asignatura_debug);
|
|
- // FIN DE CODIGO PARA DEBBUGGING
|
|
+ return sendSuccess(asignatura);
|
|
} catch (error) {
|
|
if (error instanceof HttpError) {
|
|
console.error(
|
|
@@ -105,3 +341,83 @@ Deno.serve(async (req) => {
|
|
);
|
|
}
|
|
});
|
|
+
|
|
+const jsonFromString = <T extends z.ZodTypeAny>(schema: T) =>
|
|
+ z.string().transform((str, ctx) => {
|
|
+ try {
|
|
+ return JSON.parse(str);
|
|
+ } catch {
|
|
+ ctx.addIssue({
|
|
+ code: z.ZodIssueCode.custom,
|
|
+ message: "El formato no es un JSON válido",
|
|
+ });
|
|
+ return z.NEVER;
|
|
+ }
|
|
+ }).pipe(schema);
|
|
+
|
|
+const jsonFromStringOptional = <T extends z.ZodTypeAny>(schema: T) =>
|
|
+ z.string().optional().default("{}").transform((str, ctx) => {
|
|
+ try {
|
|
+ return JSON.parse(str);
|
|
+ } catch {
|
|
+ ctx.addIssue({
|
|
+ code: z.ZodIssueCode.custom,
|
|
+ message: "El formato no es un JSON válido",
|
|
+ });
|
|
+ return z.NEVER;
|
|
+ }
|
|
+ }).pipe(schema);
|
|
+
|
|
+const DatosBasicosSchema = z.object({
|
|
+ nombre: z.string().min(1, "El nombre es requerido"),
|
|
+ codigo: z.string().min(1).optional().nullable(),
|
|
+ tipo: z.union([z.string().min(1), z.null()]).optional().transform((v) =>
|
|
+ v ?? null
|
|
+ ),
|
|
+ creditos: z.number({ invalid_type_error: "Cr├®ditos debe ser num├®rico" })
|
|
+ .positive("Cr├®ditos debe ser >= 0"),
|
|
+ horasAcademicas: z.number().int().nonnegative().optional().nullable(),
|
|
+ horasIndependientes: z.number().int().nonnegative().optional().nullable(),
|
|
+ estructuraId: z.string().uuid("estructuraId debe ser un UUID"),
|
|
+});
|
|
+
|
|
+const IAConfigSchema = z.object({
|
|
+ descripcionEnfoqueAcademico: z.string().optional(),
|
|
+ instruccionesAdicionalesIA: z.string().optional(),
|
|
+ archivosReferencia: z.array(z.string().uuid()).optional(),
|
|
+ repositoriosReferencia: z.array(z.string().uuid()).optional(),
|
|
+}).partial();
|
|
+
|
|
+const SolicitudSchema = z.object({
|
|
+ plan_estudio_id: z.string().uuid("plan_estudio_id debe ser un UUID"),
|
|
+ datosBasicos: jsonFromString(DatosBasicosSchema),
|
|
+ iaConfig: jsonFromStringOptional(IAConfigSchema),
|
|
+ archivosAdjuntos: z.array(z.instanceof(File)).optional().default([]),
|
|
+});
|
|
+
|
|
+type EdgeAIGenerateSubjectInput = z.infer<typeof SolicitudSchema>;
|
|
+
|
|
+function parseAndValidate(
|
|
+ formData: FormData,
|
|
+): { success: true; data: EdgeAIGenerateSubjectInput } | {
|
|
+ success: false;
|
|
+ errors: string[];
|
|
+} {
|
|
+ const rawInput = {
|
|
+ plan_estudio_id: formData.get("plan_estudio_id"),
|
|
+ datosBasicos: formData.get("datosBasicos"),
|
|
+ iaConfig: formData.get("iaConfig"),
|
|
+ archivosAdjuntos: formData.getAll("archivosAdjuntos"),
|
|
+ };
|
|
+
|
|
+ const result = SolicitudSchema.safeParse(rawInput);
|
|
+ if (!result.success) {
|
|
+ const errors = result.error.errors.map((issue) => {
|
|
+ const path = issue.path.length ? `${issue.path.join(".")}: ` : "";
|
|
+ return `${path}${issue.message}`;
|
|
+ });
|
|
+ return { success: false, errors };
|
|
+ }
|
|
+
|
|
+ return { success: true, data: result.data };
|
|
+}
|