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:
Jordan Ramos
2026-05-06 11:44:17 -06:00
parent 845d843e71
commit 33927b150b
18 changed files with 2164 additions and 4432 deletions

View File

@@ -1,46 +1,17 @@
// Ivanti / RiskSense Workflow Routes
// Data is cached in SQLite and refreshed on a daily schedule or on-demand.
// Auth: x-api-key header (confirmed via platform4.risksense.com/doc/swagger.json)
// Error codes: 401 bad key, 419 insufficient privileges, 429 rate limited
// Data is cached in PostgreSQL and refreshed on a daily schedule or on-demand.
const express = require('express');
const { requireGroup } = require('../middleware/auth');
const pool = require('../db');
const { requireAuth, requireGroup } = require('../middleware/auth');
const { ivantiPost } = require('../helpers/ivantiApi');
const SYNC_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
// ---------------------------------------------------------------------------
// Ensure the sync state table exists (idempotent — safe to call on every start)
// Core sync — calls Ivanti API, stores result in PostgreSQL
// ---------------------------------------------------------------------------
function initTable(db) {
return new Promise((resolve, reject) => {
db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS ivanti_sync_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
total INTEGER DEFAULT 0,
workflows_json TEXT DEFAULT '[]',
synced_at DATETIME,
sync_status TEXT DEFAULT 'never',
error_message TEXT
)
`, (err) => { if (err) return reject(err); });
db.run(`
INSERT OR IGNORE INTO ivanti_sync_state (id, total, workflows_json, sync_status)
VALUES (1, 0, '[]', 'never')
`, (err) => {
if (err) reject(err);
else resolve();
});
});
});
}
// ---------------------------------------------------------------------------
// Core sync — calls Ivanti API, stores result in SQLite
// ---------------------------------------------------------------------------
async function syncWorkflows(db) {
async function syncWorkflows() {
const apiKey = process.env.IVANTI_API_KEY;
const clientId = process.env.IVANTI_CLIENT_ID || '1550';
const firstName = process.env.IVANTI_FIRST_NAME || '';
@@ -50,12 +21,10 @@ async function syncWorkflows(db) {
if (!apiKey) {
const errMsg = 'IVANTI_API_KEY not set in .env — skipping sync';
console.warn('[Ivanti]', errMsg);
await new Promise((resolve) => {
db.run(
`UPDATE ivanti_sync_state SET sync_status='error', error_message=?, synced_at=datetime('now') WHERE id=1`,
[errMsg], resolve
);
});
await pool.query(
`UPDATE ivanti_sync_state SET sync_status='error', error_message=$1, synced_at=NOW() WHERE id=1`,
[errMsg]
);
return;
}
@@ -107,7 +76,6 @@ async function syncWorkflows(db) {
const data = JSON.parse(result.body);
// Spring Data REST format: { _embedded: { workflowBatches: [...] }, page: { totalElements, ... } }
let total = 0;
let workflows = [];
@@ -127,95 +95,89 @@ async function syncWorkflows(db) {
total = data.length;
}
await new Promise((resolve, reject) => {
db.run(
`UPDATE ivanti_sync_state
SET total=?, workflows_json=?, synced_at=datetime('now'), sync_status='success', error_message=NULL
WHERE id=1`,
[total, JSON.stringify(workflows)],
(err) => { if (err) reject(err); else resolve(); }
);
});
await pool.query(
`UPDATE ivanti_sync_state
SET total=$1, workflows_json=$2, synced_at=NOW(), sync_status='success', error_message=NULL
WHERE id=1`,
[total, JSON.stringify(workflows)]
);
console.log(`[Ivanti] Sync complete — ${total} workflows`);
} catch (err) {
const msg = err.message || 'Unknown error';
console.error('[Ivanti] Sync failed:', msg);
await new Promise((resolve) => {
db.run(
`UPDATE ivanti_sync_state SET sync_status='error', error_message=?, synced_at=datetime('now') WHERE id=1`,
[msg], resolve
);
});
await pool.query(
`UPDATE ivanti_sync_state SET sync_status='error', error_message=$1, synced_at=NOW() WHERE id=1`,
[msg]
);
}
}
// ---------------------------------------------------------------------------
// Scheduler — runs sync immediately if >24h stale, then every 24h
// ---------------------------------------------------------------------------
function scheduleSync(db) {
db.get('SELECT synced_at FROM ivanti_sync_state WHERE id = 1', (err, row) => {
if (err || !row || !row.synced_at) {
syncWorkflows(db);
async function scheduleSync() {
try {
const { rows } = await pool.query('SELECT synced_at FROM ivanti_sync_state WHERE id = 1');
const row = rows[0];
if (!row || !row.synced_at) {
syncWorkflows();
} else {
const lastSync = new Date(row.synced_at.replace(' ', 'T') + 'Z');
const lastSync = new Date(row.synced_at);
const hoursSince = (Date.now() - lastSync.getTime()) / (1000 * 60 * 60);
if (hoursSince >= 24) {
syncWorkflows(db);
syncWorkflows();
} else {
const hoursUntil = (24 - hoursSince).toFixed(1);
console.log(`[Ivanti] Last sync ${hoursSince.toFixed(1)}h ago — next auto-sync in ${hoursUntil}h`);
}
}
});
} catch (err) {
console.error('[Ivanti] Schedule check failed:', err);
syncWorkflows();
}
setInterval(() => syncWorkflows(db), SYNC_INTERVAL_MS);
setInterval(() => syncWorkflows(), SYNC_INTERVAL_MS);
}
// ---------------------------------------------------------------------------
// Helper — read current state from DB and return as JSON-ready object
// ---------------------------------------------------------------------------
function readState(db) {
return new Promise((resolve, reject) => {
db.get(
'SELECT total, workflows_json, synced_at, sync_status, error_message FROM ivanti_sync_state WHERE id = 1',
(err, row) => {
if (err) return reject(err);
if (!row) return resolve({ total: 0, workflows: [], synced_at: null, sync_status: 'never', error_message: null });
async function readState() {
const { rows } = await pool.query(
'SELECT total, workflows_json, synced_at, sync_status, error_message FROM ivanti_sync_state WHERE id = 1'
);
const row = rows[0];
if (!row) return { total: 0, workflows: [], synced_at: null, sync_status: 'never', error_message: null };
let workflows = [];
try { workflows = JSON.parse(row.workflows_json || '[]'); } catch (_) { /* leave empty */ }
let workflows = [];
try { workflows = JSON.parse(row.workflows_json || '[]'); } catch (_) { /* leave empty */ }
resolve({
total: row.total || 0,
workflows,
synced_at: row.synced_at,
sync_status: row.sync_status,
error_message: row.error_message
});
}
);
});
return {
total: row.total || 0,
workflows,
synced_at: row.synced_at,
sync_status: row.sync_status,
error_message: row.error_message
};
}
// ---------------------------------------------------------------------------
// Router
// ---------------------------------------------------------------------------
function createIvantiWorkflowsRouter(db, requireAuth) {
function createIvantiWorkflowsRouter() {
const router = express.Router();
// Init table and kick off scheduler (fire-and-forget on startup)
initTable(db)
.then(() => scheduleSync(db))
.catch((err) => console.error('[Ivanti] Init failed:', err));
// Kick off scheduler (fire-and-forget on startup)
scheduleSync().catch((err) => console.error('[Ivanti] Init failed:', err));
// All routes require authentication
router.use(requireAuth(db));
router.use(requireAuth());
// GET / — return cached data (fast, no external call)
router.get('/', async (req, res) => {
try {
res.json(await readState(db));
res.json(await readState());
} catch {
res.status(500).json({ error: 'Database error reading sync state' });
}
@@ -223,9 +185,9 @@ function createIvantiWorkflowsRouter(db, requireAuth) {
// POST /sync — trigger an immediate sync, await completion, return fresh state
router.post('/sync', requireGroup('Admin', 'Standard_User'), async (req, res) => {
await syncWorkflows(db);
await syncWorkflows();
try {
res.json(await readState(db));
res.json(await readState());
} catch {
res.status(500).json({ error: 'Sync ran but could not read updated state' });
}