550 lines
18 KiB
JavaScript
550 lines
18 KiB
JavaScript
|
|
/**
|
||
|
|
* Script de migración V2: Importa contenido real de la app al backend
|
||
|
|
*
|
||
|
|
* Lee procedures.ts y drugs.ts usando import dinámico
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { query } from '../config/database.js';
|
||
|
|
import 'dotenv/config';
|
||
|
|
import { createRequire } from 'module';
|
||
|
|
import { fileURLToPath } from 'url';
|
||
|
|
import { dirname, join } from 'path';
|
||
|
|
|
||
|
|
const require = createRequire(import.meta.url);
|
||
|
|
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'
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Obtiene ID del admin
|
||
|
|
*/
|
||
|
|
async function getAdminId() {
|
||
|
|
// Intentar primero en tes_content
|
||
|
|
let result = await query(
|
||
|
|
`SELECT id FROM tes_content.users WHERE role = 'super_admin' LIMIT 1`
|
||
|
|
);
|
||
|
|
|
||
|
|
// Si no existe, buscar en emerges_content (schema anterior)
|
||
|
|
if (result.rows.length === 0) {
|
||
|
|
result = await query(
|
||
|
|
`SELECT id FROM emerges_content.users WHERE role = 'super_admin' LIMIT 1`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Si aún no existe, crear uno temporal
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Carga procedimientos desde el archivo TypeScript
|
||
|
|
* Usa require para importar el módulo
|
||
|
|
*/
|
||
|
|
async function loadProcedures() {
|
||
|
|
try {
|
||
|
|
// Intentar importar directamente
|
||
|
|
const proceduresModule = await import('../../src/data/procedures.ts');
|
||
|
|
return proceduresModule.procedures || [];
|
||
|
|
} catch (error) {
|
||
|
|
console.log('⚠️ No se pudo importar directamente, usando datos hardcodeados...');
|
||
|
|
// Datos hardcodeados como fallback
|
||
|
|
return getHardcodedProcedures();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Carga fármacos desde el archivo TypeScript
|
||
|
|
*/
|
||
|
|
async function loadDrugs() {
|
||
|
|
try {
|
||
|
|
const drugsModule = await import('../../src/data/drugs.ts');
|
||
|
|
return drugsModule.drugs || [];
|
||
|
|
} catch (error) {
|
||
|
|
console.log('⚠️ No se pudo importar directamente, usando datos hardcodeados...');
|
||
|
|
return getHardcodedDrugs();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Procedimientos hardcodeados (fallback)
|
||
|
|
*/
|
||
|
|
function getHardcodedProcedures() {
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
id: 'rcp-adulto-svb',
|
||
|
|
title: 'RCP Adulto - Soporte Vital Básico',
|
||
|
|
shortTitle: 'RCP Adulto SVB',
|
||
|
|
category: 'soporte_vital',
|
||
|
|
subcategory: 'rcp',
|
||
|
|
priority: 'critico',
|
||
|
|
ageGroup: 'adulto',
|
||
|
|
steps: [
|
||
|
|
'Garantizar seguridad de la escena',
|
||
|
|
'Comprobar consciencia: estimular y preguntar "¿Se encuentra bien?"',
|
||
|
|
'Si no responde, llamar inmediatamente al 112',
|
||
|
|
'Abrir vía aérea: maniobra frente-mentón',
|
||
|
|
'Comprobar respiración: VER-OÍR-SENTIR (máx. 10 segundos)',
|
||
|
|
'Si no respira normal: iniciar RCP',
|
||
|
|
'Iniciar compresiones torácicas: 30 compresiones',
|
||
|
|
'Dar 2 ventilaciones de rescate',
|
||
|
|
'Continuar ciclos 30:2 sin interrupción',
|
||
|
|
'Solicitar DEA cuando esté disponible',
|
||
|
|
],
|
||
|
|
warnings: [
|
||
|
|
'Profundidad compresiones: 5-6 cm',
|
||
|
|
'Frecuencia: 100-120 compresiones/min',
|
||
|
|
'Permitir descompresión completa',
|
||
|
|
'Minimizar interrupciones (<10 seg)',
|
||
|
|
'Cambiar reanimador cada 2 min',
|
||
|
|
],
|
||
|
|
keyPoints: [
|
||
|
|
'Compresiones de calidad salvan vidas',
|
||
|
|
'No interrumpir para pulso hasta que haya signos de vida',
|
||
|
|
'La desfibrilación precoz aumenta supervivencia',
|
||
|
|
],
|
||
|
|
equipment: ['DEA', 'Bolsa-mascarilla', 'Cánula orofaríngea'],
|
||
|
|
drugs: ['Adrenalina 1mg'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'rcp-adulto-sva',
|
||
|
|
title: 'RCP Adulto - Soporte Vital Avanzado',
|
||
|
|
shortTitle: 'RCP Adulto SVA',
|
||
|
|
category: 'soporte_vital',
|
||
|
|
subcategory: 'rcp',
|
||
|
|
priority: 'critico',
|
||
|
|
ageGroup: 'adulto',
|
||
|
|
steps: [
|
||
|
|
'Continuar RCP 30:2 mientras se prepara monitorización',
|
||
|
|
'Colocar monitor/desfibrilador y analizar ritmo',
|
||
|
|
'Ritmo desfibrilable (FV/TVSP): descarga 150-200J bifásico',
|
||
|
|
'Reiniciar RCP inmediatamente 2 minutos',
|
||
|
|
'Obtener acceso IV/IO',
|
||
|
|
'Administrar adrenalina 1mg IV cada 3-5 min (tras 3ª descarga si DF)',
|
||
|
|
'Considerar amiodarona 300mg IV si FV/TVSP refractaria',
|
||
|
|
'Asegurar vía aérea avanzada cuando sea posible',
|
||
|
|
'Buscar y tratar causas reversibles (4H y 4T)',
|
||
|
|
'Si ROSC: cuidados post-parada',
|
||
|
|
],
|
||
|
|
warnings: [
|
||
|
|
'Minimizar interrupciones de compresiones',
|
||
|
|
'Adrenalina en ritmos no DF: lo antes posible',
|
||
|
|
'Amiodarona: 150mg adicionales si persiste FV/TVSP',
|
||
|
|
'Capnografía: objetivo ETCO2 >10 mmHg',
|
||
|
|
],
|
||
|
|
keyPoints: [
|
||
|
|
'4H: Hipoxia, Hipovolemia, Hipo/Hiperpotasemia, Hipotermia',
|
||
|
|
'4T: Neumotórax a Tensión, Taponamiento, Tóxicos, TEP',
|
||
|
|
],
|
||
|
|
equipment: ['Monitor/Desfibrilador', 'Material IOT', 'Acceso venoso'],
|
||
|
|
drugs: ['Adrenalina', 'Amiodarona', 'Atropina'],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'rcp-pediatrico',
|
||
|
|
title: 'RCP Pediátrico - SVB',
|
||
|
|
shortTitle: 'RCP Pediátrico',
|
||
|
|
category: 'soporte_vital',
|
||
|
|
subcategory: 'rcp',
|
||
|
|
priority: 'critico',
|
||
|
|
ageGroup: 'pediatrico',
|
||
|
|
steps: [
|
||
|
|
'Garantizar seguridad de la escena',
|
||
|
|
'Comprobar consciencia',
|
||
|
|
'Si no responde, llamar inmediatamente al 112',
|
||
|
|
'Abrir vía aérea: maniobra frente-mentón',
|
||
|
|
'Comprobar respiración (máx. 10 segundos)',
|
||
|
|
'Si no respira normal: iniciar RCP',
|
||
|
|
'Dar 5 ventilaciones de rescate iniciales',
|
||
|
|
'Comprobar signos de vida/pulso (máx. 10 seg)',
|
||
|
|
'Si no hay signos de vida: 15 compresiones torácicas',
|
||
|
|
'Continuar con ciclos 15:2',
|
||
|
|
],
|
||
|
|
warnings: [
|
||
|
|
'Lactante (<1 año): compresiones con 2 dedos',
|
||
|
|
'Niño (1-8 años): talón de una mano',
|
||
|
|
'Profundidad: 1/3 del tórax (4cm lactante, 5cm niño)',
|
||
|
|
'Frecuencia: 100-120/min',
|
||
|
|
],
|
||
|
|
keyPoints: [
|
||
|
|
'La causa más frecuente es respiratoria',
|
||
|
|
'Las 5 ventilaciones iniciales son cruciales',
|
||
|
|
'Ratio 15:2 para profesionales',
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'obstruccion-via-aerea',
|
||
|
|
title: 'Obstrucción de Vía Aérea - OVACE',
|
||
|
|
shortTitle: 'OVACE',
|
||
|
|
category: 'soporte_vital',
|
||
|
|
subcategory: 'via_aerea',
|
||
|
|
priority: 'critico',
|
||
|
|
ageGroup: 'todos',
|
||
|
|
steps: [
|
||
|
|
'Valorar gravedad: ¿Puede toser, hablar, respirar?',
|
||
|
|
'OBSTRUCCIÓN LEVE: animar a toser, vigilar',
|
||
|
|
'OBSTRUCCIÓN GRAVE consciente: 5 golpes interescapulares',
|
||
|
|
'Si no se resuelve: 5 compresiones abdominales (Heimlich)',
|
||
|
|
'Alternar 5 golpes + 5 compresiones hasta resolución',
|
||
|
|
'Si pierde consciencia: iniciar RCP',
|
||
|
|
'Antes de ventilar: revisar boca y extraer objeto visible',
|
||
|
|
],
|
||
|
|
warnings: [
|
||
|
|
'En embarazadas y obesos: compresiones torácicas',
|
||
|
|
'Lactantes: 5 golpes en espalda + 5 compresiones torácicas',
|
||
|
|
'NO hacer barrido digital a ciegas',
|
||
|
|
'Derivar siempre tras maniobras de Heimlich',
|
||
|
|
],
|
||
|
|
keyPoints: [
|
||
|
|
'La tos es el mecanismo más efectivo',
|
||
|
|
'No interferir si la tos es efectiva',
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'shock-hemorragico',
|
||
|
|
title: 'Shock Hemorrágico',
|
||
|
|
shortTitle: 'Shock Hemorrágico',
|
||
|
|
category: 'soporte_vital',
|
||
|
|
subcategory: 'shock',
|
||
|
|
priority: 'critico',
|
||
|
|
ageGroup: 'adulto',
|
||
|
|
steps: [
|
||
|
|
'Control de hemorragia externa: presión directa',
|
||
|
|
'Torniquete si hemorragia en extremidad no controlable',
|
||
|
|
'Oxigenoterapia alto flujo',
|
||
|
|
'Canalizar 2 vías IV gruesas (14-16G)',
|
||
|
|
'Fluidos: cristaloides tibios (objetivo TAS 80-90 mmHg)',
|
||
|
|
'Posición Trendelenburg si no hay TCE',
|
||
|
|
'Evitar hipotermia: mantas térmicas',
|
||
|
|
'Traslado urgente a hospital útil',
|
||
|
|
'Considerar ácido tranexámico 1g IV',
|
||
|
|
],
|
||
|
|
warnings: [
|
||
|
|
'Hipotensión permisiva: TAS 80-90 mmHg',
|
||
|
|
'Excepto en TCE: mantener TAS >90 mmHg',
|
||
|
|
'Evitar sobrecarga de fluidos',
|
||
|
|
'Torniquete: anotar hora de colocación',
|
||
|
|
],
|
||
|
|
keyPoints: [
|
||
|
|
'Clase I: <15% pérdida, FC normal, TA normal',
|
||
|
|
'Clase II: 15-30%, taquicardia, TA normal',
|
||
|
|
'Clase III: 30-40%, taquicardia, hipotensión',
|
||
|
|
'Clase IV: >40%, bradicardia, shock severo',
|
||
|
|
],
|
||
|
|
equipment: ['Torniquete', 'Agentes hemostáticos', 'Mantas térmicas'],
|
||
|
|
drugs: ['Ácido tranexámico', 'Cristaloides'],
|
||
|
|
},
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Fármacos hardcodeados (fallback)
|
||
|
|
*/
|
||
|
|
function getHardcodedDrugs() {
|
||
|
|
return [
|
||
|
|
{
|
||
|
|
id: 'oxigeno',
|
||
|
|
genericName: 'Oxígeno (O₂)',
|
||
|
|
tradeName: 'Oxígeno medicinal',
|
||
|
|
category: 'oxigenoterapia',
|
||
|
|
presentation: 'Gas medicinal. Balas de 2L, 5L, 10L, 15L. Concentración variable según dispositivo.',
|
||
|
|
adultDose: 'Mascarilla con reservorio: 10-15 L/min (FiO₂ ~85%). Mascarilla simple: 5-10 L/min (FiO₂ ~40-60%). Gafas nasales: 1-6 L/min (FiO₂ 24-44%).',
|
||
|
|
pediatricDose: 'Ajustar por respuesta. Gafas nasales: 1-4 L/min. Mascarilla simple: 5-8 L/min. En lactantes, evitar flujos >4L/min por riesgo de retinopatía.',
|
||
|
|
routes: ['Inhalatoria'],
|
||
|
|
indications: [
|
||
|
|
'Hipoxia (SpO₂ <94%)',
|
||
|
|
'Parada cardiorrespiratoria',
|
||
|
|
'Ictus',
|
||
|
|
'Síndrome Coronario Agudo',
|
||
|
|
'Trauma grave',
|
||
|
|
],
|
||
|
|
contraindications: [
|
||
|
|
'En EPOC conocida con riesgo de hipercapnia: usar Venturi 28% y titular a SpO₂ 88-92%',
|
||
|
|
],
|
||
|
|
notes: [
|
||
|
|
'NO es un fármaco inocuo',
|
||
|
|
'Humedecer si uso prolongado >2h',
|
||
|
|
'En EPOC conocida: usar Venturi 28% y titular a SpO₂ 88-92%',
|
||
|
|
],
|
||
|
|
criticalPoints: [
|
||
|
|
'Terapia, no placebo. Usarlo con indicación y precaución en EPOC',
|
||
|
|
'En EPOC conocida con riesgo de hipercapnia, usar Venturi 28% y titular a SpO₂ 88-92%',
|
||
|
|
],
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'adrenalina',
|
||
|
|
genericName: 'Adrenalina (Epinefrina)',
|
||
|
|
tradeName: 'Adrenalina Braun®',
|
||
|
|
category: 'cardiovascular',
|
||
|
|
presentation: 'ANAFILAXIA: Ampolla 1 mg/1 ml (1:1000). PCR: Ampolla 1 mg/10 ml (1:10.000). ¡LEER ETIQUETA EN VOZ ALTA!',
|
||
|
|
adultDose: 'ANAFILAXIA: 0.5 mg IM (0.5 ml de ampolla 1:1000). Repetir a los 5 min si no mejora. PCR: 1 mg IV/IO (10 ml de ampolla 1:10.000) cada 3-5 min.',
|
||
|
|
pediatricDose: 'ANAFILAXIA: 0.01 mg/kg IM (Máx. 0.5 mg). Ej: 20kg → 0.2 mg = 0.2 ml. PCR: 0.01 mg/kg IV/IO (o según protocolo local).',
|
||
|
|
routes: ['IM', 'IV', 'IO'],
|
||
|
|
dilution: 'ANAFILAXIA: Sin diluir. PCR: Sin diluir (usar ampolla 1:10.000 directamente).',
|
||
|
|
indications: [
|
||
|
|
'Anafilaxia grave: Reacción alérgica sistémica con afectación respiratoria (estridor, sibilancias, disnea) y/o cardiovascular (hipotensión, taquicardia, colapso)',
|
||
|
|
'Parada cardiorrespiratoria: Cualquier ritmo (FV/TVSP, AESP, ACR) como parte del algoritmo SVA, una vez establecida vía IV/IO',
|
||
|
|
],
|
||
|
|
contraindications: [
|
||
|
|
'No hay contraindicaciones absolutas en emergencias vitales',
|
||
|
|
'Paciente anciano o con cardiopatía isquémica: El beneficio en anafilaxia grave supera el riesgo. Administrar y monitorizar estrechamente',
|
||
|
|
],
|
||
|
|
sideEffects: ['Temblor', 'Taquicardia', 'Palidez', 'HTA', 'Arritmias'],
|
||
|
|
notes: [
|
||
|
|
'⚠️ CONCENTRACIÓN CRÍTICA: 1:1000 (1 mg/ml) para Anafilaxia IM. 1:10.000 (0.1 mg/ml) para PCR IV/IO',
|
||
|
|
'ANAFILAXIA: Fármaco salvavidas. Administración IM precoz es la intervención más importante. No esperar',
|
||
|
|
],
|
||
|
|
criticalPoints: [
|
||
|
|
'⚠️ ERROR CRÍTICO: Confundir 1:1000 con 1:10.000. Administrar 1 mg/ml (1:1000) por vía IV en PCR equivale a 10 mg (LETAL)',
|
||
|
|
'Anafilaxia: 1:1000 IM en el MUSLO. 0.5 mg adultos, 0.01 mg/kg niños. Repetir a los 5 min si no mejora',
|
||
|
|
'PCR: 1:10.000 IV/IO. 1 mg adultos, 0.01 mg/kg niños. Cada 3-5 min',
|
||
|
|
],
|
||
|
|
},
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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 || []
|
||
|
|
};
|
||
|
|
|
||
|
|
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) {
|
||
|
|
// 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...');
|
||
|
|
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...');
|
||
|
|
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();
|
||
|
|
|