diff --git a/api-v3/src/cli/fresh-start-reset.ts b/api-v3/src/cli/fresh-start-reset.ts deleted file mode 100644 index 6ec6a82..0000000 --- a/api-v3/src/cli/fresh-start-reset.ts +++ /dev/null @@ -1,267 +0,0 @@ -import 'reflect-metadata'; - -import { promises as fs } from 'node:fs'; -import { migrationDataSource } from '../database/data-source'; - -const APPLY_TOKEN = 'DHV2-FRESH-START-20260905'; - -const PRESERVED_TABLES = new Set([ - 'typeorm_migrations', - 'spatial_ref_sys', - 'users', - 'user_roles', - 'roles', - 'role_permissions', - 'permissions', - 'asset_types', - 'asset_type_parent_rules', - 'asset_attribute_definitions', - 'finding_categories', - 'finding_catalog_items', - 'finding_catalog_item_versions', - 'finding_catalog_item_asset_types', - 'finding_catalog_asset_type_profiles', - 'institutional_delivery_settings', -]); - -type AdminRow = { - id: string; - username: string; - email: string | null; - status: string; -}; - -type TableRow = { table_name: string }; -type CountRow = { count: string | number }; - -function quoteIdentifier(value: string): string { - return `"${value.replaceAll('"', '""')}"`; -} - -function requestedMode(): 'preview' | 'apply' { - const args = process.argv.slice(2); - if (args.length === 1 && args[0] === '--preview') return 'preview'; - if (args.length === 2 && args[0] === '--apply' && args[1] === APPLY_TOKEN) { - return 'apply'; - } - throw new Error( - `Uso inválido. Permitido: --preview o --apply ${APPLY_TOKEN}`, - ); -} - -async function rowCount(table: string): Promise { - const result = (await migrationDataSource.query( - `SELECT COUNT(*)::bigint AS count FROM public.${quoteIdentifier(table)}`, - )) as CountRow[]; - return Number(result[0]?.count ?? 0); -} - -async function clearMediaStorage(): Promise { - const root = process.env.ASSET_MEDIA_ROOT; - if (!root) { - process.stdout.write('MEDIA: ASSET_MEDIA_ROOT no configurado; no se tocaron archivos.\n'); - return; - } - - try { - const entries = await fs.readdir(root, { withFileTypes: true }); - for (const entry of entries) { - await fs.rm(`${root}/${entry.name}`, { recursive: true, force: true }); - } - await fs.mkdir(`${root}/imports`, { recursive: true }); - process.stdout.write(`MEDIA: almacenamiento limpiado en ${root}\n`); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - process.stdout.write(`MEDIA WARNING: no se pudo limpiar completamente ${root}: ${message}\n`); - } -} - -async function main(): Promise { - const mode = requestedMode(); - await migrationDataSource.initialize(); - - try { - await migrationDataSource.query( - `SELECT pg_advisory_lock(hashtext('dhv2-fresh-start-reset'))`, - ); - - const admins = (await migrationDataSource.query(` - SELECT DISTINCT user_row.id, user_row.username, user_row.email, user_row.status - FROM users user_row - INNER JOIN user_roles user_role ON user_role.user_id = user_row.id - INNER JOIN roles role ON role.id = user_role.role_id - WHERE role.code = 'admin' - ORDER BY user_row.username - `)) as AdminRow[]; - - if (admins.length !== 1) { - throw new Error( - `Se esperaba exactamente 1 usuario con rol admin y se encontraron ${admins.length}. Limpieza abortada.`, - ); - } - - const admin = admins[0]; - const tables = (await migrationDataSource.query(` - SELECT table_name - FROM information_schema.tables - WHERE table_schema = 'public' - AND table_type = 'BASE TABLE' - ORDER BY table_name - `)) as TableRow[]; - - const existingTables = tables.map((row) => row.table_name); - const tablesToClear = existingTables.filter( - (table) => !PRESERVED_TABLES.has(table), - ); - const tablesToPreserve = existingTables.filter((table) => - PRESERVED_TABLES.has(table), - ); - - process.stdout.write('\n============================================================\n'); - process.stdout.write(` DH V2 · FRESH START · ${mode.toUpperCase()}\n`); - process.stdout.write('============================================================\n'); - process.stdout.write( - `ADMIN PRESERVADO: ${admin.username} · ${admin.email ?? 'sin email'} · ${admin.id}\n`, - ); - - process.stdout.write('\n========== TABLAS ESTRUCTURALES CONSERVADAS ==========\n'); - for (const table of tablesToPreserve) { - process.stdout.write(`KEEP ${table.padEnd(46)} ${await rowCount(table)}\n`); - } - process.stdout.write('RESET institutional_delivery_settings emails/updated_by\n'); - - process.stdout.write('\n========== DATOS A ELIMINAR ==========\n'); - let rowsToDelete = 0; - for (const table of tablesToClear) { - const count = await rowCount(table); - rowsToDelete += count; - process.stdout.write(`CLEAR ${table.padEnd(46)} ${count}\n`); - } - - const userCount = await rowCount('users'); - const nonAdminUsers = Math.max(0, userCount - 1); - process.stdout.write(`CLEAR ${'users (excepto admin)'.padEnd(46)} ${nonAdminUsers}\n`); - rowsToDelete += nonAdminUsers; - process.stdout.write(`TOTAL FILAS OPERATIVAS/USUARIOS A RETIRAR: ${rowsToDelete}\n`); - - if (mode === 'preview') { - process.stdout.write('\nPREVIEW_OK: no se modificó ningún dato.\n'); - return; - } - - await migrationDataSource.transaction(async (manager) => { - // Se vacía el grafo operacional completo sin depender del orden físico - // de las FK. La estructura maestra está excluida por PRESERVED_TABLES. - await manager.query(`SET LOCAL session_replication_role = replica`); - - for (const table of tablesToClear) { - await manager.query(`DELETE FROM public.${quoteIdentifier(table)}`); - } - - // Antes de tocar cualquier tabla preservada reactivamos las FK reales. - // Si apareciera una dependencia RESTRICT inesperada, PostgreSQL aborta - // la transacción completa en lugar de dejar referencias huérfanas. - await manager.query(`SET LOCAL session_replication_role = origin`); - - await manager.query(` - UPDATE institutional_delivery_settings - SET office_email = NULL, - director_email = NULL, - updated_by = NULL, - updated_at = CURRENT_TIMESTAMP - WHERE id = 1 - `); - - await manager.query( - `DELETE FROM user_roles WHERE user_id <> $1::uuid`, - [admin.id], - ); - await manager.query( - ` - DELETE FROM user_roles user_role - USING roles role - WHERE user_role.user_id = $1::uuid - AND role.id = user_role.role_id - AND role.code <> 'admin' - `, - [admin.id], - ); - await manager.query(`DELETE FROM users WHERE id <> $1::uuid`, [admin.id]); - await manager.query( - ` - UPDATE users - SET status = 'ACTIVE', - failed_login_attempts = 0, - locked_until = NULL, - created_by = CASE WHEN created_by = $1::uuid THEN created_by ELSE NULL END, - updated_by = $1::uuid, - updated_at = CURRENT_TIMESTAMP - WHERE id = $1::uuid - `, - [admin.id], - ); - await manager.query( - ` - UPDATE user_roles user_role - SET assigned_by = $1::uuid - FROM roles role - WHERE user_role.user_id = $1::uuid - AND role.id = user_role.role_id - AND role.code = 'admin' - `, - [admin.id], - ); - }); - - const usersAfter = await rowCount('users'); - if (usersAfter !== 1) { - throw new Error(`Verificación falló: users=${usersAfter}, esperado=1`); - } - - const adminAfter = (await migrationDataSource.query( - ` - SELECT COUNT(DISTINCT user_row.id)::integer AS count - FROM users user_row - INNER JOIN user_roles user_role ON user_role.user_id = user_row.id - INNER JOIN roles role ON role.id = user_role.role_id - WHERE role.code = 'admin' - `, - )) as CountRow[]; - if (Number(adminAfter[0]?.count ?? 0) !== 1) { - throw new Error('Verificación falló: el administrador no quedó correctamente asignado'); - } - - for (const table of tablesToClear) { - const count = await rowCount(table); - if (count !== 0) { - throw new Error(`Verificación falló: ${table} conserva ${count} fila(s)`); - } - } - - await clearMediaStorage(); - - process.stdout.write('\n========== VERIFICACIÓN FRESH START ==========\n'); - process.stdout.write(`Usuarios: 1 (${admin.username})\n`); - process.stdout.write('Datos operativos/importados: 0\n'); - process.stdout.write('Roles/permisos/tipos/catálogo base: conservados\n'); - process.stdout.write('Configuración institucional editable: reiniciada\n'); - process.stdout.write('FRESH_START_OK\n'); - } finally { - if (migrationDataSource.isInitialized) { - try { - await migrationDataSource.query( - `SELECT pg_advisory_unlock(hashtext('dhv2-fresh-start-reset'))`, - ); - } catch { - // La conexión se destruirá de todas formas. - } - await migrationDataSource.destroy(); - } - } -} - -main().catch((error: unknown) => { - const message = error instanceof Error ? error.message : String(error); - process.stderr.write(`FRESH_START_ERROR: ${message}\n`); - process.exitCode = 1; -});