codigo0/database/migrations/001_create_schema.sql
planetazuzu 0201f16cf4
Some checks are pending
Auto Deploy to Server / deploy (push) Waiting to run
Update lab configuration 2026-03-22
2026-03-22 22:50:29 +01:00

392 lines
14 KiB
SQL
Executable file

-- =====================================================
-- 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';