465 lines
15 KiB
PL/PgSQL
465 lines
15 KiB
PL/PgSQL
-- ============================================
|
|
-- 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
|
|
-- ============================================
|
|
|