codigo0/backend/scripts/migrate-all-content.js

602 lines
18 KiB
JavaScript

/**
* 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();