codigo0/docs/SUPABASE_SCHEMA.sql

509 lines
14 KiB
PL/PgSQL

-- ============================================
-- SUPABASE SCHEMA - SISTEMA DE CONTENIDO TES
-- ============================================
--
-- FASE 4: Base de Contenido Canónica
--
-- Este schema define la estructura completa de la base de datos
-- para gestionar contenido externo (protocolos, guías, manuales, fármacos, recursos)
--
-- Diseñado para:
-- - Durabilidad (10+ años)
-- - Uso real de TES en guardia
-- - Formación continua
-- - Referencia profesional
--
-- @version 1.0.0
-- @date 2025-01-06
-- ============================================
-- ============================================
-- EXTENSIONES
-- ============================================
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Para búsqueda de texto
-- ============================================
-- ENUMS
-- ============================================
-- Tipo de contenido
CREATE TYPE content_type AS ENUM (
'protocol',
'guide',
'manual',
'drug',
'checklist'
);
-- Tipo de uso
CREATE TYPE usage_type AS ENUM (
'operativo',
'formativo',
'referencia'
);
-- Prioridad
CREATE TYPE priority AS ENUM (
'critica',
'alta',
'media',
'baja'
);
-- Estado del contenido
CREATE TYPE content_status AS ENUM (
'draft',
'in_review',
'approved',
'published',
'archived'
);
-- Tipo de recurso multimedia
CREATE TYPE media_type AS ENUM (
'image',
'video'
);
-- Contexto clínico
CREATE TYPE clinical_context AS ENUM (
'RCP',
'OVACE',
'ABCDE',
'TRIAGE',
'GLASGOW',
'ICTUS',
'SHOCK',
'TRAUMA',
'OXIGENOTERAPIA',
'VIA_AEREA',
'FARMACOLOGIA',
'OTROS'
);
-- Fuente de guía clínica
CREATE TYPE source_guideline AS ENUM (
'ERC',
'SEMES',
'AHA',
'INTERNO',
'MANUAL_TES_DIGITAL'
);
-- Acción de auditoría
CREATE TYPE audit_action AS ENUM (
'create',
'update',
'delete',
'validate',
'approve',
'publish',
'archive',
'revert'
);
-- Rol de usuario
CREATE TYPE user_role AS ENUM (
'tes',
'medico',
'formador',
'editor',
'admin'
);
-- ============================================
-- TABLA: content_items
-- ============================================
--
-- Tabla principal que almacena todo el contenido del sistema:
-- protocolos, guías, manuales, fármacos, checklists
--
CREATE TABLE content_items (
-- Identificación
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
type content_type NOT NULL,
slug TEXT UNIQUE NOT NULL,
-- Metadatos básicos
title TEXT NOT NULL,
short_title TEXT,
description TEXT,
-- Clasificación clínica
clinical_context clinical_context NOT NULL,
usage_type usage_type NOT NULL,
priority priority NOT NULL DEFAULT 'media',
-- Estado y validación
status content_status NOT NULL DEFAULT 'draft',
source_guideline source_guideline NOT NULL,
source_year INTEGER,
source_url TEXT,
-- Validación clínica
validated_by UUID,
validated_at TIMESTAMPTZ,
validator_role user_role,
validation_expires_at TIMESTAMPTZ,
-- Versionado
version TEXT NOT NULL DEFAULT '1.0.0',
latest_version TEXT NOT NULL DEFAULT '1.0.0',
-- Contenido específico (JSONB flexible)
content JSONB NOT NULL,
-- Relaciones
related_content_ids UUID[] DEFAULT '{}',
related_protocol_ids UUID[] DEFAULT '{}',
related_guide_ids UUID[] DEFAULT '{}',
related_manual_ids UUID[] DEFAULT '{}',
-- Tags y categorización
tags TEXT[] DEFAULT '{}',
category TEXT,
-- Auditoría
created_by UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by UUID,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Metadatos adicionales
metadata JSONB DEFAULT '{}',
-- Constraints
CONSTRAINT valid_version_format CHECK (version ~ '^\d+\.\d+\.\d+$'),
CONSTRAINT valid_latest_version_format CHECK (latest_version ~ '^\d+\.\d+\.\d+$')
);
-- ============================================
-- TABLA: media_resources
-- ============================================
--
-- Recursos multimedia (imágenes y vídeos)
-- Almacenados en Supabase Storage, referenciados por URL
--
CREATE TABLE media_resources (
-- Identificación
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
type media_type NOT NULL,
-- Archivo
file_url TEXT NOT NULL, -- URL completa (Supabase Storage)
thumbnail_url TEXT, -- URL del thumbnail (vídeos)
filename TEXT NOT NULL, -- Nombre del archivo original
path TEXT NOT NULL, -- Ruta relativa
-- Metadatos
title TEXT NOT NULL,
description TEXT,
alt_text TEXT NOT NULL, -- Texto alternativo (accesibilidad)
caption TEXT,
-- Clasificación
tags TEXT[] DEFAULT '{}',
block TEXT, -- Bloque temático
chapter TEXT, -- Capítulo relacionado
priority priority NOT NULL DEFAULT 'media',
usage_type usage_type[] DEFAULT '{}', -- ['operativo', 'formativo']
-- Dimensiones (imágenes)
width INTEGER,
height INTEGER,
format TEXT, -- 'png' | 'svg' | 'jpg' | 'webp'
file_size BIGINT, -- Tamaño en bytes
-- Duración (vídeos)
duration_seconds INTEGER, -- Duración en segundos
video_format TEXT, -- 'mp4' | 'webm'
-- Fuente
source TEXT,
attribution TEXT,
-- Estado
status content_status NOT NULL DEFAULT 'draft',
-- Auditoría
uploaded_by UUID NOT NULL,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- TABLA: content_resource_associations
-- ============================================
--
-- Asociación entre contenido y recursos multimedia
-- Define dónde y cómo se muestra un recurso en un contenido
--
CREATE TABLE content_resource_associations (
-- Identificación
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content_item_id UUID NOT NULL REFERENCES content_items(id) ON DELETE CASCADE,
media_resource_id UUID NOT NULL REFERENCES media_resources(id) ON DELETE CASCADE,
-- Contexto de asociación
section TEXT, -- Sección específica (ej: "pasos", "checklist", "guia_seccion_3")
position INTEGER, -- Posición en el contenido (orden)
placement TEXT DEFAULT 'inline', -- 'inline' | 'before' | 'after' | 'modal'
caption TEXT, -- Caption específico para este contexto
-- Metadatos
is_critical BOOLEAN NOT NULL DEFAULT false,
priority priority NOT NULL DEFAULT 'media',
-- Auditoría
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT unique_association UNIQUE (content_item_id, media_resource_id, section, position)
);
-- ============================================
-- TABLA: content_versions
-- ============================================
--
-- Versiones históricas de ContentItems
-- Permite versionado semántico y rollback
--
CREATE TABLE content_versions (
-- Identificación
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content_item_id UUID NOT NULL REFERENCES content_items(id) ON DELETE CASCADE,
version TEXT NOT NULL,
-- Contenido
content JSONB NOT NULL,
-- Metadatos
change_summary TEXT NOT NULL,
is_breaking BOOLEAN DEFAULT false,
created_by UUID NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Estado
is_active BOOLEAN NOT NULL DEFAULT false,
-- Constraints
CONSTRAINT valid_version_format CHECK (version ~ '^\d+\.\d+\.\d+$'),
CONSTRAINT unique_content_version UNIQUE (content_item_id, version)
);
-- ============================================
-- TABLA: audit_logs
-- ============================================
--
-- Registro de auditoría de todas las acciones
-- Trazabilidad completa de cambios
--
CREATE TABLE audit_logs (
-- Identificación
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
entity_type TEXT NOT NULL, -- 'content_item' | 'media_resource' | 'association' | 'version'
entity_id UUID NOT NULL,
-- Acción
action audit_action NOT NULL,
user_id UUID NOT NULL,
user_role user_role NOT NULL,
-- Metadatos
metadata JSONB DEFAULT '{}',
-- Timestamp
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================
-- ÍNDICES
-- ============================================
-- Índices para content_items
CREATE INDEX idx_content_items_type ON content_items(type);
CREATE INDEX idx_content_items_usage_type ON content_items(usage_type);
CREATE INDEX idx_content_items_status ON content_items(status);
CREATE INDEX idx_content_items_priority ON content_items(priority);
CREATE INDEX idx_content_items_clinical_context ON content_items(clinical_context);
CREATE INDEX idx_content_items_slug ON content_items(slug);
CREATE INDEX idx_content_items_tags ON content_items USING GIN(tags);
CREATE INDEX idx_content_items_created_at ON content_items(created_at DESC);
CREATE INDEX idx_content_items_updated_at ON content_items(updated_at DESC);
-- Índice de búsqueda de texto completo
CREATE INDEX idx_content_items_search ON content_items USING GIN(
to_tsvector('spanish',
COALESCE(title, '') || ' ' ||
COALESCE(description, '') || ' ' ||
COALESCE(short_title, '')
)
);
-- Índices para media_resources
CREATE INDEX idx_media_resources_type ON media_resources(type);
CREATE INDEX idx_media_resources_status ON media_resources(status);
CREATE INDEX idx_media_resources_priority ON media_resources(priority);
CREATE INDEX idx_media_resources_usage_type ON media_resources USING GIN(usage_type);
CREATE INDEX idx_media_resources_tags ON media_resources USING GIN(tags);
CREATE INDEX idx_media_resources_block ON media_resources(block);
CREATE INDEX idx_media_resources_uploaded_at ON media_resources(uploaded_at DESC);
-- Índices para content_resource_associations
CREATE INDEX idx_associations_content_item ON content_resource_associations(content_item_id);
CREATE INDEX idx_associations_media_resource ON content_resource_associations(media_resource_id);
CREATE INDEX idx_associations_section ON content_resource_associations(section);
CREATE INDEX idx_associations_critical ON content_resource_associations(is_critical) WHERE is_critical = true;
-- Índices para content_versions
CREATE INDEX idx_versions_content_item ON content_versions(content_item_id);
CREATE INDEX idx_versions_version ON content_versions(version);
CREATE INDEX idx_versions_active ON content_versions(is_active) WHERE is_active = true;
CREATE INDEX idx_versions_created_at ON content_versions(created_at DESC);
-- Índices para audit_logs
CREATE INDEX idx_audit_entity_type_id ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_action ON audit_logs(action);
CREATE INDEX idx_audit_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_timestamp ON audit_logs(timestamp DESC);
CREATE INDEX idx_audit_user_role ON audit_logs(user_role);
-- ============================================
-- FUNCIONES Y TRIGGERS
-- ============================================
-- Función para actualizar updated_at automáticamente
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Triggers para updated_at
CREATE TRIGGER update_content_items_updated_at
BEFORE UPDATE ON content_items
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_media_resources_updated_at
BEFORE UPDATE ON media_resources
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Función para crear audit log automáticamente
CREATE OR REPLACE FUNCTION create_audit_log()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (
entity_type,
entity_id,
action,
user_id,
user_role,
metadata
) VALUES (
TG_TABLE_NAME,
COALESCE(NEW.id, OLD.id),
CASE
WHEN TG_OP = 'INSERT' THEN 'create'
WHEN TG_OP = 'UPDATE' THEN 'update'
WHEN TG_OP = 'DELETE' THEN 'delete'
END,
COALESCE(NEW.updated_by, NEW.created_by, 'system'::UUID),
'editor', -- Por defecto, se puede mejorar con contexto
jsonb_build_object(
'old', to_jsonb(OLD),
'new', to_jsonb(NEW)
)
);
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- Triggers para audit logs (opcional, puede ser pesado)
-- Descomentar si se necesita auditoría automática completa
-- CREATE TRIGGER audit_content_items
-- AFTER INSERT OR UPDATE OR DELETE ON content_items
-- FOR EACH ROW
-- EXECUTE FUNCTION create_audit_log();
-- ============================================
-- VISTAS ÚTILES
-- ============================================
-- Vista: Contenido publicado con recursos
CREATE OR REPLACE VIEW published_content_with_resources AS
SELECT
ci.id,
ci.type,
ci.slug,
ci.title,
ci.short_title,
ci.clinical_context,
ci.usage_type,
ci.priority,
ci.status,
ci.version,
ci.created_at,
ci.updated_at,
COUNT(DISTINCT cra.id) as resource_count,
COUNT(DISTINCT CASE WHEN cra.is_critical = true THEN cra.id END) as critical_resource_count
FROM content_items ci
LEFT JOIN content_resource_associations cra ON ci.id = cra.content_item_id
WHERE ci.status = 'published'
GROUP BY ci.id;
-- Vista: Recursos sin asociar
CREATE OR REPLACE VIEW unassociated_resources AS
SELECT
mr.id,
mr.type,
mr.title,
mr.filename,
mr.priority,
mr.uploaded_at
FROM media_resources mr
LEFT JOIN content_resource_associations cra ON mr.id = cra.media_resource_id
WHERE cra.id IS NULL
AND mr.status = 'published';
-- Vista: Contenido pendiente de validación
CREATE OR REPLACE VIEW content_pending_validation AS
SELECT
ci.id,
ci.type,
ci.title,
ci.clinical_context,
ci.status,
ci.created_at,
ci.updated_at,
EXTRACT(EPOCH FROM (NOW() - ci.updated_at)) / 86400 as days_since_update
FROM content_items ci
WHERE ci.status IN ('draft', 'in_review')
ORDER BY ci.updated_at DESC;
-- ============================================
-- COMENTARIOS
-- ============================================
COMMENT ON TABLE content_items IS 'Tabla principal de contenido: protocolos, guías, manuales, fármacos, checklists';
COMMENT ON TABLE media_resources IS 'Recursos multimedia: imágenes y vídeos almacenados en Supabase Storage';
COMMENT ON TABLE content_resource_associations IS 'Asociación entre contenido y recursos multimedia';
COMMENT ON TABLE content_versions IS 'Versiones históricas de contenido para versionado y rollback';
COMMENT ON TABLE audit_logs IS 'Registro de auditoría de todas las acciones del sistema';
-- ============================================
-- FIN DEL SCHEMA
-- ============================================