850 lines
28 KiB
JavaScript
850 lines
28 KiB
JavaScript
/**
|
||
* 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();
|
||
|