feat(postgres): migrate all route files from SQLite to pg pool
- All 16 route files now import pool from ../db directly
- Removed db parameter from all factory functions
- All callbacks replaced with async/await pool.query()
- All ? placeholders converted to $1, $2... numbered params
- datetime('now') → NOW(), INSERT OR IGNORE → ON CONFLICT DO NOTHING
- LIKE → ILIKE for case-insensitive searches
- Error detection: err.code === '23505' for unique violations
- server.js no longer passes pool/db/requireAuth to route factories
- Only ivantiFindings.js still receives pool (pending task 8 rewrite)
This commit is contained in:
@@ -1,19 +1,12 @@
|
||||
// Ivanti Archive Routes — list, stats, and transition history for archived findings
|
||||
const express = require('express');
|
||||
const pool = require('../db');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
|
||||
const VALID_STATES = ['ACTIVE', 'ARCHIVED', 'RETURNED', 'CLOSED'];
|
||||
|
||||
/**
|
||||
* Find the most severe active finding related to an archived finding.
|
||||
*
|
||||
* A match requires:
|
||||
* - Exact hostname match (case-sensitive)
|
||||
* - The archive title is a case-insensitive substring of the active title, or vice versa
|
||||
* - The active finding ID differs from the archive's finding_id
|
||||
*
|
||||
* @param {Object} archive - Archive record from ivanti_finding_archives
|
||||
* @param {Array} activeFindings - Parsed entries from ivanti_findings_cache
|
||||
* @returns {{ id: string, title: string, severity: number } | null}
|
||||
*/
|
||||
function findRelatedActive(archive, activeFindings) {
|
||||
const archiveTitle = (archive.finding_title || '').toLowerCase();
|
||||
@@ -34,21 +27,13 @@ function findRelatedActive(archive, activeFindings) {
|
||||
return { id: best.id, title: best.title, severity: best.severity };
|
||||
}
|
||||
|
||||
function createIvantiArchiveRouter(db, requireAuth) {
|
||||
function createIvantiArchiveRouter() {
|
||||
const router = express.Router();
|
||||
|
||||
// All routes require authentication
|
||||
router.use(requireAuth(db));
|
||||
router.use(requireAuth());
|
||||
|
||||
/**
|
||||
* GET /
|
||||
* List archive records with optional state filtering.
|
||||
*
|
||||
* @query {string} [state] - Filter by lifecycle state (ACTIVE, ARCHIVED, RETURNED, CLOSED)
|
||||
* @returns {Object} 200 - { archives: Array<ArchiveRecord>, total: number }
|
||||
* @returns {Object} 400 - { error: string } when state param is invalid
|
||||
* @returns {Object} 500 - { error: string } on database failure
|
||||
*/
|
||||
// GET / — List archive records with optional state filtering
|
||||
router.get('/', async (req, res) => {
|
||||
const { state } = req.query;
|
||||
|
||||
@@ -61,43 +46,27 @@ function createIvantiArchiveRouter(db, requireAuth) {
|
||||
try {
|
||||
let query = 'SELECT * FROM ivanti_finding_archives';
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (state) {
|
||||
query += ' WHERE current_state = ?';
|
||||
query += ` WHERE current_state = $${paramIndex++}`;
|
||||
params.push(state);
|
||||
}
|
||||
|
||||
query += ' ORDER BY last_transition_at DESC';
|
||||
|
||||
const archives = await new Promise((resolve, reject) => {
|
||||
db.all(query, params, (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
});
|
||||
});
|
||||
const { rows: archives } = await pool.query(query, params);
|
||||
|
||||
// Fetch and parse active findings cache for related-finding enrichment
|
||||
// Fetch active findings for related-finding enrichment
|
||||
// In the new schema, active findings are in ivanti_findings table
|
||||
let activeFindings = [];
|
||||
try {
|
||||
const cacheRow = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT findings_json FROM ivanti_findings_cache WHERE id = 1',
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (cacheRow && cacheRow.findings_json) {
|
||||
activeFindings = JSON.parse(cacheRow.findings_json);
|
||||
}
|
||||
const { rows: findingsRows } = await pool.query(
|
||||
`SELECT id, title, host_name AS "hostName", severity FROM ivanti_findings WHERE state = 'open'`
|
||||
);
|
||||
activeFindings = findingsRows;
|
||||
} catch (cacheErr) {
|
||||
console.warn('Failed to load findings cache for related-active matching:', cacheErr);
|
||||
}
|
||||
|
||||
if (!Array.isArray(activeFindings)) {
|
||||
activeFindings = [];
|
||||
console.warn('Failed to load findings for related-active matching:', cacheErr);
|
||||
}
|
||||
|
||||
// Enrich each archive record with related active finding info
|
||||
@@ -113,52 +82,28 @@ function createIvantiArchiveRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /stats
|
||||
* Summary counts of archive records by lifecycle state.
|
||||
* ACTIVE is implicit: live findings in the cache that have no ARCHIVED/RETURNED archive record.
|
||||
*
|
||||
* @returns {Object} 200 - { ACTIVE: number, ARCHIVED: number, RETURNED: number, CLOSED: number, total: number }
|
||||
* @returns {Object} 500 - { error: string } on database failure
|
||||
*/
|
||||
// GET /stats — Summary counts by lifecycle state
|
||||
router.get('/stats', async (req, res) => {
|
||||
try {
|
||||
// Count archive records by state
|
||||
const rows = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT current_state, COUNT(*) as count
|
||||
FROM ivanti_finding_archives
|
||||
GROUP BY current_state`,
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
}
|
||||
);
|
||||
});
|
||||
const { rows } = await pool.query(
|
||||
`SELECT current_state, COUNT(*) as count
|
||||
FROM ivanti_finding_archives
|
||||
GROUP BY current_state`
|
||||
);
|
||||
|
||||
const stats = { ACTIVE: 0, ARCHIVED: 0, RETURNED: 0, CLOSED: 0 };
|
||||
|
||||
for (const row of rows) {
|
||||
if (stats.hasOwnProperty(row.current_state)) {
|
||||
stats[row.current_state] = row.count;
|
||||
stats[row.current_state] = parseInt(row.count);
|
||||
}
|
||||
}
|
||||
|
||||
// Compute ACTIVE: total live findings minus those with ARCHIVED or RETURNED records
|
||||
const cacheRow = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT total FROM ivanti_findings_cache WHERE id = 1',
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const liveFindingsCount = (cacheRow && cacheRow.total) || 0;
|
||||
// Findings that are ARCHIVED or RETURNED are "missing" from the live set,
|
||||
// so ACTIVE = live count (all findings currently present in sync results)
|
||||
stats.ACTIVE = liveFindingsCount;
|
||||
// ACTIVE = total live findings count
|
||||
const countResult = await pool.query(
|
||||
`SELECT COUNT(*) as total FROM ivanti_findings WHERE state = 'open'`
|
||||
);
|
||||
stats.ACTIVE = parseInt(countResult.rows[0].total) || 0;
|
||||
|
||||
const total = stats.ACTIVE + stats.ARCHIVED + stats.RETURNED + stats.CLOSED;
|
||||
|
||||
@@ -169,46 +114,27 @@ function createIvantiArchiveRouter(db, requireAuth) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /:findingId/history
|
||||
* Transition history for a specific archived finding, ordered by most recent first.
|
||||
* Returns an empty transitions array if the finding has no archive record.
|
||||
*
|
||||
* @param {string} findingId - Ivanti finding identifier (route param)
|
||||
* @returns {Object} 200 - { finding_id: string, transitions: Array<TransitionRecord> }
|
||||
* @returns {Object} 500 - { error: string } on database failure
|
||||
*/
|
||||
// GET /:findingId/history — Transition history for a specific archived finding
|
||||
router.get('/:findingId/history', async (req, res) => {
|
||||
const { findingId } = req.params;
|
||||
|
||||
try {
|
||||
const archive = await new Promise((resolve, reject) => {
|
||||
db.get(
|
||||
'SELECT id FROM ivanti_finding_archives WHERE finding_id = ?',
|
||||
[findingId],
|
||||
(err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
}
|
||||
);
|
||||
});
|
||||
const { rows: archiveRows } = await pool.query(
|
||||
'SELECT id FROM ivanti_finding_archives WHERE finding_id = $1',
|
||||
[findingId]
|
||||
);
|
||||
const archive = archiveRows[0];
|
||||
|
||||
if (!archive) {
|
||||
return res.json({ finding_id: findingId, transitions: [] });
|
||||
}
|
||||
|
||||
const transitions = await new Promise((resolve, reject) => {
|
||||
db.all(
|
||||
`SELECT * FROM ivanti_archive_transitions
|
||||
WHERE archive_id = ?
|
||||
ORDER BY transitioned_at DESC`,
|
||||
[archive.id],
|
||||
(err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows || []);
|
||||
}
|
||||
);
|
||||
});
|
||||
const { rows: transitions } = await pool.query(
|
||||
`SELECT * FROM ivanti_archive_transitions
|
||||
WHERE archive_id = $1
|
||||
ORDER BY transitioned_at DESC`,
|
||||
[archive.id]
|
||||
);
|
||||
|
||||
res.json({ finding_id: findingId, transitions });
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user