422 lines
12 KiB
JavaScript
Executable file
422 lines
12 KiB
JavaScript
Executable file
/**
|
|
* 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();
|
|
|