-- ============================================ -- SCHEMA BASE DE DATOS - SERVIDOR DE CONTENIDO TES -- ============================================ -- -- PostgreSQL en servidor propio -- Schema: tes_content -- -- @version 1.0.0 -- @date 2025-01-06 -- ============================================ -- Crear schema CREATE SCHEMA IF NOT EXISTS tes_content; -- ============================================ -- EXTENSIONES -- ============================================ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- Para búsqueda de texto -- ============================================ -- ENUMS -- ============================================ CREATE TYPE tes_content.content_type AS ENUM ( 'protocol', 'guide', 'manual', 'drug', 'checklist' ); CREATE TYPE tes_content.usage_type AS ENUM ( 'operativo', 'formativo', 'referencia' ); CREATE TYPE tes_content.priority AS ENUM ( 'critica', 'alta', 'media', 'baja' ); CREATE TYPE tes_content.content_status AS ENUM ( 'draft', 'in_review', 'approved', 'published', 'archived' ); CREATE TYPE tes_content.media_type AS ENUM ( 'image', 'video' ); CREATE TYPE tes_content.clinical_context AS ENUM ( 'RCP', 'OVACE', 'ABCDE', 'TRIAGE', 'GLASGOW', 'ICTUS', 'SHOCK', 'TRAUMA', 'OXIGENOTERAPIA', 'VIA_AEREA', 'FARMACOLOGIA', 'OTROS' ); CREATE TYPE tes_content.source_guideline AS ENUM ( 'ERC', 'SEMES', 'AHA', 'INTERNO', 'MANUAL_TES_DIGITAL' ); -- ============================================ -- TABLA: content_items -- ============================================ CREATE TABLE tes_content.content_items ( -- Identificación id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), type tes_content.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 tes_content.clinical_context NOT NULL, level tes_content.usage_type NOT NULL, -- 'operativo' | 'formativo' | 'referencia' priority tes_content.priority NOT NULL DEFAULT 'media', -- Estado y validación status tes_content.content_status NOT NULL DEFAULT 'draft', source_guideline tes_content.source_guideline NOT NULL, source_year INTEGER, source_url TEXT, -- Validación clínica validated_by UUID, validated_at TIMESTAMPTZ, validator_role TEXT, -- 'tes' | 'medico' | 'formador' 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 -- ============================================ CREATE TABLE tes_content.media_resources ( -- Identificación id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), type tes_content.media_type NOT NULL, -- Archivo (ruta local en servidor) path TEXT NOT NULL, -- Ruta relativa (ej: "/media/images/rcp/...") filename TEXT NOT NULL, -- Nombre del archivo file_url TEXT, -- URL pública (si está en CDN o servidor web) thumbnail_url TEXT, -- URL del thumbnail (vídeos) -- 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 tes_content.priority NOT NULL DEFAULT 'media', usage_type tes_content.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 tes_content.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 -- ============================================ CREATE TABLE tes_content.content_resource_associations ( -- Identificación id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), content_item_id UUID NOT NULL REFERENCES tes_content.content_items(id) ON DELETE CASCADE, media_resource_id UUID NOT NULL REFERENCES tes_content.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 tes_content.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 -- ============================================ CREATE TABLE tes_content.content_versions ( -- Identificación id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), content_item_id UUID NOT NULL REFERENCES tes_content.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 -- ============================================ CREATE TABLE tes_content.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 TEXT NOT NULL, -- 'create' | 'update' | 'delete' | 'validate' | 'approve' | 'publish' | 'archive' | 'revert' user_id UUID NOT NULL, user_role TEXT NOT NULL, -- 'tes' | 'medico' | 'formador' | 'editor' | 'admin' -- Metadatos metadata JSONB DEFAULT '{}', -- Timestamp timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================ -- TABLA: users (Panel Admin) -- ============================================ CREATE TABLE tes_content.users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), email TEXT UNIQUE NOT NULL, username TEXT NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'editor', -- 'super_admin' | 'editor_clinico' | 'editor_formativo' | 'revisor' | 'viewer' is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_login TIMESTAMPTZ ); -- ============================================ -- ÍNDICES -- ============================================ -- content_items CREATE INDEX idx_content_items_type ON tes_content.content_items(type); CREATE INDEX idx_content_items_level ON tes_content.content_items(level); CREATE INDEX idx_content_items_status ON tes_content.content_items(status); CREATE INDEX idx_content_items_priority ON tes_content.content_items(priority); CREATE INDEX idx_content_items_clinical_context ON tes_content.content_items(clinical_context); CREATE INDEX idx_content_items_slug ON tes_content.content_items(slug); CREATE INDEX idx_content_items_tags ON tes_content.content_items USING GIN(tags); CREATE INDEX idx_content_items_created_at ON tes_content.content_items(created_at DESC); CREATE INDEX idx_content_items_updated_at ON tes_content.content_items(updated_at DESC); CREATE INDEX idx_content_items_published ON tes_content.content_items(status, updated_at DESC) WHERE status = 'published'; -- Búsqueda de texto completo CREATE INDEX idx_content_items_search ON tes_content.content_items USING GIN( to_tsvector('spanish', COALESCE(title, '') || ' ' || COALESCE(description, '') || ' ' || COALESCE(short_title, '') ) ); -- media_resources CREATE INDEX idx_media_resources_type ON tes_content.media_resources(type); CREATE INDEX idx_media_resources_status ON tes_content.media_resources(status); CREATE INDEX idx_media_resources_priority ON tes_content.media_resources(priority); CREATE INDEX idx_media_resources_usage_type ON tes_content.media_resources USING GIN(usage_type); CREATE INDEX idx_media_resources_tags ON tes_content.media_resources USING GIN(tags); CREATE INDEX idx_media_resources_block ON tes_content.media_resources(block); CREATE INDEX idx_media_resources_uploaded_at ON tes_content.media_resources(uploaded_at DESC); -- content_resource_associations CREATE INDEX idx_associations_content_item ON tes_content.content_resource_associations(content_item_id); CREATE INDEX idx_associations_media_resource ON tes_content.content_resource_associations(media_resource_id); CREATE INDEX idx_associations_section ON tes_content.content_resource_associations(section); CREATE INDEX idx_associations_critical ON tes_content.content_resource_associations(is_critical) WHERE is_critical = true; -- content_versions CREATE INDEX idx_versions_content_item ON tes_content.content_versions(content_item_id); CREATE INDEX idx_versions_version ON tes_content.content_versions(version); CREATE INDEX idx_versions_active ON tes_content.content_versions(is_active) WHERE is_active = true; CREATE INDEX idx_versions_created_at ON tes_content.content_versions(created_at DESC); -- audit_logs CREATE INDEX idx_audit_entity_type_id ON tes_content.audit_logs(entity_type, entity_id); CREATE INDEX idx_audit_action ON tes_content.audit_logs(action); CREATE INDEX idx_audit_user_id ON tes_content.audit_logs(user_id); CREATE INDEX idx_audit_timestamp ON tes_content.audit_logs(timestamp DESC); -- users CREATE INDEX idx_users_email ON tes_content.users(email); CREATE INDEX idx_users_role ON tes_content.users(role); CREATE INDEX idx_users_is_active ON tes_content.users(is_active); -- ============================================ -- FUNCIONES Y TRIGGERS -- ============================================ -- Función para actualizar updated_at automáticamente CREATE OR REPLACE FUNCTION tes_content.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 tes_content.content_items FOR EACH ROW EXECUTE FUNCTION tes_content.update_updated_at_column(); CREATE TRIGGER update_media_resources_updated_at BEFORE UPDATE ON tes_content.media_resources FOR EACH ROW EXECUTE FUNCTION tes_content.update_updated_at_column(); CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON tes_content.users FOR EACH ROW EXECUTE FUNCTION tes_content.update_updated_at_column(); -- ============================================ -- VISTAS ÚTILES -- ============================================ -- Vista: Contenido publicado con recursos CREATE OR REPLACE VIEW tes_content.published_content_with_resources AS SELECT ci.id, ci.type, ci.slug, ci.title, ci.short_title, ci.clinical_context, ci.level, 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 tes_content.content_items ci LEFT JOIN tes_content.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 tes_content.unassociated_resources AS SELECT mr.id, mr.type, mr.title, mr.filename, mr.priority, mr.uploaded_at FROM tes_content.media_resources mr LEFT JOIN tes_content.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 tes_content.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 tes_content.content_items ci WHERE ci.status IN ('draft', 'in_review') ORDER BY ci.updated_at DESC; -- Vista: Estadísticas de contenido CREATE OR REPLACE VIEW tes_content.content_stats AS SELECT type, status, COUNT(*) as count, COUNT(CASE WHEN status = 'published' THEN 1 END) as published_count FROM tes_content.content_items GROUP BY type, status; -- ============================================ -- COMENTARIOS -- ============================================ COMMENT ON SCHEMA tes_content IS 'Schema dedicado para contenido del sistema TES'; COMMENT ON TABLE tes_content.content_items IS 'Tabla principal de contenido: protocolos, guías, manuales, fármacos, checklists'; COMMENT ON TABLE tes_content.media_resources IS 'Recursos multimedia: imágenes y vídeos almacenados en servidor'; COMMENT ON TABLE tes_content.content_resource_associations IS 'Asociación entre contenido y recursos multimedia'; COMMENT ON TABLE tes_content.content_versions IS 'Versiones históricas de contenido para versionado y rollback'; COMMENT ON TABLE tes_content.audit_logs IS 'Registro de auditoría de todas las acciones del sistema'; COMMENT ON TABLE tes_content.users IS 'Usuarios del panel de administración'; -- ============================================ -- PERMISOS (Ajustar según usuario de BD) -- ============================================ -- Ejemplo (ajustar usuario): -- GRANT ALL ON SCHEMA tes_content TO tu_usuario; -- GRANT ALL ON ALL TABLES IN SCHEMA tes_content TO tu_usuario; -- GRANT ALL ON ALL SEQUENCES IN SCHEMA tes_content TO tu_usuario; -- ============================================ -- FIN DEL SCHEMA -- ============================================