From 44320a76135e4345c4904dd863640615c261ab69 Mon Sep 17 00:00:00 2001 From: null Date: Wed, 3 Jun 2026 20:48:24 -0500 Subject: [PATCH] fix: validate all snowball order rows upfront, reject invalid ones with 400 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- routes/snowball.js | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/routes/snowball.js b/routes/snowball.js index ac9a67b..e93e635 100644 --- a/routes/snowball.js +++ b/routes/snowball.js @@ -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 ─────────────────────────────────────────────────────