From 878abf9d90166527af2b8d4bd489b8f90f049031 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 17 Apr 2025 22:15:48 +0200 Subject: [PATCH 1/3] feat: add `restoreTask` controller function --- src/controllers/task.controller.js | 155 +++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/src/controllers/task.controller.js b/src/controllers/task.controller.js index 70262c9..42ef212 100644 --- a/src/controllers/task.controller.js +++ b/src/controllers/task.controller.js @@ -1221,3 +1221,158 @@ export const deleteTask = async (req, res, next) => { next(error); } }; + +/** + * @desc Restore a task + * @route /api/organization/:organizationId/team/:teamId/project/:projectId/task/:taskId/restore + * @method PATCH + * @access private + */ +export const restoreTask = async (req, res, next) => { + try { + const { organizationId, teamId, projectId, taskId } = req.params; + const { restoreSubtasks = true } = req.body; + const user = req.user; + + // Check if organization exists + const orgCheck = await checkOrganization(organizationId); + if (!orgCheck.success) { + return res.status(404).json({ + success: false, + message: orgCheck.message, + }); + } + + // Check if team exists + const teamCheck = await checkTeam(teamId, organizationId); + if (!teamCheck.success) { + return res.status(404).json({ + success: false, + message: teamCheck.message, + }); + } + + // Check if project exists + const projectCheck = await checkProject(projectId, teamId, organizationId); + if (!projectCheck.success) { + return res.status(404).json({ + success: false, + message: projectCheck.message, + }); + } + + // Check if task exists, is deleted, and belongs to the project + const task = await prisma.task.findFirst({ + where: { + id: taskId, + projectId, + deletedAt: { not: null }, // Must be deleted to restore + }, + include: { + subtasks: { + where: { deletedAt: { not: null } }, // Only include deleted subtasks + select: { id: true }, + }, + parent: { + select: { + id: true, + deletedAt: true, + }, + }, + }, + }); + + if (!task) { + return res.status(404).json({ + success: false, + message: + 'Task not found, already active, or does not belong to the specified project', + }); + } + + // Check task permissions + const permissionsCheck = checkTaskPermissions( + user, + orgCheck.organization, + teamCheck.team, + projectCheck.project, + 'restore', + ); + + if (!permissionsCheck.success) { + return res.status(403).json({ + success: false, + message: permissionsCheck.message, + }); + } + + // Check if parent task exists and is not deleted (if applicable) + if (task.parent && task.parent.deletedAt) { + return res.status(400).json({ + success: false, + message: + 'Cannot restore task because its parent task is deleted. Please restore the parent task first.', + }); + } + + // Start a transaction to ensure all operations succeed or fail together + await prisma.$transaction(async (prisma) => { + // Restore the task + await prisma.task.update({ + where: { id: taskId }, + data: { + deletedAt: null, + lastModifiedBy: user.id, + updatedAt: new Date(), + }, + }); + + // Restore subtasks if requested + if (restoreSubtasks && task.subtasks && task.subtasks.length > 0) { + const subtaskIds = task.subtasks.map((subtask) => subtask.id); + + await prisma.task.updateMany({ + where: { + id: { in: subtaskIds }, + deletedAt: { not: null }, + }, + data: { + deletedAt: null, + lastModifiedBy: user.id, + updatedAt: new Date(), + }, + }); + } + }); + + // Fetch the updated task to return in the response + const restoredTask = await prisma.task.findUnique({ + where: { id: taskId }, + include: { + creator: { + select: { + id: true, + firstName: true, + lastName: true, + profilePic: true, + }, + }, + _count: { + select: { + subtasks: { + where: { deletedAt: null }, + }, + }, + }, + }, + }); + + return res.status(200).json({ + success: true, + message: `Task restored successfully${restoreSubtasks ? ' with its subtasks' : ''}`, + task: restoredTask, + }); + } catch (error) { + next(error); + } +}; From 793d756ad042dd9e21f428930cb625af409d4246 Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 17 Apr 2025 22:16:05 +0200 Subject: [PATCH 2/3] feat: add route for `restoreTask` controller function --- src/routes/task.routes.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/routes/task.routes.js b/src/routes/task.routes.js index 55ae6d3..115526d 100644 --- a/src/routes/task.routes.js +++ b/src/routes/task.routes.js @@ -5,6 +5,7 @@ import { deleteTask, getAllTasks, getSpecificTask, + restoreTask, updateTask, updateTaskPriority, updateTaskStatus, @@ -54,4 +55,10 @@ router.delete( deleteTask, ); +router.patch( + '/api/organization/:organizationId/team/:teamId/project/:projectId/task/:taskId/restore', + verifyAccessToken, + restoreTask, +); + export default router; From 34aed64a033c352acabb2bfc9ae2607c6c44869b Mon Sep 17 00:00:00 2001 From: Mohamed Dawoud Date: Thu, 17 Apr 2025 22:17:13 +0200 Subject: [PATCH 3/3] docs: add swagger docs for restore a deleted task endpoint --- README.md | 1 + src/docs/swagger.json | 191 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) diff --git a/README.md b/README.md index 6a676bc..87097e9 100644 --- a/README.md +++ b/README.md @@ -105,3 +105,4 @@ base url: `http://localhost:3000` - Get all tasks: `GET /api/organization/:organizationId/team/:teamId/project/:projectId/task/all` - Get a specific task: `GET /api/organization/:organizationId/team/:teamId/project/:projectId/task/:taskId` - Delete a task: `DELETE /api/organization/:organizationId/team/:teamId/project/:projectId/task/:taskId/delete` +- Restore the deleted task: `PATCH /api/organization/:organizationId/team/:teamId/project/:projectId/task/:taskId/restore` diff --git a/src/docs/swagger.json b/src/docs/swagger.json index fc2e4dc..bd1f0b2 100644 --- a/src/docs/swagger.json +++ b/src/docs/swagger.json @@ -5867,6 +5867,197 @@ } } } + }, + "/api/organization/{organizationId}/team/{teamId}/project/{projectId}/task/{taskId}/restore": { + "patch": { + "tags": ["Task"], + "summary": "Restore a deleted task", + "description": "Restores a soft-deleted task and optionally its subtasks. Requires the parent task to be active if the task has one.", + "security": [{ "BearerAuth": [] }], + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "description": "ID of the organization", + "schema": { "type": "string" } + }, + { + "name": "teamId", + "in": "path", + "required": true, + "description": "ID of the team", + "schema": { "type": "string" } + }, + { + "name": "projectId", + "in": "path", + "required": true, + "description": "ID of the project", + "schema": { "type": "string" } + }, + { + "name": "taskId", + "in": "path", + "required": true, + "description": "ID of the task to restore", + "schema": { "type": "string" } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "restoreSubtasks": { + "type": "boolean", + "description": "Whether to restore all deleted subtasks (default: true)", + "default": true + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Task restored successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean", "example": true }, + "message": { + "type": "string", + "example": "Task restored successfully with its subtasks" + }, + "task": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "description": { "type": "string" }, + "priority": { "type": "string" }, + "status": { "type": "string" }, + "projectId": { "type": "string" }, + "sprintId": { "type": "string" }, + "createdBy": { "type": "string" }, + "assignedTo": { "type": "string" }, + "dueDate": { "type": "string", "format": "date-time" }, + "estimatedTime": { "type": "number" }, + "parentId": { "type": "string" }, + "labels": { + "type": "array", + "items": { "type": "string" } + }, + "order": { "type": "number" }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "deletedAt": { + "type": "string", + "format": "date-time" + }, + "creator": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "firstName": { "type": "string" }, + "lastName": { "type": "string" }, + "profilePic": { "type": "string" } + } + }, + "_count": { + "type": "object", + "properties": { + "subtasks": { "type": "number" } + } + } + } + } + } + } + } + } + }, + "400": { + "description": "Bad request - parent task is deleted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean", "example": false }, + "message": { + "type": "string", + "example": "Cannot restore task because its parent task is deleted. Please restore the parent task first." + } + } + } + } + } + }, + "403": { + "description": "Forbidden - insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean", "example": false }, + "message": { + "type": "string", + "example": "You don't have permission to restore tasks in this project" + } + } + } + } + } + }, + "404": { + "description": "Not found - organization, team, project or task not found or already active", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean", "example": false }, + "message": { + "type": "string", + "example": "Task not found, already active, or does not belong to the specified project" + } + } + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { "type": "boolean", "example": false }, + "message": { + "type": "string", + "example": "Internal server error" + } + } + } + } + } + } + } + } } },