From 15c3ef0b633a96a3c7cb8ed07cb4d423b4e38515 Mon Sep 17 00:00:00 2001 From: Marharyta Rozghon <72861258+marharita08@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:45:53 +0300 Subject: [PATCH] feat(backend/shared): add action buttons to tasks bb-362 (#451) * feat(backend/shared): add endpoint to update task bb-362 * feat(backend/shared): add prehandler to check access to task bb-362 * feat(backend/shared): add endpoint to get past users tasks bb-362 * fix(backend): update imports/export for api pre handler type bb-362 --- .../src/libs/modules/controller/controller.ts | 1 + .../modules/tasks/libs/constants/constants.ts | 4 +- .../libs/hooks/check-access-to-task.hook.ts | 27 ++++++ .../src/modules/tasks/libs/hooks/hooks.ts | 1 + .../src/modules/tasks/libs/types/types.ts | 6 +- .../validation-schemas/validation-schemas.ts | 1 + .../src/modules/tasks/task.controller.ts | 93 +++++++++++++++++++ .../src/modules/tasks/task.repository.ts | 24 +++++ .../backend/src/modules/tasks/task.service.ts | 6 ++ .../hooks/check-access-to-user-data.hook.ts | 2 +- packages/shared/src/index.ts | 3 + .../src/modules/tasks/libs/enums/enums.ts | 1 + .../enums/task-validation-message.enum.ts | 7 ++ .../tasks/libs/enums/tasks-api-path.enum.ts | 2 + .../types/task-update-parameters-dto.type.ts | 5 + .../types/task-update-request-dto.type.ts | 8 ++ .../src/modules/tasks/libs/types/types.ts | 2 + .../task-update.validation-schema.ts | 27 ++++++ .../validation-schemas/validation-schemas.ts | 1 + packages/shared/src/modules/tasks/tasks.ts | 7 +- 20 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 apps/backend/src/modules/tasks/libs/hooks/check-access-to-task.hook.ts create mode 100644 apps/backend/src/modules/tasks/libs/hooks/hooks.ts create mode 100644 apps/backend/src/modules/tasks/libs/validation-schemas/validation-schemas.ts create mode 100644 packages/shared/src/modules/tasks/libs/enums/task-validation-message.enum.ts create mode 100644 packages/shared/src/modules/tasks/libs/types/task-update-parameters-dto.type.ts create mode 100644 packages/shared/src/modules/tasks/libs/types/task-update-request-dto.type.ts create mode 100644 packages/shared/src/modules/tasks/libs/validation-schemas/task-update.validation-schema.ts create mode 100644 packages/shared/src/modules/tasks/libs/validation-schemas/validation-schemas.ts diff --git a/apps/backend/src/libs/modules/controller/controller.ts b/apps/backend/src/libs/modules/controller/controller.ts index 31fafa52b..df6a80c2b 100644 --- a/apps/backend/src/libs/modules/controller/controller.ts +++ b/apps/backend/src/libs/modules/controller/controller.ts @@ -2,4 +2,5 @@ export { BaseController } from "./base-controller.module.js"; export { type APIHandlerOptions, type APIHandlerResponse, + type APIPreHandler, } from "./libs/types/types.js"; diff --git a/apps/backend/src/modules/tasks/libs/constants/constants.ts b/apps/backend/src/modules/tasks/libs/constants/constants.ts index 085611524..91a8d009d 100644 --- a/apps/backend/src/modules/tasks/libs/constants/constants.ts +++ b/apps/backend/src/modules/tasks/libs/constants/constants.ts @@ -4,4 +4,6 @@ const NO_DAYS_THIS_WEEK = 0; const NO_USER_TASK_DAYS = 0; -export { FULL_WEEK, NO_DAYS_THIS_WEEK, NO_USER_TASK_DAYS }; +const STATUS_FIELD = "status"; + +export { FULL_WEEK, NO_DAYS_THIS_WEEK, NO_USER_TASK_DAYS, STATUS_FIELD }; diff --git a/apps/backend/src/modules/tasks/libs/hooks/check-access-to-task.hook.ts b/apps/backend/src/modules/tasks/libs/hooks/check-access-to-task.hook.ts new file mode 100644 index 000000000..16c40d8a1 --- /dev/null +++ b/apps/backend/src/modules/tasks/libs/hooks/check-access-to-task.hook.ts @@ -0,0 +1,27 @@ +import { ErrorMessage } from "~/libs/enums/enums.js"; +import { type APIPreHandler } from "~/libs/modules/controller/controller.js"; +import { HTTPCode } from "~/libs/modules/http/http.js"; +import { type UserDto } from "~/modules/users/users.js"; + +import { type TaskService } from "../../task.service.js"; +import { TaskError } from "../exceptions/exceptions.js"; +import { type TaskUpdateParametersDto } from "../types/types.js"; + +const checkAccessToTask = + (taskService: TaskService): APIPreHandler => + async (request) => { + const { params, user } = request; + + const taskId = (params as TaskUpdateParametersDto).id; + + const task = await taskService.find(taskId); + + if ((user as UserDto).id !== task?.userId) { + throw new TaskError({ + message: ErrorMessage.FORBIDDEN, + status: HTTPCode.FORBIDDEN, + }); + } + }; + +export { checkAccessToTask }; diff --git a/apps/backend/src/modules/tasks/libs/hooks/hooks.ts b/apps/backend/src/modules/tasks/libs/hooks/hooks.ts new file mode 100644 index 000000000..b6bfa2927 --- /dev/null +++ b/apps/backend/src/modules/tasks/libs/hooks/hooks.ts @@ -0,0 +1 @@ +export { checkAccessToTask } from "./check-access-to-task.hook.js"; diff --git a/apps/backend/src/modules/tasks/libs/types/types.ts b/apps/backend/src/modules/tasks/libs/types/types.ts index 2268b8b70..8d8c267c1 100644 --- a/apps/backend/src/modules/tasks/libs/types/types.ts +++ b/apps/backend/src/modules/tasks/libs/types/types.ts @@ -1,2 +1,6 @@ export { type UsersTaskCreateRequestDto } from "./users-task-create-request-dto.type.js"; -export { type TaskDto } from "shared"; +export { + type TaskDto, + type TaskUpdateParametersDto, + type TaskUpdateRequestDto, +} from "shared"; diff --git a/apps/backend/src/modules/tasks/libs/validation-schemas/validation-schemas.ts b/apps/backend/src/modules/tasks/libs/validation-schemas/validation-schemas.ts new file mode 100644 index 000000000..d8975b223 --- /dev/null +++ b/apps/backend/src/modules/tasks/libs/validation-schemas/validation-schemas.ts @@ -0,0 +1 @@ +export { taskUpdateValidationSchema } from "shared"; diff --git a/apps/backend/src/modules/tasks/task.controller.ts b/apps/backend/src/modules/tasks/task.controller.ts index 9205582f0..e8495764f 100644 --- a/apps/backend/src/modules/tasks/task.controller.ts +++ b/apps/backend/src/modules/tasks/task.controller.ts @@ -9,6 +9,12 @@ import { type Logger } from "~/libs/modules/logger/logger.js"; import { type UserDto } from "~/modules/users/users.js"; import { TasksApiPath } from "./libs/enums/enums.js"; +import { checkAccessToTask } from "./libs/hooks/hooks.js"; +import { + type TaskUpdateParametersDto, + type TaskUpdateRequestDto, +} from "./libs/types/types.js"; +import { taskUpdateValidationSchema } from "./libs/validation-schemas/validation-schemas.js"; import { type TaskService } from "./task.service.js"; /*** @swagger @@ -65,6 +71,33 @@ class TaskController extends BaseController { method: "GET", path: TasksApiPath.CURRENT, }); + + this.addRoute({ + handler: (options) => + this.findPastByUserId( + options as APIHandlerOptions<{ + user: UserDto; + }>, + ), + method: "GET", + path: TasksApiPath.PAST, + }); + + this.addRoute({ + handler: (options) => + this.update( + options as APIHandlerOptions<{ + body: TaskUpdateRequestDto; + params: TaskUpdateParametersDto; + }>, + ), + method: "PATCH", + path: TasksApiPath.$ID, + preHandlers: [checkAccessToTask(taskService)], + validation: { + body: taskUpdateValidationSchema, + }, + }); } /** @@ -97,6 +130,66 @@ class TaskController extends BaseController { status: HTTPCode.OK, }; } + + /** + * @swagger + * /tasks/past: + * get: + * description: Returns an array of past users tasks + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Task" + */ + + private async findPastByUserId( + options: APIHandlerOptions<{ + user: UserDto; + }>, + ): Promise { + const { user } = options; + + return { + payload: await this.taskService.findPastByUserId(user.id), + status: HTTPCode.OK, + }; + } + + /** + * @swagger + * /tasks/current: + * get: + * description: updates status of task by id + * security: + * - bearerAuth: [] + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * $ref: "#/components/schemas/Task" + */ + + private async update( + options: APIHandlerOptions<{ + body: TaskUpdateRequestDto; + params: TaskUpdateParametersDto; + }>, + ): Promise { + return { + payload: await this.taskService.update(options.params.id, options.body), + status: HTTPCode.OK, + }; + } } export { TaskController }; diff --git a/apps/backend/src/modules/tasks/task.repository.ts b/apps/backend/src/modules/tasks/task.repository.ts index 88105b3f8..0057ead3e 100644 --- a/apps/backend/src/modules/tasks/task.repository.ts +++ b/apps/backend/src/modules/tasks/task.repository.ts @@ -1,6 +1,7 @@ import { RelationName } from "~/libs/enums/enums.js"; import { type Repository } from "~/libs/types/types.js"; +import { STATUS_FIELD } from "./libs/constants/constants.js"; import { TaskStatus } from "./libs/enums/enums.js"; import { TaskEntity } from "./task.entity.js"; import { type TaskModel } from "./task.model.js"; @@ -114,6 +115,29 @@ class TaskRepository implements Repository { }); } + public async findPastByUserId(userId: number): Promise { + const tasks = await this.taskModel + .query() + .withGraphFetched(`[${RelationName.CATEGORY}]`) + .whereIn(STATUS_FIELD, [TaskStatus.COMPLETED, TaskStatus.SKIPPED]) + .andWhere({ userId }); + + return tasks.map((task) => { + return TaskEntity.initialize({ + category: task.category.name, + categoryId: task.categoryId, + createdAt: task.createdAt, + description: task.description, + dueDate: task.dueDate, + id: task.id, + label: task.label, + status: task.status, + updatedAt: task.updatedAt, + userId: task.userId, + }); + }); + } + public async update( id: number, payload: Partial, diff --git a/apps/backend/src/modules/tasks/task.service.ts b/apps/backend/src/modules/tasks/task.service.ts index c7992fb34..40461ff94 100644 --- a/apps/backend/src/modules/tasks/task.service.ts +++ b/apps/backend/src/modules/tasks/task.service.ts @@ -98,6 +98,12 @@ class TaskService implements Service { return tasks.map((task) => task.toObject()); } + public async findPastByUserId(userId: number): Promise { + const tasks = await this.taskRepository.findPastByUserId(userId); + + return tasks.map((task) => task.toObject()); + } + public async update( id: number, payload: Partial, diff --git a/apps/backend/src/modules/users/libs/hooks/check-access-to-user-data.hook.ts b/apps/backend/src/modules/users/libs/hooks/check-access-to-user-data.hook.ts index 6eb9bb094..2270d2d1b 100644 --- a/apps/backend/src/modules/users/libs/hooks/check-access-to-user-data.hook.ts +++ b/apps/backend/src/modules/users/libs/hooks/check-access-to-user-data.hook.ts @@ -1,5 +1,5 @@ import { ErrorMessage } from "~/libs/enums/enums.js"; -import { type APIPreHandler } from "~/libs/modules/controller/libs/types/types.js"; +import { type APIPreHandler } from "~/libs/modules/controller/controller.js"; import { HTTPCode } from "~/libs/modules/http/http.js"; import { UserError } from "~/modules/users/libs/exceptions/exceptions.js"; import { diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index a73567f57..1da229493 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -91,6 +91,9 @@ export { type TaskDto, TasksApiPath, TaskStatus, + type TaskUpdateParametersDto, + type TaskUpdateRequestDto, + taskUpdateValidationSchema, } from "./modules/tasks/tasks.js"; export { type EmailDto, diff --git a/packages/shared/src/modules/tasks/libs/enums/enums.ts b/packages/shared/src/modules/tasks/libs/enums/enums.ts index 68db920ae..7c9f76e24 100644 --- a/packages/shared/src/modules/tasks/libs/enums/enums.ts +++ b/packages/shared/src/modules/tasks/libs/enums/enums.ts @@ -1,2 +1,3 @@ export { TaskStatus } from "./task-status.enum.js"; +export { TaskValidationMessage } from "./task-validation-message.enum.js"; export { TasksApiPath } from "./tasks-api-path.enum.js"; diff --git a/packages/shared/src/modules/tasks/libs/enums/task-validation-message.enum.ts b/packages/shared/src/modules/tasks/libs/enums/task-validation-message.enum.ts new file mode 100644 index 000000000..0deac08ca --- /dev/null +++ b/packages/shared/src/modules/tasks/libs/enums/task-validation-message.enum.ts @@ -0,0 +1,7 @@ +const TaskValidationMessage = { + INVALID_TYPE: + "Status must be one of the following: Completed, Current, or Skipped", + REQUIRED: "Status field is required", +} as const; + +export { TaskValidationMessage }; diff --git a/packages/shared/src/modules/tasks/libs/enums/tasks-api-path.enum.ts b/packages/shared/src/modules/tasks/libs/enums/tasks-api-path.enum.ts index 043ba7fd0..de1b96511 100644 --- a/packages/shared/src/modules/tasks/libs/enums/tasks-api-path.enum.ts +++ b/packages/shared/src/modules/tasks/libs/enums/tasks-api-path.enum.ts @@ -1,5 +1,7 @@ const TasksApiPath = { + $ID: "/:id", CURRENT: "/current", + PAST: "/past", } as const; export { TasksApiPath }; diff --git a/packages/shared/src/modules/tasks/libs/types/task-update-parameters-dto.type.ts b/packages/shared/src/modules/tasks/libs/types/task-update-parameters-dto.type.ts new file mode 100644 index 000000000..0337f963f --- /dev/null +++ b/packages/shared/src/modules/tasks/libs/types/task-update-parameters-dto.type.ts @@ -0,0 +1,5 @@ +type TaskUpdateParametersDto = { + id: number; +}; + +export { type TaskUpdateParametersDto }; diff --git a/packages/shared/src/modules/tasks/libs/types/task-update-request-dto.type.ts b/packages/shared/src/modules/tasks/libs/types/task-update-request-dto.type.ts new file mode 100644 index 000000000..513d61c02 --- /dev/null +++ b/packages/shared/src/modules/tasks/libs/types/task-update-request-dto.type.ts @@ -0,0 +1,8 @@ +import { type ValueOf } from "../../../../libs/types/types.js"; +import { type TaskStatus } from "../enums/enums.js"; + +type TaskUpdateRequestDto = { + status: ValueOf; +}; + +export { type TaskUpdateRequestDto }; diff --git a/packages/shared/src/modules/tasks/libs/types/types.ts b/packages/shared/src/modules/tasks/libs/types/types.ts index 369add4ab..04833ce69 100644 --- a/packages/shared/src/modules/tasks/libs/types/types.ts +++ b/packages/shared/src/modules/tasks/libs/types/types.ts @@ -1 +1,3 @@ export { type TaskDto } from "./task-dto.type.js"; +export { type TaskUpdateParametersDto } from "./task-update-parameters-dto.type.js"; +export { type TaskUpdateRequestDto } from "./task-update-request-dto.type.js"; diff --git a/packages/shared/src/modules/tasks/libs/validation-schemas/task-update.validation-schema.ts b/packages/shared/src/modules/tasks/libs/validation-schemas/task-update.validation-schema.ts new file mode 100644 index 000000000..3ddd66b65 --- /dev/null +++ b/packages/shared/src/modules/tasks/libs/validation-schemas/task-update.validation-schema.ts @@ -0,0 +1,27 @@ +import { z } from "zod"; + +import { TaskStatus, TaskValidationMessage } from "../enums/enums.js"; + +type TaskUpdateRequestValidationDto = { + status: z.ZodEnum< + [ + typeof TaskStatus.COMPLETED, + typeof TaskStatus.CURRENT, + typeof TaskStatus.SKIPPED, + ] + >; +}; + +const taskUpdate = z + .object({ + status: z.enum( + [TaskStatus.COMPLETED, TaskStatus.CURRENT, TaskStatus.SKIPPED], + { + invalid_type_error: TaskValidationMessage.INVALID_TYPE, + required_error: TaskValidationMessage.REQUIRED, + }, + ), + }) + .required(); + +export { taskUpdate }; diff --git a/packages/shared/src/modules/tasks/libs/validation-schemas/validation-schemas.ts b/packages/shared/src/modules/tasks/libs/validation-schemas/validation-schemas.ts new file mode 100644 index 000000000..fce417db6 --- /dev/null +++ b/packages/shared/src/modules/tasks/libs/validation-schemas/validation-schemas.ts @@ -0,0 +1 @@ +export { taskUpdate } from "./task-update.validation-schema.js"; diff --git a/packages/shared/src/modules/tasks/tasks.ts b/packages/shared/src/modules/tasks/tasks.ts index 19ebba896..39952dfa0 100644 --- a/packages/shared/src/modules/tasks/tasks.ts +++ b/packages/shared/src/modules/tasks/tasks.ts @@ -1,2 +1,7 @@ export { TasksApiPath, TaskStatus } from "./libs/enums/enums.js"; -export { type TaskDto } from "./libs/types/types.js"; +export { + type TaskDto, + type TaskUpdateParametersDto, + type TaskUpdateRequestDto, +} from "./libs/types/types.js"; +export { taskUpdate as taskUpdateValidationSchema } from "./libs/validation-schemas/validation-schemas.js";