/** * Script de migración COMPLETA: Importa TODO el contenido de la app al backend * * Migra: * - Todos los procedimientos (procedures.ts) * - Todos los fármacos (drugs.ts) - 6 fármacos completos * - Checklists de material (material-checklists.ts) * - Guías de refuerzo (guides-index.ts) */ import { query } from '../config/database.js'; import 'dotenv/config'; import { readFile } from 'fs/promises'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Mapeo de prioridades const priorityMap = { 'critico': 'critica', 'alto': 'alta', 'medio': 'media', 'bajo': 'baja' }; // Mapeo de categorías de procedimientos → clinical_context const procedureCategoryMap = { 'soporte_vital': { 'rcp': 'RCP', 'via_aerea': 'VIA_AEREA', 'shock': 'SHOCK' }, 'patologias': 'OTROS', 'escena': 'OTROS' }; /** * Obtiene ID del admin */ async function getAdminId() { let result = await query( `SELECT id FROM tes_content.users WHERE role = 'super_admin' LIMIT 1` ); if (result.rows.length === 0) { result = await query( `SELECT id FROM emerges_content.users WHERE role = 'super_admin' LIMIT 1` ); } if (result.rows.length === 0) { console.log('⚠️ No se encontró usuario admin, creando uno temporal...'); const tempResult = await query( `INSERT INTO tes_content.users (id, email, username, password_hash, role, is_active) VALUES (gen_random_uuid(), 'admin@emerges-tes.local', 'admin', 'temp', 'super_admin', true) RETURNING id` ); return tempResult.rows[0].id; } return result.rows[0].id; } /** * Lee y parsea procedures.ts usando regex mejorado */ async function loadProcedures() { const proceduresPath = join(__dirname, '../../src/data/procedures.ts'); const content = await readFile(proceduresPath, 'utf-8'); const procedures = []; const procedureRegex = /{\s*id:\s*['"]([^'"]+)['"],\s*title:\s*['"]([^'"]+)['"],\s*shortTitle:\s*['"]([^'"]+)['"],\s*category:\s*['"]([^'"]+)['"],\s*(?:subcategory:\s*['"]([^'"]*)?['"],\s*)?priority:\s*['"]([^'"]+)['"],\s*ageGroup:\s*['"]([^'"]+)['"],\s*steps:\s*\[([\s\S]*?)\],\s*warnings:\s*\[([\s\S]*?)\],\s*(?:keyPoints:\s*\[([\s\S]*?)\],)?\s*(?:equipment:\s*\[([\s\S]*?)\],)?\s*(?:drugs:\s*\[([\s\S]*?)\],)?\s*}/g; let match; while ((match = procedureRegex.exec(content)) !== null) { const [, id, title, shortTitle, category, subcategory, priority, ageGroup, stepsStr, warningsStr, keyPointsStr, equipmentStr, drugsStr] = match; const steps = extractArray(stepsStr); const warnings = extractArray(warningsStr); const keyPoints = keyPointsStr ? extractArray(keyPointsStr) : []; const equipment = equipmentStr ? extractArray(equipmentStr) : []; const drugs = drugsStr ? extractArray(drugsStr) : []; procedures.push({ id, title, shortTitle, category, subcategory: subcategory || null, priority, ageGroup, steps, warnings, keyPoints, equipment, drugs }); } return procedures; } /** * Lee y parsea drugs.ts - VERSIÓN MEJORADA para todos los fármacos */ async function loadDrugs() { const drugsPath = join(__dirname, '../../src/data/drugs.ts'); const content = await readFile(drugsPath, 'utf-8'); const drugs = []; // Regex mejorado que captura todos los campos, incluyendo arrays complejos const drugBlockRegex = /{\s*id:\s*['"]([^'"]+)['"],\s*(?:source:\s*['"]([^'"]*)?['"],\s*)?genericName:\s*['"]([^'"]+)['"],\s*tradeName:\s*['"]([^'"]+)['"],\s*category:\s*['"]([^'"]+)['"],\s*presentation:\s*['"]([^'"]+)['"],\s*adultDose:\s*['"]([^'"]+)['"],\s*(?:pediatricDose:\s*['"]([^'"]*)?['"],)?\s*routes:\s*\[([\s\S]*?)\],\s*(?:dilution:\s*['"]([^'"]*)?['"],)?\s*indications:\s*\[([\s\S]*?)\],\s*contraindications:\s*\[([\s\S]*?)\],\s*(?:sideEffects:\s*\[([\s\S]*?)\],)?\s*(?:antidote:\s*['"]([^'"]*)?['"],)?\s*(?:notes:\s*\[([\s\S]*?)\],)?\s*(?:criticalPoints:\s*\[([\s\S]*?)\],)?\s*(?:source:\s*['"]([^'"]*)?['"])?\s*}/g; let match; while ((match = drugBlockRegex.exec(content)) !== null) { const [, id, source1, genericName, tradeName, category, presentation, adultDose, pediatricDose, routesStr, dilution, indicationsStr, contraindicationsStr, sideEffectsStr, antidote, notesStr, criticalPointsStr, source2] = match; const routes = extractArray(routesStr); const indications = extractArray(indicationsStr); const contraindications = extractArray(contraindicationsStr); const sideEffects = sideEffectsStr ? extractArray(sideEffectsStr) : []; const notes = notesStr ? extractArray(notesStr) : []; const criticalPoints = criticalPointsStr ? extractArray(criticalPointsStr) : []; const source = source2 || source1 || null; drugs.push({ id, genericName, tradeName, category, presentation, adultDose, pediatricDose: pediatricDose || null, routes, dilution: dilution || null, indications, contraindications, sideEffects, antidote: antidote || null, notes, criticalPoints, source }); } return drugs; } /** * Lee y parsea guides-index.ts */ async function loadGuides() { const guidesPath = join(__dirname, '../../src/data/guides-index.ts'); const content = await readFile(guidesPath, 'utf-8'); const guides = []; // Buscar cada objeto guía usando un patrón más simple // Buscar: id: "...", titulo: "...", descripcion: "...", icono: "..." const guidePattern = /id:\s*["']([^"']+)["'],\s*titulo:\s*["']([^"']+)["'],\s*descripcion:\s*["']([^"']+)["'],\s*icono:\s*["']([^"']+)["']/g; let match; while ((match = guidePattern.exec(content)) !== null) { const [, id, titulo, descripcion, icono] = match; // Buscar scormAvailable después del icono const afterIcono = content.substring(match.index + match[0].length); const scormMatch = afterIcono.match(/scormAvailable:\s*(true|false)/); const scormAvailable = scormMatch ? scormMatch[1] === 'true' : false; guides.push({ id, titulo, descripcion, icono, scormAvailable, seccionesCount: 8 // Todas las guías tienen 8 secciones }); } return guides; } /** * Lee y parsea material-checklists.ts */ async function loadChecklists() { const checklistsPath = join(__dirname, '../../src/data/material-checklists.ts'); const content = await readFile(checklistsPath, 'utf-8'); const checklists = []; // Buscar definiciones de checklists const checklistRegex = /export const (\w+): MaterialChecklist = ([\s\S]*?);/g; let match; while ((match = checklistRegex.exec(content)) !== null) { const [, varName, checklistContent] = match; // Extraer campos básicos const idMatch = checklistContent.match(/id:\s*['"]([^'"]+)['"]/); const titleMatch = checklistContent.match(/title:\s*['"]([^'"]+)['"]/); const shortTitleMatch = checklistContent.match(/shortTitle:\s*['"]([^'"]+)['"]/); const phaseMatch = checklistContent.match(/phase:\s*['"]([^'"]+)['"]/); const descMatch = checklistContent.match(/description:\s*['"]([^'"]+)['"]/); if (idMatch && titleMatch) { checklists.push({ id: idMatch[1], title: titleMatch[1], shortTitle: shortTitleMatch ? shortTitleMatch[1] : titleMatch[1], phase: phaseMatch ? phaseMatch[1] : null, description: descMatch ? descMatch[1] : '', content: checklistContent // Guardar contenido completo para procesar después }); } } return checklists; } /** * Extrae elementos de un array de strings */ function extractArray(str) { if (!str) return []; const matches = str.match(/['"]([^'"]+)['"]/g); return matches ? matches.map(m => m.replace(/['"]/g, '')) : []; } /** * Inserta un procedimiento en la BD */ async function insertProcedure(procedure, adminId) { let clinicalContext = 'OTROS'; if (procedure.subcategory && procedureCategoryMap[procedure.category]?.[procedure.subcategory]) { clinicalContext = procedureCategoryMap[procedure.category][procedure.subcategory]; } const content = { steps: procedure.steps.map((step, index) => ({ id: `step-${index + 1}`, order: index + 1, text: step, critical: false })), warnings: procedure.warnings || [], keyPoints: procedure.keyPoints || [], equipment: procedure.equipment || [], drugs: procedure.drugs || [] }; const result = await query(` INSERT INTO tes_content.content_items ( id, type, slug, title, short_title, description, clinical_context, level, priority, status, source_guideline, version, latest_version, content, tags, category, created_by, updated_by ) VALUES ( gen_random_uuid(), 'protocol', $1, $2, $3, $4, $5::tes_content.clinical_context, 'operativo'::tes_content.usage_type, $6::tes_content.priority, 'published'::tes_content.content_status, 'INTERNO'::tes_content.source_guideline, '1.0.0', '1.0.0', $7::jsonb, $8::text[], $9, $10, $10 ) ON CONFLICT (slug) DO UPDATE SET title = EXCLUDED.title, short_title = EXCLUDED.short_title, description = EXCLUDED.description, content = EXCLUDED.content, updated_by = EXCLUDED.updated_by, updated_at = NOW() RETURNING id, slug, title `, [ procedure.id, procedure.title, procedure.shortTitle, `Protocolo operativo: ${procedure.title}`, clinicalContext, priorityMap[procedure.priority] || 'media', JSON.stringify(content), [procedure.category, procedure.subcategory].filter(Boolean), procedure.category, adminId ]); return result.rows[0]; } /** * Inserta un fármaco en la BD */ async function insertDrug(drug, adminId) { const content = { presentation: drug.presentation, adultDose: drug.adultDose, pediatricDose: drug.pediatricDose || null, routes: drug.routes, dilution: drug.dilution || null, indications: drug.indications, contraindications: drug.contraindications, sideEffects: drug.sideEffects || [], antidote: drug.antidote || null, notes: drug.notes || [], criticalPoints: drug.criticalPoints || [] }; const result = await query(` INSERT INTO tes_content.content_items ( id, type, slug, title, short_title, description, clinical_context, level, priority, status, source_guideline, version, latest_version, content, tags, category, created_by, updated_by ) VALUES ( gen_random_uuid(), 'drug', $1, $2, $3, $4, 'FARMACOLOGIA'::tes_content.clinical_context, 'operativo'::tes_content.usage_type, 'alta'::tes_content.priority, 'published'::tes_content.content_status, 'INTERNO'::tes_content.source_guideline, '1.0.0', '1.0.0', $5::jsonb, $6::text[], $7, $8, $8 ) ON CONFLICT (slug) DO UPDATE SET title = EXCLUDED.title, short_title = EXCLUDED.short_title, description = EXCLUDED.description, content = EXCLUDED.content, updated_by = EXCLUDED.updated_by, updated_at = NOW() RETURNING id, slug, title `, [ drug.id, drug.genericName, drug.tradeName, `Fármaco: ${drug.genericName} (${drug.tradeName})`, JSON.stringify(content), [drug.category, 'farmacologia'].filter(Boolean), drug.category, adminId ]); return result.rows[0]; } /** * Inserta una guía en la BD */ async function insertGuide(guide, adminId) { const content = { description: guide.descripcion, icono: guide.icono, scormAvailable: guide.scormAvailable, seccionesCount: guide.seccionesCount, secciones: [] // Se puede expandir después con las secciones reales }; const result = await query(` INSERT INTO tes_content.content_items ( id, type, slug, title, short_title, description, clinical_context, level, priority, status, source_guideline, version, latest_version, content, tags, category, created_by, updated_by ) VALUES ( gen_random_uuid(), 'guide', $1, $2, $3, $4, 'OTROS'::tes_content.clinical_context, 'formativo'::tes_content.usage_type, 'alta'::tes_content.priority, 'published'::tes_content.content_status, 'INTERNO'::tes_content.source_guideline, '1.0.0', '1.0.0', $5::jsonb, $6::text[], 'guide', $7, $7 ) ON CONFLICT (slug) DO UPDATE SET title = EXCLUDED.title, short_title = EXCLUDED.short_title, description = EXCLUDED.description, content = EXCLUDED.content, updated_by = EXCLUDED.updated_by, updated_at = NOW() RETURNING id, slug, title `, [ guide.id, guide.titulo, guide.titulo, guide.descripcion, JSON.stringify(content), ['guide', 'formativo', guide.icono].filter(Boolean), adminId ]); return result.rows[0]; } /** * Inserta un checklist en la BD */ async function insertChecklist(checklist, adminId) { const content = { phase: checklist.phase, description: checklist.description, sections: [] // Se puede expandir después }; const result = await query(` INSERT INTO tes_content.content_items ( id, type, slug, title, short_title, description, clinical_context, level, priority, status, source_guideline, version, latest_version, content, tags, category, created_by, updated_by ) VALUES ( gen_random_uuid(), 'checklist', $1, $2, $3, $4, 'OTROS'::tes_content.clinical_context, 'operativo'::tes_content.usage_type, 'media'::tes_content.priority, 'published'::tes_content.content_status, 'INTERNO'::tes_content.source_guideline, '1.0.0', '1.0.0', $5::jsonb, $6::text[], 'checklist', $7, $7 ) ON CONFLICT (slug) DO UPDATE SET title = EXCLUDED.title, short_title = EXCLUDED.short_title, description = EXCLUDED.description, content = EXCLUDED.content, updated_by = EXCLUDED.updated_by, updated_at = NOW() RETURNING id, slug, title `, [ checklist.id, checklist.title, checklist.shortTitle, checklist.description, JSON.stringify(content), ['checklist', checklist.phase].filter(Boolean), adminId ]); return result.rows[0]; } /** * Función principal */ async function migrateAllContent() { try { console.log('🔄 Iniciando migración COMPLETA de contenido...\n'); await query('SELECT 1'); console.log('✅ Conexión a base de datos establecida\n'); const adminId = await getAdminId(); console.log(`✅ Usuario admin: ${adminId}\n`); // 1. PROCEDIMIENTOS console.log('📋 Migrando PROCEDIMIENTOS...'); const procedures = await loadProcedures(); console.log(` Encontrados ${procedures.length} procedimientos\n`); for (const procedure of procedures) { try { const result = await insertProcedure(procedure, adminId); console.log(` ✅ ${result.slug}: ${result.title}`); } catch (error) { console.error(` ❌ Error migrando ${procedure.id}:`, error.message); } } console.log(''); // 2. FÁRMACOS (TODOS) console.log('💊 Migrando FÁRMACOS (TODOS)...'); const drugs = await loadDrugs(); console.log(` Encontrados ${drugs.length} fármacos\n`); for (const drug of drugs) { try { const result = await insertDrug(drug, adminId); console.log(` ✅ ${result.slug}: ${result.title}`); } catch (error) { console.error(` ❌ Error migrando ${drug.id}:`, error.message); } } console.log(''); // 3. GUÍAS console.log('📚 Migrando GUÍAS...'); try { const guides = await loadGuides(); console.log(` Encontradas ${guides.length} guías\n`); for (const guide of guides) { try { const result = await insertGuide(guide, adminId); console.log(` ✅ ${result.slug}: ${result.title}`); } catch (error) { console.error(` ❌ Error migrando ${guide.id}:`, error.message); } } } catch (error) { console.log(` ⚠️ No se pudieron cargar guías: ${error.message}`); } console.log(''); // 4. CHECKLISTS console.log('✅ Migrando CHECKLISTS...'); try { const checklists = await loadChecklists(); console.log(` Encontrados ${checklists.length} checklists\n`); for (const checklist of checklists) { try { const result = await insertChecklist(checklist, adminId); console.log(` ✅ ${result.slug}: ${result.title}`); } catch (error) { console.error(` ❌ Error migrando ${checklist.id}:`, error.message); } } } catch (error) { console.log(` ⚠️ No se pudieron cargar checklists: ${error.message}`); } console.log(''); // RESUMEN FINAL console.log('📊 RESUMEN FINAL:'); const stats = await query(` SELECT type, COUNT(*) as total, COUNT(CASE WHEN status = 'published' THEN 1 END) as published FROM tes_content.content_items GROUP BY type ORDER BY type `); stats.rows.forEach(row => { console.log(` ${row.type}: ${row.total} total, ${row.published} publicados`); }); const total = stats.rows.reduce((sum, row) => sum + parseInt(row.total), 0); console.log(`\n 📦 TOTAL: ${total} items migrados\n`); console.log('✅ Migración COMPLETA finalizada exitosamente!'); } catch (error) { console.error('❌ Error durante la migración:', error); process.exit(1); } } migrateAllContent();