implementación de la capa de aplicación y ajustes en infraestructura
This commit is contained in:
21
src/application/sim-controller.ts
Normal file
21
src/application/sim-controller.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { type Request, type Response, type NextFunction } from 'express';
|
||||
import { SimUsecases } from './sim-usecases.js';
|
||||
import ejs from 'ejs';
|
||||
import path from 'path';
|
||||
|
||||
export class SimController {
|
||||
constructor(private usecases: SimUsecases) {}
|
||||
|
||||
renderDashboard = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const proyectos = await this.usecases.getDashboardData();
|
||||
const filePath = path.join(process.cwd(), 'src/views/index.ejs');
|
||||
ejs.renderFile(filePath, { proyectos }, (err, str) => {
|
||||
if (err) return next(err);
|
||||
res.send(str);
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/application/sim-usecases.ts
Normal file
57
src/application/sim-usecases.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import axios from 'axios';
|
||||
import { type Pool } from 'pg';
|
||||
import { type Proyecto, RepositoryError } from '../domain/common.js';
|
||||
|
||||
export class SimUsecases {
|
||||
constructor(private db: Pool) {}
|
||||
|
||||
async checkAllProjectsHealth(): Promise<void> {
|
||||
try {
|
||||
const { rows: projects } = await this.db.query<Proyecto>('SELECT * FROM proyectos');
|
||||
|
||||
const checks = projects.map(async (p) => {
|
||||
let status: Proyecto['estado_codigo'] = 'OK';
|
||||
try {
|
||||
// Solo 200 es válido: cualquier otro código (301, 403, 5xx) indica un problema real del servicio.
|
||||
await axios.get(p.url_health, { timeout: 5000, validateStatus: (s) => s === 200 });
|
||||
} catch (e) {
|
||||
const axiosError = e as { code?: string };
|
||||
status = axiosError.code === 'ECONNABORTED' ? 'TIMEOUT' : 'ERROR';
|
||||
}
|
||||
|
||||
return this.db.query(
|
||||
'INSERT INTO estados (proyecto_id, estado_codigo, fecha) VALUES ($1, $2, NOW())',
|
||||
[p.id, status]
|
||||
);
|
||||
});
|
||||
|
||||
// allSettled en lugar de all: un fallo al insertar en BD no debe cancelar el resto de checks.
|
||||
await Promise.allSettled(checks);
|
||||
} catch (err) {
|
||||
throw new RepositoryError('Fallo masivo en el chequeo de salud', { cause: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
async getDashboardData(): Promise<Proyecto[]> {
|
||||
// LEFT JOIN LATERAL nos deja calcular "la última fila de estados" para cada proyecto.
|
||||
// Usamos LEFT JOIN para no perder proyectos que todavía no tienen estados registrados.
|
||||
const query = `
|
||||
SELECT p.id, p.nombre, p.url_health, e.estado_codigo, e.fecha
|
||||
FROM proyectos p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT estado_codigo, fecha
|
||||
FROM estados
|
||||
WHERE proyecto_id = p.id
|
||||
ORDER BY fecha DESC
|
||||
LIMIT 1
|
||||
) e ON true
|
||||
ORDER BY p.nombre ASC;
|
||||
`;
|
||||
try {
|
||||
const { rows } = await this.db.query<Proyecto>(query);
|
||||
return rows;
|
||||
} catch (err) {
|
||||
throw new RepositoryError('Error al recuperar datos del dashboard', { cause: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
export const pool = new pg.Pool({
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: Number(process.env.DB_PORT) || 5432,
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'password',
|
||||
database: process.env.DB_NAME || 'monitor_db'
|
||||
});
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME
|
||||
});
|
||||
|
||||
@@ -4,8 +4,6 @@ export class AppError extends Error {
|
||||
this.name = 'AppError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError { constructor(msg: string, details = {}) { super(msg, 400, details); this.name = 'ValidationError'; } }
|
||||
export class RepositoryError extends AppError { constructor(msg: string, details = {}) { super(msg, 500, details); this.name = 'RepositoryError'; } }
|
||||
|
||||
export interface Proyecto {
|
||||
|
||||
10
src/infrastructure/sim-routes-http.ts
Normal file
10
src/infrastructure/sim-routes-http.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import { SimController } from '../application/sim-controller.js';
|
||||
import { SimUsecases } from '../application/sim-usecases.js';
|
||||
|
||||
export function createRouter(usecases: SimUsecases): Router {
|
||||
const router = Router();
|
||||
const controller = new SimController(usecases);
|
||||
router.get('/', controller.renderDashboard);
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user