refactor(services): extract shared user access condition helper in TaskService
This commit is contained in:
parent
8d7a4601af
commit
bfb54558e5
3 changed files with 98 additions and 100 deletions
|
|
@ -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<JSX.Element>} 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<string, string[]>();
|
||||
for (const row of assigneesData) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<DbTask | null>} 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<DbTask | null>} 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<any[]>} The list of active tasks with creator information.
|
||||
* @returns {Promise<Array<{ task: DbTask; creatorEmail: string }>>} 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<Array<{ taskId: string; email: string }>>} 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<Array<{ task: DbTask; creatorEmail: string }>>} 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<DbTask | null>} 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<DbTask | null>} 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<DbTask | null>} 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<DbTask | null>} 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<DbTask | null>} 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<DbTask | null>} The restored task object or null if unauthorized.
|
||||
*/
|
||||
static async restoreIfAuthorized(taskId: string, userId: string) {
|
||||
const [restoredTask] = await db
|
||||
|
|
|
|||
Loading…
Reference in a new issue