/** * Script de migración: Importa contenido real de la app al backend * * Lee procedures.ts y drugs.ts y los migra a la base de datos */ import { query } from '../config/database.js'; import 'dotenv/config'; import { readFile } from 'fs/promises'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; import { randomUUID } from 'crypto'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Mapeo de prioridades (app → BD) 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' }; // Mapeo de categorías de fármacos → clinical_context const drugCategoryMap = { 'cardiovascular': 'OTROS', 'respiratorio': 'OTROS', 'neurologico': 'OTROS', 'analgesia': 'OTROS', 'oxigenoterapia': 'OTROS', 'otros': 'OTROS' }; // Mapeo de source_guideline const sourceGuidelineMap = { 'ERC': 'ERC', 'SEMES': 'SEMES', 'AHA': 'AHA', 'INTERNO': 'INTERNO', 'MANUAL_TES_DIGITAL': 'INTERNO' }; /** * Obtiene ID del admin */ async function getAdminId() { const result = await query( `SELECT id FROM tes_content.users WHERE role = 'super_admin' LIMIT 1` ); if (result.rows.length === 0) { throw new Error('No se encontró usuario admin. Ejecuta primero: node scripts/seed-admin.js'); } return result.rows[0].id; } /** * Lee y parsea procedures.ts */ async function loadProcedures() { const proceduresPath = join(__dirname, '../../src/data/procedures.ts'); const content = await readFile(proceduresPath, 'utf-8'); // Extraer el array de procedimientos usando regex const arrayMatch = content.match(/export const procedures: Procedure\[\] = \[([\s\S]*)\];/); if (!arrayMatch) { throw new Error('No se pudo encontrar el array de procedimientos'); } // Evaluar el contenido (cuidado: esto requiere que el código sea válido) // Mejor usar un parser, pero para simplicidad usaremos eval en un contexto controlado const proceduresCode = `[${arrayMatch[1]}]`; // Reemplazar valores que no son JSON válido const jsonLike = proceduresCode .replace(/'/g, '"') .replace(/(\w+):/g, '"$1":') .replace(/,\s*}/g, '}') .replace(/,\s*]/g, ']'); try { const procedures = JSON.parse(jsonLike); return procedures; } catch (error) { // Si falla el parseo simple, intentar extraer manualmente console.log('⚠️ Parseo automático falló, extrayendo manualmente...'); return extractProceduresManually(content); } } /** * Extrae procedimientos manualmente del código TypeScript */ function extractProceduresManually(content) { 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; // Extraer arrays 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; } /** * 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, '')) : []; } /** * Lee y parsea drugs.ts */ async function loadDrugs() { const drugsPath = join(__dirname, '../../src/data/drugs.ts'); const content = await readFile(drugsPath, 'utf-8'); // Extraer manualmente usando regex return extractDrugsManually(content); } /** * Extrae fármacos manualmente del código TypeScript */ function extractDrugsManually(content) { const drugs = []; const drugBlockRegex = /\{\s*id:\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, genericName, tradeName, category, presentation, adultDose, pediatricDose, routesStr, dilution, indicationsStr, contraindicationsStr, sideEffectsStr, antidote, notesStr, criticalPointsStr, source] = 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) : []; drugs.push({ id, genericName, tradeName, category, presentation, adultDose, pediatricDose: pediatricDose || null, routes, dilution: dilution || null, indications, contraindications, sideEffects, antidote: antidote || null, notes, criticalPoints, source: source || null }); } return drugs; } /** * Inserta un procedimiento en la BD */ async function insertProcedure(procedure, adminId) { // Determinar clinical_context let clinicalContext = 'OTROS'; if (procedure.subcategory && procedureCategoryMap[procedure.category]?.[procedure.subcategory]) { clinicalContext = procedureCategoryMap[procedure.category][procedure.subcategory]; } // Construir contenido JSONB 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 || [] }; // Determinar source_guideline (por defecto INTERNO) const sourceGuideline = 'INTERNO'; 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, $7::tes_content.source_guideline, '1.0.0', '1.0.0', $8::jsonb, $9::text[], $10, $11, $11 ) 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', sourceGuideline, JSON.stringify(content), [procedure.category, procedure.subcategory].filter(Boolean), procedure.category ]); return result.rows[0]; } /** * Inserta un fármaco en la BD */ async function insertDrug(drug, adminId) { // Construir contenido JSONB 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]; } /** * Función principal */ async function migrateAppContent() { try { console.log('🔄 Iniciando migración de contenido de la app al backend...\n'); // Verificar conexión await query('SELECT 1'); console.log('✅ Conexión a base de datos establecida\n'); // Obtener admin ID const adminId = await getAdminId(); console.log(`✅ Usuario admin encontrado: ${adminId}\n`); // Cargar procedimientos console.log('📋 Cargando procedimientos desde src/data/procedures.ts...'); const procedures = await loadProcedures(); console.log(` Encontrados ${procedures.length} procedimientos\n`); // Migrar procedimientos console.log('💾 Migrando procedimientos a la base de datos...'); 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(''); // Cargar fármacos console.log('💊 Cargando fármacos desde src/data/drugs.ts...'); const drugs = await loadDrugs(); console.log(` Encontrados ${drugs.length} fármacos\n`); // Migrar fármacos console.log('💾 Migrando fármacos a la base de datos...'); 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(''); // Resumen console.log('📊 Resumen de migración:'); const stats = await query(` SELECT type, COUNT(*) as total, COUNT(CASE WHEN status = 'published' THEN 1 END) as published FROM tes_content.content_items WHERE type IN ('protocol', 'drug') GROUP BY type `); stats.rows.forEach(row => { console.log(` ${row.type}: ${row.total} total, ${row.published} publicados`); }); console.log('\n✅ Migración completada exitosamente!'); } catch (error) { console.error('❌ Error durante la migración:', error); process.exit(1); } } // Ejecutar migrateAppContent();