Skip to content

Commit

Permalink
Feat: Implement API for Tracking Orphaned Tasks. (#2267)
Browse files Browse the repository at this point in the history
* feat: orphaned tasks API changes.

* feat: Added tests for orphaned tasks.

* chore: Returned without a block for usersnaptop empty and added spacing to make it more readable.

* Refactor: Update fetchOrphanedTasks to use batch query for incomplete tasks

- Replaced individual queries with `fetchIncompleteTasksByUserIds` to fetch tasks in batch for users not in the Discord server.
- Improved performance by reducing the number of database queries.
- Modified testcases based on the updates.

* fix: Changed abandoned tasks to orphaned tasks.

* Fix: Changed the validation type of orphaned to boolean instead of string.
  • Loading branch information
VinuB-Dev authored Nov 30, 2024
1 parent b6351b7 commit 41e6a5c
Show file tree
Hide file tree
Showing 10 changed files with 516 additions and 32 deletions.
37 changes: 36 additions & 1 deletion 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 @@ -134,7 +135,19 @@ const fetchPaginatedTasks = async (query) => {

const fetchTasks = async (req, res) => {
try {
const { status, page, size, prev, next, q: queryString, assignee, title, userFeatureFlag } = req.query;
const {
status,
page,
size,
prev,
next,
q: queryString,
assignee,
title,
userFeatureFlag,
orphaned,
dev,
} = req.query;
const transformedQuery = transformQuery(status, size, page, assignee, title);

if (queryString !== undefined) {
Expand All @@ -159,6 +172,28 @@ const fetchTasks = async (req, res) => {
});
}

const isOrphaned = orphaned === "true";
const isDev = dev === "true";
if (isOrphaned) {
if (!isDev) {
return res.boom.notFound("Route not found");
}
try {
const orphanedTasks = await tasksService.fetchOrphanedTasks();
if (!orphanedTasks || orphanedTasks.length === 0) {
return res.sendStatus(204);
}
const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(orphanedTasks);
return res.status(200).json({
message: "Orphan tasks fetched successfully",
data: tasksWithRdsAssigneeInfo,
});
} catch (error) {
logger.error("Error in getting tasks which were orphaned", error);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
}

const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next, userFeatureFlag });
return res.json({
message: "Tasks returned successfully!",
Expand Down
1 change: 1 addition & 0 deletions middlewares/validators/tasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ const getTasksValidator = async (req, res, next) => {
return value;
}, "Invalid query format"),
userFeatureFlag: joi.string().optional(),
orphaned: joi.boolean().optional(),
});

try {
Expand Down
66 changes: 65 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,33 @@ const markUnDoneTasksOfArchivedUsersBacklog = async (users) => {
}
};

/**
* Fetches all incomplete tasks for given user IDs.
*
* @param {string[]} userIds - The IDs of the users to fetch incomplete tasks for.
* @returns {Promise<firebase.firestore.QuerySnapshot>} - The query snapshot object.
* @throws {Error} - Throws an error if the database query fails.
*/
const fetchIncompleteTasksByUserIds = async (userIds) => {
const COMPLETED_STATUSES = [DONE, COMPLETED];

if (!userIds || userIds.length === 0) {
return [];
}
try {
const incompleteTasksQuery = await tasksModel.where("assigneeId", "in", userIds).get();

const incompleteTaskForUsers = incompleteTasksQuery.docs.filter((task) => {
return !COMPLETED_STATUSES.includes(task.data().status);
});

return incompleteTaskForUsers;
} catch (error) {
logger.error("Error when fetching incomplete tasks for users:", error);
throw error;
}
};

module.exports = {
updateTask,
fetchTasks,
Expand All @@ -720,4 +782,6 @@ module.exports = {
updateTaskStatus,
updateOrphanTasksStatus,
markUnDoneTasksOfArchivedUsersBacklog,
updateTaskStatusToDone,
fetchIncompleteTasksByUserIds,
};
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,
};
54 changes: 26 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 usersQuery = require("../models/users");
const tasksQuery = require("../models/tasks");

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

const fetchOrphanedTasks = async () => {
try {
const userSnapshot = await usersQuery.fetchUsersNotInDiscordServer();

if (userSnapshot.empty) return [];

const userIds = userSnapshot.docs.map((doc) => doc.id);

const orphanedTasksQuerySnapshot = await tasksQuery.fetchIncompleteTasksByUserIds(userIds);

if (orphanedTasksQuerySnapshot.empty) {
return [];
}

const orphanedTasks = orphanedTasksQuerySnapshot.map((doc) => doc.data());

return orphanedTasks;
} catch (error) {
logger.error(`Error in getting tasks abandoned by users: ${error}`);
throw error;
}
};

module.exports = {
updateTaskStatusToDone,
addTaskCreatedAtAndUpdatedAtFields,
fetchOrphanedTasks,
};
Loading

0 comments on commit 41e6a5c

Please sign in to comment.