diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 541290d..233ac5d 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -1,11 +1,8 @@ /** * @file dashboard/page.tsx - * @description Server component rendering the main dashboard page, handling authentication, fetching active tasks with assignees and creators, and passing data to the board container. + * @description Server component rendering the main dashboard page using the TaskService. */ -import { db } from "@/db"; -import { taskAssigneesTable, usersTable } from "@/db/schema"; -import { eq, inArray } from "drizzle-orm"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; import Board from "./Board"; @@ -14,37 +11,22 @@ import { TaskService } from "@/services/task.service"; /** * Renders the dashboard page component with user session validation, - * optimized database queries for active tasks, and team assignees + * optimized database queries for active tasks, and team assignees. * * @async * @returns {Promise} The rendered dashboard page component. */ export default async function Dashboard() { const session = await auth(); - if (!session?.user?.id) { - redirect("/login"); - } + if (!session?.user?.id) redirect("/login"); const currentUserId = session.user.id; - // All active (not deleted) tasks for which the user is either the creator or the assignee - const rawTasksWithCreator = await TaskService.findActiveTasksForUser(currentUserId); const allTaskIds = rawTasksWithCreator.map((item) => item.task.id); - // Load all assignees for these tasks - const assigneesData = - allTaskIds.length > 0 - ? await db - .select({ - taskId: taskAssigneesTable.taskId, - email: usersTable.email, - }) - .from(taskAssigneesTable) - .innerJoin(usersTable, eq(taskAssigneesTable.userId, usersTable.id)) - .where(inArray(taskAssigneesTable.taskId, allTaskIds)) - : []; + const assigneesData = await TaskService.findAssigneesForTasks(allTaskIds); const assigneesMap = new Map(); for (const row of assigneesData) { diff --git a/app/(app)/trash/page.tsx b/app/(app)/trash/page.tsx index 0243c7e..a7179f0 100644 --- a/app/(app)/trash/page.tsx +++ b/app/(app)/trash/page.tsx @@ -1,15 +1,14 @@ /** * @file trash/page.tsx - * @description Server component rendering the trash management view, fetching soft-deleted tasks belonging to the authenticated user. + * @description Server component rendering the trash management view using the TaskService. */ -import { db } from "@/db"; -import { Task, tasksTable, usersTable } from "@/db/schema"; -import { eq, and, isNotNull } from "drizzle-orm"; +import { Task } from "@/db/schema"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; -import TrashList from "./TrashList"; -import TrashHeader from "./components/TrashHeader"; +import TrashList from "../trash/TrashList"; +import TrashHeader from "../trash/components/TrashHeader"; +import { TaskService } from "@/services/task.service"; /** * Renders the trash page verifying user authentication, querying soft-deleted tasks, @@ -24,19 +23,8 @@ export default async function TrashPage() { const currentUserId = session.user.id; - const rawTrashedTasks = await db - .select({ - task: tasksTable, - creatorEmail: usersTable.email, - }) - .from(tasksTable) - .innerJoin(usersTable, eq(tasksTable.userId, usersTable.id)) - .where( - and( - eq(tasksTable.userId, currentUserId), - isNotNull(tasksTable.deletedAt), - ), - ); + const rawTrashedTasks = + await TaskService.findTrashTasksForUser(currentUserId); const tasks: Task[] = rawTrashedTasks.map(({ task }) => ({ ...task, diff --git a/services/task.service.ts b/services/task.service.ts index d881ddc..824c1e9 100644 --- a/services/task.service.ts +++ b/services/task.service.ts @@ -1,6 +1,6 @@ /** * @file services/task.service.ts - * @description Business logic service handling task permissions, database queries, and status updates. + * @description Business logic service handling task permissions, database queries, status updates, and soft deletions. */ import { db } from "@/db"; @@ -11,12 +11,40 @@ import { usersTable, } from "@/db/schema"; import { TaskStatus } from "@/types/tasks"; -import { and, eq, or, exists, isNotNull, isNull } from "drizzle-orm"; +import { and, eq, or, exists, isNotNull, isNull, inArray } from "drizzle-orm"; /** - * Service class for handling task-related operations and database interactions. + * Service class for handling task-related operations, access control, and database interactions. */ export class TaskService { + /** + * Helper: Generates the SQL condition to check if a user is either the creator or an assignee of a task. + * + * @private + * @param {string} userId - The unique identifier of the user. + * @param {any} [taskIdColumn=tasksTable.id] - The task identifier column reference. + * @returns {import("drizzle-orm").SQL} The constructed SQL condition. + */ + private static userHasAccessCondition( + userId: string, + taskIdColumn = tasksTable.id, + ) { + return or( + eq(tasksTable.userId, userId), + exists( + db + .select({ taskId: taskAssigneesTable.taskId }) + .from(taskAssigneesTable) + .where( + and( + eq(taskAssigneesTable.taskId, taskIdColumn), + eq(taskAssigneesTable.userId, userId), + ), + ), + ), + ); + } + /** * Verifies whether a user has access to a specific task as either the owner or an assignee. * @@ -35,20 +63,7 @@ export class TaskService { .where( and( eq(tasksTable.id, taskId), - or( - eq(tasksTable.userId, userId), - exists( - db - .select({ taskId: taskAssigneesTable.taskId }) - .from(taskAssigneesTable) - .where( - and( - eq(taskAssigneesTable.taskId, taskId), - eq(taskAssigneesTable.userId, userId), - ), - ), - ), - ), + this.userHasAccessCondition(userId, tasksTable.id), ), ); @@ -61,8 +76,8 @@ export class TaskService { * @async * @param {string} taskId - The unique identifier of the task to update. * @param {string} userId - The unique identifier of the user performing the update. - * @param {TaskStatus} status - The new status to apply to the task. - * @returns {Promise} The updated task object, or null if the update failed or user is unauthorized. + * @param {TaskStatus} status - The new task status to set. + * @returns {Promise} The updated task object or null if authorization fails. */ static async updateStatusIfAuthorized( taskId: string, @@ -78,20 +93,7 @@ export class TaskService { .where( and( eq(tasksTable.id, taskId), - or( - eq(tasksTable.userId, userId), - exists( - db - .select({ taskId: taskAssigneesTable.taskId }) - .from(taskAssigneesTable) - .where( - and( - eq(taskAssigneesTable.taskId, taskId), - eq(taskAssigneesTable.userId, userId), - ), - ), - ), - ), + this.userHasAccessCondition(userId, tasksTable.id), ), ) .returning(); @@ -100,12 +102,11 @@ export class TaskService { } /** - * Retrieves all active (non-deleted) tasks that a user is authorized to see - * (either as the creator or as an assignee), including their creator emails. + * Retrieves all active (non-deleted) tasks that a user is authorized to see. * * @async * @param {string} userId - The unique identifier of the user. - * @returns {Promise} The list of active tasks with creator information. + * @returns {Promise>} An array of active tasks with their creator emails. */ static async findActiveTasksForUser(userId: string) { return await db @@ -118,31 +119,58 @@ export class TaskService { .where( and( isNull(tasksTable.deletedAt), - or( - eq(tasksTable.userId, userId), - exists( - db - .select() - .from(taskAssigneesTable) - .where( - and( - eq(taskAssigneesTable.taskId, tasksTable.id), - eq(taskAssigneesTable.userId, userId), - ), - ), - ), - ), + this.userHasAccessCondition(userId, tasksTable.id), ), ); } + /** + * Retrieves assignee emails for a batch of task identifiers. + * + * @async + * @param {string[]} taskIds - An array of task unique identifiers. + * @returns {Promise>} An array mapping task IDs to assignee emails. + */ + static async findAssigneesForTasks(taskIds: string[]) { + if (taskIds.length === 0) return []; + + return await db + .select({ + taskId: taskAssigneesTable.taskId, + email: usersTable.email, + }) + .from(taskAssigneesTable) + .innerJoin(usersTable, eq(taskAssigneesTable.userId, usersTable.id)) + .where(inArray(taskAssigneesTable.taskId, taskIds)); + } + + /** + * Retrieves all soft-deleted tasks belonging to the specified user along with creator info. + * + * @async + * @param {string} userId - The unique identifier of the user. + * @returns {Promise>} An array of soft-deleted tasks in the trash. + */ + static async findTrashTasksForUser(userId: string) { + return await db + .select({ + task: tasksTable, + creatorEmail: usersTable.email, + }) + .from(tasksTable) + .innerJoin(usersTable, eq(tasksTable.userId, usersTable.id)) + .where( + and(eq(tasksTable.userId, userId), isNotNull(tasksTable.deletedAt)), + ); + } + /** * Soft-deletes a task by setting its deletion timestamp if the user is the creator. * * @async - * @param {string} taskId - The unique identifier of the task to soft-delete. - * @param {string} userId - The unique identifier of the user performing the operation. - * @returns {Promise} The updated task object with a deletion timestamp, or null if unauthorized. + * @param {string} taskId - The unique identifier of the task. + * @param {string} userId - The unique identifier of the user (must be creator). + * @returns {Promise} The soft-deleted task object or null if unauthorized. */ static async softDeleteIfAuthorized(taskId: string, userId: string) { const [updatedTask] = await db @@ -161,9 +189,9 @@ export class TaskService { * Permanently deletes a task from the database if it is already soft-deleted and the user is the creator. * * @async - * @param {string} taskId - The unique identifier of the task to permanently delete. - * @param {string} userId - The unique identifier of the user performing the operation. - * @returns {Promise} The permanently deleted task object, or null if unauthorized. + * @param {string} taskId - The unique identifier of the task. + * @param {string} userId - The unique identifier of the user (must be creator). + * @returns {Promise} The permanently deleted task object or null if unauthorized. */ static async permanentlyDeleteIfAuthorized(taskId: string, userId: string) { const [deletedTask] = await db @@ -184,9 +212,9 @@ export class TaskService { * Restores a soft-deleted task by clearing its deletion timestamp if the user is the creator. * * @async - * @param {string} taskId - The unique identifier of the task to restore. - * @param {string} userId - The unique identifier of the user performing the operation. - * @returns {Promise} The restored task object, or null if unauthorized. + * @param {string} taskId - The unique identifier of the task. + * @param {string} userId - The unique identifier of the user (must be creator). + * @returns {Promise} The restored task object or null if unauthorized. */ static async restoreIfAuthorized(taskId: string, userId: string) { const [restoredTask] = await db