602 lines
18 KiB
JavaScript
602 lines
18 KiB
JavaScript
/**
|
|
* Script de migración COMPLETA: Importa TODO el contenido de la app al backend
|
|
*
|
|
* Migra:
|
|
* - Todos los procedimientos (procedures.ts)
|
|
* - Todos los fármacos (drugs.ts) - 6 fármacos completos
|
|
* - Checklists de material (material-checklists.ts)
|
|
* - Guías de refuerzo (guides-index.ts)
|
|
*/
|
|
|
|
import { query } from '../config/database.js';
|
|
import 'dotenv/config';
|
|
import { readFile } from 'fs/promises';
|
|
import { fileURLToPath } from 'url';
|
|
import { dirname, join } from 'path';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
// Mapeo de prioridades
|
|
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() {
|
|
let result = await query(
|
|
`SELECT id FROM tes_content.users WHERE role = 'super_admin' LIMIT 1`
|
|
);
|
|
|
|
if (result.rows.length === 0) {
|
|
result = await query(
|
|
`SELECT id FROM emerges_content.users WHERE role = 'super_admin' LIMIT 1`
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Lee y parsea procedures.ts usando regex mejorado
|
|
*/
|
|
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;
|
|
|
|
const steps = extractArray(stepsStr);
|
|
const warnings = extractArray(warningsStr);
|
|
const keyPoints = keyPointsStr ? extractArray(keyPointsStr) : [];
|
|
const equipment = equipmentStr ? extractArray(equipmentStr) : [];
|
|
const drugs = drugsStr ? extractArray(drugsStr) : [];
|
|
|
|
procedures.push({
|
|
id,
|
|
title,
|
|
shortTitle,
|
|
category,
|
|
subcategory: subcategory || null,
|
|
priority,
|
|
ageGroup,
|
|
steps,
|
|
warnings,
|
|
keyPoints,
|
|
equipment,
|
|
drugs
|
|
});
|
|
}
|
|
|
|
return procedures;
|
|
}
|
|
|
|
/**
|
|
* Lee y parsea drugs.ts - VERSIÓN MEJORADA para todos los fármacos
|
|
*/
|
|
async function loadDrugs() {
|
|
const drugsPath = join(__dirname, '../../src/data/drugs.ts');
|
|
const content = await readFile(drugsPath, 'utf-8');
|
|
|
|
const drugs = [];
|
|
|
|
// Regex mejorado que captura todos los campos, incluyendo arrays complejos
|
|
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;
|
|
|
|
const routes = extractArray(routesStr);
|
|
const indications = extractArray(indicationsStr);
|
|
const contraindications = extractArray(contraindicationsStr);
|
|
const sideEffects = sideEffectsStr ? extractArray(sideEffectsStr) : [];
|
|
const notes = notesStr ? extractArray(notesStr) : [];
|
|
const criticalPoints = criticalPointsStr ? extractArray(criticalPointsStr) : [];
|
|
const source = source2 || source1 || null;
|
|
|
|
drugs.push({
|
|
id,
|
|
genericName,
|
|
tradeName,
|
|
category,
|
|
presentation,
|
|
adultDose,
|
|
pediatricDose: pediatricDose || null,
|
|
routes,
|
|
dilution: dilution || null,
|
|
indications,
|
|
contraindications,
|
|
sideEffects,
|
|
antidote: antidote || null,
|
|
notes,
|
|
criticalPoints,
|
|
source
|
|
});
|
|
}
|
|
|
|
return drugs;
|
|
}
|
|
|
|
/**
|
|
* Lee y parsea guides-index.ts
|
|
*/
|
|
async function loadGuides() {
|
|
const guidesPath = join(__dirname, '../../src/data/guides-index.ts');
|
|
const content = await readFile(guidesPath, 'utf-8');
|
|
|
|
const guides = [];
|
|
|
|
// Buscar cada objeto guía usando un patrón más simple
|
|
// Buscar: id: "...", titulo: "...", descripcion: "...", icono: "..."
|
|
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;
|
|
|
|
guides.push({
|
|
id,
|
|
titulo,
|
|
descripcion,
|
|
icono,
|
|
scormAvailable,
|
|
seccionesCount: 8 // Todas las guías tienen 8 secciones
|
|
});
|
|
}
|
|
|
|
return guides;
|
|
}
|
|
|
|
/**
|
|
* Lee y parsea material-checklists.ts
|
|
*/
|
|
async function loadChecklists() {
|
|
const checklistsPath = join(__dirname, '../../src/data/material-checklists.ts');
|
|
const content = await readFile(checklistsPath, 'utf-8');
|
|
|
|
const checklists = [];
|
|
|
|
// Buscar definiciones de checklists
|
|
const checklistRegex = /export const (\w+): MaterialChecklist = ([\s\S]*?);/g;
|
|
|
|
let match;
|
|
while ((match = checklistRegex.exec(content)) !== null) {
|
|
const [, varName, checklistContent] = match;
|
|
|
|
// Extraer campos básicos
|
|
const idMatch = checklistContent.match(/id:\s*['"]([^'"]+)['"]/);
|
|
const titleMatch = checklistContent.match(/title:\s*['"]([^'"]+)['"]/);
|
|
const shortTitleMatch = checklistContent.match(/shortTitle:\s*['"]([^'"]+)['"]/);
|
|
const phaseMatch = checklistContent.match(/phase:\s*['"]([^'"]+)['"]/);
|
|
const descMatch = checklistContent.match(/description:\s*['"]([^'"]+)['"]/);
|
|
|
|
if (idMatch && titleMatch) {
|
|
checklists.push({
|
|
id: idMatch[1],
|
|
title: titleMatch[1],
|
|
shortTitle: shortTitleMatch ? shortTitleMatch[1] : titleMatch[1],
|
|
phase: phaseMatch ? phaseMatch[1] : null,
|
|
description: descMatch ? descMatch[1] : '',
|
|
content: checklistContent // Guardar contenido completo para procesar después
|
|
});
|
|
}
|
|
}
|
|
|
|
return checklists;
|
|
}
|
|
|
|
/**
|
|
* Extrae elementos de un array de strings
|
|
*/
|
|
function extractArray(str) {
|
|
if (!str) return [];
|
|
const matches = str.match(/['"]([^'"]+)['"]/g);
|
|
return matches ? matches.map(m => m.replace(/['"]/g, '')) : [];
|
|
}
|
|
|
|
/**
|
|
* Inserta un procedimiento en la BD
|
|
*/
|
|
async function insertProcedure(procedure, adminId) {
|
|
let clinicalContext = 'OTROS';
|
|
if (procedure.subcategory && procedureCategoryMap[procedure.category]?.[procedure.subcategory]) {
|
|
clinicalContext = procedureCategoryMap[procedure.category][procedure.subcategory];
|
|
}
|
|
|
|
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) {
|
|
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];
|
|
}
|
|
|
|
/**
|
|
* Inserta una guía en la BD
|
|
*/
|
|
async function insertGuide(guide, adminId) {
|
|
const content = {
|
|
description: guide.descripcion,
|
|
icono: guide.icono,
|
|
scormAvailable: guide.scormAvailable,
|
|
seccionesCount: guide.seccionesCount,
|
|
secciones: [] // Se puede expandir después con las secciones reales
|
|
};
|
|
|
|
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(),
|
|
'guide',
|
|
$1,
|
|
$2,
|
|
$3,
|
|
$4,
|
|
'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',
|
|
$5::jsonb,
|
|
$6::text[],
|
|
'guide',
|
|
$7,
|
|
$7
|
|
)
|
|
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
|
|
`, [
|
|
guide.id,
|
|
guide.titulo,
|
|
guide.titulo,
|
|
guide.descripcion,
|
|
JSON.stringify(content),
|
|
['guide', 'formativo', guide.icono].filter(Boolean),
|
|
adminId
|
|
]);
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
/**
|
|
* Inserta un checklist en la BD
|
|
*/
|
|
async function insertChecklist(checklist, adminId) {
|
|
const content = {
|
|
phase: checklist.phase,
|
|
description: checklist.description,
|
|
sections: [] // Se puede expandir después
|
|
};
|
|
|
|
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(),
|
|
'checklist',
|
|
$1,
|
|
$2,
|
|
$3,
|
|
$4,
|
|
'OTROS'::tes_content.clinical_context,
|
|
'operativo'::tes_content.usage_type,
|
|
'media'::tes_content.priority,
|
|
'published'::tes_content.content_status,
|
|
'INTERNO'::tes_content.source_guideline,
|
|
'1.0.0',
|
|
'1.0.0',
|
|
$5::jsonb,
|
|
$6::text[],
|
|
'checklist',
|
|
$7,
|
|
$7
|
|
)
|
|
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
|
|
`, [
|
|
checklist.id,
|
|
checklist.title,
|
|
checklist.shortTitle,
|
|
checklist.description,
|
|
JSON.stringify(content),
|
|
['checklist', checklist.phase].filter(Boolean),
|
|
adminId
|
|
]);
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
/**
|
|
* Función principal
|
|
*/
|
|
async function migrateAllContent() {
|
|
try {
|
|
console.log('🔄 Iniciando migración COMPLETA de contenido...\n');
|
|
|
|
await query('SELECT 1');
|
|
console.log('✅ Conexión a base de datos establecida\n');
|
|
|
|
const adminId = await getAdminId();
|
|
console.log(`✅ Usuario admin: ${adminId}\n`);
|
|
|
|
// 1. PROCEDIMIENTOS
|
|
console.log('📋 Migrando PROCEDIMIENTOS...');
|
|
const procedures = await loadProcedures();
|
|
console.log(` Encontrados ${procedures.length} procedimientos\n`);
|
|
|
|
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('');
|
|
|
|
// 2. FÁRMACOS (TODOS)
|
|
console.log('💊 Migrando FÁRMACOS (TODOS)...');
|
|
const drugs = await loadDrugs();
|
|
console.log(` Encontrados ${drugs.length} fármacos\n`);
|
|
|
|
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('');
|
|
|
|
// 3. GUÍAS
|
|
console.log('📚 Migrando GUÍAS...');
|
|
try {
|
|
const guides = await loadGuides();
|
|
console.log(` Encontradas ${guides.length} guías\n`);
|
|
|
|
for (const guide of guides) {
|
|
try {
|
|
const result = await insertGuide(guide, adminId);
|
|
console.log(` ✅ ${result.slug}: ${result.title}`);
|
|
} catch (error) {
|
|
console.error(` ❌ Error migrando ${guide.id}:`, error.message);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log(` ⚠️ No se pudieron cargar guías: ${error.message}`);
|
|
}
|
|
console.log('');
|
|
|
|
// 4. CHECKLISTS
|
|
console.log('✅ Migrando CHECKLISTS...');
|
|
try {
|
|
const checklists = await loadChecklists();
|
|
console.log(` Encontrados ${checklists.length} checklists\n`);
|
|
|
|
for (const checklist of checklists) {
|
|
try {
|
|
const result = await insertChecklist(checklist, adminId);
|
|
console.log(` ✅ ${result.slug}: ${result.title}`);
|
|
} catch (error) {
|
|
console.error(` ❌ Error migrando ${checklist.id}:`, error.message);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log(` ⚠️ No se pudieron cargar checklists: ${error.message}`);
|
|
}
|
|
console.log('');
|
|
|
|
// RESUMEN FINAL
|
|
console.log('📊 RESUMEN FINAL:');
|
|
const stats = await query(`
|
|
SELECT
|
|
type,
|
|
COUNT(*) as total,
|
|
COUNT(CASE WHEN status = 'published' THEN 1 END) as published
|
|
FROM tes_content.content_items
|
|
GROUP BY type
|
|
ORDER BY type
|
|
`);
|
|
|
|
stats.rows.forEach(row => {
|
|
console.log(` ${row.type}: ${row.total} total, ${row.published} publicados`);
|
|
});
|
|
|
|
const total = stats.rows.reduce((sum, row) => sum + parseInt(row.total), 0);
|
|
console.log(`\n 📦 TOTAL: ${total} items migrados\n`);
|
|
|
|
console.log('✅ Migración COMPLETA finalizada exitosamente!');
|
|
|
|
} catch (error) {
|
|
console.error('❌ Error durante la migración:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
migrateAllContent();
|
|
|