chore(db): actualizar esquema/backups (auditoría de planes, hashes y conversation_id)
- Agrega fn_log_cambios_planes_estudio y trigger para registrar cambios en cambios_plan, usando response_id (reemplaza interaccion_ia_id). - Extiende planes_estudio con conversation_id (unique) y plan_hash; agrega asignatura_hash en asignaturas; corrige unaccent_immutable y añade extensión http. - Actualiza seeds/backups (data_backup.sql / scheme_backup.sql) y ajusta script de diff (sql_para_local_como_remoto.sql), elimina el diff viejo y actualiza archivos auxiliares (deno.lock, CLI temp).
This commit is contained in:
+260
-14
@@ -30,6 +30,13 @@ COMMENT ON SCHEMA "public" IS 'standard public schema';
|
||||
|
||||
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "http" WITH SCHEMA "extensions";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql";
|
||||
|
||||
|
||||
@@ -159,7 +166,8 @@ CREATE TYPE "public"."tipo_cambio" AS ENUM (
|
||||
'ACTUALIZACION_MAPA',
|
||||
'TRANSICION_ESTADO',
|
||||
'OTRO',
|
||||
'CREACION'
|
||||
'CREACION',
|
||||
'ACTUALIZACION'
|
||||
);
|
||||
|
||||
|
||||
@@ -230,6 +238,172 @@ CREATE TYPE "public"."tipo_origen" AS ENUM (
|
||||
ALTER TYPE "public"."tipo_origen" OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."fn_log_cambios_planes_estudio"() RETURNS "trigger"
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$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;$$;
|
||||
|
||||
|
||||
ALTER FUNCTION "public"."fn_log_cambios_planes_estudio"() OWNER TO "postgres";
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION "public"."set_actualizado_en"() RETURNS "trigger"
|
||||
LANGUAGE "plpgsql"
|
||||
AS $$
|
||||
@@ -246,7 +420,7 @@ ALTER FUNCTION "public"."set_actualizado_en"() OWNER TO "postgres";
|
||||
CREATE OR REPLACE FUNCTION "public"."unaccent_immutable"("text") RETURNS "text"
|
||||
LANGUAGE "sql" IMMUTABLE STRICT PARALLEL SAFE
|
||||
AS $_$
|
||||
SELECT unaccent('unaccent', $1);
|
||||
SELECT public.unaccent('public.unaccent', $1);
|
||||
$_$;
|
||||
|
||||
|
||||
@@ -313,7 +487,6 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"plan_estudio_id" "uuid" NOT NULL,
|
||||
"estructura_id" "uuid",
|
||||
"facultad_propietaria_id" "uuid",
|
||||
"codigo" "text",
|
||||
"nombre" "text" NOT NULL,
|
||||
"tipo" "public"."tipo_asignatura" DEFAULT 'OBLIGATORIA'::"public"."tipo_asignatura" NOT NULL,
|
||||
@@ -330,6 +503,7 @@ CREATE TABLE IF NOT EXISTS "public"."asignaturas" (
|
||||
"actualizado_por" "uuid",
|
||||
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"actualizado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"asignatura_hash" "text" GENERATED ALWAYS AS ("encode"(SUBSTRING("extensions"."digest"(("id")::"text", 'sha512'::"text") FROM 1 FOR 12), 'hex'::"text")) STORED,
|
||||
CONSTRAINT "asignaturas_ciclo_chk" CHECK ((("numero_ciclo" IS NULL) OR ("numero_ciclo" > 0))),
|
||||
CONSTRAINT "asignaturas_creditos_check" CHECK (("creditos" >= (0)::numeric)),
|
||||
CONSTRAINT "asignaturas_horas_semana_check" CHECK ((("horas_semana" IS NULL) OR ("horas_semana" >= 0))),
|
||||
@@ -382,13 +556,17 @@ CREATE TABLE IF NOT EXISTS "public"."cambios_plan" (
|
||||
"campo" "text",
|
||||
"valor_anterior" "jsonb",
|
||||
"valor_nuevo" "jsonb",
|
||||
"interaccion_ia_id" "uuid"
|
||||
"response_id" "text"
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE "public"."cambios_plan" OWNER TO "postgres";
|
||||
|
||||
|
||||
COMMENT ON COLUMN "public"."cambios_plan"."response_id" IS 'El ID de respuesta de OpenAI';
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "public"."carreras" (
|
||||
"id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL,
|
||||
"facultad_id" "uuid" NOT NULL,
|
||||
@@ -525,6 +703,8 @@ CREATE TABLE IF NOT EXISTS "public"."planes_estudio" (
|
||||
"creado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"actualizado_en" timestamp with time zone DEFAULT "now"() NOT NULL,
|
||||
"nombre_search" "text" GENERATED ALWAYS AS ("lower"("public"."unaccent_immutable"("nombre"))) STORED,
|
||||
"plan_hash" "text" GENERATED ALWAYS AS ("encode"(SUBSTRING("extensions"."digest"(("id")::"text", 'sha512'::"text") FROM 1 FOR 12), 'hex'::"text")) STORED,
|
||||
"conversation_id" "text",
|
||||
CONSTRAINT "planes_estudio_numero_ciclos_check" CHECK (("numero_ciclos" > 0))
|
||||
);
|
||||
|
||||
@@ -532,6 +712,10 @@ CREATE TABLE IF NOT EXISTS "public"."planes_estudio" (
|
||||
ALTER TABLE "public"."planes_estudio" OWNER TO "postgres";
|
||||
|
||||
|
||||
COMMENT ON COLUMN "public"."planes_estudio"."conversation_id" IS 'ID de conversación de OpenAI';
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE VIEW "public"."plantilla_plan" AS
|
||||
SELECT "plan"."id" AS "plan_estudio_id",
|
||||
"struct"."id" AS "estructura_id",
|
||||
@@ -714,6 +898,11 @@ ALTER TABLE ONLY "public"."notificaciones"
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."planes_estudio"
|
||||
ADD CONSTRAINT "planes_estudio_conversation_id_key" UNIQUE ("conversation_id");
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."planes_estudio"
|
||||
ADD CONSTRAINT "planes_estudio_pkey" PRIMARY KEY ("id");
|
||||
|
||||
@@ -826,6 +1015,10 @@ CREATE OR REPLACE TRIGGER "trg_planes_estudio_actualizado_en" BEFORE UPDATE ON "
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE 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"();
|
||||
|
||||
|
||||
|
||||
CREATE OR REPLACE TRIGGER "trg_usuarios_app_actualizado_en" BEFORE UPDATE ON "public"."usuarios_app" FOR EACH ROW EXECUTE FUNCTION "public"."set_actualizado_en"();
|
||||
|
||||
|
||||
@@ -854,11 +1047,6 @@ ALTER TABLE ONLY "public"."asignaturas"
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."asignaturas"
|
||||
ADD CONSTRAINT "asignaturas_facultad_propietaria_id_fkey" FOREIGN KEY ("facultad_propietaria_id") REFERENCES "public"."facultades"("id") ON DELETE SET NULL;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."asignaturas"
|
||||
ADD CONSTRAINT "asignaturas_linea_plan_fk_compuesta" FOREIGN KEY ("linea_plan_id", "plan_estudio_id") REFERENCES "public"."lineas_plan"("id", "plan_estudio_id") ON DELETE SET NULL;
|
||||
|
||||
@@ -894,11 +1082,6 @@ ALTER TABLE ONLY "public"."cambios_plan"
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."cambios_plan"
|
||||
ADD CONSTRAINT "cambios_plan_plan_estudio_id_fkey" FOREIGN KEY ("plan_estudio_id") REFERENCES "public"."planes_estudio"("id") ON DELETE CASCADE;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."carreras"
|
||||
ADD CONSTRAINT "carreras_facultad_id_fkey" FOREIGN KEY ("facultad_id") REFERENCES "public"."facultades"("id") ON DELETE RESTRICT;
|
||||
|
||||
@@ -1220,6 +1403,69 @@ GRANT USAGE ON SCHEMA "public" TO "service_role";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "anon";
|
||||
GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "authenticated";
|
||||
GRANT ALL ON FUNCTION "public"."fn_log_cambios_planes_estudio"() TO "service_role";
|
||||
|
||||
|
||||
|
||||
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 "service_role";
|
||||
|
||||
Reference in New Issue
Block a user