Skip to content

Commit 41e6a5c

Browse files
authored
Feat: Implement API for Tracking Orphaned Tasks. (#2267)
* 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.
1 parent b6351b7 commit 41e6a5c

File tree

10 files changed

+516
-32
lines changed

10 files changed

+516
-32
lines changed

controllers/tasks.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { updateUserStatusOnTaskUpdate, updateStatusOnTaskCompletion } = require("
1313
const dataAccess = require("../services/dataAccessLayer");
1414
const { parseSearchQuery } = require("../utils/tasks");
1515
const { addTaskCreatedAtAndUpdatedAtFields } = require("../services/tasks");
16+
const tasksService = require("../services/tasks");
1617
const { RQLQueryParser } = require("../utils/RQLParser");
1718
const { getMissedProgressUpdatesUsers } = require("../models/discordactions");
1819
const { logType } = require("../constants/logs");
@@ -134,7 +135,19 @@ const fetchPaginatedTasks = async (query) => {
134135

135136
const fetchTasks = async (req, res) => {
136137
try {
137-
const { status, page, size, prev, next, q: queryString, assignee, title, userFeatureFlag } = req.query;
138+
const {
139+
status,
140+
page,
141+
size,
142+
prev,
143+
next,
144+
q: queryString,
145+
assignee,
146+
title,
147+
userFeatureFlag,
148+
orphaned,
149+
dev,
150+
} = req.query;
138151
const transformedQuery = transformQuery(status, size, page, assignee, title);
139152

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

175+
const isOrphaned = orphaned === "true";
176+
const isDev = dev === "true";
177+
if (isOrphaned) {
178+
if (!isDev) {
179+
return res.boom.notFound("Route not found");
180+
}
181+
try {
182+
const orphanedTasks = await tasksService.fetchOrphanedTasks();
183+
if (!orphanedTasks || orphanedTasks.length === 0) {
184+
return res.sendStatus(204);
185+
}
186+
const tasksWithRdsAssigneeInfo = await fetchTasksWithRdsAssigneeInfo(orphanedTasks);
187+
return res.status(200).json({
188+
message: "Orphan tasks fetched successfully",
189+
data: tasksWithRdsAssigneeInfo,
190+
});
191+
} catch (error) {
192+
logger.error("Error in getting tasks which were orphaned", error);
193+
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
194+
}
195+
}
196+
162197
const paginatedTasks = await fetchPaginatedTasks({ ...transformedQuery, prev, next, userFeatureFlag });
163198
return res.json({
164199
message: "Tasks returned successfully!",

middlewares/validators/tasks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ const getTasksValidator = async (req, res, next) => {
193193
return value;
194194
}, "Invalid query format"),
195195
userFeatureFlag: joi.string().optional(),
196+
orphaned: joi.boolean().optional(),
196197
});
197198

198199
try {

models/tasks.js

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const userModel = firestore.collection("users");
44
const ItemModel = firestore.collection("itemTags");
55
const dependencyModel = firestore.collection("taskDependencies");
66
const userUtils = require("../utils/users");
7-
const { updateTaskStatusToDone } = require("../services/tasks");
87
const { chunks } = require("../utils/array");
98
const { DOCUMENT_WRITE_SIZE } = require("../constants/constants");
109
const { fromFirestoreData, toFirestoreData, buildTasks } = require("../utils/tasks");
@@ -24,6 +23,42 @@ const {
2423
const { OLD_ACTIVE, OLD_BLOCKED, OLD_PENDING, OLD_COMPLETED } = TASK_STATUS_OLD;
2524
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
2625

26+
/**
27+
* Update multiple tasks' status to DONE in one batch operation.
28+
* @param {Object[]} tasksData - Tasks data to update, must contain 'id' and 'status' fields.
29+
* @returns {Object} - Summary of the batch operation.
30+
* @property {number} totalUpdatedStatus - Number of tasks that has their status updated to DONE.
31+
* @property {number} totalOperationsFailed - Number of tasks that failed to update.
32+
* @property {string[]} updatedTaskDetails - IDs of tasks that has their status updated to DONE.
33+
* @property {string[]} failedTaskDetails - IDs of tasks that failed to update.
34+
*/
35+
const updateTaskStatusToDone = async (tasksData) => {
36+
const batch = firestore.batch();
37+
const tasksBatch = [];
38+
const summary = {
39+
totalUpdatedStatus: 0,
40+
totalOperationsFailed: 0,
41+
updatedTaskDetails: [],
42+
failedTaskDetails: [],
43+
};
44+
tasksData.forEach((task) => {
45+
const updateTaskData = { ...task, status: "DONE" };
46+
batch.update(tasksModel.doc(task.id), updateTaskData);
47+
tasksBatch.push(task.id);
48+
});
49+
try {
50+
await batch.commit();
51+
summary.totalUpdatedStatus += tasksData.length;
52+
summary.updatedTaskDetails = [...tasksBatch];
53+
return { ...summary };
54+
} catch (err) {
55+
logger.error("Firebase batch Operation Failed!");
56+
summary.totalOperationsFailed += tasksData.length;
57+
summary.failedTaskDetails = [...tasksBatch];
58+
return { ...summary };
59+
}
60+
};
61+
2762
/**
2863
* Adds and Updates tasks
2964
*
@@ -701,6 +736,33 @@ const markUnDoneTasksOfArchivedUsersBacklog = async (users) => {
701736
}
702737
};
703738

739+
/**
740+
* Fetches all incomplete tasks for given user IDs.
741+
*
742+
* @param {string[]} userIds - The IDs of the users to fetch incomplete tasks for.
743+
* @returns {Promise<firebase.firestore.QuerySnapshot>} - The query snapshot object.
744+
* @throws {Error} - Throws an error if the database query fails.
745+
*/
746+
const fetchIncompleteTasksByUserIds = async (userIds) => {
747+
const COMPLETED_STATUSES = [DONE, COMPLETED];
748+
749+
if (!userIds || userIds.length === 0) {
750+
return [];
751+
}
752+
try {
753+
const incompleteTasksQuery = await tasksModel.where("assigneeId", "in", userIds).get();
754+
755+
const incompleteTaskForUsers = incompleteTasksQuery.docs.filter((task) => {
756+
return !COMPLETED_STATUSES.includes(task.data().status);
757+
});
758+
759+
return incompleteTaskForUsers;
760+
} catch (error) {
761+
logger.error("Error when fetching incomplete tasks for users:", error);
762+
throw error;
763+
}
764+
};
765+
704766
module.exports = {
705767
updateTask,
706768
fetchTasks,
@@ -720,4 +782,6 @@ module.exports = {
720782
updateTaskStatus,
721783
updateOrphanTasksStatus,
722784
markUnDoneTasksOfArchivedUsersBacklog,
785+
updateTaskStatusToDone,
786+
fetchIncompleteTasksByUserIds,
723787
};

models/users.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,24 @@ const updateUsersWithNewUsernames = async () => {
10301030
}
10311031
};
10321032

1033+
/**
1034+
* Fetches users who are not in the Discord server.
1035+
* @returns {Promise<FirebaseFirestore.QuerySnapshot>} - A promise that resolves to a Firestore QuerySnapshot containing the users matching the criteria.
1036+
* @throws {Error} - Throws an error if the database query fails.
1037+
*/
1038+
const fetchUsersNotInDiscordServer = async () => {
1039+
try {
1040+
const usersNotInDiscordServer = await userModel
1041+
.where("roles.archived", "==", true)
1042+
.where("roles.in_discord", "==", false)
1043+
.get();
1044+
return usersNotInDiscordServer;
1045+
} catch (error) {
1046+
logger.error(`Error in getting users who are not in discord server: ${error}`);
1047+
throw error;
1048+
}
1049+
};
1050+
10331051
module.exports = {
10341052
addOrUpdate,
10351053
fetchPaginatedUsers,
@@ -1059,4 +1077,5 @@ module.exports = {
10591077
fetchUserForKeyValue,
10601078
getNonNickNameSyncedUsers,
10611079
updateUsersWithNewUsernames,
1080+
fetchUsersNotInDiscordServer,
10621081
};

services/tasks.js

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,8 @@ const firestore = require("../utils/firestore");
22
const tasksModel = firestore.collection("tasks");
33
const { chunks } = require("../utils/array");
44
const { DOCUMENT_WRITE_SIZE: FIRESTORE_BATCH_OPERATIONS_LIMIT } = require("../constants/constants");
5-
6-
const updateTaskStatusToDone = async (tasksData) => {
7-
const batch = firestore.batch();
8-
const tasksBatch = [];
9-
const summary = {
10-
totalUpdatedStatus: 0,
11-
totalOperationsFailed: 0,
12-
updatedTaskDetails: [],
13-
failedTaskDetails: [],
14-
};
15-
tasksData.forEach((task) => {
16-
const updateTaskData = { ...task, status: "DONE" };
17-
batch.update(tasksModel.doc(task.id), updateTaskData);
18-
tasksBatch.push(task.id);
19-
});
20-
try {
21-
await batch.commit();
22-
summary.totalUpdatedStatus += tasksData.length;
23-
summary.updatedTaskDetails = [...tasksBatch];
24-
return { ...summary };
25-
} catch (err) {
26-
logger.error("Firebase batch Operation Failed!");
27-
summary.totalOperationsFailed += tasksData.length;
28-
summary.failedTaskDetails = [...tasksBatch];
29-
return { ...summary };
30-
}
31-
};
5+
const usersQuery = require("../models/users");
6+
const tasksQuery = require("../models/tasks");
327

338
const addTaskCreatedAtAndUpdatedAtFields = async () => {
349
const operationStats = {
@@ -83,7 +58,30 @@ const addTaskCreatedAtAndUpdatedAtFields = async () => {
8358
return operationStats;
8459
};
8560

61+
const fetchOrphanedTasks = async () => {
62+
try {
63+
const userSnapshot = await usersQuery.fetchUsersNotInDiscordServer();
64+
65+
if (userSnapshot.empty) return [];
66+
67+
const userIds = userSnapshot.docs.map((doc) => doc.id);
68+
69+
const orphanedTasksQuerySnapshot = await tasksQuery.fetchIncompleteTasksByUserIds(userIds);
70+
71+
if (orphanedTasksQuerySnapshot.empty) {
72+
return [];
73+
}
74+
75+
const orphanedTasks = orphanedTasksQuerySnapshot.map((doc) => doc.data());
76+
77+
return orphanedTasks;
78+
} catch (error) {
79+
logger.error(`Error in getting tasks abandoned by users: ${error}`);
80+
throw error;
81+
}
82+
};
83+
8684
module.exports = {
87-
updateTaskStatusToDone,
8885
addTaskCreatedAtAndUpdatedAtFields,
86+
fetchOrphanedTasks,
8987
};

0 commit comments

Comments
 (0)