Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
155 changes: 155 additions & 0 deletions src/controllers/task.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
191 changes: 191 additions & 0 deletions src/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
}
}
}
}
}
}
},

Expand Down
7 changes: 7 additions & 0 deletions src/routes/task.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
deleteTask,
getAllTasks,
getSpecificTask,
restoreTask,
updateTask,
updateTaskPriority,
updateTaskStatus,
Expand Down Expand Up @@ -54,4 +55,10 @@ router.delete(
deleteTask,
);

router.patch(
'/api/organization/:organizationId/team/:teamId/project/:projectId/task/:taskId/restore',
verifyAccessToken,
restoreTask,
);

export default router;