Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions controllers/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { updateUserStatusOnTaskUpdate, updateStatusOnTaskCompletion } = require("
const dataAccess = require("../services/dataAccessLayer");
const { parseSearchQuery } = require("../utils/tasks");
const { addTaskCreatedAtAndUpdatedAtFields } = require("../services/tasks");
const tasksService = require("../services/tasks");
const { RQLQueryParser } = require("../utils/RQLParser");
const { getMissedProgressUpdatesUsers } = require("../models/discordactions");
const { logType } = require("../constants/logs");
Expand Down Expand Up @@ -532,6 +533,17 @@ const getUsersHandler = async (req, res) => {
}
};

const getOrphanedTasks = async (req, res) => {
try {
const data = await tasksService.fetchOrphanedTasks();
if (data.length === 0) return res.status(204).send();
return res.status(200).json({ message: "Orphan tasks fetched successfully", data });
} catch (error) {
logger.error("Error in getting tasks which were abandoned", error);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
};

module.exports = {
addNewTask,
fetchTasks,
Expand All @@ -545,4 +557,5 @@ module.exports = {
updateStatus,
getUsersHandler,
orphanTasks,
getOrphanedTasks,
};
59 changes: 58 additions & 1 deletion models/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const userModel = firestore.collection("users");
const ItemModel = firestore.collection("itemTags");
const dependencyModel = firestore.collection("taskDependencies");
const userUtils = require("../utils/users");
const { updateTaskStatusToDone } = require("../services/tasks");
const { chunks } = require("../utils/array");
const { DOCUMENT_WRITE_SIZE } = require("../constants/constants");
const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks");
Expand All @@ -24,6 +23,42 @@ const {
const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD;
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");

/**
* Update multiple tasks' status to DONE in one batch operation.
* @param {Object[]} tasksData - Tasks data to update, must contain 'id' and 'status' fields.
* @returns {Object} - Summary of the batch operation.
* @property {number} totalUpdatedStatus - Number of tasks that has their status updated to DONE.
* @property {number} totalOperationsFailed - Number of tasks that failed to update.
* @property {string[]} updatedTaskDetails - IDs of tasks that has their status updated to DONE.
* @property {string[]} failedTaskDetails - IDs of tasks that failed to update.
*/
const updateTaskStatusToDone = async (tasksData) => {
const batch = firestore.batch();
const tasksBatch = [];
const summary = {
totalUpdatedStatus: 0,
totalOperationsFailed: 0,
updatedTaskDetails: [],
failedTaskDetails: [],
};
tasksData.forEach((task) => {
const updateTaskData = { ...task, status: "DONE" };
batch.update(tasksModel.doc(task.id), updateTaskData);
tasksBatch.push(task.id);
});
try {
await batch.commit();
summary.totalUpdatedStatus += tasksData.length;
summary.updatedTaskDetails = [...tasksBatch];
return { ...summary };
} catch (err) {
logger.error("Firebase batch Operation Failed!");
summary.totalOperationsFailed += tasksData.length;
summary.failedTaskDetails = [...tasksBatch];
return { ...summary };
}
};

/**
* Adds and Updates tasks
*
Expand Down Expand Up @@ -701,6 +736,26 @@ const markUnDoneTasksOfArchivedUsersBacklog = async (users) => {
}
};

/**
* Fetch incomplete tasks assigned to a specific user
* @param {string} userId - The unique identifier for the user.
* @returns {Promise<Array>} - A promise that resolves to an array of incomplete tasks for the given user.
* @throws {Error} - Throws an error if the database query fails.
*/
const fetchIncompleteTaskForUser = async (userId) => {
const COMPLETED_STATUSES = [DONE, COMPLETED];
try {
const incompleteTaskForUser = await tasksModel
.where("assigneeId", "==", userId)
.where("status", "not-in", COMPLETED_STATUSES)
.get();
return incompleteTaskForUser;
} catch (error) {
logger.error("Error when fetching incomplete tasks:", error);
throw error;
}
};

module.exports = {
updateTask,
fetchTasks,
Expand All @@ -720,4 +775,6 @@ module.exports = {
updateTaskStatus,
updateOrphanTasksStatus,
markUnDoneTasksOfArchivedUsersBacklog,
fetchIncompleteTaskForUser,
updateTaskStatusToDone,
};
19 changes: 19 additions & 0 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,24 @@ const updateUsersWithNewUsernames = async () => {
}
};

/**
* Fetches users who are not in the Discord server.
* @returns {Promise<FirebaseFirestore.QuerySnapshot>} - A promise that resolves to a Firestore QuerySnapshot containing the users matching the criteria.
* @throws {Error} - Throws an error if the database query fails.
*/
const fetchUsersNotInDiscordServer = async () => {
try {
const usersNotInDiscordServer = await userModel
.where("roles.archived", "==", true)
.where("roles.in_discord", "==", false)
.get();
return usersNotInDiscordServer;
} catch (error) {
logger.error(`Error in getting users who are not in discord server: ${error}`);
throw error;
}
};

module.exports = {
addOrUpdate,
fetchPaginatedUsers,
Expand Down Expand Up @@ -1059,4 +1077,5 @@ module.exports = {
fetchUserForKeyValue,
getNonNickNameSyncedUsers,
updateUsersWithNewUsernames,
fetchUsersNotInDiscordServer,
};
2 changes: 2 additions & 0 deletions routes/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const { cacheResponse, invalidateCache } = require("../utils/cache");
const { ALL_TASKS } = require("../constants/cacheKeys");
const { verifyCronJob } = require("../middlewares/authorizeBot");
const { CLOUDFLARE_WORKER, CRON_JOB_HANDLER } = require("../constants/bot");
const { devFlagMiddleware } = require("../middlewares/devFlag");

const oldAuthorizationMiddleware = authorizeRoles([APPOWNER, SUPERUSER]);
const newAuthorizationMiddleware = authorizeAndAuthenticate(
Expand All @@ -33,6 +34,7 @@ const enableDevModeMiddleware = (req, res, next) => {
}
};

router.get("/orphaned-tasks", devFlagMiddleware, tasks.getOrphanedTasks);
router.get("/", getTasksValidator, cacheResponse({ invalidationKey: ALL_TASKS, expiry: 10 }), tasks.fetchTasks);
router.get("/self", authenticate, tasks.getSelfTasks);
router.get("/overdue", authenticate, authorizeRoles([SUPERUSER]), tasks.overdueTasks);
Expand Down
52 changes: 24 additions & 28 deletions services/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,8 @@ const firestore = require("../utils/firestore");
const tasksModel = firestore.collection("tasks");
const { chunks } = require("../utils/array");
const { DOCUMENT_WRITE_SIZE: FIRESTORE_BATCH_OPERATIONS_LIMIT } = require("../constants/constants");

const updateTaskStatusToDone = async (tasksData) => {
const batch = firestore.batch();
const tasksBatch = [];
const summary = {
totalUpdatedStatus: 0,
totalOperationsFailed: 0,
updatedTaskDetails: [],
failedTaskDetails: [],
};
tasksData.forEach((task) => {
const updateTaskData = { ...task, status: "DONE" };
batch.update(tasksModel.doc(task.id), updateTaskData);
tasksBatch.push(task.id);
});
try {
await batch.commit();
summary.totalUpdatedStatus += tasksData.length;
summary.updatedTaskDetails = [...tasksBatch];
return { ...summary };
} catch (err) {
logger.error("Firebase batch Operation Failed!");
summary.totalOperationsFailed += tasksData.length;
summary.failedTaskDetails = [...tasksBatch];
return { ...summary };
}
};
const { fetchUsersNotInDiscordServer } = require("../models/users");
const { fetchIncompleteTaskForUser } = require("../models/tasks");

const addTaskCreatedAtAndUpdatedAtFields = async () => {
const operationStats = {
Expand Down Expand Up @@ -83,7 +58,28 @@ const addTaskCreatedAtAndUpdatedAtFields = async () => {
return operationStats;
};

const fetchOrphanedTasks = async () => {
try {
const abandonedTasks = [];

const userSnapshot = await fetchUsersNotInDiscordServer();

for (const userDoc of userSnapshot.docs) {
const user = userDoc.data();
const abandonedTasksQuerySnapshot = await fetchIncompleteTaskForUser(user.id);

if (!abandonedTasksQuerySnapshot.empty) {
abandonedTasks.push(...abandonedTasksQuerySnapshot.docs.map((doc) => doc.data()));
}
}
return abandonedTasks;
} catch (error) {
logger.error(`Error in getting tasks abandoned by users: ${error}`);
throw error;
}
};

module.exports = {
updateTaskStatusToDone,
addTaskCreatedAtAndUpdatedAtFields,
fetchOrphanedTasks,
};
154 changes: 154 additions & 0 deletions test/fixtures/abandoned-tasks/departed-users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
const usersData = [
{
id: "user1_id",
discordId: "123456789",
github_id: "github_user1",
username: "archived_user1",
first_name: "Archived",
last_name: "User One",
linkedin_id: "archived_user1",
github_display_name: "archived-user-1",
phone: "1234567890",
email: "[email protected]",
roles: {
archived: true,
in_discord: false,
},
discordJoinedAt: "2024-01-01T00:00:00.000Z",
picture: {
publicId: "profile/user1",
url: "https://example.com/user1.jpg",
},
},
{
id: "user2_id",
discordId: "987654321",
github_id: "github_user2",
username: "archived_user2",
first_name: "Archived",
last_name: "User Two",
linkedin_id: "archived_user2",
github_display_name: "archived-user-2",
phone: "0987654321",
email: "[email protected]",
roles: {
archived: true,
in_discord: false,
},
discordJoinedAt: "2024-01-02T00:00:00.000Z",
picture: {
publicId: "profile/user2",
url: "https://example.com/user2.jpg",
},
},
{
id: "user3_id",
discordId: "555555555",
github_id: "github_user3",
username: "active_user",
first_name: "Active",
last_name: "User",
linkedin_id: "active_user",
github_display_name: "active-user",
phone: "5555555555",
email: "[email protected]",
roles: {
archived: false,
in_discord: true,
},
discordJoinedAt: "2024-01-03T00:00:00.000Z",
picture: {
publicId: "profile/user3",
url: "https://example.com/user3.jpg",
},
},
];

const tasksData = [
{
id: "task1_id",
title: "Abandoned Task 1",
type: "feature",
status: "IN_PROGRESS",
priority: "HIGH",
percentCompleted: 50,
createdAt: 1727027666,
updatedAt: 1727027999,
startedOn: 1727027777,
endsOn: 1731542400,
assignee: "archived_user1",
assigneeId: "user1_id",
github: {
issue: {
html_url: "https://github.com/org/repo/issues/1",
url: "https://api.github.com/repos/org/repo/issues/1",
},
},
dependsOn: [],
},
{
id: "task2_id",
title: "Abandoned Task 2",
type: "bug",
status: "BLOCKED",
priority: "MEDIUM",
percentCompleted: 30,
createdAt: 1727027666,
updatedAt: 1727027999,
startedOn: 1727027777,
endsOn: 1731542400,
assignee: "archived_user2",
assigneeId: "user2_id",
github: {
issue: {
html_url: "https://github.com/org/repo/issues/2",
url: "https://api.github.com/repos/org/repo/issues/2",
},
},
dependsOn: [],
},
{
id: "task3_id",
title: "Completed Archived Task",
type: "feature",
status: "DONE",
priority: "LOW",
percentCompleted: 100,
createdAt: 1727027666,
updatedAt: 1727027999,
startedOn: 1727027777,
endsOn: 1731542400,
assignee: "archived_user1",
assigneeId: "user1_id",
github: {
issue: {
html_url: "https://github.com/org/repo/issues/3",
url: "https://api.github.com/repos/org/repo/issues/3",
},
},
dependsOn: [],
},
{
id: "task4_id",
title: "Active User Task",
type: "feature",
status: "IN_PROGRESS",
priority: "HIGH",
percentCompleted: 75,
createdAt: 1727027666,
updatedAt: 1727027999,
startedOn: 1727027777,
endsOn: 1731542400,
assignee: "active_user",
assigneeId: "user3_id",
github: {
issue: {
html_url: "https://github.com/org/repo/issues/4",
url: "https://api.github.com/repos/org/repo/issues/4",
},
},
dependsOn: [],
},
];

module.exports = { usersData, tasksData };
Loading