diff --git a/routes/operations.js b/routes/operations.js index 7ff76c3..7566c4a 100644 --- a/routes/operations.js +++ b/routes/operations.js @@ -734,6 +734,21 @@ module.exports = function(pool) { const scaleFactor = (scale != null) ? parseFloat(scale) : 1.0; const dateOffset = (date_offset || '0 days').trim() || '0 days'; + // The offset is interpolated into the SQL as an interval literal, so a + // typo would surface as a Postgres parse error mid-statement. Ask + // Postgres to parse it on its own first, where the failure is cheap and + // can be reported against the field the user typed it into. Negative + // intervals are fine and useful -- '-90 days' pulls a plan back a + // quarter -- so this checks validity, not sign. + try { + await pool.query(`SELECT $1::interval`, [dateOffset]); + } catch { + return res.status(400).json({ + error: `"${dateOffset}" is not a valid interval. Try something like ` + + `"12 months", "-90 days" or "0 days".` + }); + } + // Cloning from a named segment is how a period with no baseline gets a // shape: pick the ledger line -- prior year, plan -- and copy its mix // forward. That means reading rows exclude_iters normally keeps diff --git a/ui/src/components/OperationPanel.jsx b/ui/src/components/OperationPanel.jsx index ba622da..ce17a72 100644 --- a/ui/src/components/OperationPanel.jsx +++ b/ui/src/components/OperationPanel.jsx @@ -817,6 +817,8 @@ export default function OperationPanel({