Consolidate duplicate log routes into routes/log.js
GET /versions/:id/log, DELETE /log/:logid, and PATCH /log/:logid were defined in both routes/operations.js and routes/log.js. operations.js is registered first, so its handlers shadowed log.js entirely (dead code). Move the authoritative implementations (value/units totals in GET, closed-version 403 guard in DELETE) into log.js and remove the duplicates from operations.js, keeping operations.js focused on the forecast ops. No behavior change — the served handlers were already the operations.js versions; they are now defined once. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
41349f2dee
commit
8d26629f32
152
routes/log.js
152
routes/log.js
@ -4,98 +4,100 @@ const { fcTable } = require('../lib/utils');
|
|||||||
module.exports = function(pool) {
|
module.exports = function(pool) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// list all log entries for a version, newest first, with row counts from fc_table
|
// list log entries for a version, newest first, with row counts and value/units totals
|
||||||
router.get('/versions/:id/log', async (req, res) => {
|
router.get('/versions/:id/log', async (req, res) => {
|
||||||
|
const versionId = parseInt(req.params.id);
|
||||||
try {
|
try {
|
||||||
const verResult = await pool.query(`
|
const verResult = await pool.query(
|
||||||
SELECT v.*, s.tname
|
`SELECT v.*, s.tname, s.id AS source_id FROM pf.version v JOIN pf.source s ON s.id = v.source_id WHERE v.id = $1`,
|
||||||
FROM pf.version v
|
[versionId]
|
||||||
JOIN pf.source s ON s.id = v.source_id
|
|
||||||
WHERE v.id = $1
|
|
||||||
`, [req.params.id]);
|
|
||||||
if (verResult.rows.length === 0) return res.status(404).json({ error: 'Version not found' });
|
|
||||||
|
|
||||||
const { tname, id: version_id } = verResult.rows[0];
|
|
||||||
const table = fcTable(tname, version_id);
|
|
||||||
|
|
||||||
const result = await pool.query(
|
|
||||||
`SELECT l.*,
|
|
||||||
counts.row_count
|
|
||||||
FROM pf.log l
|
|
||||||
LEFT JOIN (
|
|
||||||
SELECT pf_logid, count(*)::int AS row_count
|
|
||||||
FROM ${table}
|
|
||||||
GROUP BY pf_logid
|
|
||||||
) counts ON counts.pf_logid = l.id
|
|
||||||
WHERE l.version_id = $1
|
|
||||||
ORDER BY l.stamp DESC`,
|
|
||||||
[req.params.id]
|
|
||||||
);
|
);
|
||||||
|
if (!verResult.rows.length) return res.status(404).json({ error: 'Version not found' });
|
||||||
|
const { tname, source_id } = verResult.rows[0];
|
||||||
|
const table = fcTable(tname, versionId);
|
||||||
|
|
||||||
|
const colMeta = await pool.query(
|
||||||
|
`SELECT cname, role FROM pf.col_meta WHERE source_id = $1 AND role IN ('value', 'units')`,
|
||||||
|
[source_id]
|
||||||
|
);
|
||||||
|
const valueCol = colMeta.rows.find(c => c.role === 'value')?.cname;
|
||||||
|
const unitsCol = colMeta.rows.find(c => c.role === 'units')?.cname;
|
||||||
|
|
||||||
|
const aggCols = [
|
||||||
|
`count(f.pf_id)::int AS row_count`,
|
||||||
|
valueCol ? `sum(f."${valueCol}")::float8 AS value_total` : `NULL::float8 AS value_total`,
|
||||||
|
unitsCol ? `sum(f."${unitsCol}")::float8 AS units_total` : `NULL::float8 AS units_total`
|
||||||
|
].join(', ');
|
||||||
|
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT l.*, ${aggCols},
|
||||||
|
$2::text AS value_col,
|
||||||
|
$3::text AS units_col
|
||||||
|
FROM pf.log l
|
||||||
|
LEFT JOIN ${table} f ON f.pf_logid = l.id
|
||||||
|
WHERE l.version_id = $1
|
||||||
|
GROUP BY l.id
|
||||||
|
ORDER BY l.id DESC
|
||||||
|
`, [versionId, valueCol || null, unitsCol || null]);
|
||||||
res.json(result.rows);
|
res.json(result.rows);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(err.status || 500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// update note on a log entry
|
// undo a log entry — delete all fc rows with this logid, then delete the log entry
|
||||||
|
router.delete('/log/:logid', async (req, res) => {
|
||||||
|
const logId = parseInt(req.params.logid);
|
||||||
|
try {
|
||||||
|
const logResult = await pool.query(`
|
||||||
|
SELECT l.*, v.status, s.tname, v.id AS version_id
|
||||||
|
FROM pf.log l
|
||||||
|
JOIN pf.version v ON v.id = l.version_id
|
||||||
|
JOIN pf.source s ON s.id = v.source_id
|
||||||
|
WHERE l.id = $1
|
||||||
|
`, [logId]);
|
||||||
|
if (!logResult.rows.length) return res.status(404).json({ error: 'Log entry not found' });
|
||||||
|
const log = logResult.rows[0];
|
||||||
|
if (log.status === 'closed') return res.status(403).json({ error: 'Version is closed' });
|
||||||
|
const table = fcTable(log.tname, log.version_id);
|
||||||
|
const client = await pool.connect();
|
||||||
|
try {
|
||||||
|
await client.query('BEGIN');
|
||||||
|
const deleted = await client.query(
|
||||||
|
`DELETE FROM ${table} WHERE pf_logid = $1 RETURNING pf_id`, [logId]
|
||||||
|
);
|
||||||
|
await client.query('DELETE FROM pf.log WHERE id = $1', [logId]);
|
||||||
|
await client.query('COMMIT');
|
||||||
|
res.json({
|
||||||
|
rows_deleted: deleted.rowCount,
|
||||||
|
pf_ids: deleted.rows.map(r => r.pf_id)
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
await client.query('ROLLBACK');
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || 500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// update the note on a log entry
|
||||||
router.patch('/log/:logid', async (req, res) => {
|
router.patch('/log/:logid', async (req, res) => {
|
||||||
|
const logId = parseInt(req.params.logid);
|
||||||
const { note } = req.body;
|
const { note } = req.body;
|
||||||
try {
|
try {
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE pf.log SET note = $1 WHERE id = $2 RETURNING *`,
|
`UPDATE pf.log SET note = $1 WHERE id = $2 RETURNING *`, [note ?? null, logId]
|
||||||
[note ?? null, parseInt(req.params.logid)]
|
|
||||||
);
|
);
|
||||||
if (result.rows.length === 0) return res.status(404).json({ error: 'Log entry not found' });
|
if (!result.rows.length) return res.status(404).json({ error: 'Log entry not found' });
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: err.message });
|
res.status(err.status || 500).json({ error: err.message });
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// undo an operation — deletes all forecast rows with this logid, then the log entry
|
|
||||||
// two separate queries in a transaction to avoid FK ordering issues
|
|
||||||
router.delete('/log/:logid', async (req, res) => {
|
|
||||||
const logid = parseInt(req.params.logid);
|
|
||||||
const client = await pool.connect();
|
|
||||||
try {
|
|
||||||
// look up the log entry to find the version and fc_table name
|
|
||||||
const logResult = await client.query(`
|
|
||||||
SELECT l.*, v.id AS version_id, s.tname
|
|
||||||
FROM pf.log l
|
|
||||||
JOIN pf.version v ON v.id = l.version_id
|
|
||||||
JOIN pf.source s ON s.id = v.source_id
|
|
||||||
WHERE l.id = $1
|
|
||||||
`, [logid]);
|
|
||||||
|
|
||||||
if (logResult.rows.length === 0) {
|
|
||||||
return res.status(404).json({ error: 'Log entry not found' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tname, version_id } = logResult.rows[0];
|
|
||||||
const table = fcTable(tname, version_id);
|
|
||||||
|
|
||||||
await client.query('BEGIN');
|
|
||||||
// delete forecast rows first (logid has no FK constraint — managed by app)
|
|
||||||
const del = await client.query(
|
|
||||||
`DELETE FROM ${table} WHERE pf_logid = $1 RETURNING pf_id`,
|
|
||||||
[logid]
|
|
||||||
);
|
|
||||||
await client.query(`DELETE FROM pf.log WHERE id = $1`, [logid]);
|
|
||||||
await client.query('COMMIT');
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
message: 'Operation undone',
|
|
||||||
rows_deleted: del.rowCount,
|
|
||||||
pf_ids: del.rows.map(r => r.pf_id)
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({ error: err.message });
|
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -430,102 +430,8 @@ module.exports = function(pool) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// list log entries for a version, newest first, with row counts
|
// log routes (GET /versions/:id/log, DELETE /log/:logid, PATCH /log/:logid)
|
||||||
router.get('/versions/:id/log', async (req, res) => {
|
// live in routes/log.js — see that file.
|
||||||
const versionId = parseInt(req.params.id);
|
|
||||||
try {
|
|
||||||
const verResult = await pool.query(
|
|
||||||
`SELECT v.*, s.tname, s.id AS source_id FROM pf.version v JOIN pf.source s ON s.id = v.source_id WHERE v.id = $1`,
|
|
||||||
[versionId]
|
|
||||||
);
|
|
||||||
if (!verResult.rows.length) return res.status(404).json({ error: 'Version not found' });
|
|
||||||
const { tname, source_id } = verResult.rows[0];
|
|
||||||
const table = fcTable(tname, versionId);
|
|
||||||
|
|
||||||
const colMeta = await pool.query(
|
|
||||||
`SELECT cname, role FROM pf.col_meta WHERE source_id = $1 AND role IN ('value', 'units')`,
|
|
||||||
[source_id]
|
|
||||||
);
|
|
||||||
const valueCol = colMeta.rows.find(c => c.role === 'value')?.cname;
|
|
||||||
const unitsCol = colMeta.rows.find(c => c.role === 'units')?.cname;
|
|
||||||
|
|
||||||
const aggCols = [
|
|
||||||
`count(f.pf_id)::int AS row_count`,
|
|
||||||
valueCol ? `sum(f."${valueCol}")::float8 AS value_total` : `NULL::float8 AS value_total`,
|
|
||||||
unitsCol ? `sum(f."${unitsCol}")::float8 AS units_total` : `NULL::float8 AS units_total`
|
|
||||||
].join(', ');
|
|
||||||
|
|
||||||
const result = await pool.query(`
|
|
||||||
SELECT l.*, ${aggCols},
|
|
||||||
$2::text AS value_col,
|
|
||||||
$3::text AS units_col
|
|
||||||
FROM pf.log l
|
|
||||||
LEFT JOIN ${table} f ON f.pf_logid = l.id
|
|
||||||
WHERE l.version_id = $1
|
|
||||||
GROUP BY l.id
|
|
||||||
ORDER BY l.id DESC
|
|
||||||
`, [versionId, valueCol || null, unitsCol || null]);
|
|
||||||
res.json(result.rows);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || 500).json({ error: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// undo a log entry — delete all fc rows with this logid, then delete the log entry
|
|
||||||
router.delete('/log/:logid', async (req, res) => {
|
|
||||||
const logId = parseInt(req.params.logid);
|
|
||||||
try {
|
|
||||||
const logResult = await pool.query(`
|
|
||||||
SELECT l.*, v.status, s.tname, v.id AS version_id
|
|
||||||
FROM pf.log l
|
|
||||||
JOIN pf.version v ON v.id = l.version_id
|
|
||||||
JOIN pf.source s ON s.id = v.source_id
|
|
||||||
WHERE l.id = $1
|
|
||||||
`, [logId]);
|
|
||||||
if (!logResult.rows.length) return res.status(404).json({ error: 'Log entry not found' });
|
|
||||||
const log = logResult.rows[0];
|
|
||||||
if (log.status === 'closed') return res.status(403).json({ error: 'Version is closed' });
|
|
||||||
const table = fcTable(log.tname, log.version_id);
|
|
||||||
const client = await pool.connect();
|
|
||||||
try {
|
|
||||||
await client.query('BEGIN');
|
|
||||||
const deleted = await client.query(
|
|
||||||
`DELETE FROM ${table} WHERE pf_logid = $1 RETURNING pf_id`, [logId]
|
|
||||||
);
|
|
||||||
await client.query('DELETE FROM pf.log WHERE id = $1', [logId]);
|
|
||||||
await client.query('COMMIT');
|
|
||||||
res.json({
|
|
||||||
rows_deleted: deleted.rowCount,
|
|
||||||
pf_ids: deleted.rows.map(r => r.pf_id)
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
await client.query('ROLLBACK');
|
|
||||||
throw err;
|
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || 500).json({ error: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// update the note on a log entry
|
|
||||||
router.patch('/log/:logid', async (req, res) => {
|
|
||||||
const logId = parseInt(req.params.logid);
|
|
||||||
const { note } = req.body;
|
|
||||||
try {
|
|
||||||
const result = await pool.query(
|
|
||||||
`UPDATE pf.log SET note = $1 WHERE id = $2 RETURNING *`, [note, logId]
|
|
||||||
);
|
|
||||||
if (!result.rows.length) return res.status(404).json({ error: 'Log entry not found' });
|
|
||||||
res.json(result.rows[0]);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || 500).json({ error: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user