392 lines
14 KiB
SQL
392 lines
14 KiB
SQL
-- =====================================================
|
|
-- EMERGES TES - Esquema de Base de Datos
|
|
-- FASE 1: Infraestructura Base
|
|
-- =====================================================
|
|
--
|
|
-- Este script crea el esquema completo de base de datos
|
|
-- para el sistema de gestión de contenido de EMERGES TES.
|
|
--
|
|
-- IMPORTANTE: Ejecutar en orden, no modificar sin revisión.
|
|
-- =====================================================
|
|
|
|
-- Crear esquema principal
|
|
CREATE SCHEMA IF NOT EXISTS emerges_content;
|
|
|
|
-- Extensión para UUIDs
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
-- =====================================================
|
|
-- TABLA: content_items
|
|
-- Propósito: Almacena todos los tipos de contenido
|
|
-- =====================================================
|
|
CREATE TABLE IF NOT EXISTS emerges_content.content_items (
|
|
-- Identificación (inmutable)
|
|
id VARCHAR(100) PRIMARY KEY,
|
|
type VARCHAR(50) NOT NULL,
|
|
level VARCHAR(50) NOT NULL,
|
|
|
|
-- Contenido (editable)
|
|
title VARCHAR(500) NOT NULL,
|
|
short_title VARCHAR(200),
|
|
content JSONB NOT NULL,
|
|
content_markdown TEXT,
|
|
|
|
-- Metadatos de contenido
|
|
category VARCHAR(100),
|
|
subcategory VARCHAR(100),
|
|
priority VARCHAR(20),
|
|
age_group VARCHAR(20),
|
|
|
|
-- Versionado
|
|
version INTEGER NOT NULL DEFAULT 1,
|
|
latest_version INTEGER NOT NULL DEFAULT 1,
|
|
|
|
-- Estado y validación
|
|
status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
|
validated_by VARCHAR(100),
|
|
validated_at TIMESTAMP WITH TIME ZONE,
|
|
clinical_source VARCHAR(200),
|
|
quality_score INTEGER,
|
|
|
|
-- Revisión
|
|
reviewed_by VARCHAR(100),
|
|
reviewed_at TIMESTAMP WITH TIME ZONE,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
created_by VARCHAR(100) NOT NULL,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
updated_by VARCHAR(100) NOT NULL,
|
|
|
|
-- Constraints
|
|
CONSTRAINT chk_type CHECK (type IN ('protocol', 'guide', 'manual', 'drug')),
|
|
CONSTRAINT chk_level CHECK (level IN ('operativo', 'formativo', 'referencia')),
|
|
CONSTRAINT chk_status CHECK (status IN ('draft', 'pending', 'validated', 'rejected', 'archived')),
|
|
CONSTRAINT chk_priority CHECK (priority IN ('critico', 'alto', 'medio', 'bajo') OR priority IS NULL),
|
|
CONSTRAINT chk_quality_score CHECK (quality_score IS NULL OR (quality_score >= 0 AND quality_score <= 100))
|
|
);
|
|
|
|
-- Índices para content_items
|
|
CREATE INDEX IF NOT EXISTS idx_content_items_type ON emerges_content.content_items(type);
|
|
CREATE INDEX IF NOT EXISTS idx_content_items_level ON emerges_content.content_items(level);
|
|
CREATE INDEX IF NOT EXISTS idx_content_items_status ON emerges_content.content_items(status);
|
|
CREATE INDEX IF NOT EXISTS idx_content_items_category ON emerges_content.content_items(category);
|
|
CREATE INDEX IF NOT EXISTS idx_content_items_validated_at ON emerges_content.content_items(validated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_content_items_updated_at ON emerges_content.content_items(updated_at);
|
|
CREATE INDEX IF NOT EXISTS idx_content_items_content_gin ON emerges_content.content_items USING GIN (content);
|
|
CREATE INDEX IF NOT EXISTS idx_content_items_title_fts ON emerges_content.content_items USING GIN (to_tsvector('spanish', title));
|
|
|
|
-- =====================================================
|
|
-- TABLA: content_versions
|
|
-- Propósito: Historial de versiones
|
|
-- =====================================================
|
|
CREATE TABLE IF NOT EXISTS emerges_content.content_versions (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
content_id VARCHAR(100) NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
|
|
-- Snapshot del contenido
|
|
content JSONB NOT NULL,
|
|
content_markdown TEXT,
|
|
title VARCHAR(500) NOT NULL,
|
|
|
|
-- Metadatos de la versión
|
|
status VARCHAR(20) NOT NULL,
|
|
validated_by VARCHAR(100),
|
|
validated_at TIMESTAMP WITH TIME ZONE,
|
|
clinical_source VARCHAR(200),
|
|
|
|
-- Cambios
|
|
change_summary TEXT,
|
|
changed_fields TEXT[],
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
created_by VARCHAR(100) NOT NULL,
|
|
|
|
-- Constraints
|
|
CONSTRAINT fk_content_versions_content_id
|
|
FOREIGN KEY (content_id)
|
|
REFERENCES emerges_content.content_items(id)
|
|
ON DELETE CASCADE,
|
|
CONSTRAINT chk_version_status CHECK (status IN ('draft', 'pending', 'validated', 'rejected', 'archived')),
|
|
CONSTRAINT uq_content_versions_content_version UNIQUE (content_id, version)
|
|
);
|
|
|
|
-- Índices para content_versions
|
|
CREATE INDEX IF NOT EXISTS idx_content_versions_content_id ON emerges_content.content_versions(content_id);
|
|
CREATE INDEX IF NOT EXISTS idx_content_versions_version ON emerges_content.content_versions(version);
|
|
CREATE INDEX IF NOT EXISTS idx_content_versions_created_at ON emerges_content.content_versions(created_at);
|
|
|
|
-- =====================================================
|
|
-- TABLA: content_relations
|
|
-- Propósito: Relaciones entre contenidos
|
|
-- =====================================================
|
|
CREATE TABLE IF NOT EXISTS emerges_content.content_relations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
source_id VARCHAR(100) NOT NULL,
|
|
target_id VARCHAR(100) NOT NULL,
|
|
relation_type VARCHAR(50) NOT NULL,
|
|
|
|
-- Metadatos
|
|
is_primary BOOLEAN DEFAULT false,
|
|
order_index INTEGER,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
created_by VARCHAR(100) NOT NULL,
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT fk_content_relations_source
|
|
FOREIGN KEY (source_id)
|
|
REFERENCES emerges_content.content_items(id)
|
|
ON DELETE CASCADE,
|
|
CONSTRAINT fk_content_relations_target
|
|
FOREIGN KEY (target_id)
|
|
REFERENCES emerges_content.content_items(id)
|
|
ON DELETE CASCADE,
|
|
CONSTRAINT chk_relation_type CHECK (relation_type IN ('has_guide', 'has_manual', 'references', 'related_to')),
|
|
CONSTRAINT uq_content_relations_source_target_type UNIQUE (source_id, target_id, relation_type)
|
|
);
|
|
|
|
-- Índices para content_relations
|
|
CREATE INDEX IF NOT EXISTS idx_content_relations_source ON emerges_content.content_relations(source_id);
|
|
CREATE INDEX IF NOT EXISTS idx_content_relations_target ON emerges_content.content_relations(target_id);
|
|
CREATE INDEX IF NOT EXISTS idx_content_relations_type ON emerges_content.content_relations(relation_type);
|
|
|
|
-- =====================================================
|
|
-- TABLA: content_change_log
|
|
-- Propósito: Log de auditoría
|
|
-- =====================================================
|
|
CREATE TABLE IF NOT EXISTS emerges_content.content_change_log (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
content_id VARCHAR(100) NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
|
|
-- Cambio
|
|
action VARCHAR(20) NOT NULL,
|
|
field_path VARCHAR(200),
|
|
old_value JSONB,
|
|
new_value JSONB,
|
|
|
|
-- Contexto
|
|
change_reason TEXT,
|
|
comment TEXT,
|
|
|
|
-- Auditoría
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
created_by VARCHAR(100) NOT NULL,
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
|
|
-- Constraints
|
|
CONSTRAINT fk_content_change_log_content
|
|
FOREIGN KEY (content_id)
|
|
REFERENCES emerges_content.content_items(id)
|
|
ON DELETE CASCADE,
|
|
CONSTRAINT chk_change_log_action CHECK (action IN ('create', 'update', 'delete', 'validate', 'reject', 'archive'))
|
|
);
|
|
|
|
-- Índices para content_change_log
|
|
CREATE INDEX IF NOT EXISTS idx_content_change_log_content_id ON emerges_content.content_change_log(content_id);
|
|
CREATE INDEX IF NOT EXISTS idx_content_change_log_version ON emerges_content.content_change_log(version);
|
|
CREATE INDEX IF NOT EXISTS idx_content_change_log_created_at ON emerges_content.content_change_log(created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_content_change_log_created_by ON emerges_content.content_change_log(created_by);
|
|
|
|
-- =====================================================
|
|
-- TABLA: users
|
|
-- Propósito: Usuarios del panel administrador
|
|
-- =====================================================
|
|
CREATE TABLE IF NOT EXISTS emerges_content.users (
|
|
id VARCHAR(100) PRIMARY KEY,
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
role VARCHAR(50) NOT NULL,
|
|
|
|
-- Autenticación
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
salt VARCHAR(255) NOT NULL,
|
|
|
|
-- Estado
|
|
is_active BOOLEAN DEFAULT true,
|
|
last_login TIMESTAMP WITH TIME ZONE,
|
|
|
|
-- Metadatos
|
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
created_by VARCHAR(100),
|
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT chk_user_role CHECK (role IN ('admin_clinico', 'editor_docente', 'editor_clinico', 'revisor'))
|
|
);
|
|
|
|
-- Compatibilidad con esquemas previos de users
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'emerges_content'
|
|
AND table_name = 'users'
|
|
) THEN
|
|
-- Renombrar username -> name si aplica
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'emerges_content'
|
|
AND table_name = 'users'
|
|
AND column_name = 'username'
|
|
) AND NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'emerges_content'
|
|
AND table_name = 'users'
|
|
AND column_name = 'name'
|
|
) THEN
|
|
ALTER TABLE emerges_content.users RENAME COLUMN username TO name;
|
|
END IF;
|
|
|
|
-- Columnas faltantes
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'emerges_content'
|
|
AND table_name = 'users'
|
|
AND column_name = 'name'
|
|
) THEN
|
|
ALTER TABLE emerges_content.users ADD COLUMN name VARCHAR(200) NOT NULL DEFAULT '';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'emerges_content'
|
|
AND table_name = 'users'
|
|
AND column_name = 'salt'
|
|
) THEN
|
|
ALTER TABLE emerges_content.users ADD COLUMN salt VARCHAR(255) NOT NULL DEFAULT '';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'emerges_content'
|
|
AND table_name = 'users'
|
|
AND column_name = 'created_by'
|
|
) THEN
|
|
ALTER TABLE emerges_content.users ADD COLUMN created_by VARCHAR(100);
|
|
END IF;
|
|
|
|
-- Ajustar tipos si venimos de esquema anterior
|
|
BEGIN
|
|
ALTER TABLE emerges_content.users
|
|
ALTER COLUMN id TYPE VARCHAR(100) USING id::text;
|
|
EXCEPTION WHEN others THEN
|
|
NULL;
|
|
END;
|
|
|
|
BEGIN
|
|
ALTER TABLE emerges_content.users
|
|
ALTER COLUMN email TYPE VARCHAR(255);
|
|
EXCEPTION WHEN others THEN
|
|
NULL;
|
|
END;
|
|
|
|
BEGIN
|
|
ALTER TABLE emerges_content.users
|
|
ALTER COLUMN role TYPE VARCHAR(50);
|
|
EXCEPTION WHEN others THEN
|
|
NULL;
|
|
END;
|
|
|
|
BEGIN
|
|
ALTER TABLE emerges_content.users
|
|
ALTER COLUMN password_hash TYPE VARCHAR(255);
|
|
EXCEPTION WHEN others THEN
|
|
NULL;
|
|
END;
|
|
|
|
ALTER TABLE emerges_content.users
|
|
ALTER COLUMN is_active SET DEFAULT true;
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Índices para users
|
|
CREATE INDEX IF NOT EXISTS idx_users_email ON emerges_content.users(email);
|
|
CREATE INDEX IF NOT EXISTS idx_users_role ON emerges_content.users(role);
|
|
CREATE INDEX IF NOT EXISTS idx_users_active ON emerges_content.users(is_active);
|
|
|
|
-- =====================================================
|
|
-- TABLA: content_sync_status
|
|
-- Propósito: Estado de sincronización
|
|
-- =====================================================
|
|
CREATE TABLE IF NOT EXISTS emerges_content.content_sync_status (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
content_id VARCHAR(100) NOT NULL,
|
|
version INTEGER NOT NULL,
|
|
|
|
-- Sincronización
|
|
synced_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
|
sync_count INTEGER DEFAULT 1,
|
|
|
|
-- Constraints
|
|
CONSTRAINT fk_content_sync_status_content
|
|
FOREIGN KEY (content_id)
|
|
REFERENCES emerges_content.content_items(id)
|
|
ON DELETE CASCADE,
|
|
CONSTRAINT uq_content_sync_status_content_version UNIQUE (content_id, version)
|
|
);
|
|
|
|
-- Índices para content_sync_status
|
|
CREATE INDEX IF NOT EXISTS idx_content_sync_status_content_id ON emerges_content.content_sync_status(content_id);
|
|
CREATE INDEX IF NOT EXISTS idx_content_sync_status_synced_at ON emerges_content.content_sync_status(synced_at);
|
|
|
|
-- =====================================================
|
|
-- VISTAS ÚTILES
|
|
-- =====================================================
|
|
|
|
-- Vista: Contenido validado (solo lectura pública)
|
|
CREATE OR REPLACE VIEW emerges_content.v_validated_content AS
|
|
SELECT
|
|
id,
|
|
type,
|
|
level,
|
|
title,
|
|
short_title,
|
|
content,
|
|
content_markdown,
|
|
category,
|
|
subcategory,
|
|
priority,
|
|
age_group,
|
|
version,
|
|
validated_at,
|
|
clinical_source,
|
|
updated_at
|
|
FROM emerges_content.content_items
|
|
WHERE status = 'validated'
|
|
ORDER BY updated_at DESC;
|
|
|
|
-- Vista: Contenido pendiente de validación
|
|
CREATE OR REPLACE VIEW emerges_content.v_pending_validation AS
|
|
SELECT
|
|
ci.*,
|
|
u.name as created_by_name,
|
|
u.email as created_by_email
|
|
FROM emerges_content.content_items ci
|
|
LEFT JOIN emerges_content.users u ON ci.created_by = u.id
|
|
WHERE ci.status = 'pending'
|
|
ORDER BY ci.updated_at ASC;
|
|
|
|
-- =====================================================
|
|
-- COMENTARIOS EN TABLAS
|
|
-- =====================================================
|
|
COMMENT ON TABLE emerges_content.content_items IS 'Almacena todos los tipos de contenido (protocolos, guías, manual, fármacos)';
|
|
COMMENT ON TABLE emerges_content.content_versions IS 'Historial completo de versiones de cada contenido';
|
|
COMMENT ON TABLE emerges_content.content_relations IS 'Relaciones entre protocolos, guías y manual';
|
|
COMMENT ON TABLE emerges_content.content_change_log IS 'Log detallado de todos los cambios para auditoría';
|
|
COMMENT ON TABLE emerges_content.users IS 'Usuarios del panel administrador con roles y permisos';
|
|
COMMENT ON TABLE emerges_content.content_sync_status IS 'Trackea qué versiones han sido sincronizadas con las apps';
|
|
|