fix: validate all snowball order rows upfront, reject invalid ones with 400

PATCH /api/snowball/order silently skipped rows with bad ids or invalid
snowball_order values via bare 'continue' — no feedback, partial updates.
Now validates every item before touching the DB, returning 400 on the first
bad entry. Also adds deleted_at IS NULL filter so soft-deleted bills are
skipped instead of updated silently.
This commit is contained in:
null 2026-06-03 20:48:24 -05:00
parent e41f413f61
commit 44320a7613
1 changed files with 31 additions and 9 deletions

View File

@ -214,22 +214,44 @@ router.patch('/order', (req, res) => {
if (!Array.isArray(items)) {
return res.status(400).json(standardizeError('Request body must be an array', 'VALIDATION_ERROR'));
}
if (items.length === 0) {
return res.json({ success: true, updated: 0 });
}
// Validate every row before touching the DB — no silent skips
const parsed = [];
for (let i = 0; i < items.length; i++) {
const row = items[i];
const id = parseInt(row?.id, 10);
const order = parseInt(row?.snowball_order, 10);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json(standardizeError(
`Item at index ${i} has an invalid id: ${JSON.stringify(row?.id)}`,
'VALIDATION_ERROR',
));
}
if (!Number.isInteger(order) || order < 0) {
return res.status(400).json(standardizeError(
`Item at index ${i} has an invalid snowball_order: ${JSON.stringify(row?.snowball_order)}`,
'VALIDATION_ERROR',
));
}
parsed.push({ id, order });
}
const db = getDb();
const userId = req.user.id;
const update = db.prepare('UPDATE bills SET snowball_order = ? WHERE id = ? AND user_id = ?');
const update = db.prepare(
'UPDATE bills SET snowball_order = ? WHERE id = ? AND user_id = ? AND deleted_at IS NULL'
);
db.transaction((rows) => {
for (const row of rows) {
const id = parseInt(row.id, 10);
const order = parseInt(row.snowball_order, 10);
if (!Number.isInteger(id) || id <= 0) continue;
if (!Number.isInteger(order) || order < 0) continue;
db.transaction(() => {
for (const { id, order } of parsed) {
update.run(order, id, userId);
}
})(items);
})();
res.json({ success: true });
res.json({ success: true, updated: parsed.length });
});
// ── Snowball Plan helpers ─────────────────────────────────────────────────────