From 29dc0bbcce86b9e00a1fbea96391c888e5d56122 Mon Sep 17 00:00:00 2001 From: Kenneth Obsequio Date: Sun, 19 Jul 2026 10:50:09 +0800 Subject: [PATCH] done --- controllers/admin/courses.controller.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/controllers/admin/courses.controller.js b/controllers/admin/courses.controller.js index 0a91309..354cbca 100644 --- a/controllers/admin/courses.controller.js +++ b/controllers/admin/courses.controller.js @@ -2029,7 +2029,7 @@ exports.bulkSyncQuestions = async (req, res) => { const parent = await resolveQuestionParent(req.params); if (!parent?.parentRecord) return R.error(res, "Parent not found.", 404); - const { questions = [], updatedBy } = req.body; + const { questions = [], updatedBy, confirmFullReplace } = req.body; const existing = await QuizQuestion.findAll({ where: { [parent.parentField]: parent.parentId, ...notDeleted }, @@ -2038,6 +2038,20 @@ exports.bulkSyncQuestions = async (req, res) => { const incomingIds = questions.filter((q) => q.question_id).map((q) => q.question_id); const toArchive = existingIds.filter((id) => !incomingIds.includes(id)); + // Safety guard: a sync where NONE of the currently-existing questions are + // referenced by id would archive the entire pool in one call — indistinguishable + // from a client sending a stale/incomplete array (exactly how real questions were + // nearly lost while diagnosing this endpoint). Partial edits (dropping a question + // or two out of many) are unaffected — only a full wipe requires explicit confirmation. + if (toArchive.length > 0 && toArchive.length === existingIds.length && !confirmFullReplace) { + await t.rollback(); + return R.error( + res, + `This would archive all ${toArchive.length} existing question(s) and can't be recovered from here. Resend with confirmFullReplace: true if this is intentional.`, + 409 + ); + } + if (toArchive.length) { await QuizQuestion.update( { deletedAt: new Date(), deletedBy: updatedBy ?? null },