fix/estandarizar-migraciones #19
@@ -32,3 +32,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
|||||||
|
|
||||||
# Finder (MacOS) folder config
|
# Finder (MacOS) folder config
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
supabase/snippets
|
||||||
@@ -5,8 +5,10 @@
|
|||||||
"jsr:@david/dax@~0.44.2": "0.44.2",
|
"jsr:@david/dax@~0.44.2": "0.44.2",
|
||||||
"jsr:@david/path@0.2": "0.2.0",
|
"jsr:@david/path@0.2": "0.2.0",
|
||||||
"jsr:@david/which@~0.4.1": "0.4.1",
|
"jsr:@david/which@~0.4.1": "0.4.1",
|
||||||
|
"jsr:@openai/openai@*": "6.16.0",
|
||||||
"jsr:@openai/openai@^6.16.0": "6.16.0",
|
"jsr:@openai/openai@^6.16.0": "6.16.0",
|
||||||
"jsr:@std/bytes@^1.0.6": "1.0.6",
|
"jsr:@std/bytes@^1.0.6": "1.0.6",
|
||||||
|
"jsr:@std/dotenv@*": "0.225.6",
|
||||||
"jsr:@std/fmt@1": "1.0.9",
|
"jsr:@std/fmt@1": "1.0.9",
|
||||||
"jsr:@std/fs@1": "1.0.22",
|
"jsr:@std/fs@1": "1.0.22",
|
||||||
"jsr:@std/fs@^1.0.20": "1.0.22",
|
"jsr:@std/fs@^1.0.20": "1.0.22",
|
||||||
@@ -57,6 +59,9 @@
|
|||||||
"@std/bytes@1.0.6": {
|
"@std/bytes@1.0.6": {
|
||||||
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
|
"integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
|
||||||
},
|
},
|
||||||
|
"@std/dotenv@0.225.6": {
|
||||||
|
"integrity": "1d6f9db72f565bd26790fa034c26e45ecb260b5245417be76c2279e5734c421b"
|
||||||
|
},
|
||||||
"@std/fmt@1.0.9": {
|
"@std/fmt@1.0.9": {
|
||||||
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
|
"integrity": "2487343e8899fb2be5d0e3d35013e54477ada198854e52dd05ed0422eddcabe0"
|
||||||
},
|
},
|
||||||
@@ -89,57 +94,33 @@
|
|||||||
"npm": {
|
"npm": {
|
||||||
"@deno/darwin-arm64@2.6.4": {
|
"@deno/darwin-arm64@2.6.4": {
|
||||||
"integrity": "sha512-5pocuV2IVWYUzomXze+bDSO86uX1WPxUn82BS7BrLI3ix8Eq7l8DhvYgz064w7rwCM4CiLLSc6MAQpAI+0gUag==",
|
"integrity": "sha512-5pocuV2IVWYUzomXze+bDSO86uX1WPxUn82BS7BrLI3ix8Eq7l8DhvYgz064w7rwCM4CiLLSc6MAQpAI+0gUag==",
|
||||||
"os": [
|
"os": ["darwin"],
|
||||||
"darwin"
|
"cpu": ["arm64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/darwin-x64@2.6.4": {
|
"@deno/darwin-x64@2.6.4": {
|
||||||
"integrity": "sha512-tX7Fz47HcoXTYR4wN2WzXjqJtEPTftzjEknCYkZa8D4BzvpN7Zo1ra3Jd6bWPN9+T6mgTL6YMPMMSigXddIUcA==",
|
"integrity": "sha512-tX7Fz47HcoXTYR4wN2WzXjqJtEPTftzjEknCYkZa8D4BzvpN7Zo1ra3Jd6bWPN9+T6mgTL6YMPMMSigXddIUcA==",
|
||||||
"os": [
|
"os": ["darwin"],
|
||||||
"darwin"
|
"cpu": ["x64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/linux-arm64-glibc@2.6.4": {
|
"@deno/linux-arm64-glibc@2.6.4": {
|
||||||
"integrity": "sha512-wY4hjjVLq5idClWj6e1de9IUZ0QZ1rxo16o0dwCJpMi+w1lolxtYxGF4y7pkcxAdZOpMGsXZuYMPq1Z2n51esA==",
|
"integrity": "sha512-wY4hjjVLq5idClWj6e1de9IUZ0QZ1rxo16o0dwCJpMi+w1lolxtYxGF4y7pkcxAdZOpMGsXZuYMPq1Z2n51esA==",
|
||||||
"os": [
|
"os": ["linux"],
|
||||||
"linux"
|
"cpu": ["arm64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/linux-x64-glibc@2.6.4": {
|
"@deno/linux-x64-glibc@2.6.4": {
|
||||||
"integrity": "sha512-H9b0WJw+eWyC6GcMGCXxC/3lz2kVveyWyUrzGyQ8hJmtDJpWiDnCU2iPqEpEcwXHgAUFFyjnRaFwedwwv5ApdA==",
|
"integrity": "sha512-H9b0WJw+eWyC6GcMGCXxC/3lz2kVveyWyUrzGyQ8hJmtDJpWiDnCU2iPqEpEcwXHgAUFFyjnRaFwedwwv5ApdA==",
|
||||||
"os": [
|
"os": ["linux"],
|
||||||
"linux"
|
"cpu": ["x64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/win32-arm64@2.6.4": {
|
"@deno/win32-arm64@2.6.4": {
|
||||||
"integrity": "sha512-fMH1dx54ORbCTpMorGjDshwonIu6KfgfPC+Pq+14jeq/8604EIFXiA484p3Ov+ViZycndNHEPe8ZKPNd8xLjJA==",
|
"integrity": "sha512-fMH1dx54ORbCTpMorGjDshwonIu6KfgfPC+Pq+14jeq/8604EIFXiA484p3Ov+ViZycndNHEPe8ZKPNd8xLjJA==",
|
||||||
"os": [
|
"os": ["win32"],
|
||||||
"win32"
|
"cpu": ["arm64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@deno/win32-x64@2.6.4": {
|
"@deno/win32-x64@2.6.4": {
|
||||||
"integrity": "sha512-hAxqNrBLHCINtqDgKcRiu8QtMbtdnHOVW3OFQjzA3gZEcpnGUK3JiF5ql++83SjuQH8ViZKGcdqkioLR7Bf14A==",
|
"integrity": "sha512-hAxqNrBLHCINtqDgKcRiu8QtMbtdnHOVW3OFQjzA3gZEcpnGUK3JiF5ql++83SjuQH8ViZKGcdqkioLR7Bf14A==",
|
||||||
"os": [
|
"os": ["win32"],
|
||||||
"win32"
|
"cpu": ["x64"]
|
||||||
],
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"@isaacs/fs-minipass@4.0.1": {
|
"@isaacs/fs-minipass@4.0.1": {
|
||||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||||
@@ -376,8 +357,10 @@
|
|||||||
"dependencies": [
|
"dependencies": [
|
||||||
"jsr:@david/dax@~0.44.2",
|
"jsr:@david/dax@~0.44.2",
|
||||||
"jsr:@openai/openai@^6.16.0",
|
"jsr:@openai/openai@^6.16.0",
|
||||||
|
"jsr:@supabase/functions-js@2",
|
||||||
"jsr:@zod/zod@^4.3.5",
|
"jsr:@zod/zod@^4.3.5",
|
||||||
"jsr:@david/dax@~0.44.2"
|
"npm:@supabase/supabase-js@2",
|
||||||
|
"npm:openai@6.16.0"
|
||||||
],
|
],
|
||||||
"packageJson": {
|
"packageJson": {
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 8,
|
||||||
|
"id": "a9a99d9d",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import { load } from \"jsr:@std/dotenv\";\n",
|
||||||
|
"import OpenAI from \"jsr:@openai/openai\";\n",
|
||||||
|
"\n",
|
||||||
|
"const env = await load({\n",
|
||||||
|
" export: true,\n",
|
||||||
|
"});\n",
|
||||||
|
"\n",
|
||||||
|
"const openai = new OpenAI();"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"id": "89b3fffb",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import { createClient } from \"@supabase/supabase-js\";\n",
|
||||||
|
"const supabaseUrl = Deno.env.get(\"SUPABASE_URL\") || env.SUPABASE_URL;\n",
|
||||||
|
"const supabaseKey = Deno.env.get(\"SUPABASE_ANON_KEY\") || env.SUPABASE_ANON_KEY;\n",
|
||||||
|
"const supabase = createClient(supabaseUrl, supabaseKey);"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "33290880",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"// Create a conversation for a plan de estudio\n",
|
||||||
|
"\n",
|
||||||
|
"const { data: input } = await supabase.from(\"planes_estudio\").limit(1).select(\"id\").single();\n",
|
||||||
|
"const openaiConversation = await openai.conversations.create({\n",
|
||||||
|
" metadata: {\n",
|
||||||
|
" \"tabla\": \"planes_estudio\",\n",
|
||||||
|
" \"id\": input.id,\n",
|
||||||
|
"\n",
|
||||||
|
" instanciador: \"alex\",\n",
|
||||||
|
" },\n",
|
||||||
|
"\n",
|
||||||
|
" items: [\n",
|
||||||
|
" { type: \"message\", role: \"system\", content: \"En caso de que te pidan algo que no tiene nada que ver con planes de estudio o asignatura responde con un refusal.\" },\n",
|
||||||
|
" ],\n",
|
||||||
|
"});\n",
|
||||||
|
"\n",
|
||||||
|
"const conversationPlane = await supabase.from(\"conversation_planes_estudio\").insert({\n",
|
||||||
|
" conversation_id: openaiConversation.id,\n",
|
||||||
|
" plan_estudio_id: input.id,\n",
|
||||||
|
"}).select(\"id\").single();"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "f726b80e",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"// Archivar una conversación\n",
|
||||||
|
"const { data: conversation } = await supabase.from(\"conversation_planes_estudio\").limit(1).select(\"conversation_id\").single();\n",
|
||||||
|
"\n",
|
||||||
|
"const items = await openai.conversations.items(conversation.conversation_id);\n",
|
||||||
|
"await supabase.from(\"conversation_planes_estudio\").update({\n",
|
||||||
|
" estado: \"ARCHIVANDO\",\n",
|
||||||
|
" conversation_json: items,\n",
|
||||||
|
"}).eq(\"id\", conversationPlane.id);\n",
|
||||||
|
"\n",
|
||||||
|
"await openai.conversations.delete(conversation.conversation_id);"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "cfae6e48",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"// Listar conversaciones dadas de un plan de estudio\n",
|
||||||
|
"const { data: conversations } = await supabase.from(\"conversation_planes_estudio\").select(\"*\").eq(\"plan_estudio_id\", input.id);\n",
|
||||||
|
"conversations"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "83e060ae",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"// Add a message to the conversation\n",
|
||||||
|
"const { data: conversationToUpdate } = await supabase.from(\n",
|
||||||
|
" \"conversation_planes_estudio\",\n",
|
||||||
|
").eq(\"id\", conversationPlane.id)\n",
|
||||||
|
" .select(\"conversation_id\").single();\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Deno",
|
||||||
|
"language": "typescript",
|
||||||
|
"name": "deno"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": "typescript",
|
||||||
|
"file_extension": ".ts",
|
||||||
|
"mimetype": "text/x.typescript",
|
||||||
|
"name": "typescript",
|
||||||
|
"nbconvert_exporter": "script",
|
||||||
|
"pygments_lexer": "typescript",
|
||||||
|
"version": "5.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
|
|
||||||
create table "public"."tabla_prueba" (
|
|
||||||
"id" bigint generated by default as identity not null,
|
|
||||||
"created_at" timestamp with time zone not null default now(),
|
|
||||||
"columna_prueba" text
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
alter table "public"."tabla_prueba" enable row level security;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX tabla_prueba_pkey ON public.tabla_prueba USING btree (id);
|
|
||||||
|
|
||||||
alter table "public"."tabla_prueba" add constraint "tabla_prueba_pkey" PRIMARY KEY using index "tabla_prueba_pkey";
|
|
||||||
|
|
||||||
set check_function_bodies = off;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.set_actualizado_en()
|
|
||||||
RETURNS trigger
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
begin
|
|
||||||
new.actualizado_en = now();
|
|
||||||
return new;
|
|
||||||
end;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.unaccent_immutable(text)
|
|
||||||
RETURNS text
|
|
||||||
LANGUAGE sql
|
|
||||||
IMMUTABLE PARALLEL SAFE STRICT
|
|
||||||
AS $function$
|
|
||||||
SELECT public.unaccent('public.unaccent', $1);
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.validar_numero_ciclo_asignatura()
|
|
||||||
RETURNS trigger
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$
|
|
||||||
declare
|
|
||||||
v_numero_ciclos int;
|
|
||||||
begin
|
|
||||||
if new.numero_ciclo is null then
|
|
||||||
return new;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
select pe.numero_ciclos into v_numero_ciclos
|
|
||||||
from planes_estudio pe
|
|
||||||
where pe.id = new.plan_estudio_id;
|
|
||||||
|
|
||||||
if v_numero_ciclos is null then
|
|
||||||
raise exception 'plan_estudio_id inválido %, plan no encontrado', new.plan_estudio_id;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if new.numero_ciclo < 1 then
|
|
||||||
raise exception 'numero_ciclo debe ser >= 1 (recibido %)', new.numero_ciclo;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if new.numero_ciclo > v_numero_ciclos then
|
|
||||||
raise exception 'numero_ciclo % excede planes_estudio.numero_ciclos % para plan_estudio_id %',
|
|
||||||
new.numero_ciclo, v_numero_ciclos, new.plan_estudio_id;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
return new;
|
|
||||||
end;
|
|
||||||
$function$
|
|
||||||
;
|
|
||||||
|
|
||||||
grant delete on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant insert on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant references on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant select on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant trigger on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant truncate on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant update on table "public"."tabla_prueba" to "anon";
|
|
||||||
|
|
||||||
grant delete on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant insert on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant references on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant select on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant trigger on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant truncate on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant update on table "public"."tabla_prueba" to "authenticated";
|
|
||||||
|
|
||||||
grant delete on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant insert on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant references on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant select on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant trigger on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant truncate on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant update on table "public"."tabla_prueba" to "postgres";
|
|
||||||
|
|
||||||
grant delete on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant insert on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant references on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant select on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant trigger on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant truncate on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
grant update on table "public"."tabla_prueba" to "service_role";
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
create extension if not exists "http" with schema "extensions";
|
|
||||||
|
|
||||||
alter table "public"."asignaturas" drop constraint "asignaturas_facultad_propietaria_id_fkey";
|
|
||||||
|
|
||||||
alter table "public"."cambios_plan" drop constraint "cambios_plan_plan_estudio_id_fkey";
|
|
||||||
|
|
||||||
alter type "public"."tipo_cambio" rename to "tipo_cambio__old_version_to_be_dropped";
|
|
||||||
|
|
||||||
create type "public"."tipo_cambio" as enum ('ACTUALIZACION_CAMPO', 'ACTUALIZACION_MAPA', 'TRANSICION_ESTADO', 'OTRO', 'CREACION', 'ACTUALIZACION');
|
|
||||||
|
|
||||||
alter table "public"."cambios_asignatura" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio";
|
|
||||||
|
|
||||||
alter table "public"."cambios_plan" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio";
|
|
||||||
|
|
||||||
drop type "public"."tipo_cambio__old_version_to_be_dropped";
|
|
||||||
|
|
||||||
alter table "public"."asignaturas" drop column "facultad_propietaria_id";
|
|
||||||
|
|
||||||
alter table "public"."asignaturas" add column "asignatura_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored;
|
|
||||||
|
|
||||||
alter table "public"."cambios_plan" drop column "interaccion_ia_id";
|
|
||||||
|
|
||||||
alter table "public"."cambios_plan" add column "response_id" text;
|
|
||||||
|
|
||||||
alter table "public"."planes_estudio" add column "conversation_id" text;
|
|
||||||
|
|
||||||
alter table "public"."planes_estudio" add column "plan_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX planes_estudio_conversation_id_key ON public.planes_estudio USING btree (conversation_id);
|
|
||||||
|
|
||||||
alter table "public"."planes_estudio" add constraint "planes_estudio_conversation_id_key" UNIQUE using index "planes_estudio_conversation_id_key";
|
|
||||||
|
|
||||||
set check_function_bodies = off;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.fn_log_cambios_planes_estudio()
|
|
||||||
RETURNS trigger
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$declare
|
|
||||||
k text;
|
|
||||||
old_val jsonb;
|
|
||||||
new_val jsonb;
|
|
||||||
|
|
||||||
v_response_id text;
|
|
||||||
begin
|
|
||||||
v_response_id := nullif(new.meta_origen->>'response_id','');
|
|
||||||
|
|
||||||
-- INSERT -> CREACION
|
|
||||||
if tg_op = 'INSERT' then
|
|
||||||
insert into public.cambios_plan (
|
|
||||||
plan_estudio_id,
|
|
||||||
cambiado_por,
|
|
||||||
tipo,
|
|
||||||
campo,
|
|
||||||
valor_anterior,
|
|
||||||
valor_nuevo,
|
|
||||||
response_id
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
new.id,
|
|
||||||
new.creado_por,
|
|
||||||
'CREACION'::public.tipo_cambio,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
to_jsonb(new),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
return new;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- DELETE (opcional): si no lo quieres, bórralo
|
|
||||||
if tg_op = 'DELETE' then
|
|
||||||
insert into public.cambios_plan (
|
|
||||||
plan_estudio_id,
|
|
||||||
cambiado_por,
|
|
||||||
tipo,
|
|
||||||
campo,
|
|
||||||
valor_anterior,
|
|
||||||
valor_nuevo,
|
|
||||||
response_id
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
old.id,
|
|
||||||
old.actualizado_por,
|
|
||||||
'OTRO'::public.tipo_cambio,
|
|
||||||
'DELETE',
|
|
||||||
to_jsonb(old),
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
return old;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- UPDATE ----------------------------------------------------------
|
|
||||||
-- 1) Transición de estado
|
|
||||||
if (new.estado_actual_id is distinct from old.estado_actual_id) then
|
|
||||||
insert into public.cambios_plan (
|
|
||||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
new.id,
|
|
||||||
new.actualizado_por,
|
|
||||||
'TRANSICION_ESTADO'::public.tipo_cambio,
|
|
||||||
'estado_actual_id',
|
|
||||||
to_jsonb(old.estado_actual_id),
|
|
||||||
to_jsonb(new.estado_actual_id),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- 2) Cambios en JSONB "datos" (diff top-level por llave)
|
|
||||||
if (new.datos is distinct from old.datos) then
|
|
||||||
for k in
|
|
||||||
select distinct key
|
|
||||||
from (
|
|
||||||
select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key
|
|
||||||
union all
|
|
||||||
select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key
|
|
||||||
) t
|
|
||||||
loop
|
|
||||||
old_val := coalesce(old.datos, '{}'::jsonb) -> k;
|
|
||||||
new_val := coalesce(new.datos, '{}'::jsonb) -> k;
|
|
||||||
|
|
||||||
if (old_val is distinct from new_val) then
|
|
||||||
insert into public.cambios_plan (
|
|
||||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
new.id,
|
|
||||||
new.actualizado_por,
|
|
||||||
'ACTUALIZACION_CAMPO'::public.tipo_cambio,
|
|
||||||
k,
|
|
||||||
old_val,
|
|
||||||
new_val,
|
|
||||||
v_response_id
|
|
||||||
);
|
|
||||||
end if;
|
|
||||||
end loop;
|
|
||||||
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- 3) Cambios en columnas "normales" (uno por columna)
|
|
||||||
if (new.nombre is distinct from old.nombre) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.nivel is distinct from old.nivel) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.tipo_ciclo is distinct from old.tipo_ciclo) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.numero_ciclos is distinct from old.numero_ciclos) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.activo is distinct from old.activo) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.carrera_id is distinct from old.carrera_id) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.estructura_id is distinct from old.estructura_id) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.tipo_origen is distinct from old.tipo_origen) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
|
|
||||||
if (new.conversation_id is distinct from old.conversation_id) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'conversation_id', to_jsonb(old.conversation_id), to_jsonb(new.conversation_id), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- 🔥 consumirlo para que NO se guarde en planes_estudio
|
|
||||||
if v_response_id is not null then
|
|
||||||
new.meta_origen := new.meta_origen - 'response_id';
|
|
||||||
end if;
|
|
||||||
|
|
||||||
return new;
|
|
||||||
end;$function$
|
|
||||||
;
|
|
||||||
|
|
||||||
CREATE TRIGGER "agregar-conversation_id-asignaturas" AFTER INSERT ON public.asignaturas FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000');
|
|
||||||
|
|
||||||
CREATE TRIGGER "agregar-conversation_id-planes_estudio" AFTER INSERT ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000');
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_planes_estudio_log_cambios AFTER INSERT OR DELETE OR UPDATE ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION public.fn_log_cambios_planes_estudio();
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
verify_jwt = false
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"imports": {
|
|
||||||
"openai": "npm:openai@6.16.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Function deprecated. This endpoint was replaced by shared module `_shared/openai-service.ts`.
|
|
||||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
||||||
|
|
||||||
const json = (body: unknown, status = 200) =>
|
|
||||||
new Response(JSON.stringify(body), {
|
|
||||||
status,
|
|
||||||
headers: {
|
|
||||||
"content-type": "application/json; charset=utf-8",
|
|
||||||
"access-control-allow-origin": "*",
|
|
||||||
"access-control-allow-headers": "*",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Deno.serve((req) => {
|
|
||||||
if (req.method === "OPTIONS") return json({ ok: true });
|
|
||||||
return json(
|
|
||||||
{
|
|
||||||
ok: false,
|
|
||||||
error:
|
|
||||||
"This endpoint is deprecated. Use the shared module _shared/openai-service.ts from your functions.",
|
|
||||||
},
|
|
||||||
410,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,779 +0,0 @@
|
|||||||
// supabase/functions/ai_generate_plan/index.ts
|
|
||||||
/// <reference lib="deno.window" />
|
|
||||||
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
||||||
import { createClient } from "npm:@supabase/supabase-js@2";
|
|
||||||
|
|
||||||
const corsHeaders = {
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Access-Control-Allow-Headers":
|
|
||||||
"authorization, x-client-info, apikey, content-type",
|
|
||||||
};
|
|
||||||
|
|
||||||
function json(body: unknown, status = 200) {
|
|
||||||
return new Response(JSON.stringify(body, null, 2), {
|
|
||||||
status,
|
|
||||||
headers: { ...corsHeaders, "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ----------------- LOGGING HELPERS (diagnóstico) ----------------- */
|
|
||||||
function nowIso() {
|
|
||||||
return new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function redactAuthHeader(v: string | null) {
|
|
||||||
if (!v) return null;
|
|
||||||
const parts = v.split(" ");
|
|
||||||
if (parts.length >= 2) return `${parts[0]} [REDACTED]`;
|
|
||||||
return "[REDACTED]";
|
|
||||||
}
|
|
||||||
|
|
||||||
function safePreview(val: unknown, maxLen = 1400) {
|
|
||||||
try {
|
|
||||||
const jsonStr = JSON.stringify(val, (k, v) => {
|
|
||||||
if (typeof v === "string") {
|
|
||||||
if (v.length > 800) return v.slice(0, 800) + "…[TRUNCATED]";
|
|
||||||
if (v.startsWith("data:") && v.includes(";base64,")) return "[DATA_URL_REDACTED]";
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
});
|
|
||||||
if (!jsonStr) return String(val);
|
|
||||||
return jsonStr.length > maxLen ? jsonStr.slice(0, maxLen) + "…[TRUNCATED]" : jsonStr;
|
|
||||||
} catch {
|
|
||||||
return String(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function headersSnapshot(req: Request) {
|
|
||||||
const h: Record<string, string> = {};
|
|
||||||
for (const [k, v] of req.headers.entries()) {
|
|
||||||
if (k.toLowerCase() === "authorization") h[k] = redactAuthHeader(v) ?? "";
|
|
||||||
else h[k] = v;
|
|
||||||
}
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeNivel(raw: string): string {
|
|
||||||
const s = (raw ?? "").trim().toLowerCase();
|
|
||||||
if (!s) return "Otro";
|
|
||||||
if (s === "licenciatura" || s === "lic") return "Licenciatura";
|
|
||||||
if (s === "maestria" || s === "maestría" || s === "msc") return "Maestría";
|
|
||||||
if (s === "doctorado" || s === "phd") return "Doctorado";
|
|
||||||
if (s === "especialidad") return "Especialidad";
|
|
||||||
if (s === "diplomado") return "Diplomado";
|
|
||||||
return "Otro";
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTipoCiclo(raw: string): string {
|
|
||||||
const s = (raw ?? "").trim().toLowerCase();
|
|
||||||
if (!s) return "Otro";
|
|
||||||
if (s === "semestre" || s === "semestres" || s === "sem" || s === "semestre(s)" ||
|
|
||||||
s === "semestre" || s === "semestre" || s === "semestre") return "Semestre";
|
|
||||||
if (s === "cuatrimestre" || s === "cuatrimestres" || s === "cuatri") return "Cuatrimestre";
|
|
||||||
if (s === "trimestre" || s === "trimestres" || s === "trim") return "Trimestre";
|
|
||||||
// si viene en mayúsculas tipo frontend
|
|
||||||
if (s === "semestre" || s === "semestre".toLowerCase()) return "Semestre";
|
|
||||||
if (s === "cuatrimestre" || s === "cuatrimestre".toLowerCase()) return "Cuatrimestre";
|
|
||||||
if (s === "trimestre" || s === "trimestre".toLowerCase()) return "Trimestre";
|
|
||||||
if (s === "otro") return "Otro";
|
|
||||||
// variantes comunes
|
|
||||||
if (s === "semestre".toLowerCase()) return "Semestre";
|
|
||||||
if (s === "cuatrimestre".toLowerCase()) return "Cuatrimestre";
|
|
||||||
if (s === "trimestre".toLowerCase()) return "Trimestre";
|
|
||||||
// si llega como SEMESTRE / CUATRIMESTRE / TRIMESTRE
|
|
||||||
if (s === "semestre".toLowerCase()) return "Semestre";
|
|
||||||
if (s === "cuatrimestre".toLowerCase()) return "Cuatrimestre";
|
|
||||||
if (s === "trimestre".toLowerCase()) return "Trimestre";
|
|
||||||
// fallback
|
|
||||||
return "Otro";
|
|
||||||
}
|
|
||||||
|
|
||||||
function suggestedWeeksFor(tipoCiclo: string) {
|
|
||||||
switch (tipoCiclo) {
|
|
||||||
case "Semestre":
|
|
||||||
return "16";
|
|
||||||
case "Cuatrimestre":
|
|
||||||
return "16";
|
|
||||||
case "Trimestre":
|
|
||||||
return "12";
|
|
||||||
default:
|
|
||||||
return "16";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPrompt(params: {
|
|
||||||
datosBasicos: any;
|
|
||||||
carrera?: any;
|
|
||||||
estructura?: any;
|
|
||||||
iaConfig?: any;
|
|
||||||
}) {
|
|
||||||
const { datosBasicos, carrera, estructura, iaConfig } = params;
|
|
||||||
|
|
||||||
const nombrePlan = String(datosBasicos?.nombrePlan ?? "").trim();
|
|
||||||
const nivel = normalizeNivel(String(datosBasicos?.nivel ?? ""));
|
|
||||||
const tipoCiclo = normalizeTipoCiclo(String(datosBasicos?.tipoCiclo ?? ""));
|
|
||||||
const numCiclos = Number(datosBasicos?.numCiclos ?? 0) || 0;
|
|
||||||
|
|
||||||
const semanas = suggestedWeeksFor(tipoCiclo);
|
|
||||||
|
|
||||||
const carreraTxt = carrera
|
|
||||||
? `Carrera: ${carrera?.nombre ?? ""} (id: ${carrera?.id ?? ""})\nFacultad: ${carrera?.facultades?.nombre ?? carrera?.facultad?.nombre ?? ""}`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const system = [
|
|
||||||
"Eres un experto en diseño curricular (México) y redacción institucional.",
|
|
||||||
"Debes generar SOLO JSON válido que cumpla EXACTAMENTE el schema (sin campos extra).",
|
|
||||||
"Usa español neutro y redacción formal. Si un campo es opcional y no aplica, usa null.",
|
|
||||||
"Respeta enumeraciones (p.ej. modalidad_educativa, diseno_curricular).",
|
|
||||||
"Asegura consistencia interna entre nivel, fines, perfiles y área de estudio.",
|
|
||||||
"La carga_horaria_a_la_semana NO debe exceder 50.",
|
|
||||||
].join(" ");
|
|
||||||
|
|
||||||
const user = [
|
|
||||||
"Genera un borrador completo del PLAN DE ESTUDIOS con base en lo siguiente:",
|
|
||||||
"",
|
|
||||||
"== Datos básicos (NO los cambies) ==",
|
|
||||||
`- nivel: ${nivel}`,
|
|
||||||
`- nombre: ${nombrePlan}`,
|
|
||||||
`- tipo de ciclo: ${tipoCiclo}`,
|
|
||||||
`- número de ciclos: ${numCiclos}`,
|
|
||||||
`- duración sugerida del ciclo escolar (semanas efectivas): ${semanas}`,
|
|
||||||
"",
|
|
||||||
carreraTxt ? `== Contexto institucional ==\n${carreraTxt}\n` : "",
|
|
||||||
"== Enfoque / Población / Notas ==",
|
|
||||||
`- descripcionEnfoque: ${iaConfig?.descripcionEnfoque ?? ""}`,
|
|
||||||
`- notasAdicionales: ${iaConfig?.notasAdicionales ?? ""}`,
|
|
||||||
"",
|
|
||||||
"== Reglas adicionales ==",
|
|
||||||
"- Alinea: fines_de_aprendizaje_o_formacion ↔ perfil_de_egreso ↔ area_de_estudio.",
|
|
||||||
`- total_de_ciclos_del_plan_de_estudios debe ser \"${String(numCiclos)}\" (string).`,
|
|
||||||
`- duracion_del_ciclo_escolar debe ser \"${semanas}\" (string).`,
|
|
||||||
"- diseno_curricular: elige 'Rígido' o 'Flexible' coherente con la administración.",
|
|
||||||
"- Si modalidad_educativa es 'No escolarizada' o 'Mixta', llena justificacion_de_la_propuesta_curricular; si es 'Escolar', puede ser null.",
|
|
||||||
"- clave_del_plan_de_estudios: genera una clave administrativa verosímil (p.ej. 2026-XXX-YYY).",
|
|
||||||
"",
|
|
||||||
"Entrega únicamente el JSON final (sin Markdown, sin explicación).",
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
return { system, user, computed: { nivel, tipoCiclo, numCiclos, semanas, nombrePlan } };
|
|
||||||
}
|
|
||||||
|
|
||||||
Deno.serve(async (req) => {
|
|
||||||
const cid = crypto.randomUUID();
|
|
||||||
const t0 = performance.now();
|
|
||||||
|
|
||||||
const log = (msg: string, extra?: unknown) => {
|
|
||||||
if (extra !== undefined) {
|
|
||||||
console.log(`[${nowIso()}][ai_generate_plan][${cid}] ${msg}`, safePreview(extra));
|
|
||||||
} else {
|
|
||||||
console.log(`[${nowIso()}][ai_generate_plan][${cid}] ${msg}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const warn = (msg: string, extra?: unknown) => {
|
|
||||||
if (extra !== undefined) {
|
|
||||||
console.warn(`[${nowIso()}][ai_generate_plan][${cid}] ⚠️ ${msg}`, safePreview(extra));
|
|
||||||
} else {
|
|
||||||
console.warn(`[${nowIso()}][ai_generate_plan][${cid}] ⚠️ ${msg}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const errlog = (msg: string, extra?: unknown) => {
|
|
||||||
if (extra !== undefined) {
|
|
||||||
console.error(`[${nowIso()}][ai_generate_plan][${cid}] ❌ ${msg}`, safePreview(extra));
|
|
||||||
} else {
|
|
||||||
console.error(`[${nowIso()}][ai_generate_plan][${cid}] ❌ ${msg}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
|
||||||
log("OPTIONS preflight ✅", {
|
|
||||||
method: req.method,
|
|
||||||
url: req.url,
|
|
||||||
headers: headersSnapshot(req),
|
|
||||||
});
|
|
||||||
return new Response(null, { status: 204, headers: corsHeaders });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method !== "POST") {
|
|
||||||
warn("Method not allowed", { method: req.method });
|
|
||||||
return json({ ok: false, error: "Method not allowed", cid }, 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
log("START 🚀", { method: req.method, url: req.url });
|
|
||||||
log("Headers snapshot 🧾", headersSnapshot(req));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1) Auth requerido ✅
|
|
||||||
log("Step 1: Auth check 🔐");
|
|
||||||
const authHeaderRaw = req.headers.get("Authorization") ??
|
|
||||||
req.headers.get("authorization");
|
|
||||||
log("Authorization header present?", {
|
|
||||||
present: Boolean(authHeaderRaw),
|
|
||||||
value: redactAuthHeader(authHeaderRaw),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!authHeaderRaw) {
|
|
||||||
warn("Missing Authorization header");
|
|
||||||
return json({ ok: false, error: "Missing Authorization header", cid }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
|
||||||
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
|
||||||
log("Env presence (Supabase) 🌍", {
|
|
||||||
hasSUPABASE_URL: Boolean(SUPABASE_URL),
|
|
||||||
hasSUPABASE_ANON_KEY: Boolean(SUPABASE_ANON_KEY),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!SUPABASE_URL || !SUPABASE_ANON_KEY) {
|
|
||||||
errlog("Missing SUPABASE_URL / SUPABASE_ANON_KEY");
|
|
||||||
return json(
|
|
||||||
{ ok: false, error: "Missing SUPABASE_URL / SUPABASE_ANON_KEY", cid },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabaseUser = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
||||||
global: { headers: { Authorization: authHeaderRaw } },
|
|
||||||
});
|
|
||||||
|
|
||||||
const tAuth = performance.now();
|
|
||||||
const { data: userData, error: userErr } = await supabaseUser.auth.getUser();
|
|
||||||
log("Supabase auth.getUser() done ⏱️", { ms: Math.round(performance.now() - tAuth) });
|
|
||||||
|
|
||||||
if (userErr || !userData?.user) {
|
|
||||||
warn("Invalid token", { userErr: userErr?.message ?? userErr ?? null });
|
|
||||||
return json({ ok: false, error: "Invalid token", cid }, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = userData.user;
|
|
||||||
log("Authenticated user ✅", { userId: user.id, email: user.email ?? null });
|
|
||||||
|
|
||||||
const SERVICE_ROLE = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
||||||
log("Env presence (service role) 🗝️", { hasSERVICE_ROLE: Boolean(SERVICE_ROLE) });
|
|
||||||
|
|
||||||
const supabaseAdmin = SERVICE_ROLE
|
|
||||||
? createClient(SUPABASE_URL, SERVICE_ROLE)
|
|
||||||
: supabaseUser;
|
|
||||||
|
|
||||||
log("Supabase client selected 🧩", { adminMode: Boolean(SERVICE_ROLE) });
|
|
||||||
|
|
||||||
// 2) Parse body 🧾
|
|
||||||
log("Step 2: Parse body 🧾");
|
|
||||||
let payload: any = {};
|
|
||||||
const tParse = performance.now();
|
|
||||||
try {
|
|
||||||
payload = await req.json();
|
|
||||||
} catch (e) {
|
|
||||||
errlog("Invalid JSON body", { error: (e as Error)?.message ?? String(e) });
|
|
||||||
return json({ ok: false, error: "Invalid JSON body", cid }, 400);
|
|
||||||
}
|
|
||||||
log("Body parsed ⏱️", { ms: Math.round(performance.now() - tParse) });
|
|
||||||
log("Payload preview", { keys: Object.keys(payload ?? {}), payload: payload ? { ...payload, iaConfig: undefined } : null });
|
|
||||||
|
|
||||||
const datosBasicos = payload?.datosBasicos ?? payload?.input?.datosBasicos;
|
|
||||||
const iaConfig = payload?.iaConfig ?? payload?.input?.iaConfig ?? {};
|
|
||||||
|
|
||||||
if (!datosBasicos?.nombrePlan || !datosBasicos?.carreraId || !datosBasicos?.nivel || !datosBasicos?.tipoCiclo || !datosBasicos?.numCiclos) {
|
|
||||||
warn("Missing required datosBasicos fields", { datosBasicos });
|
|
||||||
return json({
|
|
||||||
ok: false,
|
|
||||||
error: "Missing required fields in datosBasicos (nombrePlan,carreraId,nivel,tipoCiclo,numCiclos).",
|
|
||||||
cid,
|
|
||||||
}, 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3) Asegura usuarios_app (por FK en logs) 👤
|
|
||||||
log("Step 3: Ensure usuarios_app row 👤");
|
|
||||||
const tUserUpsert = performance.now();
|
|
||||||
const upsertUser = {
|
|
||||||
id: user.id,
|
|
||||||
email: user.email ?? null,
|
|
||||||
nombre_completo: (user.user_metadata as any)?.full_name ??
|
|
||||||
(user.user_metadata as any)?.name ?? null,
|
|
||||||
};
|
|
||||||
const { error: upsertErr } = await supabaseAdmin
|
|
||||||
.from("usuarios_app")
|
|
||||||
.upsert(upsertUser, { onConflict: "id" });
|
|
||||||
log("usuarios_app upsert done ⏱️", { ms: Math.round(performance.now() - tUserUpsert), ok: !upsertErr });
|
|
||||||
if (upsertErr) {
|
|
||||||
errlog("usuarios_app upsert failed", { message: upsertErr.message });
|
|
||||||
return json({ ok: false, error: "usuarios_app upsert failed", details: upsertErr.message, cid }, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Trae carrera + facultad (contexto prompt) 🎓
|
|
||||||
log("Step 4: Fetch carrera/facultad 🎓");
|
|
||||||
const carreraId = datosBasicos.carreraId;
|
|
||||||
const tCarr = performance.now();
|
|
||||||
const { data: carrera, error: carreraErr } = await supabaseAdmin
|
|
||||||
.from("carreras")
|
|
||||||
.select("id,nombre,facultad_id,facultades(id,nombre,nombre_corto)")
|
|
||||||
.eq("id", carreraId)
|
|
||||||
.maybeSingle();
|
|
||||||
log("Carrera fetch done ⏱️", { ms: Math.round(performance.now() - tCarr), found: Boolean(carrera), carreraId });
|
|
||||||
if (carreraErr) warn("Carrera fetch error (continuing)", { message: carreraErr.message });
|
|
||||||
|
|
||||||
// 5) Resolver estructura (schema) desde estructuras_plan.definicion 🧩
|
|
||||||
log("Step 5: Resolve estructura_plan 🧩");
|
|
||||||
const estructuraIdFromPayload = datosBasicos?.estructuraId ?? payload?.estructuraId ?? null;
|
|
||||||
|
|
||||||
let estructura: any = null;
|
|
||||||
const tEst = performance.now();
|
|
||||||
|
|
||||||
if (estructuraIdFromPayload) {
|
|
||||||
log("Using estructuraId from payload", { estructuraIdFromPayload });
|
|
||||||
const { data, error } = await supabaseAdmin
|
|
||||||
.from("estructuras_plan")
|
|
||||||
.select("id,nombre,tipo,template,definicion")
|
|
||||||
.eq("id", estructuraIdFromPayload)
|
|
||||||
.single();
|
|
||||||
if (error) {
|
|
||||||
errlog("estructuraId provided but not found", { message: error.message, estructuraIdFromPayload });
|
|
||||||
return json({ ok: false, error: "estructuraId not found", details: error.message, cid }, 400);
|
|
||||||
}
|
|
||||||
estructura = data;
|
|
||||||
} else {
|
|
||||||
// default: la más reciente tipo CURRICULAR, si no existe, la más reciente en general
|
|
||||||
const { data, error } = await supabaseAdmin
|
|
||||||
.from("estructuras_plan")
|
|
||||||
.select("id,nombre,tipo,template,definicion")
|
|
||||||
.eq("tipo", "CURRICULAR")
|
|
||||||
.order("actualizado_en", { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
warn("Error fetching CURRICULAR estructura (fallback to any)", { message: error.message });
|
|
||||||
}
|
|
||||||
if (data) estructura = data;
|
|
||||||
|
|
||||||
if (!estructura) {
|
|
||||||
const { data: anyE, error: anyErr } = await supabaseAdmin
|
|
||||||
.from("estructuras_plan")
|
|
||||||
.select("id,nombre,tipo,template,definicion")
|
|
||||||
.order("actualizado_en", { ascending: false })
|
|
||||||
.limit(1)
|
|
||||||
.maybeSingle();
|
|
||||||
if (anyErr) {
|
|
||||||
errlog("No estructuras_plan found", { message: anyErr.message });
|
|
||||||
return json({ ok: false, error: "No estructuras_plan found", details: anyErr.message, cid }, 500);
|
|
||||||
}
|
|
||||||
estructura = anyE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Estructura resolved ⏱️", {
|
|
||||||
ms: Math.round(performance.now() - tEst),
|
|
||||||
estructuraId: estructura?.id ?? null,
|
|
||||||
nombre: estructura?.nombre ?? null,
|
|
||||||
tipo: estructura?.tipo ?? null,
|
|
||||||
template: estructura?.template ?? null,
|
|
||||||
definicionKeys: estructura?.definicion ? Object.keys(estructura.definicion) : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!estructura?.definicion) {
|
|
||||||
errlog("estructuras_plan.definicion missing", { estructuraId: estructura?.id ?? null });
|
|
||||||
return json({ ok: false, error: "estructuras_plan.definicion missing", cid }, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6) Resolver referencias (archivos / repositorios) 📎
|
|
||||||
log("Step 6: Resolve references 📎");
|
|
||||||
const uploadedToStorage: string[] = Array.isArray(iaConfig?.uploadedToStorage)
|
|
||||||
? iaConfig.uploadedToStorage
|
|
||||||
: [];
|
|
||||||
const repositoriosIds: string[] = Array.isArray(iaConfig?.repositoriosIds)
|
|
||||||
? iaConfig.repositoriosIds
|
|
||||||
: [];
|
|
||||||
|
|
||||||
log("Reference IDs received", {
|
|
||||||
uploadedToStorageCount: uploadedToStorage.length,
|
|
||||||
repositoriosIdsCount: repositoriosIds.length,
|
|
||||||
usarMCP: Boolean(iaConfig?.usarMCP),
|
|
||||||
});
|
|
||||||
|
|
||||||
const archivosInfo: any[] = [];
|
|
||||||
const vectorInfo: any[] = [];
|
|
||||||
|
|
||||||
if (uploadedToStorage.length) {
|
|
||||||
const { data, error } = await supabaseAdmin
|
|
||||||
.from("archivos")
|
|
||||||
.select("id,nombre,mime_type,openai_file_id,ruta_storage")
|
|
||||||
.in("id", uploadedToStorage);
|
|
||||||
if (error) warn("archivos lookup failed (continuing)", { message: error.message });
|
|
||||||
for (const a of data ?? []) archivosInfo.push(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (repositoriosIds.length) {
|
|
||||||
const { data, error } = await supabaseAdmin
|
|
||||||
.from("vector_stores")
|
|
||||||
.select("id,nombre,openai_vector_id")
|
|
||||||
.in("id", repositoriosIds);
|
|
||||||
if (error) warn("vector_stores lookup failed (continuing)", { message: error.message });
|
|
||||||
for (const v of data ?? []) vectorInfo.push(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
const openaiFileIds: string[] = archivosInfo
|
|
||||||
.map((a) => a?.openai_file_id)
|
|
||||||
.filter((x) => typeof x === "string" && x.length > 0);
|
|
||||||
const vectorStoreIds: string[] = vectorInfo
|
|
||||||
.map((v) => v?.openai_vector_id)
|
|
||||||
.filter((x) => typeof x === "string" && x.length > 0);
|
|
||||||
|
|
||||||
const rutasStorage: string[] = archivosInfo
|
|
||||||
.map((a) => a?.ruta_storage)
|
|
||||||
.filter((x) => typeof x === "string" && x.length > 0);
|
|
||||||
|
|
||||||
log("Resolved reference OpenAI IDs", {
|
|
||||||
openaiFileIdsCount: openaiFileIds.length,
|
|
||||||
vectorStoreIdsCount: vectorStoreIds.length,
|
|
||||||
rutasStorageCount: rutasStorage.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 7) Construye prompt + structured wrapper 🧠
|
|
||||||
log("Step 7: Build prompt + structured schema 🧠");
|
|
||||||
const { system, user: userPrompt, computed } = buildPrompt({
|
|
||||||
datosBasicos,
|
|
||||||
carrera,
|
|
||||||
estructura,
|
|
||||||
iaConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
// definicion en DB puede venir como (schema) o como (format). Normalizamos.
|
|
||||||
let structured: any = null;
|
|
||||||
const definicion = estructura.definicion;
|
|
||||||
|
|
||||||
if (definicion?.type === "json_schema" && definicion?.schema) {
|
|
||||||
structured = definicion;
|
|
||||||
} else if (definicion?.format?.type === "json_schema" && definicion?.format?.schema) {
|
|
||||||
structured = definicion;
|
|
||||||
} else {
|
|
||||||
structured = {
|
|
||||||
type: "json_schema",
|
|
||||||
name: "plan_datos",
|
|
||||||
strict: true,
|
|
||||||
schema: definicion,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
log("Structured wrapper summary", {
|
|
||||||
type: structured?.type ?? null,
|
|
||||||
name: structured?.name ?? null,
|
|
||||||
strict: structured?.strict ?? null,
|
|
||||||
hasSchema: Boolean(structured?.schema),
|
|
||||||
schemaTopKeys: structured?.schema ? Object.keys(structured.schema).slice(0, 20) : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 8) Llamar ai-structured (otra Edge Function) 🔁
|
|
||||||
log("Step 8: Call ai-structured 🔁");
|
|
||||||
const aiStructuredUrl = `${SUPABASE_URL}/functions/v1/ai-structured`;
|
|
||||||
|
|
||||||
const model = payload?.model ?? payload?.openai?.model ?? payload?.response?.model ?? "gpt-5";
|
|
||||||
|
|
||||||
const aiStructuredBody = {
|
|
||||||
response: {
|
|
||||||
model,
|
|
||||||
input: [
|
|
||||||
{ role: "system", content: system },
|
|
||||||
{ role: "user", content: userPrompt },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
structured,
|
|
||||||
references: {
|
|
||||||
openaiFileIds,
|
|
||||||
vectorStoreIds,
|
|
||||||
uploadedToStorage,
|
|
||||||
},
|
|
||||||
usarMCP: Boolean(iaConfig?.usarMCP),
|
|
||||||
};
|
|
||||||
|
|
||||||
log("ai-structured request snapshot 🧪", {
|
|
||||||
url: aiStructuredUrl,
|
|
||||||
model,
|
|
||||||
inputChars: { system: system.length, user: userPrompt.length },
|
|
||||||
openaiFileIdsCount: openaiFileIds.length,
|
|
||||||
vectorStoreIdsCount: vectorStoreIds.length,
|
|
||||||
usarMCP: Boolean(iaConfig?.usarMCP),
|
|
||||||
});
|
|
||||||
|
|
||||||
const tAi = performance.now();
|
|
||||||
const aiResp = await fetch(aiStructuredUrl, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: authHeaderRaw,
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(aiStructuredBody),
|
|
||||||
});
|
|
||||||
|
|
||||||
const aiJson = await aiResp.json().catch(() => null);
|
|
||||||
|
|
||||||
log("ai-structured response received ⏱️", {
|
|
||||||
ms: Math.round(performance.now() - tAi),
|
|
||||||
status: aiResp.status,
|
|
||||||
okHttp: aiResp.ok,
|
|
||||||
ok: aiJson?.ok ?? null,
|
|
||||||
aiCid: aiJson?.cid ?? null,
|
|
||||||
responseId: aiJson?.responseId ?? null,
|
|
||||||
conversationId: aiJson?.conversationId ?? null,
|
|
||||||
outputParseError: aiJson?.outputParseError ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!aiResp.ok || !aiJson) {
|
|
||||||
errlog("ai-structured HTTP failed", { status: aiResp.status, body: aiJson });
|
|
||||||
return json({
|
|
||||||
ok: false,
|
|
||||||
error: "ai-structured HTTP failed",
|
|
||||||
status: aiResp.status,
|
|
||||||
details: aiJson,
|
|
||||||
cid,
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!aiJson.ok || !aiJson.output) {
|
|
||||||
errlog("ai-structured returned ok=false or missing output", { aiJson });
|
|
||||||
// Aun así, registra interaccion_ia (sin plan) para auditoría
|
|
||||||
const { data: inter, error: interErr } = await supabaseAdmin
|
|
||||||
.from("interacciones_ia")
|
|
||||||
.insert({
|
|
||||||
usuario_id: user.id,
|
|
||||||
plan_estudio_id: null,
|
|
||||||
tipo: "GENERAR",
|
|
||||||
modelo: aiJson?.model ?? model,
|
|
||||||
temperatura: null,
|
|
||||||
prompt: {
|
|
||||||
datosBasicos,
|
|
||||||
iaConfig,
|
|
||||||
estructura: { id: estructura.id, nombre: estructura.nombre, template: estructura.template, tipo: estructura.tipo },
|
|
||||||
request: aiStructuredBody,
|
|
||||||
computed,
|
|
||||||
},
|
|
||||||
respuesta: aiJson,
|
|
||||||
aceptada: false,
|
|
||||||
conversacion_id: aiJson?.conversationId ?? null,
|
|
||||||
ids_archivos: uploadedToStorage,
|
|
||||||
ids_vector_store: repositoriosIds,
|
|
||||||
rutas_storage: rutasStorage,
|
|
||||||
})
|
|
||||||
.select("id")
|
|
||||||
.maybeSingle();
|
|
||||||
|
|
||||||
if (interErr) warn("interacciones_ia insert failed (error path)", { message: interErr.message });
|
|
||||||
|
|
||||||
return json({
|
|
||||||
ok: false,
|
|
||||||
error: "AI generation failed (structured output invalid).",
|
|
||||||
cid,
|
|
||||||
ai: aiJson,
|
|
||||||
interaccion_ia_id: inter?.id ?? null,
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9) Normaliza/asegura campos clave del output ✅
|
|
||||||
log("Step 9: Normalize output ✅");
|
|
||||||
const output = aiJson.output as Record<string, any>;
|
|
||||||
|
|
||||||
// fuerza consistencia con datosBasicos
|
|
||||||
output.nombre = computed.nombrePlan;
|
|
||||||
output.nivel = computed.nivel;
|
|
||||||
output.total_de_ciclos_del_plan_de_estudios = String(computed.numCiclos);
|
|
||||||
output.duracion_del_ciclo_escolar = String(computed.semanas);
|
|
||||||
|
|
||||||
// 10) Inserta log de IA (interacciones_ia) 🧾
|
|
||||||
log("Step 10: Insert interacciones_ia 🧾");
|
|
||||||
const tInter = performance.now();
|
|
||||||
const { data: interaccion, error: interErr } = await supabaseAdmin
|
|
||||||
.from("interacciones_ia")
|
|
||||||
.insert({
|
|
||||||
usuario_id: user.id,
|
|
||||||
plan_estudio_id: null, // se linkea luego (cuando exista el plan)
|
|
||||||
tipo: "GENERAR",
|
|
||||||
modelo: aiJson?.model ?? model,
|
|
||||||
temperatura: null,
|
|
||||||
prompt: {
|
|
||||||
datosBasicos,
|
|
||||||
iaConfig,
|
|
||||||
estructura: {
|
|
||||||
id: estructura.id,
|
|
||||||
nombre: estructura.nombre,
|
|
||||||
template: estructura.template,
|
|
||||||
tipo: estructura.tipo,
|
|
||||||
},
|
|
||||||
request: aiStructuredBody,
|
|
||||||
computed,
|
|
||||||
},
|
|
||||||
respuesta: aiJson,
|
|
||||||
aceptada: true, // se está usando para crear el plan
|
|
||||||
conversacion_id: aiJson?.conversationId ?? null,
|
|
||||||
ids_archivos: uploadedToStorage,
|
|
||||||
ids_vector_store: repositoriosIds,
|
|
||||||
rutas_storage: rutasStorage,
|
|
||||||
})
|
|
||||||
.select("id")
|
|
||||||
.single();
|
|
||||||
|
|
||||||
log("interacciones_ia insert done ⏱️", {
|
|
||||||
ms: Math.round(performance.now() - tInter),
|
|
||||||
interaccionId: interaccion?.id ?? null,
|
|
||||||
ok: !interErr,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (interErr) {
|
|
||||||
errlog("interacciones_ia insert failed", { message: interErr.message });
|
|
||||||
return json(
|
|
||||||
{ ok: false, error: "interacciones_ia insert failed", details: interErr.message, cid },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11) Crear plan (planes_estudio) 🏗️
|
|
||||||
log("Step 11: Insert planes_estudio 🏗️");
|
|
||||||
const nivelDB = normalizeNivel(String(datosBasicos?.nivel ?? ""));
|
|
||||||
const tipoCicloDB = normalizeTipoCiclo(String(datosBasicos?.tipoCiclo ?? ""));
|
|
||||||
const numeroCiclosDB = Number(datosBasicos?.numCiclos ?? 0);
|
|
||||||
|
|
||||||
// intenta setear estado BORRADOR si existe
|
|
||||||
let estadoBorradorId: string | null = null;
|
|
||||||
try {
|
|
||||||
const { data: estado } = await supabaseAdmin
|
|
||||||
.from("estados_plan")
|
|
||||||
.select("id,clave,orden")
|
|
||||||
.ilike("clave", "BORRADOR%")
|
|
||||||
.order("orden", { ascending: true })
|
|
||||||
.limit(1)
|
|
||||||
.maybeSingle();
|
|
||||||
estadoBorradorId = estado?.id ?? null;
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
const tPlan = performance.now();
|
|
||||||
const { data: plan, error: planErr } = await supabaseAdmin
|
|
||||||
.from("planes_estudio")
|
|
||||||
.insert({
|
|
||||||
carrera_id: carreraId,
|
|
||||||
estructura_id: estructura.id,
|
|
||||||
nombre: computed.nombrePlan,
|
|
||||||
nivel: nivelDB,
|
|
||||||
tipo_ciclo: tipoCicloDB,
|
|
||||||
numero_ciclos: numeroCiclosDB,
|
|
||||||
datos: output,
|
|
||||||
estado_actual_id: estadoBorradorId,
|
|
||||||
activo: true,
|
|
||||||
tipo_origen: "IA",
|
|
||||||
meta_origen: {
|
|
||||||
generado_por: "ai_generate_plan",
|
|
||||||
cid,
|
|
||||||
ai_structured: {
|
|
||||||
cid: aiJson?.cid ?? null,
|
|
||||||
responseId: aiJson?.responseId ?? null,
|
|
||||||
conversationId: aiJson?.conversationId ?? null,
|
|
||||||
model: aiJson?.model ?? model,
|
|
||||||
usage: aiJson?.usage ?? null,
|
|
||||||
},
|
|
||||||
referencias: {
|
|
||||||
uploadedToStorage,
|
|
||||||
repositoriosIds,
|
|
||||||
openaiFileIds,
|
|
||||||
vectorStoreIds,
|
|
||||||
},
|
|
||||||
iaConfig: {
|
|
||||||
descripcionEnfoque: iaConfig?.descripcionEnfoque ?? null,
|
|
||||||
poblacionObjetivo: iaConfig?.poblacionObjetivo ?? null,
|
|
||||||
notasAdicionales: iaConfig?.notasAdicionales ?? null,
|
|
||||||
usarMCP: Boolean(iaConfig?.usarMCP),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
creado_por: user.id,
|
|
||||||
actualizado_por: user.id,
|
|
||||||
})
|
|
||||||
.select("id,nombre,nivel,tipo_ciclo,numero_ciclos,carrera_id,estructura_id,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,datos")
|
|
||||||
.single();
|
|
||||||
|
|
||||||
log("planes_estudio insert done ⏱️", {
|
|
||||||
ms: Math.round(performance.now() - tPlan),
|
|
||||||
planId: plan?.id ?? null,
|
|
||||||
ok: !planErr,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (planErr) {
|
|
||||||
errlog("planes_estudio insert failed", { message: planErr.message });
|
|
||||||
return json(
|
|
||||||
{ ok: false, error: "planes_estudio insert failed", details: planErr.message, cid },
|
|
||||||
500,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12) Link interaccion_ia -> plan + historial (cambios_plan) 🧩
|
|
||||||
log("Step 12: Link interaccion + insert cambios_plan 🧩");
|
|
||||||
|
|
||||||
const { error: linkErr } = await supabaseAdmin
|
|
||||||
.from("interacciones_ia")
|
|
||||||
.update({ plan_estudio_id: plan.id })
|
|
||||||
.eq("id", interaccion.id);
|
|
||||||
|
|
||||||
if (linkErr) warn("Could not link interaccion_ia to plan (continuing)", { message: linkErr.message });
|
|
||||||
|
|
||||||
const { error: cambioErr } = await supabaseAdmin
|
|
||||||
.from("cambios_plan")
|
|
||||||
.insert({
|
|
||||||
plan_estudio_id: plan.id,
|
|
||||||
cambiado_por: user.id,
|
|
||||||
tipo: "ACTUALIZACION_CAMPO",
|
|
||||||
campo: "datos",
|
|
||||||
valor_anterior: null,
|
|
||||||
valor_nuevo: output,
|
|
||||||
interaccion_ia_id: interaccion.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cambioErr) warn("cambios_plan insert failed (continuing)", { message: cambioErr.message });
|
|
||||||
|
|
||||||
// 13) (OPCIONAL) Generar materias automáticamente (dejado comentado) 📚
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// const AUTO_GENERATE_SUBJECTS = false; // 👈 activar cuando decidas
|
|
||||||
// const AUTO_SUBJECTS_COUNT = 10; // 👈 constante pedida (número de materias)
|
|
||||||
// if (AUTO_GENERATE_SUBJECTS) {
|
|
||||||
// for (let i = 1; i <= AUTO_SUBJECTS_COUNT; i++) {
|
|
||||||
// // TODO: insertar en `asignaturas` con plan_estudio_id = plan.id
|
|
||||||
// // Recomendación: usar una estructura_asignatura default y llenar datos mínimos.
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
log("RETURN ✅", {
|
|
||||||
ok: true,
|
|
||||||
planId: plan.id,
|
|
||||||
interaccionIaId: interaccion.id,
|
|
||||||
totalMs: Math.round(performance.now() - t0),
|
|
||||||
});
|
|
||||||
|
|
||||||
return json({
|
|
||||||
ok: true,
|
|
||||||
cid,
|
|
||||||
plan,
|
|
||||||
plan_datos: output,
|
|
||||||
interaccion_ia_id: interaccion.id,
|
|
||||||
ai: {
|
|
||||||
cid: aiJson?.cid ?? null,
|
|
||||||
responseId: aiJson?.responseId ?? null,
|
|
||||||
conversationId: aiJson?.conversationId ?? null,
|
|
||||||
model: aiJson?.model ?? model,
|
|
||||||
usage: aiJson?.usage ?? null,
|
|
||||||
},
|
|
||||||
references: {
|
|
||||||
uploadedToStorage,
|
|
||||||
repositoriosIds,
|
|
||||||
openaiFileIds,
|
|
||||||
vectorStoreIds,
|
|
||||||
rutasStorage,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
errlog("Unhandled error 💥", {
|
|
||||||
message: (e as Error)?.message ?? String(e),
|
|
||||||
stack: (e as Error)?.stack ?? null,
|
|
||||||
totalMs: Math.round(performance.now() - t0),
|
|
||||||
});
|
|
||||||
return json({ ok: false, error: (e as Error)?.message ?? String(e), cid }, 500);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,110 +1,32 @@
|
|||||||
|
// 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
|
// Setup type definitions for built-in Supabase Runtime APIs
|
||||||
import "@supabase/functions-js/edge-runtime.d.ts";
|
import "@supabase/functions-js/edge-runtime.d.ts"
|
||||||
import { createClient } from "npm:@supabase/supabase-js@2";
|
|
||||||
type WebhookPayload = {
|
console.log("Hello from Functions!")
|
||||||
type: "INSERT" | "UPDATE" | "DELETE";
|
|
||||||
table: string;
|
|
||||||
schema: string;
|
|
||||||
record: Record<string, unknown> | null;
|
|
||||||
old_record: Record<string, unknown> | null;
|
|
||||||
};
|
|
||||||
const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? "";
|
|
||||||
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ??
|
|
||||||
"";
|
|
||||||
const OPENAI_API_KEY = Deno.env.get("OPENAI_API_KEY") ?? "";
|
|
||||||
const OPENAI_BASE_URL = Deno.env.get("OPENAI_BASE_URL") ??
|
|
||||||
"https://api.openai.com/v1";
|
|
||||||
const ALLOWED_SCHEMA = "public";
|
|
||||||
const ALLOWED_TABLES = new Set(["planes_estudio", "asignaturas"]);
|
|
||||||
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, {
|
|
||||||
auth: { persistSession: false },
|
|
||||||
});
|
|
||||||
function jsonResponse(status: number, body: Record<string, unknown>) {
|
|
||||||
return new Response(JSON.stringify(body), {
|
|
||||||
status,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
async function createConversationId(metadata: Record<string, string>) {
|
|
||||||
const response = await fetch(`${OPENAI_BASE_URL}/conversations`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${OPENAI_API_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ metadata }),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`OpenAI error: ${response.status} ${errorText}`);
|
|
||||||
}
|
|
||||||
const data = await response.json();
|
|
||||||
const conversationId = data?.id as string | undefined;
|
|
||||||
if (!conversationId) {
|
|
||||||
throw new Error("OpenAI response missing conversation id");
|
|
||||||
}
|
|
||||||
return conversationId;
|
|
||||||
}
|
|
||||||
Deno.serve(async (req) => {
|
Deno.serve(async (req) => {
|
||||||
if (req.method !== "POST") {
|
const { name } = await req.json()
|
||||||
return jsonResponse(405, { error: "Method not allowed" });
|
const data = {
|
||||||
|
message: `Hello ${name}!`,
|
||||||
}
|
}
|
||||||
if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
|
|
||||||
return jsonResponse(500, { error: "Supabase env vars missing" });
|
return new Response(
|
||||||
}
|
JSON.stringify(data),
|
||||||
if (!OPENAI_API_KEY) {
|
{ headers: { "Content-Type": "application/json" } },
|
||||||
return jsonResponse(500, { error: "OPENAI_API_KEY missing" });
|
)
|
||||||
}
|
})
|
||||||
let payload: WebhookPayload;
|
|
||||||
try {
|
/* To invoke locally:
|
||||||
payload = await req.json();
|
|
||||||
} catch {
|
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
|
||||||
return jsonResponse(400, { error: "Invalid JSON body" });
|
2. Make an HTTP request:
|
||||||
}
|
|
||||||
const { type, table, schema, record } = payload;
|
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/create-chat-conversation' \
|
||||||
if (schema !== ALLOWED_SCHEMA || !ALLOWED_TABLES.has(table)) {
|
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
|
||||||
return jsonResponse(400, { error: "Table not allowed" });
|
--header 'Content-Type: application/json' \
|
||||||
}
|
--data '{"name":"Functions"}'
|
||||||
if (type !== "INSERT") {
|
|
||||||
return jsonResponse(200, { status: "ignored", reason: "event_type" });
|
*/
|
||||||
}
|
|
||||||
if (!record || typeof record !== "object") {
|
|
||||||
return jsonResponse(400, { error: "Missing record" });
|
|
||||||
}
|
|
||||||
const recordId = record["id"] as string | number | undefined;
|
|
||||||
if (!recordId) {
|
|
||||||
return jsonResponse(400, { error: "Record id missing" });
|
|
||||||
}
|
|
||||||
const existingConversationId = record["conversation_id"] as
|
|
||||||
| string
|
|
||||||
| null
|
|
||||||
| undefined;
|
|
||||||
if (existingConversationId) {
|
|
||||||
return jsonResponse(200, {
|
|
||||||
status: "skipped",
|
|
||||||
conversation_id: existingConversationId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let conversationId: string;
|
|
||||||
try {
|
|
||||||
conversationId = await createConversationId({
|
|
||||||
table,
|
|
||||||
record_id: String(recordId),
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return jsonResponse(502, { error: (error as Error).message });
|
|
||||||
}
|
|
||||||
const { error } = await supabase
|
|
||||||
.from(table)
|
|
||||||
.update({ conversation_id: conversationId })
|
|
||||||
.eq("id", recordId);
|
|
||||||
if (error) {
|
|
||||||
return jsonResponse(500, { error: error.message });
|
|
||||||
}
|
|
||||||
return jsonResponse(200, {
|
|
||||||
status: "updated",
|
|
||||||
table,
|
|
||||||
record_id: recordId,
|
|
||||||
conversation_id: conversationId,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1202,42 +1202,6 @@ GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "anon";
|
|||||||
GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "authenticated";
|
GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "authenticated";
|
||||||
GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "service_role";
|
GRANT ALL ON FUNCTION "public"."set_actualizado_en"() TO "service_role";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent"("text") TO "postgres";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent"("text") TO "anon";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent"("text") TO "authenticated";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent"("text") TO "service_role";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent"("regdictionary", "text") TO "postgres";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent"("regdictionary", "text") TO "anon";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent"("regdictionary", "text") TO "authenticated";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent"("regdictionary", "text") TO "service_role";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_immutable"("text") TO "anon";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_immutable"("text") TO "authenticated";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_immutable"("text") TO "service_role";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_init"("internal") TO "postgres";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_init"("internal") TO "anon";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_init"("internal") TO "authenticated";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_init"("internal") TO "service_role";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_lexize"("internal", "internal", "internal", "internal") TO "postgres";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_lexize"("internal", "internal", "internal", "internal") TO "anon";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_lexize"("internal", "internal", "internal", "internal") TO "authenticated";
|
|
||||||
GRANT ALL ON FUNCTION "public"."unaccent_lexize"("internal", "internal", "internal", "internal") TO "service_role";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "anon";
|
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "anon";
|
||||||
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "authenticated";
|
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "authenticated";
|
||||||
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "service_role";
|
GRANT ALL ON FUNCTION "public"."validar_numero_ciclo_asignatura"() TO "service_role";
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,208 @@
|
|||||||
|
create type "public"."estado_conversacion" as enum ('ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR');
|
||||||
|
|
||||||
|
alter table "public"."planes_estudio" drop constraint "planes_estudio_conversation_id_key";
|
||||||
|
|
||||||
|
drop view if exists "public"."plantilla_plan";
|
||||||
|
|
||||||
|
drop index if exists "public"."planes_estudio_conversation_id_key";
|
||||||
|
|
||||||
|
|
||||||
|
create table "public"."conversaciones_asignatura" (
|
||||||
|
"id" uuid not null default gen_random_uuid(),
|
||||||
|
"asignatura_id" uuid not null,
|
||||||
|
"openai_conversation_id" text not null,
|
||||||
|
"estado" public.estado_conversacion not null default 'ACTIVA'::public.estado_conversacion,
|
||||||
|
"conversacion_json" jsonb not null default '{}'::jsonb,
|
||||||
|
"creado_por" uuid,
|
||||||
|
"creado_en" timestamp with time zone not null default now(),
|
||||||
|
"archivado_por" uuid,
|
||||||
|
"archivado_en" timestamp with time zone,
|
||||||
|
"intento_archivado" integer not null default 0
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
create table "public"."conversaciones_plan" (
|
||||||
|
"id" uuid not null default gen_random_uuid(),
|
||||||
|
"plan_estudio_id" uuid not null,
|
||||||
|
"openai_conversation_id" text not null,
|
||||||
|
"estado" public.estado_conversacion not null default 'ACTIVA'::public.estado_conversacion,
|
||||||
|
"conversacion_json" jsonb not null default '{}'::jsonb,
|
||||||
|
"creado_por" uuid,
|
||||||
|
"creado_en" timestamp with time zone not null default now(),
|
||||||
|
"archivado_por" uuid,
|
||||||
|
"archivado_en" timestamp with time zone,
|
||||||
|
"intento_archivado" integer not null default 0
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
alter table "public"."planes_estudio" drop column "conversation_id";
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX conversaciones_asignatura_openai_id_unico ON public.conversaciones_asignatura USING btree (openai_conversation_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX conversaciones_asignatura_pkey ON public.conversaciones_asignatura USING btree (id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX conversaciones_plan_openai_id_unico ON public.conversaciones_plan USING btree (openai_conversation_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX conversaciones_plan_pkey ON public.conversaciones_plan USING btree (id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_conv_asig_asignatura ON public.conversaciones_asignatura USING btree (asignatura_id);
|
||||||
|
|
||||||
|
CREATE INDEX idx_conv_asig_estado ON public.conversaciones_asignatura USING btree (estado);
|
||||||
|
|
||||||
|
CREATE INDEX idx_conv_plan_estado ON public.conversaciones_plan USING btree (estado);
|
||||||
|
|
||||||
|
CREATE INDEX idx_conv_plan_plan_estudio ON public.conversaciones_plan USING btree (plan_estudio_id);
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_pkey" PRIMARY KEY using index "conversaciones_asignatura_pkey";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_pkey" PRIMARY KEY using index "conversaciones_plan_pkey";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_archivado_por_fkey" FOREIGN KEY (archivado_por) REFERENCES public.usuarios_app(id) ON DELETE SET NULL not valid;
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_asignatura" validate constraint "conversaciones_asignatura_archivado_por_fkey";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_asignatura_id_fkey" FOREIGN KEY (asignatura_id) REFERENCES public.asignaturas(id) ON DELETE CASCADE not valid;
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_asignatura" validate constraint "conversaciones_asignatura_asignatura_id_fkey";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_creado_por_fkey" FOREIGN KEY (creado_por) REFERENCES public.usuarios_app(id) ON DELETE SET NULL not valid;
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_asignatura" validate constraint "conversaciones_asignatura_creado_por_fkey";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_asignatura" add constraint "conversaciones_asignatura_openai_id_unico" UNIQUE using index "conversaciones_asignatura_openai_id_unico";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_archivado_por_fkey" FOREIGN KEY (archivado_por) REFERENCES public.usuarios_app(id) ON DELETE SET NULL not valid;
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_plan" validate constraint "conversaciones_plan_archivado_por_fkey";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_creado_por_fkey" FOREIGN KEY (creado_por) REFERENCES public.usuarios_app(id) ON DELETE SET NULL not valid;
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_plan" validate constraint "conversaciones_plan_creado_por_fkey";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_openai_id_unico" UNIQUE using index "conversaciones_plan_openai_id_unico";
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_plan" add constraint "conversaciones_plan_plan_estudio_id_fkey" FOREIGN KEY (plan_estudio_id) REFERENCES public.planes_estudio(id) ON DELETE CASCADE not valid;
|
||||||
|
|
||||||
|
alter table "public"."conversaciones_plan" validate constraint "conversaciones_plan_plan_estudio_id_fkey";
|
||||||
|
|
||||||
|
create or replace view "public"."plantilla_plan" as SELECT plan.id AS plan_estudio_id,
|
||||||
|
struct.id AS estructura_id,
|
||||||
|
struct.template_id
|
||||||
|
FROM (public.planes_estudio plan
|
||||||
|
JOIN public.estructuras_plan struct ON ((plan.estructura_id = struct.id)));
|
||||||
|
|
||||||
|
|
||||||
|
grant delete on table "public"."conversaciones_asignatura" to "anon";
|
||||||
|
|
||||||
|
grant insert on table "public"."conversaciones_asignatura" to "anon";
|
||||||
|
|
||||||
|
grant references on table "public"."conversaciones_asignatura" to "anon";
|
||||||
|
|
||||||
|
grant select on table "public"."conversaciones_asignatura" to "anon";
|
||||||
|
|
||||||
|
grant trigger on table "public"."conversaciones_asignatura" to "anon";
|
||||||
|
|
||||||
|
grant truncate on table "public"."conversaciones_asignatura" to "anon";
|
||||||
|
|
||||||
|
grant update on table "public"."conversaciones_asignatura" to "anon";
|
||||||
|
|
||||||
|
grant delete on table "public"."conversaciones_asignatura" to "authenticated";
|
||||||
|
|
||||||
|
grant insert on table "public"."conversaciones_asignatura" to "authenticated";
|
||||||
|
|
||||||
|
grant references on table "public"."conversaciones_asignatura" to "authenticated";
|
||||||
|
|
||||||
|
grant select on table "public"."conversaciones_asignatura" to "authenticated";
|
||||||
|
|
||||||
|
grant trigger on table "public"."conversaciones_asignatura" to "authenticated";
|
||||||
|
|
||||||
|
grant truncate on table "public"."conversaciones_asignatura" to "authenticated";
|
||||||
|
|
||||||
|
grant update on table "public"."conversaciones_asignatura" to "authenticated";
|
||||||
|
|
||||||
|
grant delete on table "public"."conversaciones_asignatura" to "postgres";
|
||||||
|
|
||||||
|
grant insert on table "public"."conversaciones_asignatura" to "postgres";
|
||||||
|
|
||||||
|
grant references on table "public"."conversaciones_asignatura" to "postgres";
|
||||||
|
|
||||||
|
grant select on table "public"."conversaciones_asignatura" to "postgres";
|
||||||
|
|
||||||
|
grant trigger on table "public"."conversaciones_asignatura" to "postgres";
|
||||||
|
|
||||||
|
grant truncate on table "public"."conversaciones_asignatura" to "postgres";
|
||||||
|
|
||||||
|
grant update on table "public"."conversaciones_asignatura" to "postgres";
|
||||||
|
|
||||||
|
grant delete on table "public"."conversaciones_asignatura" to "service_role";
|
||||||
|
|
||||||
|
grant insert on table "public"."conversaciones_asignatura" to "service_role";
|
||||||
|
|
||||||
|
grant references on table "public"."conversaciones_asignatura" to "service_role";
|
||||||
|
|
||||||
|
grant select on table "public"."conversaciones_asignatura" to "service_role";
|
||||||
|
|
||||||
|
grant trigger on table "public"."conversaciones_asignatura" to "service_role";
|
||||||
|
|
||||||
|
grant truncate on table "public"."conversaciones_asignatura" to "service_role";
|
||||||
|
|
||||||
|
grant update on table "public"."conversaciones_asignatura" to "service_role";
|
||||||
|
|
||||||
|
grant delete on table "public"."conversaciones_plan" to "anon";
|
||||||
|
|
||||||
|
grant insert on table "public"."conversaciones_plan" to "anon";
|
||||||
|
|
||||||
|
grant references on table "public"."conversaciones_plan" to "anon";
|
||||||
|
|
||||||
|
grant select on table "public"."conversaciones_plan" to "anon";
|
||||||
|
|
||||||
|
grant trigger on table "public"."conversaciones_plan" to "anon";
|
||||||
|
|
||||||
|
grant truncate on table "public"."conversaciones_plan" to "anon";
|
||||||
|
|
||||||
|
grant update on table "public"."conversaciones_plan" to "anon";
|
||||||
|
|
||||||
|
grant delete on table "public"."conversaciones_plan" to "authenticated";
|
||||||
|
|
||||||
|
grant insert on table "public"."conversaciones_plan" to "authenticated";
|
||||||
|
|
||||||
|
grant references on table "public"."conversaciones_plan" to "authenticated";
|
||||||
|
|
||||||
|
grant select on table "public"."conversaciones_plan" to "authenticated";
|
||||||
|
|
||||||
|
grant trigger on table "public"."conversaciones_plan" to "authenticated";
|
||||||
|
|
||||||
|
grant truncate on table "public"."conversaciones_plan" to "authenticated";
|
||||||
|
|
||||||
|
grant update on table "public"."conversaciones_plan" to "authenticated";
|
||||||
|
|
||||||
|
grant delete on table "public"."conversaciones_plan" to "postgres";
|
||||||
|
|
||||||
|
grant insert on table "public"."conversaciones_plan" to "postgres";
|
||||||
|
|
||||||
|
grant references on table "public"."conversaciones_plan" to "postgres";
|
||||||
|
|
||||||
|
grant select on table "public"."conversaciones_plan" to "postgres";
|
||||||
|
|
||||||
|
grant trigger on table "public"."conversaciones_plan" to "postgres";
|
||||||
|
|
||||||
|
grant truncate on table "public"."conversaciones_plan" to "postgres";
|
||||||
|
|
||||||
|
grant update on table "public"."conversaciones_plan" to "postgres";
|
||||||
|
|
||||||
|
grant delete on table "public"."conversaciones_plan" to "service_role";
|
||||||
|
|
||||||
|
grant insert on table "public"."conversaciones_plan" to "service_role";
|
||||||
|
|
||||||
|
grant references on table "public"."conversaciones_plan" to "service_role";
|
||||||
|
|
||||||
|
grant select on table "public"."conversaciones_plan" to "service_role";
|
||||||
|
|
||||||
|
grant trigger on table "public"."conversaciones_plan" to "service_role";
|
||||||
|
|
||||||
|
grant truncate on table "public"."conversaciones_plan" to "service_role";
|
||||||
|
|
||||||
|
grant update on table "public"."conversaciones_plan" to "service_role";
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,205 +0,0 @@
|
|||||||
create extension if not exists "http" with schema "extensions";
|
|
||||||
|
|
||||||
alter table "public"."asignaturas" drop constraint if exists "asignaturas_facultad_propietaria_id_fkey";
|
|
||||||
|
|
||||||
alter table "public"."cambios_plan" drop constraint "cambios_plan_plan_estudio_id_fkey";
|
|
||||||
|
|
||||||
alter type "public"."tipo_cambio" rename to "tipo_cambio__old_version_to_be_dropped";
|
|
||||||
|
|
||||||
create type "public"."tipo_cambio" as enum ('ACTUALIZACION_CAMPO', 'ACTUALIZACION_MAPA', 'TRANSICION_ESTADO', 'OTRO', 'CREACION', 'ACTUALIZACION');
|
|
||||||
|
|
||||||
alter table "public"."cambios_asignatura" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio";
|
|
||||||
|
|
||||||
alter table "public"."cambios_plan" alter column tipo type "public"."tipo_cambio" using tipo::text::"public"."tipo_cambio";
|
|
||||||
|
|
||||||
drop type "public"."tipo_cambio__old_version_to_be_dropped";
|
|
||||||
|
|
||||||
alter table "public"."asignaturas" drop column if exists "facultad_propietaria_id";
|
|
||||||
|
|
||||||
alter table "public"."asignaturas" add column "asignatura_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored;
|
|
||||||
|
|
||||||
alter table "public"."cambios_plan" drop column "interaccion_ia_id";
|
|
||||||
|
|
||||||
alter table "public"."cambios_plan" add column "response_id" text;
|
|
||||||
|
|
||||||
alter table "public"."planes_estudio" add column "conversation_id" text;
|
|
||||||
|
|
||||||
alter table "public"."planes_estudio" add column "plan_hash" text generated always as (encode(SUBSTRING(extensions.digest((id)::text, 'sha512'::text) FROM 1 FOR 12), 'hex'::text)) stored;
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX planes_estudio_conversation_id_key ON public.planes_estudio USING btree (conversation_id);
|
|
||||||
|
|
||||||
alter table "public"."planes_estudio" add constraint "planes_estudio_conversation_id_key" UNIQUE using index "planes_estudio_conversation_id_key";
|
|
||||||
|
|
||||||
set check_function_bodies = off;
|
|
||||||
|
|
||||||
CREATE OR REPLACE FUNCTION public.fn_log_cambios_planes_estudio()
|
|
||||||
RETURNS trigger
|
|
||||||
LANGUAGE plpgsql
|
|
||||||
AS $function$declare
|
|
||||||
k text;
|
|
||||||
old_val jsonb;
|
|
||||||
new_val jsonb;
|
|
||||||
|
|
||||||
v_response_id text;
|
|
||||||
begin
|
|
||||||
v_response_id := nullif(new.meta_origen->>'response_id','');
|
|
||||||
|
|
||||||
-- INSERT -> CREACION
|
|
||||||
if tg_op = 'INSERT' then
|
|
||||||
insert into public.cambios_plan (
|
|
||||||
plan_estudio_id,
|
|
||||||
cambiado_por,
|
|
||||||
tipo,
|
|
||||||
campo,
|
|
||||||
valor_anterior,
|
|
||||||
valor_nuevo,
|
|
||||||
response_id
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
new.id,
|
|
||||||
new.creado_por,
|
|
||||||
'CREACION'::public.tipo_cambio,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
to_jsonb(new),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
return new;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- DELETE (opcional): si no lo quieres, bórralo
|
|
||||||
if tg_op = 'DELETE' then
|
|
||||||
insert into public.cambios_plan (
|
|
||||||
plan_estudio_id,
|
|
||||||
cambiado_por,
|
|
||||||
tipo,
|
|
||||||
campo,
|
|
||||||
valor_anterior,
|
|
||||||
valor_nuevo,
|
|
||||||
response_id
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
old.id,
|
|
||||||
old.actualizado_por,
|
|
||||||
'OTRO'::public.tipo_cambio,
|
|
||||||
'DELETE',
|
|
||||||
to_jsonb(old),
|
|
||||||
null,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
return old;
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- UPDATE ----------------------------------------------------------
|
|
||||||
-- 1) Transición de estado
|
|
||||||
if (new.estado_actual_id is distinct from old.estado_actual_id) then
|
|
||||||
insert into public.cambios_plan (
|
|
||||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
new.id,
|
|
||||||
new.actualizado_por,
|
|
||||||
'TRANSICION_ESTADO'::public.tipo_cambio,
|
|
||||||
'estado_actual_id',
|
|
||||||
to_jsonb(old.estado_actual_id),
|
|
||||||
to_jsonb(new.estado_actual_id),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- 2) Cambios en JSONB "datos" (diff top-level por llave)
|
|
||||||
if (new.datos is distinct from old.datos) then
|
|
||||||
for k in
|
|
||||||
select distinct key
|
|
||||||
from (
|
|
||||||
select jsonb_object_keys(coalesce(old.datos, '{}'::jsonb)) as key
|
|
||||||
union all
|
|
||||||
select jsonb_object_keys(coalesce(new.datos, '{}'::jsonb)) as key
|
|
||||||
) t
|
|
||||||
loop
|
|
||||||
old_val := coalesce(old.datos, '{}'::jsonb) -> k;
|
|
||||||
new_val := coalesce(new.datos, '{}'::jsonb) -> k;
|
|
||||||
|
|
||||||
if (old_val is distinct from new_val) then
|
|
||||||
insert into public.cambios_plan (
|
|
||||||
plan_estudio_id, cambiado_por, tipo, campo, valor_anterior, valor_nuevo, response_id
|
|
||||||
)
|
|
||||||
values (
|
|
||||||
new.id,
|
|
||||||
new.actualizado_por,
|
|
||||||
'ACTUALIZACION_CAMPO'::public.tipo_cambio,
|
|
||||||
k,
|
|
||||||
old_val,
|
|
||||||
new_val,
|
|
||||||
v_response_id
|
|
||||||
);
|
|
||||||
end if;
|
|
||||||
end loop;
|
|
||||||
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- 3) Cambios en columnas "normales" (uno por columna)
|
|
||||||
if (new.nombre is distinct from old.nombre) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'nombre', to_jsonb(old.nombre), to_jsonb(new.nombre), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.nivel is distinct from old.nivel) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'nivel', to_jsonb(old.nivel), to_jsonb(new.nivel), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.tipo_ciclo is distinct from old.tipo_ciclo) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_ciclo', to_jsonb(old.tipo_ciclo), to_jsonb(new.tipo_ciclo), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.numero_ciclos is distinct from old.numero_ciclos) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'numero_ciclos', to_jsonb(old.numero_ciclos), to_jsonb(new.numero_ciclos), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.activo is distinct from old.activo) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'activo', to_jsonb(old.activo), to_jsonb(new.activo), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.carrera_id is distinct from old.carrera_id) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'carrera_id', to_jsonb(old.carrera_id), to_jsonb(new.carrera_id), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.estructura_id is distinct from old.estructura_id) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'estructura_id', to_jsonb(old.estructura_id), to_jsonb(new.estructura_id), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
if (new.tipo_origen is distinct from old.tipo_origen) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'tipo_origen', to_jsonb(old.tipo_origen), to_jsonb(new.tipo_origen), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
|
|
||||||
if (new.conversation_id is distinct from old.conversation_id) then
|
|
||||||
insert into public.cambios_plan values (gen_random_uuid(), new.id, new.actualizado_por, now(),
|
|
||||||
'ACTUALIZACION'::public.tipo_cambio, 'conversation_id', to_jsonb(old.conversation_id), to_jsonb(new.conversation_id), null);
|
|
||||||
end if;
|
|
||||||
|
|
||||||
-- 🔥 consumirlo para que NO se guarde en planes_estudio
|
|
||||||
if v_response_id is not null then
|
|
||||||
new.meta_origen := new.meta_origen - 'response_id';
|
|
||||||
end if;
|
|
||||||
|
|
||||||
return new;
|
|
||||||
end;$function$
|
|
||||||
;
|
|
||||||
|
|
||||||
CREATE TRIGGER "agregar-conversation_id-asignaturas" AFTER INSERT ON public.asignaturas FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000');
|
|
||||||
|
|
||||||
CREATE TRIGGER "agregar-conversation_id-planes_estudio" AFTER INSERT ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION supabase_functions.http_request('https://exdkssurzmjnnhgtiama.supabase.co/functions/v1/create-chat-conversation', 'POST', '{"Content-type":"application/json","Authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImV4ZGtzc3Vyem1qbm5oZ3RpYW1hIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTM3ODYzMiwiZXhwIjoyMDU2OTU0NjMyfQ.s-GHuwnYbIYoMZN9dFbAKgxNyAtQllRCRPLy-GIRaro"}', '{}', '5000');
|
|
||||||
|
|
||||||
CREATE TRIGGER trg_planes_estudio_log_cambios AFTER INSERT OR DELETE OR UPDATE ON public.planes_estudio FOR EACH ROW EXECUTE FUNCTION public.fn_log_cambios_planes_estudio();
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,128 +0,0 @@
|
|||||||
INSERT INTO public.facultades (nombre, nombre_corto, color, icono) VALUES
|
|
||||||
--('Otros', 'OTRO', '#64748B', 'MoreHorizontal'),
|
|
||||||
('Coordinación de Desarrollo Humano Profesional', 'CDHP', '#F472B6', 'HeartHandshake'),
|
|
||||||
('Centro de Idiomas', 'CI', '#2DD4BF', 'Languages'),
|
|
||||||
--('Campus Virtual La Salle', 'CVLS', '#8B5CF6', 'Monitor'),
|
|
||||||
('Escuela de Altos Estudios en Salud', 'EAES', '#F43F5E', 'Stethoscope'),
|
|
||||||
--('Escuela Preparatoria Unidad Santa Teresa', 'EPST', '#EAB308', 'Backpack'),
|
|
||||||
--('Escuela Preparatoria Unidad Condesa', 'EPUC', '#F59E0B', 'BookOpen'),
|
|
||||||
('Facultad Mexicana de Arquitectura, Diseño y Comunicación', 'FAMADYC', '#EC4899', 'DraftingCompass'),
|
|
||||||
--('Facultad de Negocios', 'NEG', '#3B82F6', 'Briefcase'),
|
|
||||||
('Facultad de Derecho', 'DER', '#64748B', 'Scale'),
|
|
||||||
('Facultad de Humanidades y Ciencias Sociales', 'HUM', '#6366F1', 'Users'),
|
|
||||||
--('Facultad de Ingeniería', 'ING', '#F97316', 'Cpu'),
|
|
||||||
('Facultad Mexicana de Medicina', 'MED', '#10B981', 'HeartPulse'),
|
|
||||||
('Facultad de Ciencias Químicas', 'FCQ', '#84CC16', 'FlaskConical'),
|
|
||||||
('Escuela de Ciencias Religiosas', 'ECR', '#0EA5E9', 'BookHeart'),
|
|
||||||
('Escuela de Ciencias de la Educación', 'EDU', '#FB7185', 'GraduationCap');
|
|
||||||
--('Dirección Campus Virtual', 'DCV', '#A855F7', 'Globe');
|
|
||||||
|
|
||||||
INSERT INTO public.carreras (facultad_id, nombre, nombre_corto) VALUES
|
|
||||||
-- Coordinación de Desarrollo Humano Profesional
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Coordinación de Desarrollo Humano Profesional' LIMIT 1), 'Área Curricular Común 2013', 'ACC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Coordinación de Desarrollo Humano Profesional' LIMIT 1), 'Área Curricular Común 2021', 'ACC'),
|
|
||||||
|
|
||||||
-- Centro de Idiomas
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Centro de Idiomas' LIMIT 1), 'Área Curricular Común (Centro de Idiomas) 2013', 'ACCI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Centro de Idiomas' LIMIT 1), 'Área Curricular Común (Centro de Idiomas) 2021', 'ACCI'),
|
|
||||||
|
|
||||||
-- Escuela de Altos Estudios en Salud
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Enfermería', 'LENF'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Fisioterapia y Promoción para la Salud', 'LFPS'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Ciencias en el Deporte', 'LCD'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Medicina Dental', 'LMD'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Maestría en Nutrición Clínica', 'MNC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Escuela de Altos Estudios en Salud' LIMIT 1), 'Licenciatura en Fisioterapia', 'LFIS'),
|
|
||||||
|
|
||||||
-- Facultad Mexicana de Arquitectura, Diseño y Comunicación
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Arquitectura', 'ARQ'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Ciencias de la Comunicación', 'LCC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Diseño Gráfico y Digital', 'LDGD'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Diseño de Ambientes Interiores y Exteriores', 'LDAI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Especialidad en Gestión y Administración de Proyectos', 'EGAP'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Maestría en Estrategia e Innovación en Marcas', 'MEIM'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Maestría en Dirección Estratégica de Comunicación', 'MDEC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Comunicación', 'LCOM'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Licenciatura en Diseño de Productos', 'LDP'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Arquitectura, Diseño y Comunicación' LIMIT 1), 'Especialidad en Dirección de Proyectos', 'EDP'),
|
|
||||||
|
|
||||||
-- Facultad de Derecho
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Licenciatura en Derecho', 'LDER'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Licenciatura en Relaciones Internacionales', 'LRI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Derecho Financiero', 'MDF'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Especialidad en Derecho de Empresa', 'EDE'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Gobernanza y Estrategia Internacional', 'MGEI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Doctorado en Ciencias Jurídicas', 'DCJ'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Derecho Civil', 'MDC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Derecho de Empresa', 'MDE'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Derecho' LIMIT 1), 'Maestría en Justicia Penal', 'MJP'),
|
|
||||||
|
|
||||||
-- Facultad de Humanidades y Ciencias Sociales
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Educación Preescolar', 'LEP'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Ciencias Religiosas', 'LCR'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Filosofía', 'LFIL'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Maestría en Filosofía Social', 'MFS'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Área de Gestión Educativa', 'AGE'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Área de Intervención Docente', 'AID'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Especialidad en Gestión de los Aprendizajes', 'EGA'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Doctorado en Educación', 'DOCE'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Psicología', 'LPS'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Educación Primaria', 'LEPR'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Ciencias de la Educación', 'LCE'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Pedagogía', 'LPED'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Humanidades y Ciencias Sociales' LIMIT 1), 'Licenciatura en Teología', 'LTEO'),
|
|
||||||
|
|
||||||
-- Facultad Mexicana de Medicina
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad Mexicana de Medicina' LIMIT 1), 'Licenciatura en Médico Cirujano', 'LMC'),
|
|
||||||
|
|
||||||
-- Facultad de Ciencias Químicas
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Licenciatura en Ingeniería Química', 'LIQ'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Licenciatura en Químico Farmacéutico Biólogo', 'QFB'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Licenciatura en Química de Alimentos', 'LQA'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Licenciatura en Ingeniería Ambiental', 'LIA'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Maestría en Ciencia de los Alimentos y Nutrición Humana', 'MCAN'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Maestría en Farmacología Clínica', 'MFC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Maestría en Calidad y Estadística Aplicada', 'MCEA'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ciencias Químicas' LIMIT 1), 'Maestría en Ingeniería de Proyectos', 'MIP'),
|
|
||||||
|
|
||||||
-- Facultad de Negocios
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Contaduría y Finanzas', 'LCF'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Mercadotecnia', 'LMER'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Comercio y Negocios Internacionales', 'LCNI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Administración', 'LADM'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Tecnologías de Información', 'LTI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Dirección de Organizaciones de la Salud', 'MDOS'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Actuaría', 'LACT'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Especialidad en Mercadotecnia y Publicidad', 'EMP'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Especialidad en Finanzas Corporativas y Bursátiles', 'EFCB'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Especialidad en Logística y Cadena de Suministro', 'ELCS'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Especialidad en Administración de Organizaciones de la Salud', 'EAOS'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Ciencias Actuariales', 'MCA'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Doctorado en Administración', 'DADM'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Administración de Organizaciones de la Salud', 'MAOS'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Administración de Negocios Internacionales', 'MANI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Tecnologías de Información en la Dirección de Negocios', 'MTID'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Administración', 'MADM'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Ingeniería Económica y Financiera', 'MIEF'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Administración de Negocios Internacionales (MIEX)', 'MIEX'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Maestría en Gestión Estratégica del Capital Humano', 'MGEC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Administración e Innovación de Negocios', 'LAIN'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Ingeniería Económica y Financiera', 'LIEF'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Negocios' LIMIT 1), 'Licenciatura en Transformación Digital para los Negocios', 'LTDN'),
|
|
||||||
|
|
||||||
-- Facultad de Ingeniería
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Electrónica', 'LIE'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Civil', 'LIC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Mecánica y en Sistemas Energéticos', 'LIME'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Electrónica para Sistemas Inteligentes', 'LIES'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Mecatrónica', 'LIM'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Industrial', 'LII'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Cibernética y Sistemas Computacionales', 'LICS'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Licenciatura en Ingeniería Biomédica', 'LIB'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Maestría en Gerencia de Proyectos Inmobiliarios', 'MGPI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Maestría en Dirección Industrial', 'MDI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Especialidad en Dirección Industrial', 'EDI'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Maestría en Gestión de Proyectos y de Empresas Constructoras', 'MGPC'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Especialidad en Energías Renovables', 'EER'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Maestría en Ciberseguridad', 'MCIB'),
|
|
||||||
((SELECT id FROM public.facultades WHERE nombre = 'Facultad de Ingeniería' LIMIT 1), 'Especialidad en Inteligencia de Datos', 'EID');
|
|
||||||
Reference in New Issue
Block a user