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

850 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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