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

422 lines
12 KiB
JavaScript

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