codigo0/backend/scripts/sync-content-to-db.js

850 lines
28 KiB
JavaScript
Raw Normal View History

2026-01-19 08:10:16 +00:00
/**
* SCRIPT DE SINCRONIZACIÓN MASIVA DE CONTENIDO
*
* Sincroniza contenido desde archivos locales (procedures.ts, drugs.ts, guides-index.ts)
* hacia la base de datos PostgreSQL de forma masiva e inteligente.
*
* Características:
* - Migración masiva (no uno por uno)
* - Idempotente (puede ejecutarse múltiples veces)
* - Modo dry-run (ver qué haría sin ejecutar)
* - Detección de cambios (solo actualiza lo necesario)
* - Genera reportes detallados
* - Maneja relaciones bidireccionales
* - Soporta mejoras incrementales
*
* Uso:
* node backend/scripts/sync-content-to-db.js # Ejecutar
* node backend/scripts/sync-content-to-db.js --dry-run # Ver qué haría
* node backend/scripts/sync-content-to-db.js --type=protocols # Solo protocolos
* node backend/scripts/sync-content-to-db.js --force # Forzar actualización
*/
import { query } from '../config/database.js';
import { readFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { createHash } from 'crypto';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Configuración
const DRY_RUN = process.argv.includes('--dry-run');
const FORCE = process.argv.includes('--force');
const TYPE_FILTER = process.argv.find(arg => arg.startsWith('--type='))?.split('=')[1];
// Estadísticas
const stats = {
protocols: { created: 0, updated: 0, skipped: 0, errors: 0 },
drugs: { created: 0, updated: 0, skipped: 0, errors: 0 },
guides: { created: 0, updated: 0, skipped: 0, errors: 0 },
checklists: { created: 0, updated: 0, skipped: 0, errors: 0 },
relations: { created: 0, skipped: 0, errors: 0 },
};
/**
* Obtener usuario admin para operaciones
*/
async function getAdminUser() {
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 seed-admin.js primero.');
}
return result.rows[0].id;
}
/**
* Calcular hash de contenido para detectar cambios
*/
function calculateContentHash(content) {
const contentString = JSON.stringify(content, Object.keys(content).sort());
return createHash('sha256').update(contentString).digest('hex');
}
/**
* Mapear prioridad local a prioridad BD
*/
function mapPriority(priority) {
const map = {
'critico': 'critica',
'alto': 'alta',
'medio': 'media',
'bajo': 'baja',
};
return map[priority] || 'media';
}
/**
* Mapear contexto clínico desde categoría
*/
function mapClinicalContext(category, subcategory) {
const contextMap = {
'soporte_vital': 'RCP',
'patologias': 'OTROS',
'escena': 'ABCDE',
'trauma': 'TRAUMA',
'shock': 'SHOCK',
'ictus': 'ICTUS',
'via_aerea': 'VIA_AEREA',
'oxigenoterapia': 'OXIGENOTERAPIA',
};
if (subcategory === 'rcp') return 'RCP';
if (subcategory === 'ovace') return 'OVACE';
if (subcategory === 'shock') return 'SHOCK';
if (subcategory === 'ictus') return 'ICTUS';
return contextMap[category] || 'OTROS';
}
/**
* Sincronizar un protocolo
*/
async function syncProtocol(procedure, adminId) {
try {
const content = {
steps: procedure.steps || [],
warnings: procedure.warnings || [],
keyPoints: procedure.keyPoints || [],
equipment: procedure.equipment || [],
drugs: procedure.drugs || [],
category: procedure.category,
subcategory: procedure.subcategory,
ageGroup: procedure.ageGroup || 'adulto',
};
const contentHash = calculateContentHash(content);
const clinicalContext = mapClinicalContext(procedure.category, procedure.subcategory);
const priority = mapPriority(procedure.priority);
// Verificar si existe
const existing = await query(
`SELECT id, content, updated_at FROM tes_content.content_items
WHERE slug = $1 AND type = 'protocol'`,
[procedure.id]
);
if (existing.rows.length > 0) {
const existingItem = existing.rows[0];
const existingContent = existingItem.content;
const existingHash = calculateContentHash(existingContent);
// Si el hash es igual y no es force, saltar
if (contentHash === existingHash && !FORCE) {
stats.protocols.skipped++;
return { action: 'skipped', id: existingItem.id };
}
// Actualizar
if (!DRY_RUN) {
await query(
`UPDATE tes_content.content_items SET
title = $1,
short_title = $2,
description = $3,
clinical_context = $4,
level = 'operativo',
priority = $5::tes_content.priority,
content = $6::jsonb,
tags = $7::text[],
category = $8,
subcategory = $9,
age_group = $10,
updated_by = $11,
updated_at = NOW()
WHERE slug = $12 AND type = 'protocol'`,
[
procedure.title,
procedure.shortTitle || null,
`Protocolo operativo: ${procedure.title}`,
clinicalContext || null,
priority || 'media',
JSON.stringify(content),
[procedure.category, procedure.subcategory].filter(Boolean),
procedure.category || null,
procedure.subcategory || null,
procedure.ageGroup || 'adulto',
adminId,
procedure.id,
]
);
}
stats.protocols.updated++;
return { action: 'updated', id: existingItem.id };
}
// Crear nuevo
if (!DRY_RUN) {
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, subcategory, age_group,
created_by, updated_by
) VALUES (
$1, 'protocol', $2, $3, $4, $5,
$6, 'operativo', $7::tes_content.content_priority,
'published'::tes_content.content_status,
'INTERNO', 1, 1,
$8::jsonb,
$9::text[],
$10,
$11,
$12,
$13, $13
)
RETURNING id, slug, title`,
[
procedure.id, // Usar el id como primary key
procedure.id, // slug
procedure.title,
procedure.shortTitle || null,
`Protocolo operativo: ${procedure.title}`,
clinicalContext || null,
priority || 'media',
JSON.stringify(content),
[procedure.category, procedure.subcategory].filter(Boolean),
procedure.category || null,
procedure.subcategory || null,
procedure.ageGroup || 'adulto',
adminId,
]
);
stats.protocols.created++;
return { action: 'created', id: result.rows[0].id };
}
stats.protocols.created++;
return { action: 'created', id: null };
} catch (error) {
console.error(`❌ Error sincronizando protocolo ${procedure.id}:`, error.message);
stats.protocols.errors++;
return { action: 'error', error: error.message };
}
}
/**
* Sincronizar un fármaco
*/
async function syncDrug(drug, adminId) {
try {
const content = {
genericName: drug.genericName,
tradeName: drug.tradeName,
category: drug.category,
presentation: drug.presentation,
adultDose: drug.adultDose,
pediatricDose: drug.pediatricDose,
routes: drug.routes || [],
dilution: drug.dilution,
indications: drug.indications || [],
contraindications: drug.contraindications || [],
sideEffects: drug.sideEffects,
antidote: drug.antidote,
notes: drug.notes,
criticalPoints: drug.criticalPoints,
source: drug.source,
};
const contentHash = calculateContentHash(content);
const priority = mapPriority('medio'); // Fármacos generalmente media prioridad
// Verificar si existe
const existing = await query(
`SELECT id, content FROM tes_content.content_items
WHERE slug = $1 AND type = 'drug'`,
[drug.id]
);
if (existing.rows.length > 0) {
const existingItem = existing.rows[0];
const existingHash = calculateContentHash(existingItem.content);
if (contentHash === existingHash && !FORCE) {
stats.drugs.skipped++;
return { action: 'skipped', id: existingItem.id };
}
if (!DRY_RUN) {
await query(
`UPDATE tes_content.content_items SET
title = $1,
short_title = $2,
description = $3,
clinical_context = 'FARMACOLOGIA'::tes_content.clinical_context,
level = 'referencia'::tes_content.usage_type,
priority = $4::tes_content.priority,
content = $5::jsonb,
tags = $6::text[],
category = $7,
updated_by = $8,
updated_at = NOW()
WHERE id = $9`,
[
drug.genericName,
drug.tradeName,
`Fármaco: ${drug.genericName}`,
priority,
JSON.stringify(content),
[drug.category, ...(drug.indications || [])].filter(Boolean),
drug.category,
adminId,
existingItem.id,
]
);
}
stats.drugs.updated++;
return { action: 'updated', id: existingItem.id };
}
if (!DRY_RUN) {
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,
'referencia'::tes_content.usage_type,
$5::tes_content.priority,
'published'::tes_content.content_status,
'INTERNO'::tes_content.source_guideline,
'1.0.0', '1.0.0',
$6::jsonb,
$7::text[],
$8,
$9, $9
)
RETURNING id, slug, title`,
[
drug.id,
drug.genericName,
drug.tradeName,
`Fármaco: ${drug.genericName}`,
priority,
JSON.stringify(content),
[drug.category, ...(drug.indications || [])].filter(Boolean),
drug.category,
adminId,
]
);
stats.drugs.created++;
return { action: 'created', id: result.rows[0].id };
}
stats.drugs.created++;
return { action: 'created', id: null };
} catch (error) {
console.error(`❌ Error sincronizando fármaco ${drug.id}:`, error.message);
stats.drugs.errors++;
return { action: 'error', error: error.message };
}
}
/**
* Sincronizar una guía
*/
async function syncGuide(guide, adminId) {
try {
const content = {
titulo: guide.titulo,
descripcion: guide.descripcion,
icono: guide.icono,
secciones: guide.secciones || [],
protocoloOperativo: guide.protocoloOperativo,
scormAvailable: guide.scormAvailable || false,
};
const contentHash = calculateContentHash(content);
// Verificar si existe
const existing = await query(
`SELECT id, content FROM tes_content.content_items
WHERE slug = $1 AND type = 'guide'`,
[guide.id]
);
if (existing.rows.length > 0) {
const existingItem = existing.rows[0];
const existingHash = calculateContentHash(existingItem.content);
if (contentHash === existingHash && !FORCE) {
stats.guides.skipped++;
return { action: 'skipped', id: existingItem.id };
}
if (!DRY_RUN) {
await query(
`UPDATE tes_content.content_items SET
title = $1,
description = $2,
clinical_context = 'OTROS'::tes_content.clinical_context,
level = 'formativo'::tes_content.usage_type,
priority = 'alta'::tes_content.priority,
content = $3::jsonb,
tags = $4::text[],
updated_by = $5,
updated_at = NOW()
WHERE id = $6`,
[
guide.titulo,
guide.descripcion,
JSON.stringify(content),
['guia', 'formativo', guide.id],
adminId,
existingItem.id,
]
);
}
stats.guides.updated++;
return { action: 'updated', id: existingItem.id };
}
if (!DRY_RUN) {
const result = await query(
`INSERT INTO tes_content.content_items (
id, type, slug, title, description,
clinical_context, level, priority, status,
source_guideline, version, latest_version,
content, tags,
created_by, updated_by
) VALUES (
gen_random_uuid(),
'guide',
$1, $2, $3,
'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',
$4::jsonb,
$5::text[],
$6, $6
)
RETURNING id, slug, title`,
[
guide.id,
guide.titulo,
guide.descripcion,
JSON.stringify(content),
['guia', 'formativo', guide.id],
adminId,
]
);
stats.guides.created++;
return { action: 'created', id: result.rows[0].id };
}
stats.guides.created++;
return { action: 'created', id: null };
} catch (error) {
console.error(`❌ Error sincronizando guía ${guide.id}:`, error.message);
stats.guides.errors++;
return { action: 'error', error: error.message };
}
}
/**
* Sincronizar relaciones bidireccionales desde protocol-guide-manual-mapping
*/
async function syncRelations(adminId) {
try {
console.log('📋 Sincronizando relaciones bidireccionales...');
// Verificar si existe tabla content_relations
const tableExists = await query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'tes_content'
AND table_name = 'content_relations'
)
`);
if (!tableExists.rows[0].exists) {
console.log(' ⚠️ Tabla content_relations no existe aún');
console.log(' Ejecuta las migraciones primero (ver VALIDACION_TECNICA_FASE_B_C.md)');
stats.relations.skipped++;
return;
}
// Importar mapping
try {
const mappingModule = await import('../../src/data/protocol-guide-manual-mapping.ts?t=' + Date.now());
const mappings = mappingModule.protocolGuideManualMapping || [];
let relationsCreated = 0;
for (const mapping of mappings) {
if (!mapping.protocoloId || !mapping.guiaId) continue;
// Buscar IDs de contenido
const protocolResult = await query(
`SELECT id FROM tes_content.content_items WHERE slug = $1 AND type = 'protocol'`,
[mapping.protocoloId]
);
const guideResult = await query(
`SELECT id FROM tes_content.content_items WHERE slug = $1 AND type = 'guide'`,
[mapping.guiaId]
);
if (protocolResult.rows.length === 0 || guideResult.rows.length === 0) {
continue; // Contenido no existe aún
}
const protocolId = protocolResult.rows[0].id;
const guideId = guideResult.rows[0].id;
// Verificar si relación ya existe
const existing = await query(
`SELECT id FROM tes_content.content_relations
WHERE source_content_id = $1 AND target_content_id = $2
AND relation_type = 'protocol_to_guide'`,
[protocolId, guideId]
);
if (existing.rows.length > 0) {
continue; // Ya existe
}
// Crear relación bidireccional
if (!DRY_RUN) {
// Relación protocolo → guía
await query(
`INSERT INTO tes_content.content_relations
(source_content_id, target_content_id, relation_type, is_bidirectional, created_by)
VALUES ($1, $2, 'protocol_to_guide', true, $3)`,
[protocolId, guideId, adminId]
);
// Relación guía → protocolo (bidireccional)
await query(
`INSERT INTO tes_content.content_relations
(source_content_id, target_content_id, relation_type, is_bidirectional, created_by)
VALUES ($1, $2, 'guide_to_protocol', true, $3)`,
[guideId, protocolId, adminId]
);
}
relationsCreated++;
}
stats.relations.created = relationsCreated;
console.log(`${relationsCreated} relaciones creadas`);
} catch (error) {
console.log(' ⚠️ No se pudieron cargar relaciones automáticamente');
console.log(' Las relaciones se pueden gestionar desde el admin panel');
}
} catch (error) {
console.error('❌ Error sincronizando relaciones:', error.message);
stats.relations.errors++;
}
}
/**
* Extraer array de strings desde código TypeScript
*/
function extractArray(str) {
if (!str) return [];
return str
.split(',')
.map(s => s.trim().replace(/^['"]|['"]$/g, ''))
.filter(Boolean);
}
/**
* Cargar procedures desde archivo
*/
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;
procedures.push({
id,
title,
shortTitle,
category,
subcategory: subcategory || null,
priority,
ageGroup,
steps: extractArray(stepsStr),
warnings: extractArray(warningsStr),
keyPoints: keyPointsStr ? extractArray(keyPointsStr) : [],
equipment: equipmentStr ? extractArray(equipmentStr) : [],
drugs: drugsStr ? extractArray(drugsStr) : [],
});
}
return procedures;
}
/**
* Cargar drugs desde archivo
*/
async function loadDrugs() {
const drugsPath = join(__dirname, '../../src/data/drugs.ts');
const content = await readFile(drugsPath, 'utf-8');
const drugs = [];
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;
drugs.push({
id,
genericName,
tradeName,
category,
presentation,
adultDose,
pediatricDose: pediatricDose || null,
routes: extractArray(routesStr),
dilution: dilution || null,
indications: extractArray(indicationsStr),
contraindications: extractArray(contraindicationsStr),
sideEffects: sideEffectsStr ? extractArray(sideEffectsStr) : [],
antidote: antidote || null,
notes: notesStr ? extractArray(notesStr) : [],
criticalPoints: criticalPointsStr ? extractArray(criticalPointsStr) : [],
source: source2 || source1 || null,
});
}
return drugs;
}
/**
* Cargar guides desde archivo
*/
async function loadGuides() {
const guidesPath = join(__dirname, '../../src/data/guides-index.ts');
const content = await readFile(guidesPath, 'utf-8');
const guides = [];
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;
// Buscar secciones (simplificado - extraer estructura básica)
const seccionesMatch = afterIcono.match(/secciones:\s*\[([\s\S]*?)\]/);
const secciones = seccionesMatch ? [] : []; // Por ahora vacío, se puede mejorar
guides.push({
id,
titulo,
descripcion,
icono,
scormAvailable,
secciones: secciones,
protocoloOperativo: null, // Se puede extraer si existe
});
}
return guides;
}
/**
* Cargar contenido desde archivos locales
*/
async function loadLocalContent() {
const content = {
procedures: [],
drugs: [],
guides: [],
};
try {
// Cargar procedures
if (!TYPE_FILTER || TYPE_FILTER === 'protocols') {
content.procedures = await loadProcedures();
}
// Cargar drugs
if (!TYPE_FILTER || TYPE_FILTER === 'drugs') {
content.drugs = await loadDrugs();
}
// Cargar guides
if (!TYPE_FILTER || TYPE_FILTER === 'guides') {
content.guides = await loadGuides();
}
} catch (error) {
console.error('❌ Error cargando contenido local:', error.message);
throw error;
}
return content;
}
/**
* Función principal
*/
async function main() {
console.log('🚀 SCRIPT DE SINCRONIZACIÓN MASIVA DE CONTENIDO\n');
console.log('═══════════════════════════════════════════════\n');
if (DRY_RUN) {
console.log('⚠️ MODO DRY-RUN: No se realizarán cambios en la BD\n');
}
if (FORCE) {
console.log('⚡ MODO FORCE: Se actualizarán todos los items (incluso sin cambios)\n');
}
if (TYPE_FILTER) {
console.log(`🔍 FILTRO: Solo sincronizando tipo "${TYPE_FILTER}"\n`);
}
try {
// 1. Obtener usuario admin
console.log('👤 Obteniendo usuario admin...');
const adminId = await getAdminUser();
console.log(` ✅ Usuario admin: ${adminId}\n`);
// 2. Cargar contenido local
console.log('📂 Cargando contenido desde archivos locales...');
const localContent = await loadLocalContent();
console.log(` ✅ Protocolos: ${localContent.procedures.length}`);
console.log(` ✅ Fármacos: ${localContent.drugs.length}`);
console.log(` ✅ Guías: ${localContent.guides.length}\n`);
// 3. Sincronizar protocolos
if (localContent.procedures.length > 0 && (!TYPE_FILTER || TYPE_FILTER === 'protocols')) {
console.log('📋 Sincronizando protocolos...');
for (const procedure of localContent.procedures) {
const result = await syncProtocol(procedure, adminId);
if (result.action === 'created') {
console.log(` ✅ Creado: ${procedure.title}`);
} else if (result.action === 'updated') {
console.log(` 🔄 Actualizado: ${procedure.title}`);
} else if (result.action === 'skipped') {
console.log(` ⏭️ Saltado: ${procedure.title} (sin cambios)`);
}
}
console.log('');
}
// 4. Sincronizar fármacos
if (localContent.drugs.length > 0 && (!TYPE_FILTER || TYPE_FILTER === 'drugs')) {
console.log('💊 Sincronizando fármacos...');
for (const drug of localContent.drugs) {
const result = await syncDrug(drug, adminId);
if (result.action === 'created') {
console.log(` ✅ Creado: ${drug.genericName}`);
} else if (result.action === 'updated') {
console.log(` 🔄 Actualizado: ${drug.genericName}`);
} else if (result.action === 'skipped') {
console.log(` ⏭️ Saltado: ${drug.genericName} (sin cambios)`);
}
}
console.log('');
}
// 5. Sincronizar guías
if (localContent.guides.length > 0 && (!TYPE_FILTER || TYPE_FILTER === 'guides')) {
console.log('📚 Sincronizando guías...');
for (const guide of localContent.guides) {
const result = await syncGuide(guide, adminId);
if (result.action === 'created') {
console.log(` ✅ Creado: ${guide.titulo}`);
} else if (result.action === 'updated') {
console.log(` 🔄 Actualizado: ${guide.titulo}`);
} else if (result.action === 'skipped') {
console.log(` ⏭️ Saltado: ${guide.titulo} (sin cambios)`);
}
}
console.log('');
}
// 6. Sincronizar relaciones (opcional)
if (!TYPE_FILTER) {
await syncRelations(adminId);
console.log('');
}
// 7. Mostrar resumen
console.log('═══════════════════════════════════════════════');
console.log('📊 RESUMEN DE SINCRONIZACIÓN\n');
const totalCreated = stats.protocols.created + stats.drugs.created + stats.guides.created;
const totalUpdated = stats.protocols.updated + stats.drugs.updated + stats.guides.updated;
const totalSkipped = stats.protocols.skipped + stats.drugs.skipped + stats.guides.skipped;
const totalErrors = stats.protocols.errors + stats.drugs.errors + stats.guides.errors;
console.log('Protocolos:');
console.log(` ✅ Creados: ${stats.protocols.created}`);
console.log(` 🔄 Actualizados: ${stats.protocols.updated}`);
console.log(` ⏭️ Saltados: ${stats.protocols.skipped}`);
console.log(` ❌ Errores: ${stats.protocols.errors}\n`);
console.log('Fármacos:');
console.log(` ✅ Creados: ${stats.drugs.created}`);
console.log(` 🔄 Actualizados: ${stats.drugs.updated}`);
console.log(` ⏭️ Saltados: ${stats.drugs.skipped}`);
console.log(` ❌ Errores: ${stats.drugs.errors}\n`);
console.log('Guías:');
console.log(` ✅ Creadas: ${stats.guides.created}`);
console.log(` 🔄 Actualizadas: ${stats.guides.updated}`);
console.log(` ⏭️ Saltadas: ${stats.guides.skipped}`);
console.log(` ❌ Errores: ${stats.guides.errors}\n`);
console.log('═══════════════════════════════════════════════');
console.log(`TOTAL: ${totalCreated} creados, ${totalUpdated} actualizados, ${totalSkipped} saltados, ${totalErrors} errores\n`);
if (DRY_RUN) {
console.log('⚠️ MODO DRY-RUN: No se realizaron cambios reales\n');
} else {
console.log('✅ Sincronización completada\n');
}
} catch (error) {
console.error('\n❌ Error en sincronización:', error);
process.exit(1);
}
}
// Ejecutar
main();