From e5117ec7798b31ebc9e4d99c4ec89937c480d572 Mon Sep 17 00:00:00 2001 From: Mehul Kiran Chaudhari <55375534+MehulKChaudhari@users.noreply.github.com> Date: Tue, 2 Apr 2024 21:55:09 +0530 Subject: [PATCH 1/6] feat: Filter orphan tasks and mark them BACKLOG --- controllers/tasks.js | 14 +++++++++ middlewares/validators/discordactions.js | 8 +----- middlewares/validators/tasks.js | 12 ++++++++ middlewares/validators/utils.ts | 12 ++++++++ models/tasks.js | 36 ++++++++++++++++++++++++ routes/tasks.js | 2 ++ 6 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 middlewares/validators/utils.ts diff --git a/controllers/tasks.js b/controllers/tasks.js index 8d10ede51..dda3db4de 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -489,6 +489,19 @@ const updateStatus = async (req, res) => { } }; +const orphanTasks = async (req, res) => { + try { + const { lastOrphanTasksFilteration = 0 } = req.body; + + const tasksData = await tasks.updateOrphanTasksStatus(lastOrphanTasksFilteration); + + return res.status(200).json({ message: "Orphan tasks filtered", tasksData }); + } catch (error) { + logger.error("Error in filtering orphan tasks", error); + return res.boom.badImplementation(INTERNAL_SERVER_ERROR); + } +}; + const getUsersHandler = async (req, res) => { try { const { size, cursor, q: queryString } = req.query; @@ -548,4 +561,5 @@ module.exports = { assignTask, updateStatus, getUsersHandler, + orphanTasks, }; diff --git a/middlewares/validators/discordactions.js b/middlewares/validators/discordactions.js index 2b1b6a2be..b325e4216 100644 --- a/middlewares/validators/discordactions.js +++ b/middlewares/validators/discordactions.js @@ -1,4 +1,5 @@ const Joi = require("joi"); +const { validateMillisecondsTimestamp } = require("./utils"); const validateGroupRoleBody = async (req, res, next) => { const schema = Joi.object({ @@ -28,13 +29,6 @@ const validateMemberRoleBody = async (req, res, next) => { } }; -const validateMillisecondsTimestamp = async (reqBody, timestampProperty) => { - const schema = Joi.object({ - [timestampProperty]: Joi.number().unit("milliseconds").required(), - }); - return schema.validateAsync(reqBody); -}; - const validateUpdateUsersNicknameStatusBody = async (req, res, next) => { try { await validateMillisecondsTimestamp(req.body, "lastNicknameUpdate"); diff --git a/middlewares/validators/tasks.js b/middlewares/validators/tasks.js index 5912834e6..0cb5fd17f 100644 --- a/middlewares/validators/tasks.js +++ b/middlewares/validators/tasks.js @@ -7,6 +7,7 @@ const { Operators } = require("../../typeDefinitions/rqlParser"); const { daysOfWeek } = require("../../constants/constants"); const TASK_STATUS_ENUM = Object.values(TASK_STATUS); const MAPPED_TASK_STATUS_ENUM = Object.keys(MAPPED_TASK_STATUS); +const { validateMillisecondsTimestamp } = require("./utils"); const createTask = async (req, res, next) => { const schema = joi @@ -262,10 +263,21 @@ const getUsersValidator = async (req, res, next) => { res.boom.badRequest(error.details[0].message); } }; + +const filterOrphanTasksValidator = async (req, res, next) => { + try { + await validateMillisecondsTimestamp(req.body, "lastOrphanTasksFilteration"); + next(); + } catch (error) { + logger.error(`Error while validating request body for Orphan Tasks Filteration payload : ${error}`); + res.boom.badRequest(error); + } +}; module.exports = { createTask, updateTask, updateSelfTask, getTasksValidator, getUsersValidator, + filterOrphanTasksValidator, }; diff --git a/middlewares/validators/utils.ts b/middlewares/validators/utils.ts new file mode 100644 index 000000000..5974c08ce --- /dev/null +++ b/middlewares/validators/utils.ts @@ -0,0 +1,12 @@ +const Joi = require("joi"); + +const validateMillisecondsTimestamp = async (reqBody, timestampProperty) => { + const schema = Joi.object({ + [timestampProperty]: Joi.number().unit("milliseconds").required(), + }); + return schema.validateAsync(reqBody); +}; + +module.exports = { + validateMillisecondsTimestamp +} diff --git a/models/tasks.js b/models/tasks.js index 64ad2751a..fe2280bdd 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -1,5 +1,6 @@ const firestore = require("../utils/firestore"); const tasksModel = firestore.collection("tasks"); +const userModel = firestore.collection("users"); const ItemModel = firestore.collection("itemTags"); const dependencyModel = firestore.collection("taskDependencies"); const userUtils = require("../utils/users"); @@ -631,6 +632,40 @@ const updateTaskStatus = async () => { } }; +const updateOrphanTasksStatus = async (lastOrphanTasksFilteration) => { + const lastTimestamp = Number(lastOrphanTasksFilteration); + try { + const users = []; + const currentTimeStamp = Date.now(); + + const usersQuerySnapshot = await userModel + .where("roles.in_discord", "==", false) + .where("updated_at", ">=", lastTimestamp) + .where("updated_at", "<=", currentTimeStamp) + .get(); + + usersQuerySnapshot.forEach((user) => users.push({ ...user.data(), id: user.id })); + + let orphanTasksUpdatedCount = 0; + + for (const user of users) { + const tasksQuerySnapshot = await tasksModel + .where("assigneeId", "==", user.id) + .where("status", "not-in", ["COMPLETED", "BACKLOG"]) + .get(); + tasksQuerySnapshot.forEach(async (taskDoc) => { + await tasksModel.doc(taskDoc.id).update({ status: "BACKLOG" }); + orphanTasksUpdatedCount++; + }); + } + + return { orphanTasksUpdatedCount }; + } catch (error) { + logger.error("Error marking tasks as backlog:", error); + throw error; + } +}; + module.exports = { updateTask, fetchTasks, @@ -648,4 +683,5 @@ module.exports = { getBuiltTasks, getOverdueTasks, updateTaskStatus, + updateOrphanTasksStatus, }; diff --git a/routes/tasks.js b/routes/tasks.js index e1bd45554..5d8d80eae 100644 --- a/routes/tasks.js +++ b/routes/tasks.js @@ -8,6 +8,7 @@ const { updateSelfTask, getTasksValidator, getUsersValidator, + filterOrphanTasksValidator, } = require("../middlewares/validators/tasks"); const authorizeRoles = require("../middlewares/authorizeRoles"); const { authorizeAndAuthenticate } = require("../middlewares/authorizeUsersAndService"); @@ -67,5 +68,6 @@ router.patch("/assign/self", authenticate, invalidateCache({ invalidationKeys: [ router.get("/users/discord", verifyCronJob, getUsersValidator, tasks.getUsersHandler); router.post("/migration", authenticate, authorizeRoles([SUPERUSER]), tasks.updateStatus); +router.post("/orphanTasks", verifyCronJob, filterOrphanTasksValidator, tasks.orphanTasks); module.exports = router; From cbe9676448b7754263641676bdd4ae0c9c022bce Mon Sep 17 00:00:00 2001 From: Mehul Kiran Chaudhari <55375534+MehulKChaudhari@users.noreply.github.com> Date: Wed, 3 Apr 2024 02:45:21 +0530 Subject: [PATCH 2/6] add: tests --- controllers/tasks.js | 2 +- test/integration/tasks.test.js | 73 ++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index dda3db4de..03ce51607 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -495,7 +495,7 @@ const orphanTasks = async (req, res) => { const tasksData = await tasks.updateOrphanTasksStatus(lastOrphanTasksFilteration); - return res.status(200).json({ message: "Orphan tasks filtered", tasksData }); + return res.status(200).json({ message: "Orphan tasks filtered successfully", tasksData }); } catch (error) { logger.error("Error in filtering orphan tasks", error); return res.boom.badImplementation(INTERNAL_SERVER_ERROR); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index 4711176bb..b79296dab 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -21,6 +21,7 @@ const { TASK_STATUS, tasksUsersStatus } = require("../../constants/tasks"); const updateTaskStatus = require("../fixtures/tasks/tasks1")(); const userStatusData = require("../fixtures/userStatus/userStatus"); const tasksModel = firestore.collection("tasks"); +const userDBModel = firestore.collection("users"); const discordService = require("../../services/discordService"); const { CRON_JOB_HANDLER } = require("../../constants/bot"); const { logType } = require("../../constants/logs"); @@ -1538,4 +1539,76 @@ describe("Tasks", function () { expect(tasksLogs.body.error).to.be.equal("Error: Error occurred"); }); }); + + describe("POST /tasks/orphanTasks", function () { + let jwtToken; + + beforeEach(async function () { + const user1 = userData[6]; + user1.roles.in_discord = false; + user1.updated_at = 1712053284000; + const user2 = userData[18]; + user2.updated_at = 1712064084000; + const [{ id: userId }, { id: userId2 }] = await Promise.all([userDBModel.add(user1), userDBModel.add(user2)]); + + const task1 = { + assigneeId: userId, + status: "ACTIVE", + }; + const task2 = { + assigneeId: userId2, + status: "COMPLETED", + }; + const task3 = { + assigneeId: userId2, + status: "IN_PROGRESS", + }; + await Promise.all([tasksModel.add(task1), tasksModel.add(task2), tasksModel.add(task3)]); + + jwtToken = generateCronJobToken({ name: CRON_JOB_HANDLER }); + }); + + afterEach(async function () { + await cleanDb(); + }); + it("Should update status of orphan tasks to BACKLOG", async function (done) { + chai + .request(app) + .post("/tasks/orphanTasks") + .set("Authorization", `Bearer ${jwtToken}`) + .send({ + lastOrphanTasksFilteration: 1712040715000, + }) + .end((err, res) => { + if (err) { + return done(err); + } + expect(res).to.have.status(200); + expect(res).to.be.an("object"); + expect(res.body).to.deep.equal({ + message: "Orphan tasks filtered successfully", + tasksData: { + orphanTasksUpdatedCount: 2, + }, + }); + return done(); + }); + }).timeout(10000); + + it("Should return 400 if not cron worker", async function () { + const nonSuperUserId = await addUser(appOwner); + const nonSuperUserJwt = authService.generateAuthToken({ userId: nonSuperUserId }); + const res = ( + await chai.request(app).post("/tasks/orphanTasks").set("cookie", `${cookieName}=${nonSuperUserJwt}`) + ).send({ + lastOrphanTasksFilteration: 1712040715000, + }); + expect(res).to.have.status(400); + expect(res.body).to.deep.equal({ + statusCode: 400, + error: "Bad Request", + message: "Unauthorized Cron Worker", + }); + }); + }); }); From a530bc095d6773aa094535a514ff217353380b04 Mon Sep 17 00:00:00 2001 From: Mehul Kiran Chaudhari <55375534+MehulKChaudhari@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:07:16 +0530 Subject: [PATCH 3/6] fix: tests --- models/tasks.js | 2 +- test/integration/tasks.test.js | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/models/tasks.js b/models/tasks.js index fe2280bdd..0f506f377 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -654,8 +654,8 @@ const updateOrphanTasksStatus = async (lastOrphanTasksFilteration) => { .where("status", "not-in", ["COMPLETED", "BACKLOG"]) .get(); tasksQuerySnapshot.forEach(async (taskDoc) => { - await tasksModel.doc(taskDoc.id).update({ status: "BACKLOG" }); orphanTasksUpdatedCount++; + await tasksModel.doc(taskDoc.id).update({ status: "BACKLOG" }); }); } diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index b79296dab..cfcfafaea 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -1598,11 +1598,14 @@ describe("Tasks", function () { it("Should return 400 if not cron worker", async function () { const nonSuperUserId = await addUser(appOwner); const nonSuperUserJwt = authService.generateAuthToken({ userId: nonSuperUserId }); - const res = ( - await chai.request(app).post("/tasks/orphanTasks").set("cookie", `${cookieName}=${nonSuperUserJwt}`) - ).send({ - lastOrphanTasksFilteration: 1712040715000, - }); + const res = await chai + .request(app) + .post("/tasks/orphanTasks") + .set("Authorization", `Bearer ${nonSuperUserJwt}`) + .send({ + lastOrphanTasksFilteration: 1712040715000, + }); + expect(res).to.have.status(400); expect(res.body).to.deep.equal({ statusCode: 400, From 4cf13a60becc88a51596cbfb1aa0ec4bf3aa3aa1 Mon Sep 17 00:00:00 2001 From: Mehul Kiran Chaudhari <55375534+MehulKChaudhari@users.noreply.github.com> Date: Wed, 3 Apr 2024 23:13:42 +0530 Subject: [PATCH 4/6] fix: tests --- test/integration/tasks.test.js | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index cfcfafaea..e396d041b 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -1571,28 +1571,18 @@ describe("Tasks", function () { afterEach(async function () { await cleanDb(); }); - it("Should update status of orphan tasks to BACKLOG", async function (done) { - chai - .request(app) - .post("/tasks/orphanTasks") - .set("Authorization", `Bearer ${jwtToken}`) - .send({ - lastOrphanTasksFilteration: 1712040715000, - }) - .end((err, res) => { - if (err) { - return done(err); - } - expect(res).to.have.status(200); - expect(res).to.be.an("object"); - expect(res.body).to.deep.equal({ - message: "Orphan tasks filtered successfully", - tasksData: { - orphanTasksUpdatedCount: 2, - }, - }); - return done(); - }); + it("Should update status of orphan tasks to BACKLOG", async function () { + const res = await chai.request(app).post("/tasks/orphanTasks").set("Authorization", `Bearer ${jwtToken}`).send({ + lastOrphanTasksFilteration: 1712040715000, + }); + + expect(res).to.have.status(200); + expect(res.body).to.deep.equal({ + message: "Orphan tasks filtered successfully", + tasksData: { + orphanTasksUpdatedCount: 2, + }, + }); }).timeout(10000); it("Should return 400 if not cron worker", async function () { From 6b22f24e02475e386684eb84178f4a3355affdee Mon Sep 17 00:00:00 2001 From: Mehul Kiran Chaudhari <55375534+MehulKChaudhari@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:10:40 +0530 Subject: [PATCH 5/6] fix: variable names --- controllers/tasks.js | 6 +++--- middlewares/validators/tasks.js | 2 +- models/tasks.js | 4 ++-- test/integration/tasks.test.js | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/controllers/tasks.js b/controllers/tasks.js index 03ce51607..8dec7f83c 100644 --- a/controllers/tasks.js +++ b/controllers/tasks.js @@ -491,11 +491,11 @@ const updateStatus = async (req, res) => { const orphanTasks = async (req, res) => { try { - const { lastOrphanTasksFilteration = 0 } = req.body; + const { lastOrphanTasksFilterationTimestamp = 0 } = req.body; - const tasksData = await tasks.updateOrphanTasksStatus(lastOrphanTasksFilteration); + const updatedTasksData = await tasks.updateOrphanTasksStatus(lastOrphanTasksFilterationTimestamp); - return res.status(200).json({ message: "Orphan tasks filtered successfully", tasksData }); + return res.status(200).json({ message: "Orphan tasks filtered successfully", updatedTasksData }); } catch (error) { logger.error("Error in filtering orphan tasks", error); return res.boom.badImplementation(INTERNAL_SERVER_ERROR); diff --git a/middlewares/validators/tasks.js b/middlewares/validators/tasks.js index 0cb5fd17f..3a3718c4f 100644 --- a/middlewares/validators/tasks.js +++ b/middlewares/validators/tasks.js @@ -266,7 +266,7 @@ const getUsersValidator = async (req, res, next) => { const filterOrphanTasksValidator = async (req, res, next) => { try { - await validateMillisecondsTimestamp(req.body, "lastOrphanTasksFilteration"); + await validateMillisecondsTimestamp(req.body, "lastOrphanTasksFilterationTimestamp"); next(); } catch (error) { logger.error(`Error while validating request body for Orphan Tasks Filteration payload : ${error}`); diff --git a/models/tasks.js b/models/tasks.js index 0f506f377..fd7353051 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -632,8 +632,8 @@ const updateTaskStatus = async () => { } }; -const updateOrphanTasksStatus = async (lastOrphanTasksFilteration) => { - const lastTimestamp = Number(lastOrphanTasksFilteration); +const updateOrphanTasksStatus = async (lastOrphanTasksFilterationTimestamp) => { + const lastTimestamp = Number(lastOrphanTasksFilterationTimestamp); try { const users = []; const currentTimeStamp = Date.now(); diff --git a/test/integration/tasks.test.js b/test/integration/tasks.test.js index e396d041b..52cf24f05 100644 --- a/test/integration/tasks.test.js +++ b/test/integration/tasks.test.js @@ -1573,13 +1573,13 @@ describe("Tasks", function () { }); it("Should update status of orphan tasks to BACKLOG", async function () { const res = await chai.request(app).post("/tasks/orphanTasks").set("Authorization", `Bearer ${jwtToken}`).send({ - lastOrphanTasksFilteration: 1712040715000, + lastOrphanTasksFilterationTimestamp: 1712040715000, }); expect(res).to.have.status(200); expect(res.body).to.deep.equal({ message: "Orphan tasks filtered successfully", - tasksData: { + updatedTasksData: { orphanTasksUpdatedCount: 2, }, }); @@ -1593,7 +1593,7 @@ describe("Tasks", function () { .post("/tasks/orphanTasks") .set("Authorization", `Bearer ${nonSuperUserJwt}`) .send({ - lastOrphanTasksFilteration: 1712040715000, + lastOrphanTasksFilterationTimestamp: 1712040715000, }); expect(res).to.have.status(400); From d6a953e810f2a0b8ac2f084ef518f73c35626a04 Mon Sep 17 00:00:00 2001 From: Mehul Kiran Chaudhari <55375534+MehulKChaudhari@users.noreply.github.com> Date: Thu, 4 Apr 2024 01:19:42 +0530 Subject: [PATCH 6/6] fix: use contants --- models/tasks.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/models/tasks.js b/models/tasks.js index fd7353051..adb118cb3 100644 --- a/models/tasks.js +++ b/models/tasks.js @@ -9,7 +9,8 @@ const { chunks } = require("../utils/array"); const { DOCUMENT_WRITE_SIZE } = require("../constants/constants"); const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks"); const { TASK_TYPE, TASK_STATUS, TASK_STATUS_OLD, TASK_SIZE } = require("../constants/tasks"); -const { IN_PROGRESS, NEEDS_REVIEW, IN_REVIEW, ASSIGNED, BLOCKED, SMOKE_TESTING, COMPLETED, SANITY_CHECK } = TASK_STATUS; +const { IN_PROGRESS, NEEDS_REVIEW, IN_REVIEW, ASSIGNED, BLOCKED, SMOKE_TESTING, COMPLETED, SANITY_CHECK, BACKLOG } = + TASK_STATUS; const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD; const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages"); @@ -651,11 +652,11 @@ const updateOrphanTasksStatus = async (lastOrphanTasksFilterationTimestamp) => { for (const user of users) { const tasksQuerySnapshot = await tasksModel .where("assigneeId", "==", user.id) - .where("status", "not-in", ["COMPLETED", "BACKLOG"]) + .where("status", "not-in", [COMPLETED, BACKLOG]) .get(); tasksQuerySnapshot.forEach(async (taskDoc) => { orphanTasksUpdatedCount++; - await tasksModel.doc(taskDoc.id).update({ status: "BACKLOG" }); + await tasksModel.doc(taskDoc.id).update({ status: BACKLOG }); }); }