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